diff --git a/.gitignore b/.gitignore index 3324e4a..4ee2f34 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.tf* result .pre-commit-config.yaml +.terraform* diff --git a/flake.lock b/flake.lock index 4fd8c22..7a1c615 100644 --- a/flake.lock +++ b/flake.lock @@ -22,6 +22,38 @@ "type": "gitlab" } }, + "bats-assert": { + "flake": false, + "locked": { + "lastModified": 1636059754, + "narHash": "sha256-ewME0l27ZqfmAwJO4h5biTALc9bDLv7Bl3ftBzBuZwk=", + "owner": "bats-core", + "repo": "bats-assert", + "rev": "34551b1d7f8c7b677c1a66fc0ac140d6223409e5", + "type": "github" + }, + "original": { + "owner": "bats-core", + "repo": "bats-assert", + "type": "github" + } + }, + "bats-support": { + "flake": false, + "locked": { + "lastModified": 1548869839, + "narHash": "sha256-Gr4ntadr42F2Ks8Pte2D4wNDbijhujuoJi4OPZnTAZU=", + "owner": "bats-core", + "repo": "bats-support", + "rev": "d140a65044b2d6810381935ae7f0c94c7023c8c3", + "type": "github" + }, + "original": { + "owner": "bats-core", + "repo": "bats-support", + "type": "github" + } + }, "cachix": { "inputs": { "devenv": "devenv_2", @@ -348,6 +380,21 @@ "type": "github" } }, + "flake-utils_6": { + "locked": { + "lastModified": 1634851050, + "narHash": "sha256-N83GlSGPJJdcqhUxSCS/WwW5pksYf3VP1M13cDRTSVA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c91f3de5adaf1de973b797ef7485e441a65b8935", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "gitignore": { "inputs": { "nixpkgs": [ @@ -794,6 +841,21 @@ "type": "github" } }, + "nixpkgs_7": { + "locked": { + "lastModified": 1636823747, + "narHash": "sha256-oWo1nElRAOZqEf90Yek2ixdHyjD+gqtS/pAgwaQ9UhQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "f6a2ed2082d9a51668c86ba27d0b5496f7a2ea93", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, "poetry2nix": { "inputs": { "flake-utils": "flake-utils", @@ -898,7 +960,8 @@ "nix-devtools": "nix-devtools", "nix-gitlab-ci": "nix-gitlab-ci", "nixpkgs": "nixpkgs_6", - "systems": "systems_5" + "systems": "systems_5", + "terranix": "terranix" } }, "systems": { @@ -975,6 +1038,43 @@ "repo": "default", "type": "github" } + }, + "terranix": { + "inputs": { + "bats-assert": "bats-assert", + "bats-support": "bats-support", + "flake-utils": "flake-utils_6", + "nixpkgs": "nixpkgs_7", + "terranix-examples": "terranix-examples" + }, + "locked": { + "lastModified": 1695406838, + "narHash": "sha256-xiUfVD6rtsVWFotVtUW3Q1nQh4obKzgvpN1wqZuGXvM=", + "owner": "terranix", + "repo": "terranix", + "rev": "fc9077ca02ab5681935dbf0ecd725c4d889b9275", + "type": "github" + }, + "original": { + "owner": "terranix", + "repo": "terranix", + "type": "github" + } + }, + "terranix-examples": { + "locked": { + "lastModified": 1636300201, + "narHash": "sha256-0n1je1WpiR6XfCsvi8ZK7GrpEnMl+DpwhWaO1949Vbc=", + "owner": "terranix", + "repo": "terranix-examples", + "rev": "a934aa1cf88f6bd6c6ddb4c77b77ec6e1660bd5e", + "type": "github" + }, + "original": { + "owner": "terranix", + "repo": "terranix-examples", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index ee8bccb..5a7a9be 100644 --- a/flake.nix +++ b/flake.nix @@ -45,12 +45,42 @@ task = { enable = true; alias = ","; - tasks = {}; + tasks = { + "build" = { + requires.vars = ["TEMPLATE"]; + cmds = [ + "nix build .#{{ .TEMPLATE }}" + "install result {{ .TEMPLATE }}/template.tf.json" + ]; + }; + "validate" = { + desc = "Validate the resulting terraform files"; + deps = ["build"]; + requires.vars = ["TEMPLATE"]; + dir = "{{ .TEMPLATE }}"; + cmds = [ + "${pkgs.opentofu}/bin/tofu init" + "${pkgs.opentofu}/bin/tofu validate" + ]; + }; + "upload-to-coder" = { + desc = "Uploads the specified template to coder"; + deps = ["build" "validate"]; + requires.vars = ["TEMPLATE"]; + dir = "{{ .TEMPLATE }}"; + interactive = true; + cmd = ''${pkgs.coder}/bin/coder templates push "{{ .TEMPLATE }}"''; + }; + }; }; }; packages = { nix-coder-image = pkgs.callPackage ./image.nix {}; + nix-kubernetes = inputs.terranix.lib.terranixConfiguration { + inherit system; + modules = [./nix-kubernetes]; + }; }; }; }; @@ -73,5 +103,7 @@ inputs.devenv.follows = "devenv"; inputs.systems.follows = "systems"; }; + + terranix.url = "github:terranix/terranix"; }; } diff --git a/nix-kubernetes/coder.nix b/nix-kubernetes/coder.nix new file mode 100644 index 0000000..26fa0bd --- /dev/null +++ b/nix-kubernetes/coder.nix @@ -0,0 +1,42 @@ +{...}: { + locals."git_repo_folder" = let + split_repo = ''split("/", data.coder_parameter.git_repo.value)''; + in ''try(element(${split_repo}, length(${split_repo}) - 1), "")''; + + data = { + coder_external_auth."gitlab" = { + id = "gitlab"; + optional = false; + }; + coder_workspace."me" = {}; + }; + + resource = { + coder_agent."coder" = { + arch = "\${var.arch}"; + os = "linux"; + }; + coder_script."git_clone" = { + agent_id = "\${coder_agent.coder.id}"; + display_name = "Git Clone"; + icon = "/icon/git.svg"; + script = let + repo = ''''${data.coder_parameter.git_repo.value}''; + repo_folder = ''''${local.git_repo_folder}''; + in '' + #!/usr/bin/env bash + set -eux + + echo "Cloning repo \"${repo}\" if it does not exist" + mkdir -p ~/repos + pushd ~/repos + if [[ ! -z "${repo}" && ! -d "${repo_folder}" ]] then + git clone ${repo} ${repo_folder} + fi + popd + ''; + run_on_start = true; + start_blocks_login = true; + }; + }; +} diff --git a/nix-kubernetes/default.nix b/nix-kubernetes/default.nix new file mode 100644 index 0000000..ac0ae83 --- /dev/null +++ b/nix-kubernetes/default.nix @@ -0,0 +1,24 @@ +{...}: { + imports = [ + ./parameters.nix + ./variables.nix + ./coder.nix + ./kubernetes.nix + ]; + + terraform.required_providers = { + coder = { + source = "coder/coder"; + version = "0.21.0"; + }; + kubernetes = { + source = "hashicorp/kubernetes"; + version = "2.29.0"; + }; + }; + + provider = { + coder = {}; + kubernetes = {}; + }; +} diff --git a/nix-kubernetes/kubernetes.nix b/nix-kubernetes/kubernetes.nix new file mode 100644 index 0000000..128da0f --- /dev/null +++ b/nix-kubernetes/kubernetes.nix @@ -0,0 +1,130 @@ +{...}: { + resource = { + kubernetes_pod."workspace" = { + count = "\${data.coder_workspace.me.start_count}"; + metadata = { + name = "coder-\${lower(data.coder_workspace.me.owner)}-\${lower(data.coder_workspace.me.name)}"; + namespace = "\${var.namespace}"; + annotations."com.coder.user.email" = "\${data.coder_workspace.me.owner_email}"; + labels = { + "app.kubernetes.io/instance" = "coder-workspace-\${lower(data.coder_workspace.me.owner)}-\${lower(data.coder_workspace.me.name)}"; + "app.kubernetes.io/name" = "coder-workspace"; + "app.kubernetes.io/part-of" = "coder"; + "com.coder.resource" = "true"; + "com.coder.user.id" = "\${data.coder_workspace.me.owner_id}"; + "com.coder.user.name" = "\${data.coder_workspace.me.owner}"; + "com.coder.workspace.id" = "\${data.coder_workspace.me.id}"; + "com.coder.workspace.name" = "\${data.coder_workspace.me.name}"; + }; + }; + spec = { + affinity.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"]; + }; + }; + }; + container = [ + { + name = "workspace"; + image = "registry.gitlab.com/technofab/coder-templates/coder-workspace:\${data.coder_parameter.image_tag.value}"; + command = ["/bin/sh" "-c" "\${resource.coder_agent.coder.init_script}"]; + env = [ + { + name = "CODER_AGENT_TOKEN"; + value = "\${resource.coder_agent.coder.token}"; + } + ]; + resources = { + requests = { + # TODO: allow configuring this via variables (template wide) + cpu = "250m"; + memory = "512Mi"; + }; + limits = { + cpu = "\${data.coder_parameter.cpu.value}"; + memory = "\${data.coder_parameter.memory.value}"; + }; + }; + security_context.run_as_user = "1000"; + volume_mount = [ + { + mount_path = "/home"; + name = "home"; + read_only = false; + } + { + mount_path = "/nix"; + name = "nix-store"; + read_only = false; + } + ]; + } + ]; + security_context = { + fs_group = "1000"; + run_as_user = "1000"; + }; + volume = [ + { + name = "home"; + persistent_volume_claim.claim_name = "\${resource.kubernetes_persistent_volume_claim.home.metadata.0.name}"; + } + { + name = "nix-store"; + persistent_volume_claim.claim_name = "\${resource.kubernetes_persistent_volume_claim.nix-store.metadata.0.name}"; + } + ]; + }; + }; + kubernetes_persistent_volume_claim."home" = { + metadata = { + name = "coder-home-\${lower(data.coder_workspace.me.owner)}-\${lower(data.coder_workspace.me.name)}"; + namespace = "\${var.namespace}"; + annotations."com.coder.user.email" = "\${data.coder_workspace.me.owner_email}"; + labels = { + "app.kubernetes.io/instance" = "coder-pvc-home-\${lower(data.coder_workspace.me.owner)}-\${lower(data.coder_workspace.me.name)}"; + "app.kubernetes.io/name" = "coder-pvc"; + "app.kubernetes.io/part-of" = "coder"; + "com.coder.resource" = "true"; + "com.coder.user.id" = "\${data.coder_workspace.me.owner_id}"; + "com.coder.user.name" = "\${data.coder_workspace.me.owner}"; + "com.coder.workspace.id" = "\${data.coder_workspace.me.id}"; + "com.coder.workspace.name" = "\${data.coder_workspace.me.name}"; + }; + }; + spec = { + access_modes = ["ReadWriteOnce"]; + resources.requests.storage = "\${data.coder_parameter.home_disk_size.value}Gi"; + }; + wait_until_bound = false; + }; + kubernetes_persistent_volume_claim."nix-store" = { + metadata = { + name = "coder-nix-store-\${lower(data.coder_workspace.me.owner)}-\${lower(data.coder_workspace.me.name)}"; + namespace = "\${var.namespace}"; + annotations."com.coder.user.email" = "\${data.coder_workspace.me.owner_email}"; + labels = { + "app.kubernetes.io/instance" = "coder-pvc-nix-store-\${lower(data.coder_workspace.me.owner)}-\${lower(data.coder_workspace.me.name)}"; + "app.kubernetes.io/name" = "coder-pvc"; + "app.kubernetes.io/part-of" = "coder"; + "com.coder.resource" = "true"; + "com.coder.user.id" = "\${data.coder_workspace.me.owner_id}"; + "com.coder.user.name" = "\${data.coder_workspace.me.owner}"; + "com.coder.workspace.id" = "\${data.coder_workspace.me.id}"; + "com.coder.workspace.name" = "\${data.coder_workspace.me.name}"; + }; + }; + spec = { + access_modes = ["ReadWriteOnce"]; + resources.requests.storage = "\${data.coder_parameter.nix_store_disk_size.value}Gi"; + }; + wait_until_bound = false; + }; + }; +} diff --git a/nix-kubernetes/parameters.nix b/nix-kubernetes/parameters.nix new file mode 100644 index 0000000..cd305f9 --- /dev/null +++ b/nix-kubernetes/parameters.nix @@ -0,0 +1,78 @@ +{...}: { + data.coder_parameter = { + git_repo = { + name = "Git Repository"; + description = '' + URI for a git repository which should automatically be cloned to ~/repos/ + ''; + default = ""; + order = 1; + type = "string"; + mutable = true; + }; + image_tag = { + name = "Image Tag"; + description = '' + Which container image tag should be used. + ''; + default = "latest"; + order = 2; + type = "string"; + mutable = true; + }; + cpu = { + name = "CPU"; + description = '' + CPU Limit for Kubernetes Pod. Kubernetes Notation (eg. 500m) + ''; + default = "500m"; + order = 3; + type = "string"; + mutable = true; + }; + memory = { + name = "Memory"; + description = '' + Memory Limit for Kubernetes Pod. Kubernetes Notation (eg. 1Gi) + ''; + default = "1Gi"; + order = 4; + type = "string"; + mutable = true; + }; + home_disk_size = { + name = "Home Disk Size"; + description = '' + Size for the /home PV in GB + ''; + default = 5; + order = 5; + type = "number"; + mutable = true; + validation = [ + { + min = 1; + max = 100; + monotonic = "increasing"; + } + ]; + }; + nix_store_disk_size = { + name = "Nix Store Disk Size"; + description = '' + Size for the /nix PV in GB. This might grow pretty big. + ''; + default = 5; + order = 6; + type = "number"; + mutable = true; + validation = [ + { + min = 1; + max = 100; + monotonic = "increasing"; + } + ]; + }; + }; +} diff --git a/nix-kubernetes/variables.nix b/nix-kubernetes/variables.nix new file mode 100644 index 0000000..33de9dd --- /dev/null +++ b/nix-kubernetes/variables.nix @@ -0,0 +1,16 @@ +{...}: { + variable = { + namespace = { + type = "string"; + description = "Kubernetes namespace (must exist prior to creating workspaces)"; + }; + arch = { + type = "string"; + description = "Architecture of the host"; + validation = { + condition = ''''${contains(["amd64", "arm64"], var.arch)}''; + error_message = "Invalid architecture selected"; + }; + }; + }; +}