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: