From c996285b2661864c2c7262f33747cc3c594204b4 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Fri, 10 Oct 2025 18:59:37 +0200 Subject: [PATCH] Support context and add configurations factory to workstation cluster module, add FAST project template (#3401) * add context to workstation-cluster module * context test * workstations project template --- fast/project-templates/README.md | 3 + .../gce-workstation-cluster/README.md | 97 +++++ .../data/workstation-configs/default.yaml | 34 ++ .../gce-workstation-cluster/main.tf | 50 +++ .../gce-workstation-cluster/outputs.tf | 20 + .../gce-workstation-cluster/project.yaml | 69 +++ .../gce-workstation-cluster/variables.tf | 105 +++++ .../os-apt-registries/project.yaml | 3 +- modules/project-factory/projects-defaults.tf | 51 +-- modules/workstation-cluster/README.md | 20 +- modules/workstation-cluster/factory.tf | 107 +++++ modules/workstation-cluster/iam.tf | 178 +++++--- modules/workstation-cluster/main.tf | 81 +++- .../schemas/workstation-config.schema.json | 409 ++++++++++++++++++ modules/workstation-cluster/variables.tf | 68 ++- .../workstation_cluster/context.tfvars | 71 +++ .../modules/workstation_cluster/context.yaml | 117 +++++ tests/modules/workstation_cluster/tftest.yaml | 17 + 18 files changed, 1342 insertions(+), 158 deletions(-) create mode 100644 fast/project-templates/README.md create mode 100644 fast/project-templates/gce-workstation-cluster/README.md create mode 100644 fast/project-templates/gce-workstation-cluster/data/workstation-configs/default.yaml create mode 100644 fast/project-templates/gce-workstation-cluster/main.tf create mode 100644 fast/project-templates/gce-workstation-cluster/outputs.tf create mode 100644 fast/project-templates/gce-workstation-cluster/project.yaml create mode 100644 fast/project-templates/gce-workstation-cluster/variables.tf create mode 100644 modules/workstation-cluster/factory.tf create mode 100644 modules/workstation-cluster/schemas/workstation-config.schema.json create mode 100644 tests/modules/workstation_cluster/context.tfvars create mode 100644 tests/modules/workstation_cluster/context.yaml create mode 100644 tests/modules/workstation_cluster/tftest.yaml diff --git a/fast/project-templates/README.md b/fast/project-templates/README.md new file mode 100644 index 000000000..e0156fd03 --- /dev/null +++ b/fast/project-templates/README.md @@ -0,0 +1,3 @@ +# FAST Project Templates + +Each of the folders contained here is a separate Terraform configuration, that can be used to bring up specific sets of resources. All the configurations include a `project.yaml` file in project factory format, that can be directly used after some light edits to bring up a project with the required prerequisites. The project file can also be used as documentation to create a suitable project via other means, where a project factory is not available. diff --git a/fast/project-templates/gce-workstation-cluster/README.md b/fast/project-templates/gce-workstation-cluster/README.md new file mode 100644 index 000000000..1677c69a0 --- /dev/null +++ b/fast/project-templates/gce-workstation-cluster/README.md @@ -0,0 +1,97 @@ +# Cloud Workstations Cluster + +This simple setup allows creating and configuring one Cloud Workstation Cluster, and an arbitrary number of workstation configurations and workstations via a dedicated factory. + +## Prerequisites + +The [`project.yaml`](./project.yaml) file describes the project-level configuration needed in terms of API activation and IAM bindings. + +If you are deploying this inside a FAST-enabled organization, the file can be lightly edited to match your configuration, and then used directly in the [project factory](../../stages/2-project-factory/). + +This Terraform can of course be deployed using any pre-existing project. In that case use the YAML file to determine the configuration you need to set on the project: + +- enable the APIs listed under `services` +- grant the permissions listed under `iam` to the principal running Terraform, either machine (service account) or human + +## VPC-SC Integration + +This example assumes a private cluster is needed, and provisions a PSC Endpoint for private connectivity. For more details on private clusters and VPC-SC see [this documentation page](https://cloud.google.com/workstations/docs/configure-vpc-service-controls-private-clusters). + +An additional egress policy is needed to allow monitoring traffic for the cluster to the tenant project on the Google side. The following snippet can be added to the egress policy factory in the VPC-SC stage, and editied so that project numbers match. It should of course also be enabled in the perimeter definition. + +```yaml +from: + identities: + - serviceAccount:service-1234567890@gcp-sa-workstations.iam.gserviceaccount.com + resources: + - projects/3456789012 +to: + operations: + - service_name: monitoring.googleapis.com + method_selectors: + - "*" + resources: + - projects/1234567890 +``` + +## Additional Configuration Steps + +The workstations are accessible via the PSC Endpoint, once a DNS record for the cluster hostname has been configured. The cluster hostname is available from this example's outputs. + +## Variable Configuration + +This is an example of running this stage. Note that the `apt_remote_registries` has a default value that can be used when no IAM is needed at the registry level, and the default set of remotes is fine. + +```hcl +project_id = "my-project" +location = "europe-west3" +network_config = { + network = "projects/ldj-prod-net-landing-0/global/networks/prod-landing-0" + subnetwork = "projects/ldj-prod-net-landing-0/regions/europe-west8/subnetworks/ws" + psc_endpoint_address = "10.0.18.10" +} +# tftest skip +``` + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [network_config](variables.tf#L72) | VPC and subnet for the cluster. | object({…}) | ✓ | | +| [project_id](variables.tf#L92) | Project id where the cluster will be created. | string | ✓ | | +| [annotations](variables.tf#L17) | Workstation cluster annotations. | map(string) | | {} | +| [context](variables.tf#L23) | Context-specific interpolations. | object({…}) | | {} | +| [display_name](variables.tf#L38) | Display name. | string | | null | +| [domain](variables.tf#L44) | Domain. | string | | null | +| [factories_config](variables.tf#L50) | Path to folder with YAML resource description data files. | object({…}) | | {} | +| [id](variables.tf#L59) | Workstation cluster ID. | string | | "ws-cluster-0" | +| [labels](variables.tf#L66) | Workstation cluster labels. | map(string) | | {} | +| [private_cluster_config](variables.tf#L82) | Private cluster config. | object({…}) | | {} | +| [service_accounts](variables.tf#L98) | Project factory managed service accounts to populate context. | map(object({…})) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [hostname](outputs.tf#L17) | Cluster hostname. | | + +## Test + +```hcl +module "test" { + source = "./fabric/fast/project-templates/os-apt-registries" + project_id = "my-project" + location = "europe-west3" + apt_remote_registries = [ + { path = "DEBIAN debian/dists/bookworm" }, + { + path = "DEBIAN debian-security/dists/bookworm-security" + # grant specific access permissions to this registry + writer_principals = [ + "serviceAccount:vm-default@prod-proj-0.iam.gserviceaccount.com" + ] + } + ] +} +# tftest modules=3 resources=4 +``` diff --git a/fast/project-templates/gce-workstation-cluster/data/workstation-configs/default.yaml b/fast/project-templates/gce-workstation-cluster/data/workstation-configs/default.yaml new file mode 100644 index 000000000..e9d4aa616 --- /dev/null +++ b/fast/project-templates/gce-workstation-cluster/data/workstation-configs/default.yaml @@ -0,0 +1,34 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# yaml-language-server: $schema=../../../../../modules/workstation-cluster/schemas/workstation-config.schema.json + +display_name: Default configuration. +container: + image: us-central1-docker.pkg.dev/cloud-workstations-images/predefined/code-oss:latest +gce_instance: + disable_public_ip_addresses: true + machine_type: e2-medium + service_account: $iam_principals:service_accounts/prod-gce-ws-0/ws-default + service_account_scopes: + - https://www.googleapis.com/auth/cloud-platform +persistent_directories: + - mount_path: /home + gce_pd: + size_gb: 10 + fs_type: ext4 + disk_type: pd-balanced + reclaim_policy: DELETE +workstations: + test-0: {} \ No newline at end of file diff --git a/fast/project-templates/gce-workstation-cluster/main.tf b/fast/project-templates/gce-workstation-cluster/main.tf new file mode 100644 index 000000000..879daac69 --- /dev/null +++ b/fast/project-templates/gce-workstation-cluster/main.tf @@ -0,0 +1,50 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + location = reverse(split("/", var.network_config.subnetwork))[2] +} + +module "cluster" { + source = "../../../modules/workstation-cluster" + project_id = var.project_id + id = var.id + location = local.location + network_config = var.network_config + private_cluster_config = var.private_cluster_config + factories_config = var.factories_config + context = merge(var.context, { + iam_principals = merge(var.context.iam_principals, { + for k, v in var.service_accounts : "service_accounts/${k}" => v.email + }) + }) +} + +module "ws-addresses" { + source = "../../../modules/net-address" + count = var.network_config.psc_endpoint_address != null ? 1 : 0 + project_id = var.project_id + psc_addresses = { + ws-cluster-0 = { + address = var.network_config.psc_endpoint_address + subnet_self_link = var.network_config.subnetwork + region = local.location + service_attachment = { + psc_service_attachment_link = module.cluster.service_attachment_uri + } + } + } +} diff --git a/fast/project-templates/gce-workstation-cluster/outputs.tf b/fast/project-templates/gce-workstation-cluster/outputs.tf new file mode 100644 index 000000000..18b2d610a --- /dev/null +++ b/fast/project-templates/gce-workstation-cluster/outputs.tf @@ -0,0 +1,20 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "hostname" { + description = "Cluster hostname." + value = module.cluster.cluster_hostname +} diff --git a/fast/project-templates/gce-workstation-cluster/project.yaml b/fast/project-templates/gce-workstation-cluster/project.yaml new file mode 100644 index 000000000..fa314514a --- /dev/null +++ b/fast/project-templates/gce-workstation-cluster/project.yaml @@ -0,0 +1,69 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# yaml-language-server: $schema=../..//stages/2-project-factory/schemas/project.schema.json + +# TODO: edit and uncomment the following line to create the project in a folder +# parent: $folder_ids:shared +services: + - artifactregistry.googleapis.com + - compute.googleapis.com + - servicedirectory.googleapis.com + - workstations.googleapis.com +automation: + # TODO: edit the automation project and optionally edit resource names + project: $project_ids:iac-0 + service_accounts: + rw: + description: Read/write automation service account for workstations. + bucket: + # this reuses the existing stage state bucket and creates a folder in it + name: iac-stage-state + create: false + managed_folders: + gce-workstation-cluster: + iam: + roles/storage.objectCreator: + # TODO: the project id in the service account ref matches this file name + - $iam_principals:service_accounts/gce-workstation-cluster/automation/rw + roles/storage.objectViewer: + - $iam_principals:service_accounts/gce-workstation-cluster/automation/rw +iam_by_principals: + # TODO: the project id in the service account ref matches this file name + $iam_principals:service_accounts/gce-workstation-cluster/automation/rw: + - roles/compute.admin + - roles/iam.serviceAccountUser + - roles/servicedirectory.admin + - roles/workstations.admin + $iam_principals:service_accounts/gce-workstation-cluster/ws-default: + - roles/logging.logWriter + - roles/monitoring.metricWriter +# org_policies: +# compute.restrictSharedVpcSubnetworks: +# rules: +# - allow: +# values: +# - ${subnet_self_links["prod-landing/europe-west8/ws"]} +service_accounts: + ws-default: + display_name: Workstations default service account. +shared_vpc_service_config: + # TODO: edit the host project + host_project: $project_ids:prod-landing + network_users: + - $iam_principals:service_accounts/gce-workstation-cluster/automation/rw + service_agent_iam: + roles/compute.networkUser: + - $service_agents:compute + - $service_agents:workstations diff --git a/fast/project-templates/gce-workstation-cluster/variables.tf b/fast/project-templates/gce-workstation-cluster/variables.tf new file mode 100644 index 000000000..702b9247b --- /dev/null +++ b/fast/project-templates/gce-workstation-cluster/variables.tf @@ -0,0 +1,105 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "annotations" { + description = "Workstation cluster annotations." + type = map(string) + default = {} +} + +variable "context" { + description = "Context-specific interpolations." + type = object({ + condition_vars = optional(map(map(string)), {}) + custom_roles = optional(map(string), {}) + iam_principals = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + subnetworks = optional(map(string), {}) + }) + default = {} + nullable = false +} + +variable "display_name" { + description = "Display name." + type = string + default = null +} + +variable "domain" { + description = "Domain." + type = string + default = null +} + +variable "factories_config" { + description = "Path to folder with YAML resource description data files." + type = object({ + workstation_configs = optional(string, "data/workstation-configs") + }) + nullable = false + default = {} +} + +variable "id" { + description = "Workstation cluster ID." + type = string + nullable = false + default = "ws-cluster-0" +} + +variable "labels" { + description = "Workstation cluster labels." + type = map(string) + default = {} +} + +variable "network_config" { + description = "VPC and subnet for the cluster." + nullable = false + type = object({ + network = string + subnetwork = string + psc_endpoint_address = optional(string) + }) +} + +variable "private_cluster_config" { + description = "Private cluster config." + type = object({ + allowed_projects = optional(list(string)) + enable_private_endpoint = optional(bool, true) + }) + nullable = false + default = {} +} + +variable "project_id" { + description = "Project id where the cluster will be created." + type = string + nullable = false +} + +variable "service_accounts" { + description = "Project factory managed service accounts to populate context." + type = map(object({ + email = string + })) + nullable = false + default = {} +} diff --git a/fast/project-templates/os-apt-registries/project.yaml b/fast/project-templates/os-apt-registries/project.yaml index 8ce64d571..b701d4028 100644 --- a/fast/project-templates/os-apt-registries/project.yaml +++ b/fast/project-templates/os-apt-registries/project.yaml @@ -31,7 +31,6 @@ automation: rw: description: Read/write automation service account for apt registries. bucket: - description: Terraform state bucket for apt registries. # this reuses the existing stage state bucket and creates a folder in it name: iac-stage-state create: false @@ -39,7 +38,7 @@ automation: os-apt: iam: roles/storage.objectCreator: - # the project id in the service account ref matches this file name + # TODO: the project id in the service account ref matches this file name - $iam_principals:service_accounts/os-apt-registries/automation/rw roles/storage.objectViewer: - $iam_principals:service_accounts/os-apt-registries/automation/rw diff --git a/modules/project-factory/projects-defaults.tf b/modules/project-factory/projects-defaults.tf index 09b1ccbfc..a12f0756f 100644 --- a/modules/project-factory/projects-defaults.tf +++ b/modules/project-factory/projects-defaults.tf @@ -140,18 +140,15 @@ locals { ) shared_vpc_service_config = ( # type: object({...}) try(v.shared_vpc_service_config, null) != null - ? merge( - { - host_project = null - iam_bindings_additive = {} - network_users = [] - service_agent_iam = {} - service_agent_subnet_iam = {} - service_iam_grants = [] - network_subnet_users = {} - }, - v.shared_vpc_service_config - ) + ? { + host_project = try(v.shared_vpc_service_config.host_project, null) + iam_bindings_additive = try(v.shared_vpc_service_config.iam_bindings_additive, {}) + network_users = try(v.shared_vpc_service_config.network_users, []) + service_agent_iam = try(v.shared_vpc_service_config.service_agent_iam, {}) + service_agent_subnet_iam = try(v.shared_vpc_service_config.service_agent_subnet_iam, {}) + service_iam_grants = try(v.shared_vpc_service_config.service_iam_grants, []) + network_subnet_users = try(v.shared_vpc_service_config.network_subnet_users, {}) + } : local.data_defaults.defaults.shared_vpc_service_config ) tag_bindings = coalesce( # type: map(string) @@ -267,27 +264,15 @@ locals { ) service_encryption_key_ids = {} services = [] - shared_vpc_service_config = merge( - { - host_project = null - iam_bindings_additive = {} - network_users = [] - service_agent_iam = {} - service_agent_subnet_iam = {} - service_iam_grants = [] - network_subnet_users = {} - }, - try(local._data_defaults.defaults.shared_vpc_service_config, { - host_project = null - iam_bindings_additive = {} - network_users = [] - service_agent_iam = {} - service_agent_subnet_iam = {} - service_iam_grants = [] - network_subnet_users = {} - } - ) - ) + shared_vpc_service_config = { + host_project = try(local._data_defaults.defaults.shared_vpc_service_config.host_project, null) + iam_bindings_additive = try(local._data_defaults.defaults.shared_vpc_service_config.iam_bindings_additive, {}) + network_users = try(local._data_defaults.defaults.shared_vpc_service_config.network_users, []) + service_agent_iam = try(local._data_defaults.defaults.shared_vpc_service_config.service_agent_iam, {}) + service_agent_subnet_iam = try(local._data_defaults.defaults.shared_vpc_service_config.service_agent_subnet_iam, {}) + service_iam_grants = try(local._data_defaults.defaults.shared_vpc_service_config.service_iam_grants, []) + network_subnet_users = try(local._data_defaults.defaults.shared_vpc_service_config.network_subnet_users, {}) + } tag_bindings = {} service_accounts = {} universe = null diff --git a/modules/workstation-cluster/README.md b/modules/workstation-cluster/README.md index e6f690905..c59cf17f3 100644 --- a/modules/workstation-cluster/README.md +++ b/modules/workstation-cluster/README.md @@ -172,16 +172,18 @@ module "workstation-cluster" { | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [id](variables.tf#L35) | Workstation cluster ID. | string | ✓ | | -| [location](variables.tf#L46) | Location. | string | ✓ | | -| [network_config](variables.tf#L51) | Network configuration. | object({…}) | ✓ | | -| [project_id](variables.tf#L69) | Cluster ID. | string | ✓ | | -| [workstation_configs](variables.tf#L74) | Workstation configurations. | map(object({…})) | ✓ | | +| [id](variables.tf#L59) | Workstation cluster ID. | string | ✓ | | +| [location](variables.tf#L70) | Location. | string | ✓ | | +| [network_config](variables.tf#L75) | Network configuration. | object({…}) | ✓ | | +| [project_id](variables.tf#L93) | Cluster ID. | string | ✓ | | | [annotations](variables.tf#L17) | Workstation cluster annotations. | map(string) | | {} | -| [display_name](variables.tf#L23) | Display name. | string | | null | -| [domain](variables.tf#L29) | Domain. | string | | null | -| [labels](variables.tf#L40) | Workstation cluster labels. | map(string) | | {} | -| [private_cluster_config](variables.tf#L59) | Private cluster config. | object({…}) | | {} | +| [context](variables.tf#L23) | Context-specific interpolations. | object({…}) | | {} | +| [display_name](variables.tf#L38) | Display name. | string | | null | +| [domain](variables.tf#L44) | Domain. | string | | null | +| [factories_config](variables.tf#L50) | Path to folder with YAML resource description data files. | object({…}) | | {} | +| [labels](variables.tf#L64) | Workstation cluster labels. | map(string) | | {} | +| [private_cluster_config](variables.tf#L83) | Private cluster config. | object({…}) | | {} | +| [workstation_configs](variables.tf#L98) | Workstation configurations. | map(object({…})) | | {} | ## Outputs diff --git a/modules/workstation-cluster/factory.tf b/modules/workstation-cluster/factory.tf new file mode 100644 index 000000000..bb1716081 --- /dev/null +++ b/modules/workstation-cluster/factory.tf @@ -0,0 +1,107 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + _f_files = try(fileset(local._f_paths.workstation_configs, "*.yaml"), []) + _f_paths = { + for k, v in var.factories_config : k => v == null ? null : pathexpand(v) + } + _f_raw = { + for f in local._f_files : trimsuffix(f, ".yaml") => yamldecode(file( + "${local._f_paths.workstation_configs}/${f}" + )) + } + f_workstation_configs = { + for k, v in local._f_raw : k => { + annotations = try(v.annotations, null) + display_name = try(v.display_name, null) + enable_audit_agent = try(v.enable_audit_agent, null) + iam = try(v.iam, {}) + iam_bindings = try(v.iam_bindings, {}) + iam_bindings_additive = try(v.iam_bindings_additive, {}) + labels = try(v.labels, null) + max_workstations = try(v.max_workstations, null) + replica_zones = try(v.replica_zones, null) + timeouts = try(v.timeouts, {}) + container = ( + lookup(v, "container", null) == null ? null : { + args = try(v.container.args, []) + command = try(v.container.command, []) + env = try(v.container.env, {}) + image = try(v.container.image, null) + run_as_user = try(v.container.run_as_user, null) + working_dir = try(v.container.working_dir, null) + } + ) + encryption_key = ( + lookup(v, "encryption_key", null) == null ? null : { + kms_key = try(v.encryption_key.kms_key, null) + kms_key_service_account = try(v.encryption_key.kms_key_service_account, null) + } + ) + gce_instance = ( + lookup(v, "gce_instance", null) == null ? null : { + boot_disk_size_gb = try(v.gce_instance.boot_disk_size_gb, null) + disable_public_ip_addresses = try(v.gce_instance.disable_public_ip_addresses, false) + enable_confidential_compute = try(v.gce_instance.enable_confidential_compute, false) + enable_nested_virtualization = try(v.gce_instance.enable_nested_virtualization, false) + machine_type = try(v.gce_instance.machine_type, null) + pool_size = try(v.gce_instance.pool_size, null) + service_account = try(v.gce_instance.service_account, null) + service_account_scopes = try(v.gce_instance.service_account_scopes, null) + tags = try(v.gce_instance.tags, null) + accelerators = try(v.gce_instance.accelerators, []) + shielded_instance_config = ( + try(v.gce_instance.shielded_instance_config, null) == null ? null : { + enable_secure_boot = try( + v.gce_instance.shielded_instance_config.enable_secure_boot, false + ) + enable_vtpm = try( + v.gce_instance.shielded_instance_config.enable_vtpm, false + ) + enable_integrity_monitoring = try( + v.gce_instance.shielded_instance_config.enable_integrity_monitoring, false + ) + } + ) + } + ) + persistent_directories = [ + for vv in try(v.persistent_directories, []) : { + mount_path = try(vv.mount_path, null) + gce_pd = try(vv.gce_pd, null) == null ? null : { + size_gb = try(vv.gce_pd.size_gb, null) + fs_type = try(vv.gce_pd.fs_type, null) + disk_type = try(vv.gce_pd.disk_type, null) + source_snapshot = try(vv.gce_pd.source_snapshot, null) + reclaim_policy = try(vv.gce_pd.reclaim_policy, null) + } + } + ] + workstations = { + for kk, vv in try(v.workstations, {}) : kk => { + annotations = try(vv.annotations, null) + display_name = try(vv.display_name, null) + env = try(vv.env, null) + labels = try(vv.labels, null) + iam = try(vv.iam, {}) + iam_bindings = try(vv.iam_bindings, {}) + iam_bindings_additive = try(vv.iam_bindings_additive, {}) + } + } + } + } +} diff --git a/modules/workstation-cluster/iam.tf b/modules/workstation-cluster/iam.tf index 491c5f212..058d9899a 100644 --- a/modules/workstation-cluster/iam.tf +++ b/modules/workstation-cluster/iam.tf @@ -17,105 +17,143 @@ # tfdoc:file:description IAM bindings locals { - workstation_config_iam = merge([for k1, v1 in var.workstation_configs : { for k2, v2 in v1.iam : - "${k1}-${k2}" => { - workstation_config_id = k1 - role = k2 - members = v2 - } }]...) - workstation_config_iam_bindings = merge([for k1, v1 in var.workstation_configs : { for k2, v2 in v1.iam_bindings : - "${k1}-${k2}" => merge(v2, { - workstation_config_id = k1 - }) }]...) - workstation_config_iam_bindings_additive = merge([for k1, v1 in var.workstation_configs : { for k2, v2 in v1.iam_bindings_additive : - "${k1}-${k2}" => merge(v2, { - workstation_config_id = k1 - }) }]...) - workstation_iam = merge(flatten([for k1, v1 in var.workstation_configs : [for k2, v2 in v1.workstations : - { for k3, v3 in v2.iam : "${k1}-${k2}-${k3}" => { - workstation_config_id = k1 - workstation_id = k2 - role = k3 - members = v3 - } }]])...) - workstation_iam_bindings = merge(flatten([for k1, v1 in var.workstation_configs : [for k2, v2 in v1.workstations : - { for k3, v3 in v2.iam_bindings : "${k1}-${k2}-${k3}" => merge(v3, { - workstation_config_id = k1 - workstation_id = k2 - }) }]])...) - workstation_iam_bindings_additive = merge(flatten([for k1, v1 in var.workstation_configs : [for k2, v2 in v1.workstations : - { for k3, v3 in v2.iam_bindings_additive : "${k1}-${k2}-${k3}" => merge(v3, { - workstation_config_id = k1 - workstation_id = k2 - }) }]])...) + gwcc = google_workstations_workstation_config.configs + gwcw = google_workstations_workstation.workstations + workstation_config_iam = merge([ + for k1, v1 in local.workstation_configs : { + for k2, v2 in v1.iam : "${k1}-${k2}" => { + workstation_config_id = k1 + role = k2 + members = v2 + } + } + ]...) + workstation_config_iam_bindings = merge([ + for k1, v1 in local.workstation_configs : { + for k2, v2 in v1.iam_bindings : "${k1}-${k2}" => merge(v2, { + workstation_config_id = k1 + }) + } + ]...) + workstation_config_iam_bindings_additive = merge([ + for k1, v1 in local.workstation_configs : { + for k2, v2 in v1.iam_bindings_additive : "${k1}-${k2}" => merge(v2, { + workstation_config_id = k1 + }) + } + ]...) + workstation_iam = merge(flatten([ + for k1, v1 in local.workstation_configs : [ + for k2, v2 in v1.workstations : { + for k3, v3 in v2.iam : "${k1}-${k2}-${k3}" => { + workstation_config_id = k1 + workstation_id = k2 + role = k3 + members = v3 + } + } + ] + ])...) + workstation_iam_bindings = merge(flatten([ + for k1, v1 in local.workstation_configs : [ + for k2, v2 in v1.workstations : { + for k3, v3 in v2.iam_bindings : "${k1}-${k2}-${k3}" => merge(v3, { + workstation_config_id = k1 + workstation_id = k2 + }) + } + ] + ])...) + workstation_iam_bindings_additive = merge(flatten([ + for k1, v1 in local.workstation_configs : [ + for k2, v2 in v1.workstations : { + for k3, v3 in v2.iam_bindings_additive : "${k1}-${k2}-${k3}" => merge(v3, { + workstation_config_id = k1 + workstation_id = k2 + }) + } + ] + ])...) } resource "google_workstations_workstation_config_iam_binding" "authoritative" { provider = google-beta for_each = local.workstation_config_iam - project = google_workstations_workstation_config.configs[each.value.workstation_config_id].project - location = google_workstations_workstation_config.configs[each.value.workstation_config_id].location - workstation_cluster_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_cluster_id - workstation_config_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_config_id - role = each.value.role - members = each.value.members + project = local.gwcc[each.value.workstation_config_id].project + location = local.gwcc[each.value.workstation_config_id].location + workstation_cluster_id = local.gwcc[each.value.workstation_config_id].workstation_cluster_id + workstation_config_id = local.gwcc[each.value.workstation_config_id].workstation_config_id + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + members = [ + for v in each.value.members : lookup(local.ctx.iam_principals, v, v) + ] } resource "google_workstations_workstation_config_iam_binding" "bindings" { provider = google-beta for_each = local.workstation_config_iam_bindings - project = google_workstations_workstation_config.configs[each.value.workstation_config_id].project - location = google_workstations_workstation_config.configs[each.value.workstation_config_id].location - workstation_cluster_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_cluster_id - workstation_config_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_config_id - role = each.value.role - members = each.value.members + project = local.gwcc[each.value.workstation_config_id].project + location = local.gwcc[each.value.workstation_config_id].location + workstation_cluster_id = local.gwcc[each.value.workstation_config_id].workstation_cluster_id + workstation_config_id = local.gwcc[each.value.workstation_config_id].workstation_config_id + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + members = [ + for v in each.value.members : lookup(local.ctx.iam_principals, v, v) + ] } resource "google_workstations_workstation_config_iam_member" "bindings" { provider = google-beta for_each = local.workstation_config_iam_bindings_additive - project = google_workstations_workstation_config.configs[each.value.workstation_config_id].project - location = google_workstations_workstation_config.configs[each.value.workstation_config_id].location - workstation_cluster_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_cluster_id - workstation_config_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_config_id - role = each.value.role - member = each.value.member + project = local.gwcc[each.value.workstation_config_id].project + location = local.gwcc[each.value.workstation_config_id].location + workstation_cluster_id = local.gwcc[each.value.workstation_config_id].workstation_cluster_id + workstation_config_id = local.gwcc[each.value.workstation_config_id].workstation_config_id + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + member = lookup( + local.ctx.iam_principals, each.value.member, each.value.member + ) } resource "google_workstations_workstation_iam_binding" "authoritative" { provider = google-beta for_each = local.workstation_iam - project = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].project - location = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].location - workstation_cluster_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id - workstation_config_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id - workstation_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id - role = each.value.role - members = each.value.members + project = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].project + location = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].location + workstation_cluster_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id + workstation_config_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id + workstation_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + members = [ + for v in each.value.members : lookup(local.ctx.iam_principals, v, v) + ] } resource "google_workstations_workstation_iam_binding" "bindings" { provider = google-beta for_each = local.workstation_iam_bindings - project = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].project - location = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].location - workstation_cluster_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id - workstation_config_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id - workstation_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id - role = each.value.role - members = each.value.members + project = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].project + location = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].location + workstation_cluster_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id + workstation_config_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id + workstation_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + members = [ + for v in each.value.members : lookup(local.ctx.iam_principals, v, v) + ] } resource "google_workstations_workstation_iam_member" "bindings" { provider = google-beta for_each = local.workstation_iam_bindings_additive - project = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].project - location = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].location - workstation_cluster_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id - workstation_config_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id - workstation_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id - role = each.value.role - member = each.value.member + project = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].project + location = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].location + workstation_cluster_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id + workstation_config_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id + workstation_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id + role = lookup(local.ctx.custom_roles, each.value.role, each.value.role) + member = lookup( + local.ctx.iam_principals, each.value.member, each.value.member + ) } diff --git a/modules/workstation-cluster/main.tf b/modules/workstation-cluster/main.tf index 9a1d15d31..8a662ea32 100644 --- a/modules/workstation-cluster/main.tf +++ b/modules/workstation-cluster/main.tf @@ -15,24 +15,43 @@ */ locals { - workstations = merge(flatten([for k1, v1 in var.workstation_configs : - { for k2, v2 in v1.workstations : - "${k1}-${k2}" => merge({ + ctx = { + for k, v in var.context : k => { + for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv + } if k != "condition_vars" + } + ctx_p = "$" + workstation_configs = merge( + var.workstation_configs, local.f_workstation_configs + ) + workstations = merge(flatten([ + for k1, v1 in local.workstation_configs : { + for k2, v2 in v1.workstations : "${k1}-${k2}" => merge({ workstation_config_id = k1 workstation_id = k2 - }, v2) }])...) + }, v2) + } + ])...) } resource "google_workstations_workstation_cluster" "cluster" { provider = google-beta workstation_cluster_id = var.id - project = var.project_id - display_name = var.display_name - network = var.network_config.network - subnetwork = var.network_config.subnetwork - location = var.location - annotations = var.annotations - labels = var.labels + project = lookup( + local.ctx.project_ids, var.project_id, var.project_id + ) + display_name = var.display_name + location = lookup( + local.ctx.locations, var.location, var.location + ) + network = lookup( + local.ctx.networks, var.network_config.network, var.network_config.network + ) + subnetwork = lookup( + local.ctx.subnetworks, var.network_config.subnetwork, var.network_config.subnetwork + ) + annotations = var.annotations + labels = var.labels dynamic "private_cluster_config" { for_each = var.private_cluster_config == null ? [] : [""] content { @@ -49,29 +68,37 @@ resource "google_workstations_workstation_cluster" "cluster" { } resource "google_workstations_workstation_config" "configs" { - for_each = var.workstation_configs + for_each = local.workstation_configs provider = google-beta project = google_workstations_workstation_cluster.cluster.project - workstation_config_id = each.key - workstation_cluster_id = google_workstations_workstation_cluster.cluster.workstation_cluster_id location = google_workstations_workstation_cluster.cluster.location + workstation_cluster_id = google_workstations_workstation_cluster.cluster.workstation_cluster_id + workstation_config_id = each.key + annotations = each.value.annotations display_name = each.value.display_name + labels = each.value.labels max_usable_workstations = each.value.max_workstations + replica_zones = each.value.replica_zones idle_timeout = ( - each.value.timeouts.idle == null ? null : "${each.value.timeouts.idle}s" + try(each.value.timeouts.idle, null) == null + ? null + : "${each.value.timeouts.idle}s" ) running_timeout = ( - each.value.timeouts.running == null ? null : "${each.value.timeouts.running}s" + try(each.value.timeouts.running, null) == null + ? null : + "${each.value.timeouts.running}s" ) - replica_zones = each.value.replica_zones - annotations = each.value.annotations - labels = each.value.labels dynamic "host" { for_each = each.value.gce_instance == null ? [] : [""] content { gce_instance { - machine_type = each.value.gce_instance.machine_type - service_account = each.value.gce_instance.service_account + machine_type = each.value.gce_instance.machine_type + service_account = each.value.gce_instance.service_account == null ? null : lookup( + local.ctx.iam_principals, + each.value.gce_instance.service_account, + each.value.gce_instance.service_account + ) service_account_scopes = each.value.gce_instance.service_account_scopes pool_size = each.value.gce_instance.pool_size boot_disk_size_gb = each.value.gce_instance.boot_disk_size_gb @@ -116,8 +143,16 @@ resource "google_workstations_workstation_config" "configs" { dynamic "encryption_key" { for_each = each.value.encryption_key == null ? [] : [""] content { - kms_key = each.value.encryption_key.kms_key - kms_key_service_account = each.value.encryption_key.kms_key_service_account + kms_key = each.value.encryption_key.kms_key + kms_key_service_account = ( + each.value.encryption_key.kms_key_service_account == null + ? null + : lookup( + local.ctx.iam_principals, + each.value.encryption_key.kms_key_service_account, + each.value.encryption_key.kms_key_service_account + ) + ) } } dynamic "persistent_directories" { diff --git a/modules/workstation-cluster/schemas/workstation-config.schema.json b/modules/workstation-cluster/schemas/workstation-config.schema.json new file mode 100644 index 000000000..8a93ed156 --- /dev/null +++ b/modules/workstation-cluster/schemas/workstation-config.schema.json @@ -0,0 +1,409 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Workstation Config", + "type": "object", + "additionalProperties": false, + "properties": { + "annotations": { + "type": "object", + "description": "Annotations for the object (optional).", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "string" + } + } + }, + "container": { + "type": "object", + "additionalProperties": false, + "description": "Container configuration (optional).", + "properties": { + "args": { + "type": "array", + "description": "Container arguments (optional, defaults to []).", + "items": { + "type": "string" + }, + "default": [] + }, + "command": { + "type": "array", + "description": "Container command (optional, defaults to []).", + "items": { + "type": "string" + }, + "default": [] + }, + "env": { + "type": "object", + "description": "Container environment variables (optional, defaults to {}).", + "additionalProperties": { + "type": "string" + }, + "default": {} + }, + "image": { + "type": "string", + "description": "Container image URL (optional)." + }, + "run_as_user": { + "type": "string", + "description": "User to run the container as (optional)." + }, + "working_dir": { + "type": "string", + "description": "Container working directory (optional)." + } + } + }, + "display_name": { + "type": "string", + "description": "Human-readable display name (optional)." + }, + "enable_audit_agent": { + "type": "boolean", + "description": "Whether to enable the audit agent (optional)." + }, + "encryption_key": { + "type": "object", + "additionalProperties": false, + "description": "Customer-managed encryption key configuration (optional).", + "properties": { + "kms_key": { + "type": "string", + "description": "The KMS key resource name (required)." + }, + "kms_key_service_account": { + "type": "string", + "description": "The service account to use for the KMS key (required)." + } + }, + "required": [ + "kms_key", + "kms_key_service_account" + ] + }, + "gce_instance": { + "type": "object", + "additionalProperties": false, + "description": "GCE instance configuration (optional).", + "properties": { + "machine_type": { + "type": "string", + "description": "Machine type (optional)." + }, + "service_account": { + "type": "string", + "description": "Service account for the GCE instance (optional)." + }, + "service_account_scopes": { + "type": "array", + "description": "Service account scopes (optional, defaults to []).", + "items": { + "type": "string" + }, + "default": [] + }, + "pool_size": { + "type": "number", + "description": "Size of the GCE instance pool (optional)." + }, + "boot_disk_size_gb": { + "type": "number", + "description": "Boot disk size in GB (optional)." + }, + "tags": { + "type": "array", + "description": "Network tags (optional).", + "items": { + "type": "string" + } + }, + "disable_public_ip_addresses": { + "type": "boolean", + "description": "Whether to disable public IP addresses (optional, defaults to false).", + "default": false + }, + "enable_nested_virtualization": { + "type": "boolean", + "description": "Whether to enable nested virtualization (optional, defaults to false).", + "default": false + }, + "shielded_instance_config": { + "type": "object", + "additionalProperties": false, + "description": "Shielded instance configuration (optional).", + "properties": { + "enable_secure_boot": { + "type": "boolean", + "description": "Whether to enable Secure Boot (optional, defaults to false).", + "default": false + }, + "enable_vtpm": { + "type": "boolean", + "description": "Whether to enable vTPM (optional, defaults to false).", + "default": false + }, + "enable_integrity_monitoring": { + "type": "boolean", + "description": "Whether to enable integrity monitoring (optional, defaults to false).", + "default": false + } + } + }, + "enable_confidential_compute": { + "type": "boolean", + "description": "Whether to enable Confidential Compute (optional, defaults to false).", + "default": false + }, + "accelerators": { + "type": "array", + "description": "Accelerator configuration (optional, defaults to []).", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "description": "Accelerator type (optional)." + }, + "count": { + "type": "number", + "description": "Number of accelerators (optional)." + } + } + }, + "default": [] + } + } + }, + "iam": { + "type": "object", + "description": "IAM policy per role for the resource (optional, defaults to {}).", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "default": {} + }, + "iam_bindings": { + "type": "object", + "description": "IAM bindings for the resource (optional, defaults to {}).", + "additionalProperties": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "The role name (required)." + }, + "members": { + "type": "array", + "description": "List of members (required).", + "items": { + "type": "string" + } + } + }, + "required": [ + "role", + "members" + ] + }, + "default": {} + }, + "iam_bindings_additive": { + "type": "object", + "description": "Additive IAM bindings for the resource (optional, defaults to {}).", + "additionalProperties": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "The role name (required)." + }, + "member": { + "type": "string", + "description": "The member (required)." + } + }, + "required": [ + "role", + "member" + ] + }, + "default": {} + }, + "labels": { + "type": "object", + "description": "Labels for the object (optional).", + "additionalProperties": { + "type": "string" + } + }, + "max_workstations": { + "type": "number", + "description": "Maximum number of workstations (optional)." + }, + "persistent_directories": { + "type": "array", + "description": "Persistent directory configurations (optional, defaults to []).", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "mount_path": { + "type": "string", + "description": "Mount path for the directory (optional)." + }, + "gce_pd": { + "type": "object", + "additionalProperties": false, + "description": "GCE persistent disk configuration (optional).", + "properties": { + "size_gb": { + "type": "number", + "description": "Size of the persistent disk in GB (optional)." + }, + "fs_type": { + "type": "string", + "description": "Filesystem type (optional)." + }, + "disk_type": { + "type": "string", + "description": "Disk type (optional)." + }, + "source_snapshot": { + "type": "string", + "description": "Source snapshot (optional)." + }, + "reclaim_policy": { + "type": "string", + "description": "Reclaim policy (optional)." + } + } + } + } + }, + "default": [] + }, + "replica_zones": { + "type": "array", + "description": "Zones for replicas (optional).", + "items": { + "type": "string" + } + }, + "timeouts": { + "type": "object", + "additionalProperties": false, + "description": "Timeout configuration (optional, defaults to {}).", + "properties": { + "idle": { + "type": "number", + "description": "Idle timeout in seconds (optional)." + }, + "running": { + "type": "number", + "description": "Running timeout in seconds (optional)." + } + }, + "default": {} + }, + "workstations": { + "type": "object", + "description": "Workstation configurations by name (optional, defaults to {}).", + "additionalProperties": { + "type": "object", + "properties": { + "annotations": { + "type": "object", + "description": "Annotations for the workstation (optional).", + "additionalProperties": { + "type": "string" + } + }, + "display_name": { + "type": "string", + "description": "Workstation display name (optional)." + }, + "env": { + "type": "object", + "description": "Environment variables (optional).", + "additionalProperties": { + "type": "string" + } + }, + "iam": { + "type": "object", + "description": "IAM policy per role for the workstation (optional, defaults to {}).", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "default": {} + }, + "iam_bindings": { + "type": "object", + "description": "IAM bindings for the workstation (optional, defaults to {}).", + "additionalProperties": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "The role name (required)." + }, + "members": { + "type": "array", + "description": "List of members (required).", + "items": { + "type": "string" + } + } + }, + "required": [ + "role", + "members" + ] + }, + "default": {} + }, + "iam_bindings_additive": { + "type": "object", + "description": "Additive IAM bindings for the workstation (optional, defaults to {}).", + "additionalProperties": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "The role name (required)." + }, + "member": { + "type": "string", + "description": "The member (required)." + } + }, + "required": [ + "role", + "member" + ] + }, + "default": {} + }, + "labels": { + "type": "object", + "description": "Labels for the workstation (optional).", + "additionalProperties": { + "type": "string" + } + } + } + }, + "default": {} + } + } +} \ No newline at end of file diff --git a/modules/workstation-cluster/variables.tf b/modules/workstation-cluster/variables.tf index 71e0bc303..38f448aa3 100644 --- a/modules/workstation-cluster/variables.tf +++ b/modules/workstation-cluster/variables.tf @@ -20,6 +20,21 @@ variable "annotations" { default = {} } +variable "context" { + description = "Context-specific interpolations." + type = object({ + condition_vars = optional(map(map(string)), {}) + custom_roles = optional(map(string), {}) + iam_principals = optional(map(string), {}) + locations = optional(map(string), {}) + networks = optional(map(string), {}) + project_ids = optional(map(string), {}) + subnetworks = optional(map(string), {}) + }) + default = {} + nullable = false +} + variable "display_name" { description = "Display name." type = string @@ -32,6 +47,15 @@ variable "domain" { default = null } +variable "factories_config" { + description = "Path to folder with YAML resource description data files." + type = object({ + workstation_configs = optional(string) + }) + nullable = false + default = {} +} + variable "id" { description = "Workstation cluster ID." type = string @@ -74,40 +98,43 @@ variable "project_id" { variable "workstation_configs" { description = "Workstation configurations." type = map(object({ - annotations = optional(map(string)) - container = optional(object({ - image = optional(string) - command = optional(list(string), []) - args = optional(list(string), []) - working_dir = optional(string) - env = optional(map(string), {}) - run_as_user = optional(string) - })) + annotations = optional(map(string)) display_name = optional(string) enable_audit_agent = optional(bool) + labels = optional(map(string)) + max_workstations = optional(number) + replica_zones = optional(list(string)) + container = optional(object({ + args = optional(list(string), []) + command = optional(list(string), []) + env = optional(map(string), {}) + image = optional(string) + run_as_user = optional(string) + working_dir = optional(string) + })) encryption_key = optional(object({ kms_key = string kms_key_service_account = string })) gce_instance = optional(object({ + boot_disk_size_gb = optional(number) + disable_public_ip_addresses = optional(bool, false) + enable_confidential_compute = optional(bool, false) + enable_nested_virtualization = optional(bool, false) machine_type = optional(string) + pool_size = optional(number) service_account = optional(string) service_account_scopes = optional(list(string), []) - pool_size = optional(number) - boot_disk_size_gb = optional(number) tags = optional(list(string)) - disable_public_ip_addresses = optional(bool, false) - enable_nested_virtualization = optional(bool, false) + accelerators = optional(list(object({ + type = optional(string) + count = optional(number) + })), []) shielded_instance_config = optional(object({ enable_secure_boot = optional(bool, false) enable_vtpm = optional(bool, false) enable_integrity_monitoring = optional(bool, false) })) - enable_confidential_compute = optional(bool, false) - accelerators = optional(list(object({ - type = optional(string) - count = optional(number) - })), []) })) iam = optional(map(list(string)), {}) iam_bindings = optional(map(object({ @@ -118,8 +145,6 @@ variable "workstation_configs" { role = string member = string })), {}) - labels = optional(map(string)) - max_workstations = optional(number) persistent_directories = optional(list(object({ mount_path = optional(string) gce_pd = optional(object({ @@ -130,7 +155,6 @@ variable "workstation_configs" { reclaim_policy = optional(string) })) })), []) - replica_zones = optional(list(string)) timeouts = optional(object({ idle = optional(number) running = optional(number) @@ -151,4 +175,6 @@ variable "workstation_configs" { labels = optional(map(string)) })), {}) })) + nullable = false + default = {} } diff --git a/tests/modules/workstation_cluster/context.tfvars b/tests/modules/workstation_cluster/context.tfvars new file mode 100644 index 000000000..7c0bd84a8 --- /dev/null +++ b/tests/modules/workstation_cluster/context.tfvars @@ -0,0 +1,71 @@ +context = { + condition_vars = { + names = { + my-id = "myid" + } + } + custom_roles = { + myrole = "organizations/366118655033/roles/myRoleOne" + } + iam_principals = { + myuser = "user:test-user@example.com" + myuser2 = "user:test-user2@example.com" + } + locations = { + ew8 = "europe-west8" + } + networks = { + "dev-spoke-0" : "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0" + } + subnetworks = { + "default" : "projects/foo-dev-net-spoke-0/regions/europe-west8/subnetworks/default" + } + project_ids = { + test = "dev-test-0" + } +} + +project_id = "$project_ids:test" +id = "test-0" +location = "$locations:ew8" +network_config = { + network = "$networks:dev-spoke-0" + subnetwork = "$subnetworks:default" +} +workstation_configs = { + my-workstation-config = { + workstations = { + my-workstation = { + labels = { + team = "my-team" + } + iam = { + "roles/workstations.user" = ["$iam_principals:myuser"] + } + } + } + iam = { + "roles/viewer" = ["$iam_principals:myuser2"] + } + iam_bindings = { + workstations-config-viewer = { + role = "$custom_roles:myrole" + members = ["$iam_principals:myuser"] + condition = { + title = "limited-access" + expression = "resource.name.startsWith('my-')" + } + } + } + iam_bindings_additive = { + workstations-config-editor = { + role = "roles/editor" + member = "group:group3@my-org.com" + condition = { + title = "limited-access" + expression = "resource.name.startsWith('$${names.my-id}-')" + } + } + } + } +} diff --git a/tests/modules/workstation_cluster/context.yaml b/tests/modules/workstation_cluster/context.yaml new file mode 100644 index 000000000..d3dda752f --- /dev/null +++ b/tests/modules/workstation_cluster/context.yaml @@ -0,0 +1,117 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + google_workstations_workstation.workstations["my-workstation-config-my-workstation"]: + annotations: null + display_name: null + effective_labels: + goog-terraform-provisioned: 'true' + team: my-team + env: null + labels: + team: my-team + location: europe-west8 + project: dev-test-0 + source_workstation: null + terraform_labels: + goog-terraform-provisioned: 'true' + team: my-team + timeouts: null + workstation_cluster_id: test-0 + workstation_config_id: my-workstation-config + workstation_id: my-workstation + google_workstations_workstation_cluster.cluster: + annotations: null + display_name: null + domain_config: [] + effective_labels: + goog-terraform-provisioned: 'true' + labels: null + location: europe-west8 + network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0 + private_cluster_config: + - enable_private_endpoint: false + project: dev-test-0 + subnetwork: projects/foo-dev-net-spoke-0/regions/europe-west8/subnetworks/default + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + workstation_cluster_id: test-0 + google_workstations_workstation_config.configs["my-workstation-config"]: + annotations: null + disable_tcp_connections: null + display_name: null + effective_labels: + goog-terraform-provisioned: 'true' + enable_audit_agent: null + encryption_key: [] + idle_timeout: 1200s + labels: null + location: europe-west8 + project: dev-test-0 + readiness_checks: [] + running_timeout: 43200s + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + workstation_cluster_id: test-0 + workstation_config_id: my-workstation-config + google_workstations_workstation_config_iam_binding.authoritative["my-workstation-config-roles/viewer"]: + condition: [] + location: europe-west8 + members: + - user:test-user2@example.com + project: dev-test-0 + role: roles/viewer + workstation_cluster_id: test-0 + workstation_config_id: my-workstation-config + google_workstations_workstation_config_iam_binding.bindings["my-workstation-config-workstations-config-viewer"]: + condition: [] + location: europe-west8 + members: + - user:test-user@example.com + project: dev-test-0 + role: organizations/366118655033/roles/myRoleOne + workstation_cluster_id: test-0 + workstation_config_id: my-workstation-config + google_workstations_workstation_config_iam_member.bindings["my-workstation-config-workstations-config-editor"]: + condition: [] + location: europe-west8 + member: group:group3@my-org.com + project: dev-test-0 + role: roles/editor + workstation_cluster_id: test-0 + workstation_config_id: my-workstation-config + google_workstations_workstation_iam_binding.authoritative["my-workstation-config-my-workstation-roles/workstations.user"]: + condition: [] + location: europe-west8 + members: + - user:test-user@example.com + project: dev-test-0 + role: roles/workstations.user + workstation_cluster_id: test-0 + workstation_config_id: my-workstation-config + workstation_id: my-workstation + +counts: + google_workstations_workstation: 1 + google_workstations_workstation_cluster: 1 + google_workstations_workstation_config: 1 + google_workstations_workstation_config_iam_binding: 2 + google_workstations_workstation_config_iam_member: 1 + google_workstations_workstation_iam_binding: 1 + modules: 0 + resources: 7 diff --git a/tests/modules/workstation_cluster/tftest.yaml b/tests/modules/workstation_cluster/tftest.yaml new file mode 100644 index 000000000..74784ca40 --- /dev/null +++ b/tests/modules/workstation_cluster/tftest.yaml @@ -0,0 +1,17 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module: modules/workstation-cluster +tests: + context: