From cb234fd35bacf160f419b816ca8544a41ee9f415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Sauv=C3=A8re?= Date: Fri, 4 Oct 2024 15:49:48 +0200 Subject: [PATCH] feat: implement the new iam interface in `artifact-registry` (#2606) Co-authored-by: Julio Castillo --- modules/artifact-registry/README.md | 89 ++++++++++++-- modules/artifact-registry/iam.tf | 66 +++++++++++ modules/artifact-registry/main.tf | 10 -- modules/artifact-registry/outputs.tf | 16 ++- modules/artifact-registry/variables-iam.tf | 78 +++++++++++++ modules/artifact-registry/variables.tf | 6 - .../artifact_registry/examples/iam.yaml | 110 ++++++++++++++++++ 7 files changed, 347 insertions(+), 28 deletions(-) create mode 100644 modules/artifact-registry/iam.tf create mode 100644 modules/artifact-registry/variables-iam.tf create mode 100644 tests/modules/artifact_registry/examples/iam.yaml 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: {}