From a35fe7e90f588a94d341f870c8dd51fa8713c958 Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Mon, 13 Oct 2025 16:25:24 -0300 Subject: [PATCH 01/12] move alternative deploy strategies to new files --- 0-bootstrap/{cb.tf => build_cb.tf} | 0 ...hub.tf.example => build_github.tf.example} | 0 ...lab.tf.example => build_gitlab.tf.example} | 0 ...ns.tf.example => build_jenkins.tf.example} | 0 ...ample => build_terraform_cloud.tf.example} | 2 +- 0-bootstrap/outputs.tf | 149 ------------ 0-bootstrap/outputs_cb.tf | 69 ++++++ 0-bootstrap/outputs_github..tf.example | 23 ++ 0-bootstrap/outputs_gitlab.tf.example | 23 ++ 0-bootstrap/outputs_jenkins.tf.example | 49 ++++ .../outputs_terraform_cloud.tf.example | 56 +++++ 0-bootstrap/variables.tf | 214 ------------------ 0-bootstrap/variables_github..tf.example | 46 ++++ 0-bootstrap/variables_gitlab.tf.example | 52 +++++ 0-bootstrap/variables_jenkins.tf.example | 96 ++++++++ .../variables_terraform_cloud.tf.example | 80 +++++++ 0-bootstrap/versions_cb.tf | 39 ++++ ...ersions.tf => versions_github..tf.example} | 20 +- 0-bootstrap/versions_gitlab.tf.example | 45 ++++ .../versions_terraform_cloud.tf.example | 45 ++++ helpers/foundation-deployer/gcp/gcp.go | 1 - 21 files changed, 628 insertions(+), 381 deletions(-) rename 0-bootstrap/{cb.tf => build_cb.tf} (100%) rename 0-bootstrap/{github.tf.example => build_github.tf.example} (100%) rename 0-bootstrap/{gitlab.tf.example => build_gitlab.tf.example} (100%) rename 0-bootstrap/{jenkins.tf.example => build_jenkins.tf.example} (100%) rename 0-bootstrap/{terraform_cloud.tf.example => build_terraform_cloud.tf.example} (99%) create mode 100644 0-bootstrap/outputs_cb.tf create mode 100644 0-bootstrap/outputs_github..tf.example create mode 100644 0-bootstrap/outputs_gitlab.tf.example create mode 100644 0-bootstrap/outputs_jenkins.tf.example create mode 100644 0-bootstrap/outputs_terraform_cloud.tf.example create mode 100644 0-bootstrap/variables_github..tf.example create mode 100644 0-bootstrap/variables_gitlab.tf.example create mode 100644 0-bootstrap/variables_jenkins.tf.example create mode 100644 0-bootstrap/variables_terraform_cloud.tf.example create mode 100644 0-bootstrap/versions_cb.tf rename 0-bootstrap/{versions.tf => versions_github..tf.example} (80%) create mode 100644 0-bootstrap/versions_gitlab.tf.example create mode 100644 0-bootstrap/versions_terraform_cloud.tf.example diff --git a/0-bootstrap/cb.tf b/0-bootstrap/build_cb.tf similarity index 100% rename from 0-bootstrap/cb.tf rename to 0-bootstrap/build_cb.tf diff --git a/0-bootstrap/github.tf.example b/0-bootstrap/build_github.tf.example similarity index 100% rename from 0-bootstrap/github.tf.example rename to 0-bootstrap/build_github.tf.example diff --git a/0-bootstrap/gitlab.tf.example b/0-bootstrap/build_gitlab.tf.example similarity index 100% rename from 0-bootstrap/gitlab.tf.example rename to 0-bootstrap/build_gitlab.tf.example diff --git a/0-bootstrap/jenkins.tf.example b/0-bootstrap/build_jenkins.tf.example similarity index 100% rename from 0-bootstrap/jenkins.tf.example rename to 0-bootstrap/build_jenkins.tf.example diff --git a/0-bootstrap/terraform_cloud.tf.example b/0-bootstrap/build_terraform_cloud.tf.example similarity index 99% rename from 0-bootstrap/terraform_cloud.tf.example rename to 0-bootstrap/build_terraform_cloud.tf.example index 43aa54a2a..61a95ee89 100644 --- a/0-bootstrap/terraform_cloud.tf.example +++ b/0-bootstrap/build_terraform_cloud.tf.example @@ -304,7 +304,7 @@ module "tfc_agent_gke" { service_account_email = google_service_account.terraform-env-sa["bootstrap"].email service_account_id = google_service_account.terraform-env-sa["bootstrap"].id - //If you are using Terraform Cloud Agents, un-comment this block after the first apply according README instructions + //If you are using Terraform Cloud Agents, un-comment this block after the first apply according to README instructions # providers = { # kubernetes = kubernetes # } diff --git a/0-bootstrap/outputs.tf b/0-bootstrap/outputs.tf index 01e6fe09b..297811c79 100644 --- a/0-bootstrap/outputs.tf +++ b/0-bootstrap/outputs.tf @@ -80,152 +80,3 @@ output "optional_groups" { description = "List of Google Groups created that are optional to the Example Foundation steps." value = var.groups.create_optional_groups == false ? tomap(var.groups.optional_groups) : tomap({ for key, value in module.optional_group : key => value.id }) } - -/* ---------------------------------------- - Specific to cloudbuild_module - ---------------------------------------- */ -# Comment-out the cloudbuild_bootstrap module and its outputs if you want to use -# GitHub Actions, GitLab CI/CD, Terraform Cloud, or Jenkins instead of Cloud Build -output "cloudbuild_project_id" { - description = "Project where Cloud Build configuration and terraform container image will reside." - value = module.tf_source.cloudbuild_project_id -} - -output "gcs_bucket_cloudbuild_artifacts" { - description = "Bucket used to store Cloud Build artifacts in cicd project." - value = { for key, value in module.tf_workspace : key => replace(value.artifacts_bucket, local.bucket_self_link_prefix, "") } -} - -output "gcs_bucket_cloudbuild_logs" { - description = "Bucket used to store Cloud Build logs in cicd project." - value = { for key, value in module.tf_workspace : key => replace(value.logs_bucket, local.bucket_self_link_prefix, "") } -} - -output "cloud_builder_artifact_repo" { - description = "Artifact Registry (AR) Repository created to store TF Cloud Builder images." - value = "projects/${module.tf_source.cloudbuild_project_id}/locations/${var.default_region}/repositories/${module.tf_cloud_builder.artifact_repo}" -} - -output "csr_repos" { - description = "List of Cloud Source Repos created by the module, linked to Cloud Build triggers." - value = { for k, v in module.tf_source.csr_repos : k => { - "id" = v.id, - "name" = v.name, - "project" = v.project, - "url" = v.url, - } - } -} - -output "cloud_build_private_worker_pool_id" { - description = "ID of the Cloud Build private worker pool." - value = module.tf_private_pool.private_worker_pool_id -} - -output "cloud_build_worker_range_id" { - description = "The Cloud Build private worker IP range ID." - value = module.tf_private_pool.worker_range_id -} - -output "cloud_build_worker_peered_ip_range" { - description = "The IP range of the peered service network." - value = module.tf_private_pool.worker_peered_ip_range -} - -output "cloud_build_peered_network_id" { - description = "The ID of the Cloud Build peered network." - value = module.tf_private_pool.peered_network_id -} - -/* ---------------------------------------- - Specific to github_bootstrap - ---------------------------------------- */ -# Un-comment github_bootstrap and its outputs if you want to use GitHub Actions instead of Cloud Build -# output "cicd_project_id" { -# description = "Project where the CI/CD infrastructure for GitHub Action resides." -# value = module.gh_cicd.project_id -# } - -/* ---------------------------------------- - Specific to jenkins_bootstrap module - ---------------------------------------- */ -# # Un-comment the jenkins_bootstrap module and its outputs if you want to use Jenkins instead of Cloud Build -# output "cicd_project_id" { -# description = "Project where the [CI/CD Pipeline](/docs/GLOSSARY.md#foundation-cicd-pipeline) (Jenkins Agents and terraform builder container image) reside." -# value = module.jenkins_bootstrap.cicd_project_id -# } - -# output "jenkins_agent_gce_instance_id" { -# description = "Jenkins Agent GCE Instance id." -# value = module.jenkins_bootstrap.jenkins_agent_gce_instance_id -# } - -# output "jenkins_agent_vpc_id" { -# description = "Jenkins Agent VPC name." -# value = module.jenkins_bootstrap.jenkins_agent_vpc_id -# } - -# output "jenkins_agent_sa_email" { -# description = "Email for privileged custom service account for Jenkins Agent GCE instance." -# value = module.jenkins_bootstrap.jenkins_agent_sa_email -# } - -# output "jenkins_agent_sa_name" { -# description = "Fully qualified name for privileged custom service account for Jenkins Agent GCE instance." -# value = module.jenkins_bootstrap.jenkins_agent_sa_name -# } - -# output "gcs_bucket_jenkins_artifacts" { -# description = "Bucket used to store Jenkins artifacts in Jenkins project." -# value = module.jenkins_bootstrap.gcs_bucket_jenkins_artifacts -# } - -/* ---------------------------------------- - Specific to gitlab_bootstrap - ---------------------------------------- */ -# Un-comment gitlab_bootstrap and its outputs if you want to use GitLab CI/CD instead of Cloud Build -# output "cicd_project_id" { -# description = "Project where the CI/CD infrastructure for GitLab CI/CD resides." -# value = module.gitlab_cicd.project_id -# } - -/* ---------------------------------------- - Specific to tfc_bootstrap - ---------------------------------------- */ -# Un-comment tfc_bootstrap and its outputs if you want to use Terraform Cloud instead of Cloud Build -# output "cicd_project_id" { -# description = "Project where the CI/CD infrastructure for Terraform Cloud resides." -# value = module.tfc_cicd.project_id -# } -# -# output "tfc_org_name" { -# description = "Name of the TFC organization." -# value = var.tfc_org_name -# } - -/* ---------------------------------------- - Specific to tfc_bootstrap with Terraform Cloud Agents - ---------------------------------------- */ -# Un-comment if you want to use Terraform Cloud Agents -# (In other words, un-comment if you set enable_tfc_cloud_agents to true on .tfvars) - -# output "kubernetes_endpoint" { -# description = "The GKE cluster endpoint" -# sensitive = true -# value = module.tfc_agent_gke[0].kubernetes_endpoint -# } - -# output "service_account" { -# description = "The default service account used for TFC agent nodes" -# value = module.tfc_agent_gke[0].service_account -# } - -# output "cluster_name" { -# description = "GKE cluster name" -# value = module.tfc_agent_gke[0].cluster_name -# } - -# output "hub_cluster_membership_id" { -# value = module.tfc_agent_gke[0].hub_cluster_membership_id -# description = "The ID of the cluster membership" -# } diff --git a/0-bootstrap/outputs_cb.tf b/0-bootstrap/outputs_cb.tf new file mode 100644 index 000000000..2fa0be3ea --- /dev/null +++ b/0-bootstrap/outputs_cb.tf @@ -0,0 +1,69 @@ +/** + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* ---------------------------------------- + Specific to cloudbuild_module + ---------------------------------------- */ +output "cloudbuild_project_id" { + description = "Project where Cloud Build configuration and terraform container image will reside." + value = module.tf_source.cloudbuild_project_id +} + +output "gcs_bucket_cloudbuild_artifacts" { + description = "Bucket used to store Cloud Build artifacts in cicd project." + value = { for key, value in module.tf_workspace : key => replace(value.artifacts_bucket, local.bucket_self_link_prefix, "") } +} + +output "gcs_bucket_cloudbuild_logs" { + description = "Bucket used to store Cloud Build logs in cicd project." + value = { for key, value in module.tf_workspace : key => replace(value.logs_bucket, local.bucket_self_link_prefix, "") } +} + +output "cloud_builder_artifact_repo" { + description = "Artifact Registry (AR) Repository created to store TF Cloud Builder images." + value = "projects/${module.tf_source.cloudbuild_project_id}/locations/${var.default_region}/repositories/${module.tf_cloud_builder.artifact_repo}" +} + +output "csr_repos" { + description = "List of Cloud Source Repos created by the module, linked to Cloud Build triggers." + value = { for k, v in module.tf_source.csr_repos : k => { + "id" = v.id, + "name" = v.name, + "project" = v.project, + "url" = v.url, + } + } +} + +output "cloud_build_private_worker_pool_id" { + description = "ID of the Cloud Build private worker pool." + value = module.tf_private_pool.private_worker_pool_id +} + +output "cloud_build_worker_range_id" { + description = "The Cloud Build private worker IP range ID." + value = module.tf_private_pool.worker_range_id +} + +output "cloud_build_worker_peered_ip_range" { + description = "The IP range of the peered service network." + value = module.tf_private_pool.worker_peered_ip_range +} + +output "cloud_build_peered_network_id" { + description = "The ID of the Cloud Build peered network." + value = module.tf_private_pool.peered_network_id +} diff --git a/0-bootstrap/outputs_github..tf.example b/0-bootstrap/outputs_github..tf.example new file mode 100644 index 000000000..817fb482a --- /dev/null +++ b/0-bootstrap/outputs_github..tf.example @@ -0,0 +1,23 @@ +/** + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* ---------------------------------------- + Specific to github_bootstrap + ---------------------------------------- */ +output "cicd_project_id" { + description = "Project where the CI/CD infrastructure for GitHub Action resides." + value = module.gh_cicd.project_id +} diff --git a/0-bootstrap/outputs_gitlab.tf.example b/0-bootstrap/outputs_gitlab.tf.example new file mode 100644 index 000000000..0768500bc --- /dev/null +++ b/0-bootstrap/outputs_gitlab.tf.example @@ -0,0 +1,23 @@ +/** + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* ---------------------------------------- + Specific to gitlab_bootstrap + ---------------------------------------- */ +output "cicd_project_id" { + description = "Project where the CI/CD infrastructure for GitLab CI/CD resides." + value = module.gitlab_cicd.project_id +} diff --git a/0-bootstrap/outputs_jenkins.tf.example b/0-bootstrap/outputs_jenkins.tf.example new file mode 100644 index 000000000..66d00d9f5 --- /dev/null +++ b/0-bootstrap/outputs_jenkins.tf.example @@ -0,0 +1,49 @@ +/** + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/* ---------------------------------------- + Specific to jenkins_bootstrap module + ---------------------------------------- */ +output "cicd_project_id" { + description = "Project where the [CI/CD Pipeline](/docs/GLOSSARY.md#foundation-cicd-pipeline) (Jenkins Agents and terraform builder container image) reside." + value = module.jenkins_bootstrap.cicd_project_id +} + +output "jenkins_agent_gce_instance_id" { + description = "Jenkins Agent GCE Instance id." + value = module.jenkins_bootstrap.jenkins_agent_gce_instance_id +} + +output "jenkins_agent_vpc_id" { + description = "Jenkins Agent VPC name." + value = module.jenkins_bootstrap.jenkins_agent_vpc_id +} + +output "jenkins_agent_sa_email" { + description = "Email for privileged custom service account for Jenkins Agent GCE instance." + value = module.jenkins_bootstrap.jenkins_agent_sa_email +} + +output "jenkins_agent_sa_name" { + description = "Fully qualified name for privileged custom service account for Jenkins Agent GCE instance." + value = module.jenkins_bootstrap.jenkins_agent_sa_name +} + +output "gcs_bucket_jenkins_artifacts" { + description = "Bucket used to store Jenkins artifacts in Jenkins project." + value = module.jenkins_bootstrap.gcs_bucket_jenkins_artifacts +} diff --git a/0-bootstrap/outputs_terraform_cloud.tf.example b/0-bootstrap/outputs_terraform_cloud.tf.example new file mode 100644 index 000000000..a37ebcc5a --- /dev/null +++ b/0-bootstrap/outputs_terraform_cloud.tf.example @@ -0,0 +1,56 @@ +/** + * Copyright 2021 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* ---------------------------------------- + Specific to tfc_bootstrap + ---------------------------------------- */ +# Un-comment tfc_bootstrap and its outputs if you want to use Terraform Cloud instead of Cloud Build +output "cicd_project_id" { + description = "Project where the CI/CD infrastructure for Terraform Cloud resides." + value = module.tfc_cicd.project_id +} + +output "tfc_org_name" { + description = "Name of the TFC organization." + value = var.tfc_org_name +} + +/* ---------------------------------------- + Specific to tfc_bootstrap with Terraform Cloud Agents + ---------------------------------------- */ +# Un-comment if you want to use Terraform Cloud Agents +# (In other words, un-comment if you set enable_tfc_cloud_agents to true on .tfvars) + +# output "kubernetes_endpoint" { +# description = "The GKE cluster endpoint" +# sensitive = true +# value = module.tfc_agent_gke[0].kubernetes_endpoint +# } + +# output "service_account" { +# description = "The default service account used for TFC agent nodes" +# value = module.tfc_agent_gke[0].service_account +# } + +# output "cluster_name" { +# description = "GKE cluster name" +# value = module.tfc_agent_gke[0].cluster_name +# } + +# output "hub_cluster_membership_id" { +# value = module.tfc_agent_gke[0].hub_cluster_membership_id +# description = "The ID of the cluster membership" +# } diff --git a/0-bootstrap/variables.tf b/0-bootstrap/variables.tf index a2ceeded4..a860fece2 100644 --- a/0-bootstrap/variables.tf +++ b/0-bootstrap/variables.tf @@ -170,218 +170,4 @@ variable "initial_group_config" { default = "WITH_INITIAL_OWNER" } -/* ---------------------------------------- - Specific to github_bootstrap - ---------------------------------------- */ - -# Un-comment github_bootstrap and its outputs if you want to use GitHub Actions instead of Cloud Build -# variable "gh_repos" { -# description = < Date: Mon, 13 Oct 2025 18:48:57 -0300 Subject: [PATCH 02/12] add rename script --- 0-bootstrap/scripts/choose_build_type.sh | 58 ++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100755 0-bootstrap/scripts/choose_build_type.sh diff --git a/0-bootstrap/scripts/choose_build_type.sh b/0-bootstrap/scripts/choose_build_type.sh new file mode 100755 index 000000000..27bfad21b --- /dev/null +++ b/0-bootstrap/scripts/choose_build_type.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Define the base path where the build type files are +SCRIPTS_DIR="$( dirname -- "$0"; )" +BASE_PATH="$SCRIPTS_DIR/.." +TARGET_BUILD="$1" + +# Define the allowed build types for validation +build_types=("cb" "github" "gitlab" "jenkins" "terraform_cloud") + +# Validate the build_type input +valid_build_type=false +for valid_type in "${build_types[@]}"; do + if [ "$TARGET_BUILD" = "$valid_type" ]; then + valid_build_type=true + break + fi +done + +if [ "$valid_build_type" = false ]; then + echo "Error: Invalid build type '$TARGET_BUILD'. Must be one of: ${build_types[*]}" + exit 1 +fi + +# Rename *_cb.tf files to *_cb.tf.example if BUILD_TYPE is not "cb" +if [ "$TARGET_BUILD" != "cb" ]; then + find "$BASE_PATH" -name "*_cb.tf" -print0 | while IFS= read -r -d $'\0' file; do + new_name="${file}.example" + echo "Renaming \"$file\" to \"$new_name\"" + mv "$file" "$new_name" + done +fi + +# Rename *_BUILD_TYPE.tf.example to *_BUILD_TYPE.tf if they exist +find "$BASE_PATH" -name "*_$TARGET_BUILD.tf.example" -print0 | while IFS= read -r -d $'\0' file; do + # Extract the base name without the .example extension + base_name="${file%.tf.example}" + new_name="$base_name.tf" + + echo "Renaming \"$file\" to \"$new_name\"" + mv "$file" "$new_name" +done + +echo "File renaming complete." From 9a41b1c8863e81269d69de85b660a767c99ba3d9 Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Wed, 15 Oct 2025 10:25:06 -0300 Subject: [PATCH 03/12] update README instructions for renamng build type files --- 0-bootstrap/README-GitHub.md | 15 ++------------- 0-bootstrap/README-GitLab.md | 17 ++++------------- 0-bootstrap/README-Jenkins.md | 15 ++------------- 0-bootstrap/README-Terraform-Cloud.md | 16 ++-------------- 4 files changed, 10 insertions(+), 53 deletions(-) diff --git a/0-bootstrap/README-GitHub.md b/0-bootstrap/README-GitHub.md index 8b03f0040..1b7736272 100644 --- a/0-bootstrap/README-GitHub.md +++ b/0-bootstrap/README-GitHub.md @@ -148,20 +148,9 @@ You must be [authenticated to GitHub](https://docs.github.com/en/authentication/ cd ./envs/shared ``` -1. In the versions file `./versions.tf` un-comment the `github` required provider -1. In the variables file `./variables.tf` un-comment variables in the section `Specific to github_bootstrap` -1. In the outputs file `./outputs.tf` Comment-out outputs in the section `Specific to cloudbuild_module` -1. In the outputs file `./outputs.tf` un-comment outputs in the section `Specific to github_bootstrap` -1. Rename file `./cb.tf` to `./cb.tf.example` - - ```bash - mv ./cb.tf ./cb.tf.example - ``` - -1. Rename file `./github.tf.example` to `./github.tf` - +1. Run the helper script `choose_build_type.sh` to enable Bootstrap GitHub version ```bash - mv ./github.tf.example ./github.tf + ./scripts/choose_build_type.sh github ``` 1. Rename file `terraform.example.tfvars` to `terraform.tfvars` diff --git a/0-bootstrap/README-GitLab.md b/0-bootstrap/README-GitLab.md index 56adbb6c0..e0d1dce1f 100644 --- a/0-bootstrap/README-GitLab.md +++ b/0-bootstrap/README-GitLab.md @@ -8,6 +8,8 @@ It is a best practice to have two separate projects here (`prj-b-seed` and `prj- On one hand, `prj-b-seed` stores terraform state and has the Service Accounts able to create / modify infrastructure. On the other hand, the authentication infrastructure using [Workload identity federation](https://cloud.google.com/iam/docs/workload-identity-federation) is implemented in `prj-b-cicd-wif-gl`. +## Requirements + To run the instructions described in this document, install the following: - [Google Cloud SDK](https://cloud.google.com/sdk/install) version 393.0.0 or later @@ -216,20 +218,9 @@ Run the `0-bootstrap/scripts/git_create_branches_helper.sh` script to create the cd ./envs/shared ``` -1. In the versions file `./versions.tf` un-comment the `gitlab` required provider -1. In the variables file `./variables.tf` un-comment variables in the section `Specific to gitlab_bootstrap` -1. In the outputs file `./outputs.tf` Comment-out outputs in the section `Specific to cloudbuild_module` -1. In the outputs file `./outputs.tf` un-comment outputs in the section `Specific to gitlab_bootstrap` -1. Rename file `./cb.tf` to `./cb.tf.example` - - ```bash - mv ./cb.tf ./cb.tf.example - ``` - -1. Rename file `./gitlab.tf.example` to `./gitlab.tf` - +1. Run the helper script `choose_build_type.sh` to enable Bootstrap GitHub version ```bash - mv ./gitlab.tf.example ./gitlab.tf + ./scripts/choose_build_type.sh gitlab ``` 1. Rename file `terraform.example.tfvars` to `terraform.tfvars` diff --git a/0-bootstrap/README-Jenkins.md b/0-bootstrap/README-Jenkins.md index 7d880bb68..841fdbe8d 100644 --- a/0-bootstrap/README-Jenkins.md +++ b/0-bootstrap/README-Jenkins.md @@ -139,22 +139,11 @@ You arrived to these instructions because you are using the `jenkins_bootstrap` cd ./envs/shared ``` -1. Activate the Jenkins module and disable the Cloud Build module. This implies manually editing the following files: - 1. Rename file `./cb.tf` to `./cb.tf.example` - - ```bash - mv ./cb.tf ./cb.tf.example - ``` - - 1. Rename file `./jenkins.tf.example` to `./jenkins.tf` - +1. Run the helper script `choose_build_type.sh` to enable Bootstrap GitHub version ```bash - mv ./jenkins.tf.example ./jenkins.tf + ./scripts/choose_build_type.sh jenkins ``` - 1. Un-comment the `jenkins_bootstrap` variables in `./variables.tf` - 1. Un-comment the `jenkins_bootstrap` outputs in `./outputs.tf` - 1. Comment-out the `cloudbuild_bootstrap` outputs in `./outputs.tf` 1. Rename `terraform.example.tfvars` to `terraform.tfvars` and update the file with values from your environment. ```bash diff --git a/0-bootstrap/README-Terraform-Cloud.md b/0-bootstrap/README-Terraform-Cloud.md index 81bc4bf05..4feb1c91f 100644 --- a/0-bootstrap/README-Terraform-Cloud.md +++ b/0-bootstrap/README-Terraform-Cloud.md @@ -136,21 +136,9 @@ You must be authenticated to the VCS provider. See [GitHub authentication](https cd ./envs/shared ``` -1. In the versions file `./versions.tf` un-comment the `tfe` required provider -1. In the variables file `./variables.tf` un-comment variables in the section `Specific to tfc_bootstrap` -1. In the outputs file `./outputs.tf` Comment-out outputs in the section `Specific to cloudbuild_module` -1. In the outputs file `./outputs.tf` un-comment outputs in the section `Specific to tfc_bootstrap` - 1. If you want to use [Terraform Cloud with Agents](https://developer.hashicorp.com/terraform/cloud-docs/agents), in addition to `Specific to tfc_bootstrap`, un-comment outputs in the section `Specific to tfc_bootstrap with Terraform Cloud Agents` and update `enable_tfc_cloud_agents` to `true` variable at `terraform.tfvars` -1. Rename file `./cb.tf` to `./cb.tf.example` - - ```bash - mv ./cb.tf ./cb.tf.example - ``` - -1. Rename file `.terraform_cloud.tf.example` to `./terraform_cloud.tf` - +1. Run the helper script `choose_build_type.sh` to enable Bootstrap GitHub version ```bash - mv ./terraform_cloud.tf.example ./terraform_cloud.tf + ./scripts/choose_build_type.sh terraform_cloud ``` 1. Rename file `terraform.example.tfvars` to `terraform.tfvars` From cb7b2f1931b2fa71da7a3649bb0097df8a94fcb3 Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Wed, 29 Oct 2025 16:50:25 -0300 Subject: [PATCH 04/12] deploy with github --- .....tf.example => outputs_github.tf.example} | 0 0-bootstrap/terraform.example.tfvars | 2 +- ...tf.example => variables_github.tf.example} | 0 ....tf.example => versions_github.tf.example} | 0 Makefile | 2 +- build/github-tf-plan-all.yaml | 66 ++++ build/run_gcp_auth.sh | 7 +- helpers/foundation-deployer/github/github.go | 318 ++++++++++++++++++ .../foundation-deployer/global.tfvars.example | 19 ++ helpers/foundation-deployer/go.mod | 3 + helpers/foundation-deployer/go.sum | 5 + helpers/foundation-deployer/main.go | 63 ++-- helpers/foundation-deployer/msg/msg.go | 14 + helpers/foundation-deployer/stages/apply.go | 314 +++++++++++++++-- helpers/foundation-deployer/stages/data.go | 118 +++++-- helpers/foundation-deployer/stages/destroy.go | 4 +- .../foundation-deployer/stages/executor.go | 65 ++++ .../foundation-deployer/stages/validate.go | 10 + helpers/foundation-deployer/stages/vet.go | 3 +- helpers/foundation-deployer/utils/files.go | 65 ++++ .../foundation-deployer/utils/files_test.go | 64 ++++ helpers/foundation-deployer/utils/git.go | 63 +++- helpers/foundation-deployer/utils/git_test.go | 22 +- 23 files changed, 1127 insertions(+), 100 deletions(-) rename 0-bootstrap/{outputs_github..tf.example => outputs_github.tf.example} (100%) rename 0-bootstrap/{variables_github..tf.example => variables_github.tf.example} (100%) rename 0-bootstrap/{versions_github..tf.example => versions_github.tf.example} (100%) create mode 100644 build/github-tf-plan-all.yaml create mode 100644 helpers/foundation-deployer/github/github.go create mode 100644 helpers/foundation-deployer/stages/executor.go diff --git a/0-bootstrap/outputs_github..tf.example b/0-bootstrap/outputs_github.tf.example similarity index 100% rename from 0-bootstrap/outputs_github..tf.example rename to 0-bootstrap/outputs_github.tf.example diff --git a/0-bootstrap/terraform.example.tfvars b/0-bootstrap/terraform.example.tfvars index 9b85c95fa..c2b6b3871 100644 --- a/0-bootstrap/terraform.example.tfvars +++ b/0-bootstrap/terraform.example.tfvars @@ -68,7 +68,7 @@ default_region_kms = "us" # to prevent saving the `gh_token` in plain text in this file, # export the GitHub fine grained access token in the command line # as an environment variable before running terraform. -# Run the following commnad in your shell: +# Run the following command in your shell: # export TF_VAR_gh_token="YOUR-FINE-GRAINED-ACCESS-TOKEN" diff --git a/0-bootstrap/variables_github..tf.example b/0-bootstrap/variables_github.tf.example similarity index 100% rename from 0-bootstrap/variables_github..tf.example rename to 0-bootstrap/variables_github.tf.example diff --git a/0-bootstrap/versions_github..tf.example b/0-bootstrap/versions_github.tf.example similarity index 100% rename from 0-bootstrap/versions_github..tf.example rename to 0-bootstrap/versions_github.tf.example diff --git a/Makefile b/Makefile index ef98b9a88..787cdfdb3 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ REGISTRY_URL := gcr.io/cloud-foundation-cicd .PHONY: docker_test_lint docker_test_lint: docker run --rm -it \ - -e ENABLE_PARALLEL=1 \ + -e ENABLE_PARALLEL=0 \ -e DISABLE_TFLINT=1 \ -e EXCLUDE_LINT_DIRS \ -v $(CURDIR):/workspace \ diff --git a/build/github-tf-plan-all.yaml b/build/github-tf-plan-all.yaml new file mode 100644 index 000000000..ccf379dde --- /dev/null +++ b/build/github-tf-plan-all.yaml @@ -0,0 +1,66 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "tf-plan-all" +on: + push: + branches: + - "plan" + +env: + PROJECT_ID: ${{ secrets.PROJECT_ID }} + TF_BACKEND: ${{ secrets.TF_BACKEND }} + TF_VAR_gh_token: ${{ secrets.TF_VAR_gh_token }} + TF_IN_AUTOMATION: "true" + +jobs: + run: + runs-on: "ubuntu-latest" + permissions: + contents: "read" + id-token: "write" + issues: "write" + pull-requests: "write" + + steps: + - uses: "actions/checkout@v4" + + - id: "auth" + uses: "google-github-actions/auth@v2" + with: + token_format: "access_token" + workload_identity_provider: ${{ secrets.WIF_PROVIDER_NAME }} + service_account: ${{ secrets.SERVICE_ACCOUNT_EMAIL }} + + - uses: "google-github-actions/setup-gcloud@v2" + with: + install_components: "beta,terraform-tools" + + - uses: "hashicorp/setup-terraform@v3" + with: + terraform_version: "1.5.7" + + - id: setup + shell: bash + run: | + echo "Adding bucket information to backends" + for i in `find . -name 'backend.tf'` + do + sed -i'' -e "s/UPDATE_ME/${TF_BACKEND}/" $i + sed -i'' -e "s/UPDATE_PROJECTS_BACKEND/${TF_BACKEND}/" $i + done + + - id: plan-validate-all + run: | + ${GITHUB_WORKSPACE}/tf-wrapper.sh plan_validate_all "${GITHUB_REF_NAME}" "${GITHUB_WORKSPACE}/policy-library" "${PROJECT_ID}" "FILESYSTEM" "GITHUB" diff --git a/build/run_gcp_auth.sh b/build/run_gcp_auth.sh index d2ed72d51..59616cf3a 100755 --- a/build/run_gcp_auth.sh +++ b/build/run_gcp_auth.sh @@ -29,15 +29,16 @@ SA="$3" # System folder to save the temporary files SAVE_PATH="$4" -# TODO +# Save the OIDC token to a file. +# gcloud requires the OIDC token to be passed as a file. echo "${OIDC_TOKEN}" > "${SAVE_PATH}"/.ci_job_token_file -# TODO +# Exchange the OIDC token for a Google Cloud credential. gcloud iam workload-identity-pools \ create-cred-config "${WIF_PROVIDER}" \ --service-account="${SA}" \ --output-file="${SAVE_PATH}"/.gcp_generated_credentials.json \ --credential-source-file="${SAVE_PATH}"/.ci_job_token_file \ -# TODO +# Authenticate using the generated credential. gcloud auth login --cred-file="${SAVE_PATH}"/.gcp_generated_credentials.json --update-adc diff --git a/helpers/foundation-deployer/github/github.go b/helpers/foundation-deployer/github/github.go new file mode 100644 index 000000000..10720ceea --- /dev/null +++ b/helpers/foundation-deployer/github/github.go @@ -0,0 +1,318 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package github + +import ( + "context" + "fmt" + "io" + "net/http" + "regexp" + "time" + + "github.com/google/go-github/v58/github" + "github.com/mitchellh/go-testing-interface" + "github.com/terraform-google-modules/terraform-example-foundation/test/integration/testutils" + "golang.org/x/oauth2" +) + +const ( + StatusQueued = "queued" + StatusPending = "pending" + StatusWaiting = "waiting" + StatusWorking = "in_progress" + statusCompleted = "completed" + StatusSuccess = "success" + StatusFailure = "failure" + StatusCancelled = "cancelled" + StatusTimeout = "timed_out" + StatusNeutral = "neutral" + StatusSkipped = "skipped" + StatusActionRequired = "action_required" +) + +var ( + retryRegexp = map[*regexp.Regexp]string{} +) + +func init() { + for e, m := range testutils.RetryableTransientErrors { + r, err := regexp.Compile(fmt.Sprintf("(?s)%s", e)) //(?s) enables dot (.) to match newline. + if err != nil { + panic(fmt.Sprintf("failed to compile regex %s: %s", e, err.Error())) + } + retryRegexp[r] = m + } +} + +type GH struct { + TriggerNewBuild func(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (int64, string, string, error) + sleepTime time.Duration +} + +func NewGH() GH { + return GH{ + TriggerNewBuild: triggerNewBuild, + sleepTime: 20, + } +} +func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (int64, string, string, error) { + if token == "" { + return 0, "", "", fmt.Errorf("GITHUB_TOKEN not set") + } + + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(ctx, ts) + client := github.NewClient(tc) + + _, err := client.Actions.RerunWorkflowByID(ctx, owner, repo, runID) + if err != nil { + return 0, "", "", fmt.Errorf("Error re-running workflow: %v", err) + } + opts := &github.ListWorkflowRunsOptions{ + + ListOptions: github.ListOptions{ + PerPage: 1, + Page: 1, + }, + } + + // wait action creation + time.Sleep(30 * time.Second) + + runs, _, err := client.Actions.ListRepositoryWorkflowRuns(ctx, owner, repo, opts) + + if err != nil { + return 0, "", "", fmt.Errorf("Error listing workflow runs: %v", err) + } + + if len(runs.WorkflowRuns) == 0 { + return 0, "", "", fmt.Errorf("No workflow runs found for repo %s/%s ", owner, repo) + } + + var newRunID int64 + if runs.WorkflowRuns[0].ID != nil { + newRunID = *runs.WorkflowRuns[0].ID + } + + var status string + if runs.WorkflowRuns[0].Status != nil { + status = *runs.WorkflowRuns[0].Status + } + + var conclusion string + if runs.WorkflowRuns[0].Conclusion != nil { + conclusion = *runs.WorkflowRuns[0].Conclusion + } + + return newRunID, status, conclusion, nil +} + +func (g GH) GetLastBuildState(t testing.TB, ctx context.Context, owner, repo, token string) (int64, string, string, error) { + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(ctx, ts) + client := github.NewClient(tc) + + opts := &github.ListWorkflowRunsOptions{ + + ListOptions: github.ListOptions{ + PerPage: 1, + }, + } + runs, _, err := client.Actions.ListRepositoryWorkflowRuns(ctx, owner, repo, opts) + + if err != nil { + return 0, "", "", fmt.Errorf("Error listing workflow runs: %v", err) + } + + if len(runs.WorkflowRuns) == 0 { + return 0, "", "", fmt.Errorf("no action workflow found for repo: %s/%s", owner, repo) + } + + var runID int64 + var status string + var conclusion string + + if runs.WorkflowRuns[0].ID != nil { + runID = *runs.WorkflowRuns[0].ID + } + if runs.WorkflowRuns[0].Status != nil { + status = *runs.WorkflowRuns[0].Status + } + if runs.WorkflowRuns[0].Conclusion != nil { + conclusion = *runs.WorkflowRuns[0].Conclusion + } + + return runID, status, conclusion, nil +} + +func (g GH) GetActionState(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (string, string, error) { + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(ctx, ts) + client := github.NewClient(tc) + + run, _, err := client.Actions.GetWorkflowRunByID(ctx, owner, repo, runID) + if err != nil { + return "", "", fmt.Errorf("Error getting workflow run: %v", err) + } + + var status string + if run.Status != nil { + status = *run.Status + } + + var conclusion string + if run.Conclusion != nil { + conclusion = *run.Conclusion + } + return status, conclusion, nil +} + +func (g GH) GetBuildLogs(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (string, error) { + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + tc := oauth2.NewClient(ctx, ts) + client := github.NewClient(tc) + + listJobsOpts := &github.ListWorkflowJobsOptions{ + ListOptions: github.ListOptions{PerPage: 10}, + } + jobs, _, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, listJobsOpts) + if err != nil { + return "", fmt.Errorf("Error listing jobs for run %d: %v", runID, err) + } + + for _, job := range jobs.Jobs { + if job.Status == nil || job.Conclusion == nil { + continue + } + + if *job.Status == statusCompleted && *job.Conclusion == StatusFailure { + jobID := *job.ID + + logURL, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 3) + if err != nil { + return "", fmt.Errorf(" ERROR: Could not get log URL for job %d: %v\n", jobID, err) + } + if resp.StatusCode != http.StatusFound { + return "", fmt.Errorf(" ERROR: Expected a 302 redirect for job logs, but got %s\n", resp.Status) + } + + logContentResp, err := http.Get(logURL.String()) + if err != nil { + return "", fmt.Errorf(" ERROR: Could not download logs from %s: %v\n", logURL, err) + } + defer logContentResp.Body.Close() + + if logContentResp.StatusCode != http.StatusOK { + return "", fmt.Errorf(" ERROR: Expected status 200 OK from log URL, but got %s\n", logContentResp.Status) + } + + bodyBytes, err := io.ReadAll(logContentResp.Body) + if err != nil { + return "", fmt.Errorf("Error reading response body: %v", err) + } + return string(bodyBytes), nil + } + } + return "", nil +} + +func (g GH) GetFinalActionState(t testing.TB, ctx context.Context, owner, repo, token string, runID int64, maxBuildRetry int) (string, string, error) { + var status, conclusion string + var err error + count := 0 + fmt.Printf("waiting for action %d execution.\n", runID) + status, conclusion, err = g.GetActionState(t, ctx, owner, repo, token, runID) + if err != nil { + return "", "", err + } + fmt.Printf("action status is %s\n", status) + for status != statusCompleted { + fmt.Printf("action status is %s\n", status) + if count >= maxBuildRetry { + return "", "", fmt.Errorf("timeout waiting for action '%d' execution", runID) + } + count = count + 1 + time.Sleep(g.sleepTime * time.Second) + status, conclusion, err = g.GetActionState(t, ctx, owner, repo, token, runID) + if err != nil { + return "", "", err + } + } + fmt.Printf("final action state is %s\n", conclusion) + return status, conclusion, nil +} + +// WaitBuildSuccess waits for the current build in a repo to finish. +func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token string, failureMsg string, maxBuildRetry, maxErrorRetries int, timeBetweenErrorRetries time.Duration) error { + var status, conclusion string + var runID int64 + var err error + + ctx := context.Background() + // wait action creation + time.Sleep(30 * time.Second) + + runID, status, conclusion, err = g.GetLastBuildState(t, ctx, owner, repo, token) + if err != nil { + return err + } + for i := 0; i < maxErrorRetries; i++ { + if status != statusCompleted { + status, conclusion, err = g.GetFinalActionState(t, ctx, owner, repo, token, runID, maxBuildRetry) + if err != nil { + return err + } + } + + if conclusion != StatusSuccess { + logs, err := g.GetBuildLogs(t, ctx, owner, repo, token, runID) + if err != nil { + return err + } + if !g.IsRetryableError(t, logs) { + return fmt.Errorf("%s\nSee:\nhttps://github.com/%s/%s/actions/runs/%d\nfor details", failureMsg, owner, repo, runID) + } + fmt.Println("build failed with retryable error. a new build will be triggered.") + } else { + return nil // Build succeeded + } + + // Trigger a new build + runID, status, conclusion, err = g.TriggerNewBuild(t, ctx, owner, repo, token, runID) + if err != nil { + return fmt.Errorf("failed to trigger new action (attempt %d/%d): %w", i+1, maxErrorRetries, err) + } + fmt.Printf("triggered new action with ID: %d (attempt %d/%d)\n", runID, i+1, maxErrorRetries) + if i < maxErrorRetries-1 { + time.Sleep(timeBetweenErrorRetries) // Wait before retrying + } + } + return fmt.Errorf("%s action failed after %d retries", failureMsg, maxErrorRetries) +} + +// IsRetryableError checks the logs of a failed Cloud Build build +// and verify if the error is a transient one and can be retried +func (g GH) IsRetryableError(t testing.TB, logs string) bool { + found := false + for pattern, msg := range retryRegexp { + if pattern.MatchString(logs) { + found = true + fmt.Printf("error '%s' is worth of a retry\n", msg) + break + } + } + return found +} diff --git a/helpers/foundation-deployer/global.tfvars.example b/helpers/foundation-deployer/global.tfvars.example index 4ed9169f0..404133c73 100644 --- a/helpers/foundation-deployer/global.tfvars.example +++ b/helpers/foundation-deployer/global.tfvars.example @@ -30,6 +30,9 @@ validator_project_id = "EXISTING_PROJECT_ID" project_deletion_policy = "PREVENT" # Use "DELETE" to allow deletion of the projects folder_deletion_protection = true +// build type to use. One of "cb", "github", "gitlab" +build_type = "cb" + // 0-bootstrap inputs // https://github.com/terraform-google-modules/terraform-example-foundation/blob/master/0-bootstrap/README.md#inputs @@ -82,6 +85,22 @@ groups = { } } +// Uncomment for GitHub or GitLab deploy +// See: +// GitHub: https://github.com/terraform-google-modules/terraform-example-foundation/blob/main/0-bootstrap/README-GitHub.md#requirements +// GitLab: https://github.com/terraform-google-modules/terraform-example-foundation/blob/main/0-bootstrap/README-GitLab.md#requirements +// git_repos = { +// token = "ACCESS_TOKEN" +// owner = "REPO_OWNER" +// bootstrap = "BOOTSTRAP_REPO_URL" +// organization = "ORGANIZATION_REPO_URL" +// environments = "ENVIRONMENTS_REPO_URL" +// networks = "NETWORKS_REPO_URL" +// projects = "PROJECTS_REPO_URL" +// cicd_runner = "CICD_RUNNER_REPO_URL" // Required if GitLab +// } + + // 1-org inputs // https://github.com/terraform-google-modules/terraform-example-foundation/blob/master/1-org/envs/shared/README.md#inputs diff --git a/helpers/foundation-deployer/go.mod b/helpers/foundation-deployer/go.mod index 1eacdb529..4ab447447 100644 --- a/helpers/foundation-deployer/go.mod +++ b/helpers/foundation-deployer/go.mod @@ -15,6 +15,8 @@ require ( google.golang.org/api v0.206.0 ) +require github.com/google/go-querystring v1.1.0 // indirect + require ( cloud.google.com/go/auth v0.10.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.5 // indirect @@ -28,6 +30,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-github/v58 v58.0.0 github.com/google/s2a-go v0.1.8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect diff --git a/helpers/foundation-deployer/go.sum b/helpers/foundation-deployer/go.sum index 29fa8a09f..78b22e394 100644 --- a/helpers/foundation-deployer/go.sum +++ b/helpers/foundation-deployer/go.sum @@ -53,9 +53,14 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v58 v58.0.0 h1:Una7GGERlF/37XfkPwpzYJe0Vp4dt2k1kCjlxwjIvzw= +github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/helpers/foundation-deployer/main.go b/helpers/foundation-deployer/main.go index ba84985f9..060671fb8 100644 --- a/helpers/foundation-deployer/main.go +++ b/helpers/foundation-deployer/main.go @@ -105,6 +105,7 @@ func main() { FoundationPath: globalTFVars.FoundationCodePath, CheckoutPath: globalTFVars.CodeCheckoutPath, PolicyPath: filepath.Join(globalTFVars.FoundationCodePath, "policy-library"), + BuildType: globalTFVars.BuildType, EnableHubAndSpoke: globalTFVars.EnableHubAndSpoke, DisablePrompt: cfg.disablePrompt, Logger: utils.GetLogger(cfg.quiet), @@ -171,21 +172,22 @@ func main() { // destroy stages if cfg.destroy { // Note: destroy is only terraform destroy, local directories are not deleted. - // 5-app-infra - msg.PrintStageMsg("Destroying 5-app-infra stage") - err = s.RunDestroyStep("bu1-example-app", func() error { - io := stages.GetInfraPipelineOutputs(t, conf.CheckoutPath, "bu1-example-app") - return stages.DestroyExampleAppStage(t, s, io, conf) - }) - if err != nil { - fmt.Printf("# Example app step destroy failed. Error: %s\n", err.Error()) - os.Exit(3) + if conf.BuildType == stages.BuildTypeCBCSR { + // 5-app-infra + msg.PrintStageMsg("Destroying 5-app-infra stage") + err = s.RunDestroyStep("bu1-example-app", func() error { + io := stages.GetInfraPipelineOutputs(t, conf.CheckoutPath, "bu1-example-app") + return stages.DestroyExampleAppStage(t, s, io, conf) + }) + if err != nil { + fmt.Printf("# Example app step destroy failed. Error: %s\n", err.Error()) + os.Exit(3) + } } - // 4-projects msg.PrintStageMsg("Destroying 4-projects stage") err = s.RunDestroyStep("gcp-projects", func() error { - bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath) + bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath, conf.BuildType) return stages.DestroyProjectsStage(t, s, bo, conf) }) if err != nil { @@ -196,7 +198,7 @@ func main() { // 3-networks msg.PrintStageMsg("Destroying 3-networks stage") err = s.RunDestroyStep("gcp-networks", func() error { - bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath) + bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath, conf.BuildType) return stages.DestroyNetworksStage(t, s, bo, conf) }) if err != nil { @@ -207,7 +209,7 @@ func main() { // 2-environments msg.PrintStageMsg("Destroying 2-environments stage") err = s.RunDestroyStep("gcp-environments", func() error { - bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath) + bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath, conf.BuildType) return stages.DestroyEnvStage(t, s, bo, conf) }) if err != nil { @@ -218,7 +220,7 @@ func main() { // 1-org msg.PrintStageMsg("Destroying 1-org stage") err = s.RunDestroyStep("gcp-org", func() error { - bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath) + bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath, conf.BuildType) return stages.DestroyOrgStage(t, s, bo, conf) }) if err != nil { @@ -251,14 +253,18 @@ func main() { msg.PrintStageMsg("Deploying 0-bootstrap stage") skipInnerBuildMsg := s.IsStepComplete("gcp-bootstrap") err = s.RunStep("gcp-bootstrap", func() error { - return stages.DeployBootstrapStage(t, s, globalTFVars, conf) + if globalTFVars.BuildType == stages.BuildTypeCBCSR { + return stages.DeployBootstrapStage(t, s, globalTFVars, conf) + } else { + return stages.DeployGenericBootstrapStage(t, s, globalTFVars, conf) + } }) if err != nil { fmt.Printf("# Bootstrap step failed. Error: %s\n", err.Error()) os.Exit(3) } - bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath) + bo := stages.GetBootstrapStepOutputs(t, conf.FoundationPath, conf.BuildType) if skipInnerBuildMsg { msg.PrintBuildMsg(bo.CICDProject, bo.DefaultRegion, conf.DisablePrompt) @@ -310,18 +316,21 @@ func main() { os.Exit(3) } - // 5-app-infra - msg.PrintStageMsg("Deploying 5-app-infra stage") - io := stages.GetInfraPipelineOutputs(t, conf.CheckoutPath, "bu1-example-app") - io.RemoteStateBucket = bo.RemoteStateBucketProjects + if conf.BuildType == stages.BuildTypeCBCSR { + // 5-app-infra + msg.PrintStageMsg("Deploying 5-app-infra stage") + io := stages.GetInfraPipelineOutputs(t, conf.CheckoutPath, "bu1-example-app") + io.RemoteStateBucket = bo.RemoteStateBucketProjects - msg.PrintBuildMsg(io.InfraPipeProj, io.DefaultRegion, conf.DisablePrompt) + msg.PrintBuildMsg(io.InfraPipeProj, io.DefaultRegion, conf.DisablePrompt) - err = s.RunStep("bu1-example-app", func() error { - return stages.DeployExampleAppStage(t, s, globalTFVars, io, conf) - }) - if err != nil { - fmt.Printf("# Example app step failed. Error: %s\n", err.Error()) - os.Exit(3) + err = s.RunStep("bu1-example-app", func() error { + return stages.DeployExampleAppStage(t, s, globalTFVars, io, conf) + }) + if err != nil { + fmt.Printf("# Example app step failed. Error: %s\n", err.Error()) + os.Exit(3) + } } + } diff --git a/helpers/foundation-deployer/msg/msg.go b/helpers/foundation-deployer/msg/msg.go index c2972be2a..cc794a16a 100644 --- a/helpers/foundation-deployer/msg/msg.go +++ b/helpers/foundation-deployer/msg/msg.go @@ -25,6 +25,7 @@ const ( size = 70 readmeURL = "https://github.com/terraform-google-modules/terraform-example-foundation/blob/master/%s/README.md" cloudBuildURL = "https://console.cloud.google.com/cloud-build/builds;region=%s?project=%s" + githubActionURL = "https://github.com/%s/%s/actions" buildErrorURL = "https://console.cloud.google.com/cloud-build/builds;region=%s/%s?project=%s" quotaURL = "https://support.google.com/code/contact/billing_quota_increase" troubleQuotaURL = "https://github.com/terraform-google-modules/terraform-example-foundation/blob/master/docs/TROUBLESHOOTING.md#billing-quota-exceeded" @@ -56,6 +57,9 @@ func pad(msg string, size int) string { func CloudBuildURL(project, region string) string { return fmt.Sprintf(cloudBuildURL, region, project) } +func GitHubActionURL(owner, repo string) string { + return fmt.Sprintf(githubActionURL, owner, repo) +} func BuildErrorURL(project, region, build string) string { return fmt.Sprintf(buildErrorURL, region, build, project) @@ -79,6 +83,16 @@ func PrintBuildMsg(project, region string, disablePrompt bool) { } } +func PrintGHActionMsg(owner, repo string, disablePrompt bool) { + fmt.Println("") + fmt.Println("# Follow workflow execution and check results in the GitHub:") + fmt.Printf("# %s\n", GitHubActionURL(owner, repo)) + if !disablePrompt { + PressEnter("# Press Enter to continue at any time") + fmt.Println("") + } +} + func PrintQuotaMsg(sa string, disablePrompt bool) { fmt.Println("") fmt.Println("# Request a billing quota increase for the service account of stage 4-projects") diff --git a/helpers/foundation-deployer/stages/apply.go b/helpers/foundation-deployer/stages/apply.go index 0667d2c9a..7b464fa8c 100644 --- a/helpers/foundation-deployer/stages/apply.go +++ b/helpers/foundation-deployer/stages/apply.go @@ -30,6 +30,185 @@ import ( "github.com/terraform-google-modules/terraform-example-foundation/test/integration/testutils" ) +func DeployGenericBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c CommonConf) error { + bootstrapTfvars := BootstrapTfvars{ + OrgID: tfvars.OrgID, + DefaultRegion: tfvars.DefaultRegion, + DefaultRegion2: tfvars.DefaultRegion2, + DefaultRegionGCS: tfvars.DefaultRegionGCS, + DefaultRegionKMS: tfvars.DefaultRegionKMS, + BillingAccount: tfvars.BillingAccount, + ParentFolder: tfvars.ParentFolder, + ProjectPrefix: tfvars.ProjectPrefix, + FolderPrefix: tfvars.FolderPrefix, + BucketForceDestroy: tfvars.BucketForceDestroy, + BucketTfstateKmsForceDestroy: tfvars.BucketTfstateKmsForceDestroy, + WorkflowDeletionProtection: tfvars.WorkflowDeletionProtection, + Groups: tfvars.Groups, + InitialGroupConfig: tfvars.InitialGroupConfig, + FolderDeletionProtection: tfvars.FolderDeletionProtection, + ProjectDeletionPolicy: tfvars.ProjectDeletionPolicy, + } + + var buildExecutor Executor + var repoURL string + var err error + var envVars map[string]string + + if tfvars.BuildType == BuildTypeGiHub { + bootstrapTfvars.GitHubRepos = &GitHubRepos{ + Owner: tfvars.GitRepos.Owner, + Bootstrap: tfvars.GitRepos.Bootstrap, + Organization: tfvars.GitRepos.Organization, + Environments: tfvars.GitRepos.Environments, + Networks: tfvars.GitRepos.Networks, + Projects: tfvars.GitRepos.Projects, + } + envVars = map[string]string{ + "TF_VAR_gh_token": tfvars.GitRepos.Token, + } + repoURL = utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Bootstrap) + buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Bootstrap, tfvars.GitRepos.Token) + } + + if tfvars.BuildType == BuildTypeGitLab { + bootstrapTfvars.GitLabRepos = &GitLabRepos{ + Owner: tfvars.GitRepos.Owner, + Bootstrap: tfvars.GitRepos.Bootstrap, + Organization: tfvars.GitRepos.Organization, + Environments: tfvars.GitRepos.Environments, + Networks: tfvars.GitRepos.Networks, + Projects: tfvars.GitRepos.Projects, + CICDRunner: *tfvars.GitRepos.CICDRunner, + } + envVars = map[string]string{ + "TF_VAR_gitlab_token": tfvars.GitRepos.Token, + } + } + + utils.RenameBuildFiles(filepath.Join(c.FoundationPath, BootstrapStep), tfvars.BuildType) + + err = utils.WriteTfvars(filepath.Join(c.FoundationPath, BootstrapStep, "terraform.tfvars"), bootstrapTfvars) + if err != nil { + return err + } + + EmptyProject := "" + NoRegion := "" + + terraformDir := filepath.Join(c.FoundationPath, BootstrapStep) + options := &terraform.Options{ + TerraformDir: terraformDir, + Logger: c.Logger, + NoColor: true, + RetryableTerraformErrors: testutils.RetryableTransientErrors, + MaxRetries: MaxErrorRetries, + TimeBetweenRetries: TimeBetweenErrorRetries, + EnvVars: envVars, + } + + // terraform deploy + err = applyLocal(t, options, "", c.PolicyPath, c.ValidatorProject) + if err != nil { + return err + } + + // read bootstrap outputs + backendBucket := terraform.Output(t, options, "gcs_bucket_tfstate") + backendBucketProjects := terraform.Output(t, options, "projects_gcs_bucket_tfstate") + + // replace backend and terraform init migrate + err = s.RunStep("gcp-bootstrap.migrate-state", func() error { + options.MigrateState = true + err = utils.CopyFile(filepath.Join(options.TerraformDir, "backend.tf.example"), filepath.Join(options.TerraformDir, "backend.tf")) + if err != nil { + return err + } + err = utils.ReplaceStringInFile(filepath.Join(options.TerraformDir, "backend.tf"), "UPDATE_ME", backendBucket) + if err != nil { + return err + } + _, err := terraform.InitE(t, options) + return err + }) + if err != nil { + return err + } + + // replace all backend files + err = s.RunStep("gcp-bootstrap.replace-backend-files", func() error { + files, err := utils.FindFiles(c.FoundationPath, "backend.tf") + if err != nil { + return err + } + for _, file := range files { + err = utils.ReplaceStringInFile(file, "UPDATE_ME", backendBucket) + if err != nil { + return err + } + err = utils.ReplaceStringInFile(file, "UPDATE_PROJECTS_BACKEND", backendBucketProjects) + if err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + + gcpBootstrapPath := filepath.Join(c.CheckoutPath, BootstrapRepo) + bootstrapConf := utils.GitClone(t, tfvars.BuildType, "", repoURL, gcpBootstrapPath, EmptyProject, c.Logger) + stageConf := StageConf{ + Stage: BootstrapRepo, + CICDProject: EmptyProject, + DefaultRegion: NoRegion, + Step: BootstrapStep, + Repo: BootstrapRepo, + CustomTargetDirPath: "envs/shared", + GitConf: bootstrapConf, + Envs: []string{"shared"}, + BuildType: tfvars.BuildType, + BuildExecutor: buildExecutor, + } + + // if groups creation is enable the helper will just push the code + // because Cloud Build build will fail until bootstrap + // service account is granted Group Admin role in the + // Google Workspace by a Super Admin. + // https://github.com/terraform-google-modules/terraform-google-group/blob/main/README.md#google-workspace-formerly-known-as-g-suite-roles + if tfvars.HasGroupsCreation() { + err = saveBootstrapCodeOnly(t, stageConf, s, c) + } else { + err = deployStage(t, stageConf, s, c) + } + + if err != nil { + return err + } + + // Init gcp-bootstrap terraform + err = s.RunStep("gcp-bootstrap.init-tf", func() error { + options := &terraform.Options{ + TerraformDir: filepath.Join(gcpBootstrapPath, "envs", "shared"), + Logger: c.Logger, + NoColor: true, + RetryableTerraformErrors: testutils.RetryableTransientErrors, + MaxRetries: MaxErrorRetries, + TimeBetweenRetries: TimeBetweenErrorRetries, + EnvVars: envVars, + } + _, err := terraform.InitE(t, options) + return err + }) + if err != nil { + return err + } + fmt.Println("end of bootstrap deploy") + + return nil +} + func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c CommonConf) error { bootstrapTfvars := BootstrapTfvars{ OrgID: tfvars.OrgID, @@ -77,6 +256,7 @@ func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co MaxRetries: MaxErrorRetries, TimeBetweenRetries: TimeBetweenErrorRetries, } + // terraform deploy err = applyLocal(t, options, "", c.PolicyPath, c.ValidatorProject) if err != nil { @@ -132,14 +312,15 @@ func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co msg.PrintBuildMsg(cbProjectID, defaultRegion, c.DisablePrompt) // Check if image build was successful. - err = gcp.NewGCP().WaitBuildSuccess(t, cbProjectID, defaultRegion, "tf-cloudbuilder", "", "Terraform Image builder Build Failed for tf-cloudbuilder repository.", MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) + buildTFBuilderExecutor := NewGCPExecutor(cbProjectID, defaultRegion, "tf-cloudbuilder") + err = buildTFBuilderExecutor.WaitBuildSuccess(t, "", "Terraform Image builder Build Failed for tf-cloudbuilder repository.") if err != nil { return err } //prepare policies repo gcpPoliciesPath := filepath.Join(c.CheckoutPath, PoliciesRepo) - policiesConf := utils.CloneCSR(t, PoliciesRepo, gcpPoliciesPath, cbProjectID, c.Logger) + policiesConf := utils.GitClone(t, "CSR", PoliciesRepo, "", gcpPoliciesPath, cbProjectID, c.Logger) policiesBranch := "main" err = s.RunStep("gcp-bootstrap.gcp-policies", func() error { @@ -151,7 +332,8 @@ func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co //prepare bootstrap repo gcpBootstrapPath := filepath.Join(c.CheckoutPath, BootstrapRepo) - bootstrapConf := utils.CloneCSR(t, BootstrapRepo, gcpBootstrapPath, cbProjectID, c.Logger) + bootstrapConf := utils.GitClone(t, "CSR", BootstrapRepo, "", gcpBootstrapPath, cbProjectID, c.Logger) + buildExecutor := NewGCPExecutor(cbProjectID, defaultRegion, BootstrapRepo) stageConf := StageConf{ Stage: BootstrapRepo, CICDProject: cbProjectID, @@ -161,6 +343,8 @@ func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co CustomTargetDirPath: "envs/shared", GitConf: bootstrapConf, Envs: []string{"shared"}, + BuildType: BuildTypeCBCSR, + BuildExecutor: buildExecutor, } // if groups creation is enable the helper will just push the code @@ -242,7 +426,18 @@ func DeployOrgStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo return err } - conf := utils.CloneCSR(t, OrgRepo, filepath.Join(c.CheckoutPath, OrgRepo), outputs.CICDProject, c.Logger) + var conf utils.GitRepo + var buildExecutor Executor + + if c.BuildType == BuildTypeGiHub { + buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization, tfvars.GitRepos.Token) + repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization) + conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, OrgRepo), "", c.Logger) + } else { + buildExecutor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, OrgRepo) + conf = utils.GitClone(t, "CSR", OrgRepo, "", filepath.Join(c.CheckoutPath, OrgRepo), outputs.CICDProject, c.Logger) + } + stageConf := StageConf{ Stage: OrgRepo, CICDProject: outputs.CICDProject, @@ -251,6 +446,8 @@ func DeployOrgStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo Repo: OrgRepo, GitConf: conf, Envs: []string{"shared"}, + BuildType: c.BuildType, + BuildExecutor: buildExecutor, } return deployStage(t, stageConf, s, c) @@ -268,7 +465,18 @@ func DeployEnvStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo return err } - conf := utils.CloneCSR(t, EnvironmentsRepo, filepath.Join(c.CheckoutPath, EnvironmentsRepo), outputs.CICDProject, c.Logger) + var conf utils.GitRepo + var buildExecutor Executor + + if c.BuildType == BuildTypeGiHub { + buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments, tfvars.GitRepos.Token) + repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments) + conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, EnvironmentsRepo), "", c.Logger) + } else { + buildExecutor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, EnvironmentsRepo) + conf = utils.GitClone(t, "CSR", EnvironmentsRepo, "", filepath.Join(c.CheckoutPath, EnvironmentsRepo), outputs.CICDProject, c.Logger) + } + stageConf := StageConf{ Stage: EnvironmentsRepo, CICDProject: outputs.CICDProject, @@ -277,6 +485,8 @@ func DeployEnvStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo Repo: EnvironmentsRepo, GitConf: conf, Envs: []string{"production", "nonproduction", "development"}, + BuildType: c.BuildType, + BuildExecutor: buildExecutor, } return deployStage(t, stageConf, s, c) @@ -332,7 +542,18 @@ func DeployNetworksStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu return err } - conf := utils.CloneCSR(t, NetworksRepo, filepath.Join(c.CheckoutPath, NetworksRepo), outputs.CICDProject, c.Logger) + var conf utils.GitRepo + var buildExecutor Executor + + if c.BuildType == BuildTypeGiHub { + buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks, tfvars.GitRepos.Token) + repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks) + conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, NetworksRepo), "", c.Logger) + } else { + buildExecutor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, NetworksRepo) + conf = utils.GitClone(t, "CSR", NetworksRepo, "", filepath.Join(c.CheckoutPath, NetworksRepo), outputs.CICDProject, c.Logger) + } + stageConf := StageConf{ Stage: NetworksRepo, StageSA: outputs.NetworkSA, @@ -345,6 +566,8 @@ func DeployNetworksStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu LocalSteps: localStep, GroupingUnits: []string{"envs"}, Envs: []string{"production", "nonproduction", "development"}, + BuildType: c.BuildType, + BuildExecutor: buildExecutor, } return deployStage(t, stageConf, s, c) } @@ -384,7 +607,19 @@ func DeployProjectsStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu } } - conf := utils.CloneCSR(t, ProjectsRepo, filepath.Join(c.CheckoutPath, ProjectsRepo), outputs.CICDProject, c.Logger) + // conf := utils.GitClone(t, "CSR", ProjectsRepo, "", filepath.Join(c.CheckoutPath, ProjectsRepo), outputs.CICDProject, c.Logger) + var conf utils.GitRepo + var buildExecutor Executor + + if c.BuildType == BuildTypeGiHub { + buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects, tfvars.GitRepos.Token) + repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects) + conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, ProjectsRepo), "", c.Logger) + } else { + buildExecutor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, ProjectsRepo) + conf = utils.GitClone(t, "CSR", ProjectsRepo, "", filepath.Join(c.CheckoutPath, ProjectsRepo), outputs.CICDProject, c.Logger) + } + stageConf := StageConf{ Stage: ProjectsRepo, StageSA: outputs.ProjectsSA, @@ -397,6 +632,8 @@ func DeployProjectsStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu LocalSteps: []string{"shared"}, GroupingUnits: []string{"business_unit_1"}, Envs: []string{"production", "nonproduction", "development"}, + BuildType: c.BuildType, + BuildExecutor: buildExecutor, } return deployStage(t, stageConf, s, c) @@ -425,9 +662,8 @@ func DeployExampleAppStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, out return err } } - //prepare policies repo gcpPoliciesPath := filepath.Join(c.CheckoutPath, "gcp-policies-app-infra") - policiesConf := utils.CloneCSR(t, PoliciesRepo, gcpPoliciesPath, outputs.InfraPipeProj, c.Logger) + policiesConf := utils.GitClone(t, "CSR", PoliciesRepo, "", gcpPoliciesPath, outputs.InfraPipeProj, c.Logger) policiesBranch := "main" err = s.RunStep("bu1-example-app.gcp-policies-app-infra", func() error { return preparePoliciesRepo(policiesConf, policiesBranch, c.FoundationPath, gcpPoliciesPath) @@ -436,7 +672,7 @@ func DeployExampleAppStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, out return err } - conf := utils.CloneCSR(t, AppInfraRepo, filepath.Join(c.CheckoutPath, AppInfraRepo), outputs.InfraPipeProj, c.Logger) + conf := utils.GitClone(t, "CSR", AppInfraRepo, "", filepath.Join(c.CheckoutPath, AppInfraRepo), outputs.InfraPipeProj, c.Logger) stageConf := StageConf{ Stage: AppInfraRepo, CICDProject: outputs.InfraPipeProj, @@ -458,7 +694,7 @@ func deployStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error } err = s.RunStep(fmt.Sprintf("%s.copy-code", sc.Stage), func() error { - return copyStepCode(t, sc.GitConf, c.FoundationPath, c.CheckoutPath, sc.Repo, sc.Step, sc.CustomTargetDirPath) + return copyStepCode(t, sc.GitConf, c.FoundationPath, c.CheckoutPath, sc.Repo, sc.Step, sc.CustomTargetDirPath, sc.BuildType) }) if err != nil { return err @@ -490,7 +726,7 @@ func deployStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error } err = s.RunStep(fmt.Sprintf("%s.plan", sc.Stage), func() error { - return planStage(t, sc.GitConf, sc.CICDProject, sc.DefaultRegion, sc.Repo) + return planStage(t, sc.GitConf, sc.CICDProject, sc.DefaultRegion, sc.Repo, sc.BuildExecutor) }) if err != nil { return err @@ -502,7 +738,7 @@ func deployStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error if env == "shared" { aEnv = "production" } - return applyEnv(t, sc.GitConf, sc.CICDProject, sc.DefaultRegion, sc.Repo, aEnv) + return applyEnv(t, sc.GitConf, sc.CICDProject, sc.DefaultRegion, sc.Repo, aEnv, sc.BuildExecutor) }) if err != nil { return err @@ -529,7 +765,7 @@ func preparePoliciesRepo(policiesConf utils.GitRepo, policiesBranch, foundationP return policiesConf.PushBranch(policiesBranch, "origin") } -func copyStepCode(t testing.TB, conf utils.GitRepo, foundationPath, checkoutPath, repo, step, customPath string) error { +func copyStepCode(t testing.TB, conf utils.GitRepo, foundationPath, checkoutPath, repo, step, customPath, buildType string) error { gcpPath := filepath.Join(checkoutPath, repo) targetDir := gcpPath if customPath != "" { @@ -539,18 +775,44 @@ func copyStepCode(t testing.TB, conf utils.GitRepo, foundationPath, checkoutPath if err != nil { return err } - err = utils.CopyFile(filepath.Join(foundationPath, "build/cloudbuild-tf-apply.yaml"), filepath.Join(gcpPath, "cloudbuild-tf-apply.yaml")) - if err != nil { - return err + + if buildType == BuildTypeCBCSR { + err = utils.CopyFile(filepath.Join(foundationPath, "build/cloudbuild-tf-apply.yaml"), filepath.Join(gcpPath, "cloudbuild-tf-apply.yaml")) + if err != nil { + return err + } + err = utils.CopyFile(filepath.Join(foundationPath, "build/cloudbuild-tf-plan.yaml"), filepath.Join(gcpPath, "cloudbuild-tf-plan.yaml")) + if err != nil { + return err + } } - err = utils.CopyFile(filepath.Join(foundationPath, "build/cloudbuild-tf-plan.yaml"), filepath.Join(gcpPath, "cloudbuild-tf-plan.yaml")) - if err != nil { - return err + if buildType == BuildTypeGiHub { + err := utils.CopyDirectory(filepath.Join(foundationPath, "policy-library"), filepath.Join(gcpPath, "policy-library")) + if err != nil { + return err + } + err = os.MkdirAll(filepath.Join(gcpPath, ".github/workflows/"), 0755) + if err != nil { + return err + } + err = utils.CopyFile(filepath.Join(foundationPath, "build/github-tf-apply.yaml"), filepath.Join(gcpPath, ".github/workflows/github-tf-apply.yaml")) + if err != nil { + return err + } + err = utils.CopyFile(filepath.Join(foundationPath, "build/github-tf-plan-all.yaml"), filepath.Join(gcpPath, ".github/workflows/github-tf-plan-all.yaml")) + if err != nil { + return err + } + err = utils.CopyFile(filepath.Join(foundationPath, "build/github-tf-pull-request.yaml"), filepath.Join(gcpPath, ".github/workflows/github-tf-pull-request.yaml")) + if err != nil { + return err + } } + return utils.CopyFile(filepath.Join(foundationPath, "build/tf-wrapper.sh"), filepath.Join(gcpPath, "tf-wrapper.sh")) } -func planStage(t testing.TB, conf utils.GitRepo, project, region, repo string) error { +func planStage(t testing.TB, conf utils.GitRepo, project, region, repo string, buildExecutor Executor) error { err := conf.CommitFiles(fmt.Sprintf("Initialize %s repo", repo)) if err != nil { @@ -566,7 +828,7 @@ func planStage(t testing.TB, conf utils.GitRepo, project, region, repo string) e return err } - return gcp.NewGCP().WaitBuildSuccess(t, project, region, repo, commitSha, fmt.Sprintf("Terraform %s plan build Failed.", repo), MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) + return buildExecutor.WaitBuildSuccess(t, commitSha, fmt.Sprintf("Terraform %s plan build Failed.", repo)) } func saveBootstrapCodeOnly(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error { @@ -577,7 +839,7 @@ func saveBootstrapCodeOnly(t testing.TB, sc StageConf, s steps.Steps, c CommonCo } err = s.RunStep(fmt.Sprintf("%s.copy-code", sc.Stage), func() error { - return copyStepCode(t, sc.GitConf, c.FoundationPath, c.CheckoutPath, sc.Repo, sc.Step, sc.CustomTargetDirPath) + return copyStepCode(t, sc.GitConf, c.FoundationPath, c.CheckoutPath, sc.Repo, sc.Step, sc.CustomTargetDirPath, sc.BuildType) }) if err != nil { return err @@ -616,7 +878,7 @@ func saveBootstrapCodeOnly(t testing.TB, sc StageConf, s steps.Steps, c CommonCo return nil } -func applyEnv(t testing.TB, conf utils.GitRepo, project, region, repo, environment string) error { +func applyEnv(t testing.TB, conf utils.GitRepo, project, region, repo, environment string, buildExecutor Executor) error { err := conf.CheckoutBranch(environment) if err != nil { return err @@ -630,7 +892,7 @@ func applyEnv(t testing.TB, conf utils.GitRepo, project, region, repo, environme return err } - return gcp.NewGCP().WaitBuildSuccess(t, project, region, repo, commitSha, fmt.Sprintf("Terraform %s apply %s build Failed.", repo, environment), MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) + return buildExecutor.WaitBuildSuccess(t, commitSha, fmt.Sprintf("Terraform %s apply %s build Failed.", repo, environment)) } func applyLocal(t testing.TB, options *terraform.Options, serviceAccount, policyPath, validatorProjectID string) error { @@ -654,7 +916,7 @@ func applyLocal(t testing.TB, options *terraform.Options, serviceAccount, policy // Runs gcloud terraform vet if validatorProjectID != "" { - err = TerraformVet(t, options.TerraformDir, policyPath, validatorProjectID) + err = TerraformVet(t, options.TerraformDir, policyPath, validatorProjectID, options.EnvVars) if err != nil { return err } diff --git a/helpers/foundation-deployer/stages/data.go b/helpers/foundation-deployer/stages/data.go index 046d0cf0f..171fe2885 100644 --- a/helpers/foundation-deployer/stages/data.go +++ b/helpers/foundation-deployer/stages/data.go @@ -29,23 +29,28 @@ import ( ) const ( - PoliciesRepo = "gcp-policies" - BootstrapRepo = "gcp-bootstrap" - OrgRepo = "gcp-org" - EnvironmentsRepo = "gcp-environments" - NetworksRepo = "gcp-networks" - ProjectsRepo = "gcp-projects" - AppInfraRepo = "bu1-example-app" - BootstrapStep = "0-bootstrap" - OrgStep = "1-org" - EnvironmentsStep = "2-environments" - HubAndSpokeStep = "3-networks-hub-and-spoke" - SvpcStep = "3-networks-svpc" - ProjectsStep = "4-projects" - AppInfraStep = "5-app-infra" - MaxErrorRetries = 2 - TimeBetweenErrorRetries = 2 * time.Minute - MaxBuildRetries = 40 + PoliciesRepo = "gcp-policies" + BootstrapRepo = "gcp-bootstrap" + OrgRepo = "gcp-org" + EnvironmentsRepo = "gcp-environments" + NetworksRepo = "gcp-networks" + ProjectsRepo = "gcp-projects" + AppInfraRepo = "bu1-example-app" + BootstrapStep = "0-bootstrap" + OrgStep = "1-org" + EnvironmentsStep = "2-environments" + HubAndSpokeStep = "3-networks-hub-and-spoke" + SvpcStep = "3-networks-svpc" + ProjectsStep = "4-projects" + AppInfraStep = "5-app-infra" + MaxErrorRetries = 2 + TimeBetweenErrorRetries = 2 * time.Minute + MaxBuildRetries = 40 + BuildTypeCBCSR = "cb" + BuildTypeGiHub = "github" + BuildTypeGitLab = "gitlab" + CloudBuildProjectIdOutput = "cloudbuild_project_id" + CICDProjectIdOutput = "cicd_project_id" ) type CommonConf struct { @@ -53,6 +58,7 @@ type CommonConf struct { CheckoutPath string PolicyPath string ValidatorProject string + BuildType string EnableHubAndSpoke bool DisablePrompt bool Logger *logger.Logger @@ -71,6 +77,8 @@ type StageConf struct { GroupingUnits []string Envs []string LocalSteps []string + BuildType string + BuildExecutor Executor } type BootstrapOutputs struct { @@ -133,6 +141,37 @@ type GcpGroups struct { KmsAdmin *string `cty:"kms_admin"` } +type GitRepos struct { + Token string `cty:"token"` + Owner string `cty:"owner"` + Bootstrap string `cty:"bootstrap"` + Organization string `cty:"organization"` + Environments string `cty:"environments"` + Networks string `cty:"networks"` + Projects string `cty:"projects"` + // AppInfra string `cty:"app_infra"` //TODO + CICDRunner *string `cty:"cicd_runner"` +} + +type GitHubRepos struct { + Owner string `cty:"owner"` + Bootstrap string `cty:"bootstrap"` + Organization string `cty:"organization"` + Environments string `cty:"environments"` + Networks string `cty:"networks"` + Projects string `cty:"projects"` +} + +type GitLabRepos struct { + Owner string `cty:"owner"` + Bootstrap string `cty:"bootstrap"` + Organization string `cty:"organization"` + Environments string `cty:"environments"` + Networks string `cty:"networks"` + Projects string `cty:"projects"` + CICDRunner string `cty:"cicd_runner"` +} + // GlobalTFVars contains all the configuration for the deploy type GlobalTFVars struct { OrgID string `hcl:"org_id"` @@ -170,6 +209,8 @@ type GlobalTFVars struct { InitialGroupConfig *string `hcl:"initial_group_config"` FolderDeletionProtection *bool `hcl:"folder_deletion_protection"` ProjectDeletionPolicy string `hcl:"project_deletion_policy"` + BuildType string `hcl:"build_type"` + GitRepos *GitRepos `hcl:"git_repos"` } // HasValidatorProj checks if a Validator Project was provided @@ -203,22 +244,24 @@ func (g GlobalTFVars) CheckString(s string) { } type BootstrapTfvars struct { - OrgID string `hcl:"org_id"` - BillingAccount string `hcl:"billing_account"` - DefaultRegion string `hcl:"default_region"` - DefaultRegion2 string `hcl:"default_region_2"` - DefaultRegionGCS string `hcl:"default_region_gcs"` - DefaultRegionKMS string `hcl:"default_region_kms"` - ParentFolder *string `hcl:"parent_folder"` - ProjectPrefix *string `hcl:"project_prefix"` - FolderPrefix *string `hcl:"folder_prefix"` - BucketForceDestroy *bool `hcl:"bucket_force_destroy"` - BucketTfstateKmsForceDestroy *bool `hcl:"bucket_tfstate_kms_force_destroy"` - WorkflowDeletionProtection *bool `hcl:"workflow_deletion_protection"` - Groups Groups `hcl:"groups"` - InitialGroupConfig *string `hcl:"initial_group_config"` - FolderDeletionProtection *bool `hcl:"folder_deletion_protection"` - ProjectDeletionPolicy string `hcl:"project_deletion_policy"` + OrgID string `hcl:"org_id"` + BillingAccount string `hcl:"billing_account"` + DefaultRegion string `hcl:"default_region"` + DefaultRegion2 string `hcl:"default_region_2"` + DefaultRegionGCS string `hcl:"default_region_gcs"` + DefaultRegionKMS string `hcl:"default_region_kms"` + ParentFolder *string `hcl:"parent_folder"` + ProjectPrefix *string `hcl:"project_prefix"` + FolderPrefix *string `hcl:"folder_prefix"` + BucketForceDestroy *bool `hcl:"bucket_force_destroy"` + BucketTfstateKmsForceDestroy *bool `hcl:"bucket_tfstate_kms_force_destroy"` + WorkflowDeletionProtection *bool `hcl:"workflow_deletion_protection"` + Groups Groups `hcl:"groups"` + InitialGroupConfig *string `hcl:"initial_group_config"` + FolderDeletionProtection *bool `hcl:"folder_deletion_protection"` + ProjectDeletionPolicy string `hcl:"project_deletion_policy"` + GitHubRepos *GitHubRepos `hcl:"gh_repos"` + GitLabRepos *GitLabRepos `hcl:"gl_repos"` } type OrgTfvars struct { @@ -285,14 +328,19 @@ type AppInfraCommonTfvars struct { ImageDigest string `hcl:"confidential_image_digest"` } -func GetBootstrapStepOutputs(t testing.TB, foundationPath string) BootstrapOutputs { +func GetBootstrapStepOutputs(t testing.TB, foundationPath string, buildType string) BootstrapOutputs { options := &terraform.Options{ TerraformDir: filepath.Join(foundationPath, "0-bootstrap"), Logger: logger.Discard, NoColor: true, } + cicdProjectIDOutput := CICDProjectIdOutput + if buildType != BuildTypeCBCSR { + cicdProjectIDOutput = CICDProjectIdOutput + } + return BootstrapOutputs{ - CICDProject: terraform.Output(t, options, "cloudbuild_project_id"), + CICDProject: terraform.Output(t, options, cicdProjectIDOutput), RemoteStateBucket: terraform.Output(t, options, "gcs_bucket_tfstate"), RemoteStateBucketProjects: terraform.Output(t, options, "projects_gcs_bucket_tfstate"), DefaultRegion: terraform.OutputMap(t, options, "common_config")["default_region"], diff --git a/helpers/foundation-deployer/stages/destroy.go b/helpers/foundation-deployer/stages/destroy.go index 6d70e74b3..542610862 100644 --- a/helpers/foundation-deployer/stages/destroy.go +++ b/helpers/foundation-deployer/stages/destroy.go @@ -165,7 +165,7 @@ func destroyStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error MaxRetries: MaxErrorRetries, TimeBetweenRetries: TimeBetweenErrorRetries, } - conf := utils.CloneCSR(t, sc.Repo, gcpPath, sc.CICDProject, c.Logger) + conf := utils.GitClone(t, "CSR", sc.Repo, "", gcpPath, sc.CICDProject, c.Logger) branch := e if branch == "shared" { branch = "production" @@ -199,7 +199,7 @@ func destroyStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error MaxRetries: MaxErrorRetries, TimeBetweenRetries: TimeBetweenErrorRetries, } - conf := utils.CloneCSR(t, ProjectsRepo, gcpPath, sc.CICDProject, c.Logger) + conf := utils.GitClone(t, "CSR", ProjectsRepo, "", gcpPath, sc.CICDProject, c.Logger) err := conf.CheckoutBranch("production") if err != nil { return err diff --git a/helpers/foundation-deployer/stages/executor.go b/helpers/foundation-deployer/stages/executor.go new file mode 100644 index 000000000..a33f48e9b --- /dev/null +++ b/helpers/foundation-deployer/stages/executor.go @@ -0,0 +1,65 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stages + +import ( + "github.com/mitchellh/go-testing-interface" + "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/gcp" + "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/github" +) + +type Executor interface { + WaitBuildSuccess(t testing.TB, commitSha, failureMsg string) error +} + +type GCPExecutor struct { + executor gcp.GCP + project string + region string + repo string +} + +func (e *GCPExecutor) WaitBuildSuccess(t testing.TB, commitSha, failureMsg string) error { + return e.executor.WaitBuildSuccess(t, e.project, e.region, e.repo, commitSha, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) +} + +func NewGCPExecutor(project, region, repo string) *GCPExecutor { + return &GCPExecutor{ + project: project, + region: region, + repo: repo, + } +} + +type GitHubExecutor struct { + executor github.GH + url string + owner string + repo string + token string +} + +func NewGitHubExecutor(owner, repo, token string) *GitHubExecutor { + return &GitHubExecutor{ + executor: github.NewGH(), + owner: owner, + repo: repo, + token: token, + } +} + +func (e *GitHubExecutor) WaitBuildSuccess(t testing.TB, commitSha, failureMsg string) error { + return e.executor.WaitBuildSuccess(t, e.owner, e.repo, e.token, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) +} diff --git a/helpers/foundation-deployer/stages/validate.go b/helpers/foundation-deployer/stages/validate.go index 9ab20dbc1..41be508dc 100644 --- a/helpers/foundation-deployer/stages/validate.go +++ b/helpers/foundation-deployer/stages/validate.go @@ -111,6 +111,16 @@ func ValidateBasicFields(t testing.TB, g GlobalTFVars) { } } +func ValidateGitRepos(t testing.TB, g GlobalTFVars) { + // + + if g.BuildType == BuildTypeGiHub { + // check if owner was provided + // check if all repo was provided + // test if token is valid + } +} + // ValidateDestroyFlags checks if the flags to allow the destruction of the infrastructure are enabled func ValidateDestroyFlags(t testing.TB, g GlobalTFVars) { trueFlags := []string{} diff --git a/helpers/foundation-deployer/stages/vet.go b/helpers/foundation-deployer/stages/vet.go index c435fd4d9..3e3aea6b0 100644 --- a/helpers/foundation-deployer/stages/vet.go +++ b/helpers/foundation-deployer/stages/vet.go @@ -32,7 +32,7 @@ import ( ) // TerraformVet runs gcloud terraform vet on the plan of the provided terraform directory -func TerraformVet(t testing.TB, terraformDir, policyPath, project string) error { +func TerraformVet(t testing.TB, terraformDir, policyPath, project string, envVars map[string]string) error { fmt.Println("") fmt.Println("# Running gcloud terraform vet") @@ -46,6 +46,7 @@ func TerraformVet(t testing.TB, terraformDir, policyPath, project string) error RetryableTerraformErrors: testutils.RetryableTransientErrors, MaxRetries: MaxErrorRetries, TimeBetweenRetries: TimeBetweenErrorRetries, + EnvVars: envVars, } _, err := terraform.PlanE(t, options) if err != nil { diff --git a/helpers/foundation-deployer/utils/files.go b/helpers/foundation-deployer/utils/files.go index b7eab50b9..2a1efc1e9 100644 --- a/helpers/foundation-deployer/utils/files.go +++ b/helpers/foundation-deployer/utils/files.go @@ -16,16 +16,25 @@ package utils import ( "bytes" + "fmt" "io/fs" "os" "path/filepath" + "strings" ) const ( TerraformTempDir = ".terraform" TerraformLockFile = ".terraform.lock.hcl" + DisableFileSuffix = ".example" + DefaultBuild = "cb" + GitHubBuild = "github" + GitLabBuild = "gitlab" ) +// Allowed build types. +var AllowedBuildTypes = []string{"cb", "github", "gitlab", "jenkins", "terraform_cloud"} + // CopyFile copies a single file from the src path to the dest path func CopyFile(src string, dest string) error { s, err := os.Stat(src) @@ -103,3 +112,59 @@ func FileExists(filename string) (bool, error) { } return false, err } + +// RenameBuildFiles renames files based on targetBuild string. +func RenameBuildFiles(basePath, targetBuild string) error { + // Validate build type. + validBuildType := false + for _, validType := range AllowedBuildTypes { + if targetBuild == validType { + validBuildType = true + break + } + } + + if !validBuildType { + return fmt.Errorf("invalid build type '%s'. Must be one of: %s", targetBuild, strings.Join(AllowedBuildTypes, ", ")) + } + + // Rename default build files to "*.example" + if targetBuild != DefaultBuild { + patternDefault := filepath.Join(basePath, fmt.Sprintf("*_%s.tf", DefaultBuild)) + filesDefault, err := filepath.Glob(patternDefault) + if err != nil { + return fmt.Errorf("error finding files (%s rename): %w", DefaultBuild, err) + } + + for _, file := range filesDefault { + newName := file + DisableFileSuffix + fmt.Printf("Renaming \"%s\" to \"%s\"\n", file, newName) + + err := os.Rename(file, newName) + if err != nil { + return fmt.Errorf("error renaming file \"%s\": %w", file, err) + } + } + + // Rename *_BUILD_TYPE.tf.example to *_BUILD_TYPE.tf if they exist. + pattern := filepath.Join(basePath, fmt.Sprintf("*_%s.tf%s", targetBuild, DisableFileSuffix)) + files, err := filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("error finding files (target rename): %w", err) + } + + for _, file := range files { + baseName := strings.TrimSuffix(file, fmt.Sprintf(".tf%s", DisableFileSuffix)) + newName := baseName + ".tf" + + fmt.Printf("Renaming \"%s\" to \"%s\"\n", file, newName) + + err := os.Rename(file, newName) + if err != nil { + return fmt.Errorf("error renaming file \"%s\": %w", file, err) + } + } + } + + return nil +} diff --git a/helpers/foundation-deployer/utils/files_test.go b/helpers/foundation-deployer/utils/files_test.go index 5245bdaa3..32f9785cb 100644 --- a/helpers/foundation-deployer/utils/files_test.go +++ b/helpers/foundation-deployer/utils/files_test.go @@ -15,6 +15,7 @@ package utils import ( + "fmt" "os" "path/filepath" "testing" @@ -22,6 +23,8 @@ import ( "github.com/stretchr/testify/assert" ) +var testBuildTypes = []string{"cb", "github", "gitlab", "jenkins", "terraform_cloud"} + func writeTempFile(dir, name, content string) (string, error) { f := filepath.Join(dir, name) err := os.WriteFile(f, []byte(content), 0644) @@ -95,3 +98,64 @@ func TestFindFiles(t *testing.T) { assert.Len(t, files, 1, "must have found only one file") assert.Equal(t, filepath.Join(base, "one", "two", "three", "four", filename), files[0]) } + +func createRenameTestFiles(t *testing.T, tempDir string, targetBuild string, defaultBuild string) { + t.Helper() // Mark this function as a helper function. + // Create some test files to rename. + var filesToCreate []string + + // Files for the defaultBuild rename logic + filesToCreate = append(filesToCreate, filepath.Join(tempDir, fmt.Sprintf("test_%s.tf", defaultBuild))) + + // Files for the target build rename logic + filesToCreate = append(filesToCreate, filepath.Join(tempDir, fmt.Sprintf("test_%s.tf.example", targetBuild))) + filesToCreate = append(filesToCreate, filepath.Join(tempDir, "test_other.tf")) // Create a non-matching file + + for _, filename := range filesToCreate { + err := os.WriteFile(filename, []byte("test content"), 0644) // Write simple content + if err != nil { + t.Fatalf("Failed to create test file %s: %v", filename, err) + } + } +} + +func checkRenamedFiles(t *testing.T, tempDir string, targetBuild string, defaultBuild string) { + t.Helper() + // Check the files renamed successfully. + if targetBuild != DefaultBuild { + expectedNewName := filepath.Join(tempDir, fmt.Sprintf("test_%s.tf", targetBuild)) + assert.FileExists(t, expectedNewName, "File %s should exist after rename", expectedNewName) + + originalName := filepath.Join(tempDir, fmt.Sprintf("test_%s.tf.example", targetBuild)) + assert.NoFileExists(t, originalName, "File %s should not exist after rename", originalName) + + expectedDefaultBuildName := filepath.Join(tempDir, fmt.Sprintf("test_%s.tf.example", defaultBuild)) + assert.FileExists(t, expectedDefaultBuildName, "File %s should exist after default build rename", expectedDefaultBuildName) + + originalDefaultBuildName := filepath.Join(tempDir, fmt.Sprintf("test_%s.tf", defaultBuild)) + assert.NoFileExists(t, originalDefaultBuildName, "File %s should not exist after rename", originalDefaultBuildName) + + otherName := filepath.Join(tempDir, "test_other.tf") + assert.FileExists(t, otherName, "File %s should exist after default build rename", otherName) + } +} + +func TestRenameFiles(t *testing.T) { + for _, targetBuild := range AllowedBuildTypes { + t.Run(targetBuild, func(t *testing.T) { + tempDir := t.TempDir() + createRenameTestFiles(t, tempDir, targetBuild, DefaultBuild) + + err := RenameBuildFiles(tempDir, targetBuild) + assert.NoError(t, err, "RenameBuildFiles failed for targetBuild %s: %v", targetBuild, err) + checkRenamedFiles(t, tempDir, targetBuild, DefaultBuild) + }) + } + // Add a test case for an invalid build type + t.Run("invalid", func(t *testing.T) { + tempDir := t.TempDir() + err := RenameBuildFiles(tempDir, "invalid_build_type") + assert.Error(t, err, "RenameBuildFiles should have failed for invalid build type") + assert.Contains(t, err.Error(), "invalid build type", "RenameFiles should have returned an error about the build type") + }) +} diff --git a/helpers/foundation-deployer/utils/git.go b/helpers/foundation-deployer/utils/git.go index 7c2b55b3d..cfb208a1b 100644 --- a/helpers/foundation-deployer/utils/git.go +++ b/helpers/foundation-deployer/utils/git.go @@ -16,7 +16,10 @@ package utils import ( "fmt" + "net/url" "os" + "os/exec" + "path" "strings" "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" @@ -29,17 +32,50 @@ type GitRepo struct { conf *git.CmdCfg } +// GitClone clones git repositories, supporting CSR, Github and Gitlab type of source control +func GitClone(t testing.TB, repositoryType, repositoryName, repositoryURL, path, project string, logger *logger.Logger) GitRepo { + conf := GitRepo{} + if repositoryType != "CSR" { + conf = cloneGit(t, repositoryURL, path, logger) + } else { + conf = cloneCSR(t, repositoryName, path, project, logger) + } + return conf +} + // CloneCSR clones a Google Cloud Source repository and returns a CmdConfig pointing to the repository. -func CloneCSR(t testing.TB, name, path, project string, logger *logger.Logger) GitRepo { +func cloneCSR(t testing.TB, repositoryName, path, project string, logger *logger.Logger) GitRepo { _, err := os.Stat(path) if os.IsNotExist(err) { - gcloud.Runf(t, "source repos clone %s %s --project %s", name, path, project) + gcloud.Runf(t, "source repos clone %s %s --project %s", repositoryName, path, project) } return GitRepo{ conf: git.NewCmdConfig(t, git.WithDir(path), git.WithLogger(logger)), } } +// cloneGit clones a Github or Gitlab repository and returns a CmdConfig pointing to the repository. +func cloneGit(t testing.TB, repositoryUrl, path string, logger *logger.Logger) GitRepo { + _, err := os.Stat(path) + + if os.IsNotExist(err) { + cmd := exec.Command("git", "clone", repositoryUrl, path) + fmt.Printf("Executing command %s", cmd) + // Run the command and capture its standard output + output, err := cmd.Output() + if err != nil { + t.Fatalf("Error executing git command: %v", err) + } + // Convert the output to a string and trim whitespace + branchName := strings.TrimSpace(string(output)) + fmt.Printf("Current Git branch: %s\n", branchName) + } + + return GitRepo{ + conf: git.NewCmdConfig(t, git.WithDir(path), git.WithLogger(logger)), + } +} + // GetCurrentBranch gets the current branch in the repository. func (g GitRepo) GetCurrentBranch() (string, error) { return g.conf.RunCmdE("branch", "-q", "--show-current") @@ -107,3 +143,26 @@ func (g GitRepo) AddRemote(name, url string) error { func (g GitRepo) GetCommitSha() (string, error) { return g.conf.RunCmdE("rev-parse", "HEAD") } + +func BuildGitHubURL(owner, repoName string) string { + return fmt.Sprintf("https://github.com/%s/%s.git", owner, repoName) +} + +// ExtractRepoNameFromGitHubURL parses a GitHub URL and returns the repository name. +func ExtractRepoNameFromGitHubURL(githubURL string) (string, error) { + parsedURL, err := url.Parse(githubURL) + if err != nil { + return "", fmt.Errorf("failed to parse URL: %w", err) + } + + repoNameWithSuffix := path.Base(parsedURL.Path) + if repoNameWithSuffix == "" || repoNameWithSuffix == "." || repoNameWithSuffix == "/" { + return "", fmt.Errorf("could not find a repository name in the URL path: %s", githubURL) + } + + repoName := strings.TrimSuffix(repoNameWithSuffix, ".git") + if repoName == "" { + return "", fmt.Errorf("extracted repository name is empty after processing: %s", githubURL) + } + return repoName, nil +} diff --git a/helpers/foundation-deployer/utils/git_test.go b/helpers/foundation-deployer/utils/git_test.go index c9441ab86..c4f04d280 100644 --- a/helpers/foundation-deployer/utils/git_test.go +++ b/helpers/foundation-deployer/utils/git_test.go @@ -39,6 +39,24 @@ func createLocalRepo(t *testing.T, repo string) string { return local } +func TestGenericClone(t *testing.T) { + repo := "terraform-google-secret" + fileSystemRepo := createLocalRepo(t, repo) + + dir := t.TempDir() + path := filepath.Join(dir, repo) + err := os.MkdirAll(dir, 0755) + assert.NoError(t, err) + + local := GitClone(t, "Generic", "terraform-google-secret", "file://"+fileSystemRepo, path, "", logger.Discard) + err = local.CheckoutBranch("unit-test") + assert.NoError(t, err) + + localBranch, err := local.GetCurrentBranch() + assert.NoError(t, err) + assert.Equal(t, localBranch, "unit-test", "current branch should be 'unit-test'") +} + func TestGit(t *testing.T) { remote := "origin" repo := createLocalRepo(t, "my-git-repo") @@ -46,7 +64,7 @@ func TestGit(t *testing.T) { err := CopyDirectory(repo, originPath) assert.NoError(t, err) - local := CloneCSR(t, "my-git-repo", repo, "", logger.Discard) + local := GitClone(t, "CSR", "my-git-repo", "", repo, "", logger.Discard) err = local.AddRemote(remote, originPath) assert.NoError(t, err) @@ -70,7 +88,7 @@ func TestGit(t *testing.T) { assert.NoError(t, err) assert.True(t, hasUpstream, "branch 'unit-test' should have a remote") - origin := CloneCSR(t, "my-git-repo", originPath, "", logger.Discard) + origin := GitClone(t, "CSR", "my-git-repo", "", originPath, "", logger.Discard) files, err := FindFiles(originPath, "go.mod") assert.NoError(t, err) assert.Len(t, files, 0, "'go.mod' file should not exist on main branch") From af0a7e31f5e7b473b614b291cdd36c027bc9c087 Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Fri, 31 Oct 2025 17:13:39 -0300 Subject: [PATCH 05/12] set prevention destroy falg --- 1-org/envs/shared/folders.tf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/1-org/envs/shared/folders.tf b/1-org/envs/shared/folders.tf index 90c69454a..484589573 100644 --- a/1-org/envs/shared/folders.tf +++ b/1-org/envs/shared/folders.tf @@ -19,13 +19,13 @@ *****************************************/ resource "google_folder" "common" { - display_name = "${local.folder_prefix}-common" - parent = local.parent - # deletion_protection = var.folder_deletion_protection // uncommnet after updating "GoogleCloudPlatform/cloud-functions/google" to provider v6 + display_name = "${local.folder_prefix}-common" + parent = local.parent + deletion_protection = var.folder_deletion_protection } resource "google_folder" "network" { - display_name = "${local.folder_prefix}-network" - parent = local.parent - # deletion_protection = var.folder_deletion_protection // uncommnet after updating "GoogleCloudPlatform/cloud-functions/google" to provider v6 + display_name = "${local.folder_prefix}-network" + parent = local.parent + deletion_protection = var.folder_deletion_protection } From dde61fe57fd18f42873729fef35a7c11b753c3d1 Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Fri, 31 Oct 2025 17:14:20 -0300 Subject: [PATCH 06/12] fix non CB flow --- .../shared/example_infra_pipeline.tf | 31 +++++++++++++------ 4-projects/business_unit_1/shared/outputs.tf | 6 ++-- 4-projects/business_unit_1/shared/remote.tf | 2 +- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/4-projects/business_unit_1/shared/example_infra_pipeline.tf b/4-projects/business_unit_1/shared/example_infra_pipeline.tf index 0e4848598..0bbd91854 100644 --- a/4-projects/business_unit_1/shared/example_infra_pipeline.tf +++ b/4-projects/business_unit_1/shared/example_infra_pipeline.tf @@ -16,7 +16,7 @@ locals { repo_names = ["bu1-example-app"] - cmd_prompt = "gcloud builds submit . --tag ${local.confidential_space_image_tag} --project=${local.cloudbuild_project_id} --service-account=projects/${local.cloudbuild_project_id}/serviceAccounts/tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com --gcs-log-dir=gs://${module.infra_pipelines[0].log_buckets["bu1-example-app"]} --worker-pool=${local.cloud_build_private_worker_pool_id} || ( sleep 46 && gcloud builds submit . --tag ${local.confidential_space_image_tag} --project=${local.cloudbuild_project_id} --service-account=projects/${local.cloudbuild_project_id}/serviceAccounts/tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com --gcs-log-dir=gs://${module.infra_pipelines[0].log_buckets["bu1-example-app"]} --worker-pool=${local.cloud_build_private_worker_pool_id})" + cmd_prompt = local.enable_cloudbuild_deploy ? "gcloud builds submit . --tag ${local.confidential_space_image_tag} --project=${local.cloudbuild_project_id} --service-account=projects/${local.cloudbuild_project_id}/serviceAccounts/tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com --gcs-log-dir=gs://${module.infra_pipelines[0].log_buckets["bu1-example-app"]} --worker-pool=${local.cloud_build_private_worker_pool_id} || ( sleep 46 && gcloud builds submit . --tag ${local.confidential_space_image_tag} --project=${local.cloudbuild_project_id} --service-account=projects/${local.cloudbuild_project_id}/serviceAccounts/tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com --gcs-log-dir=gs://${module.infra_pipelines[0].log_buckets["bu1-example-app"]} --worker-pool=${local.cloud_build_private_worker_pool_id})" : "" confidential_space_image_version = "latest" confidential_space_image_tag = "${var.default_region}-docker.pkg.dev/${local.cloudbuild_project_id}/tf-runners/confidential_space_image:${local.confidential_space_image_version}" @@ -27,19 +27,24 @@ locals { } resource "google_project_iam_member" "build_roles" { - for_each = toset(local.iam_roles_build) - project = local.cloudbuild_project_id - role = each.key - member = "serviceAccount:tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com" + for_each = toset(local.enable_cloudbuild_deploy ? local.iam_roles_build : []) + + project = local.cloudbuild_project_id + role = each.key + member = "serviceAccount:tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com" } resource "google_project_iam_member" "bucket_admin_binding" { + count = local.enable_cloudbuild_deploy ? 1 : 0 + project = local.cloudbuild_project_id role = "roles/storage.objectAdmin" member = "serviceAccount:${local.projects_terraform_sa}" } resource "google_artifact_registry_repository_iam_member" "builder_on_artifact_registry" { + count = local.enable_cloudbuild_deploy ? 1 : 0 + project = local.cloudbuild_project_id location = var.default_region repository = "tf-runners" @@ -48,24 +53,32 @@ resource "google_artifact_registry_repository_iam_member" "builder_on_artifact_r } resource "google_project_iam_member" "cloudbuild_logging" { + count = local.enable_cloudbuild_deploy ? 1 : 0 + project = local.cloudbuild_project_id role = "roles/logging.logWriter" member = "serviceAccount:${module.app_infra_cloudbuild_project[0].sa}" } resource "google_project_iam_member" "workload_identity_admin" { + count = local.enable_cloudbuild_deploy ? 1 : 0 + project = module.app_infra_cloudbuild_project[0].project_id role = "roles/iam.workloadIdentityPoolAdmin" member = "serviceAccount:${module.app_infra_cloudbuild_project[0].sa}" } resource "google_storage_bucket_iam_member" "cloudbuild_storage_read" { + count = local.enable_cloudbuild_deploy ? 1 : 0 + bucket = module.infra_pipelines[0].log_buckets["bu1-example-app"] role = "roles/storage.admin" member = "serviceAccount:${module.app_infra_cloudbuild_project[0].sa}" } resource "google_storage_bucket_iam_member" "cloudbuild_sa_storage_admin" { + count = local.enable_cloudbuild_deploy ? 1 : 0 + bucket = module.infra_pipelines[0].log_buckets["bu1-example-app"] role = "roles/storage.admin" member = "serviceAccount:tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com" @@ -131,8 +144,10 @@ resource "time_sleep" "wait_iam_propagation" { } module "build_confidential_space_image" { - source = "terraform-google-modules/gcloud/google" - version = "~> 4.0" + source = "terraform-google-modules/gcloud/google" + version = "~> 4.0" + count = local.enable_cloudbuild_deploy ? 1 : 0 + upgrade = false module_depends_on = [time_sleep.wait_iam_propagation] @@ -155,5 +170,3 @@ module "build_confidential_space_image" { resource "null_resource" "jenkins_cicd" { count = !local.enable_cloudbuild_deploy ? 1 : 0 } - - diff --git a/4-projects/business_unit_1/shared/outputs.tf b/4-projects/business_unit_1/shared/outputs.tf index 0cfc52f4f..46ddb1f11 100644 --- a/4-projects/business_unit_1/shared/outputs.tf +++ b/4-projects/business_unit_1/shared/outputs.tf @@ -65,15 +65,15 @@ output "enable_cloudbuild_deploy" { output "artifact_registry_repository_id" { description = "Artifact Registry ID." - value = module.infra_pipelines[0].artifact_registry_repository_id + value = try(module.infra_pipelines[0].artifact_registry_repository_id, "") } output "bootstrap_cloudbuild_project_id" { description = "Cloudbuild project ID." - value = local.cloudbuild_project_id + value = try(local.cloudbuild_project_id, "") } output "image_name" { description = "Image path used by confidential space instance." - value = local.confidential_space_image_tag + value = try(local.confidential_space_image_tag, "") } diff --git a/4-projects/business_unit_1/shared/remote.tf b/4-projects/business_unit_1/shared/remote.tf index 7836b6649..a89e87eff 100644 --- a/4-projects/business_unit_1/shared/remote.tf +++ b/4-projects/business_unit_1/shared/remote.tf @@ -27,7 +27,7 @@ locals { cloud_build_private_worker_pool_id = try(data.terraform_remote_state.bootstrap.outputs.cloud_build_private_worker_pool_id, "") cloud_builder_artifact_repo = try(data.terraform_remote_state.bootstrap.outputs.cloud_builder_artifact_repo, "") enable_cloudbuild_deploy = local.cloud_builder_artifact_repo != "" - cloudbuild_project_id = data.terraform_remote_state.bootstrap.outputs.cloudbuild_project_id + cloudbuild_project_id = try(data.terraform_remote_state.bootstrap.outputs.cloudbuild_project_id, "") projects_terraform_sa = data.terraform_remote_state.bootstrap.outputs.projects_step_terraform_service_account_email } From 0aa2d2dcb9b01943ecd10c2f7b11c216acdddf75 Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Thu, 6 Nov 2025 15:46:29 -0300 Subject: [PATCH 07/12] add gitlab support --- helpers/foundation-deployer/README.md | 8 +- helpers/foundation-deployer/gcp/gcp.go | 35 +- helpers/foundation-deployer/github/github.go | 42 ++- helpers/foundation-deployer/gitlab/gitlab.go | 283 +++++++++++++++ .../foundation-deployer/global.tfvars.example | 29 +- helpers/foundation-deployer/go.mod | 30 +- helpers/foundation-deployer/go.sum | 30 ++ helpers/foundation-deployer/main.go | 33 +- helpers/foundation-deployer/stages/apply.go | 331 ++++++++---------- helpers/foundation-deployer/stages/data.go | 21 +- helpers/foundation-deployer/stages/destroy.go | 31 +- .../foundation-deployer/stages/executor.go | 21 ++ .../foundation-deployer/stages/validate.go | 10 - helpers/foundation-deployer/utils/git.go | 19 +- helpers/foundation-deployer/utils/retry.go | 38 ++ 15 files changed, 644 insertions(+), 317 deletions(-) create mode 100644 helpers/foundation-deployer/gitlab/gitlab.go create mode 100644 helpers/foundation-deployer/utils/retry.go diff --git a/helpers/foundation-deployer/README.md b/helpers/foundation-deployer/README.md index c5a9b042c..af03d132c 100644 --- a/helpers/foundation-deployer/README.md +++ b/helpers/foundation-deployer/README.md @@ -16,7 +16,7 @@ Helper tool to deploy the Terraform example foundation using Cloud Build and Clo Your environment need to use the same [Terraform](https://www.terraform.io/downloads.html) version used on the build pipeline. Otherwise, you might experience Terraform state snapshot lock errors. -Version 1.5.7 is the last version before the license model change. To use a later version of Terraform, ensure that the Terraform version used in the Operational System to manually execute part of the steps in `3-networks` and `4-projects` is the same version configured in the following code +Version 1.5.7 is the last version before the license model change. To use a later version of Terraform, ensure that the Terraform version used in the Operating System to manually execute part of the steps in `3-networks` and `4-projects` is the same version configured in the following code - 0-bootstrap/modules/jenkins-agent/variables.tf ``` @@ -73,7 +73,7 @@ Version 1.5.7 is the last version before the license model change. To use a late ### Prepare the deploy environment -- Create a directory in the file system to host the Cloud Source repositories the will be created and a copy of the terraform example foundation. +- Create a directory in the file system to host the Git Repositories that will be created (Cloud Source repositories, Github , or GitLab) and a copy of the terraform example foundation repository. - Clone the `terraform-example-foundation` repository on this directory. ```text @@ -106,10 +106,6 @@ Version 1.5.7 is the last version before the license model change. To use a late By default the foundation regional resources are deployed in `us-west1` and `us-central1` regions and multi-regional resources are deployed in the `US` multi-region. -In addition to the variables declared in the file `global.tfvars` for configuring location, there are two locals, `default_region1` and `default_region2`, in each one of the environments (`production`, `nonproduction`, and `development`) in the network steps (`3-networks-svpc` and `3-networks-hub-and-spoke`). -They are located in the [main.tf](../../3-networks-svpc/envs/production/main.tf#L20-L21) files for each environments. -Change the two locals **before** starting the deployment to deploy in other regions. - **Note:** the region used for the variable `default_region` in the file `global.tfvars` **MUST** be one of the regions used for the `default_region1` and `default_region2` locals. ### Application default credentials diff --git a/helpers/foundation-deployer/gcp/gcp.go b/helpers/foundation-deployer/gcp/gcp.go index 2ce54cd29..ba86202cb 100644 --- a/helpers/foundation-deployer/gcp/gcp.go +++ b/helpers/foundation-deployer/gcp/gcp.go @@ -18,7 +18,6 @@ import ( "context" "encoding/json" "fmt" - "regexp" "strings" "time" @@ -27,6 +26,8 @@ import ( "github.com/mitchellh/go-testing-interface" "github.com/tidwall/gjson" + localutil "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/utils" + "github.com/terraform-google-modules/terraform-example-foundation/test/integration/testutils" "google.golang.org/api/cloudbuild/v1" @@ -51,20 +52,6 @@ type Build struct { CreateTime string `json:"createTime"` } -var ( - retryRegexp = map[*regexp.Regexp]string{} -) - -func init() { - for e, m := range testutils.RetryableTransientErrors { - r, err := regexp.Compile(fmt.Sprintf("(?s)%s", e)) //(?s) enables dot (.) to match newline. - if err != nil { - panic(fmt.Sprintf("failed to compile regex %s: %s", e, err.Error())) - } - retryRegexp[r] = m - } -} - type GCP struct { Runf func(t testing.TB, cmd string, args ...interface{}) gjson.Result RunCmd func(t testing.TB, cmd string, args ...interface{}) string @@ -209,7 +196,8 @@ func (g GCP) WaitBuildSuccess(t testing.TB, project, region, repo, commitSha, fa } if status != StatusSuccess { - if !g.IsRetryableError(t, project, region, build) { + logs := g.GetBuildLogs(t, project, region, build) + if !localutil.IsRetryableError(t, logs) { return fmt.Errorf("%s\nSee:\nhttps://console.cloud.google.com/cloud-build/builds;region=%s/%s?project=%s\nfor details", failureMsg, region, build, project) } fmt.Println("build failed with retryable error. a new build will be triggered.") @@ -230,21 +218,6 @@ func (g GCP) WaitBuildSuccess(t testing.TB, project, region, repo, commitSha, fa return fmt.Errorf("%s\nbuild failed after %d retries.\nSee Cloud Build logs for details", failureMsg, maxErrorRetries) } -// IsRetryableError checks the logs of a failed Cloud Build build -// and verify if the error is a transient one and can be retried -func (g GCP) IsRetryableError(t testing.TB, projectID, region, build string) bool { - logs := g.GetBuildLogs(t, projectID, region, build) - found := false - for pattern, msg := range retryRegexp { - if pattern.MatchString(logs) { - found = true - fmt.Printf("error '%s' is worth of a retry\n", msg) - break - } - } - return found -} - // HasSccNotification checks if a Security Command Center notification exists func (g GCP) HasSccNotification(t testing.TB, orgID, sccName string) bool { filter := fmt.Sprintf("name=organizations/%s/locations/global/notificationConfigs/%s", orgID, sccName) diff --git a/helpers/foundation-deployer/github/github.go b/helpers/foundation-deployer/github/github.go index 10720ceea..1a5511502 100644 --- a/helpers/foundation-deployer/github/github.go +++ b/helpers/foundation-deployer/github/github.go @@ -25,6 +25,9 @@ import ( "github.com/google/go-github/v58/github" "github.com/mitchellh/go-testing-interface" "github.com/terraform-google-modules/terraform-example-foundation/test/integration/testutils" + + localutil "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/utils" + "golang.org/x/oauth2" ) @@ -62,25 +65,30 @@ type GH struct { sleepTime time.Duration } +// NewGH creates a new wrapper for The GitHub API func NewGH() GH { return GH{ TriggerNewBuild: triggerNewBuild, sleepTime: 20, } } +// triggerNewBuild triggers a new action execution func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (int64, string, string, error) { - if token == "" { - return 0, "", "", fmt.Errorf("GITHUB_TOKEN not set") - } - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) tc := oauth2.NewClient(ctx, ts) client := github.NewClient(tc) - _, err := client.Actions.RerunWorkflowByID(ctx, owner, repo, runID) + resp, err := client.Actions.RerunWorkflowByID(ctx, owner, repo, runID) if err != nil { return 0, "", "", fmt.Errorf("Error re-running workflow: %v", err) } + if resp.StatusCode != http.StatusCreated { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return 0, "", "", fmt.Errorf("Error re-running workflow status: %d body parsing error: %v", resp.StatusCode, err) + } + return 0, "", "", fmt.Errorf("Error re-running workflow status: %d body: %s", resp.StatusCode, string(bodyBytes)) + } opts := &github.ListWorkflowRunsOptions{ ListOptions: github.ListOptions{ @@ -120,7 +128,8 @@ func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token strin return newRunID, status, conclusion, nil } -func (g GH) GetLastBuildState(t testing.TB, ctx context.Context, owner, repo, token string) (int64, string, string, error) { +// GetLastActionState returns the state of the latest action +func (g GH) GetLastActionState(t testing.TB, ctx context.Context, owner, repo, token string) (int64, string, string, error) { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) tc := oauth2.NewClient(ctx, ts) client := github.NewClient(tc) @@ -158,6 +167,7 @@ func (g GH) GetLastBuildState(t testing.TB, ctx context.Context, owner, repo, to return runID, status, conclusion, nil } +// GetActionState returns the state of a given action func (g GH) GetActionState(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (string, string, error) { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) tc := oauth2.NewClient(ctx, ts) @@ -180,6 +190,7 @@ func (g GH) GetActionState(t testing.TB, ctx context.Context, owner, repo, token return status, conclusion, nil } +// GetBuildLogs returns the execution logs of an action func (g GH) GetBuildLogs(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (string, error) { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) tc := oauth2.NewClient(ctx, ts) @@ -229,6 +240,7 @@ func (g GH) GetBuildLogs(t testing.TB, ctx context.Context, owner, repo, token s return "", nil } +// GetFinalActionState returns the final state of an action func (g GH) GetFinalActionState(t testing.TB, ctx context.Context, owner, repo, token string, runID int64, maxBuildRetry int) (string, string, error) { var status, conclusion string var err error @@ -265,7 +277,7 @@ func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token string, failureMsg // wait action creation time.Sleep(30 * time.Second) - runID, status, conclusion, err = g.GetLastBuildState(t, ctx, owner, repo, token) + runID, status, conclusion, err = g.GetLastActionState(t, ctx, owner, repo, token) if err != nil { return err } @@ -282,7 +294,7 @@ func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token string, failureMsg if err != nil { return err } - if !g.IsRetryableError(t, logs) { + if localutil.IsRetryableError(t, logs) { return fmt.Errorf("%s\nSee:\nhttps://github.com/%s/%s/actions/runs/%d\nfor details", failureMsg, owner, repo, runID) } fmt.Println("build failed with retryable error. a new build will be triggered.") @@ -302,17 +314,3 @@ func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token string, failureMsg } return fmt.Errorf("%s action failed after %d retries", failureMsg, maxErrorRetries) } - -// IsRetryableError checks the logs of a failed Cloud Build build -// and verify if the error is a transient one and can be retried -func (g GH) IsRetryableError(t testing.TB, logs string) bool { - found := false - for pattern, msg := range retryRegexp { - if pattern.MatchString(logs) { - found = true - fmt.Printf("error '%s' is worth of a retry\n", msg) - break - } - } - return found -} diff --git a/helpers/foundation-deployer/gitlab/gitlab.go b/helpers/foundation-deployer/gitlab/gitlab.go new file mode 100644 index 000000000..8d9cccb10 --- /dev/null +++ b/helpers/foundation-deployer/gitlab/gitlab.go @@ -0,0 +1,283 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gitlab + +import ( + "context" + "fmt" + "io" + "net/http" + "regexp" + "time" + + "github.com/mitchellh/go-testing-interface" + "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/utils" + + gitlab "gitlab.com/gitlab-org/api/client-go" +) + +const ( + StatusCreated = "created" + StatusManual = "manual" + StatusPending = "pending" + StatusPreparing = "preparing" + StatusSkipped = "skipped" + StatusWaitingForResource = "waiting_for_resource" + StatusScheduled = "scheduled" + StatusRunning = "running" + StatusSuccess = "success" + StatusFailed = "failed" + StatusCancelled = "canceled" + StatusCanceling = "canceling" +) + +var ( + retryRegexp = map[*regexp.Regexp]string{} +) + +type GL struct { + TriggerNewBuild func(t testing.TB, ctx context.Context, owner, project, token string, jobID int) (int, error) + sleepTime time.Duration +} + +// NewGL creates a new GitLab wrapper for The GitLab API +func NewGL() GL { + return GL{ + TriggerNewBuild: triggerNewBuild, + sleepTime: 20, + } +} + +// triggerNewBuild triggers a new job execution +func triggerNewBuild(t testing.TB, ctx context.Context, owner, project, token string, jobID int) (int, error) { + + git, err := gitlab.NewClient(token) + if err != nil { + return 0, fmt.Errorf("Failed to create client: %v", err) + } + + job, resp, err := git.Jobs.RetryJob(fmt.Sprintf("%s/%s", owner, project), jobID, gitlab.WithContext(ctx)) + if err != nil { + return 0, fmt.Errorf("Error retrying job: %v", err) + } + if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode <= http.StatusNetworkAuthenticationRequired { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return 0, fmt.Errorf("error Status: %d, failed to read response body: %v", resp.StatusCode, err) + } + return 0, fmt.Errorf("error Status: %d\n body: %s", resp.StatusCode, string(bodyBytes)) + } + return job.ID, nil +} + +// GetLastJobStatus returns the status of the latest executed job +func (g GL) GetLastJobStatus(t testing.TB, ctx context.Context, owner, project, token string) (string, int, error) { + git, err := gitlab.NewClient(token) + if err != nil { + return "", 0, fmt.Errorf("Failed to create client: %v", err) + } + + includeRetried := true + opts := &gitlab.ListJobsOptions{ + ListOptions: gitlab.ListOptions{ + PerPage: 1, + Page: 1, + OrderBy: "id", + Sort: "desc", + }, + IncludeRetried: &includeRetried, + } + + jobs, _, err := git.Jobs.ListProjectJobs(fmt.Sprintf("%s/%s", owner, project), opts, gitlab.WithContext(ctx)) + if err != nil { + return "", 0, fmt.Errorf("Error listing project jobs: %v", err) + } + + if len(jobs) == 0 { + return "", 0, fmt.Errorf("No jobs found for project: %s/%s", owner, project) + } + + return jobs[0].Status, jobs[0].ID, nil +} + +// GetJobLogs returns the execution logs of a given job +func (g GL) GetJobLogs(t testing.TB, ctx context.Context, owner, project, token string, jobID int) (string, error) { + git, err := gitlab.NewClient(token) + if err != nil { + return "", fmt.Errorf("Failed to create client: %v", err) + } + + reader, resp, err := git.Jobs.GetTraceFile(fmt.Sprintf("%s/%s", owner, project), jobID) + if err != nil { + return "", fmt.Errorf("Error getting job trace file: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("Expected status 200 OK, but got %s for job %d trace", resp.Status, jobID) + } + + logBytes, err := io.ReadAll(reader) + if err != nil { + return "", fmt.Errorf("Failed to read job %d trace from bytes.Reader: %v", jobID, err) + } + + return string(logBytes), nil +} + +// GetJobStatus returns the status of a given gob +func (g GL) GetJobStatus(t testing.TB, ctx context.Context, owner, project, token string, jobID int) (string, error) { + git, err := gitlab.NewClient(token) + if err != nil { + return "", fmt.Errorf("Failed to create client: %v", err) + } + + job, _, err := git.Jobs.GetJob(fmt.Sprintf("%s/%s", owner, project), jobID, gitlab.WithContext(ctx)) + if err != nil { + return "", fmt.Errorf("Error retrying job: %v", err) + } + + return job.Status, nil +} + +// GetFinalJobStatus return the final state of a running job +func (g GL) GetFinalJobStatus(t testing.TB, ctx context.Context, owner, project, token string, jobID int, maxBuildRetry int) (string, error) { + var status string + var err error + count := 0 + fmt.Printf("waiting for job %d execution.\n", jobID) + + status, err = g.GetJobStatus(t, ctx, owner, project, token, jobID) + if err != nil { + return "", err + } + fmt.Printf("job status is %s\n", status) + for status != StatusSuccess && status != StatusFailed && status != StatusCancelled { + fmt.Printf("job status is %s\n", status) + if count >= maxBuildRetry { + return "", fmt.Errorf("timeout waiting for job '%d' execution", jobID) + } + count = count + 1 + time.Sleep(g.sleepTime * time.Second) + status, err = g.GetJobStatus(t, ctx, owner, project, token, jobID) + if err != nil { + return "", err + } + } + fmt.Printf("final job state is %s\n", status) + return status, nil +} + +// WaitBuildSuccess waits for the current job in a project to finish. +func (g GL) WaitBuildSuccess(t testing.TB, owner, project, token, failureMsg string, maxBuildRetry, maxErrorRetries int, timeBetweenErrorRetries time.Duration) error { + var status string + var jobID int + var err error + + ctx := context.Background() + // wait job creation + time.Sleep(30 * time.Second) + + status, jobID, err = g.GetLastJobStatus(t, ctx, owner, project, token) + if err != nil { + return err + } + for i := 0; i < maxErrorRetries; i++ { + if status != StatusSuccess && status != StatusFailed && status != StatusCancelled { + status, err = g.GetFinalJobStatus(t, ctx, owner, project, token, jobID, maxBuildRetry) + if err != nil { + return err + } + } + + if status != StatusSuccess { + logs, err := g.GetJobLogs(t, ctx, owner, project, token, jobID) + if err != nil { + return err + } + if utils.IsRetryableError(t, logs) { + return fmt.Errorf("%s\nSee:\nhttps://gitlab.com/%s/%s/-/jobs/%d\nfor details", failureMsg, owner, project, jobID) + } + fmt.Println("job failed with retryable error. a new job will be triggered.") + } else { + return nil // job succeeded + } + + // Trigger a new build + jobID, err = g.TriggerNewBuild(t, ctx, owner, project, token, jobID) + if err != nil { + return fmt.Errorf("failed to trigger new job (attempt %d/%d): %w", i+1, maxErrorRetries, err) + } + fmt.Printf("triggered new job with ID: %d (attempt %d/%d)\n", jobID, i+1, maxErrorRetries) + if i < maxErrorRetries-1 { + time.Sleep(timeBetweenErrorRetries) // Wait before retrying + } + } + return fmt.Errorf("%s job failed after %d retries", failureMsg, maxErrorRetries) +} + +// AddProjectsToJobTokenScope adds a list of projects to the token scope of a project that host the runner image +func (g GL) AddProjectsToJobTokenScope(t testing.TB, owner, cicdProject, token string, repos []string) error { + ctx := context.Background() + git, err := gitlab.NewClient(token) + if err != nil { + return fmt.Errorf("Failed to create client: %v", err) + } + runnerProjectPath := fmt.Sprintf("%s/%s", owner, cicdProject) + + projectsToAdd := make([]string, len(repos)) + for i, repo := range repos { + projectsToAdd[i] = fmt.Sprintf("%s/%s", owner, repo) + } + + runnerProjectID, err := getProjectIDByPath(git, ctx, runnerProjectPath) + if err != nil { + return fmt.Errorf("Could not get the project ID for the runner repository. Aborting. Error: %v", err) + } + for _, targetProjectPath := range projectsToAdd { + + targetProjectID, err := getProjectIDByPath(git, ctx, targetProjectPath) + if err != nil { + return fmt.Errorf("Could not find project ID. Error: %v", err) + } + + opts := &gitlab.JobTokenInboundAllowOptions{ + TargetProjectID: &targetProjectID, + } + + _, resp, err := git.JobTokenScope.AddProjectToJobScopeAllowList(runnerProjectID, opts, gitlab.WithContext(ctx)) + if err != nil { + // GitLab often returns a 409 Conflict if the project is already in the list. + if resp != nil && resp.StatusCode != http.StatusConflict { + return fmt.Errorf("failed to add project. Status: %s. Error: %v", resp.Status, err) + } + continue + } + + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("DONE (Status: %s)\n", resp.Status) + } + } + + return nil +} + +// getProjectIDByPath returns the ID or a project given its path +func getProjectIDByPath(git *gitlab.Client, ctx context.Context, projectPath string) (int, error) { + project, _, err := git.Projects.GetProject(projectPath, nil, gitlab.WithContext(ctx)) + if err != nil { + return 0, fmt.Errorf("failed to get project '%s': %w", projectPath, err) + } + return project.ID, nil +} diff --git a/helpers/foundation-deployer/global.tfvars.example b/helpers/foundation-deployer/global.tfvars.example index 404133c73..da9d2e720 100644 --- a/helpers/foundation-deployer/global.tfvars.example +++ b/helpers/foundation-deployer/global.tfvars.example @@ -86,20 +86,29 @@ groups = { } // Uncomment for GitHub or GitLab deploy -// See: -// GitHub: https://github.com/terraform-google-modules/terraform-example-foundation/blob/main/0-bootstrap/README-GitHub.md#requirements -// GitLab: https://github.com/terraform-google-modules/terraform-example-foundation/blob/main/0-bootstrap/README-GitLab.md#requirements + // git_repos = { -// token = "ACCESS_TOKEN" // owner = "REPO_OWNER" -// bootstrap = "BOOTSTRAP_REPO_URL" -// organization = "ORGANIZATION_REPO_URL" -// environments = "ENVIRONMENTS_REPO_URL" -// networks = "NETWORKS_REPO_URL" -// projects = "PROJECTS_REPO_URL" -// cicd_runner = "CICD_RUNNER_REPO_URL" // Required if GitLab +// bootstrap = "BOOTSTRAP_REPO" +// organization = "ORGANIZATION_REPO" +// environments = "ENVIRONMENTS_REPO" +// networks = "NETWORKS_REPO" +// projects = "PROJECTS_REPO" +// cicd_runner = "CICD_RUNNER_REPO" // Required for GitLab // } +// to prevent saving the git token in plain text in this file, +// export the token in the command line +// as an environment variable before running this helper. +// Run the following command in your shell: + +// export GIT_TOKEN="YOUR-ACCESS-TOKEN" + +// See: +// GitHub: https://github.com/terraform-google-modules/terraform-example-foundation/blob/main/0-bootstrap/README-GitHub.md#requirements +// GitLab: https://github.com/terraform-google-modules/terraform-example-foundation/blob/main/0-bootstrap/README-GitLab.md#requirements + + // 1-org inputs // https://github.com/terraform-google-modules/terraform-example-foundation/blob/master/1-org/envs/shared/README.md#inputs diff --git a/helpers/foundation-deployer/go.mod b/helpers/foundation-deployer/go.mod index 4ab447447..00e389fe1 100644 --- a/helpers/foundation-deployer/go.mod +++ b/helpers/foundation-deployer/go.mod @@ -9,13 +9,19 @@ require ( github.com/gruntwork-io/terratest v0.48.1 github.com/hashicorp/hcl/v2 v2.23.0 github.com/mitchellh/go-testing-interface v1.14.2-0.20210821155943-2d9075ca8770 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/terraform-google-modules/terraform-example-foundation/test/integration v0.0.0-20240808135927-5f1fd0f4104a github.com/tidwall/gjson v1.18.0 google.golang.org/api v0.206.0 ) -require github.com/google/go-querystring v1.1.0 // indirect +require ( + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + gitlab.com/gitlab-org/api/client-go v0.158.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/time v0.12.0 // indirect +) require ( cloud.google.com/go/auth v0.10.2 // indirect @@ -29,7 +35,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-github/v58 v58.0.0 github.com/google/s2a-go v0.1.8 // indirect github.com/google/uuid v1.6.0 // indirect @@ -61,17 +67,17 @@ require ( go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/crypto v0.40.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.35.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect google.golang.org/grpc v1.67.1 // indirect - google.golang.org/protobuf v1.35.1 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/helpers/foundation-deployer/go.sum b/helpers/foundation-deployer/go.sum index 78b22e394..92fe3406a 100644 --- a/helpers/foundation-deployer/go.sum +++ b/helpers/foundation-deployer/go.sum @@ -57,6 +57,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github/v58 v58.0.0 h1:Una7GGERlF/37XfkPwpzYJe0Vp4dt2k1kCjlxwjIvzw= github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= @@ -81,6 +82,10 @@ github.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jc github.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= @@ -120,6 +125,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/terraform-google-modules/terraform-example-foundation/test/integration v0.0.0-20240808135927-5f1fd0f4104a h1:4Ih0BauwdUTF+YuA55/qY8Q+d5brYKPpae0YWkB9D2A= github.com/terraform-google-modules/terraform-example-foundation/test/integration v0.0.0-20240808135927-5f1fd0f4104a/go.mod h1:p8CvVuYRey5Nb8dipH5KM+eY+TnqfLgDnQ5M1a7oHiw= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -140,6 +146,8 @@ github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +gitlab.com/gitlab-org/api/client-go v0.158.0 h1:CfWA94ZaU4STlIfsYBGcpks3eUVojXvNFaytmNptbX8= +gitlab.com/gitlab-org/api/client-go v0.158.0/go.mod h1:D0DHF7ILUfFo/JcoGMAEndiKMm8SiP/WjyJ4OfXxCKw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= @@ -154,12 +162,17 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -168,26 +181,39 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -195,6 +221,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.206.0 h1:A27GClesCSheW5P2BymVHjpEeQ2XHH8DI8Srs2HI2L8= google.golang.org/api v0.206.0/go.mod h1:BtB8bfjTYIrai3d8UyvPmV9REGgox7coh+ZRwm0b+W8= @@ -226,6 +254,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/helpers/foundation-deployer/main.go b/helpers/foundation-deployer/main.go index 060671fb8..4f0cd001c 100644 --- a/helpers/foundation-deployer/main.go +++ b/helpers/foundation-deployer/main.go @@ -111,6 +111,19 @@ func main() { Logger: utils.GetLogger(cfg.quiet), } + // validate git configuration for GitHub and GitLab + if globalTFVars.BuildType == stages.BuildTypeGiHub || globalTFVars.BuildType == stages.BuildTypeGitLab { + token := os.Getenv("GIT_TOKEN") + if token == "" { + fmt.Println("# GIT_TOKEN environment variable not set. It is required for GitHub and GitLab.") + os.Exit(1) + } + if globalTFVars.GitRepos == nil { + fmt.Printf("# for build type %s variable 'git_repos' is required\n", globalTFVars.BuildType) + os.Exit(1) + } + conf.GitToken = token + } // only enable services if they are not already enabled if globalTFVars.HasValidatorProj() { conf.ValidatorProject = *globalTFVars.ValidatorProjectID @@ -169,6 +182,17 @@ func main() { return } + var envVars map[string]string + if conf.BuildType != stages.BuildTypeGiHub { + envVars = map[string]string{ + "TF_VAR_gh_token": conf.GitToken, + } + } + if conf.BuildType != stages.BuildTypeGitLab { + envVars = map[string]string{ + "TF_VAR_gitlab_token": conf.GitToken, + } + } // destroy stages if cfg.destroy { // Note: destroy is only terraform destroy, local directories are not deleted. @@ -231,7 +255,7 @@ func main() { // 0-bootstrap msg.PrintStageMsg("Destroying 0-bootstrap stage") err = s.RunDestroyStep("gcp-bootstrap", func() error { - return stages.DestroyBootstrapStage(t, s, conf) + return stages.DestroyBootstrapStage(t, s, conf, envVars) }) if err != nil { fmt.Printf("# Bootstrap step destroy failed. Error: %s\n", err.Error()) @@ -253,11 +277,8 @@ func main() { msg.PrintStageMsg("Deploying 0-bootstrap stage") skipInnerBuildMsg := s.IsStepComplete("gcp-bootstrap") err = s.RunStep("gcp-bootstrap", func() error { - if globalTFVars.BuildType == stages.BuildTypeCBCSR { - return stages.DeployBootstrapStage(t, s, globalTFVars, conf) - } else { - return stages.DeployGenericBootstrapStage(t, s, globalTFVars, conf) - } + return stages.DeployBootstrapStage(t, s, globalTFVars, conf) + }) if err != nil { fmt.Printf("# Bootstrap step failed. Error: %s\n", err.Error()) diff --git a/helpers/foundation-deployer/stages/apply.go b/helpers/foundation-deployer/stages/apply.go index 7b464fa8c..3e15e09f2 100644 --- a/helpers/foundation-deployer/stages/apply.go +++ b/helpers/foundation-deployer/stages/apply.go @@ -23,6 +23,7 @@ import ( "github.com/mitchellh/go-testing-interface" "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/gcp" + "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/gitlab" "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/msg" "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/steps" "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/utils" @@ -30,7 +31,56 @@ import ( "github.com/terraform-google-modules/terraform-example-foundation/test/integration/testutils" ) -func DeployGenericBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c CommonConf) error { +func buildGitLabCICDImage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c CommonConf) error { + gl := gitlab.NewGL() + cicdPath := filepath.Join(c.CheckoutPath, "gcp-cicd-runner") + repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner) + + conf := utils.GitClone(t, tfvars.BuildType, "", repoURL, cicdPath, "", c.Logger) + conf.CheckoutBranch("image") + + err := utils.CopyFile(filepath.Join(c.FoundationPath, "build/gitlab-ci.yml"), filepath.Join(cicdPath, ".gitlab-ci.yml")) + if err != nil { + return err + } + + err = utils.CopyFile(filepath.Join(c.FoundationPath, "0-bootstrap/Dockerfile"), filepath.Join(cicdPath, "Dockerfile")) + if err != nil { + return err + } + + err = conf.CommitFiles("Initialize CI/CD runner project") + if err != nil { + return err + } + err = conf.PushBranch("image", "origin") + if err != nil { + return err + } + + fmt.Println("") + fmt.Println("# Follow job execution in GitLab:") + fmt.Printf("# %s\n", fmt.Sprintf("https://gitlab.com/%s/%s/-/jobs", tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner)) + + failureMsg := fmt.Sprintf("CI/CD runner image job failed %s/%s repository.", tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner) + err = gl.WaitBuildSuccess(t, tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner, c.GitToken, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) + if err != nil { + return err + } + projectsToAdd := make([]string, 5) + projectsToAdd[0] = tfvars.GitRepos.Bootstrap + projectsToAdd[1] = tfvars.GitRepos.Organization + projectsToAdd[2] = tfvars.GitRepos.Environments + projectsToAdd[3] = tfvars.GitRepos.Networks + projectsToAdd[4] = tfvars.GitRepos.Projects + return gl.AddProjectsToJobTokenScope(t, tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner, c.GitToken, projectsToAdd) +} + +func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c CommonConf) error { + + var err error + var envVars map[string]string + bootstrapTfvars := BootstrapTfvars{ OrgID: tfvars.OrgID, DefaultRegion: tfvars.DefaultRegion, @@ -50,11 +100,6 @@ func DeployGenericBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVar ProjectDeletionPolicy: tfvars.ProjectDeletionPolicy, } - var buildExecutor Executor - var repoURL string - var err error - var envVars map[string]string - if tfvars.BuildType == BuildTypeGiHub { bootstrapTfvars.GitHubRepos = &GitHubRepos{ Owner: tfvars.GitRepos.Owner, @@ -65,10 +110,8 @@ func DeployGenericBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVar Projects: tfvars.GitRepos.Projects, } envVars = map[string]string{ - "TF_VAR_gh_token": tfvars.GitRepos.Token, + "TF_VAR_gh_token": c.GitToken, } - repoURL = utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Bootstrap) - buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Bootstrap, tfvars.GitRepos.Token) } if tfvars.BuildType == BuildTypeGitLab { @@ -82,7 +125,7 @@ func DeployGenericBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVar CICDRunner: *tfvars.GitRepos.CICDRunner, } envVars = map[string]string{ - "TF_VAR_gitlab_token": tfvars.GitRepos.Token, + "TF_VAR_gitlab_token": c.GitToken, } } @@ -93,9 +136,6 @@ func DeployGenericBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVar return err } - EmptyProject := "" - NoRegion := "" - terraformDir := filepath.Join(c.FoundationPath, BootstrapStep) options := &terraform.Options{ TerraformDir: terraformDir, @@ -114,6 +154,7 @@ func DeployGenericBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVar } // read bootstrap outputs + defaultRegion := terraform.OutputMap(t, options, "common_config")["default_region"] backendBucket := terraform.Output(t, options, "gcs_bucket_tfstate") backendBucketProjects := terraform.Output(t, options, "projects_gcs_bucket_tfstate") @@ -157,184 +198,63 @@ func DeployGenericBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVar return err } + var stageConf StageConf + var cbProjectID string + var executor Executor + var bootstrapConf utils.GitRepo + gcpBootstrapPath := filepath.Join(c.CheckoutPath, BootstrapRepo) - bootstrapConf := utils.GitClone(t, tfvars.BuildType, "", repoURL, gcpBootstrapPath, EmptyProject, c.Logger) - stageConf := StageConf{ - Stage: BootstrapRepo, - CICDProject: EmptyProject, - DefaultRegion: NoRegion, - Step: BootstrapStep, - Repo: BootstrapRepo, - CustomTargetDirPath: "envs/shared", - GitConf: bootstrapConf, - Envs: []string{"shared"}, - BuildType: tfvars.BuildType, - BuildExecutor: buildExecutor, - } - // if groups creation is enable the helper will just push the code - // because Cloud Build build will fail until bootstrap - // service account is granted Group Admin role in the - // Google Workspace by a Super Admin. - // https://github.com/terraform-google-modules/terraform-google-group/blob/main/README.md#google-workspace-formerly-known-as-g-suite-roles - if tfvars.HasGroupsCreation() { - err = saveBootstrapCodeOnly(t, stageConf, s, c) - } else { - err = deployStage(t, stageConf, s, c) - } + if tfvars.BuildType == BuildTypeCBCSR { + cbProjectID = terraform.Output(t, options, "cloudbuild_project_id") - if err != nil { - return err - } + msg.PrintBuildMsg(cbProjectID, defaultRegion, c.DisablePrompt) - // Init gcp-bootstrap terraform - err = s.RunStep("gcp-bootstrap.init-tf", func() error { - options := &terraform.Options{ - TerraformDir: filepath.Join(gcpBootstrapPath, "envs", "shared"), - Logger: c.Logger, - NoColor: true, - RetryableTerraformErrors: testutils.RetryableTransientErrors, - MaxRetries: MaxErrorRetries, - TimeBetweenRetries: TimeBetweenErrorRetries, - EnvVars: envVars, + // Check if image build was successful. + buildTFBuilderExecutor := NewGCPExecutor(cbProjectID, defaultRegion, "tf-cloudbuilder") + err = buildTFBuilderExecutor.WaitBuildSuccess(t, "", "Terraform Image builder Build Failed for tf-cloudbuilder repository.") + if err != nil { + return err } - _, err := terraform.InitE(t, options) - return err - }) - if err != nil { - return err - } - fmt.Println("end of bootstrap deploy") - - return nil -} - -func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c CommonConf) error { - bootstrapTfvars := BootstrapTfvars{ - OrgID: tfvars.OrgID, - DefaultRegion: tfvars.DefaultRegion, - DefaultRegion2: tfvars.DefaultRegion2, - DefaultRegionGCS: tfvars.DefaultRegionGCS, - DefaultRegionKMS: tfvars.DefaultRegionKMS, - BillingAccount: tfvars.BillingAccount, - ParentFolder: tfvars.ParentFolder, - ProjectPrefix: tfvars.ProjectPrefix, - FolderPrefix: tfvars.FolderPrefix, - BucketForceDestroy: tfvars.BucketForceDestroy, - BucketTfstateKmsForceDestroy: tfvars.BucketTfstateKmsForceDestroy, - WorkflowDeletionProtection: tfvars.WorkflowDeletionProtection, - Groups: tfvars.Groups, - InitialGroupConfig: tfvars.InitialGroupConfig, - FolderDeletionProtection: tfvars.FolderDeletionProtection, - ProjectDeletionPolicy: tfvars.ProjectDeletionPolicy, - } - err := utils.WriteTfvars(filepath.Join(c.FoundationPath, BootstrapStep, "terraform.tfvars"), bootstrapTfvars) - if err != nil { - return err - } + //prepare policies repo + gcpPoliciesPath := filepath.Join(c.CheckoutPath, PoliciesRepo) + policiesConf := utils.GitClone(t, "CSR", PoliciesRepo, "", gcpPoliciesPath, cbProjectID, c.Logger) + policiesBranch := "main" - // delete README-Jenkins.md due to private key checker false positive - jenkinsReadme := filepath.Join(c.FoundationPath, BootstrapStep, "README-Jenkins.md") - exist, err := utils.FileExists(jenkinsReadme) - if err != nil { - return err - } - if exist { - err = os.Remove(jenkinsReadme) + err = s.RunStep("gcp-bootstrap.gcp-policies", func() error { + return preparePoliciesRepo(policiesConf, policiesBranch, c.FoundationPath, gcpPoliciesPath) + }) if err != nil { return err } - } - - terraformDir := filepath.Join(c.FoundationPath, BootstrapStep) - options := &terraform.Options{ - TerraformDir: terraformDir, - Logger: c.Logger, - NoColor: true, - RetryableTerraformErrors: testutils.RetryableTransientErrors, - MaxRetries: MaxErrorRetries, - TimeBetweenRetries: TimeBetweenErrorRetries, - } - // terraform deploy - err = applyLocal(t, options, "", c.PolicyPath, c.ValidatorProject) - if err != nil { - return err + bootstrapConf = utils.GitClone(t, "CSR", BootstrapRepo, "", gcpBootstrapPath, cbProjectID, c.Logger) + executor = NewGCPExecutor(cbProjectID, defaultRegion, BootstrapRepo) } - // read bootstrap outputs - defaultRegion := terraform.OutputMap(t, options, "common_config")["default_region"] - cbProjectID := terraform.Output(t, options, "cloudbuild_project_id") - backendBucket := terraform.Output(t, options, "gcs_bucket_tfstate") - backendBucketProjects := terraform.Output(t, options, "projects_gcs_bucket_tfstate") + if tfvars.BuildType == BuildTypeGiHub { + executor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Bootstrap, c.GitToken) + repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Bootstrap) + bootstrapConf = utils.GitClone(t, tfvars.BuildType, "", repoURL, gcpBootstrapPath, cbProjectID, c.Logger) - // replace backend and terraform init migrate - err = s.RunStep("gcp-bootstrap.migrate-state", func() error { - options.MigrateState = true - err = utils.CopyFile(filepath.Join(options.TerraformDir, "backend.tf.example"), filepath.Join(options.TerraformDir, "backend.tf")) - if err != nil { - return err - } - err = utils.ReplaceStringInFile(filepath.Join(options.TerraformDir, "backend.tf"), "UPDATE_ME", backendBucket) - if err != nil { - return err - } - _, err := terraform.InitE(t, options) - return err - }) - if err != nil { - return err } - // replace all backend files - err = s.RunStep("gcp-bootstrap.replace-backend-files", func() error { - files, err := utils.FindFiles(c.FoundationPath, "backend.tf") + if tfvars.BuildType == BuildTypeGitLab { + + err = s.RunStep("gcp-bootstrap.build-cicd-runner", func() error { + return buildGitLabCICDImage(t, s, tfvars, c) + }) if err != nil { return err } - for _, file := range files { - err = utils.ReplaceStringInFile(file, "UPDATE_ME", backendBucket) - if err != nil { - return err - } - err = utils.ReplaceStringInFile(file, "UPDATE_PROJECTS_BACKEND", backendBucketProjects) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return err - } - - msg.PrintBuildMsg(cbProjectID, defaultRegion, c.DisablePrompt) - - // Check if image build was successful. - buildTFBuilderExecutor := NewGCPExecutor(cbProjectID, defaultRegion, "tf-cloudbuilder") - err = buildTFBuilderExecutor.WaitBuildSuccess(t, "", "Terraform Image builder Build Failed for tf-cloudbuilder repository.") - if err != nil { - return err - } - - //prepare policies repo - gcpPoliciesPath := filepath.Join(c.CheckoutPath, PoliciesRepo) - policiesConf := utils.GitClone(t, "CSR", PoliciesRepo, "", gcpPoliciesPath, cbProjectID, c.Logger) - policiesBranch := "main" - err = s.RunStep("gcp-bootstrap.gcp-policies", func() error { - return preparePoliciesRepo(policiesConf, policiesBranch, c.FoundationPath, gcpPoliciesPath) - }) - if err != nil { - return err + executor = NewGitLabExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Bootstrap, c.GitToken) + repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Bootstrap) + bootstrapConf = utils.GitClone(t, tfvars.BuildType, "", repoURL, gcpBootstrapPath, cbProjectID, c.Logger) } - //prepare bootstrap repo - gcpBootstrapPath := filepath.Join(c.CheckoutPath, BootstrapRepo) - bootstrapConf := utils.GitClone(t, "CSR", BootstrapRepo, "", gcpBootstrapPath, cbProjectID, c.Logger) - buildExecutor := NewGCPExecutor(cbProjectID, defaultRegion, BootstrapRepo) - stageConf := StageConf{ + stageConf = StageConf{ Stage: BootstrapRepo, CICDProject: cbProjectID, DefaultRegion: defaultRegion, @@ -343,8 +263,8 @@ func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co CustomTargetDirPath: "envs/shared", GitConf: bootstrapConf, Envs: []string{"shared"}, - BuildType: BuildTypeCBCSR, - BuildExecutor: buildExecutor, + BuildType: tfvars.BuildType, + Executor: executor, } // if groups creation is enable the helper will just push the code @@ -361,6 +281,7 @@ func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co if err != nil { return err } + // Init gcp-bootstrap terraform err = s.RunStep("gcp-bootstrap.init-tf", func() error { options := &terraform.Options{ @@ -370,6 +291,7 @@ func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co RetryableTerraformErrors: testutils.RetryableTransientErrors, MaxRetries: MaxErrorRetries, TimeBetweenRetries: TimeBetweenErrorRetries, + EnvVars: envVars, } _, err := terraform.InitE(t, options) return err @@ -427,14 +349,18 @@ func DeployOrgStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo } var conf utils.GitRepo - var buildExecutor Executor + var executor Executor if c.BuildType == BuildTypeGiHub { - buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization, tfvars.GitRepos.Token) + executor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization, c.GitToken) repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, OrgRepo), "", c.Logger) + } else if tfvars.BuildType == BuildTypeGitLab { + executor = NewGitLabExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization, c.GitToken) + repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization) + conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, OrgRepo), "", c.Logger) } else { - buildExecutor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, OrgRepo) + executor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, OrgRepo) conf = utils.GitClone(t, "CSR", OrgRepo, "", filepath.Join(c.CheckoutPath, OrgRepo), outputs.CICDProject, c.Logger) } @@ -447,7 +373,7 @@ func DeployOrgStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo GitConf: conf, Envs: []string{"shared"}, BuildType: c.BuildType, - BuildExecutor: buildExecutor, + Executor: executor, } return deployStage(t, stageConf, s, c) @@ -466,14 +392,18 @@ func DeployEnvStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo } var conf utils.GitRepo - var buildExecutor Executor + var executor Executor if c.BuildType == BuildTypeGiHub { - buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments, tfvars.GitRepos.Token) + executor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments, c.GitToken) repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, EnvironmentsRepo), "", c.Logger) + } else if c.BuildType == BuildTypeGitLab { + executor = NewGitLabExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments, c.GitToken) + repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments) + conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, EnvironmentsRepo), "", c.Logger) } else { - buildExecutor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, EnvironmentsRepo) + executor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, EnvironmentsRepo) conf = utils.GitClone(t, "CSR", EnvironmentsRepo, "", filepath.Join(c.CheckoutPath, EnvironmentsRepo), outputs.CICDProject, c.Logger) } @@ -486,7 +416,7 @@ func DeployEnvStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo GitConf: conf, Envs: []string{"production", "nonproduction", "development"}, BuildType: c.BuildType, - BuildExecutor: buildExecutor, + Executor: executor, } return deployStage(t, stageConf, s, c) @@ -543,14 +473,18 @@ func DeployNetworksStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu } var conf utils.GitRepo - var buildExecutor Executor + var executor Executor if c.BuildType == BuildTypeGiHub { - buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks, tfvars.GitRepos.Token) + executor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks, c.GitToken) repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, NetworksRepo), "", c.Logger) + } else if c.BuildType == BuildTypeGitLab { + executor = NewGitLabExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks, c.GitToken) + repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks) + conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, NetworksRepo), "", c.Logger) } else { - buildExecutor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, NetworksRepo) + executor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, NetworksRepo) conf = utils.GitClone(t, "CSR", NetworksRepo, "", filepath.Join(c.CheckoutPath, NetworksRepo), outputs.CICDProject, c.Logger) } @@ -567,7 +501,7 @@ func DeployNetworksStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu GroupingUnits: []string{"envs"}, Envs: []string{"production", "nonproduction", "development"}, BuildType: c.BuildType, - BuildExecutor: buildExecutor, + Executor: executor, } return deployStage(t, stageConf, s, c) } @@ -607,16 +541,19 @@ func DeployProjectsStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu } } - // conf := utils.GitClone(t, "CSR", ProjectsRepo, "", filepath.Join(c.CheckoutPath, ProjectsRepo), outputs.CICDProject, c.Logger) var conf utils.GitRepo - var buildExecutor Executor + var executor Executor if c.BuildType == BuildTypeGiHub { - buildExecutor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects, tfvars.GitRepos.Token) + executor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects, c.GitToken) repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, ProjectsRepo), "", c.Logger) + } else if c.BuildType == BuildTypeGitLab { + executor = NewGitLabExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects, c.GitToken) + repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects) + conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, ProjectsRepo), "", c.Logger) } else { - buildExecutor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, ProjectsRepo) + executor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, ProjectsRepo) conf = utils.GitClone(t, "CSR", ProjectsRepo, "", filepath.Join(c.CheckoutPath, ProjectsRepo), outputs.CICDProject, c.Logger) } @@ -633,7 +570,7 @@ func DeployProjectsStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu GroupingUnits: []string{"business_unit_1"}, Envs: []string{"production", "nonproduction", "development"}, BuildType: c.BuildType, - BuildExecutor: buildExecutor, + Executor: executor, } return deployStage(t, stageConf, s, c) @@ -726,7 +663,7 @@ func deployStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error } err = s.RunStep(fmt.Sprintf("%s.plan", sc.Stage), func() error { - return planStage(t, sc.GitConf, sc.CICDProject, sc.DefaultRegion, sc.Repo, sc.BuildExecutor) + return planStage(t, sc.GitConf, sc.CICDProject, sc.DefaultRegion, sc.Repo, sc.Executor) }) if err != nil { return err @@ -738,7 +675,7 @@ func deployStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error if env == "shared" { aEnv = "production" } - return applyEnv(t, sc.GitConf, sc.CICDProject, sc.DefaultRegion, sc.Repo, aEnv, sc.BuildExecutor) + return applyEnv(t, sc.GitConf, sc.CICDProject, sc.DefaultRegion, sc.Repo, aEnv, sc.Executor) }) if err != nil { return err @@ -786,11 +723,13 @@ func copyStepCode(t testing.TB, conf utils.GitRepo, foundationPath, checkoutPath return err } } - if buildType == BuildTypeGiHub { + if buildType != BuildTypeCBCSR { err := utils.CopyDirectory(filepath.Join(foundationPath, "policy-library"), filepath.Join(gcpPath, "policy-library")) if err != nil { return err } + } + if buildType == BuildTypeGiHub { err = os.MkdirAll(filepath.Join(gcpPath, ".github/workflows/"), 0755) if err != nil { return err @@ -808,6 +747,16 @@ func copyStepCode(t testing.TB, conf utils.GitRepo, foundationPath, checkoutPath return err } } + if buildType == BuildTypeGitLab { + err = utils.CopyFile(filepath.Join(foundationPath, "build/gitlab-ci.yml"), filepath.Join(gcpPath, ".gitlab-ci.yml")) + if err != nil { + return err + } + err = utils.CopyFile(filepath.Join(foundationPath, "build/run_gcp_auth.sh"), filepath.Join(gcpPath, "run_gcp_auth.sh")) + if err != nil { + return err + } + } return utils.CopyFile(filepath.Join(foundationPath, "build/tf-wrapper.sh"), filepath.Join(gcpPath, "tf-wrapper.sh")) } diff --git a/helpers/foundation-deployer/stages/data.go b/helpers/foundation-deployer/stages/data.go index 171fe2885..ff1c4862f 100644 --- a/helpers/foundation-deployer/stages/data.go +++ b/helpers/foundation-deployer/stages/data.go @@ -62,6 +62,7 @@ type CommonConf struct { EnableHubAndSpoke bool DisablePrompt bool Logger *logger.Logger + GitToken string } type StageConf struct { @@ -78,7 +79,7 @@ type StageConf struct { Envs []string LocalSteps []string BuildType string - BuildExecutor Executor + Executor Executor } type BootstrapOutputs struct { @@ -142,15 +143,13 @@ type GcpGroups struct { } type GitRepos struct { - Token string `cty:"token"` - Owner string `cty:"owner"` - Bootstrap string `cty:"bootstrap"` - Organization string `cty:"organization"` - Environments string `cty:"environments"` - Networks string `cty:"networks"` - Projects string `cty:"projects"` - // AppInfra string `cty:"app_infra"` //TODO - CICDRunner *string `cty:"cicd_runner"` + Owner string `cty:"owner"` + Bootstrap string `cty:"bootstrap"` + Organization string `cty:"organization"` + Environments string `cty:"environments"` + Networks string `cty:"networks"` + Projects string `cty:"projects"` + CICDRunner *string `cty:"cicd_runner"` } type GitHubRepos struct { @@ -334,7 +333,7 @@ func GetBootstrapStepOutputs(t testing.TB, foundationPath string, buildType stri Logger: logger.Discard, NoColor: true, } - cicdProjectIDOutput := CICDProjectIdOutput + cicdProjectIDOutput := CloudBuildProjectIdOutput if buildType != BuildTypeCBCSR { cicdProjectIDOutput = CICDProjectIdOutput } diff --git a/helpers/foundation-deployer/stages/destroy.go b/helpers/foundation-deployer/stages/destroy.go index 542610862..5d60e116f 100644 --- a/helpers/foundation-deployer/stages/destroy.go +++ b/helpers/foundation-deployer/stages/destroy.go @@ -27,9 +27,13 @@ import ( "github.com/terraform-google-modules/terraform-example-foundation/test/integration/testutils" ) -func DestroyBootstrapStage(t testing.TB, s steps.Steps, c CommonConf) error { +var ( + emptyEnvVars = map[string]string{} +) + +func DestroyBootstrapStage(t testing.TB, s steps.Steps, c CommonConf, envVars map[string]string) error { - if err := forceBackendMigration(t, BootstrapRepo, "envs", "shared", c); err != nil { + if err := forceBackendMigration(t, BootstrapRepo, "envs", "shared", c, envVars); err != nil { return err } @@ -40,13 +44,13 @@ func DestroyBootstrapStage(t testing.TB, s steps.Steps, c CommonConf) error { GroupingUnits: []string{"envs"}, Envs: []string{"shared"}, } - return destroyStage(t, stageConf, s, c) + return destroyStage(t, stageConf, s, c, envVars) } // forceBackendMigration removes backend.tf file to force migration of the // terraform state from GCS to the local directory. // Before changing the backend we ensure it is has been initialized. -func forceBackendMigration(t testing.TB, repo, groupUnit, env string, c CommonConf) error { +func forceBackendMigration(t testing.TB, repo, groupUnit, env string, c CommonConf, envVars map[string]string) error { tfDir := filepath.Join(c.CheckoutPath, repo, groupUnit, env) backendF := filepath.Join(tfDir, "backend.tf") @@ -62,6 +66,7 @@ func forceBackendMigration(t testing.TB, repo, groupUnit, env string, c CommonCo RetryableTerraformErrors: testutils.RetryableTransientErrors, MaxRetries: MaxErrorRetries, TimeBetweenRetries: TimeBetweenErrorRetries, + EnvVars: envVars, } _, err := terraform.InitE(t, options) if err != nil { @@ -94,7 +99,7 @@ func DestroyOrgStage(t testing.TB, s steps.Steps, outputs BootstrapOutputs, c Co GroupingUnits: []string{"envs"}, Envs: []string{"shared"}, } - return destroyStage(t, stageConf, s, c) + return destroyStage(t, stageConf, s, c, emptyEnvVars) } func DestroyEnvStage(t testing.TB, s steps.Steps, outputs BootstrapOutputs, c CommonConf) error { @@ -107,7 +112,7 @@ func DestroyEnvStage(t testing.TB, s steps.Steps, outputs BootstrapOutputs, c Co GroupingUnits: []string{"envs"}, Envs: []string{"development", "nonproduction", "production"}, } - return destroyStage(t, stageConf, s, c) + return destroyStage(t, stageConf, s, c, emptyEnvVars) } func DestroyNetworksStage(t testing.TB, s steps.Steps, outputs BootstrapOutputs, c CommonConf) error { @@ -122,7 +127,7 @@ func DestroyNetworksStage(t testing.TB, s steps.Steps, outputs BootstrapOutputs, GroupingUnits: []string{"envs"}, Envs: []string{"development", "nonproduction", "production"}, } - return destroyStage(t, stageConf, s, c) + return destroyStage(t, stageConf, s, c, emptyEnvVars) } func DestroyProjectsStage(t testing.TB, s steps.Steps, outputs BootstrapOutputs, c CommonConf) error { @@ -136,7 +141,7 @@ func DestroyProjectsStage(t testing.TB, s steps.Steps, outputs BootstrapOutputs, GroupingUnits: []string{"business_unit_1"}, Envs: []string{"development", "nonproduction", "production"}, } - return destroyStage(t, stageConf, s, c) + return destroyStage(t, stageConf, s, c, emptyEnvVars) } func DestroyExampleAppStage(t testing.TB, s steps.Steps, outputs InfraPipelineOutputs, c CommonConf) error { @@ -149,10 +154,10 @@ func DestroyExampleAppStage(t testing.TB, s steps.Steps, outputs InfraPipelineOu GroupingUnits: []string{"business_unit_1"}, Envs: []string{"development", "nonproduction", "production"}, } - return destroyStage(t, stageConf, s, c) + return destroyStage(t, stageConf, s, c, emptyEnvVars) } -func destroyStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error { +func destroyStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf, envVars map[string]string) error { gcpPath := filepath.Join(c.CheckoutPath, sc.Repo) for _, e := range sc.Envs { err := s.RunDestroyStep(fmt.Sprintf("%s.%s", sc.Repo, e), func() error { @@ -164,8 +169,9 @@ func destroyStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error RetryableTerraformErrors: testutils.RetryableTransientErrors, MaxRetries: MaxErrorRetries, TimeBetweenRetries: TimeBetweenErrorRetries, + EnvVars: envVars, } - conf := utils.GitClone(t, "CSR", sc.Repo, "", gcpPath, sc.CICDProject, c.Logger) + conf := utils.GetRepoOnly(t, gcpPath, c.Logger) branch := e if branch == "shared" { branch = "production" @@ -198,8 +204,9 @@ func destroyStage(t testing.TB, sc StageConf, s steps.Steps, c CommonConf) error RetryableTerraformErrors: testutils.RetryableTransientErrors, MaxRetries: MaxErrorRetries, TimeBetweenRetries: TimeBetweenErrorRetries, + EnvVars: envVars, } - conf := utils.GitClone(t, "CSR", ProjectsRepo, "", gcpPath, sc.CICDProject, c.Logger) + conf := utils.GetRepoOnly(t, gcpPath, c.Logger) err := conf.CheckoutBranch("production") if err != nil { return err diff --git a/helpers/foundation-deployer/stages/executor.go b/helpers/foundation-deployer/stages/executor.go index a33f48e9b..658d31167 100644 --- a/helpers/foundation-deployer/stages/executor.go +++ b/helpers/foundation-deployer/stages/executor.go @@ -18,6 +18,7 @@ import ( "github.com/mitchellh/go-testing-interface" "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/gcp" "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/github" + "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/gitlab" ) type Executor interface { @@ -63,3 +64,23 @@ func NewGitHubExecutor(owner, repo, token string) *GitHubExecutor { func (e *GitHubExecutor) WaitBuildSuccess(t testing.TB, commitSha, failureMsg string) error { return e.executor.WaitBuildSuccess(t, e.owner, e.repo, e.token, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) } + +type GitLabExecutor struct { + executor gitlab.GL + owner string + project string + token string +} + +func NewGitLabExecutor(owner, project, token string) *GitLabExecutor { + return &GitLabExecutor{ + executor: gitlab.NewGL(), + owner: owner, + project: project, + token: token, + } +} + +func (e *GitLabExecutor) WaitBuildSuccess(t testing.TB, commitSha, failureMsg string) error { + return e.executor.WaitBuildSuccess(t, e.owner, e.project, e.token, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) +} diff --git a/helpers/foundation-deployer/stages/validate.go b/helpers/foundation-deployer/stages/validate.go index 41be508dc..9ab20dbc1 100644 --- a/helpers/foundation-deployer/stages/validate.go +++ b/helpers/foundation-deployer/stages/validate.go @@ -111,16 +111,6 @@ func ValidateBasicFields(t testing.TB, g GlobalTFVars) { } } -func ValidateGitRepos(t testing.TB, g GlobalTFVars) { - // - - if g.BuildType == BuildTypeGiHub { - // check if owner was provided - // check if all repo was provided - // test if token is valid - } -} - // ValidateDestroyFlags checks if the flags to allow the destruction of the infrastructure are enabled func ValidateDestroyFlags(t testing.TB, g GlobalTFVars) { trueFlags := []string{} diff --git a/helpers/foundation-deployer/utils/git.go b/helpers/foundation-deployer/utils/git.go index cfb208a1b..46b1552be 100644 --- a/helpers/foundation-deployer/utils/git.go +++ b/helpers/foundation-deployer/utils/git.go @@ -34,13 +34,10 @@ type GitRepo struct { // GitClone clones git repositories, supporting CSR, Github and Gitlab type of source control func GitClone(t testing.TB, repositoryType, repositoryName, repositoryURL, path, project string, logger *logger.Logger) GitRepo { - conf := GitRepo{} - if repositoryType != "CSR" { - conf = cloneGit(t, repositoryURL, path, logger) - } else { - conf = cloneCSR(t, repositoryName, path, project, logger) + if repositoryType == "CSR" { + return cloneCSR(t, repositoryName, path, project, logger) } - return conf + return cloneGit(t, repositoryURL, path, logger) } // CloneCSR clones a Google Cloud Source repository and returns a CmdConfig pointing to the repository. @@ -54,6 +51,12 @@ func cloneCSR(t testing.TB, repositoryName, path, project string, logger *logger } } +func GetRepoOnly(t testing.TB, path string, logger *logger.Logger) GitRepo { + return GitRepo{ + conf: git.NewCmdConfig(t, git.WithDir(path), git.WithLogger(logger)), + } +} + // cloneGit clones a Github or Gitlab repository and returns a CmdConfig pointing to the repository. func cloneGit(t testing.TB, repositoryUrl, path string, logger *logger.Logger) GitRepo { _, err := os.Stat(path) @@ -148,6 +151,10 @@ func BuildGitHubURL(owner, repoName string) string { return fmt.Sprintf("https://github.com/%s/%s.git", owner, repoName) } +func BuildGitLabURL(owner, repoName string) string { + return fmt.Sprintf("https://gitlab.com/%s/%s.git", owner, repoName) +} + // ExtractRepoNameFromGitHubURL parses a GitHub URL and returns the repository name. func ExtractRepoNameFromGitHubURL(githubURL string) (string, error) { parsedURL, err := url.Parse(githubURL) diff --git a/helpers/foundation-deployer/utils/retry.go b/helpers/foundation-deployer/utils/retry.go new file mode 100644 index 000000000..31e95047e --- /dev/null +++ b/helpers/foundation-deployer/utils/retry.go @@ -0,0 +1,38 @@ +package utils + +import ( + "fmt" + "regexp" + + "github.com/terraform-google-modules/terraform-example-foundation/test/integration/testutils" + + "github.com/mitchellh/go-testing-interface" +) + +var ( + retryRegexp = map[*regexp.Regexp]string{} +) + +func init() { + for e, m := range testutils.RetryableTransientErrors { + r, err := regexp.Compile(fmt.Sprintf("(?s)%s", e)) //(?s) enables dot (.) to match newline. + if err != nil { + panic(fmt.Sprintf("failed to compile regex %s: %s", e, err.Error())) + } + retryRegexp[r] = m + } +} + +// IsRetryableError checks the logs of a failed execution +// and verify if the error is a transient one and can be retried +func IsRetryableError(t testing.TB, logs string) bool { + found := false + for pattern, msg := range retryRegexp { + if pattern.MatchString(logs) { + found = true + fmt.Printf("error '%s' is worth of a retry\n", msg) + break + } + } + return found +} From 13716d2cd0d21b062c63b79c18fc910785109b92 Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Thu, 6 Nov 2025 20:36:25 -0300 Subject: [PATCH 08/12] code review fixes --- 0-bootstrap/scripts/choose_build_type.sh | 10 +-- helpers/foundation-deployer/github/github.go | 58 +++++++--------- helpers/foundation-deployer/gitlab/gitlab.go | 35 +++++----- helpers/foundation-deployer/main.go | 2 +- helpers/foundation-deployer/stages/apply.go | 67 +++++++++++-------- .../foundation-deployer/stages/executor.go | 1 - .../foundation-deployer/utils/files_test.go | 2 - helpers/foundation-deployer/utils/retry.go | 15 +++++ test/integration/testutils/api.go | 4 +- 9 files changed, 98 insertions(+), 96 deletions(-) diff --git a/0-bootstrap/scripts/choose_build_type.sh b/0-bootstrap/scripts/choose_build_type.sh index 27bfad21b..d593b5faa 100755 --- a/0-bootstrap/scripts/choose_build_type.sh +++ b/0-bootstrap/scripts/choose_build_type.sh @@ -23,15 +23,7 @@ TARGET_BUILD="$1" build_types=("cb" "github" "gitlab" "jenkins" "terraform_cloud") # Validate the build_type input -valid_build_type=false -for valid_type in "${build_types[@]}"; do - if [ "$TARGET_BUILD" = "$valid_type" ]; then - valid_build_type=true - break - fi -done - -if [ "$valid_build_type" = false ]; then +if [[ ! " ${build_types[*]} " =~ " ${TARGET_BUILD} " ]]; then echo "Error: Invalid build type '$TARGET_BUILD'. Must be one of: ${build_types[*]}" exit 1 fi diff --git a/helpers/foundation-deployer/github/github.go b/helpers/foundation-deployer/github/github.go index 1a5511502..70b6bb6be 100644 --- a/helpers/foundation-deployer/github/github.go +++ b/helpers/foundation-deployer/github/github.go @@ -19,14 +19,13 @@ import ( "fmt" "io" "net/http" - "regexp" + "os" "time" "github.com/google/go-github/v58/github" "github.com/mitchellh/go-testing-interface" - "github.com/terraform-google-modules/terraform-example-foundation/test/integration/testutils" - localutil "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/utils" + "github.com/terraform-google-modules/terraform-example-foundation/helpers/foundation-deployer/utils" "golang.org/x/oauth2" ) @@ -46,20 +45,6 @@ const ( StatusActionRequired = "action_required" ) -var ( - retryRegexp = map[*regexp.Regexp]string{} -) - -func init() { - for e, m := range testutils.RetryableTransientErrors { - r, err := regexp.Compile(fmt.Sprintf("(?s)%s", e)) //(?s) enables dot (.) to match newline. - if err != nil { - panic(fmt.Sprintf("failed to compile regex %s: %s", e, err.Error())) - } - retryRegexp[r] = m - } -} - type GH struct { TriggerNewBuild func(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (int64, string, string, error) sleepTime time.Duration @@ -72,6 +57,7 @@ func NewGH() GH { sleepTime: 20, } } + // triggerNewBuild triggers a new action execution func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (int64, string, string, error) { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) @@ -80,14 +66,14 @@ func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token strin resp, err := client.Actions.RerunWorkflowByID(ctx, owner, repo, runID) if err != nil { - return 0, "", "", fmt.Errorf("Error re-running workflow: %v", err) + return 0, "", "", fmt.Errorf("error re-running workflow: %v", err) } if resp.StatusCode != http.StatusCreated { bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return 0, "", "", fmt.Errorf("Error re-running workflow status: %d body parsing error: %v", resp.StatusCode, err) + return 0, "", "", fmt.Errorf("error re-running workflow status: %d body parsing error: %v", resp.StatusCode, err) } - return 0, "", "", fmt.Errorf("Error re-running workflow status: %d body: %s", resp.StatusCode, string(bodyBytes)) + return 0, "", "", fmt.Errorf("error re-running workflow status: %d body: %s", resp.StatusCode, string(bodyBytes)) } opts := &github.ListWorkflowRunsOptions{ @@ -103,11 +89,11 @@ func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token strin runs, _, err := client.Actions.ListRepositoryWorkflowRuns(ctx, owner, repo, opts) if err != nil { - return 0, "", "", fmt.Errorf("Error listing workflow runs: %v", err) + return 0, "", "", fmt.Errorf("error listing workflow runs: %v", err) } if len(runs.WorkflowRuns) == 0 { - return 0, "", "", fmt.Errorf("No workflow runs found for repo %s/%s ", owner, repo) + return 0, "", "", fmt.Errorf("no workflow runs found for repo %s/%s", owner, repo) } var newRunID int64 @@ -143,7 +129,7 @@ func (g GH) GetLastActionState(t testing.TB, ctx context.Context, owner, repo, t runs, _, err := client.Actions.ListRepositoryWorkflowRuns(ctx, owner, repo, opts) if err != nil { - return 0, "", "", fmt.Errorf("Error listing workflow runs: %v", err) + return 0, "", "", fmt.Errorf("error listing workflow runs: %v", err) } if len(runs.WorkflowRuns) == 0 { @@ -175,7 +161,7 @@ func (g GH) GetActionState(t testing.TB, ctx context.Context, owner, repo, token run, _, err := client.Actions.GetWorkflowRunByID(ctx, owner, repo, runID) if err != nil { - return "", "", fmt.Errorf("Error getting workflow run: %v", err) + return "", "", fmt.Errorf("error getting workflow run: %v", err) } var status string @@ -201,7 +187,7 @@ func (g GH) GetBuildLogs(t testing.TB, ctx context.Context, owner, repo, token s } jobs, _, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, listJobsOpts) if err != nil { - return "", fmt.Errorf("Error listing jobs for run %d: %v", runID, err) + return "", fmt.Errorf("error listing jobs for run %d: %v", runID, err) } for _, job := range jobs.Jobs { @@ -214,25 +200,31 @@ func (g GH) GetBuildLogs(t testing.TB, ctx context.Context, owner, repo, token s logURL, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 3) if err != nil { - return "", fmt.Errorf(" ERROR: Could not get log URL for job %d: %v\n", jobID, err) + return "", fmt.Errorf("error: Could not get log URL for job %d: %v", jobID, err) } if resp.StatusCode != http.StatusFound { - return "", fmt.Errorf(" ERROR: Expected a 302 redirect for job logs, but got %s\n", resp.Status) + return "", fmt.Errorf("error: Expected a 302 redirect for job logs, but got %s", resp.Status) } logContentResp, err := http.Get(logURL.String()) if err != nil { - return "", fmt.Errorf(" ERROR: Could not download logs from %s: %v\n", logURL, err) + return "", fmt.Errorf("error: Could not download logs from %s: %v", logURL, err) } - defer logContentResp.Body.Close() + + defer func() { + err := logContentResp.Body.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "error closing execution log file: %s", err) + } + }() if logContentResp.StatusCode != http.StatusOK { - return "", fmt.Errorf(" ERROR: Expected status 200 OK from log URL, but got %s\n", logContentResp.Status) + return "", fmt.Errorf("error: Expected status 200 OK from log URL, but got %s", logContentResp.Status) } bodyBytes, err := io.ReadAll(logContentResp.Body) if err != nil { - return "", fmt.Errorf("Error reading response body: %v", err) + return "", fmt.Errorf("error reading response body: %v", err) } return string(bodyBytes), nil } @@ -283,7 +275,7 @@ func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token string, failureMsg } for i := 0; i < maxErrorRetries; i++ { if status != statusCompleted { - status, conclusion, err = g.GetFinalActionState(t, ctx, owner, repo, token, runID, maxBuildRetry) + _, conclusion, err = g.GetFinalActionState(t, ctx, owner, repo, token, runID, maxBuildRetry) if err != nil { return err } @@ -294,7 +286,7 @@ func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token string, failureMsg if err != nil { return err } - if localutil.IsRetryableError(t, logs) { + if utils.IsRetryableError(t, logs) { return fmt.Errorf("%s\nSee:\nhttps://github.com/%s/%s/actions/runs/%d\nfor details", failureMsg, owner, repo, runID) } fmt.Println("build failed with retryable error. a new build will be triggered.") diff --git a/helpers/foundation-deployer/gitlab/gitlab.go b/helpers/foundation-deployer/gitlab/gitlab.go index 8d9cccb10..6bf25cd9b 100644 --- a/helpers/foundation-deployer/gitlab/gitlab.go +++ b/helpers/foundation-deployer/gitlab/gitlab.go @@ -19,7 +19,6 @@ import ( "fmt" "io" "net/http" - "regexp" "time" "github.com/mitchellh/go-testing-interface" @@ -43,10 +42,6 @@ const ( StatusCanceling = "canceling" ) -var ( - retryRegexp = map[*regexp.Regexp]string{} -) - type GL struct { TriggerNewBuild func(t testing.TB, ctx context.Context, owner, project, token string, jobID int) (int, error) sleepTime time.Duration @@ -65,12 +60,12 @@ func triggerNewBuild(t testing.TB, ctx context.Context, owner, project, token st git, err := gitlab.NewClient(token) if err != nil { - return 0, fmt.Errorf("Failed to create client: %v", err) + return 0, fmt.Errorf("failed to create client: %v", err) } job, resp, err := git.Jobs.RetryJob(fmt.Sprintf("%s/%s", owner, project), jobID, gitlab.WithContext(ctx)) if err != nil { - return 0, fmt.Errorf("Error retrying job: %v", err) + return 0, fmt.Errorf("error retrying job: %v", err) } if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode <= http.StatusNetworkAuthenticationRequired { bodyBytes, err := io.ReadAll(resp.Body) @@ -86,7 +81,7 @@ func triggerNewBuild(t testing.TB, ctx context.Context, owner, project, token st func (g GL) GetLastJobStatus(t testing.TB, ctx context.Context, owner, project, token string) (string, int, error) { git, err := gitlab.NewClient(token) if err != nil { - return "", 0, fmt.Errorf("Failed to create client: %v", err) + return "", 0, fmt.Errorf("failed to create client: %v", err) } includeRetried := true @@ -102,11 +97,11 @@ func (g GL) GetLastJobStatus(t testing.TB, ctx context.Context, owner, project, jobs, _, err := git.Jobs.ListProjectJobs(fmt.Sprintf("%s/%s", owner, project), opts, gitlab.WithContext(ctx)) if err != nil { - return "", 0, fmt.Errorf("Error listing project jobs: %v", err) + return "", 0, fmt.Errorf("error listing project jobs: %v", err) } if len(jobs) == 0 { - return "", 0, fmt.Errorf("No jobs found for project: %s/%s", owner, project) + return "", 0, fmt.Errorf("no jobs found for project: %s/%s", owner, project) } return jobs[0].Status, jobs[0].ID, nil @@ -116,21 +111,21 @@ func (g GL) GetLastJobStatus(t testing.TB, ctx context.Context, owner, project, func (g GL) GetJobLogs(t testing.TB, ctx context.Context, owner, project, token string, jobID int) (string, error) { git, err := gitlab.NewClient(token) if err != nil { - return "", fmt.Errorf("Failed to create client: %v", err) + return "", fmt.Errorf("failed to create client: %v", err) } reader, resp, err := git.Jobs.GetTraceFile(fmt.Sprintf("%s/%s", owner, project), jobID) if err != nil { - return "", fmt.Errorf("Error getting job trace file: %v", err) + return "", fmt.Errorf("error getting job trace file: %v", err) } if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("Expected status 200 OK, but got %s for job %d trace", resp.Status, jobID) + return "", fmt.Errorf("expected status 200 OK, but got %s for job %d trace", resp.Status, jobID) } logBytes, err := io.ReadAll(reader) if err != nil { - return "", fmt.Errorf("Failed to read job %d trace from bytes.Reader: %v", jobID, err) + return "", fmt.Errorf("failed to read job %d trace from bytes.Reader: %v", jobID, err) } return string(logBytes), nil @@ -140,12 +135,12 @@ func (g GL) GetJobLogs(t testing.TB, ctx context.Context, owner, project, token func (g GL) GetJobStatus(t testing.TB, ctx context.Context, owner, project, token string, jobID int) (string, error) { git, err := gitlab.NewClient(token) if err != nil { - return "", fmt.Errorf("Failed to create client: %v", err) + return "", fmt.Errorf("failed to create client: %v", err) } job, _, err := git.Jobs.GetJob(fmt.Sprintf("%s/%s", owner, project), jobID, gitlab.WithContext(ctx)) if err != nil { - return "", fmt.Errorf("Error retrying job: %v", err) + return "", fmt.Errorf("error retrying job: %v", err) } return job.Status, nil @@ -232,7 +227,7 @@ func (g GL) AddProjectsToJobTokenScope(t testing.TB, owner, cicdProject, token s ctx := context.Background() git, err := gitlab.NewClient(token) if err != nil { - return fmt.Errorf("Failed to create client: %v", err) + return fmt.Errorf("failed to create client: %v", err) } runnerProjectPath := fmt.Sprintf("%s/%s", owner, cicdProject) @@ -243,13 +238,13 @@ func (g GL) AddProjectsToJobTokenScope(t testing.TB, owner, cicdProject, token s runnerProjectID, err := getProjectIDByPath(git, ctx, runnerProjectPath) if err != nil { - return fmt.Errorf("Could not get the project ID for the runner repository. Aborting. Error: %v", err) + return fmt.Errorf("could not get the project ID for the runner repository. Aborting. Error: %v", err) } for _, targetProjectPath := range projectsToAdd { targetProjectID, err := getProjectIDByPath(git, ctx, targetProjectPath) if err != nil { - return fmt.Errorf("Could not find project ID. Error: %v", err) + return fmt.Errorf("could not find project ID. Error: %v", err) } opts := &gitlab.JobTokenInboundAllowOptions{ @@ -266,7 +261,7 @@ func (g GL) AddProjectsToJobTokenScope(t testing.TB, owner, cicdProject, token s } if resp.StatusCode != http.StatusCreated { - return fmt.Errorf("DONE (Status: %s)\n", resp.Status) + return fmt.Errorf("done (Status: %s)", resp.Status) } } diff --git a/helpers/foundation-deployer/main.go b/helpers/foundation-deployer/main.go index 4f0cd001c..4b9f972d6 100644 --- a/helpers/foundation-deployer/main.go +++ b/helpers/foundation-deployer/main.go @@ -188,7 +188,7 @@ func main() { "TF_VAR_gh_token": conf.GitToken, } } - if conf.BuildType != stages.BuildTypeGitLab { + if conf.BuildType == stages.BuildTypeGitLab { envVars = map[string]string{ "TF_VAR_gitlab_token": conf.GitToken, } diff --git a/helpers/foundation-deployer/stages/apply.go b/helpers/foundation-deployer/stages/apply.go index 3e15e09f2..6b50feb60 100644 --- a/helpers/foundation-deployer/stages/apply.go +++ b/helpers/foundation-deployer/stages/apply.go @@ -37,9 +37,12 @@ func buildGitLabCICDImage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner) conf := utils.GitClone(t, tfvars.BuildType, "", repoURL, cicdPath, "", c.Logger) - conf.CheckoutBranch("image") + err := conf.CheckoutBranch("image") + if err != nil { + return err + } - err := utils.CopyFile(filepath.Join(c.FoundationPath, "build/gitlab-ci.yml"), filepath.Join(cicdPath, ".gitlab-ci.yml")) + err = utils.CopyFile(filepath.Join(c.FoundationPath, "build/gitlab-ci.yml"), filepath.Join(cicdPath, ".gitlab-ci.yml")) if err != nil { return err } @@ -129,7 +132,10 @@ func DeployBootstrapStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co } } - utils.RenameBuildFiles(filepath.Join(c.FoundationPath, BootstrapStep), tfvars.BuildType) + err = utils.RenameBuildFiles(filepath.Join(c.FoundationPath, BootstrapStep), tfvars.BuildType) + if err != nil { + return err + } err = utils.WriteTfvars(filepath.Join(c.FoundationPath, BootstrapStep, "terraform.tfvars"), bootstrapTfvars) if err != nil { @@ -351,15 +357,16 @@ func DeployOrgStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo var conf utils.GitRepo var executor Executor - if c.BuildType == BuildTypeGiHub { + switch c.BuildType { + case BuildTypeGiHub: executor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization, c.GitToken) repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, OrgRepo), "", c.Logger) - } else if tfvars.BuildType == BuildTypeGitLab { + case BuildTypeGitLab: executor = NewGitLabExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization, c.GitToken) repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Organization) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, OrgRepo), "", c.Logger) - } else { + default: executor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, OrgRepo) conf = utils.GitClone(t, "CSR", OrgRepo, "", filepath.Join(c.CheckoutPath, OrgRepo), outputs.CICDProject, c.Logger) } @@ -394,15 +401,16 @@ func DeployEnvStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs Bo var conf utils.GitRepo var executor Executor - if c.BuildType == BuildTypeGiHub { + switch c.BuildType { + case BuildTypeGiHub: executor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments, c.GitToken) repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, EnvironmentsRepo), "", c.Logger) - } else if c.BuildType == BuildTypeGitLab { + case BuildTypeGitLab: executor = NewGitLabExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments, c.GitToken) repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Environments) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, EnvironmentsRepo), "", c.Logger) - } else { + default: executor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, EnvironmentsRepo) conf = utils.GitClone(t, "CSR", EnvironmentsRepo, "", filepath.Join(c.CheckoutPath, EnvironmentsRepo), outputs.CICDProject, c.Logger) } @@ -475,15 +483,16 @@ func DeployNetworksStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu var conf utils.GitRepo var executor Executor - if c.BuildType == BuildTypeGiHub { + switch c.BuildType { + case BuildTypeGiHub: executor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks, c.GitToken) repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, NetworksRepo), "", c.Logger) - } else if c.BuildType == BuildTypeGitLab { + case BuildTypeGitLab: executor = NewGitLabExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks, c.GitToken) repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Networks) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, NetworksRepo), "", c.Logger) - } else { + default: executor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, NetworksRepo) conf = utils.GitClone(t, "CSR", NetworksRepo, "", filepath.Join(c.CheckoutPath, NetworksRepo), outputs.CICDProject, c.Logger) } @@ -544,15 +553,16 @@ func DeployProjectsStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu var conf utils.GitRepo var executor Executor - if c.BuildType == BuildTypeGiHub { + switch c.BuildType { + case BuildTypeGiHub: executor = NewGitHubExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects, c.GitToken) repoURL := utils.BuildGitHubURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, ProjectsRepo), "", c.Logger) - } else if c.BuildType == BuildTypeGitLab { + case BuildTypeGitLab: executor = NewGitLabExecutor(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects, c.GitToken) repoURL := utils.BuildGitLabURL(tfvars.GitRepos.Owner, tfvars.GitRepos.Projects) conf = utils.GitClone(t, tfvars.BuildType, "", repoURL, filepath.Join(c.CheckoutPath, ProjectsRepo), "", c.Logger) - } else { + default: executor = NewGCPExecutor(outputs.CICDProject, outputs.DefaultRegion, ProjectsRepo) conf = utils.GitClone(t, "CSR", ProjectsRepo, "", filepath.Join(c.CheckoutPath, ProjectsRepo), outputs.CICDProject, c.Logger) } @@ -708,28 +718,21 @@ func copyStepCode(t testing.TB, conf utils.GitRepo, foundationPath, checkoutPath if customPath != "" { targetDir = filepath.Join(gcpPath, customPath) } + err := utils.CopyDirectory(filepath.Join(foundationPath, step), targetDir) if err != nil { return err } - if buildType == BuildTypeCBCSR { - err = utils.CopyFile(filepath.Join(foundationPath, "build/cloudbuild-tf-apply.yaml"), filepath.Join(gcpPath, "cloudbuild-tf-apply.yaml")) - if err != nil { - return err - } - err = utils.CopyFile(filepath.Join(foundationPath, "build/cloudbuild-tf-plan.yaml"), filepath.Join(gcpPath, "cloudbuild-tf-plan.yaml")) - if err != nil { - return err - } - } if buildType != BuildTypeCBCSR { err := utils.CopyDirectory(filepath.Join(foundationPath, "policy-library"), filepath.Join(gcpPath, "policy-library")) if err != nil { return err } } - if buildType == BuildTypeGiHub { + + switch buildType { + case BuildTypeGiHub: err = os.MkdirAll(filepath.Join(gcpPath, ".github/workflows/"), 0755) if err != nil { return err @@ -746,8 +749,7 @@ func copyStepCode(t testing.TB, conf utils.GitRepo, foundationPath, checkoutPath if err != nil { return err } - } - if buildType == BuildTypeGitLab { + case BuildTypeGitLab: err = utils.CopyFile(filepath.Join(foundationPath, "build/gitlab-ci.yml"), filepath.Join(gcpPath, ".gitlab-ci.yml")) if err != nil { return err @@ -756,6 +758,15 @@ func copyStepCode(t testing.TB, conf utils.GitRepo, foundationPath, checkoutPath if err != nil { return err } + default: //BuildTypeCBCSR + err = utils.CopyFile(filepath.Join(foundationPath, "build/cloudbuild-tf-apply.yaml"), filepath.Join(gcpPath, "cloudbuild-tf-apply.yaml")) + if err != nil { + return err + } + err = utils.CopyFile(filepath.Join(foundationPath, "build/cloudbuild-tf-plan.yaml"), filepath.Join(gcpPath, "cloudbuild-tf-plan.yaml")) + if err != nil { + return err + } } return utils.CopyFile(filepath.Join(foundationPath, "build/tf-wrapper.sh"), filepath.Join(gcpPath, "tf-wrapper.sh")) diff --git a/helpers/foundation-deployer/stages/executor.go b/helpers/foundation-deployer/stages/executor.go index 658d31167..573dff86d 100644 --- a/helpers/foundation-deployer/stages/executor.go +++ b/helpers/foundation-deployer/stages/executor.go @@ -46,7 +46,6 @@ func NewGCPExecutor(project, region, repo string) *GCPExecutor { type GitHubExecutor struct { executor github.GH - url string owner string repo string token string diff --git a/helpers/foundation-deployer/utils/files_test.go b/helpers/foundation-deployer/utils/files_test.go index 32f9785cb..9dcd44165 100644 --- a/helpers/foundation-deployer/utils/files_test.go +++ b/helpers/foundation-deployer/utils/files_test.go @@ -23,8 +23,6 @@ import ( "github.com/stretchr/testify/assert" ) -var testBuildTypes = []string{"cb", "github", "gitlab", "jenkins", "terraform_cloud"} - func writeTempFile(dir, name, content string) (string, error) { f := filepath.Join(dir, name) err := os.WriteFile(f, []byte(content), 0644) diff --git a/helpers/foundation-deployer/utils/retry.go b/helpers/foundation-deployer/utils/retry.go index 31e95047e..6c802bf37 100644 --- a/helpers/foundation-deployer/utils/retry.go +++ b/helpers/foundation-deployer/utils/retry.go @@ -1,3 +1,18 @@ + +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package utils import ( diff --git a/test/integration/testutils/api.go b/test/integration/testutils/api.go index 3672ce1cd..a047c153e 100644 --- a/test/integration/testutils/api.go +++ b/test/integration/testutils/api.go @@ -46,8 +46,8 @@ func CheckAPIEnabled(t *testing.T, projectID, api string) (bool, error) { resultName := result.Get("name").String() resultState := result.Get("state").String() if !strings.Contains(resultName, api) || resultState != "ENABLED" { - return true, fmt.Errorf("API %s is not enabled in project %s", api, projectID) + return true, fmt.Errorf("the API %s is not enabled in project %s", api, projectID) } - logger.Log(t, fmt.Sprintf("API %s is enabled in project %s", api, projectID)) + logger.Log(t, fmt.Sprintf("the API %s is enabled in project %s", api, projectID)) return false, nil } From 7b9330c49a085c8304b16279a97d46e150f5d690 Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Thu, 6 Nov 2025 22:12:19 -0300 Subject: [PATCH 09/12] code review fixes --- 0-bootstrap/scripts/choose_build_type.sh | 2 +- helpers/foundation-deployer/github/github.go | 18 ++++++++++-------- helpers/foundation-deployer/stages/executor.go | 2 +- helpers/foundation-deployer/utils/git.go | 10 ++++------ 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/0-bootstrap/scripts/choose_build_type.sh b/0-bootstrap/scripts/choose_build_type.sh index d593b5faa..72ae5f26d 100755 --- a/0-bootstrap/scripts/choose_build_type.sh +++ b/0-bootstrap/scripts/choose_build_type.sh @@ -23,7 +23,7 @@ TARGET_BUILD="$1" build_types=("cb" "github" "gitlab" "jenkins" "terraform_cloud") # Validate the build_type input -if [[ ! " ${build_types[*]} " =~ " ${TARGET_BUILD} " ]]; then +if [[ ! " ${build_types[*]} " == *" ${TARGET_BUILD} "* ]]; then echo "Error: Invalid build type '$TARGET_BUILD'. Must be one of: ${build_types[*]}" exit 1 fi diff --git a/helpers/foundation-deployer/github/github.go b/helpers/foundation-deployer/github/github.go index 70b6bb6be..a80ce5f4b 100644 --- a/helpers/foundation-deployer/github/github.go +++ b/helpers/foundation-deployer/github/github.go @@ -46,7 +46,7 @@ const ( ) type GH struct { - TriggerNewBuild func(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (int64, string, string, error) + TriggerNewBuild func(t testing.TB, ctx context.Context, owner, repo, token, commitSha string, runID int64) (int64, string, string, error) sleepTime time.Duration } @@ -59,7 +59,7 @@ func NewGH() GH { } // triggerNewBuild triggers a new action execution -func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token string, runID int64) (int64, string, string, error) { +func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token, commitSha string, runID int64) (int64, string, string, error) { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) tc := oauth2.NewClient(ctx, ts) client := github.NewClient(tc) @@ -75,8 +75,9 @@ func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token strin } return 0, "", "", fmt.Errorf("error re-running workflow status: %d body: %s", resp.StatusCode, string(bodyBytes)) } - opts := &github.ListWorkflowRunsOptions{ + opts := &github.ListWorkflowRunsOptions{ + HeadSHA: commitSha, ListOptions: github.ListOptions{ PerPage: 1, Page: 1, @@ -115,17 +116,18 @@ func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token strin } // GetLastActionState returns the state of the latest action -func (g GH) GetLastActionState(t testing.TB, ctx context.Context, owner, repo, token string) (int64, string, string, error) { +func (g GH) GetLastActionState(t testing.TB, ctx context.Context, owner, repo, token, commitSha string) (int64, string, string, error) { ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) tc := oauth2.NewClient(ctx, ts) client := github.NewClient(tc) opts := &github.ListWorkflowRunsOptions{ - + HeadSHA: commitSha, ListOptions: github.ListOptions{ PerPage: 1, }, } + runs, _, err := client.Actions.ListRepositoryWorkflowRuns(ctx, owner, repo, opts) if err != nil { @@ -260,7 +262,7 @@ func (g GH) GetFinalActionState(t testing.TB, ctx context.Context, owner, repo, } // WaitBuildSuccess waits for the current build in a repo to finish. -func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token string, failureMsg string, maxBuildRetry, maxErrorRetries int, timeBetweenErrorRetries time.Duration) error { +func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token, commitSha, failureMsg string, maxBuildRetry, maxErrorRetries int, timeBetweenErrorRetries time.Duration) error { var status, conclusion string var runID int64 var err error @@ -269,7 +271,7 @@ func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token string, failureMsg // wait action creation time.Sleep(30 * time.Second) - runID, status, conclusion, err = g.GetLastActionState(t, ctx, owner, repo, token) + runID, status, conclusion, err = g.GetLastActionState(t, ctx, owner, repo, token, commitSha) if err != nil { return err } @@ -295,7 +297,7 @@ func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token string, failureMsg } // Trigger a new build - runID, status, conclusion, err = g.TriggerNewBuild(t, ctx, owner, repo, token, runID) + runID, status, conclusion, err = g.TriggerNewBuild(t, ctx, owner, repo, token, commitSha, runID) if err != nil { return fmt.Errorf("failed to trigger new action (attempt %d/%d): %w", i+1, maxErrorRetries, err) } diff --git a/helpers/foundation-deployer/stages/executor.go b/helpers/foundation-deployer/stages/executor.go index 573dff86d..6b5fbe5f4 100644 --- a/helpers/foundation-deployer/stages/executor.go +++ b/helpers/foundation-deployer/stages/executor.go @@ -61,7 +61,7 @@ func NewGitHubExecutor(owner, repo, token string) *GitHubExecutor { } func (e *GitHubExecutor) WaitBuildSuccess(t testing.TB, commitSha, failureMsg string) error { - return e.executor.WaitBuildSuccess(t, e.owner, e.repo, e.token, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) + return e.executor.WaitBuildSuccess(t, e.owner, e.repo, e.token, commitSha, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) } type GitLabExecutor struct { diff --git a/helpers/foundation-deployer/utils/git.go b/helpers/foundation-deployer/utils/git.go index 46b1552be..8e68870a0 100644 --- a/helpers/foundation-deployer/utils/git.go +++ b/helpers/foundation-deployer/utils/git.go @@ -63,15 +63,13 @@ func cloneGit(t testing.TB, repositoryUrl, path string, logger *logger.Logger) G if os.IsNotExist(err) { cmd := exec.Command("git", "clone", repositoryUrl, path) - fmt.Printf("Executing command %s", cmd) - // Run the command and capture its standard output - output, err := cmd.Output() + fmt.Printf("Executing command %s\n", cmd.String()) + // Run the command and capture its combined output + output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("Error executing git command: %v", err) } - // Convert the output to a string and trim whitespace - branchName := strings.TrimSpace(string(output)) - fmt.Printf("Current Git branch: %s\n", branchName) + fmt.Printf("git clone output:\n%s\n", string(output)) } return GitRepo{ From 40a0db61f71cd76edf0d4174431315532185f4dc Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Fri, 7 Nov 2025 12:00:09 -0300 Subject: [PATCH 10/12] use commit SHA in github and gitlab job search --- helpers/foundation-deployer/gitlab/gitlab.go | 38 +++++++++++++++---- helpers/foundation-deployer/stages/apply.go | 9 ++++- .../foundation-deployer/stages/executor.go | 2 +- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/helpers/foundation-deployer/gitlab/gitlab.go b/helpers/foundation-deployer/gitlab/gitlab.go index 6bf25cd9b..52dfdee54 100644 --- a/helpers/foundation-deployer/gitlab/gitlab.go +++ b/helpers/foundation-deployer/gitlab/gitlab.go @@ -77,15 +77,37 @@ func triggerNewBuild(t testing.TB, ctx context.Context, owner, project, token st return job.ID, nil } -// GetLastJobStatus returns the status of the latest executed job -func (g GL) GetLastJobStatus(t testing.TB, ctx context.Context, owner, project, token string) (string, int, error) { +// GetLastJobStatusForSHA finds the latest job associated with a specific commit SHA +func (g GL) GetLastJobStatusForSHA(t testing.TB, ctx context.Context, owner, project, token, sha string) (string, int, error) { git, err := gitlab.NewClient(token) if err != nil { return "", 0, fmt.Errorf("failed to create client: %v", err) } + projectPath := fmt.Sprintf("%s/%s", owner, project) + + pipelineOpts := &gitlab.ListProjectPipelinesOptions{ + ListOptions: gitlab.ListOptions{ + PerPage: 1, + Page: 1, + OrderBy: "id", + Sort: "desc", + }, + SHA: &sha, + } + + pipelines, _, err := git.Pipelines.ListProjectPipelines(projectPath, pipelineOpts, gitlab.WithContext(ctx)) + if err != nil { + return "", 0, fmt.Errorf("error listing pipelines for SHA %s: %w", sha, err) + } + + if len(pipelines) == 0 { + return "", 0, fmt.Errorf("no pipelines found for project '%s' at SHA '%s'", projectPath, sha) + } + + latestPipelineID := pipelines[0].ID includeRetried := true - opts := &gitlab.ListJobsOptions{ + jobOpts := &gitlab.ListJobsOptions{ ListOptions: gitlab.ListOptions{ PerPage: 1, Page: 1, @@ -95,13 +117,13 @@ func (g GL) GetLastJobStatus(t testing.TB, ctx context.Context, owner, project, IncludeRetried: &includeRetried, } - jobs, _, err := git.Jobs.ListProjectJobs(fmt.Sprintf("%s/%s", owner, project), opts, gitlab.WithContext(ctx)) + jobs, _, err := git.Jobs.ListPipelineJobs(projectPath, latestPipelineID, jobOpts, gitlab.WithContext(ctx)) if err != nil { - return "", 0, fmt.Errorf("error listing project jobs: %v", err) + return "", 0, fmt.Errorf("error listing jobs for pipeline %d: %w", latestPipelineID, err) } if len(jobs) == 0 { - return "", 0, fmt.Errorf("no jobs found for project: %s/%s", owner, project) + return "", 0, fmt.Errorf("no jobs found for pipeline %d", latestPipelineID) } return jobs[0].Status, jobs[0].ID, nil @@ -175,7 +197,7 @@ func (g GL) GetFinalJobStatus(t testing.TB, ctx context.Context, owner, project, } // WaitBuildSuccess waits for the current job in a project to finish. -func (g GL) WaitBuildSuccess(t testing.TB, owner, project, token, failureMsg string, maxBuildRetry, maxErrorRetries int, timeBetweenErrorRetries time.Duration) error { +func (g GL) WaitBuildSuccess(t testing.TB, owner, project, token, commitSha, failureMsg string, maxBuildRetry, maxErrorRetries int, timeBetweenErrorRetries time.Duration) error { var status string var jobID int var err error @@ -184,7 +206,7 @@ func (g GL) WaitBuildSuccess(t testing.TB, owner, project, token, failureMsg str // wait job creation time.Sleep(30 * time.Second) - status, jobID, err = g.GetLastJobStatus(t, ctx, owner, project, token) + status, jobID, err = g.GetLastJobStatusForSHA(t, ctx, owner, project, token, commitSha) if err != nil { return err } diff --git a/helpers/foundation-deployer/stages/apply.go b/helpers/foundation-deployer/stages/apply.go index 6b50feb60..37b4e1239 100644 --- a/helpers/foundation-deployer/stages/apply.go +++ b/helpers/foundation-deployer/stages/apply.go @@ -61,21 +61,28 @@ func buildGitLabCICDImage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, c Co return err } + commitSha, err := conf.GetCommitSha() + if err != nil { + return err + } + fmt.Println("") fmt.Println("# Follow job execution in GitLab:") fmt.Printf("# %s\n", fmt.Sprintf("https://gitlab.com/%s/%s/-/jobs", tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner)) failureMsg := fmt.Sprintf("CI/CD runner image job failed %s/%s repository.", tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner) - err = gl.WaitBuildSuccess(t, tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner, c.GitToken, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) + err = gl.WaitBuildSuccess(t, tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner, c.GitToken, commitSha, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) if err != nil { return err } + projectsToAdd := make([]string, 5) projectsToAdd[0] = tfvars.GitRepos.Bootstrap projectsToAdd[1] = tfvars.GitRepos.Organization projectsToAdd[2] = tfvars.GitRepos.Environments projectsToAdd[3] = tfvars.GitRepos.Networks projectsToAdd[4] = tfvars.GitRepos.Projects + return gl.AddProjectsToJobTokenScope(t, tfvars.GitRepos.Owner, *tfvars.GitRepos.CICDRunner, c.GitToken, projectsToAdd) } diff --git a/helpers/foundation-deployer/stages/executor.go b/helpers/foundation-deployer/stages/executor.go index 6b5fbe5f4..67d7946c2 100644 --- a/helpers/foundation-deployer/stages/executor.go +++ b/helpers/foundation-deployer/stages/executor.go @@ -81,5 +81,5 @@ func NewGitLabExecutor(owner, project, token string) *GitLabExecutor { } func (e *GitLabExecutor) WaitBuildSuccess(t testing.TB, commitSha, failureMsg string) error { - return e.executor.WaitBuildSuccess(t, e.owner, e.project, e.token, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) + return e.executor.WaitBuildSuccess(t, e.owner, e.project, e.token, commitSha, failureMsg, MaxBuildRetries, MaxErrorRetries, TimeBetweenErrorRetries) } From 8edfbbaede48d22c6fd4fe1a5259278be7e2d29e Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Fri, 7 Nov 2025 14:54:53 -0300 Subject: [PATCH 11/12] code review fixes --- 0-bootstrap/README-GitLab.md | 2 +- 0-bootstrap/README-Jenkins.md | 2 +- 0-bootstrap/README-Terraform-Cloud.md | 2 +- helpers/foundation-deployer/github/github.go | 2 +- helpers/foundation-deployer/gitlab/gitlab.go | 2 +- helpers/foundation-deployer/main.go | 7 +++-- helpers/foundation-deployer/utils/files.go | 29 ++++++++++---------- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/0-bootstrap/README-GitLab.md b/0-bootstrap/README-GitLab.md index e0d1dce1f..cd993df42 100644 --- a/0-bootstrap/README-GitLab.md +++ b/0-bootstrap/README-GitLab.md @@ -218,7 +218,7 @@ Run the `0-bootstrap/scripts/git_create_branches_helper.sh` script to create the cd ./envs/shared ``` -1. Run the helper script `choose_build_type.sh` to enable Bootstrap GitHub version +1. Run the helper script `choose_build_type.sh` to enable Bootstrap GitLab version ```bash ./scripts/choose_build_type.sh gitlab ``` diff --git a/0-bootstrap/README-Jenkins.md b/0-bootstrap/README-Jenkins.md index 841fdbe8d..cfe8e7236 100644 --- a/0-bootstrap/README-Jenkins.md +++ b/0-bootstrap/README-Jenkins.md @@ -139,7 +139,7 @@ You arrived to these instructions because you are using the `jenkins_bootstrap` cd ./envs/shared ``` -1. Run the helper script `choose_build_type.sh` to enable Bootstrap GitHub version +1. Run the helper script `choose_build_type.sh` to enable Bootstrap Jenkins version ```bash ./scripts/choose_build_type.sh jenkins ``` diff --git a/0-bootstrap/README-Terraform-Cloud.md b/0-bootstrap/README-Terraform-Cloud.md index 4feb1c91f..b5aa6a19d 100644 --- a/0-bootstrap/README-Terraform-Cloud.md +++ b/0-bootstrap/README-Terraform-Cloud.md @@ -136,7 +136,7 @@ You must be authenticated to the VCS provider. See [GitHub authentication](https cd ./envs/shared ``` -1. Run the helper script `choose_build_type.sh` to enable Bootstrap GitHub version +1. Run the helper script `choose_build_type.sh` to enable Bootstrap Terraform Cloud version ```bash ./scripts/choose_build_type.sh terraform_cloud ``` diff --git a/helpers/foundation-deployer/github/github.go b/helpers/foundation-deployer/github/github.go index a80ce5f4b..e30dc5a8c 100644 --- a/helpers/foundation-deployer/github/github.go +++ b/helpers/foundation-deployer/github/github.go @@ -288,7 +288,7 @@ func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token, commitSha, failur if err != nil { return err } - if utils.IsRetryableError(t, logs) { + if !utils.IsRetryableError(t, logs) { return fmt.Errorf("%s\nSee:\nhttps://github.com/%s/%s/actions/runs/%d\nfor details", failureMsg, owner, repo, runID) } fmt.Println("build failed with retryable error. a new build will be triggered.") diff --git a/helpers/foundation-deployer/gitlab/gitlab.go b/helpers/foundation-deployer/gitlab/gitlab.go index 52dfdee54..0e775c3a3 100644 --- a/helpers/foundation-deployer/gitlab/gitlab.go +++ b/helpers/foundation-deployer/gitlab/gitlab.go @@ -223,7 +223,7 @@ func (g GL) WaitBuildSuccess(t testing.TB, owner, project, token, commitSha, fai if err != nil { return err } - if utils.IsRetryableError(t, logs) { + if !utils.IsRetryableError(t, logs) { return fmt.Errorf("%s\nSee:\nhttps://gitlab.com/%s/%s/-/jobs/%d\nfor details", failureMsg, owner, project, jobID) } fmt.Println("job failed with retryable error. a new job will be triggered.") diff --git a/helpers/foundation-deployer/main.go b/helpers/foundation-deployer/main.go index 4b9f972d6..0e0089fe7 100644 --- a/helpers/foundation-deployer/main.go +++ b/helpers/foundation-deployer/main.go @@ -183,12 +183,13 @@ func main() { } var envVars map[string]string - if conf.BuildType != stages.BuildTypeGiHub { + + switch conf.BuildType { + case stages.BuildTypeGiHub: envVars = map[string]string{ "TF_VAR_gh_token": conf.GitToken, } - } - if conf.BuildType == stages.BuildTypeGitLab { + case stages.BuildTypeGitLab: envVars = map[string]string{ "TF_VAR_gitlab_token": conf.GitToken, } diff --git a/helpers/foundation-deployer/utils/files.go b/helpers/foundation-deployer/utils/files.go index 2a1efc1e9..34ffcf729 100644 --- a/helpers/foundation-deployer/utils/files.go +++ b/helpers/foundation-deployer/utils/files.go @@ -145,26 +145,25 @@ func RenameBuildFiles(basePath, targetBuild string) error { return fmt.Errorf("error renaming file \"%s\": %w", file, err) } } + } - // Rename *_BUILD_TYPE.tf.example to *_BUILD_TYPE.tf if they exist. - pattern := filepath.Join(basePath, fmt.Sprintf("*_%s.tf%s", targetBuild, DisableFileSuffix)) - files, err := filepath.Glob(pattern) - if err != nil { - return fmt.Errorf("error finding files (target rename): %w", err) - } + // Rename *_BUILD_TYPE.tf.example to *_BUILD_TYPE.tf if they exist. + pattern := filepath.Join(basePath, fmt.Sprintf("*_%s.tf%s", targetBuild, DisableFileSuffix)) + files, err := filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("error finding files (target rename): %w", err) + } - for _, file := range files { - baseName := strings.TrimSuffix(file, fmt.Sprintf(".tf%s", DisableFileSuffix)) - newName := baseName + ".tf" + for _, file := range files { + baseName := strings.TrimSuffix(file, fmt.Sprintf(".tf%s", DisableFileSuffix)) + newName := baseName + ".tf" - fmt.Printf("Renaming \"%s\" to \"%s\"\n", file, newName) + fmt.Printf("Renaming \"%s\" to \"%s\"\n", file, newName) - err := os.Rename(file, newName) - if err != nil { - return fmt.Errorf("error renaming file \"%s\": %w", file, err) - } + err := os.Rename(file, newName) + if err != nil { + return fmt.Errorf("error renaming file \"%s\": %w", file, err) } } - return nil } From 30df6adcafe5a418de85b0ca6cffb38e1f57a015 Mon Sep 17 00:00:00 2001 From: Daniel Andrade Date: Fri, 7 Nov 2025 15:35:44 -0300 Subject: [PATCH 12/12] review fixes --- 0-bootstrap/scripts/choose_build_type.sh | 2 +- helpers/foundation-deployer/README.md | 2 +- helpers/foundation-deployer/github/github.go | 10 +++++++--- helpers/foundation-deployer/gitlab/gitlab.go | 5 +++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/0-bootstrap/scripts/choose_build_type.sh b/0-bootstrap/scripts/choose_build_type.sh index 72ae5f26d..9460c2a46 100755 --- a/0-bootstrap/scripts/choose_build_type.sh +++ b/0-bootstrap/scripts/choose_build_type.sh @@ -15,7 +15,7 @@ # limitations under the License. # Define the base path where the build type files are -SCRIPTS_DIR="$( dirname -- "$0"; )" +SCRIPTS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" BASE_PATH="$SCRIPTS_DIR/.." TARGET_BUILD="$1" diff --git a/helpers/foundation-deployer/README.md b/helpers/foundation-deployer/README.md index af03d132c..d0f3a35f6 100644 --- a/helpers/foundation-deployer/README.md +++ b/helpers/foundation-deployer/README.md @@ -73,7 +73,7 @@ Version 1.5.7 is the last version before the license model change. To use a late ### Prepare the deploy environment -- Create a directory in the file system to host the Git Repositories that will be created (Cloud Source repositories, Github , or GitLab) and a copy of the terraform example foundation repository. +- Create a directory in the file system to host the Git Repositories that will be created (Cloud Source repositories, Github, or GitLab) and a copy of the terraform example foundation repository. - Clone the `terraform-example-foundation` repository on this directory. ```text diff --git a/helpers/foundation-deployer/github/github.go b/helpers/foundation-deployer/github/github.go index e30dc5a8c..fdd97bef7 100644 --- a/helpers/foundation-deployer/github/github.go +++ b/helpers/foundation-deployer/github/github.go @@ -84,7 +84,10 @@ func triggerNewBuild(t testing.TB, ctx context.Context, owner, repo, token, comm }, } - // wait action creation + // wait for the new workflow run to be created and appear + // in the API results after re-running it + // needed because RerunWorkflowByID dos no return + // the ID of the new workflow time.Sleep(30 * time.Second) runs, _, err := client.Actions.ListRepositoryWorkflowRuns(ctx, owner, repo, opts) @@ -267,10 +270,11 @@ func (g GH) WaitBuildSuccess(t testing.TB, owner, repo, token, commitSha, failur var runID int64 var err error - ctx := context.Background() - // wait action creation + // wait for the new workflow and action to be created and appear in the API results + // after the code being pushed to the repository time.Sleep(30 * time.Second) + ctx := context.Background() runID, status, conclusion, err = g.GetLastActionState(t, ctx, owner, repo, token, commitSha) if err != nil { return err diff --git a/helpers/foundation-deployer/gitlab/gitlab.go b/helpers/foundation-deployer/gitlab/gitlab.go index 0e775c3a3..8027656cc 100644 --- a/helpers/foundation-deployer/gitlab/gitlab.go +++ b/helpers/foundation-deployer/gitlab/gitlab.go @@ -202,10 +202,11 @@ func (g GL) WaitBuildSuccess(t testing.TB, owner, project, token, commitSha, fai var jobID int var err error - ctx := context.Background() - // wait job creation + // wait for the new job to be created and appear in the API results + // after the code being pushed to the project time.Sleep(30 * time.Second) + ctx := context.Background() status, jobID, err = g.GetLastJobStatusForSHA(t, ctx, owner, project, token, commitSha) if err != nil { return err