diff --git a/build/int.cloudbuild.yaml b/build/int.cloudbuild.yaml index 2a0eeb9a..2c030caf 100644 --- a/build/int.cloudbuild.yaml +++ b/build/int.cloudbuild.yaml @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -timeout: 3600s +timeout: 4200s steps: - id: swap-module-refs name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' @@ -249,6 +249,44 @@ steps: args: ['/bin/bash', '-c', 'cft test run TestCloudBuildWorkspaceSimpleGitLab --stage teardown --verbose'] secretEnv: ['IM_GITLAB_PAT'] +- id: apply-cloudbuild-connection-github + waitFor: + - create-all + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestCloudBuildRepoConnectionGithub --stage apply --verbose'] + secretEnv: ['IM_GITHUB_PAT'] +- id: verify-cloudbuild-connection-github + waitFor: + - apply-cloudbuild-connection-github + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestCloudBuildRepoConnectionGithub --stage verify --verbose'] + secretEnv: ['IM_GITHUB_PAT'] +- id: teardown-cloudbuild-connection-github + waitFor: + - verify-cloudbuild-connection-github + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestCloudBuildRepoConnectionGithub --stage teardown --verbose'] + secretEnv: ['IM_GITHUB_PAT'] + +- id: apply-cloudbuild-connection-gitlab + waitFor: + - create-all + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestCloudBuildRepoConnectionGitLab --stage apply --verbose'] + secretEnv: ['IM_GITLAB_PAT'] +- id: verify-cloudbuild-connection-gitlab + waitFor: + - apply-cloudbuild-connection-gitlab + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestCloudBuildRepoConnectionGitLab --stage verify --verbose'] + secretEnv: ['IM_GITLAB_PAT'] +- id: teardown-cloudbuild-connection-gitlab + waitFor: + - verify-cloudbuild-connection-gitlab + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestCloudBuildRepoConnectionGitLab --stage teardown --verbose'] + secretEnv: ['IM_GITLAB_PAT'] + availableSecrets: secretManager: - versionName: $_IM_GITHUB_PAT_SECRET_ID/versions/latest diff --git a/examples/cloudbuild_repo_connection_github/README.md b/examples/cloudbuild_repo_connection_github/README.md new file mode 100644 index 00000000..9eb9108d --- /dev/null +++ b/examples/cloudbuild_repo_connection_github/README.md @@ -0,0 +1,35 @@ +## Overview + +The example will create Cloud Build repositories (2nd gen) using a Github connection. + +## Github Requirements for Cloud Build Connection + +When using a Cloud Build repositories (2nd gen) GitHub repository, a Cloud Build connection to your repository provider will be created. + +For GitHub connections you will need: + +- Install the [Cloud Build App](https://github.com/apps/google-cloud-build) on Github. +- Create a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) on Github with [scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes) `repo` and `read:user` (or if app is installed in a organization use `read:org`). + +For more information on this topic refer to the Cloud Build repositories (2nd gen) documentation for +[Connect to a GitHub repository](https://cloud.google.com/build/docs/automating-builds/github/connect-repo-github?generation=2nd-gen). + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| github\_app\_id | The application ID for the Cloudbuild GitHub app. | `string` | n/a | yes | +| github\_pat | The personal access token for authenticating with GitHub. | `string` | n/a | yes | +| project\_id | The ID of the project in which to provision resources. | `string` | n/a | yes | +| repository\_name | The name of the test repository. | `string` | n/a | yes | +| repository\_url | The HTTPS clone URL of the repository, ending with .git. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| cloud\_build\_repositories\_2nd\_gen\_connection | Cloudbuild connection created. | +| cloud\_build\_repositories\_2nd\_gen\_repositories | Created repositories. | + + diff --git a/examples/cloudbuild_repo_connection_github/main.tf b/examples/cloudbuild_repo_connection_github/main.tf new file mode 100644 index 00000000..c0280d26 --- /dev/null +++ b/examples/cloudbuild_repo_connection_github/main.tf @@ -0,0 +1,34 @@ +/** + * Copyright 2024 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. + */ + +module "github_connection" { + source = "terraform-google-modules/bootstrap/google//modules/cloudbuild_repo_connection" + version = "~> 9.0" + + project_id = var.project_id + credential_config = { + credential_type = "GITHUBv2" + github_pat = var.github_pat + github_app_id = var.github_app_id + } + + cloud_build_repositories = { + "test_repo" = { + repository_name = var.repository_name + repository_url = var.repository_url + }, + } +} diff --git a/examples/cloudbuild_repo_connection_github/outputs.tf b/examples/cloudbuild_repo_connection_github/outputs.tf new file mode 100644 index 00000000..636e38bd --- /dev/null +++ b/examples/cloudbuild_repo_connection_github/outputs.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2024 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. + */ + +output "cloud_build_repositories_2nd_gen_connection" { + description = "Cloudbuild connection created." + value = module.github_connection.cloud_build_repositories_2nd_gen_connection +} + +output "cloud_build_repositories_2nd_gen_repositories" { + description = "Created repositories." + value = module.github_connection.cloud_build_repositories_2nd_gen_repositories +} + diff --git a/examples/cloudbuild_repo_connection_github/variables.tf b/examples/cloudbuild_repo_connection_github/variables.tf new file mode 100644 index 00000000..bdd3bd08 --- /dev/null +++ b/examples/cloudbuild_repo_connection_github/variables.tf @@ -0,0 +1,41 @@ +/** + * Copyright 2024 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. + */ + +variable "project_id" { + description = "The ID of the project in which to provision resources." + type = string +} + +variable "github_pat" { + description = "The personal access token for authenticating with GitHub." + type = string +} + +variable "github_app_id" { + description = "The application ID for the Cloudbuild GitHub app." + type = string +} + +variable "repository_url" { + description = "The HTTPS clone URL of the repository, ending with .git." + type = string +} + +variable "repository_name" { + description = "The name of the test repository." + type = string +} + diff --git a/examples/cloudbuild_repo_connection_gitlab/README.md b/examples/cloudbuild_repo_connection_gitlab/README.md new file mode 100644 index 00000000..4a3dc5bf --- /dev/null +++ b/examples/cloudbuild_repo_connection_gitlab/README.md @@ -0,0 +1,31 @@ +## Overview + +The example will create Cloud Build repositories (2nd gen) using a Gitlab connection. + +## Gitlab Requirements for Cloud Build Connection + +When using a Cloud Build repositories (2nd gen) GitLab repository, a Cloud Build connection to your repository provider will be needed. + +For more information on this topic refer to the Cloud Build repositories (2nd gen) documentation: +- [Connect to a GitLab host](https://cloud.google.com/build/docs/automating-builds/gitlab/connect-host-gitlab) +- [Connect to a GitLab repository](https://cloud.google.com/build/docs/automating-builds/github/connect-repo-github?generation=2nd-gen) + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| gitlab\_authorizer\_credential | Credential for GitLab authorizer | `string` | n/a | yes | +| gitlab\_read\_authorizer\_credential | Credential for GitLab read authorizer | `string` | n/a | yes | +| project\_id | The ID of the project in which to provision resources. | `string` | n/a | yes | +| repository\_name | The name of the test repository. | `string` | n/a | yes | +| repository\_url | The HTTPS clone URL of the repository, ending with .git. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| cloud\_build\_repositories\_2nd\_gen\_connection | Cloudbuild connection created. | +| cloud\_build\_repositories\_2nd\_gen\_repositories | Created repositories. | + + diff --git a/examples/cloudbuild_repo_connection_gitlab/main.tf b/examples/cloudbuild_repo_connection_gitlab/main.tf new file mode 100644 index 00000000..4a0a6bf3 --- /dev/null +++ b/examples/cloudbuild_repo_connection_gitlab/main.tf @@ -0,0 +1,33 @@ +/** + * Copyright 2024 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. + */ + +module "gitlab_connection" { + source = "../../modules/cloudbuild_repo_connection" + + project_id = var.project_id + credential_config = { + credential_type = "GITLABv2" + gitlab_authorizer_credential = var.gitlab_authorizer_credential + gitlab_read_authorizer_credential = var.gitlab_read_authorizer_credential + } + + cloud_build_repositories = { + "test_repo" = { + repository_name = var.repository_name + repository_url = var.repository_url + }, + } +} diff --git a/examples/cloudbuild_repo_connection_gitlab/outputs.tf b/examples/cloudbuild_repo_connection_gitlab/outputs.tf new file mode 100644 index 00000000..a1426a2d --- /dev/null +++ b/examples/cloudbuild_repo_connection_gitlab/outputs.tf @@ -0,0 +1,25 @@ +/** + * Copyright 2024 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. + */ + +output "cloud_build_repositories_2nd_gen_connection" { + description = "Cloudbuild connection created." + value = module.gitlab_connection.cloud_build_repositories_2nd_gen_connection +} + +output "cloud_build_repositories_2nd_gen_repositories" { + description = "Created repositories." + value = module.gitlab_connection.cloud_build_repositories_2nd_gen_repositories +} diff --git a/examples/cloudbuild_repo_connection_gitlab/variables.tf b/examples/cloudbuild_repo_connection_gitlab/variables.tf new file mode 100644 index 00000000..01f26b81 --- /dev/null +++ b/examples/cloudbuild_repo_connection_gitlab/variables.tf @@ -0,0 +1,41 @@ +/** + * Copyright 2024 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. + */ + +variable "project_id" { + description = "The ID of the project in which to provision resources." + type = string +} + +variable "repository_url" { + description = "The HTTPS clone URL of the repository, ending with .git." + type = string +} + +variable "repository_name" { + description = "The name of the test repository." + type = string +} + +variable "gitlab_authorizer_credential" { + description = "Credential for GitLab authorizer" + type = string +} + +variable "gitlab_read_authorizer_credential" { + description = "Credential for GitLab read authorizer" + type = string +} + diff --git a/modules/cloudbuild_repo_connection/README.md b/modules/cloudbuild_repo_connection/README.md new file mode 100644 index 00000000..a864cc84 --- /dev/null +++ b/modules/cloudbuild_repo_connection/README.md @@ -0,0 +1,27 @@ +# Overview + +This module is designed to establish the corresponding Cloud Build repositories (2nd gen) based on the `cloud_build_repositories` variable, where users can specify the repository names and URLs from their own version control systems. + +Additionally, it will create and manage secret versions, as well as configure the necessary permissions for cloud build service agent when utilizing Cloud Build repositories (2nd gen). + +Users will provide the required secrets through the `credential_config` variable, indicating their chosen Git provider. Currently, the module supports both GitHub and GitLab. + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| cloud\_build\_repositories | Cloud Build repositories configuration:
- repository\_name: The name of the repository to be used in Cloud Build.
- repository\_url: The HTTPS clone URL for the repository. This URL must end with '.git' and be a valid HTTPS URL.

Each entry in this map must contain both `repository_name` and `repository_url` to properly integrate with the Cloud Build service. |
map(object({
repository_name = string,
repository_url = string,
}))
| n/a | yes | +| cloudbuild\_connection\_name | Cloudbuild Connection Name. | `string` | `"generic-cloudbuild-connection"` | no | +| credential\_config | Credential configuration options:
- credential\_type: Specifies the type of credential being used. Supported types are 'GITHUBv2' and 'GITLABv2'.
- github\_secret\_id: (Optional) The secret ID for GitHub credentials. Default is "cb-github-pat".
- github\_pat: (Optional) The personal access token for GitHub authentication.
- github\_app\_id: (Optional) The application ID for a GitHub App used for authentication. For app installation, follow this link: https://github.com/apps/google-cloud-build
- gitlab\_read\_authorizer\_credential: (Optional) The read authorizer credential for GitLab access.
- gitlab\_read\_authorizer\_credential\_secret\_id: (Optional) The secret ID for the GitLab read authorizer credential. Default is "cb-gitlab-read-api-credential".
- gitlab\_authorizer\_credential: (Optional) The authorizer credential for GitLab access.
- gitlab\_authorizer\_credential\_secret\_id: (Optional) The secret ID for the GitLab authorizer credential. Default is "cb-gitlab-api-credential". |
object({
credential_type = string
github_secret_id = optional(string, "cb-github-pat")
github_pat = optional(string)
github_app_id = optional(string)
gitlab_read_authorizer_credential = optional(string)
gitlab_read_authorizer_credential_secret_id = optional(string, "cb-gitlab-read-api-credential")
gitlab_authorizer_credential = optional(string)
gitlab_authorizer_credential_secret_id = optional(string, "cb-gitlab-api-credential")
})
| n/a | yes | +| location | Resources location. | `string` | `"us-central1"` | no | +| project\_id | The project id to create the secret and assign cloudbuild service account permissions. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| cloud\_build\_repositories\_2nd\_gen\_connection | The unique identifier of the Cloud Build connection created within the specified Google Cloud project.
Example format: projects/{{project}}/locations/{{location}}/connections/{{name}} | +| cloud\_build\_repositories\_2nd\_gen\_repositories | A map of created repositories associated with the Cloud Build connection.
Each entry contains the repository's unique identifier and its remote URL.
Example format:
"key\_name" = {
"id" = "projects/{{project}}/locations/{{location}}/connections/{{parent\_connection}}/repositories/{{name}}",
"url" = "https://github.com/{{account/org}}/{{repository_name}}.git"
} | + + diff --git a/modules/cloudbuild_repo_connection/cloudbuild.tf b/modules/cloudbuild_repo_connection/cloudbuild.tf new file mode 100644 index 00000000..dc301b6e --- /dev/null +++ b/modules/cloudbuild_repo_connection/cloudbuild.tf @@ -0,0 +1,66 @@ +/** + * Copyright 2024 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. + */ + +resource "google_cloudbuildv2_connection" "connection" { + project = var.project_id + location = var.location + name = "${var.cloudbuild_connection_name}-${random_id.suffix.dec}" + + dynamic "github_config" { + for_each = local.is_github ? [1] : [] + content { + app_installation_id = var.credential_config.github_app_id + authorizer_credential { + oauth_token_secret_version = "${google_secret_manager_secret.github_token[0].id}/versions/latest" + } + } + } + + dynamic "gitlab_config" { + for_each = local.is_gitlab ? [1] : [] + content { + host_uri = null + authorizer_credential { + user_token_secret_version = google_secret_manager_secret_version.gitlab_api_token[0].name + } + read_authorizer_credential { + user_token_secret_version = google_secret_manager_secret_version.gitlab_read_api_token[0].name + } + webhook_secret_secret_version = google_secret_manager_secret_version.gitlab_webhook[0].name + } + } + + depends_on = [time_sleep.secret_iam_permission_propagation] +} + +resource "time_sleep" "secret_iam_permission_propagation" { + create_duration = "30s" + + depends_on = [ + google_secret_manager_secret_iam_member.github_accessor, + google_secret_manager_secret_iam_member.gitlab_token_accessor + ] +} + +resource "google_cloudbuildv2_repository" "repositories" { + for_each = var.cloud_build_repositories + + project = var.project_id + location = var.location + name = each.value.repository_name + remote_uri = each.value.repository_url + parent_connection = google_cloudbuildv2_connection.connection.name +} diff --git a/modules/cloudbuild_repo_connection/main.tf b/modules/cloudbuild_repo_connection/main.tf new file mode 100644 index 00000000..71420876 --- /dev/null +++ b/modules/cloudbuild_repo_connection/main.tf @@ -0,0 +1,129 @@ +/** + * Copyright 2024 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. + */ + +data "google_project" "project_id" { + project_id = var.project_id +} + +locals { + is_github = var.credential_config.credential_type == "GITHUBv2" + is_gitlab = var.credential_config.credential_type == "GITLABv2" + + gitlab_secrets_iterator = local.is_gitlab ? { + "api" = google_secret_manager_secret.gitlab_api_token[0].id, + "read_api" = google_secret_manager_secret.gitlab_read_api_token[0].id, + "webhook" = google_secret_manager_secret.gitlab_webhook[0].id + } : {} +} + +resource "random_id" "suffix" { + byte_length = 4 +} + +# Github Secret +resource "google_secret_manager_secret" "github_token" { + count = local.is_github ? 1 : 0 + + project = var.project_id + secret_id = "${var.credential_config.github_secret_id}-${random_id.suffix.dec}" + + replication { + auto { + + } + } +} + +resource "google_secret_manager_secret_version" "github_token" { + count = local.is_github ? 1 : 0 + + secret = google_secret_manager_secret.github_token[0].id + secret_data = var.credential_config.github_pat +} + +resource "google_secret_manager_secret_iam_member" "github_accessor" { + count = local.is_github ? 1 : 0 + + secret_id = google_secret_manager_secret.github_token[0].id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:service-${data.google_project.project_id.number}@gcp-sa-cloudbuild.iam.gserviceaccount.com" +} + +# Gitlab secret +resource "google_secret_manager_secret" "gitlab_api_token" { + count = local.is_gitlab ? 1 : 0 + + project = var.project_id + secret_id = "${var.credential_config.gitlab_authorizer_credential_secret_id}-${random_id.suffix.dec}" + + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "gitlab_api_token" { + count = local.is_gitlab ? 1 : 0 + + secret = google_secret_manager_secret.gitlab_api_token[0].id + secret_data = var.credential_config.gitlab_authorizer_credential +} + +resource "google_secret_manager_secret" "gitlab_read_api_token" { + count = local.is_gitlab ? 1 : 0 + + project = var.project_id + secret_id = "${var.credential_config.gitlab_read_authorizer_credential_secret_id}-${random_id.suffix.dec}" + replication { + auto {} + } +} + +resource "google_secret_manager_secret_version" "gitlab_read_api_token" { + count = local.is_gitlab ? 1 : 0 + + secret = google_secret_manager_secret.gitlab_read_api_token[0].id + secret_data = var.credential_config.gitlab_read_authorizer_credential +} + +resource "google_secret_manager_secret" "gitlab_webhook" { + count = local.is_gitlab ? 1 : 0 + + project = var.project_id + secret_id = "cb-gitlab-webhook-${random_id.suffix.dec}" + replication { + auto {} + } +} + +resource "random_uuid" "random_webhook_secret" { + count = local.is_gitlab ? 1 : 0 +} + +resource "google_secret_manager_secret_version" "gitlab_webhook" { + count = local.is_gitlab ? 1 : 0 + + secret = google_secret_manager_secret.gitlab_webhook[0].id + secret_data = random_uuid.random_webhook_secret[0].result +} + +resource "google_secret_manager_secret_iam_member" "gitlab_token_accessor" { + for_each = local.gitlab_secrets_iterator + + project = var.project_id + secret_id = each.value + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:service-${data.google_project.project_id.number}@gcp-sa-cloudbuild.iam.gserviceaccount.com" +} diff --git a/modules/cloudbuild_repo_connection/outputs.tf b/modules/cloudbuild_repo_connection/outputs.tf new file mode 100644 index 00000000..5336cb57 --- /dev/null +++ b/modules/cloudbuild_repo_connection/outputs.tf @@ -0,0 +1,36 @@ +/** + * Copyright 2024 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. + */ + +output "cloud_build_repositories_2nd_gen_connection" { + description = < { "id" : v.id, "url" : v.remote_uri } } +} diff --git a/modules/cloudbuild_repo_connection/variables.tf b/modules/cloudbuild_repo_connection/variables.tf new file mode 100644 index 00000000..f1f6a4c4 --- /dev/null +++ b/modules/cloudbuild_repo_connection/variables.tf @@ -0,0 +1,99 @@ +/** + * Copyright 2024 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. + */ + +variable "project_id" { + description = "The project id to create the secret and assign cloudbuild service account permissions." + type = string +} + +variable "credential_config" { + description = <<-EOT + Credential configuration options: + - credential_type: Specifies the type of credential being used. Supported types are 'GITHUBv2' and 'GITLABv2'. + - github_secret_id: (Optional) The secret ID for GitHub credentials. Default is "cb-github-pat". + - github_pat: (Optional) The personal access token for GitHub authentication. + - github_app_id: (Optional) The application ID for a GitHub App used for authentication. For app installation, follow this link: https://github.com/apps/google-cloud-build + - gitlab_read_authorizer_credential: (Optional) The read authorizer credential for GitLab access. + - gitlab_read_authorizer_credential_secret_id: (Optional) The secret ID for the GitLab read authorizer credential. Default is "cb-gitlab-read-api-credential". + - gitlab_authorizer_credential: (Optional) The authorizer credential for GitLab access. + - gitlab_authorizer_credential_secret_id: (Optional) The secret ID for the GitLab authorizer credential. Default is "cb-gitlab-api-credential". + EOT + type = object({ + credential_type = string + github_secret_id = optional(string, "cb-github-pat") + github_pat = optional(string) + github_app_id = optional(string) + gitlab_read_authorizer_credential = optional(string) + gitlab_read_authorizer_credential_secret_id = optional(string, "cb-gitlab-read-api-credential") + gitlab_authorizer_credential = optional(string) + gitlab_authorizer_credential_secret_id = optional(string, "cb-gitlab-api-credential") + }) + + validation { + condition = var.credential_config.credential_type == "GITLABv2" || var.credential_config.credential_type == "GITHUBv2" + error_message = "Specify one of the valid credential_types: 'GITLABv2' or 'GITHUBv2'." + } + + validation { + condition = var.credential_config.credential_type == "GITLABv2" ? ( + var.credential_config.gitlab_read_authorizer_credential != null && + var.credential_config.gitlab_authorizer_credential != null + ) : true + + error_message = "For 'GITLABv2', 'gitlab_read_authorizer_credential' and 'gitlab_authorizer_credential' must be defined." + } + + validation { + condition = var.credential_config.credential_type == "GITHUBv2" ? ( + var.credential_config.github_pat != null && + var.credential_config.github_app_id != null + ) : true + + error_message = "For 'GITHUBv2', 'github_pat' and 'github_app_id' must be defined." + } +} + +variable "cloud_build_repositories" { + description = <<-EOT + Cloud Build repositories configuration: + - repository_name: The name of the repository to be used in Cloud Build. + - repository_url: The HTTPS clone URL for the repository. This URL must end with '.git' and be a valid HTTPS URL. + + Each entry in this map must contain both `repository_name` and `repository_url` to properly integrate with the Cloud Build service. + EOT + type = map(object({ + repository_name = string, + repository_url = string, + })) + + validation { + condition = alltrue([for k, v in var.cloud_build_repositories : try(length(regex("^https://.*\\.git$", v.repository_url)) > 0, false)]) + error_message = "Each repository_url must be a valid HTTPS git clone URL ending with '.git'." + } + +} + +variable "location" { + description = "Resources location." + type = string + default = "us-central1" +} + +variable "cloudbuild_connection_name" { + description = "Cloudbuild Connection Name." + type = string + default = "generic-cloudbuild-connection" +} diff --git a/modules/cloudbuild_repo_connection/versions.tf b/modules/cloudbuild_repo_connection/versions.tf new file mode 100644 index 00000000..12cf63d4 --- /dev/null +++ b/modules/cloudbuild_repo_connection/versions.tf @@ -0,0 +1,43 @@ +/** + * Copyright 2024 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. + */ + +terraform { + required_version = ">=1.3" + + required_providers { + google = { + source = "hashicorp/google" + # Exclude 4.31.0 for https://github.com/hashicorp/terraform-provider-google/issues/12226 + version = ">= 4.17, != 4.31.0, < 6" + } + + time = { + source = "hashicorp/time" + version = ">= 0.12.0" + } + + random = { + source = "hashicorp/random" + version = ">= 3.6.2" + } + + google-beta = { + source = "hashicorp/google-beta" + # Exclude 4.31.0 for https://github.com/hashicorp/terraform-provider-google/issues/12226 + version = ">= 4.17, != 4.31.0, < 6" + } + } +} diff --git a/test/integration/cloudbuild_repo_connection_github/cloudbuild_repo_connection_github_test.go b/test/integration/cloudbuild_repo_connection_github/cloudbuild_repo_connection_github_test.go new file mode 100644 index 00000000..15a0acaf --- /dev/null +++ b/test/integration/cloudbuild_repo_connection_github/cloudbuild_repo_connection_github_test.go @@ -0,0 +1,151 @@ +// Copyright 2024 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 cloudbuild_repo_connection_github + +import ( + "context" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + cftutils "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" + "github.com/google/go-github/v63/github" + "github.com/stretchr/testify/assert" + "github.com/terraform-google-modules/terraform-google-bootstrap/test/integration/utils" +) + +type GitHubClient struct { + t *testing.T + client *github.Client + owner string + repoName string + repository *github.Repository +} + +func NewGitHubClient(t *testing.T, token, owner, repo string) *GitHubClient { + t.Helper() + client := github.NewClient(nil).WithAuthToken(token) + return &GitHubClient{ + t: t, + client: client, + owner: owner, + repoName: repo, + } +} + +func (gh *GitHubClient) GetRepository(ctx context.Context) *github.Repository { + repo, resp, err := gh.client.Repositories.Get(ctx, gh.owner, gh.repoName) + if resp.StatusCode != 404 && err != nil { + gh.t.Fatal(err.Error()) + } + gh.repository = repo + return repo +} + +func (gh *GitHubClient) CreateRepository(ctx context.Context, org, repoName string) *github.Repository { + newRepo := &github.Repository{ + Name: github.String(repoName), + AutoInit: github.Bool(true), + Private: github.Bool(true), + Visibility: github.String("private"), + } + repo, _, err := gh.client.Repositories.Create(ctx, org, newRepo) + if err != nil { + gh.t.Fatal(err.Error()) + } + gh.repository = repo + return repo +} + +func (gh *GitHubClient) DeleteRepository(ctx context.Context) { + resp, err := gh.client.Repositories.Delete(ctx, gh.owner, *gh.repository.Name) + if resp.StatusCode != 404 && err != nil { + gh.t.Fatal(err.Error()) + } +} + +func TestCloudBuildRepoConnectionGithub(t *testing.T) { + ctx := context.Background() + + repoName := fmt.Sprintf("conn-gh-%s", utils.GetRandomStringFromSetup(t)) + + githubPAT := cftutils.ValFromEnv(t, "IM_GITHUB_PAT") + + owner := "im-goose" + client := NewGitHubClient(t, githubPAT, owner, repoName) + + repo := client.GetRepository(ctx) + if repo == nil { + client.CreateRepository(ctx, client.owner, client.repoName) + } + + repoURL := client.repository.GetCloneURL() + + resourcesLocation := "us-central1" + vars := map[string]interface{}{ + "github_pat": githubPAT, + "github_app_id": "47590865", + "repository_name": repoName, + "repository_url": repoURL, + } + + bpt := tft.NewTFBlueprintTest(t, tft.WithVars(vars)) + + bpt.DefineVerify(func(assert *assert.Assertions) { + bpt.DefaultVerify(assert) + + t.Cleanup(func() { + // Delete the repository if we hit a failed state + if t.Failed() { + client.DeleteRepository(ctx) + } + }) + + // validate if repository was created using the connection + projectId := bpt.GetTFSetupStringOutput("project_id") + connectionId := bpt.GetStringOutput("cloud_build_repositories_2nd_gen_connection") + + connectionIdRegexPattern := `^projects/[^/]+/locations/[^/]+/connections/[^/]+$` + re := regexp.MustCompile(connectionIdRegexPattern) + assert.True(re.MatchString(connectionId), "Connection ID should be in format projects/{{project}}/locations/{{location}}/connections/{{name}}") + + connectionSlice := strings.Split(connectionId, "/") + // Extract the project, location and connection name from the connection_id + connectionProjectId := connectionSlice[1] + connectionLocation := connectionSlice[3] + connectionName := connectionSlice[len(connectionSlice)-1] + + // Assert that the resource was created in the specified project and region + assert.Equal(projectId, connectionProjectId, "Connection project id should be the same as input project id.") + assert.Equal(resourcesLocation, connectionLocation, fmt.Sprintf("Connection location should be '%s'.", resourcesLocation)) + + repository := gcloud.Runf(t, "builds repositories describe %s --project %s --region %s --connection %s", repoName, projectId, resourcesLocation, connectionName) + + assert.Equal(repoURL, repository.Get("remoteUri").String(), "Git clone URL must be the same on the created resource.") + }) + + bpt.DefineTeardown(func(assert *assert.Assertions) { + // Guarantee clean up even if the normal gcloud/teardown run into errors + t.Cleanup(func() { + client.DeleteRepository(ctx) + bpt.DefaultTeardown(assert) + }) + }) + + bpt.Test() +} diff --git a/test/integration/cloudbuild_repo_connection_gitlab/cloudbuild_repo_connection_gitlab_test.go b/test/integration/cloudbuild_repo_connection_gitlab/cloudbuild_repo_connection_gitlab_test.go new file mode 100644 index 00000000..f39e31b9 --- /dev/null +++ b/test/integration/cloudbuild_repo_connection_gitlab/cloudbuild_repo_connection_gitlab_test.go @@ -0,0 +1,165 @@ +// Copyright 2024 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 cloudbuild_repo_connection_gitlab + +import ( + "fmt" + "regexp" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + cftutils "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" + "github.com/stretchr/testify/assert" + "github.com/terraform-google-modules/terraform-google-bootstrap/test/integration/utils" + "github.com/xanzy/go-gitlab" +) + +type GitLabClient struct { + t *testing.T + client *gitlab.Client + group string + namespace int + repo string + project *gitlab.Project +} + +func NewGitLabClient(t *testing.T, token, owner, repo string) *GitLabClient { + t.Helper() + client, err := gitlab.NewClient(token) + if err != nil { + t.Fatal(err.Error()) + } + return &GitLabClient{ + t: t, + client: client, + group: "infrastructure-manager", + namespace: 84326276, + repo: repo, + } +} + +func (gl *GitLabClient) ProjectName() string { + return fmt.Sprintf("%s/%s", gl.group, gl.repo) +} + +func (gl *GitLabClient) GetProject() *gitlab.Project { + proj, resp, err := gl.client.Projects.GetProject(gl.ProjectName(), nil) + if resp.StatusCode != 404 && err != nil { + gl.t.Fatalf("got status code %d, error %s", resp.StatusCode, err.Error()) + } + gl.project = proj + return proj +} + +func (gl *GitLabClient) CreateProject() { + opts := &gitlab.CreateProjectOptions{ + Name: gitlab.Ptr(gl.repo), + // ID of the the Infrastructure Manager group (gitlab.com/infrastructure-manager) + NamespaceID: gitlab.Ptr(gl.namespace), + // Required otherwise Cloud Build errors on creating the connection + InitializeWithReadme: gitlab.Ptr(true), + } + proj, _, err := gl.client.Projects.CreateProject(opts) + if err != nil { + gl.t.Fatal(err.Error()) + } + gl.project = proj +} + +func (gl *GitLabClient) AddFileToProject(file []byte) { + opts := &gitlab.CreateFileOptions{ + Branch: gitlab.Ptr("main"), + CommitMessage: gitlab.Ptr("Initial config commit"), + Content: gitlab.Ptr(string(file)), + } + _, _, err := gl.client.RepositoryFiles.CreateFile(gl.ProjectName(), "main.tf", opts) + if err != nil { + gl.t.Fatal(err.Error()) + } +} + +func (gl *GitLabClient) DeleteProject() { + resp, err := gl.client.Projects.DeleteProject(gl.ProjectName()) + if resp.StatusCode != 404 && err != nil { + gl.t.Errorf("error deleting project with status %s and error %s", resp.Status, err.Error()) + } + gl.project = nil +} + +func TestCloudBuildRepoConnectionGitLab(t *testing.T) { + repoName := fmt.Sprintf("conn-gl-%s", utils.GetRandomStringFromSetup(t)) + gitlabPAT := cftutils.ValFromEnv(t, "IM_GITLAB_PAT") + owner := "infrastructure-manager" + + client := NewGitLabClient(t, gitlabPAT, owner, repoName) + proj := client.GetProject() + if proj == nil { + client.CreateProject() + } + + resourcesLocation := "us-central1" + vars := map[string]interface{}{ + "gitlab_read_authorizer_credential": gitlabPAT, + "gitlab_authorizer_credential": gitlabPAT, + "repository_name": repoName, + "repository_url": client.project.HTTPURLToRepo, + } + bpt := tft.NewTFBlueprintTest(t, tft.WithVars(vars)) + + bpt.DefineVerify(func(assert *assert.Assertions) { + bpt.DefaultVerify(assert) + + t.Cleanup(func() { + // Delete the repository if we hit a failed state + if t.Failed() { + client.DeleteProject() + } + }) + + // validate if repository was created using the connection + projectId := bpt.GetTFSetupStringOutput("project_id") + connectionId := bpt.GetStringOutput("cloud_build_repositories_2nd_gen_connection") + + connectionIdRegexPattern := `^projects/[^/]+/locations/[^/]+/connections/[^/]+$` + re := regexp.MustCompile(connectionIdRegexPattern) + assert.True(re.MatchString(connectionId), "Connection ID should be in format projects/{{project}}/locations/{{location}}/connections/{{name}}") + + connectionSlice := strings.Split(connectionId, "/") + // Extract the project, location and connection name from the connection_id + connectionProjectId := connectionSlice[1] + connectionLocation := connectionSlice[3] + connectionName := connectionSlice[len(connectionSlice)-1] + + // Assert that the resource was created in the specified project and region + assert.Equal(projectId, connectionProjectId, "Connection project id should be the same as input project id.") + assert.Equal(resourcesLocation, connectionLocation, fmt.Sprintf("Connection location should be '%s'.", resourcesLocation)) + + repository := gcloud.Runf(t, "builds repositories describe %s --project %s --region %s --connection %s", repoName, projectId, resourcesLocation, connectionName) + + assert.Equal(client.project.HTTPURLToRepo, repository.Get("remoteUri").String(), "Git clone URL must be the same on the created resource.") + }) + + bpt.DefineTeardown(func(assert *assert.Assertions) { + // Guarantee clean up even if the normal gcloud/teardown run into errors + t.Cleanup(func() { + client.DeleteProject() + bpt.DefaultTeardown(assert) + }) + }) + + bpt.Test() +}