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()
+}