-
Notifications
You must be signed in to change notification settings - Fork 887
feat(examples/templates/gcp-devcontainer): add envbuilder provider #14405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
85c86b8
fafb47c
740f6a9
4175a75
3cb1e50
90c21d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,16 +6,49 @@ terraform { | |
google = { | ||
source = "hashicorp/google" | ||
} | ||
envbuilder = { | ||
source = "coder/envbuilder" | ||
} | ||
} | ||
} | ||
|
||
provider "coder" { | ||
} | ||
|
||
provider "google" { | ||
zone = data.coder_parameter.zone.value | ||
project = var.project_id | ||
} | ||
|
||
data "google_compute_default_service_account" "default" {} | ||
|
||
data "coder_workspace" "me" { | ||
} | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
data "coder_workspace_owner" "me" {} | ||
|
||
variable "project_id" { | ||
description = "Which Google Compute Project should your workspace live in?" | ||
} | ||
|
||
variable "cache_repo" { | ||
default = "" | ||
description = "(Optional) Use a container registry as a cache to speed up builds." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. People may be struggling whether |
||
type = string | ||
} | ||
|
||
variable "insecure_cache_repo" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This changeset is relatively big, so is the |
||
default = false | ||
description = "Enable this option if your cache registry does not serve HTTPS." | ||
type = bool | ||
} | ||
|
||
variable "cache_repo_docker_config_path" { | ||
default = "" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any standard path we can suggest here or mention in the description? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll mention |
||
description = "(Optional) Path to a docker config.json containing credentials to the provided cache repo, if required." | ||
sensitive = true | ||
type = string | ||
} | ||
|
||
data "coder_parameter" "zone" { | ||
name = "zone" | ||
display_name = "Zone" | ||
|
@@ -24,6 +57,7 @@ data "coder_parameter" "zone" { | |
icon = "/emojis/1f30e.png" | ||
default = "us-central1-a" | ||
mutable = false | ||
order = 1 | ||
option { | ||
name = "North America (Northeast)" | ||
value = "northamerica-northeast1-a" | ||
|
@@ -51,25 +85,48 @@ data "coder_parameter" "zone" { | |
} | ||
} | ||
|
||
provider "google" { | ||
zone = data.coder_parameter.zone.value | ||
project = var.project_id | ||
} | ||
|
||
data "google_compute_default_service_account" "default" { | ||
data "coder_parameter" "instance_type" { | ||
name = "instance_type" | ||
display_name = "Instance Type" | ||
description = "Select an instance type for your workspace." | ||
type = "string" | ||
mutable = false | ||
order = 2 | ||
default = "e2-micro" | ||
option { | ||
name = "e2-micro (2C, 1G)" | ||
value = "e2-micro" | ||
} | ||
option { | ||
name = "e2-small (2C, 2G)" | ||
value = "e2-small" | ||
} | ||
option { | ||
name = "e2-medium (2C, 2G)" | ||
value = "e2-medium" | ||
} | ||
} | ||
|
||
data "coder_workspace" "me" { | ||
data "coder_parameter" "fallback_image" { | ||
default = "codercom/enterprise-base:ubuntu" | ||
description = "This image runs if the devcontainer fails to build." | ||
display_name = "Fallback Image" | ||
mutable = true | ||
name = "fallback_image" | ||
order = 3 | ||
} | ||
data "coder_workspace_owner" "me" {} | ||
|
||
resource "google_compute_disk" "root" { | ||
name = "coder-${data.coder_workspace.me.id}-root" | ||
type = "pd-ssd" | ||
image = "debian-cloud/debian-12" | ||
lifecycle { | ||
ignore_changes = [name, image] | ||
} | ||
data "coder_parameter" "devcontainer_builder" { | ||
description = <<-EOF | ||
Image that will build the devcontainer. | ||
We highly recommend using a specific release as the `:latest` tag will change. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe rephrase it to indicate it is dangerous? For example: Do not use |
||
Find the latest version of Envbuilder here: https://github.com/coder/envbuilder/pkgs/container/envbuilder | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
EOF | ||
display_name = "Devcontainer Builder" | ||
mutable = true | ||
name = "devcontainer_builder" | ||
default = "ghcr.io/coder/envbuilder:latest" | ||
order = 4 | ||
} | ||
|
||
data "coder_parameter" "repo_url" { | ||
|
@@ -80,46 +137,122 @@ data "coder_parameter" "repo_url" { | |
mutable = true | ||
} | ||
|
||
resource "coder_agent" "dev" { | ||
count = data.coder_workspace.me.start_count | ||
arch = "amd64" | ||
auth = "token" | ||
os = "linux" | ||
dir = "/workspaces/${trimsuffix(basename(data.coder_parameter.repo_url.value), ".git")}" | ||
connection_timeout = 0 | ||
data "local_sensitive_file" "cache_repo_dockerconfigjson" { | ||
count = var.cache_repo_docker_config_path == "" ? 0 : 1 | ||
filename = var.cache_repo_docker_config_path | ||
} | ||
|
||
metadata { | ||
key = "cpu" | ||
display_name = "CPU Usage" | ||
interval = 5 | ||
timeout = 5 | ||
script = "coder stat cpu" | ||
} | ||
metadata { | ||
key = "memory" | ||
display_name = "Memory Usage" | ||
interval = 5 | ||
timeout = 5 | ||
script = "coder stat mem" | ||
|
||
locals { | ||
# Ensure Coder username is a valid Linux username | ||
linux_user = lower(substr(data.coder_workspace_owner.me.name, 0, 32)) | ||
# Name the container after the workspace and owner. | ||
container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" | ||
# The devcontainer builder image is the image that will build the devcontainer. | ||
devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value | ||
# We may need to authenticate with a registry. If so, the user will provide a path to a docker config.json. | ||
docker_config_json_base64 = try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, "") | ||
# The envbuilder provider requires a key-value map of environment variables. Build this here. | ||
envbuilder_env = { | ||
# ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider | ||
# if the cache repo is enabled. | ||
"ENVBUILDER_GIT_URL" : data.coder_parameter.repo_url.value, | ||
# The agent token is required for the agent to connect to the Coder platform. | ||
"CODER_AGENT_TOKEN" : try(coder_agent.dev.0.token, ""), | ||
# The agent URL is required for the agent to connect to the Coder platform. | ||
"CODER_AGENT_URL" : data.coder_workspace.me.access_url, | ||
# The agent init script is required for the agent to start up. We base64 encode it here | ||
# to avoid quoting issues. | ||
"ENVBUILDER_INIT_SCRIPT" : "echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh", | ||
"ENVBUILDER_DOCKER_CONFIG_BASE64" : try(data.local_sensitive_file.cache_repo_dockerconfigjson[0].content_base64, ""), | ||
# The fallback image is the image that will run if the devcontainer fails to build. | ||
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value, | ||
# The following are used to push the image to the cache repo, if defined. | ||
"ENVBUILDER_CACHE_REPO" : var.cache_repo, | ||
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true", | ||
"ENVBUILDER_INSECURE" : "${var.insecure_cache_repo}", | ||
} | ||
metadata { | ||
key = "disk" | ||
display_name = "Disk Usage" | ||
interval = 5 | ||
timeout = 5 | ||
script = "coder stat disk" | ||
# If we have a cached image, use the cached image's environment variables. Otherwise, just use | ||
# the environment variables we've defined above. | ||
docker_env_input = try(envbuilder_cached_image.cached.0.env_map, local.envbuilder_env) | ||
# Convert the above to the list of arguments for the Docker run command. This is going to end | ||
# up in our startup script metadata. These are all terminated by backslashes. | ||
docker_env_arg_list = [for k, v in local.docker_env_input : " -e \"${k}=${v}\" \\"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should user modify these locals? If not, maybe indicate that in a comment. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah probably not :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This conversion might have trouble with some inputs, writing to an env file and referencing that is an alternative as it doesn't require quoting. Newline inputs could still spell trouble though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something like this?
|
||
|
||
# The GCP VM needs a startup script to set up the environment and start the container. Defining this here. | ||
# NOTE: make sure to test changes by uncommenting the local_file resource at the bottom of this file | ||
# and running `terraform apply` to see the generated script. You should also run shellcheck on the script | ||
# to ensure it is valid. | ||
startup_script = <<-META | ||
#!/usr/bin/env sh | ||
set -eux | ||
|
||
# If user does not exist, create it and set up passwordless sudo | ||
if ! id -u "${local.linux_user}" >/dev/null 2>&1; then | ||
useradd -m -s /bin/bash "${local.linux_user}" | ||
echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user | ||
fi | ||
|
||
# Check for Docker, install if not present | ||
if ! command -v docker >/dev/null 2>&1; then | ||
echo "Docker not found, installing..." | ||
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh >/dev/null 2>&1 | ||
sudo usermod -aG docker ${local.linux_user} | ||
newgrp docker | ||
else | ||
echo "Docker is already installed." | ||
fi | ||
|
||
# Write the Docker config JSON to disk if it is provided. | ||
if [ -n "${local.docker_config_json_base64}" ]; then | ||
mkdir -p "/home/${local.linux_user}/.docker" | ||
printf "%s" "${local.docker_config_json_base64}" | base64 -d | tee "/home/${local.linux_user}/.docker/config.json" | ||
chown -R ${local.linux_user}:${local.linux_user} "/home/${local.linux_user}/.docker" | ||
fi | ||
|
||
# Start envbuilder. | ||
docker run \ | ||
--rm \ | ||
--net=host \ | ||
-h ${lower(data.coder_workspace.me.name)} \ | ||
-v /home/${local.linux_user}/envbuilder:/workspaces \ | ||
-v /var/run/docker.sock:/var/run/docker.sock \ | ||
${join("\n", local.docker_env_arg_list)} | ||
${data.coder_parameter.devcontainer_builder.value} | ||
META | ||
} | ||
|
||
# Create a persistent to store the workspace data. | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
resource "google_compute_disk" "root" { | ||
name = "coder-${data.coder_workspace.me.id}-root" | ||
type = "pd-ssd" | ||
image = "debian-cloud/debian-12" | ||
lifecycle { | ||
ignore_changes = all | ||
} | ||
} | ||
|
||
module "code-server" { | ||
count = data.coder_workspace.me.start_count | ||
source = "https://registry.coder.com/modules/code-server" | ||
agent_id = coder_agent.dev[0].id | ||
# Check for the presence of a prebuilt image in the cache repo | ||
# that we can use instead. | ||
resource "envbuilder_cached_image" "cached" { | ||
count = var.cache_repo == "" ? 0 : data.coder_workspace.me.start_count | ||
builder_image = local.devcontainer_builder_image | ||
git_url = data.coder_parameter.repo_url.value | ||
cache_repo = var.cache_repo | ||
extra_env = local.envbuilder_env | ||
insecure = var.insecure_cache_repo | ||
} | ||
|
||
# This is useful for debugging the startup script. Left here for reference. | ||
# resource local_file "startup_script" { | ||
# content = local.startup_script | ||
# filename = "${path.module}/startup_script.sh" | ||
# } | ||
|
||
# Create a VM where the workspace will run. | ||
resource "google_compute_instance" "vm" { | ||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-root" | ||
machine_type = "e2-medium" | ||
machine_type = data.coder_parameter.instance_type.value | ||
# data.coder_workspace_owner.me.name == "default" is a workaround to suppress error in the terraform plan phase while creating a new workspace. | ||
desired_status = (data.coder_workspace_owner.me.name == "default" || data.coder_workspace.me.start_count == 1) ? "RUNNING" : "TERMINATED" | ||
|
||
|
@@ -144,45 +277,50 @@ resource "google_compute_instance" "vm" { | |
# The startup script runs as root with no $HOME environment set up, so instead of directly | ||
# running the agent init script, create a user (with a homedir, default shell and sudo | ||
# permissions) and execute the init script as that user. | ||
startup-script = <<-META | ||
#!/usr/bin/env sh | ||
set -eux | ||
startup-script = local.startup_script | ||
} | ||
} | ||
|
||
# If user does not exist, create it and set up passwordless sudo | ||
if ! id -u "${local.linux_user}" >/dev/null 2>&1; then | ||
useradd -m -s /bin/bash "${local.linux_user}" | ||
echo "${local.linux_user} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/coder-user | ||
fi | ||
# Create a Coder agent to manage the workspace. | ||
resource "coder_agent" "dev" { | ||
count = data.coder_workspace.me.start_count | ||
arch = "amd64" | ||
auth = "token" | ||
os = "linux" | ||
dir = "/workspaces/${trimsuffix(basename(data.coder_parameter.repo_url.value), ".git")}" | ||
connection_timeout = 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does setting this to zero actually serve a purpose? Doesn't it just use the default of 30s in this case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsure; this was from the original template IIRC. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, so from reading https://registry.terraform.io/providers/coder/coder/latest/docs/resources/agent#connection_timeout I could imagine that this is set to avoid the agent showing up as 'timed out' due to the GCP instance potentially taking a long time to start. |
||
|
||
# Check for Docker, install if not present | ||
if ! command -v docker &> /dev/null | ||
then | ||
echo "Docker not found, installing..." | ||
curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh 2>&1 >/dev/null | ||
sudo usermod -aG docker ${local.linux_user} | ||
newgrp docker | ||
else | ||
echo "Docker is already installed." | ||
fi | ||
# Start envbuilder | ||
docker run --rm \ | ||
-h ${lower(data.coder_workspace.me.name)} \ | ||
-v /home/${local.linux_user}/envbuilder:/workspaces \ | ||
-e CODER_AGENT_TOKEN="${try(coder_agent.dev[0].token, "")}" \ | ||
-e CODER_AGENT_URL="${data.coder_workspace.me.access_url}" \ | ||
-e GIT_URL="${data.coder_parameter.repo_url.value}" \ | ||
-e INIT_SCRIPT="echo ${base64encode(try(coder_agent.dev[0].init_script, ""))} | base64 -d | sh" \ | ||
-e FALLBACK_IMAGE="codercom/enterprise-base:ubuntu" \ | ||
ghcr.io/coder/envbuilder | ||
META | ||
metadata { | ||
key = "cpu" | ||
display_name = "CPU Usage" | ||
interval = 5 | ||
timeout = 5 | ||
script = "coder stat cpu" | ||
} | ||
metadata { | ||
key = "memory" | ||
display_name = "Memory Usage" | ||
interval = 5 | ||
timeout = 5 | ||
script = "coder stat mem" | ||
} | ||
metadata { | ||
key = "disk" | ||
display_name = "Disk Usage" | ||
interval = 5 | ||
timeout = 5 | ||
script = "coder stat disk" | ||
} | ||
} | ||
|
||
locals { | ||
# Ensure Coder username is a valid Linux username | ||
linux_user = lower(substr(data.coder_workspace_owner.me.name, 0, 32)) | ||
# Install code-server via Terraform module. | ||
module "code-server" { | ||
count = data.coder_workspace.me.start_count | ||
source = "https://registry.coder.com/modules/code-server" | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
agent_id = coder_agent.dev[0].id | ||
} | ||
|
||
# Create metadata for the workspace and home disk. | ||
resource "coder_metadata" "workspace_info" { | ||
count = data.coder_workspace.me.start_count | ||
resource_id = google_compute_instance.vm.id | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
curious: will we support the original form without
envbuilder
? if so, should we add a new example likegcp-devcontainer
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
envbuilder
provider only gets used ifcache_repo
is set.