diff --git a/modules/artifact-registry/README.md b/modules/artifact-registry/README.md
index f3623e3e3..a3eca65e2 100644
--- a/modules/artifact-registry/README.md
+++ b/modules/artifact-registry/README.md
@@ -8,6 +8,7 @@ This module simplifies the creation of repositories using Google Cloud Artifact
- [Additional Docker and Maven Options](#additional-docker-and-maven-options)
- [Other Formats](#other-formats)
- [Cleanup Policies](#cleanup-policies)
+- [IAM](#iam)
- [Variables](#variables)
- [Outputs](#outputs)
@@ -210,6 +211,75 @@ module "registry-docker" {
}
# tftest modules=1 resources=1 inventory=cleanup-policies.yaml
```
+
+## IAM
+
+This module implements the same IAM interface than the other modules.
+You can choose one (and only one) of the three options below:
+
+```hcl
+# Authoritative IAM bindings
+module "authoritative_iam" {
+ source = "./fabric/modules/artifact-registry"
+ project_id = "myproject"
+ location = "europe-west1"
+ name = "myregistry"
+ format = { docker = { standard = {} } }
+ iam = {
+ "roles/artifactregistry.admin" = ["group:cicd@example.com"]
+ }
+}
+
+# Authoritative IAM bindings (with conditions)
+module "authoritative_iam_conditions" {
+ source = "./fabric/modules/artifact-registry"
+ project_id = "myproject"
+ location = "europe-west1"
+ name = "myregistry"
+ format = { docker = { standard = {} } }
+ iam_bindings = {
+ "ci-admin" = {
+ members = ["group:cicd@example.com"]
+ role = "roles/artifactregistry.admin"
+ // condition = {
+ // expression = string
+ // title = string
+ // description = optional(string)
+ // }
+ }
+ }
+}
+
+# Additive IAM bindings
+module "additive_iam" {
+ source = "./fabric/modules/artifact-registry"
+ project_id = "myproject"
+ location = "europe-west1"
+ name = "myregistry"
+ format = { docker = { standard = {} } }
+ iam_bindings_additive = {
+ "ci-admin" = {
+ member = "group:cicd@example.com"
+ role = "roles/artifactregistry.admin"
+ // condition = {
+ // expression = string
+ // title = string
+ // description = optional(string)
+ // }
+ }
+ "ci-read" = {
+ member = "group:cicd-read@example.com"
+ role = "roles/artifactregistry.reader"
+ // condition = {
+ // expression = string
+ // title = string
+ // description = optional(string)
+ // }
+ }
+ }
+}
+# tftest modules=3 resources=7
+```
## Variables
@@ -217,21 +287,24 @@ module "registry-docker" {
|---|---|:---:|:---:|:---:|
| [cleanup_policies](variables.tf#L17) | Object containing details about the cleanup policies for an Artifact Registry repository. | map(object({…default = null | ✓ | |
| [format](variables.tf#L56) | Repository format. | object({…}) | ✓ | |
-| [location](variables.tf#L208) | Registry location. Use `gcloud beta artifacts locations list' to get valid values. | string | ✓ | |
-| [name](variables.tf#L213) | Registry name. | string | ✓ | |
-| [project_id](variables.tf#L218) | Registry project id. | string | ✓ | |
+| [location](variables.tf#L202) | Registry location. Use `gcloud beta artifacts locations list' to get valid values. | string | ✓ | |
+| [name](variables.tf#L207) | Registry name. | string | ✓ | |
+| [project_id](variables.tf#L212) | Registry project id. | string | ✓ | |
| [cleanup_policy_dry_run](variables.tf#L38) | If true, the cleanup pipeline is prevented from deleting versions in this repository. | bool | | null |
| [description](variables.tf#L44) | An optional description for the repository. | string | | "Terraform-managed registry" |
| [encryption_key](variables.tf#L50) | The KMS key name to use for encryption at rest. | string | | null |
-| [iam](variables.tf#L196) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [labels](variables.tf#L202) | Labels to be attached to the registry. | map(string) | | {} |
+| [iam](variables-iam.tf#L36) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables-iam.tf#L43) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables-iam.tf#L58) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_by_principals](variables-iam.tf#L73) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} |
+| [labels](variables.tf#L196) | Labels to be attached to the registry. | map(string) | | {} |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [id](outputs.tf#L17) | Fully qualified repository id. | |
-| [name](outputs.tf#L25) | Repository name. | |
-| [repository](outputs.tf#L33) | Repository object. | |
-| [url](outputs.tf#L41) | Repository URL. | |
+| [name](outputs.tf#L27) | Repository name. | |
+| [repository](outputs.tf#L37) | Repository object. | |
+| [url](outputs.tf#L47) | Repository URL. | |
diff --git a/modules/artifact-registry/iam.tf b/modules/artifact-registry/iam.tf
new file mode 100644
index 000000000..62b0dff63
--- /dev/null
+++ b/modules/artifact-registry/iam.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.
+ */
+
+moved {
+ from = google_artifact_registry_repository_iam_binding.bindings
+ to = google_artifact_registry_repository_iam_binding.authoritative
+}
+
+resource "google_artifact_registry_repository_iam_binding" "authoritative" {
+ for_each = local.iam
+ project = var.project_id
+ location = google_artifact_registry_repository.registry.location
+ repository = google_artifact_registry_repository.registry.name
+ role = each.key
+ members = each.value
+}
+
+# renamed as bindings2 to allow the moved block above
+resource "google_artifact_registry_repository_iam_binding" "bindings2" {
+ for_each = var.iam_bindings
+ project = var.project_id
+ location = google_artifact_registry_repository.registry.location
+ repository = google_artifact_registry_repository.registry.name
+ role = each.value.role
+ members = each.value.members
+
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
+
+resource "google_artifact_registry_repository_iam_member" "members" {
+ for_each = var.iam_bindings_additive
+ project = var.project_id
+ location = google_artifact_registry_repository.registry.location
+ repository = google_artifact_registry_repository.registry.name
+ role = each.value.role
+ member = each.value.member
+
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
diff --git a/modules/artifact-registry/main.tf b/modules/artifact-registry/main.tf
index bef5c4f89..685ea9bc6 100644
--- a/modules/artifact-registry/main.tf
+++ b/modules/artifact-registry/main.tf
@@ -197,13 +197,3 @@ resource "google_artifact_registry_repository" "registry" {
}
}
-
-resource "google_artifact_registry_repository_iam_binding" "bindings" {
- provider = google-beta
- for_each = var.iam
- project = var.project_id
- location = google_artifact_registry_repository.registry.location
- repository = google_artifact_registry_repository.registry.name
- role = each.key
- members = each.value
-}
diff --git a/modules/artifact-registry/outputs.tf b/modules/artifact-registry/outputs.tf
index ad3cb7dcf..b471e6dbf 100644
--- a/modules/artifact-registry/outputs.tf
+++ b/modules/artifact-registry/outputs.tf
@@ -18,7 +18,9 @@ output "id" {
description = "Fully qualified repository id."
value = google_artifact_registry_repository.registry.id
depends_on = [
- google_artifact_registry_repository_iam_binding.bindings
+ google_artifact_registry_repository_iam_binding.authoritative,
+ google_artifact_registry_repository_iam_binding.bindings2,
+ google_artifact_registry_repository_iam_member.members,
]
}
@@ -26,7 +28,9 @@ output "name" {
description = "Repository name."
value = google_artifact_registry_repository.registry.name
depends_on = [
- google_artifact_registry_repository_iam_binding.bindings
+ google_artifact_registry_repository_iam_binding.authoritative,
+ google_artifact_registry_repository_iam_binding.bindings2,
+ google_artifact_registry_repository_iam_member.members,
]
}
@@ -34,7 +38,9 @@ output "repository" {
description = "Repository object."
value = google_artifact_registry_repository.registry
depends_on = [
- google_artifact_registry_repository_iam_binding.bindings
+ google_artifact_registry_repository_iam_binding.authoritative,
+ google_artifact_registry_repository_iam_binding.bindings2,
+ google_artifact_registry_repository_iam_member.members,
]
}
@@ -47,6 +53,8 @@ output "url" {
])
depends_on = [
google_artifact_registry_repository.registry,
- google_artifact_registry_repository_iam_binding.bindings
+ google_artifact_registry_repository_iam_binding.authoritative,
+ google_artifact_registry_repository_iam_binding.bindings2,
+ google_artifact_registry_repository_iam_member.members,
]
}
diff --git a/modules/artifact-registry/variables-iam.tf b/modules/artifact-registry/variables-iam.tf
new file mode 100644
index 000000000..1f7530abf
--- /dev/null
+++ b/modules/artifact-registry/variables-iam.tf
@@ -0,0 +1,78 @@
+/**
+ * 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.
+ */
+
+# tfdoc:file:description IAM bindings
+
+locals {
+ _iam_principal_roles = distinct(flatten(values(var.iam_by_principals)))
+ _iam_principals = {
+ for r in local._iam_principal_roles : r => [
+ for k, v in var.iam_by_principals :
+ k if try(index(v, r), null) != null
+ ]
+ }
+ iam = {
+ for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) :
+ role => concat(
+ try(var.iam[role], []),
+ try(local._iam_principals[role], [])
+ )
+ }
+}
+
+variable "iam" {
+ description = "IAM bindings in {ROLE => [MEMBERS]} format."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
+
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_by_principals" {
+ description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable."
+ type = map(list(string))
+ default = {}
+ nullable = false
+}
diff --git a/modules/artifact-registry/variables.tf b/modules/artifact-registry/variables.tf
index 4d46e4a99..6c86daec9 100644
--- a/modules/artifact-registry/variables.tf
+++ b/modules/artifact-registry/variables.tf
@@ -193,12 +193,6 @@ variable "format" {
}
}
-variable "iam" {
- description = "IAM bindings in {ROLE => [MEMBERS]} format."
- type = map(list(string))
- default = {}
-}
-
variable "labels" {
description = "Labels to be attached to the registry."
type = map(string)
diff --git a/tests/modules/artifact_registry/examples/iam.yaml b/tests/modules/artifact_registry/examples/iam.yaml
new file mode 100644
index 000000000..bf295cc62
--- /dev/null
+++ b/tests/modules/artifact_registry/examples/iam.yaml
@@ -0,0 +1,110 @@
+# 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.
+
+values:
+ module.additive_iam.google_artifact_registry_repository.registry:
+ cleanup_policies: []
+ cleanup_policy_dry_run: null
+ description: Terraform-managed registry
+ docker_config: []
+ effective_labels:
+ goog-terraform-provisioned: 'true'
+ format: DOCKER
+ kms_key_name: null
+ labels: null
+ location: europe-west1
+ maven_config: []
+ mode: STANDARD_REPOSITORY
+ project: myproject
+ remote_repository_config: []
+ repository_id: myregistry
+ terraform_labels:
+ goog-terraform-provisioned: 'true'
+ timeouts: null
+ virtual_repository_config: []
+ module.additive_iam.google_artifact_registry_repository_iam_member.members["ci-admin"]:
+ condition: []
+ location: europe-west1
+ member: group:cicd@example.com
+ project: myproject
+ role: roles/artifactregistry.admin
+ module.additive_iam.google_artifact_registry_repository_iam_member.members["ci-read"]:
+ condition: []
+ location: europe-west1
+ member: group:cicd-read@example.com
+ project: myproject
+ role: roles/artifactregistry.reader
+ module.authoritative_iam.google_artifact_registry_repository.registry:
+ cleanup_policies: []
+ cleanup_policy_dry_run: null
+ description: Terraform-managed registry
+ docker_config: []
+ effective_labels:
+ goog-terraform-provisioned: 'true'
+ format: DOCKER
+ kms_key_name: null
+ labels: null
+ location: europe-west1
+ maven_config: []
+ mode: STANDARD_REPOSITORY
+ project: myproject
+ remote_repository_config: []
+ repository_id: myregistry
+ terraform_labels:
+ goog-terraform-provisioned: 'true'
+ timeouts: null
+ virtual_repository_config: []
+ module.authoritative_iam.google_artifact_registry_repository_iam_binding.authoritative["roles/artifactregistry.admin"]:
+ condition: []
+ location: europe-west1
+ members:
+ - group:cicd@example.com
+ project: myproject
+ role: roles/artifactregistry.admin
+ module.authoritative_iam_conditions.google_artifact_registry_repository.registry:
+ cleanup_policies: []
+ cleanup_policy_dry_run: null
+ description: Terraform-managed registry
+ docker_config: []
+ effective_labels:
+ goog-terraform-provisioned: 'true'
+ format: DOCKER
+ kms_key_name: null
+ labels: null
+ location: europe-west1
+ maven_config: []
+ mode: STANDARD_REPOSITORY
+ project: myproject
+ remote_repository_config: []
+ repository_id: myregistry
+ terraform_labels:
+ goog-terraform-provisioned: 'true'
+ timeouts: null
+ virtual_repository_config: []
+ module.authoritative_iam_conditions.google_artifact_registry_repository_iam_binding.bindings2["ci-admin"]:
+ condition: []
+ location: europe-west1
+ members:
+ - group:cicd@example.com
+ project: myproject
+ role: roles/artifactregistry.admin
+
+counts:
+ google_artifact_registry_repository: 3
+ google_artifact_registry_repository_iam_binding: 2
+ google_artifact_registry_repository_iam_member: 2
+ modules: 3
+ resources: 7
+
+outputs: {}