From 6d79c5aad04f02746fc7d45f44f4da8e9e2b0031 Mon Sep 17 00:00:00 2001 From: Aleksandr Ilin Date: Wed, 7 Oct 2026 04:36:22 -0700 Subject: [PATCH 1/2] feat(deepspacecartel/kubernetes-devcontainer): Kubernetes workspaces built from a repo's devcontainer.json New namespace deepspacecartel and its first template: workspaces on Kubernetes built from a git repository's own devcontainer.json by devcontainer-builder (base image, Features, Dockerfile), with its lifecycle commands, environment, mounts, forwarded ports, VS Code extensions and settings - self-hosted Codespaces. Co-Authored-By: Claude Opus 5.5 --- registry/deepspacecartel/.images/avatar.png | Bin 0 -> 25808 bytes registry/deepspacecartel/README.md | 14 + .../kubernetes-devcontainer/README.md | 67 + .../templates/kubernetes-devcontainer/main.tf | 1222 +++++++++++++++++ 4 files changed, 1303 insertions(+) create mode 100644 registry/deepspacecartel/.images/avatar.png create mode 100644 registry/deepspacecartel/README.md create mode 100644 registry/deepspacecartel/templates/kubernetes-devcontainer/README.md create mode 100644 registry/deepspacecartel/templates/kubernetes-devcontainer/main.tf diff --git a/registry/deepspacecartel/.images/avatar.png b/registry/deepspacecartel/.images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..7f1414873f31e25d5607599dbc82fec202d4b8c0 GIT binary patch literal 25808 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 literal 0 HcmV?d00001 diff --git a/registry/deepspacecartel/README.md b/registry/deepspacecartel/README.md new file mode 100644 index 000000000..943587f7a --- /dev/null +++ b/registry/deepspacecartel/README.md @@ -0,0 +1,14 @@ +--- +display_name: Deep Space Cartel +bio: Self-hosted Dev Containers for Coder on Kubernetes - devcontainer-builder. +github: DeepSpaceCartel +avatar: ./.images/avatar.png +website: https://deepspacecartel.com +status: community +--- + +# Deep Space Cartel + +Maintainers of [devcontainer-builder](https://github.com/DeepSpaceCartel/devcontainer-builder): +Dev Containers for Coder on Kubernetes - workspaces built from a repository's own +`devcontainer.json`, on your own cluster. diff --git a/registry/deepspacecartel/templates/kubernetes-devcontainer/README.md b/registry/deepspacecartel/templates/kubernetes-devcontainer/README.md new file mode 100644 index 000000000..ad6e3ec3d --- /dev/null +++ b/registry/deepspacecartel/templates/kubernetes-devcontainer/README.md @@ -0,0 +1,67 @@ +--- +display_name: Kubernetes (Dev Containers) +description: Workspaces built from a repository's devcontainer.json on Kubernetes - self-hosted Codespaces +icon: ../../../../.icons/devcontainers.svg +verified: false +tags: [kubernetes, devcontainer, container] +--- + +# Kubernetes (Dev Containers) + +Workspaces built from a git repository's own `devcontainer.json`, on your Kubernetes +cluster. Enter a repository and a branch: the image is built from its Dev Container +configuration (base image, Features, Dockerfile) by +[devcontainer-builder](https://github.com/DeepSpaceCartel/devcontainer-builder), and the +workspace runs it with the configuration's lifecycle commands, environment, mounts, +forwarded ports, VS Code extensions and settings - like GitHub Codespaces, self-hosted. + + + +## What you get + +- **Any repository**: one with `.devcontainer/devcontainer.json` or `.devcontainer.json` + builds from it; one without gets a generic image and an offer to add a configuration. +- **VS Code Desktop and VS Code in the browser** (Microsoft's VS Code Server, extensions + from the Marketplace), opened on the cloned folder. +- **Persistence**: the home and the repository live on a per-workspace volume. +- **Rebuild**: the [Dev Containers for Coder in K8S](https://marketplace.visualstudio.com/items?itemName=deepspacecartel.devcontainer-builder) + extension (installed in every workspace) offers to rebuild when the branch's Dev Container + configuration changes, and clones repositories into new workspaces from a local VS Code. +- **Private repositories** with each user's own account (`external_auth_id`). +- **`hostRequirements`** are reserved on the node, as minimums. + +## Prerequisites + +This template calls services it does not deploy: + +1. **devcontainer-builder** and a **BuildKit** daemon in the cluster - one Helm chart + installs both: + + ```sh + helm install devcontainer-builder oci://ghcr.io/deepspacecartel/charts/devcontainer-builder \ + --namespace devcontainer-builder --create-namespace \ + --set buildkit.deploy.enabled=true \ + --set registryAuth.registries[0].registry=ghcr.io \ + --set registryAuth.registries[0].username= \ + --set registryAuth.registries[0].password= + ``` + +2. A **registry** the cluster's nodes can pull the built images from (an image pull secret + in the workspaces namespace if it's private). +3. A **namespace** for the workspaces. + +See [Getting started](https://deepspacecartel.github.io/devcontainer-builder/) for the full +setup, and the [template guide](https://deepspacecartel.github.io/devcontainer-builder/guides/coder-workspace-template/) +for every `devcontainer.json` property it maps. + +## Push the template + +```sh +coder templates push kubernetes-devcontainer -d . \ + --var namespace=coder-workspaces \ + --var devcontainer_builder_endpoint=http://devcontainer-builder.devcontainer-builder.svc.cluster.local:8080 +``` + +Optional variables: `image_pull_secret_name`, `external_auth_id` (e.g. `github`, for private +repositories), `max_cpu`/`max_memory`, `allow_privileged`, `accept_vscode_license`, +`max_forwarded_ports`, `vscode_extension` - see `main.tf`. diff --git a/registry/deepspacecartel/templates/kubernetes-devcontainer/main.tf b/registry/deepspacecartel/templates/kubernetes-devcontainer/main.tf new file mode 100644 index 000000000..6ef452fcf --- /dev/null +++ b/registry/deepspacecartel/templates/kubernetes-devcontainer/main.tf @@ -0,0 +1,1222 @@ +# A Coder Workspace Template for Kubernetes, adapted from the official +# https://registry.coder.com/templates/coder/kubernetes template. The one +# real change from that upstream template: instead of a fixed/parameterized +# `image` variable, a workspace-level `coder_parameter` asks for a git +# repository (the one thing a stranger actually wants to type in), and a +# `devcontainerbuilder_build` resource (from +# https://github.com/DeepSpaceCartel/terraform-provider-devcontainer-builder) +# turns it into a real, pushed image before the Deployment ever starts - +# see https://deepspacecartel.github.io/devcontainer-builder/guides/coder-workspace-template/ for the full walkthrough and +# the platform prerequisites this template itself does NOT set up (a +# running devcontainer-builder instance, BuildKit). +# +# Persistence: one PVC per workspace (coder--data), mounted twice via +# subPaths - `home/` at the remote user's home and `workspaces/` at +# /workspaces - so both survive a pod restart. A `seed-home` init container +# copies the image's own home into the PVC on first start, so the mount +# doesn't hide the image's dotfiles (.bashrc, nvm, oh-my-zsh, ...). +# +# Dev Container behavior: a `devcontainerbuilder_devcontainer` data source +# reads the built image's merged devcontainer.json (base image + Features + +# the repo's own config). Its `remoteUser` decides whose home is persisted; +# its lifecycle commands (onCreate/updateContent/postCreate/postStart) run +# from a login-blocking script on every start - a pod's root filesystem is +# fresh each time, so they must be idempotent; and its +# customizations.vscode extensions/settings are installed into +# ~/.vscode-server, where VS Code Desktop picks them up. +terraform { + required_providers { + coder = { + source = "coder/coder" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + devcontainerbuilder = { + source = "deepspacecartel/devcontainer-builder" + # >= 0.3.0 for the data source's runtime/env_scripts/variables (and a + # devcontainer-builder service >= 0.3.0 behind it). + version = ">= 0.3.0" + } + } +} + +provider "coder" { +} + +# --- Template-level variables ----------------------------------------- +# Set once, when this template is pushed/configured in Coder (`coder +# templates push -var ...`) - not per-workspace. See the guide for where +# each of these values actually comes from. + +variable "use_kubeconfig" { + type = bool + description = <<-EOF + Use host kubeconfig? (true/false) + + Set this to false if the Coder host is itself running as a Pod on the same + Kubernetes cluster as you are deploying workspaces to. + + Set this to true if the Coder host is running outside the Kubernetes cluster + for workspaces. A valid "~/.kube/config" must be present on the Coder host. + EOF + default = false +} + +variable "namespace" { + type = string + description = "The Kubernetes namespace to create workspace Deployments in (must exist prior to creating workspaces)." +} + +variable "devcontainer_builder_endpoint" { + type = string + description = "Base URL of an already-running devcontainer-builder instance, e.g. http://devcontainer-builder.devcontainer-builder.svc.cluster.local:8080. This template never deploys devcontainer-builder or BuildKit itself - both are cluster-level platform infrastructure, set up once before this template is ever pushed. See the guide." +} + +variable "image_pull_secret_name" { + type = string + description = "Name of an existing Kubernetes Secret (kubernetes.io/dockerconfigjson) in `namespace`, used as an imagePullSecret for the built image. Leave empty if the registry allows anonymous pulls from your cluster's nodes." + default = "" +} + +variable "accept_vscode_license" { + type = bool + description = "Accept Microsoft's VS Code Server license (https://aka.ms/vscode-server-license) on behalf of this template's users, enabling VS Code in the browser (vscode-web, extensions from the Microsoft Marketplace). The terms allow use within your own organization; set false if workspaces are offered to others. VS Code Desktop is unaffected." + default = true +} + +variable "allow_privileged" { + type = bool + description = "Honor devcontainer.json's `privileged: true`, `--security-opt seccomp=unconfined` and capabilities beyond Pod Security baseline's list (e.g. SYS_PTRACE, NET_ADMIN). Off by default: they need a namespace whose Pod Security level allows them, and privileged pods can take over their node." + default = false +} + +variable "max_forwarded_ports" { + type = number + description = "How many of devcontainer.json's forwardPorts get a dashboard app. Coder needs a fixed number of app slots; unused ones are hidden." + default = 10 +} + +variable "vscode_extension" { + type = string + description = "devcontainer-builder's VS Code extension (\"Dev Containers for Coder in K8S\"), installed into every workspace's VS Code: it offers to rebuild the workspace when the branch's Dev Container configuration changes on origin, and to add one where there's none. A Marketplace ID, or the http(s):// URL of a VSIX (e.g. an unreleased build). Empty to not install it." + default = "deepspacecartel.devcontainer-builder" +} + +variable "external_auth_id" { + type = string + description = "ID of a Coder external auth provider for the git host (e.g. \"github\", see https://coder.com/docs/admin/external-auth), for private repositories. When set, creating a workspace asks the user to link that account, the workspace's clone uses it (the agent's GIT_ASKPASS), and so does the image build: the user's own token is sent to devcontainer-builder instead of service-side gitCredentials. Empty: public repositories, or credentials configured on devcontainer-builder or below." + default = "" +} + +variable "max_cpu" { + type = number + description = "Most CPU cores a repository's hostRequirements.cpus can reserve (requests) - above it, the workspace gets this many and a warning." + default = 8 +} + +variable "max_memory" { + type = number + description = "Most memory, in GiB, a repository's hostRequirements.memory can reserve (requests) - above it, the workspace gets this much and a warning." + default = 32 +} + +variable "git_credentials_username" { + type = string + description = "Optional HTTPS git username, used for every workspace built from this template. Leave empty for public repositories only - devcontainer-builder's own server-side gitCredentials (configured once on the service itself) is the better place for per-host credentials that should apply regardless of which template/caller is asking." + default = "" +} + +variable "git_credentials_token" { + type = string + description = "Optional HTTPS git token/password, paired with git_credentials_username." + default = "" + sensitive = true +} + +# --- Workspace parameters ----------------------------------------------- +# What the person creating a workspace from this template actually fills +# in - this is the whole point of this template over the raw HTTP API or +# the bare Terraform module/provider used directly: type a repo URL, get a +# running workspace. + +data "coder_parameter" "repository" { + name = "repository" + display_name = "Git repository" + description = "A git repository containing a .devcontainer.json (or .devcontainer/devcontainer.json) at its root. https://, ssh://, or SCP-style (git@host:path) all work." + icon = "/icon/git.svg" + mutable = false +} + +data "coder_parameter" "branch" { + name = "branch" + display_name = "Branch" + description = "Branch to build." + default = "main" + icon = "/icon/git.svg" + mutable = false +} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU" + description = "The number of CPU cores" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 Cores" + value = "2" + } + option { + name = "4 Cores" + value = "4" + } + option { + name = "6 Cores" + value = "6" + } + option { + name = "8 Cores" + value = "8" + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory" + description = "The amount of memory in GB" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 GB" + value = "2" + } + option { + name = "4 GB" + value = "4" + } + option { + name = "6 GB" + value = "6" + } + option { + name = "8 GB" + value = "8" + } +} + +data "coder_parameter" "disk_size" { + name = "disk_size" + display_name = "Disk size" + description = "The size of the persistent disk (home + /workspaces) in GB" + default = "10" + type = "number" + icon = "/emojis/1f4be.png" + mutable = false + validation { + min = 1 + max = 99999 + } +} + +# ${localEnv:NAME} in devcontainer.json refers to the developer's machine, +# which a workspace doesn't have - values come from here instead. Coder +# can't create one parameter per variable (parameters can't depend on the +# image), so it's one free-form parameter; the "Dev Container variables" +# metadata item lists what the image uses and what's still unset. +data "coder_parameter" "devcontainer_variables" { + name = "devcontainer_variables" + display_name = "Dev Container variables" + description = "Values for $${localEnv:NAME} in the repository's devcontainer.json: one NAME=value per line. Changes apply on the next restart. Visible to anyone who can see this workspace's settings." + type = "string" + form_type = "textarea" + default = "" + mutable = true + icon = "/icon/docker.svg" +} + +# Bump to rebuild the image from the branch's latest commit on the next +# start. The working copy in /workspaces is left as it is. +data "coder_parameter" "rebuild" { + name = "rebuild" + display_name = "Rebuild" + description = "Increase to rebuild the image from the branch's latest commit on the next start (e.g. after devcontainer.json changes). Your working copy is not touched." + type = "number" + default = 0 + mutable = true + icon = "/icon/docker.svg" +} + +provider "kubernetes" { + # Authenticate via ~/.kube/config or a Coder-specific ServiceAccount, depending on admin preferences + config_path = var.use_kubeconfig == true ? "~/.kube/config" : null +} + +provider "devcontainerbuilder" { + endpoint = var.devcontainer_builder_endpoint +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + # Only read while the workspace is starting (count = start_count below), + # so stopping a workspace never depends on devcontainer-builder being up. + dc = one(data.devcontainerbuilder_devcontainer.workspace[*]) + + # The user tools, IDEs and hooks run as (remoteUser, else containerUser, + # else the image's USER, else root - resolved by devcontainer-builder) + # and its uid/gid/home, recorded by the build: the pod runs as that user + # from the start, and that home is persisted. + remote_user = try(coalesce(local.dc.remote_user, local.dc.container_user), "root") + runtime = try(local.dc.runtime, null) + # An image built before devcontainer-builder 0.3.0 has no recorded + # account. Fall back to uid/gid 1000 - what this template used before it + # recorded them - so a workspace upgraded from an older template version + # still starts (with a warning) and can then be rebuilt: Rebuild only + # takes effect once a start has succeeded on this template version + # (replace_triggered_by doesn't fire when terraform_data.rebuild is + # created, only when it changes). + recorded_uid = try(local.runtime.remote_user_uid, null) + recorded_gid = try(local.runtime.remote_user_gid, null) + uid = local.recorded_uid != null ? local.recorded_uid : 1000 + gid = local.recorded_gid != null ? local.recorded_gid : 1000 + home_dir = try(coalesce(local.runtime.remote_user_home), local.remote_user == "root" ? "/root" : "/home/${local.remote_user}") + + lifecycle_scripts = try(local.dc.lifecycle_scripts, {}) + env_scripts = try(local.dc.env_scripts, {}) + vscode_extensions = try(local.dc.extensions, []) + vscode_settings = try(local.dc.settings_json, "{}") + host_requirements = try(local.dc.host_requirements, null) + warnings = try(local.dc.warnings, []) + variables = try(local.dc.variables, []) + + # devcontainer.json's workspaceFolder (default: Dev Containers' + # "Clone Repository in Container Volume" convention, + # /workspaces/), with the workspace placeholders filled in. + # The repository is cloned there. + repo_name = trimsuffix(basename(trimsuffix(data.coder_parameter.repository.value, "/")), ".git") + raw_workspace_folder = coalesce(try(local.dc.workspace_folder, null), "/workspaces/$${localWorkspaceFolderBasename}") + workspace_folder = replace(replace(replace(local.raw_workspace_folder, + "$${localWorkspaceFolderBasename}", local.repo_name), + "$${containerWorkspaceFolderBasename}", local.repo_name), + "$${devcontainerId}", data.coder_workspace.me.id) + + # Mount targets may use the workspace placeholders too. + mounts = [for i, m in try(local.dc.mounts, []) : merge(m, { + index = i + target = replace(replace(replace(replace(m.target, + "$${containerWorkspaceFolder}", local.workspace_folder), + "$${localWorkspaceFolder}", local.workspace_folder), + "$${containerWorkspaceFolderBasename}", basename(local.workspace_folder)), + "$${localWorkspaceFolderBasename}", basename(local.workspace_folder)) + })] + volume_mounts = [for m in local.mounts : m if m.kind == "volume"] + tmpfs_mounts = [for m in local.mounts : m if m.kind == "tmpfs"] + # PVC directory per named volume (volumes/); anonymous volumes + # get one per position. + volume_dirs = [for m in local.volume_mounts : "volumes/${coalesce(m.source, "anonymous-${m.index}")}"] + + ports = try(local.dc.forward_ports, []) + + # hostRequirements are minimums, as in the spec: the scheduler reserves + # them (requests, capped by var.max_cpu/max_memory), and the limits are + # the larger of them and the CPU/Memory parameters. Storage is the larger + # of it and the Disk parameter, so the PVC never shrinks. Where the + # parameter wins, values stay "Gi" as before (no spurious diffs). + gib = 1073741824 + param_cpu = tonumber(data.coder_parameter.cpu.value) + param_memory = tonumber(data.coder_parameter.memory.value) * local.gib + param_disk = tonumber(data.coder_parameter.disk_size.value) * local.gib + required_cpu = try(local.host_requirements.cpus, null) + required_mem = try(local.host_requirements.memory_bytes, null) + required_disk = try(local.host_requirements.storage_bytes, null) + reserved_cpu = local.required_cpu != null ? min(local.required_cpu, var.max_cpu) : null + reserved_mem = local.required_mem != null ? floor(min(local.required_mem, var.max_memory * local.gib)) : null + cpu_request = local.reserved_cpu != null ? tostring(local.reserved_cpu) : "250m" + memory_request = local.reserved_mem != null ? tostring(local.reserved_mem) : "512Mi" + cpu_limit = local.reserved_cpu != null && try(local.reserved_cpu > local.param_cpu, false) ? tostring(local.reserved_cpu) : data.coder_parameter.cpu.value + memory_limit = local.reserved_mem != null && try(local.reserved_mem > local.param_memory, false) ? tostring(local.reserved_mem) : "${data.coder_parameter.memory.value}Gi" + disk_size = local.required_disk != null && try(local.required_disk > local.param_disk, false) ? tostring(floor(local.required_disk)) : "${data.coder_parameter.disk_size.value}Gi" + resource_warnings = concat( + local.required_cpu != null && try(local.required_cpu > var.max_cpu, false) ? ["hostRequirements.cpus ${local.required_cpu} is more than this template allows (max_cpu ${var.max_cpu}) - reserving ${var.max_cpu}"] : [], + local.required_mem != null && try(local.required_mem > var.max_memory * local.gib, false) ? ["hostRequirements.memory ${format("%.1f", local.required_mem / local.gib)} GiB is more than this template allows (max_memory ${var.max_memory} GiB) - reserving ${var.max_memory} GiB"] : [], + ) + # What the workspace got, for the agent metadata: reserved/limit, and + # whether devcontainer.json's hostRequirements set the reservation. + resources_summary = join(" ยท ", [ + "CPU ${local.cpu_request} / ${local.cpu_limit}", + "memory ${local.memory_request} / ${local.memory_limit}", + "disk ${local.disk_size}", + local.required_cpu != null || local.required_mem != null || local.required_disk != null ? "from hostRequirements" : "from the parameters", + ]) + gpu_json = try(local.host_requirements.gpu_json, null) + # gpu: true or {cores, memory} -> one GPU; "optional" or unset -> none. + gpu_limit = local.gpu_json != null && local.gpu_json != "\"optional\"" && local.gpu_json != "false" ? { "nvidia.com/gpu" = "1" } : {} + + # Pod Security "baseline" (the usual namespace level) only allows these + # added capabilities; anything else (SYS_PTRACE, NET_ADMIN, ...) would get + # the whole pod rejected, so it's only added with var.allow_privileged. + baseline_capabilities = ["AUDIT_WRITE", "CHOWN", "DAC_OVERRIDE", "FOWNER", "FSETID", "KILL", "MKNOD", "NET_BIND_SERVICE", "SETFCAP", "SETGID", "SETPCAP", "SETUID", "SYS_CHROOT"] + requested_capabilities = try(local.runtime.cap_add, []) + capabilities = var.allow_privileged ? local.requested_capabilities : [for c in local.requested_capabilities : c if contains(local.baseline_capabilities, c)] + skipped_capabilities = var.allow_privileged ? [] : [for c in local.requested_capabilities : c if !contains(local.baseline_capabilities, c)] + all_warnings = concat(local.warnings, local.resource_warnings, length(local.skipped_capabilities) > 0 ? [ + "capAdd ${join(", ", local.skipped_capabilities)} not added - Pod Security baseline forbids it (template variable allow_privileged enables it)" + ] : [], local.dc != null && local.recorded_uid == null ? [ + "the image doesn't record its remote user's uid/gid (built by devcontainer-builder older than 0.3.0) - running as uid/gid 1000; bump the Rebuild parameter to rebuild it" + ] : []) + + # Mount points that live on the PVC (the workspace folder itself, and any + # mount target inside /workspaces, the home or the workspace folder): + # seed-home creates them as the remote user first, or kubelet would + # create them root-owned and unwritable (e.g. the workspace folder, when + # a volume is mounted at /node_modules). + mount_point_dirs = compact([for t in concat([local.workspace_folder], [for m in local.mounts : m.target]) : + startswith(t, "/workspaces/") ? "workspaces/${trimprefix(t, "/workspaces/")}" : + startswith(t, "${local.home_dir}/") ? "home/${trimprefix(t, "${local.home_dir}/")}" : + !startswith(local.workspace_folder, "/workspaces/") && startswith(t, "${local.workspace_folder}/") ? "workspace-folder/${trimprefix(t, "${local.workspace_folder}/")}" : "" + ]) + + # localEnv variables without a default that the user must provide - + # ignoring ones only used by mounts, which only matter for bind mounts + # (dropped anyway: there's no host). + required_variables = [for v in local.variables : v.name if v.kind == "localEnv" && v.default == null && length([for u in v.used_in : u if !startswith(u, "mounts")]) > 0] + # Names only - the checks below must never reference the parameter itself: + # Terraform prints referenced values in a failed check's diagnostics, and + # the parameter holds the user's tokens. + missing_variables = nonsensitive([for name in local.required_variables : nonsensitive(name) if !can(regex("(?m)^${name}=.+", data.coder_parameter.devcontainer_variables.value))]) + # Not secrets, but derived from the data source (whose registry + # credentials input is sensitive) - unmarked so the build log shows them. + warning_lines = nonsensitive([for w in local.all_warnings : nonsensitive(w)]) +} + +# --- The actual build ---------------------------------------------------- +# Runs once per workspace, on `coder create`/whenever the repository or +# branch parameter changes (both are immutable=true above, so in practice +# only on create - see the guide's note on why these aren't mutable). +# Everything registryAuth/gitCredentials-shaped is left to devcontainer- +# builder's own ambient server-side config by default (image_spec.registry +# unset - resolved by the service's own registry mapping rules); the +# git_credentials block below is the one optional exception, for a +# template-wide default rather than requiring registryMapping-style +# server config for git. +resource "terraform_data" "rebuild" { + input = data.coder_parameter.rebuild.value +} + +# The workspace owner's linked git account (var.external_auth_id). With +# count = 0 Coder doesn't ask for it at all. +data "coder_external_auth" "git" { + count = var.external_auth_id != "" ? 1 : 0 + id = var.external_auth_id +} + +resource "devcontainerbuilder_build" "workspace" { + repository = data.coder_parameter.repository.value + branch = data.coder_parameter.branch.value + + # A Rebuild bump replaces the build: the old image tag is deleted first + # and the branch's latest commit built (destroy-then-create is right - + # an unchanged branch rebuilds to the same tag). + # Credentials are only needed for the build itself: a linked account's + # token is refreshed over time, and must not replace (rebuild) the image + # each time it changes. A Rebuild bump builds with the current one. + lifecycle { + replace_triggered_by = [terraform_data.rebuild] + ignore_changes = [git_credentials] + } + + # Template-wide credentials win; else the owner's linked account + # ("oauth2" as the username works for GitHub and GitLab tokens alike). + # git_credentials is a nested-object attribute (terraform-plugin-framework), + # not a legacy SDKv2 block - conditionally assign the object itself (or + # null to omit), not a `dynamic` block. + git_credentials = var.git_credentials_username != "" ? { + username = var.git_credentials_username + token = var.git_credentials_token + } : length(data.coder_external_auth.git) > 0 ? { + username = "oauth2" + token = data.coder_external_auth.git[0].access_token + } : null +} + +# The built image's Dev Container metadata - GET /devcontainer on +# devcontainer-builder, read straight from the registry. +data "devcontainerbuilder_devcontainer" "workspace" { + count = data.coder_workspace.me.start_count + registry = devcontainerbuilder_build.workspace.resolved_registry + name = devcontainerbuilder_build.workspace.resolved_name + tag = devcontainerbuilder_build.workspace.resolved_tag +} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + # No `dir`: it's deprecated, and anything but $HOME breaks Coder Desktop + # file sync - so terminals/SSH start in $HOME. code-server and + # vscode_desktop open the repo. + + # The built-in VS Code Desktop button takes its folder from `dir`, so + # without it VS Code opens in $HOME with no folder - module.vscode_desktop + # replaces it. Unset display_apps fields keep their defaults. + display_apps { + vscode = false + } + + metadata { + display_name = "Dev Container variables" + key = "7_devcontainer_variables" + script = <<-EOT + missing="" + for name in ${join(" ", local.required_variables)}; do + eval "value=\$${DEVCONTAINER_LOCALENV_$name-}" + [ -n "$value" ] || missing="$missing $name" + done + if [ -n "$missing" ]; then echo "unset:$missing"; else echo "all set"; fi + EOT + interval = 60 + timeout = 1 + } + + metadata { + display_name = "Resources (reserved / limit)" + key = "9_resources" + script = "echo '${local.resources_summary}'" + interval = 3600 + timeout = 1 + } + + metadata { + display_name = "Dev Container warnings" + key = "8_devcontainer_warnings" + script = "echo '${length(local.all_warnings) == 0 ? "none" : "${length(local.all_warnings)} - see the build log"}'" + interval = 3600 + timeout = 1 + } + + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Data Disk" + key = "3_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + script = </dev/null 2>&1 || ssh-keyscan -H "$host" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true + ;; + esac + echo "devcontainer: cloning $repository ($branch) into $workspace_folder" + mkdir -p "$workspace_folder" + git -C "$workspace_folder" init -q -b "$branch" && + git -C "$workspace_folder" remote add origin "$repository" && + git -C "$workspace_folder" fetch -q origin "+refs/heads/$branch:refs/remotes/origin/$branch" || { + echo "devcontainer: cloning $repository failed" >&2 + rm -rf "$workspace_folder/.git" + exit 1 + } + target="origin/$branch" + if [ -n "$commit" ] && { git -C "$workspace_folder" cat-file -e "$commit^{commit}" 2>/dev/null || git -C "$workspace_folder" fetch -q origin "$commit"; }; then + target="$commit" + fi + git -C "$workspace_folder" checkout -q -B "$branch" "$target" && + git -C "$workspace_folder" branch -q --set-upstream-to="origin/$branch" "$branch" && + echo "devcontainer: checked out $(git -C "$workspace_folder" rev-parse --short HEAD), the commit the image was built from" || exit 1 + fi + + run_hook() { + [ -n "$2" ] || return 0 + echo "$2" | base64 -d > "$dir/$1.sh" || return 1 + (cd "$workspace_folder" && sh "$dir/$1.sh") + } + + # initializeCommand runs on the host before the container exists in Dev + # Containers; here it's simply the first hook. + run_hook initializeCommand '${base64encode(lookup(local.lifecycle_scripts, "initializeCommand", ""))}' || exit $? + run_hook onCreateCommand '${base64encode(lookup(local.lifecycle_scripts, "onCreateCommand", ""))}' || exit $? + run_hook updateContentCommand '${base64encode(lookup(local.lifecycle_scripts, "updateContentCommand", ""))}' || exit $? + run_hook postCreateCommand '${base64encode(lookup(local.lifecycle_scripts, "postCreateCommand", ""))}' || exit $? + run_hook postStartCommand '${base64encode(lookup(local.lifecycle_scripts, "postStartCommand", ""))}' || exit $? + EOT +} + +# postAttachCommand runs each time a tool attaches in Dev Containers. There +# is no attach event here, so it runs once per start - without blocking +# login, after the clone. +resource "coder_script" "devcontainer_post_attach" { + # Always present (count can't depend on the image's metadata, which is + # unknown until the first build) - exits at once if no hook is set. + count = data.coder_workspace.me.start_count + agent_id = coder_agent.main.id + display_name = "Dev Container postAttachCommand" + icon = "/icon/docker.svg" + run_on_start = true + start_blocks_login = false + script = <<-EOT + #!/bin/sh + set -u + hook='${base64encode(lookup(local.lifecycle_scripts, "postAttachCommand", ""))}' + [ -n "$hook" ] || exit 0 + workspace_folder='${local.workspace_folder}' + until [ -f "$workspace_folder/.git/index" ] && [ ! -e "$workspace_folder/.git/index.lock" ]; do sleep 2; done + script="$HOME/.cache/devcontainer-lifecycle/postAttachCommand.sh" + mkdir -p "$(dirname "$script")" + echo "$hook" | base64 -d > "$script" + cd "$workspace_folder" && sh "$script" + EOT +} + +# customizations.vscode from the image's merged metadata, for VS Code +# Desktop: extensions are installed - from the Microsoft Marketplace - into +# ~/.vscode-server/extensions and settings merged into its Machine +# settings, the same folder Desktop's own remote server uses (home is +# persisted). The installer is Desktop's own server if it has connected +# before, else Microsoft's latest VS Code Server, downloaded once into +# ~/.cache - the same server Desktop downloads on first connect, used here +# only as an installer, never served. Doesn't block login: a Desktop +# window that attaches mid-install sees the rest after a reload. +resource "coder_script" "devcontainer_vscode" { + # Always present, for the same reason - exits at once if there's nothing + # to install. + count = data.coder_workspace.me.start_count + agent_id = coder_agent.main.id + display_name = "Dev Container VS Code extensions" + icon = "/icon/code.svg" + run_on_start = true + start_blocks_login = false + script = <<-EOT + #!/bin/sh + set -u + extensions='${join(" ", local.vscode_extensions)}' + settings_b64='${base64encode(local.vscode_settings)}' + own_extension='${var.vscode_extension}' + data_dir="$HOME/.vscode-server" + if [ -z "$extensions" ] && [ "$settings_b64" = "${base64encode("{}")}" ] && [ -z "$own_extension" ]; then + exit 0 + fi + + server=$(ls -td "$data_dir"/cli/servers/Stable-*/server 2>/dev/null | head -n 1) + if [ -z "$server" ] || [ ! -x "$server/bin/code-server" ]; then + server="$HOME/.cache/vscode-server-installer" + if [ ! -x "$server/bin/code-server" ]; then + case "$(uname -m)" in + x86_64) arch=x64 ;; + aarch64 | arm64) arch=arm64 ;; + *) echo "devcontainer: no VS Code Server for $(uname -m), skipping extensions" >&2; exit 0 ;; + esac + url="https://update.code.visualstudio.com/latest/server-linux-$arch/stable" + rm -rf "$server" && mkdir -p "$server" + if command -v curl >/dev/null 2>&1; then fetch="curl -fsSL"; else fetch="wget -qO-"; fi + if ! $fetch "$url" | tar -xz -C "$server" --strip-components 1; then + echo "devcontainer: could not download VS Code Server, skipping extensions" >&2 + rm -rf "$server" + exit 0 + fi + fi + fi + + # devcontainer-builder's own extension (vscode-extension/), always the + # latest (--force updates it). It was + # deepspacecartel.devcontainer-builder-rebuild before 0.5.0 - removed, + # or both would run. + "$server/bin/code-server" --extensions-dir "$data_dir/extensions" --uninstall-extension deepspacecartel.devcontainer-builder-rebuild >/dev/null 2>&1 || true + case "$own_extension" in + "") ;; + http://* | https://*) + vsix="$HOME/.cache/devcontainer-builder.vsix" + if command -v curl >/dev/null 2>&1; then download="curl -fsSL -o"; else download="wget -qO"; fi + if $download "$vsix.tmp" "$own_extension" && mv "$vsix.tmp" "$vsix"; then + own_extension="$vsix" + else + rm -f "$vsix.tmp" + echo "devcontainer: could not download $own_extension" >&2 + own_extension="" + fi + ;; + esac + if [ -n "$own_extension" ]; then + out=$("$server/bin/code-server" --extensions-dir "$data_dir/extensions" --install-extension "$own_extension" --force 2>&1) + case "$out" in + *"Failed Installing"* | *"not found"*) echo "devcontainer: could not install $own_extension: $out" >&2 ;; + *) echo "devcontainer: $own_extension ready" ;; + esac + fi + + for extension in $extensions; do + out=$("$server/bin/code-server" --extensions-dir "$data_dir/extensions" --install-extension "$extension" 2>&1) + case "$out" in + *"Failed Installing"* | *"not found"*) echo "devcontainer: could not install $extension: $out" >&2 ;; + *) echo "devcontainer: $extension ready" ;; + esac + done + + if [ "$settings_b64" != "${base64encode("{}")}" ]; then + mkdir -p "$data_dir/data/Machine" + echo "$settings_b64" | base64 -d > "$HOME/.cache/devcontainer-vscode-settings.json" + "$server/node" -e ' + const fs = require("fs"); + const [target, incoming] = process.argv.slice(1); + let current = {}; + if (fs.existsSync(target)) { + try { current = JSON.parse(fs.readFileSync(target, "utf8")); } + catch (e) { console.error("devcontainer: " + target + " is not plain JSON, leaving it unchanged"); process.exit(0); } + } + fs.writeFileSync(target, JSON.stringify(Object.assign(current, JSON.parse(fs.readFileSync(incoming, "utf8"))), null, 2) + require("os").EOL); + console.log("devcontainer: merged VS Code settings into " + target); + ' "$data_dir/data/Machine/settings.json" "$HOME/.cache/devcontainer-vscode-settings.json" + fi + EOT +} + +# Everything in devcontainer.json the workspace couldn't honor, as one +# Terraform warning in the workspace's build log (full text, one per line) - +# the agent metadata above only has room for a count. +check "devcontainer_warnings" { + assert { + condition = length(local.warning_lines) == 0 + error_message = "devcontainer.json settings this workspace doesn't honor:\n- ${join("\n- ", local.warning_lines)}" + } +} + +check "devcontainer_variables" { + assert { + condition = length(local.missing_variables) == 0 + error_message = "devcontainer.json uses $${localEnv:...} variables without a value: ${join(", ", local.missing_variables)}. Add NAME=value lines to the workspace's \"Dev Container variables\" setting and restart." + } +} + +# devcontainer.json's forwardPorts as dashboard apps. Coder needs a fixed +# number of apps at plan time, so there are var.max_forwarded_ports slots; +# the ones without a port are hidden. +resource "coder_app" "forwarded_port" { + count = data.coder_workspace.me.start_count * var.max_forwarded_ports + agent_id = coder_agent.main.id + slug = "port-${count.index}" + display_name = count.index < length(local.ports) ? coalesce(try(local.ports[count.index].label, null), "Port ${try(local.ports[count.index].port, 0)}") : null + url = "${try(local.ports[count.index].protocol, "") == "https" ? "https" : "http"}://localhost:${try(local.ports[count.index].port, 0)}" + icon = count.index < length(local.ports) ? "/icon/widgets.svg" : null + subdomain = true + share = "owner" + hidden = count.index >= length(local.ports) +} + +# For the rebuild prompt extension (see devcontainer_vscode): the commit +# the image was built from, compared with origin/, and the Rebuild +# value it increases. +resource "coder_env" "devcontainer_image_commit" { + agent_id = coder_agent.main.id + name = "DEVCONTAINER_IMAGE_COMMIT" + value = devcontainerbuilder_build.workspace.commit != null ? devcontainerbuilder_build.workspace.commit : "" +} + +resource "coder_env" "devcontainer_branch" { + agent_id = coder_agent.main.id + name = "DEVCONTAINER_BRANCH" + value = data.coder_parameter.branch.value +} + +resource "coder_env" "devcontainer_rebuild" { + agent_id = coder_agent.main.id + name = "DEVCONTAINER_REBUILD" + value = data.coder_parameter.rebuild.value +} + +# Commits made in the workspace are attributed to its owner. +resource "coder_env" "git_author_name" { + agent_id = coder_agent.main.id + name = "GIT_AUTHOR_NAME" + value = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) +} + +resource "coder_env" "git_committer_name" { + agent_id = coder_agent.main.id + name = "GIT_COMMITTER_NAME" + value = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) +} + +resource "coder_env" "git_author_email" { + agent_id = coder_agent.main.id + name = "GIT_AUTHOR_EMAIL" + value = data.coder_workspace_owner.me.email +} + +resource "coder_env" "git_committer_email" { + agent_id = coder_agent.main.id + name = "GIT_COMMITTER_EMAIL" + value = data.coder_workspace_owner.me.email +} + +resource "kubernetes_persistent_volume_claim_v1" "data" { + metadata { + name = "coder-${data.coder_workspace.me.id}-data" + namespace = var.namespace + labels = { + "app.kubernetes.io/name" = "coder-pvc" + "app.kubernetes.io/instance" = "coder-pvc-${data.coder_workspace.me.id}" + "app.kubernetes.io/part-of" = "coder" + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + annotations = { + "com.coder.user.email" = data.coder_workspace_owner.me.email + } + } + wait_until_bound = false + spec { + access_modes = ["ReadWriteOnce"] + resources { + requests = { + storage = local.disk_size + } + } + } + + # The size comes from the image's hostRequirements while the workspace + # runs, and from the parameter while it's stopped - never shrink (or + # churn) the claim over that. + lifecycle { + ignore_changes = [spec[0].resources[0].requests] + } +} + +resource "kubernetes_deployment_v1" "main" { + count = data.coder_workspace.me.start_count + depends_on = [ + kubernetes_persistent_volume_claim_v1.data + ] + + wait_for_rollout = false + metadata { + name = "coder-${data.coder_workspace.me.id}" + namespace = var.namespace + labels = { + "app.kubernetes.io/name" = "coder-workspace" + "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" + "app.kubernetes.io/part-of" = "coder" + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + annotations = { + "com.coder.user.email" = data.coder_workspace_owner.me.email + } + } + + spec { + replicas = 1 + selector { + match_labels = { + "app.kubernetes.io/name" = "coder-workspace" + "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" + "app.kubernetes.io/part-of" = "coder" + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + } + strategy { + type = "Recreate" + } + + template { + metadata { + labels = { + "app.kubernetes.io/name" = "coder-workspace" + "app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}" + "app.kubernetes.io/part-of" = "coder" + "com.coder.resource" = "true" + "com.coder.workspace.id" = data.coder_workspace.me.id + "com.coder.workspace.name" = data.coder_workspace.me.name + "com.coder.user.id" = data.coder_workspace_owner.me.id + "com.coder.user.username" = data.coder_workspace_owner.me.name + } + } + spec { + # Everything runs as the remote user; fsGroup makes the PVC + # writable for it (OnRootMismatch: only re-owned when the volume + # root doesn't match, not on every start). + security_context { + run_as_user = local.uid + run_as_group = local.gid + fs_group = local.gid + fs_group_change_policy = "OnRootMismatch" + } + + # devcontainer.json's `init` / --init: the pause container becomes + # PID 1 and reaps zombies, like Docker's tini. + share_process_namespace = try(local.runtime.init, false) + hostname = try(local.runtime.hostname, null) + + dynamic "host_aliases" { + for_each = try(local.runtime.host_aliases, []) + content { + ip = host_aliases.value.ip + hostnames = host_aliases.value.hostnames + } + } + + dynamic "image_pull_secrets" { + for_each = var.image_pull_secret_name != "" ? [1] : [] + content { + name = var.image_pull_secret_name + } + } + + # Runs before "dev", with the same image and user: there, the image's + # own home is still visible (nothing is mounted over it), and the PVC + # is at /mnt/data. On first start only (marker file), it copies that + # home into the PVC's home/ - so the subPath mount below doesn't hide + # the image's dotfiles - and from then on the user's own changes win. + # It pre-creates every subPath dir (kubelet would create them + # root-owned). + init_container { + name = "seed-home" + image = devcontainerbuilder_build.workspace.image + image_pull_policy = "IfNotPresent" + command = ["sh", "-c", <<-EOT + set -eu + mkdir -p /mnt/data/home /mnt/data/workspaces /mnt/data/workspace-folder + for dir in $VOLUME_DIRS $MOUNT_POINT_DIRS; do mkdir -p "/mnt/data/$dir"; done + if [ ! -e /mnt/data/home/.devcontainer-home-seeded ]; then + cp -R --preserve=mode,timestamps,links --no-clobber "$HOME_DIR"/. /mnt/data/home/ + touch /mnt/data/home/.devcontainer-home-seeded + echo "seed-home: seeded /mnt/data/home from $HOME_DIR" + fi + EOT + ] + env { + name = "HOME_DIR" + value = local.home_dir + } + env { + name = "VOLUME_DIRS" + value = join(" ", local.volume_dirs) + } + env { + name = "MOUNT_POINT_DIRS" + value = join(" ", local.mount_point_dirs) + } + resources { + requests = { + "cpu" = "50m" + "memory" = "64Mi" + } + limits = { + "cpu" = "500m" + "memory" = "256Mi" + } + } + volume_mount { + mount_path = "/mnt/data" + name = "data" + } + } + + container { + name = "dev" + # The one real change from the upstream coder/kubernetes template + # - a fixed/parameterized `image` variable becomes the real, + # pushed image this workspace's own devcontainerbuilder_build + # resource just built. + image = devcontainerbuilder_build.workspace.image + image_pull_policy = "IfNotPresent" + # Sets the Dev Container environment (workspace variables, the + # user's Dev Container variables, then containerEnv and remoteEnv - + # so `$${PATH}:/x` expands against the image's real PATH), then + # runs the agent. Everything the agent starts - terminals, IDEs, + # the lifecycle scripts - inherits that environment. + command = ["sh", "-c", <<-EOT + set -eu + export DEVCONTAINER_WORKSPACE_FOLDER="$DC_WORKSPACE_FOLDER" + export DEVCONTAINER_WORKSPACE_FOLDER_BASENAME="$(basename "$DC_WORKSPACE_FOLDER")" + export DEVCONTAINER_ID="$DC_ID" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in '' | '#'*) continue ;; esac + key="$${line%%=*}" + case "$key" in '' | *[!A-Za-z0-9_]*) echo "workspace: ignoring Dev Container variable line '$key'" >&2; continue ;; esac + export "DEVCONTAINER_LOCALENV_$key=$${line#*=}" + done < /tmp/devcontainer-env.sh + . /tmp/devcontainer-env.sh + done + set -u + rm -f /tmp/devcontainer-env.sh + export HOME="$DC_HOME" USER="$DC_REMOTE_USER" LOGNAME="$DC_REMOTE_USER" + init="$DC_INIT_SCRIPT" + unset DC_WORKSPACE_FOLDER DC_ID DC_VARIABLES DC_CONTAINER_ENV DC_REMOTE_ENV DC_REMOTE_USER DC_INIT_SCRIPT DC_HOME + exec sh -c "$init" + EOT + ] + security_context { + privileged = var.allow_privileged && try(local.runtime.privileged, false) + capabilities { + add = local.capabilities + } + dynamic "seccomp_profile" { + for_each = var.allow_privileged && try(local.runtime.seccomp_unconfined, false) ? [1] : [] + content { + type = "Unconfined" + } + } + } + env { + name = "CODER_AGENT_TOKEN" + value = coder_agent.main.token + } + env { + name = "DC_INIT_SCRIPT" + value = coder_agent.main.init_script + } + env { + name = "DC_REMOTE_USER" + value = local.remote_user + } + env { + name = "DC_HOME" + value = local.home_dir + } + env { + name = "DC_WORKSPACE_FOLDER" + value = local.workspace_folder + } + env { + name = "DC_ID" + value = data.coder_workspace.me.id + } + env { + name = "DC_VARIABLES" + value = data.coder_parameter.devcontainer_variables.value + } + env { + name = "DC_CONTAINER_ENV" + value = base64encode(lookup(local.env_scripts, "containerEnv", "")) + } + env { + name = "DC_REMOTE_ENV" + value = base64encode(lookup(local.env_scripts, "remoteEnv", "")) + } + resources { + requests = { + "cpu" = local.cpu_request + "memory" = local.memory_request + } + limits = merge({ + "cpu" = local.cpu_limit + "memory" = local.memory_limit + }, local.gpu_limit) + } + volume_mount { + mount_path = local.home_dir + name = "data" + sub_path = "home" + read_only = false + } + volume_mount { + mount_path = "/workspaces" + name = "data" + sub_path = "workspaces" + read_only = false + } + # A workspaceFolder outside /workspaces gets its own directory on + # the PVC; otherwise this mount points at an unused path. + volume_mount { + mount_path = startswith(local.workspace_folder, "/workspaces/") ? "/mnt/.devcontainer-unused-workspace-folder" : local.workspace_folder + name = "data" + sub_path = "workspace-folder" + read_only = false + } + # devcontainer.json's volume mounts, persisted on the PVC. + dynamic "volume_mount" { + for_each = local.volume_mounts + content { + mount_path = volume_mount.value.target + name = "data" + sub_path = "volumes/${coalesce(volume_mount.value.source, "anonymous-${volume_mount.value.index}")}" + read_only = volume_mount.value.read_only + } + } + dynamic "volume_mount" { + for_each = local.tmpfs_mounts + content { + mount_path = volume_mount.value.target + name = "tmpfs-${volume_mount.value.index}" + } + } + volume_mount { + mount_path = "/dev/shm" + name = "dshm" + } + } + + volume { + name = "data" + persistent_volume_claim { + claim_name = kubernetes_persistent_volume_claim_v1.data.metadata.0.name + read_only = false + } + } + + # /dev/shm sized from runArgs --shm-size (Docker's default is 64 MiB). + volume { + name = "dshm" + empty_dir { + medium = "Memory" + size_limit = try(local.runtime.shm_size_bytes, null) != null ? tostring(local.runtime.shm_size_bytes) : "64Mi" + } + } + + dynamic "volume" { + for_each = local.tmpfs_mounts + content { + name = "tmpfs-${volume.value.index}" + empty_dir { + medium = "Memory" + } + } + } + + affinity { + # Spreads workspace pods evenly across nodes. + pod_anti_affinity { + preferred_during_scheduling_ignored_during_execution { + weight = 1 + pod_affinity_term { + topology_key = "kubernetes.io/hostname" + label_selector { + match_expressions { + key = "app.kubernetes.io/name" + operator = "In" + values = ["coder-workspace"] + } + } + } + } + } + } + } + } + } +} From c59b45f1c00924668a0d3a2871c826f6b34bf99c Mon Sep 17 00:00:00 2001 From: Aleksandr Ilin Date: Wed, 7 Oct 2026 07:02:46 -0700 Subject: [PATCH 2/2] feat(deepspacecartel/kubernetes-devcontainer): sync with devcontainer-builder main, add a screenshot Template: per-workspace image tags, robust clone and hook scripts, hostRequirements as minimums, optional GitHub external auth for private repositories, subdomain_apps for deployments without a wildcard access URL, ordered parameters. README: a screenshot of the rebuild prompt. Co-Authored-By: Claude Opus 5.5 --- .../kubernetes-devcontainer-rebuild.png | Bin 0 -> 101825 bytes .../kubernetes-devcontainer/README.md | 6 +- .../templates/kubernetes-devcontainer/main.tf | 275 ++++++++++++++---- 3 files changed, 225 insertions(+), 56 deletions(-) create mode 100644 registry/deepspacecartel/.images/kubernetes-devcontainer-rebuild.png diff --git a/registry/deepspacecartel/.images/kubernetes-devcontainer-rebuild.png b/registry/deepspacecartel/.images/kubernetes-devcontainer-rebuild.png new file mode 100644 index 0000000000000000000000000000000000000000..72199b140c91a229c2a386b0e7a1f94927f787eb GIT binary patch literal 101825 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 literal 0 HcmV?d00001 diff --git a/registry/deepspacecartel/templates/kubernetes-devcontainer/README.md b/registry/deepspacecartel/templates/kubernetes-devcontainer/README.md index ad6e3ec3d..b1986a359 100644 --- a/registry/deepspacecartel/templates/kubernetes-devcontainer/README.md +++ b/registry/deepspacecartel/templates/kubernetes-devcontainer/README.md @@ -15,7 +15,7 @@ configuration (base image, Features, Dockerfile) by workspace runs it with the configuration's lifecycle commands, environment, mounts, forwarded ports, VS Code extensions and settings - like GitHub Codespaces, self-hosted. - +![A workspace offering to rebuild after its Dev Container configuration changed on the branch](../../.images/kubernetes-devcontainer-rebuild.png) ## What you get @@ -29,6 +29,7 @@ forwarded ports, VS Code extensions and settings - like GitHub Codespaces, self- configuration changes, and clones repositories into new workspaces from a local VS Code. - **Private repositories** with each user's own account (`external_auth_id`). - **`hostRequirements`** are reserved on the node, as minimums. +- **Each workspace builds its own image tag**, so one workspace's rebuild never affects another. ## Prerequisites @@ -64,4 +65,5 @@ coder templates push kubernetes-devcontainer -d . \ Optional variables: `image_pull_secret_name`, `external_auth_id` (e.g. `github`, for private repositories), `max_cpu`/`max_memory`, `allow_privileged`, `accept_vscode_license`, -`max_forwarded_ports`, `vscode_extension` - see `main.tf`. +`max_forwarded_ports`, `subdomain_apps` (set `false` without a wildcard access URL), +`vscode_extension` - see `main.tf`. diff --git a/registry/deepspacecartel/templates/kubernetes-devcontainer/main.tf b/registry/deepspacecartel/templates/kubernetes-devcontainer/main.tf index 6ef452fcf..b94bd9b4e 100644 --- a/registry/deepspacecartel/templates/kubernetes-devcontainer/main.tf +++ b/registry/deepspacecartel/templates/kubernetes-devcontainer/main.tf @@ -25,18 +25,26 @@ # customizations.vscode extensions/settings are installed into # ~/.vscode-server, where VS Code Desktop picks them up. terraform { + # check blocks (1.5). + required_version = ">= 1.5.0" required_providers { coder = { source = "coder/coder" + # coder_parameter's form_type (2.4.0, fixed in 2.5.0); the vscode-web + # and vscode-desktop modules need >= 2.5 too. + version = ">= 2.5.0" } kubernetes = { source = "hashicorp/kubernetes" + # The pod security context's fs_group_change_policy (2.16.0). + version = ">= 2.16.0" } devcontainerbuilder = { source = "deepspacecartel/devcontainer-builder" # >= 0.3.0 for the data source's runtime/env_scripts/variables (and a - # devcontainer-builder service >= 0.3.0 behind it). - version = ">= 0.3.0" + # devcontainer-builder service >= 0.3.0 behind it); < 2.0.0 because a + # major version may change the schema this template uses. + version = ">= 0.3.0, < 2.0.0" } } } @@ -91,6 +99,12 @@ variable "allow_privileged" { default = false } +variable "subdomain_apps" { + type = bool + description = "Serve VS Code in the browser and the forwarded-port apps on their own subdomains (Coder's recommendation). They need a wildcard access URL on the Coder deployment (CODER_WILDCARD_ACCESS_URL, https://coder.com/docs/admin/networking/wildcard-access-url); set false without one, and they're served on paths of the main Coder URL instead." + default = true +} + variable "max_forwarded_ports" { type = number description = "How many of devcontainer.json's forwardPorts get a dashboard app. Coder needs a fixed number of app slots; unused ones are hidden." @@ -143,7 +157,8 @@ variable "git_credentials_token" { data "coder_parameter" "repository" { name = "repository" display_name = "Git repository" - description = "A git repository containing a .devcontainer.json (or .devcontainer/devcontainer.json) at its root. https://, ssh://, or SCP-style (git@host:path) all work." + order = 1 + description = "The git repository to work on: https://, ssh://, or SCP-style (git@host:path). Its .devcontainer/devcontainer.json (or .devcontainer.json) defines the workspace; without one, it gets devcontainer-builder's fallback image." icon = "/icon/git.svg" mutable = false } @@ -151,6 +166,7 @@ data "coder_parameter" "repository" { data "coder_parameter" "branch" { name = "branch" display_name = "Branch" + order = 2 description = "Branch to build." default = "main" icon = "/icon/git.svg" @@ -160,6 +176,7 @@ data "coder_parameter" "branch" { data "coder_parameter" "cpu" { name = "cpu" display_name = "CPU" + order = 3 description = "The number of CPU cores" default = "2" icon = "/icon/memory.svg" @@ -185,24 +202,25 @@ data "coder_parameter" "cpu" { data "coder_parameter" "memory" { name = "memory" display_name = "Memory" - description = "The amount of memory in GB" + order = 4 + description = "The amount of memory in GiB" default = "2" icon = "/icon/memory.svg" mutable = true option { - name = "2 GB" + name = "2 GiB" value = "2" } option { - name = "4 GB" + name = "4 GiB" value = "4" } option { - name = "6 GB" + name = "6 GiB" value = "6" } option { - name = "8 GB" + name = "8 GiB" value = "8" } } @@ -210,7 +228,8 @@ data "coder_parameter" "memory" { data "coder_parameter" "disk_size" { name = "disk_size" display_name = "Disk size" - description = "The size of the persistent disk (home + /workspaces) in GB" + order = 5 + description = "The size of the persistent disk (home + /workspaces) in GiB, set when the workspace is created" default = "10" type = "number" icon = "/emojis/1f4be.png" @@ -229,6 +248,7 @@ data "coder_parameter" "disk_size" { data "coder_parameter" "devcontainer_variables" { name = "devcontainer_variables" display_name = "Dev Container variables" + order = 6 description = "Values for $${localEnv:NAME} in the repository's devcontainer.json: one NAME=value per line. Changes apply on the next restart. Visible to anyone who can see this workspace's settings." type = "string" form_type = "textarea" @@ -242,6 +262,7 @@ data "coder_parameter" "devcontainer_variables" { data "coder_parameter" "rebuild" { name = "rebuild" display_name = "Rebuild" + order = 7 description = "Increase to rebuild the image from the branch's latest commit on the next start (e.g. after devcontainer.json changes). Your working copy is not touched." type = "number" default = 0 @@ -304,8 +325,13 @@ locals { "$${containerWorkspaceFolderBasename}", local.repo_name), "$${devcontainerId}", data.coder_workspace.me.id) + # The workspace folder gets its own directory on the PVC unless it's + # already persisted by the /workspaces mount (at or below it) - or is the + # home itself, which a second mount at the same path would collide with. + workspace_folder_mounted = !(local.workspace_folder == "/workspaces" || startswith(local.workspace_folder, "/workspaces/") || local.workspace_folder == local.home_dir) + # Mount targets may use the workspace placeholders too. - mounts = [for i, m in try(local.dc.mounts, []) : merge(m, { + requested_mounts = [for i, m in try(local.dc.mounts, []) : merge(m, { index = i target = replace(replace(replace(replace(m.target, "$${containerWorkspaceFolder}", local.workspace_folder), @@ -313,6 +339,20 @@ locals { "$${containerWorkspaceFolderBasename}", basename(local.workspace_folder)), "$${localWorkspaceFolderBasename}", basename(local.workspace_folder)) })] + # A pod with two volume mounts at the same path is rejected, so a mount + # whose target is one of the template's own mount points (or an earlier + # mount's target) is dropped, with a warning. Compared without a + # trailing slash. + reserved_mount_paths = compact(["/workspaces", local.home_dir, "/dev/shm", local.workspace_folder_mounted ? local.workspace_folder : ""]) + mount_keys = [for m in local.requested_mounts : trimsuffix(m.target, "/")] + mount_collides = [for i, key in local.mount_keys : + contains(local.reserved_mount_paths, key) || contains(slice(local.mount_keys, 0, i), key) + ] + mounts = [for i, m in local.requested_mounts : m if !local.mount_collides[i]] + colliding_mounts = [for i, m in local.requested_mounts : m if local.mount_collides[i]] + mount_warnings = [for m in local.colliding_mounts : + "mounts: the ${m.kind} mount at ${m.target} is not mounted - that path is already a mount point (the home, /workspaces, the workspace folder, /dev/shm or another mount)" + ] volume_mounts = [for m in local.mounts : m if m.kind == "volume"] tmpfs_mounts = [for m in local.mounts : m if m.kind == "tmpfs"] # PVC directory per named volume (volumes/); anonymous volumes @@ -363,7 +403,7 @@ locals { requested_capabilities = try(local.runtime.cap_add, []) capabilities = var.allow_privileged ? local.requested_capabilities : [for c in local.requested_capabilities : c if contains(local.baseline_capabilities, c)] skipped_capabilities = var.allow_privileged ? [] : [for c in local.requested_capabilities : c if !contains(local.baseline_capabilities, c)] - all_warnings = concat(local.warnings, local.resource_warnings, length(local.skipped_capabilities) > 0 ? [ + all_warnings = concat(local.warnings, local.resource_warnings, local.mount_warnings, length(local.skipped_capabilities) > 0 ? [ "capAdd ${join(", ", local.skipped_capabilities)} not added - Pod Security baseline forbids it (template variable allow_privileged enables it)" ] : [], local.dc != null && local.recorded_uid == null ? [ "the image doesn't record its remote user's uid/gid (built by devcontainer-builder older than 0.3.0) - running as uid/gid 1000; bump the Rebuild parameter to rebuild it" @@ -377,13 +417,15 @@ locals { mount_point_dirs = compact([for t in concat([local.workspace_folder], [for m in local.mounts : m.target]) : startswith(t, "/workspaces/") ? "workspaces/${trimprefix(t, "/workspaces/")}" : startswith(t, "${local.home_dir}/") ? "home/${trimprefix(t, "${local.home_dir}/")}" : - !startswith(local.workspace_folder, "/workspaces/") && startswith(t, "${local.workspace_folder}/") ? "workspace-folder/${trimprefix(t, "${local.workspace_folder}/")}" : "" + local.workspace_folder_mounted && startswith(t, "${local.workspace_folder}/") ? "workspace-folder/${trimprefix(t, "${local.workspace_folder}/")}" : "" ]) # localEnv variables without a default that the user must provide - # ignoring ones only used by mounts, which only matter for bind mounts # (dropped anyway: there's no host). - required_variables = [for v in local.variables : v.name if v.kind == "localEnv" && v.default == null && length([for u in v.used_in : u if !startswith(u, "mounts")]) > 0] + # Only shell-safe names (the service only reports those, but they end up + # in a shell script and a regex below, so it's checked here too). + required_variables = [for v in local.variables : v.name if v.kind == "localEnv" && v.default == null && can(regex("^[A-Za-z_][A-Za-z0-9_]*$", v.name)) && length([for u in v.used_in : u if !startswith(u, "mounts")]) > 0] # Names only - the checks below must never reference the parameter itself: # Terraform prints referenced values in a failed check's diagnostics, and # the parameter holds the user's tokens. @@ -391,6 +433,31 @@ locals { # Not secrets, but derived from the data source (whose registry # credentials input is sensitive) - unmarked so the build log shows them. warning_lines = nonsensitive([for w in local.all_warnings : nonsensitive(w)]) + + # Values written into the scripts below as single-quoted shell words + # ('...' with each ' as '\''): they come from the user's parameters and + # the repository's devcontainer.json, so they may contain anything. + sh_workspace_folder = "'${replace(local.workspace_folder, "'", "'\\''")}'" + sh_repository = "'${replace(data.coder_parameter.repository.value, "'", "'\\''")}'" + sh_branch = "'${replace(data.coder_parameter.branch.value, "'", "'\\''")}'" + sh_own_extension = "'${replace(var.vscode_extension, "'", "'\\''")}'" + # One extension ID per line. + sh_extensions = "'${replace(join("\n", local.vscode_extensions), "'", "'\\''")}'" + + # Where the lifecycle script records its exit status: on the pod's root + # filesystem, which is fresh on every start (unlike the home). + lifecycle_status_file = "/tmp/devcontainer-lifecycle.status" + + # The architecture the agent binary, the image and the node agree on. + arch = "amd64" + + # This workspace's own image tag. Without one, the tag is sha-, + # shared by every workspace built from the same repository and commit - + # and deleting the image (a Rebuild, or deleting the workspace) would + # delete it from under the others. The Rebuild value is part of it so a + # rebuilt image never reuses a tag a node may have cached (the pod pulls + # IfNotPresent). Lowercase, OCI tag characters only, at most 128. + image_tag = substr(replace(lower("ws-${data.coder_workspace.me.id}-${data.coder_parameter.rebuild.value}"), "/[^a-z0-9_.-]/", "-"), 0, 128) } # --- The actual build ---------------------------------------------------- @@ -418,9 +485,15 @@ resource "devcontainerbuilder_build" "workspace" { repository = data.coder_parameter.repository.value branch = data.coder_parameter.branch.value - # A Rebuild bump replaces the build: the old image tag is deleted first - # and the branch's latest commit built (destroy-then-create is right - - # an unchanged branch rebuilds to the same tag). + # The image goes to this workspace's own tag (see local.image_tag); its + # registry and name are still resolved by devcontainer-builder. + image_spec = { + tag = local.image_tag + } + + # A Rebuild bump replaces the build: the old image tag (this workspace's + # only) is deleted first and the branch's latest commit built under the + # new one. # Credentials are only needed for the build itself: a linked account's # token is refreshed over time, and must not replace (rebuild) the image # each time it changes. A Rebuild bump builds with the current one. @@ -450,11 +523,12 @@ data "devcontainerbuilder_devcontainer" "workspace" { registry = devcontainerbuilder_build.workspace.resolved_registry name = devcontainerbuilder_build.workspace.resolved_name tag = devcontainerbuilder_build.workspace.resolved_tag + platform = "linux/${local.arch}" } resource "coder_agent" "main" { os = "linux" - arch = "amd64" + arch = local.arch # No `dir`: it's deprecated, and anything but $HOME breaks Coder Desktop # file sync - so terminals/SSH start in $HOME. code-server and # vscode_desktop open the repo. @@ -561,6 +635,7 @@ module "vscode_web" { folder = local.workspace_folder accept_license = true install_prefix = "$HOME/.cache/vscode-web" + subdomain = var.subdomain_apps } # VS Code Desktop, opened on the cloned repo (in place of the agent's @@ -581,7 +656,9 @@ module "vscode_desktop" { # from the repo folder, before login. In Dev Containers the first three run once per # container; here every start is a fresh root filesystem, so all four run # on every start and must be idempotent. Output is in the agent's startup -# logs. +# logs. Its exit status goes to local.lifecycle_status_file (on the pod's +# own filesystem, so never left over from an earlier start), which +# postAttachCommand waits for. resource "coder_script" "devcontainer_lifecycle" { count = data.coder_workspace.me.start_count agent_id = coder_agent.main.id @@ -592,7 +669,10 @@ resource "coder_script" "devcontainer_lifecycle" { script = <<-EOT #!/bin/sh set -u - workspace_folder='${local.workspace_folder}' + status_file='${local.lifecycle_status_file}' + rm -f "$status_file" + trap 'rc=$?; echo "$rc" > "$status_file.tmp" && mv -f "$status_file.tmp" "$status_file"; exit "$rc"' EXIT + workspace_folder=${local.sh_workspace_folder} dir="$HOME/.cache/devcontainer-lifecycle" mkdir -p "$dir" @@ -600,36 +680,72 @@ resource "coder_script" "devcontainer_lifecycle" { # the commit the image was built from (on its branch), so the hooks # below run the scripts that image was built for. git init + fetch # rather than `git clone`: the folder may already hold mount points - # (e.g. a node_modules volume). An existing working copy is never - # touched. Private repos authenticate like the agent: GIT_ASKPASS - # (Coder external auth) for HTTPS, `coder gitssh` for SSH. - repository='${data.coder_parameter.repository.value}' - branch='${data.coder_parameter.branch.value}' + # (e.g. a node_modules volume). Private repos authenticate like the + # agent: GIT_ASKPASS (Coder external auth) for HTTPS, `coder gitssh` + # for SSH. + # + # A finished clone is marked with .git/devcontainer-cloned, and is never + # touched again. A .git without the marker is either a working copy + # (HEAD has a commit - e.g. cloned by an older template version: kept, + # and marked) or a clone that was cut short before its checkout + # finished (HEAD unborn: removed and retried). + repository=${local.sh_repository} + branch=${local.sh_branch} commit='${devcontainerbuilder_build.workspace.commit != null ? devcontainerbuilder_build.workspace.commit : ""}' - if [ ! -e "$workspace_folder/.git" ]; then + git_dir="$workspace_folder/.git" + cloned_marker="$git_dir/devcontainer-cloned" + retry=false + if [ -d "$git_dir" ] && [ ! -e "$cloned_marker" ]; then + if git -C "$workspace_folder" rev-parse --verify -q HEAD >/dev/null 2>&1; then + touch "$cloned_marker" + else + echo "devcontainer: $workspace_folder has an unfinished clone, starting over" + rm -rf "$git_dir" + retry=true + fi + fi + if [ ! -e "$git_dir" ]; then + if ! command -v git >/dev/null 2>&1; then + echo "devcontainer: the image has no git, so $repository can't be cloned - add the git Feature (\"ghcr.io/devcontainers/features/git:1\": {}) to devcontainer.json's features, and rebuild" >&2 + exit 1 + fi case "$repository" in ssh://* | *@*:*) host=$(printf '%s' "$repository" | sed -E 's#^ssh://##; s#^[^@]*@##; s#[:/].*$##') mkdir -p "$HOME/.ssh" && touch "$HOME/.ssh/known_hosts" - ssh-keygen -F "$host" -f "$HOME/.ssh/known_hosts" >/dev/null 2>&1 || ssh-keyscan -H "$host" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true + ssh-keygen -F "$host" -f "$HOME/.ssh/known_hosts" >/dev/null 2>&1 || ssh-keyscan -H -- "$host" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true ;; esac echo "devcontainer: cloning $repository ($branch) into $workspace_folder" mkdir -p "$workspace_folder" - git -C "$workspace_folder" init -q -b "$branch" && + # `init` + symbolic-ref rather than `init -b`, which needs git 2.28. + git -C "$workspace_folder" init -q && + git -C "$workspace_folder" symbolic-ref HEAD "refs/heads/$branch" && git -C "$workspace_folder" remote add origin "$repository" && git -C "$workspace_folder" fetch -q origin "+refs/heads/$branch:refs/remotes/origin/$branch" || { echo "devcontainer: cloning $repository failed" >&2 - rm -rf "$workspace_folder/.git" + rm -rf "$git_dir" exit 1 } target="origin/$branch" if [ -n "$commit" ] && { git -C "$workspace_folder" cat-file -e "$commit^{commit}" 2>/dev/null || git -C "$workspace_folder" fetch -q origin "$commit"; }; then target="$commit" fi - git -C "$workspace_folder" checkout -q -B "$branch" "$target" && - git -C "$workspace_folder" branch -q --set-upstream-to="origin/$branch" "$branch" && - echo "devcontainer: checked out $(git -C "$workspace_folder" rev-parse --short HEAD), the commit the image was built from" || exit 1 + # A retry overwrites the files the interrupted checkout already + # wrote. A first checkout doesn't: files that were already in the + # folder and are in the way fail it (git lists them), and its .git is + # removed, so the next start tries the same way again. + force="" + if [ "$retry" = true ]; then force="-f"; fi + if ! git -C "$workspace_folder" checkout -q $force -B "$branch" "$target"; then + echo "devcontainer: checking out $target in $workspace_folder failed" >&2 + [ "$retry" = true ] || rm -rf "$git_dir" + exit 1 + fi + git -C "$workspace_folder" branch -q --set-upstream-to="origin/$branch" "$branch" || + echo "devcontainer: couldn't set origin/$branch as $branch's upstream" >&2 + touch "$cloned_marker" + echo "devcontainer: checked out $(git -C "$workspace_folder" rev-parse --short HEAD), the commit the image was built from" fi run_hook() { @@ -650,7 +766,8 @@ resource "coder_script" "devcontainer_lifecycle" { # postAttachCommand runs each time a tool attaches in Dev Containers. There # is no attach event here, so it runs once per start - without blocking -# login, after the clone. +# login, after the lifecycle script above has succeeded (as in Dev +# Containers, where it follows postStartCommand). Gives up after 30 minutes. resource "coder_script" "devcontainer_post_attach" { # Always present (count can't depend on the image's metadata, which is # unknown until the first build) - exits at once if no hook is set. @@ -665,8 +782,23 @@ resource "coder_script" "devcontainer_post_attach" { set -u hook='${base64encode(lookup(local.lifecycle_scripts, "postAttachCommand", ""))}' [ -n "$hook" ] || exit 0 - workspace_folder='${local.workspace_folder}' - until [ -f "$workspace_folder/.git/index" ] && [ ! -e "$workspace_folder/.git/index.lock" ]; do sleep 2; done + workspace_folder=${local.sh_workspace_folder} + status_file='${local.lifecycle_status_file}' + echo "devcontainer: waiting for the Dev Container lifecycle script before postAttachCommand" + waited=0 + until [ -s "$status_file" ]; do + if [ "$waited" -ge 1800 ]; then + echo "devcontainer: the Dev Container lifecycle script hasn't finished after 30 minutes, not running postAttachCommand" >&2 + exit 1 + fi + sleep 2 + waited=$((waited + 2)) + done + status=$(cat "$status_file") + if [ "$status" != 0 ]; then + echo "devcontainer: the Dev Container lifecycle script failed (exit status $status), not running postAttachCommand" >&2 + exit 1 + fi script="$HOME/.cache/devcontainer-lifecycle/postAttachCommand.sh" mkdir -p "$(dirname "$script")" echo "$hook" | base64 -d > "$script" @@ -695,9 +827,9 @@ resource "coder_script" "devcontainer_vscode" { script = <<-EOT #!/bin/sh set -u - extensions='${join(" ", local.vscode_extensions)}' + extensions=${local.sh_extensions} settings_b64='${base64encode(local.vscode_settings)}' - own_extension='${var.vscode_extension}' + own_extension=${local.sh_own_extension} data_dir="$HOME/.vscode-server" if [ -z "$extensions" ] && [ "$settings_b64" = "${base64encode("{}")}" ] && [ -z "$own_extension" ]; then exit 0 @@ -750,7 +882,8 @@ resource "coder_script" "devcontainer_vscode" { esac fi - for extension in $extensions; do + printf '%s\n' "$extensions" | while IFS= read -r extension; do + [ -n "$extension" ] || continue out=$("$server/bin/code-server" --extensions-dir "$data_dir/extensions" --install-extension "$extension" 2>&1) case "$out" in *"Failed Installing"* | *"not found"*) echo "devcontainer: could not install $extension: $out" >&2 ;; @@ -758,20 +891,37 @@ resource "coder_script" "devcontainer_vscode" { esac done + # The repository's settings are defaults: a setting is written only if + # the Machine settings don't have it yet, or still have the value this + # script wrote last time (recorded in devcontainer-vscode-settings.applied.json) + # - so a repository's change reaches settings the user hasn't changed, + # and the user's own values are never overwritten. if [ "$settings_b64" != "${base64encode("{}")}" ]; then mkdir -p "$data_dir/data/Machine" echo "$settings_b64" | base64 -d > "$HOME/.cache/devcontainer-vscode-settings.json" "$server/node" -e ' const fs = require("fs"); - const [target, incoming] = process.argv.slice(1); - let current = {}; - if (fs.existsSync(target)) { - try { current = JSON.parse(fs.readFileSync(target, "utf8")); } - catch (e) { console.error("devcontainer: " + target + " is not plain JSON, leaving it unchanged"); process.exit(0); } + const [target, incoming, appliedFile] = process.argv.slice(1); + const read = (file) => fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : {}; + const has = (object, key) => Object.prototype.hasOwnProperty.call(object, key); + const same = (a, b) => JSON.stringify(a) === JSON.stringify(b); + let current; + try { current = read(target); } + catch (e) { console.error("devcontainer: " + target + " is not plain JSON, leaving it unchanged"); process.exit(0); } + let applied = {}; + try { applied = read(appliedFile); } catch (e) {} + const repository = read(incoming); + const written = []; + for (const [key, value] of Object.entries(repository)) { + const isDefault = !has(current, key) || (has(applied, key) && same(current[key], applied[key])); + if (isDefault && !same(current[key], value)) { current[key] = value; written.push(key); } } - fs.writeFileSync(target, JSON.stringify(Object.assign(current, JSON.parse(fs.readFileSync(incoming, "utf8"))), null, 2) + require("os").EOL); - console.log("devcontainer: merged VS Code settings into " + target); - ' "$data_dir/data/Machine/settings.json" "$HOME/.cache/devcontainer-vscode-settings.json" + if (written.length > 0) fs.writeFileSync(target, JSON.stringify(current, null, 2) + require("os").EOL); + const nowApplied = {}; + for (const [key, value] of Object.entries(repository)) if (same(current[key], value)) nowApplied[key] = value; + fs.writeFileSync(appliedFile, JSON.stringify(nowApplied, null, 2) + require("os").EOL); + console.log("devcontainer: VS Code settings in " + target + ": " + (written.length > 0 ? "set " + written.join(", ") : "nothing to change")); + ' "$data_dir/data/Machine/settings.json" "$HOME/.cache/devcontainer-vscode-settings.json" "$HOME/.cache/devcontainer-vscode-settings.applied.json" fi EOT } @@ -803,7 +953,7 @@ resource "coder_app" "forwarded_port" { display_name = count.index < length(local.ports) ? coalesce(try(local.ports[count.index].label, null), "Port ${try(local.ports[count.index].port, 0)}") : null url = "${try(local.ports[count.index].protocol, "") == "https" ? "https" : "http"}://localhost:${try(local.ports[count.index].port, 0)}" icon = count.index < length(local.ports) ? "/icon/widgets.svg" : null - subdomain = true + subdomain = var.subdomain_apps share = "owner" hidden = count.index >= length(local.ports) } @@ -882,9 +1032,11 @@ resource "kubernetes_persistent_volume_claim_v1" "data" { } } - # The size comes from the image's hostRequirements while the workspace - # runs, and from the parameter while it's stopped - never shrink (or - # churn) the claim over that. + # The size is decided when the workspace is created. Later it would come + # from the image's hostRequirements while the workspace runs and from the + # parameter while it's stopped, so changes are ignored: never shrink (or + # churn) the claim over that. A larger hostRequirements.storage after a + # rebuild doesn't grow it (not every storage class can expand volumes). lifecycle { ignore_changes = [spec[0].resources[0].requests] } @@ -962,6 +1114,11 @@ resource "kubernetes_deployment_v1" "main" { share_process_namespace = try(local.runtime.init, false) hostname = try(local.runtime.hostname, null) + # Only nodes of the agent's (and the image's) architecture. + node_selector = { + "kubernetes.io/arch" = local.arch + } + dynamic "host_aliases" { for_each = try(local.runtime.host_aliases, []) content { @@ -992,10 +1149,19 @@ resource "kubernetes_deployment_v1" "main" { set -eu mkdir -p /mnt/data/home /mnt/data/workspaces /mnt/data/workspace-folder for dir in $VOLUME_DIRS $MOUNT_POINT_DIRS; do mkdir -p "/mnt/data/$dir"; done + # A file that can't be copied (unreadable, a socket, ...) is + # only a warning: it mustn't keep the workspace from starting. + # -n (don't overwrite) is in GNU, BusyBox and BSD cp, though + # some report skipped files as a failure. if [ ! -e /mnt/data/home/.devcontainer-home-seeded ]; then - cp -R --preserve=mode,timestamps,links --no-clobber "$HOME_DIR"/. /mnt/data/home/ + if [ ! -d "$HOME_DIR" ]; then + echo "seed-home: $HOME_DIR isn't in the image, starting with an empty home" + elif cp -R -p -n "$HOME_DIR"/. /mnt/data/home/; then + echo "seed-home: seeded /mnt/data/home from $HOME_DIR" + else + echo "seed-home: warning: some of $HOME_DIR couldn't be copied (see above); seeded the rest" + fi touch /mnt/data/home/.devcontainer-home-seeded - echo "seed-home: seeded /mnt/data/home from $HOME_DIR" fi EOT ] @@ -1140,10 +1306,11 @@ resource "kubernetes_deployment_v1" "main" { sub_path = "workspaces" read_only = false } - # A workspaceFolder outside /workspaces gets its own directory on - # the PVC; otherwise this mount points at an unused path. + # A workspaceFolder outside /workspaces (and not the home itself) + # gets its own directory on the PVC; otherwise this mount points at + # an unused path. volume_mount { - mount_path = startswith(local.workspace_folder, "/workspaces/") ? "/mnt/.devcontainer-unused-workspace-folder" : local.workspace_folder + mount_path = local.workspace_folder_mounted ? local.workspace_folder : "/mnt/.devcontainer-unused-workspace-folder" name = "data" sub_path = "workspace-folder" read_only = false