diff --git a/README.md b/README.md index 065b225..069a778 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,5 @@ This template will do the following: - Add volumes and firewall policy to the instance - Ask the user if code-server should be installed and if so installs it +# Push +coder template push --variable "hcloud_token=token" diff --git a/cloud-config.yaml.tftpl b/cloud-config.yaml.tftpl index eb5e6dd..b493590 100644 --- a/cloud-config.yaml.tftpl +++ b/cloud-config.yaml.tftpl @@ -3,7 +3,7 @@ users: - name: ${username} sudo: ["ALL=(ALL) NOPASSWD:ALL"] groups: sudo - shell: /bin/bash + shell: /usr/bin/bash packages: - git - curl @@ -42,28 +42,7 @@ write_files: [Install] WantedBy=multi-user.target -%{ if code_server_setup ~} - - path: /tmp/install_code_server.sh - permissions: "0777" - content: | - #!/bin/bash - CODE_SERVER_DOWNLOAD_URL=$(curl -sL https://api.github.com/repos/coder/code-server/releases/latest | jq -r '.assets[].browser_download_url' | grep "amd64.deb") - curl -fL $CODE_SERVER_DOWNLOAD_URL -o /tmp/code_server.deb - dpkg -i /tmp/code_server.deb - systemctl enable --now code-server@${username} - rm /tmp/code_server.deb - - path: /home/${username}/.config/code-server/config.yaml - permissions: "0644" - content: | - bind-addr: 127.0.0.1:8080 - auth: none - cert: false -%{ endif ~} runcmd: - chown ${username}:${username} /home/${username} - - systemctl enable coder-agent - - systemctl start coder-agent -%{ if code_server_setup ~} - - /tmp/install_code_server.sh - - rm /tmp/install_code_server.sh -%{ endif } + - systemctl enable --now coder-agent + - resize2fs /dev/sdb diff --git a/coder.tf b/coder.tf new file mode 100644 index 0000000..e1e9fc5 --- /dev/null +++ b/coder.tf @@ -0,0 +1,116 @@ +data "coder_parameter" "volume_size" { + name = "volume_size" + description = "Disk Size in GB" + default = 10 + type = "number" + mutable = true + validation { + min = 10 + max = 250 + monotonic = "increasing" + } +} + +data "coder_parameter" "code_server_extentions" { + name = "code-server extentions" + type = "list(string)" + description = "List of code-server extentions" + mutable = true + default = jsonencode([ + "dracula-theme.theme-dracula", + "ms-toolsai.jupyter", + "redhat.vscode-yaml", + "redhat.vscode-xml", + "redhat.ansible", + "samuelcolvin.jinjahtml", + "PKief.material-icon-theme", + "hashicorp.terraform", + "hashicorp.hcl", + "GitLab.gitlab-workflow", + "scala-lang.scala", + "scalameta.metals" + ]) +} + +data "coder_workspace" "me" { +} + +data "coder_workspace_owner" "me" { +} + +resource "coder_agent" "dev" { + arch = strcontains(module.hcloud_instance_type.value, "cax") ? "arm64" : "amd64" #change this! + os = "linux" + + metadata { + display_name = "CPU Usage" + key = "cpu" + script = <= 10 - error_message = "Invalid volume size!" - } -} - -variable "code_server" { - description = "Should Code Server be installed?" - default = "true" - validation { - condition = contains(["true","false"], var.code_server) - error_message = "Your answer can only be yes or no!" - } -} - -data "coder_workspace" "me" { -} - -resource "coder_agent" "dev" { - arch = "amd64" - os = "linux" -} - -resource "coder_app" "code-server" { - count = var.code_server ? 1 : 0 - agent_id = coder_agent.dev.id - name = "code-server" - icon = "/icon/code.svg" - url = "http://localhost:8080" - relative_path = true -} - -# Generate a dummy ssh key that is not accessible so Hetzner cloud does not spam the admin with emails. -resource "tls_private_key" "rsa_4096" { - algorithm = "RSA" - rsa_bits = 4096 -} - -resource "hcloud_ssh_key" "root" { - name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" - public_key = tls_private_key.rsa_4096.public_key_openssh -} - -resource "hcloud_server" "root" { - count = data.coder_workspace.me.start_count - name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" - server_type = var.instance_type - location = var.instance_location - image = var.instance_os - ssh_keys = [hcloud_ssh_key.root.id] - user_data = templatefile("cloud-config.yaml.tftpl", { - username = data.coder_workspace.me.owner - volume_path = "/dev/disk/by-id/scsi-0HC_Volume_${hcloud_volume.root.id}" - init_script = base64encode(coder_agent.dev.init_script) - coder_agent_token = coder_agent.dev.token - code_server_setup = var.code_server - }) -} - -resource "hcloud_volume" "root" { - name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" - size = var.volume_size - format = "ext4" - location = var.instance_location -} - -resource "hcloud_volume_attachment" "root" { - count = data.coder_workspace.me.start_count - volume_id = hcloud_volume.root.id - server_id = hcloud_server.root[count.index].id - automount = false -} - -resource "hcloud_firewall" "root" { - name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" - rule { - direction = "in" - protocol = "icmp" - source_ips = [ - "0.0.0.0/0", - "::/0" - ] - } -} - -resource "hcloud_firewall_attachment" "root_fw_attach" { - count = data.coder_workspace.me.start_count - firewall_id = hcloud_firewall.root.id - server_ids = [hcloud_server.root[count.index].id] -} diff --git a/modules.tf b/modules.tf new file mode 100644 index 0000000..67c7054 --- /dev/null +++ b/modules.tf @@ -0,0 +1,38 @@ +module "hcloud_region" { + source = "./modules/hcloud-region" + coder_parameter_order = 0 +} + +module "hcloud_instance_type" { + source = "./modules/hcloud-instance-type" + token = var.hcloud_token + coder_parameter_order = 1 +} + +module "hcloud_os_type" { + source = "./modules/hcloud-os-type" + token = var.hcloud_token + coder_parameter_order = 2 +} + +module "docker" { + source = "./modules/docker" + agent_id = coder_agent.dev.id +} + +module "dotfiles" { + source = "registry.coder.com/modules/dotfiles/coder" + version = "1.0.27" + agent_id = coder_agent.dev.id + coder_parameter_order = 3 +} + +module "settings" { + source = "registry.coder.com/modules/code-server/coder" + version = "1.0.27" + agent_id = coder_agent.dev.id + extensions = jsondecode(data.coder_parameter.code_server_extentions.value) + #settings = { #make configurable + # "workbench.colorTheme" = "Dracula" + #} +} diff --git a/modules/docker/main.tf b/modules/docker/main.tf new file mode 100644 index 0000000..9f737a1 --- /dev/null +++ b/modules/docker/main.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.0" + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +resource "coder_script" "docker" { + agent_id = var.agent_id + display_name = "Docker" + script = templatefile("${path.module}/run.sh", { + }) + icon = "/icon/docker.svg" + run_on_start = true + start_blocks_login = true +} diff --git a/modules/docker/run.sh b/modules/docker/run.sh new file mode 100644 index 0000000..0c8ac08 --- /dev/null +++ b/modules/docker/run.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +printf "docker starting install\n" + +script="$(curl -sS -o- "https://get.docker.com" 2>&1)" +if [ $? -ne 0 ]; then + echo "Failed to download docker installation script: $script" + exit 1 +fi + +output="$(bash <<< "$script" 2>&1)" +if [ $? -ne 0 ]; then + echo "Failed to install docker: $output" + exit 1 +fi + +# Get the OS name +os_name=$(cat /etc/*-release | grep '^ID=' | cut -d'=' -f2) + +# Check if the OS is Fedora, AlmaLinux, or Rocky Linux +if [[ "$os_name" == "fedora" ]] || [[ "$os_name" == "almalinux" ]] || [[ "$os_name" == "rocky" ]]; then + sudo systemctl enable --now docker 2>&1 +fi + +printf "🥳 docker has been installed\n\n" diff --git a/modules/hcloud-instance-type/coder_parameter.tf b/modules/hcloud-instance-type/coder_parameter.tf new file mode 100644 index 0000000..15d44c5 --- /dev/null +++ b/modules/hcloud-instance-type/coder_parameter.tf @@ -0,0 +1,28 @@ +data "coder_parameter" "instance_type" { + name = "hcloud_instance_type" + display_name = var.display_name + description = var.description + icon = "https://cdn.hetzner.com/assets/Uploads/icon-circle-cloud.svg" + mutable = var.mutable + default = var.default != null && var.default != "" ? var.default : null + order = var.coder_parameter_order + dynamic "option" { + for_each = { + for k, v in data.hcloud_server_types.ds.server_types : k => v + if contains(var.architecture, v.architecture) && + contains(var.cpu_type, v.cpu_type) && + var.include_deprecated ? true : !v.is_deprecated + } + content { + name = option.value.description + description = <<-EOF + arch ${option.value.architecture} + cpu_type ${option.value.cpu_type} + cores ${option.value.cores} + ram ${option.value.memory}GB + disk ${option.value.disk}GB + EOF + value = option.value.name + } + } +} diff --git a/modules/hcloud-instance-type/data.tf b/modules/hcloud-instance-type/data.tf new file mode 100644 index 0000000..ce9cb92 --- /dev/null +++ b/modules/hcloud-instance-type/data.tf @@ -0,0 +1,2 @@ +data "hcloud_server_types" "ds" { +} diff --git a/modules/hcloud-instance-type/output.tf b/modules/hcloud-instance-type/output.tf new file mode 100644 index 0000000..28e7266 --- /dev/null +++ b/modules/hcloud-instance-type/output.tf @@ -0,0 +1,7 @@ +output "value" { + value = data.coder_parameter.instance_type.value +} + +output "server_types" { + value = data.hcloud_server_types.ds +} diff --git a/modules/hcloud-instance-type/provider.tf b/modules/hcloud-instance-type/provider.tf new file mode 100644 index 0000000..ec91a21 --- /dev/null +++ b/modules/hcloud-instance-type/provider.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">= 1.0" + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.11" + } + hcloud = { + source = "hetznercloud/hcloud" + version = ">= 1.46" + } + } +} + +provider "hcloud" { + token = var.token +} diff --git a/modules/hcloud-instance-type/variables.tf b/modules/hcloud-instance-type/variables.tf new file mode 100644 index 0000000..4684cb7 --- /dev/null +++ b/modules/hcloud-instance-type/variables.tf @@ -0,0 +1,76 @@ +variable "token" { + description = <<-EOF + Coder requires a Hetzner Cloud token to provision workspaces. + EOF + sensitive = true + validation { + condition = length(var.token) == 64 + error_message = "Please provide a valid Hetzner Cloud API token." + } +} + +variable "display_name" { + default = "hcloud Instance type" + description = "The display name of the parameter." + type = string + validation { + condition = length(var.display_name) > 0 + error_message = "Display must not be empty" + } +} + +variable "description" { + default = "The region to deploy workspace infrastructure." + description = "The description of the parameter." + type = string +} + +variable "default" { + default = null + description = "Default instance type" + type = string +} + +variable "architecture" { + description = "List of instance architectures to include." + type = set(string) + default = [ + "arm", + "x86" + ] + validation { + condition = length(var.architecture) > 0 + error_message = "At least one architecture must be specified!" + } +} + +variable "cpu_type" { + description = "List of cpu types to include." + type = set(string) + default = [ + "shared", + "dedicated" + ] + validation { + condition = length(var.cpu_type) > 0 + error_message = "At least one cpu type must be specified!" + } +} + +variable "include_deprecated" { + description = "Should deprecated instances be included?" + type = bool + default = false +} + +variable "mutable" { + default = true + description = "Whether the parameter can be changed after creation." + type = bool +} + +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} diff --git a/modules/hcloud-os-type/coder_parameter.tf b/modules/hcloud-os-type/coder_parameter.tf new file mode 100644 index 0000000..af001af --- /dev/null +++ b/modules/hcloud-os-type/coder_parameter.tf @@ -0,0 +1,21 @@ +data "coder_parameter" "os" { + name = "hcloud_os" + display_name = var.display_name + description = var.description + icon = "https://cdn.hetzner.com/assets/Uploads/icon-circle-cloud.svg" + mutable = var.mutable + default = var.default != null && var.default != "" ? var.default : null + order = var.coder_parameter_order + dynamic "option" { + for_each = local.os_list + content { + name = replace(option.value, "-", " ") + value = option.value + # the icon for rocky and alma needs to have a linux appended + icon = ( + "/icon/${split("-", option.value)[0] == "alma" || split("-", option.value)[0] == "rocky" ? + "${split("-", option.value)[0]}linux" : split("-", option.value)[0]}.svg" + ) + } + } +} diff --git a/modules/hcloud-os-type/data.tf b/modules/hcloud-os-type/data.tf new file mode 100644 index 0000000..70cef38 --- /dev/null +++ b/modules/hcloud-os-type/data.tf @@ -0,0 +1,11 @@ +data "hcloud_images" "images" { + include_deprecated = var.include_deprecated +} + +locals { + os_list = sort(distinct([ + for item in data.hcloud_images.images.images : item.name if + contains(var.os_flavor, item.os_flavor) && + item.type == "system" #make configurable + ])) +} diff --git a/modules/hcloud-os-type/output.tf b/modules/hcloud-os-type/output.tf new file mode 100644 index 0000000..8f00948 --- /dev/null +++ b/modules/hcloud-os-type/output.tf @@ -0,0 +1,11 @@ +output "value" { + value = data.coder_parameter.os.value +} + +output "hcloud_images_all" { + value = data.hcloud_images.images +} + +output "hcloud_images_filtered" { + value = local.os_list +} diff --git a/modules/hcloud-os-type/provider.tf b/modules/hcloud-os-type/provider.tf new file mode 100644 index 0000000..ec91a21 --- /dev/null +++ b/modules/hcloud-os-type/provider.tf @@ -0,0 +1,17 @@ +terraform { + required_version = ">= 1.0" + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.11" + } + hcloud = { + source = "hetznercloud/hcloud" + version = ">= 1.46" + } + } +} + +provider "hcloud" { + token = var.token +} diff --git a/modules/hcloud-os-type/variables.tf b/modules/hcloud-os-type/variables.tf new file mode 100644 index 0000000..43e240c --- /dev/null +++ b/modules/hcloud-os-type/variables.tf @@ -0,0 +1,65 @@ +variable "token" { + description = <<-EOF + Coder requires a Hetzner Cloud token to provision workspaces. + EOF + sensitive = true + validation { + condition = length(var.token) == 64 + error_message = "Please provide a valid Hetzner Cloud API token." + } +} + +variable "display_name" { + default = "hcloud Instance type" + description = "The display name of the parameter." + type = string + validation { + condition = length(var.display_name) > 0 + error_message = "Display must not be empty" + } +} + +variable "description" { + default = "The region to deploy workspace infrastructure." + description = "The description of the parameter." + type = string +} + +variable "default" { + default = "ubuntu-24.04" + description = "Default instance type" + type = string +} + +variable "os_flavor" { + description = "List of instance os flavors to include." + type = set(string) + default = [ + "ubuntu", #mandatory or change default + "debian", + "fedora", + "alma" + ] + validation { + condition = length(var.os_flavor) > 0 + error_message = "At least one flavor must be specified!" + } +} + +variable "include_deprecated" { + description = "Should deprecated instances be included?" + type = bool + default = false +} + +variable "mutable" { + default = true + description = "Whether the parameter can be changed after creation." + type = bool +} + +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} diff --git a/modules/hcloud-region/coder_parameter.tf b/modules/hcloud-region/coder_parameter.tf new file mode 100644 index 0000000..539aeec --- /dev/null +++ b/modules/hcloud-region/coder_parameter.tf @@ -0,0 +1,21 @@ +data "coder_parameter" "region" { + name = "hcloud_region" + display_name = var.display_name + description = var.description + icon = "https://cdn.hetzner.com/assets/Uploads/icon-circle-cloud.svg" + mutable = var.mutable + default = var.default != null && var.default != "" ? var.default : null + order = var.coder_parameter_order + dynamic "option" { + for_each = { + for k, v in local.zones : k => v + if anytrue([for d in var.regions : startswith(k, d)]) + } + content { + icon = try(var.custom_icons[option.key], option.value.icon) + name = option.value.name + description = option.key + value = option.key + } + } +} diff --git a/modules/hcloud-region/output.tf b/modules/hcloud-region/output.tf new file mode 100644 index 0000000..3fefde5 --- /dev/null +++ b/modules/hcloud-region/output.tf @@ -0,0 +1,3 @@ +output "value" { + value = data.coder_parameter.region.value +} diff --git a/modules/hcloud-region/provider.tf b/modules/hcloud-region/provider.tf new file mode 100644 index 0000000..2d42a52 --- /dev/null +++ b/modules/hcloud-region/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.11" + } + } +} diff --git a/modules/hcloud-region/variables.tf b/modules/hcloud-region/variables.tf new file mode 100644 index 0000000..ed71e76 --- /dev/null +++ b/modules/hcloud-region/variables.tf @@ -0,0 +1,48 @@ +variable "display_name" { + default = "hcloud Region" + description = "The display name of the parameter." + type = string +} + +variable "description" { + default = "The region to deploy workspace infrastructure." + description = "The description of the parameter." + type = string +} + +variable "default" { + default = "fsn1" + description = "Default zone" + type = string +} + +variable "regions" { + description = "List of hcloud regions to include." + type = list(string) + default = [ + "fsn1", + "nbg1", + "hel1", + "ash", + "hil", + "sin" + ] +} + +variable "mutable" { + default = false + description = "Whether the parameter can be changed after creation." + type = bool +} + +variable "custom_icons" { + default = {} + description = "A map of custom icons for region IDs." + type = map(string) +} + +variable "coder_parameter_order" { + type = number + description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)." + default = null +} diff --git a/modules/hcloud-region/zones.tf b/modules/hcloud-region/zones.tf new file mode 100644 index 0000000..402ab79 --- /dev/null +++ b/modules/hcloud-region/zones.tf @@ -0,0 +1,32 @@ +locals { + zones = { + # Germany + fsn1 = { + name = "Falkenstein" + icon = "/emojis/1f1e9-1f1ea.png" + } + nbg1 = { + name = "Nuremberg" + icon = "/emojis/1f1e9-1f1ea.png" + } + # Finland + hel1 = { + name = "Helsinki" + icon = "/emojis/1f1eb-1f1ee.png" + } + # United States of America + ash = { + name = "Ashburn" + icon = "/emojis/1f1fa-1f1f8.png" + } + hil = { + name = "Hillsboro" + icon = "/emojis/1f1fa-1f1f8.png" + } + # Singapore + sin = { + name = "Singapore" + icon = "/emojis/1f1f8-1f1ec.png" + } + } +} diff --git a/provider.tf b/provider.tf new file mode 100644 index 0000000..504eeaa --- /dev/null +++ b/provider.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.5.3" + } + hcloud = { + source = "hetznercloud/hcloud" + version = "1.51.0" + } + } +} + +provider "hcloud" { + token = var.hcloud_token +} + +provider "coder" { +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..b31c457 --- /dev/null +++ b/variables.tf @@ -0,0 +1,10 @@ +variable "hcloud_token" { + description = <<-EOF + Coder requires a Hetzner Cloud token to provision workspaces. + EOF + sensitive = true + validation { + condition = length(var.hcloud_token) == 64 + error_message = "Please provide a valid Hetzner Cloud API token." + } +}