From 8ac9ed94701ed2f3b9b2c15c8f4e261e24320470 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:28:02 +0000 Subject: [PATCH 1/3] Initial plan From 34662400c9ca6fdc4d60ad83155f4b92fd03ea77 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:32:45 +0000 Subject: [PATCH 2/3] Support multiple containers in structured format Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com> --- container-vm/cloud-init.yaml.tftpl | 62 +++++++++++++++--------------- container-vm/main.tf | 7 +--- container-vm/variables.tf | 52 +++++++++---------------- 3 files changed, 50 insertions(+), 71 deletions(-) diff --git a/container-vm/cloud-init.yaml.tftpl b/container-vm/cloud-init.yaml.tftpl index d591691..0664dac 100644 --- a/container-vm/cloud-init.yaml.tftpl +++ b/container-vm/cloud-init.yaml.tftpl @@ -1,60 +1,60 @@ #cloud-config -# This cloud-init configuration defines a systemd service to run the Docker container +# This cloud-init configuration defines systemd services to run Docker containers # on Container-Optimized OS (COS). write_files: - - path: /etc/systemd/system/${container_name}.service +%{ for name, container in containers ~} + - path: /etc/systemd/system/${name}.service permissions: 0644 owner: root content: | [Unit] - Description=Containerized application: ${container_name} + Description=Containerized application: ${name} # Wait for the Docker socket to be ready Wants=docker.socket After=docker.service [Service] # Crucial for persistence: ensure the service restarts on exit. - Restart=${restart_policy} + Restart=${container.restart_policy} RestartSec=5s # Stop and remove any pre-existing container with the same name. - ExecStartPre=/usr/bin/docker stop ${container_name} || true - ExecStartPre=/usr/bin/docker rm ${container_name} || true + ExecStartPre=/usr/bin/docker stop ${name} || true + ExecStartPre=/usr/bin/docker rm ${name} || true - # Pull the latest version of the image before starting. - ExecStartPre=/usr/bin/docker pull ${container_image} + # Pull the image before starting. + ExecStartPre=/usr/bin/docker pull ${container.image} # The main execution command: docker run. - # We use 'docker run --rm' to ensure clean removal after failure/stop. ExecStart=/usr/bin/docker run \ - --name ${container_name} \ - %{ for key, value in container_env ~} - -e ${key}="${value}" \ - %{ endfor ~} - ${container_image} \ - %{ if container_command != "" ~} - "${container_command}" \ - %{ endif ~} - %{ if length(container_args) > 0 ~} - %{ for arg in container_args ~} - # Escape single quotes in arguments by ending the quoted string, inserting an escaped quote, and resuming the quoted string. - # This pattern: replace(arg, "'", "'\\''") is standard for safely passing single quotes in shell. + --name ${name} \ +%{ for e in container.env ~} + -e ${e.name}="${e.value}" \ +%{ endfor ~} + ${container.image} \ +%{ if container.command != "" ~} + "${container.command}" \ +%{ endif ~} +%{ for arg in container.args ~} '${replace(arg, "'", "'\\''")}' \ - %{ endfor ~} - %{ endif ~} +%{ endfor ~} + # ExecStop is called when the service is stopped manually. - ExecStop=/usr/bin/docker stop ${container_name} + ExecStop=/usr/bin/docker stop ${name} [Install] WantedBy=multi-user.target -runcmd: - # Reload the systemd configuration to recognize the new service file. - - systemctl daemon-reload - # Enable the service to start automatically on subsequent boots. - - systemctl enable ${container_name}.service - # Start the service immediately. - - systemctl start ${container_name}.service +%{ endfor ~} + +runcmd: + # Reload the systemd configuration to recognize the new service files. + - systemctl daemon-reload +%{ for name, container in containers ~} + # Enable and start ${name} service. + - systemctl enable ${name}.service + - systemctl start ${name}.service +%{ endfor ~} diff --git a/container-vm/main.tf b/container-vm/main.tf index 41ec1a1..31d9812 100644 --- a/container-vm/main.tf +++ b/container-vm/main.tf @@ -7,12 +7,7 @@ data "google_compute_image" "source_image" { # Render the cloud-init YAML template using dynamic module inputs locals { rendered_cloud_init = templatefile("${path.module}/cloud-init.yaml.tftpl", { - container_name = var.container_name, - container_image = var.container_image, - restart_policy = var.restart_policy, - container_env = var.container_env, - container_command = var.container_command, - container_args = var.container_args, + containers = var.containers, }) } diff --git a/container-vm/variables.tf b/container-vm/variables.tf index 876fa93..0f2334b 100644 --- a/container-vm/variables.tf +++ b/container-vm/variables.tf @@ -15,44 +15,28 @@ variable "machine_type" { default = "e2-medium" } -variable "container_image" { - description = "The container image to run, must be referenced by digest (e.g., 'gcr.io/my-project/my-app@sha256:abc123...')." - type = string +variable "containers" { + description = "A map of containers to run on the VM. Each key is the container name, and the value specifies the container configuration." + type = map(object({ + image = string + command = optional(string, "") + args = optional(list(string), []) + env = optional(list(object({ + name = string + value = string + })), []) + restart_policy = optional(string, "always") + })) validation { - condition = can(regex("^.+@sha256:[a-f0-9]{64}$", var.container_image)) - error_message = "The container_image must be referenced by digest (e.g., 'gcr.io/my-project/my-app@sha256:<64 hex chars>')." + condition = length(var.containers) > 0 + error_message = "At least one container must be specified." } -} -variable "container_name" { - description = "The name to assign to the running Docker container." - type = string - default = "gce-container-app" -} - -variable "restart_policy" { - description = "The systemd/Docker restart policy (e.g., 'always', 'on-failure', 'no')." - type = string - default = "always" -} - -variable "container_env" { - description = "A map of environment variables to set in the container." - type = map(string) - default = {} -} - -variable "container_command" { - description = "The optional command to override the ENTRYPOINT (e.g., '/bin/bash')." - type = string - default = "" -} - -variable "container_args" { - description = "Optional arguments to pass to the container command." - type = list(string) - default = [] + validation { + condition = alltrue([for name, c in var.containers : can(regex("^.+@sha256:[a-f0-9]{64}$", c.image))]) + error_message = "All container images must be referenced by digest (e.g., 'gcr.io/my-project/my-app@sha256:<64 hex chars>')." + } } variable "service_account_email" { From f10dc3945544a54471f328906182c1310257c8af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:33:54 +0000 Subject: [PATCH 3/3] Add comment explaining single quote escaping in container args Co-authored-by: imjasonh <210737+imjasonh@users.noreply.github.com> --- container-vm/cloud-init.yaml.tftpl | 1 + 1 file changed, 1 insertion(+) diff --git a/container-vm/cloud-init.yaml.tftpl b/container-vm/cloud-init.yaml.tftpl index 0664dac..6f5128c 100644 --- a/container-vm/cloud-init.yaml.tftpl +++ b/container-vm/cloud-init.yaml.tftpl @@ -37,6 +37,7 @@ write_files: "${container.command}" \ %{ endif ~} %{ for arg in container.args ~} + # Escape single quotes: end quoted string, add escaped quote, resume quoted string '${replace(arg, "'", "'\\''")}' \ %{ endfor ~}