diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..7fb91be --- /dev/null +++ b/main.tf @@ -0,0 +1,511 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "~> 2.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + envbuilder = { + source = "coder/envbuilder" + } + } +} + +provider "coder" {} +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 "envbuilder" {} + +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +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 + default = "default" + description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace." +} + +variable "cache_repo" { + default = "" + description = "Use a container registry as a cache to speed up builds." + type = string +} + +variable "insecure_cache_repo" { + default = false + description = "Enable this option if your cache registry does not serve HTTPS." + type = bool +} + +data "coder_parameter" "cpu" { + type = "number" + name = "cpu" + display_name = "CPU" + description = "CPU limit (cores)." + default = "1" + icon = "/icon/memory.svg" + mutable = true + option { + name = "1 Core" + value = "1" + } + option { + name = "2 Cores" + value = "2" + } + option { + name = "3 Cores" + value = "3" + } + option { + name = "4 Cores" + value = "4" + } + order = 1 +} + +data "coder_parameter" "memory" { + type = "number" + name = "memory" + display_name = "Memory" + description = "Memory limit (GiB)." + default = "1" + icon = "/icon/memory.svg" + mutable = true + option { + name = "1 GB" + value = "1" + } + option { + name = "2 GB" + value = "2" + } + option { + name = "4 GB" + value = "4" + } + option { + name = "6 GB" + value = "6" + } + option { + name = "8 GB" + value = "8" + } + order = 2 +} + +data "coder_parameter" "workspaces_volume_size" { + name = "workspaces_volume_size" + display_name = "Workspaces volume size" + description = "Size of the `/workspaces` volume (GiB)." + default = "10" + type = "number" + icon = "/emojis/1f4be.png" + mutable = false + validation { + min = 1 + max = 20 + } + order = 3 +} + +data "coder_parameter" "repo" { + description = "Select an ssh repository to automatically clone and start working with a devcontainer (from the default branch)." + display_name = "Repository (ssh)" + mutable = true + name = "repo" + order = 4 + type = "string" +} + +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 = 6 +} + +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. +Find the latest version of Envbuilder here: https://github.com/coder/envbuilder/pkgs/container/envbuilder +EOF + display_name = "Devcontainer Builder" + mutable = true + name = "devcontainer_builder" + default = "ghcr.io/coder/envbuilder:latest" + order = 7 +} + +variable "cache_repo_secret_name" { + default = "" + description = "Path to a docker config.json containing credentials to the provided cache repo, if required." + sensitive = true + type = string +} + +data "kubernetes_secret" "cache_repo_dockerconfig_secret" { + count = var.cache_repo_secret_name == "" ? 0 : 1 + metadata { + name = var.cache_repo_secret_name + namespace = var.namespace + } +} + +locals { + deployment_name = "coder-${lower(data.coder_workspace.me.id)}" + devcontainer_builder_image = data.coder_parameter.devcontainer_builder.value + git_author_name = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name) + git_author_email = data.coder_workspace_owner.me.email + repo_url = data.coder_parameter.repo.value + # Extract match result as a list of strings + repo_name_matches = regexall(".*/([^/]+)$", local.repo_url) + # Get first match if available, else fallback to "default" + repo_name_with_git = ( + length(local.repo_name_matches) > 0 ? + local.repo_name_matches[0][0] : + "default" + ) + # Remove ".git" if present + repo_name = ( + endswith(local.repo_name_with_git, ".git") ? + substr(local.repo_name_with_git, 0, length(local.repo_name_with_git) - 4) : + local.repo_name_with_git + ) + # Final path to be used + repo_path = "/workspaces/${local.repo_name}" + # The envbuilder provider requires a key-value map of environment variables. + envbuilder_env = { + # ENVBUILDER_GIT_URL and ENVBUILDER_CACHE_REPO will be overridden by the provider + # if the cache repo is enabled. + "ENVBUILDER_GIT_URL" : local.repo_url, + "ENVBUILDER_CACHE_REPO" : var.cache_repo, + "CODER_AGENT_TOKEN" : coder_agent.main.token, + # Use the docker gateway if the access URL is 127.0.0.1 + "CODER_AGENT_URL" : replace(data.coder_workspace.me.access_url, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), + # Use the docker gateway if the access URL is 127.0.0.1 + "ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"), + "ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value, + "ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")), + "ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true", + "ENVBUILDER_INSECURE" : "${var.insecure_cache_repo}", + "ENVBUILDER_GIT_SSH_PRIVATE_KEY_BASE64": base64encode(data.coder_workspace_owner.me.ssh_private_key), + # You may need to adjust this if you get an error regarding deleting files when building the workspace. + # For example, when testing in KinD, it was necessary to set `/product_name` and `/product_uuid` in + # addition to `/var/run`. + # "ENVBUILDER_IGNORE_PATHS": "/product_name,/product_uuid,/var/run", + } +} + +# 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 = local.repo_url + cache_repo = var.cache_repo + extra_env = local.envbuilder_env + insecure = var.insecure_cache_repo +} + +resource "kubernetes_persistent_volume_claim" "workspaces" { + metadata { + name = "coder-${lower(data.coder_workspace.me.id)}-workspaces" + namespace = var.namespace + labels = { + "app.kubernetes.io/name" = "coder-${lower(data.coder_workspace.me.id)}-workspaces" + "app.kubernetes.io/instance" = "coder-${lower(data.coder_workspace.me.id)}-workspaces" + "app.kubernetes.io/part-of" = "coder" + //Coder-specific labels. + "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 = "${data.coder_parameter.workspaces_volume_size.value}Gi" + } + } + # storage_class_name = "local-path" # Configure the StorageClass to use here, if required. + } +} + +resource "kubernetes_deployment" "main" { + count = data.coder_workspace.me.start_count + depends_on = [ + kubernetes_persistent_volume_claim.workspaces + ] + wait_for_rollout = false + metadata { + name = local.deployment_name + namespace = var.namespace + labels = { + "app.kubernetes.io/name" = "coder-workspace" + "app.kubernetes.io/instance" = local.deployment_name + "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" + } + } + strategy { + type = "Recreate" + } + + template { + metadata { + labels = { + "app.kubernetes.io/name" = "coder-workspace" + } + } + spec { + security_context {} + + container { + name = "dev" + image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image + image_pull_policy = "Always" + security_context {} + + # Set the environment using cached_image.cached.0.env if the cache repo is enabled. + # Otherwise, use the local.envbuilder_env. + # You could alternatively write the environment variables to a ConfigMap or Secret + # and use that as `env_from`. + dynamic "env" { + for_each = nonsensitive(var.cache_repo == "" ? local.envbuilder_env : envbuilder_cached_image.cached.0.env_map) + content { + name = env.key + value = env.value + } + } + + resources { + requests = { + "cpu" = "250m" + "memory" = "512Mi" + } + limits = { + "cpu" = "${data.coder_parameter.cpu.value}" + "memory" = "${data.coder_parameter.memory.value}Gi" + } + } + volume_mount { + mount_path = "/workspaces" + name = "workspaces" + read_only = false + } + } + + volume { + name = "workspaces" + persistent_volume_claim { + claim_name = kubernetes_persistent_volume_claim.workspaces.metadata.0.name + read_only = false + } + } + + affinity { + // This affinity attempts to spread out all 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"] + } + } + } + } + } + } + } + } + } +} + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + startup_script = <<-EOT + set -e + + # Add any commands that should be executed at workspace startup (e.g install requirements, start a program, etc) here + EOT + dir = "/workspaces" + + # These environment variables allow you to make Git commits right away after creating a + # workspace. Note that they take precedence over configuration defined in ~/.gitconfig! + # You can remove this block if you'd prefer to configure Git manually or using + # dotfiles. (see docs/dotfiles.md) + env = { + GIT_AUTHOR_NAME = local.git_author_name + GIT_AUTHOR_EMAIL = local.git_author_email + GIT_COMMITTER_NAME = local.git_author_name + GIT_COMMITTER_EMAIL = local.git_author_email + } + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + 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 = "Workspaces Disk" + key = "3_workspaces_disk" + script = "coder stat disk --path /workspaces" + 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" + # get load avg scaled by number of cores + script = <<EOT + echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }' + EOT + interval = 60 + timeout = 1 + } + + metadata { + display_name = "Swap Usage (Host)" + key = "7_swap_host" + script = <<EOT + free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }' + EOT + interval = 10 + timeout = 1 + } +} + +# See https://registry.coder.com/modules/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/code-server/coder" + + # This ensures that the latest version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = ">= 1.0.0" + + agent_id = coder_agent.main.id + order = 1 +} + +# See https://registry.coder.com/modules/jetbrains-gateway +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + + # JetBrains IDEs to make available for the user to select + jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + default = "IU" + + # Default folder to open when starting a JetBrains IDE + folder = local.repo_path + + # This ensures that the latest version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. + version = ">= 1.0.0" + + agent_id = coder_agent.main.id + agent_name = "main" + order = 5 +} + +resource "coder_metadata" "container_info" { + count = data.coder_workspace.me.start_count + resource_id = coder_agent.main.id + item { + key = "workspace image" + value = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image + } + item { + key = "git url" + value = local.repo_url + } + item { + key = "cache repo" + value = var.cache_repo == "" ? "not enabled" : var.cache_repo + } +}