diff --git a/coder-compose.yaml b/coder-compose.yaml new file mode 100644 index 0000000..df7247f --- /dev/null +++ b/coder-compose.yaml @@ -0,0 +1,115 @@ +services: + coder: + user: root + group_add: + - 988 + image: 'ghcr.io/coder/coder:${CODER_VERSION:-latest}' + expose: + - '7080' + environment: + CODER_PG_CONNECTION_URL: 'postgresql://${POSTGRES_USER:-username}:${POSTGRES_PASSWORD:-password}@database/${POSTGRES_DB:-coder}?sslmode=disable' + CODER_HTTP_ADDRESS: '0.0.0.0:7080' + CODER_ACCESS_URL: '${CODER_ACCESS_URL}' + CODER_WILDCARD_ACCESS_URL: '${CODER_WILDCARD_ACCESS_URL}' + CODER_ADDRESS: '${CODER_ADDRESS}' + COOLIFY_RESOURCE_UUID: bwk8ckcok8o84cc0o4os4sso + COOLIFY_CONTAINER_NAME: coder-bwk8ckcok8o84cc0o4os4sso + COOLIFY_URL: 'http://dev.lab' + COOLIFY_FQDN: dev.lab + volumes: + - '/var/run/docker.sock:/var/run/docker.sock' + - 'bwk8ckcok8o84cc0o4os4sso_coder-home:/home/coder' + - /home/trav/code-tools:/home/coder/resources + depends_on: + database: + condition: service_healthy + healthcheck: + test: + - CMD-SHELL + - 'curl -f -s http://localhost:7080/healthz || exit 1' + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + labels: + - glance.name=Coder + - 'glance.icon=https://cdn.jsdelivr.net/gh/selfhst/icons/webp/coder.webp' + - 'glance.url=http://dev.lab' + - 'glance.description=Mad Science Lab' + - glance.id=coder + - glance.category=dev + - glance.hide=false + - coolify.managed=true + - coolify.version=4.0.0-beta.420.6 + - coolify.serviceId=41 + - coolify.type=service + - coolify.name=coder-bwk8ckcok8o84cc0o4os4sso + - coolify.resourceName=coder + - coolify.projectName=development + - coolify.serviceName=coder + - coolify.environmentName=production + - coolify.pullRequestId=0 + - coolify.service.subId=308 + - coolify.service.subType=application + - coolify.service.subName=coder + - traefik.enable=true + - traefik.http.middlewares.gzip.compress=true + - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https + - traefik.http.routers.http-0-bwk8ckcok8o84cc0o4os4sso-coder.entryPoints=http + - traefik.http.routers.http-0-bwk8ckcok8o84cc0o4os4sso-coder.middlewares=gzip + - 'traefik.http.routers.http-0-bwk8ckcok8o84cc0o4os4sso-coder.rule=Host(`dev.lab`) && PathPrefix(`/`)' + - traefik.http.routers.http-0-bwk8ckcok8o84cc0o4os4sso-coder.service=http-0-bwk8ckcok8o84cc0o4os4sso-coder + - traefik.http.services.http-0-bwk8ckcok8o84cc0o4os4sso-coder.loadbalancer.server.port=7080 + container_name: coder-bwk8ckcok8o84cc0o4os4sso + restart: unless-stopped + networks: + bwk8ckcok8o84cc0o4os4sso: null + database: + image: 'postgres:17' + environment: + POSTGRES_USER: '${POSTGRES_USER:-username}' + POSTGRES_PASSWORD: '${POSTGRES_PASSWORD:-password}' + POSTGRES_DB: '${POSTGRES_DB:-coder}' + COOLIFY_RESOURCE_UUID: bwk8ckcok8o84cc0o4os4sso + COOLIFY_CONTAINER_NAME: database-bwk8ckcok8o84cc0o4os4sso + volumes: + - 'bwk8ckcok8o84cc0o4os4sso_coder-data:/var/lib/postgresql/data' + healthcheck: + test: + - CMD-SHELL + - 'pg_isready -U ${POSTGRES_USER:-username} -d ${POSTGRES_DB:-coder}' + interval: 5s + timeout: 5s + retries: 5 + labels: + - glance.name=Postgres-Coder + - glance.parent=coder + - glance.hide=false + - coolify.managed=true + - coolify.version=4.0.0-beta.420.6 + - coolify.serviceId=41 + - coolify.type=service + - coolify.name=database-bwk8ckcok8o84cc0o4os4sso + - coolify.resourceName=coder + - coolify.projectName=development + - coolify.serviceName=database + - coolify.environmentName=production + - coolify.pullRequestId=0 + - coolify.service.subId=38 + - coolify.service.subType=database + - coolify.service.subName=database + container_name: database-bwk8ckcok8o84cc0o4os4sso + restart: unless-stopped + networks: + bwk8ckcok8o84cc0o4os4sso: null +volumes: + bwk8ckcok8o84cc0o4os4sso_coder-home: + name: bwk8ckcok8o84cc0o4os4sso_coder-home + bwk8ckcok8o84cc0o4os4sso_coder-data: + name: bwk8ckcok8o84cc0o4os4sso_coder-data +networks: + bwk8ckcok8o84cc0o4os4sso: + name: bwk8ckcok8o84cc0o4os4sso + external: true +configs: { } +secrets: { } diff --git a/main.py b/main.py new file mode 100644 index 0000000..d9ebb1c --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from code-tools!") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c1f9821 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "code-tools" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] diff --git a/tf/.terraform/modules/jetbrains_gateway/README.md b/tf/.terraform/modules/jetbrains_gateway/README.md new file mode 100644 index 0000000..73c1e12 --- /dev/null +++ b/tf/.terraform/modules/jetbrains_gateway/README.md @@ -0,0 +1,133 @@ +--- +display_name: JetBrains Gateway +description: Add a one-click button to launch JetBrains Gateway IDEs in the dashboard. +icon: ../.icons/gateway.svg +maintainer_github: coder +verified: true +tags: [ide, jetbrains, helper, parameter] +--- + +# JetBrains Gateway + +This module adds a JetBrains Gateway Button to open any workspace with a single click. + +JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM. +Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements. + +```tf +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.28" + agent_id = coder_agent.example.id + folder = "/home/coder/example" + jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"] + default = "GO" +} +``` + +![JetBrains Gateway IDes list](../.images/jetbrains-gateway.png) + +## Examples + +### Add GoLand and WebStorm as options with the default set to GoLand + +```tf +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.28" + agent_id = coder_agent.example.id + folder = "/home/coder/example" + jetbrains_ides = ["GO", "WS"] + default = "GO" +} +``` + +### Use the latest version of each IDE + +```tf +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.28" + agent_id = coder_agent.example.id + folder = "/home/coder/example" + jetbrains_ides = ["IU", "PY"] + default = "IU" + latest = true +} +``` + +### Use fixed versions set by `jetbrains_ide_versions` + +```tf +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.28" + agent_id = coder_agent.example.id + folder = "/home/coder/example" + jetbrains_ides = ["IU", "PY"] + default = "IU" + latest = false + jetbrains_ide_versions = { + "IU" = { + build_number = "243.21565.193" + version = "2024.3" + } + "PY" = { + build_number = "243.21565.199" + version = "2024.3" + } + } +} +``` + +### Use the latest EAP version + +```tf +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.28" + agent_id = coder_agent.example.id + folder = "/home/coder/example" + jetbrains_ides = ["GO", "WS"] + default = "GO" + latest = true + channel = "eap" +} +``` + +### Custom base link + +Due to the highest priority of the `ide_download_link` parameter in the `(jetbrains-gateway://...` within IDEA, the pre-configured download address will be overridden when using [IDEA's offline mode](https://www.jetbrains.com/help/idea/fully-offline-mode.html). Therefore, it is necessary to configure the `download_base_link` parameter for the `jetbrains_gateway` module to change the value of `ide_download_link`. + +```tf +module "jetbrains_gateway" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.28" + agent_id = coder_agent.example.id + folder = "/home/coder/example" + jetbrains_ides = ["GO", "WS"] + releases_base_link = "https://releases.internal.site/" + download_base_link = "https://download.internal.site/" + default = "GO" +} +``` + +## Supported IDEs + +This module and JetBrains Gateway support the following JetBrains IDEs: + +- [GoLand (`GO`)](https://www.jetbrains.com/go/) +- [WebStorm (`WS`)](https://www.jetbrains.com/webstorm/) +- [IntelliJ IDEA Ultimate (`IU`)](https://www.jetbrains.com/idea/) +- [PyCharm Professional (`PY`)](https://www.jetbrains.com/pycharm/) +- [PhpStorm (`PS`)](https://www.jetbrains.com/phpstorm/) +- [CLion (`CL`)](https://www.jetbrains.com/clion/) +- [RubyMine (`RM`)](https://www.jetbrains.com/ruby/) +- [Rider (`RD`)](https://www.jetbrains.com/rider/) +- [RustRover (`RR`)](https://www.jetbrains.com/rust/) diff --git a/tf/.terraform/modules/jetbrains_gateway/main.test.ts b/tf/.terraform/modules/jetbrains_gateway/main.test.ts new file mode 100644 index 0000000..ea04a77 --- /dev/null +++ b/tf/.terraform/modules/jetbrains_gateway/main.test.ts @@ -0,0 +1,43 @@ +import { it, expect, describe } from "bun:test"; +import { + runTerraformInit, + testRequiredVariables, + runTerraformApply, +} from "../test"; + +describe("jetbrains-gateway", async () => { + await runTerraformInit(import.meta.dir); + + await testRequiredVariables(import.meta.dir, { + agent_id: "foo", + folder: "/home/foo", + }); + + it("should create a link with the default values", async () => { + const state = await runTerraformApply(import.meta.dir, { + // These are all required. + agent_id: "foo", + folder: "/home/coder", + }); + expect(state.outputs.url.value).toBe( + "jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz", + ); + + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "gateway", + ); + + expect(coder_app).not.toBeNull(); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.order).toBeNull(); + }); + + it("default to first ide", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/home/foo", + jetbrains_ides: '["IU", "GO", "PY"]', + }); + expect(state.outputs.identifier.value).toBe("IU"); + }); +}); diff --git a/tf/.terraform/modules/jetbrains_gateway/main.tf b/tf/.terraform/modules/jetbrains_gateway/main.tf new file mode 100644 index 0000000..d197399 --- /dev/null +++ b/tf/.terraform/modules/jetbrains_gateway/main.tf @@ -0,0 +1,341 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + http = { + source = "hashicorp/http" + version = ">= 3.0" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "slug" { + type = string + description = "The slug for the coder_app. Allows resuing the module with the same template." + default = "gateway" +} + +variable "agent_name" { + type = string + description = "Agent name. (unused). Will be removed in a future version" + + default = "" +} + +variable "folder" { + type = string + description = "The directory to open in the IDE. e.g. /home/coder/project" + validation { + condition = can(regex("^(?:/[^/]+)+$", var.folder)) + error_message = "The folder must be a full path and must not start with a ~." + } +} + +variable "default" { + default = "" + type = string + description = "Default IDE" +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +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 +} + +variable "latest" { + type = bool + description = "Whether to fetch the latest version of the IDE." + default = false +} + +variable "channel" { + type = string + description = "JetBrains IDE release channel. Valid values are release and eap." + default = "release" + validation { + condition = can(regex("^(release|eap)$", var.channel)) + error_message = "The channel must be either release or eap." + } +} + +variable "jetbrains_ide_versions" { + type = map(object({ + build_number = string + version = string + })) + description = "The set of versions for each jetbrains IDE" + default = { + "IU" = { + build_number = "243.21565.193" + version = "2024.3" + } + "PS" = { + build_number = "243.21565.202" + version = "2024.3" + } + "WS" = { + build_number = "243.21565.180" + version = "2024.3" + } + "PY" = { + build_number = "243.21565.199" + version = "2024.3" + } + "CL" = { + build_number = "243.21565.238" + version = "2024.1" + } + "GO" = { + build_number = "243.21565.208" + version = "2024.3" + } + "RM" = { + build_number = "243.21565.197" + version = "2024.3" + } + "RD" = { + build_number = "243.21565.191" + version = "2024.3" + } + "RR" = { + build_number = "243.22562.230" + version = "2024.3" + } + } + validation { + condition = ( + alltrue([ + for code in keys(var.jetbrains_ide_versions) : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"], code) + ]) + ) + error_message = "The jetbrains_ide_versions must contain a map of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"])}." + } +} + +variable "jetbrains_ides" { + type = list(string) + description = "The list of IDE product codes." + default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"] + validation { + condition = ( + alltrue([ + for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"], code) + ]) + ) + error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"])}." + } + # check if the list is empty + validation { + condition = length(var.jetbrains_ides) > 0 + error_message = "The jetbrains_ides must not be empty." + } + # check if the list contains duplicates + validation { + condition = length(var.jetbrains_ides) == length(toset(var.jetbrains_ides)) + error_message = "The jetbrains_ides must not contain duplicates." + } +} + +variable "releases_base_link" { + type = string + description = "" + default = "https://data.services.jetbrains.com" + validation { + condition = can(regex("^https?://.+$", var.releases_base_link)) + error_message = "The releases_base_link must be a valid HTTP/S address." + } +} + +variable "download_base_link" { + type = string + description = "" + default = "https://download.jetbrains.com" + validation { + condition = can(regex("^https?://.+$", var.download_base_link)) + error_message = "The download_base_link must be a valid HTTP/S address." + } +} + +data "http" "jetbrains_ide_versions" { + for_each = var.latest ? toset(var.jetbrains_ides) : toset([]) + url = "${var.releases_base_link}/products/releases?code=${each.key}&latest=true&type=${var.channel}" +} + +locals { + jetbrains_ides = { + "GO" = { + icon = "/icon/goland.svg", + name = "GoLand", + identifier = "GO", + build_number = var.jetbrains_ide_versions["GO"].build_number, + download_link = "${var.download_base_link}/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz" + version = var.jetbrains_ide_versions["GO"].version + }, + "WS" = { + icon = "/icon/webstorm.svg", + name = "WebStorm", + identifier = "WS", + build_number = var.jetbrains_ide_versions["WS"].build_number, + download_link = "${var.download_base_link}/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz" + version = var.jetbrains_ide_versions["WS"].version + }, + "IU" = { + icon = "/icon/intellij.svg", + name = "IntelliJ IDEA Ultimate", + identifier = "IU", + build_number = var.jetbrains_ide_versions["IU"].build_number, + download_link = "${var.download_base_link}/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz" + version = var.jetbrains_ide_versions["IU"].version + }, + "PY" = { + icon = "/icon/pycharm.svg", + name = "PyCharm Professional", + identifier = "PY", + build_number = var.jetbrains_ide_versions["PY"].build_number, + download_link = "${var.download_base_link}/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz" + version = var.jetbrains_ide_versions["PY"].version + }, + "CL" = { + icon = "/icon/clion.svg", + name = "CLion", + identifier = "CL", + build_number = var.jetbrains_ide_versions["CL"].build_number, + download_link = "${var.download_base_link}/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz" + version = var.jetbrains_ide_versions["CL"].version + }, + "PS" = { + icon = "/icon/phpstorm.svg", + name = "PhpStorm", + identifier = "PS", + build_number = var.jetbrains_ide_versions["PS"].build_number, + download_link = "${var.download_base_link}/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz" + version = var.jetbrains_ide_versions["PS"].version + }, + "RM" = { + icon = "/icon/rubymine.svg", + name = "RubyMine", + identifier = "RM", + build_number = var.jetbrains_ide_versions["RM"].build_number, + download_link = "${var.download_base_link}/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz" + version = var.jetbrains_ide_versions["RM"].version + }, + "RD" = { + icon = "/icon/rider.svg", + name = "Rider", + identifier = "RD", + build_number = var.jetbrains_ide_versions["RD"].build_number, + download_link = "${var.download_base_link}/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz" + version = var.jetbrains_ide_versions["RD"].version + }, + "RR" = { + icon = "/icon/rustrover.svg", + name = "RustRover", + identifier = "RR", + build_number = var.jetbrains_ide_versions["RR"].build_number, + download_link = "${var.download_base_link}/rustrover/RustRover-${var.jetbrains_ide_versions["RR"].version}.tar.gz" + version = var.jetbrains_ide_versions["RR"].version + } + } + + icon = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].icon + json_data = var.latest ? jsondecode(data.http.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].response_body) : {} + key = var.latest ? keys(local.json_data)[0] : "" + display_name = local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].name + identifier = data.coder_parameter.jetbrains_ide.value + download_link = var.latest ? local.json_data[local.key][0].downloads.linux.link : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].download_link + build_number = var.latest ? local.json_data[local.key][0].build : local.jetbrains_ides[data.coder_parameter.jetbrains_ide.value].build_number + version = var.latest ? local.json_data[local.key][0].version : var.jetbrains_ide_versions[data.coder_parameter.jetbrains_ide.value].version +} + +data "coder_parameter" "jetbrains_ide" { + type = "string" + name = "jetbrains_ide" + display_name = "JetBrains IDE" + icon = "/icon/gateway.svg" + mutable = true + default = var.default == "" ? var.jetbrains_ides[0] : var.default + order = var.coder_parameter_order + + dynamic "option" { + for_each = var.jetbrains_ides + content { + icon = local.jetbrains_ides[option.value].icon + name = local.jetbrains_ides[option.value].name + value = option.value + } + } +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_app" "gateway" { + agent_id = var.agent_id + slug = var.slug + display_name = local.display_name + icon = local.icon + external = true + order = var.order + url = join("", [ + "jetbrains-gateway://connect#type=coder&workspace=", + data.coder_workspace.me.name, + "&owner=", + data.coder_workspace_owner.me.name, + "&folder=", + var.folder, + "&url=", + data.coder_workspace.me.access_url, + "&token=", + "$SESSION_TOKEN", + "&ide_product_code=", + data.coder_parameter.jetbrains_ide.value, + "&ide_build_number=", + local.build_number, + "&ide_download_link=", + local.download_link, + ]) +} + +output "identifier" { + value = local.identifier +} + +output "display_name" { + value = local.display_name +} + +output "icon" { + value = local.icon +} + +output "download_link" { + value = local.download_link +} + +output "build_number" { + value = local.build_number +} + +output "version" { + value = local.version +} + +output "url" { + value = coder_app.gateway.url +} diff --git a/tf/.terraform/modules/modules.json b/tf/.terraform/modules/modules.json new file mode 100644 index 0000000..9b8d9e3 --- /dev/null +++ b/tf/.terraform/modules/modules.json @@ -0,0 +1 @@ +{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"jetbrains_gateway","Source":"registry.coder.com/modules/jetbrains-gateway/coder","Version":"1.0.29","Dir":".terraform/modules/jetbrains_gateway"}]} \ No newline at end of file diff --git a/tf/.terraform/providers/registry.terraform.io/hashicorp/http/3.5.0/linux_amd64/LICENSE.txt b/tf/.terraform/providers/registry.terraform.io/hashicorp/http/3.5.0/linux_amd64/LICENSE.txt new file mode 100644 index 0000000..b9ac071 --- /dev/null +++ b/tf/.terraform/providers/registry.terraform.io/hashicorp/http/3.5.0/linux_amd64/LICENSE.txt @@ -0,0 +1,375 @@ +Copyright (c) 2017 HashiCorp, Inc. + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/tf/.terraform/providers/registry.terraform.io/hashicorp/http/3.5.0/linux_amd64/terraform-provider-http_v3.5.0_x5 b/tf/.terraform/providers/registry.terraform.io/hashicorp/http/3.5.0/linux_amd64/terraform-provider-http_v3.5.0_x5 new file mode 100755 index 0000000..d677cc9 Binary files /dev/null and b/tf/.terraform/providers/registry.terraform.io/hashicorp/http/3.5.0/linux_amd64/terraform-provider-http_v3.5.0_x5 differ diff --git a/tf/README.md b/tf/README.md new file mode 100644 index 0000000..cee8876 --- /dev/null +++ b/tf/README.md @@ -0,0 +1,446 @@ +# Terraform Coder Development Environment + +A comprehensive development environment deployment using Terraform and Coder, providing isolated workspaces with integrated development tools, databases, and AI-powered coding assistants. + +## 🏗️ Architecture Overview + +This configuration deploys self-contained development workspaces using Docker containers orchestrated by Coder. Each workspace includes: + +- **Isolated Development Container** with VS Code, terminal access, and full development toolchain +- **Database Services** (PostgreSQL, Redis, Qdrant) with persistent storage +- **Management Interfaces** (pgAdmin, Qdrant Dashboard) +- **AI Development Tools** (Claude Code, Cursor, Windsurf support) +- **Reverse Proxy Integration** for seamless web access + +### Network Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Reverse Proxy │ +│ http://dev.lab │ +│ *.dev.lab wildcard │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ┌─────────────────────┼─────────────────────┐ + │ Workspace A │ Workspace B │ + │ network-{id-a} │ network-{id-b} │ + │ │ │ + │ ┌─────────────────┐ │ ┌─────────────────┐ │ + │ │ Dev Container │ │ │ Dev Container │ │ + │ │ VS Code:8080 │ │ │ VS Code:8080 │ │ + │ └─────────────────┘ │ └─────────────────┘ │ + │ │ │ + │ ┌─────────────────┐ │ ┌─────────────────┐ │ + │ │ PostgreSQL:5432 │ │ │ PostgreSQL:5432 │ │ + │ │ Redis:6379 │ │ │ Redis:6379 │ │ + │ │ Qdrant:6333 │ │ │ Qdrant:6333 │ │ + │ │ pgAdmin:5050 │ │ │ pgAdmin:5050 │ │ + │ └─────────────────┘ │ └─────────────────┘ │ + └─────────────────────┼─────────────────────┘ +``` + +**Key Benefits:** +- ✅ **Complete Network Isolation** between workspaces +- ✅ **No Port Conflicts** - same ports used in different networks +- ✅ **Scalable** - unlimited concurrent workspaces +- ✅ **Secure** - services only accessible through authenticated Coder session + +## 📁 File Structure + +``` +tf/ +├── README.md # This file +├── main.tf # Provider config, parameters, networks, volumes +├── variables.tf # All configurable parameters +├── terraform.tfvars # Variable assignments +├── workspace.tf # Main development container and Coder agent +├── services.tf # Database containers (PostgreSQL, Redis, Qdrant) +├── apps.tf # Coder applications for service access +├── scripts.tf # AI tools installation and configuration +└── outputs.tf # Workspace information exports +``` + +## 🛠️ Included Services & Tools + +### Development Environment +- **Container Base**: Microsoft DevContainers Universal image +- **Languages**: Node.js 20, Python 3.12, Rust (latest stable) +- **Package Managers**: npm, uv (Python), Cargo (Rust) +- **System Tools**: make, tree, jq, curl, wget, build-essential + +### Database Services +- **PostgreSQL 17** with Alpine Linux base + - Connection pooling and performance optimization + - pg_stat_statements enabled for query analysis + - PostgreSQL client tools included in workspace +- **Redis 7** with Alpine Linux base + - Authentication enabled with configurable password + - AOF persistence with everysec fsync + - LRU eviction policy with configurable memory limits +- **Qdrant Vector Database** (latest) + - HTTP API on port 6333, gRPC on port 6334 + - Persistent storage for vector collections + - Web dashboard for collection management + +### Management Interfaces +- **pgAdmin 4** - PostgreSQL administration interface +- **Qdrant Dashboard** - Vector database management +- **VS Code Server** - Browser-based IDE +- **Terminal Access** - Full bash shell access + +### AI Development Tools +- **Claude Code CLI** - Anthropic's official CLI +- **Cursor Support** - AI-powered code editor integration +- **Windsurf Support** - Codeium's development environment + +### Development Packages + +#### Node.js (Global) +```javascript +repomix, create-next-app, nodemon, concurrently +@types/node, typescript, eslint, prettier +``` + +#### Python (via uv) +```python +fastapi, uvicorn, requests, pandas, numpy +psycopg2-binary, redis, qdrant-client, python-dotenv +``` + +## 🚀 Quick Start + +### Prerequisites + +1. **Coder Instance** running and accessible +2. **Reverse Proxy** configured with wildcard subdomain support +3. **Docker** daemon accessible to Coder +4. **Terraform** >= 1.0 installed + +### Environment Setup + +Set these environment variables in your Coder deployment: +```bash +CODER_ACCESS_URL=http://dev.lab +CODER_WILDCARD_ACCESS_URL=*.dev.lab +``` + +### Deployment Steps + +1. **Clone and Navigate** + ```bash + git clone + cd tf/ + ``` + +2. **Review Configuration** + ```bash + # Edit terraform.tfvars to match your needs + vim terraform.tfvars + ``` + +3. **Deploy Infrastructure** + ```bash + terraform init + terraform plan + terraform apply + ``` + +4. **Access Workspace** + - Navigate to your Coder instance + - Create new workspace using this template + - Select your preferred Git repository + - Choose whether to enable database services + +## ⚙️ Configuration + +### Key Variables (terraform.tfvars) + +#### Resource Limits +```hcl +workspace_memory_limit = 16384 # 16GB RAM +workspace_cpu_limit = 4 # 4 CPU cores +``` + +#### Service Configuration +```hcl +# Database passwords (change in production!) +postgres_password = "devpassword" +redis_password = "devpassword" + +# Database tuning +postgres_max_connections = 100 +redis_max_memory = "512mb" +``` + +#### Feature Toggles +```hcl +enable_pgadmin = true # PostgreSQL admin interface +enable_monitoring = true # Resource monitoring +enable_jupyter = false # Jupyter Lab for data science +``` + +#### Tool Versions +```hcl +node_version = "20" # Node.js LTS +python_version = "3.12" # Python latest stable +postgres_version = "17" # PostgreSQL latest +redis_version = "7" # Redis latest stable +``` + +### Coder Parameters + +When creating a workspace, you'll be prompted for: + +1. **Git Repository** - Select from your available repositories +2. **Enable Services** - Toggle database services on/off +3. **Enable AI Tools** - Toggle AI development tool installation + +## 🌐 Service Access + +### Web Applications + +All services are accessible through Coder's reverse proxy with subdomain routing: + +| Service | URL Pattern | Description | +|---------|-------------|-------------| +| VS Code | `code-server-{workspace}.dev.lab` | Browser-based IDE | +| Terminal | Available in Coder dashboard | Full bash shell | +| pgAdmin | `pgadmin-{workspace}.dev.lab` | PostgreSQL management | +| Qdrant | `qdrant-dashboard-{workspace}.dev.lab` | Vector DB dashboard | +| Dev Server | `nextjs-3000-{workspace}.dev.lab` | Next.js dev server | +| API Server | `api-8000-{workspace}.dev.lab` | FastAPI/Flask server | +| Vite Dev | `vite-5173-{workspace}.dev.lab` | Vite development | + +### Database Connections + +From within your workspace container: + +```bash +# PostgreSQL +export POSTGRES_URL="postgresql://postgres:devpassword@postgres-{workspace-id}:5432/postgres" +psql $POSTGRES_URL + +# Redis +export REDIS_URL="redis://:devpassword@redis-{workspace-id}:6379" +redis-cli -u $REDIS_URL + +# Qdrant +export QDRANT_URL="http://qdrant-{workspace-id}:6333" +curl $QDRANT_URL/health +``` + +### Environment Variables + +These are automatically set in your workspace: + +```bash +NODE_VERSION=20 +PYTHON_VERSION=3.12 +POSTGRES_URL=postgresql://postgres:***@postgres-{id}:5432/postgres +REDIS_URL=redis://:***@redis-{id}:6379 +QDRANT_URL=http://qdrant-{id}:6333 +``` + +## 🔧 Development Workflow + +### Initial Setup + +1. **Access Workspace** + ```bash + # Run environment info script + devinfo + ``` + +2. **Verify Services** + ```bash + # Check PostgreSQL + pg_isready -h postgres-{workspace-id} -U postgres + + # Check Redis + redis-cli -h redis-{workspace-id} ping + + # Check Qdrant + curl http://qdrant-{workspace-id}:6333/health + ``` + +### Common Tasks + +#### Next.js Project +```bash +# Create new Next.js app +npx create-next-app@latest my-app +cd my-app +npm run dev # Accessible at nextjs-3000-{workspace}.dev.lab +``` + +#### Python FastAPI Project +```bash +# Activate Python environment +source /home/coder/.venv/bin/activate + +# Create FastAPI app +uv add fastapi uvicorn +# Your app runs on port 8000, accessible via reverse proxy +``` + +#### Database Development +```bash +# Connect to PostgreSQL +psql $POSTGRES_URL + +# Create tables, run migrations, etc. +# Access pgAdmin for GUI management +``` + +## 🔍 Monitoring & Debugging + +### Built-in Monitoring + +Coder automatically tracks: +- **CPU Usage** - Updated every 60 seconds +- **RAM Usage** - Updated every 60 seconds +- **Disk Usage** - Updated every 5 minutes +- **Git Branch** - Updated every 5 minutes + +### Health Checks + +All services include comprehensive health checks: +- **PostgreSQL**: `pg_isready` command +- **Redis**: Connection test with `redis-cli` +- **Qdrant**: HTTP health endpoint +- **Web Services**: HTTP response verification + +### Logs and Debugging + +```bash +# Container logs +docker logs postgres-{workspace-id} +docker logs redis-{workspace-id} +docker logs qdrant-{workspace-id} + +# Service status +docker ps --filter label=coder.workspace_id={workspace-id} + +# Network inspection +docker network inspect coder-{workspace-id} +``` + +## 🔐 Security Considerations + +### Network Security +- **Isolated Networks** - Each workspace has its own Docker network +- **No Host Exposure** - Services only accessible through authenticated Coder session +- **Internal Communication** - Services communicate using internal DNS names + +### Authentication +- **Database Passwords** - Configurable via terraform.tfvars +- **Coder Authentication** - All access requires Coder login +- **sudo Access** - Granted to `coder` user for development flexibility + +### Data Persistence +- **Database Data** - Persistent Docker volumes per workspace +- **Workspace Files** - Persistent across container restarts +- **User Configuration** - Home directory persistence + +## 🚨 Troubleshooting + +### Common Issues + +#### "Unable to find user coder" +**Solution**: Container automatically creates `coder` user during startup. If issues persist, check container logs. + +#### Port Already in Use +**Solution**: This configuration uses no host port mappings. All routing is handled internally. + +#### Services Not Accessible +**Solutions**: +1. Verify reverse proxy wildcard configuration +2. Check Coder environment variables: + ```bash + echo $CODER_ACCESS_URL + echo $CODER_WILDCARD_ACCESS_URL + ``` +3. Confirm service health via Coder dashboard + +#### Database Connection Issues +**Solutions**: +1. Verify service is enabled in workspace parameters +2. Check container status: `docker ps` +3. Test internal connectivity: `curl http://postgres-{id}:5432` + +### Debug Commands + +```bash +# Environment information +devinfo + +# Network connectivity +docker network ls | grep coder +docker network inspect coder-{workspace-id} + +# Service health +curl http://qdrant-{workspace-id}:6333/health +pg_isready -h postgres-{workspace-id} -U postgres +redis-cli -h redis-{workspace-id} ping + +# Container status +docker ps --filter label=coder.workspace_id={workspace-id} +``` + +## 🔄 Updates & Maintenance + +### Updating Tool Versions + +1. **Modify terraform.tfvars** + ```hcl + node_version = "22" # Update Node.js version + python_version = "3.13" # Update Python version + ``` + +2. **Apply Changes** + ```bash + terraform plan + terraform apply + ``` + +3. **Restart Workspaces** - Changes apply to new workspace instances + +### Adding New Services + +1. **Add to services.tf** - Define new container resource +2. **Add to apps.tf** - Create Coder app for access +3. **Update variables.tf** - Add configuration options +4. **Update startup script** in workspace.tf if needed + +## 🤝 Contributing + +### Adding New Features + +1. Follow the existing file structure and naming conventions +2. Add proper health checks for new services +3. Update this README with new service documentation +4. Test with multiple concurrent workspaces + +### Best Practices + +- **Resource Labels** - All resources should include `coder.workspace_id` label +- **Network Isolation** - New services should join workspace network +- **No Host Ports** - Use internal networking only +- **Health Checks** - All web services need health check endpoints +- **Persistent Data** - Use Docker volumes for data that should survive restarts + +## 📚 References + +- [Coder Documentation](https://coder.com/docs) +- [Terraform Docker Provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs) +- [PostgreSQL Docker Hub](https://hub.docker.com/_/postgres) +- [Redis Docker Hub](https://hub.docker.com/_/redis) +- [Qdrant Documentation](https://qdrant.tech/documentation/) + +## 📝 License + +This configuration is provided as-is for development purposes. Modify passwords and security settings for production use. + +--- + +**🚀 Happy Coding!** Your isolated development environment is ready for productive development with full database support and AI-powered tools. \ No newline at end of file diff --git a/tf/apps.tf b/tf/apps.tf new file mode 100644 index 0000000..06683ed --- /dev/null +++ b/tf/apps.tf @@ -0,0 +1,273 @@ +# ============================================================================= +# Coder Applications - Service Access Points +# Web interfaces and tools for development services +# ============================================================================= + +# ============================================================================= +# IDE and Code Editor Access +# ============================================================================= + +# VS Code Server +resource "coder_app" "code_server" { + agent_id = coder_agent.main.id + slug = "code-server" + display_name = "VS Code" + url = "http://localhost:8080" + icon = "/icon/code.svg" + subdomain = true + share = "owner" + order = 1 + + healthcheck { + url = "http://localhost:8080/healthz" + interval = 10 + threshold = 5 + } +} + +# Terminal Access +resource "coder_app" "terminal" { + agent_id = coder_agent.main.id + slug = "terminal" + display_name = "Terminal" + icon = "/icon/terminal.svg" + command = "bash" + order = 2 +} + +# ============================================================================= +# Service Port Forwarding for Docker Container Access +# Note: Using direct container URLs since containers are on same Docker network +# ============================================================================= + +# ============================================================================= +# Database Management Interfaces +# ============================================================================= + +# pgAdmin - PostgreSQL Administration +resource "coder_app" "pgadmin" { + count = data.coder_parameter.enable_services.value && data.coder_parameter.enable_pgadmin.value ? 1 : 0 + agent_id = coder_agent.main.id + slug = "pgadmin" + display_name = "pgAdmin" + url = "http://pgadmin-${local.workspace_id}:80" + icon = "/icon/postgresql.svg" + subdomain = true + share = "owner" + order = 10 + + healthcheck { + url = "http://pgadmin-${local.workspace_id}:80" + interval = 15 + threshold = 5 + } +} + +# Qdrant Dashboard - Vector Database Management +resource "coder_app" "qdrant" { + count = data.coder_parameter.enable_services.value ? 1 : 0 + agent_id = coder_agent.main.id + slug = "qdrant-dashboard" + display_name = "Qdrant Dashboard" + url = "http://qdrant-${local.workspace_id}:6333/dashboard" + icon = "/icon/database.svg" + subdomain = true + share = "owner" + order = 11 + + healthcheck { + url = "http://qdrant-${local.workspace_id}:6333/api/cluster" + interval = 30 + threshold = 10 + } +} + + +# ============================================================================= +# Development Server Ports +# ============================================================================= + +# Next.js Development Server (default port 3000) +resource "coder_app" "nextjs_dev" { + agent_id = coder_agent.main.id + slug = "nextjs-3000" + display_name = "Next.js Dev Server" + url = "http://localhost:3000" + icon = "/icon/react.svg" + subdomain = true + share = "owner" + + healthcheck { + url = "http://localhost:3000" + interval = 10 + threshold = 10 + } +} + +# Generic Development Server (port 3000) +resource "coder_app" "dev_server_3000" { + agent_id = coder_agent.main.id + slug = "dev-3000" + display_name = "Dev Server (3000)" + url = "http://localhost:3000" + icon = "/icon/web.svg" + subdomain = true + share = "owner" + order = 21 + + healthcheck { + url = "http://localhost:3000" + interval = 10 + threshold = 10 + } +} + +# API Server - FastAPI/Flask (port 8000) +resource "coder_app" "api_server_8000" { + agent_id = coder_agent.main.id + slug = "api-8000" + display_name = "API Server (8000)" + url = "http://localhost:8000" + icon = "/icon/api.svg" + subdomain = true + share = "owner" + order = 20 + + healthcheck { + url = "http://localhost:8000/health" + interval = 10 + threshold = 10 + } +} + +# Vite Development Server (port 5173) +resource "coder_app" "vite_dev" { + agent_id = coder_agent.main.id + slug = "vite-5173" + display_name = "Vite Dev Server" + url = "http://localhost:5173" + icon = "/icon/web.svg" + subdomain = true + share = "owner" + + healthcheck { + url = "http://localhost:5173" + interval = 10 + threshold = 10 + } +} + +# Rust Development Server (port 8080) +resource "coder_app" "rust_server" { + agent_id = coder_agent.main.id + slug = "rust-8080" + display_name = "Rust Server (8080)" + url = "http://localhost:8080" + icon = "/icon/code.svg" + subdomain = true + share = "owner" + + healthcheck { + url = "http://localhost:8080/health" + interval = 10 + threshold = 10 + } +} + +# ============================================================================= +# Data Science and Analytics Tools +# ============================================================================= + +# Jupyter Lab (if enabled) +resource "coder_app" "jupyter" { + count = data.coder_parameter.enable_jupyter.value ? 1 : 0 + agent_id = coder_agent.main.id + slug = "jupyter" + display_name = "Jupyter Lab" + url = "http://localhost:8888" + icon = "/icon/python.svg" + subdomain = true + share = "owner" + + healthcheck { + url = "http://localhost:8888" + interval = 15 + threshold = 10 + } +} + +# ============================================================================= +# Utility and Management Applications +# ============================================================================= + +# Environment Information +resource "coder_app" "env_info" { + agent_id = coder_agent.main.id + slug = "env-info" + display_name = "Environment Info" + icon = "/icon/info.svg" + command = "devinfo" +} + +# Database Connection Tester +resource "coder_app" "db_tester" { + count = data.coder_parameter.enable_services.value ? 1 : 0 + agent_id = coder_agent.main.id + slug = "db-tester" + display_name = "Database Tester" + icon = "/icon/terminal.svg" + command = "bash -c 'echo \"=== Database Connection Test ===\"; echo \"PostgreSQL: postgres-${local.workspace_id}:5432\"; echo \"Redis: redis-${local.workspace_id}:6379\"; echo \"Qdrant: qdrant-${local.workspace_id}:6333\"; echo; echo \"Test PostgreSQL:\"; pg_isready -h postgres-${local.workspace_id} -p 5432 -U postgres || echo \"PostgreSQL not ready\"; echo; echo \"Test Redis:\"; redis-cli -h redis-${local.workspace_id} -p 6379 -a \"${var.redis_password}\" ping || echo \"Redis not ready\"; echo; echo \"Test Qdrant:\"; curl -f http://qdrant-${local.workspace_id}:6333 || echo \"Qdrant not ready\"; echo; read -p \"Press Enter to exit...\"'" +} + +# Development Logs Viewer +resource "coder_app" "dev_logs" { + agent_id = coder_agent.main.id + slug = "dev-logs" + display_name = "Development Logs" + icon = "/icon/terminal.svg" + command = "bash" +} + +# Git Repository Manager +resource "coder_app" "git_manager" { + agent_id = coder_agent.main.id + slug = "git" + display_name = "Git Repository" + icon = "/icon/git.svg" + command = "bash" +} + +# ============================================================================= +# AI Development Tools Access +# ============================================================================= + +# Claude Code CLI Access +resource "coder_app" "claude_code" { + count = data.coder_parameter.enable_ai_tools.value && data.coder_parameter.enable_claude_code.value ? 1 : 0 + agent_id = coder_agent.main.id + slug = "claude-code" + display_name = "Claude Code" + icon = "/icon/ai.svg" + command = "claude" +} + +# JetBrains Gateway +resource "coder_app" "jetbrains_gateway" { + count = data.coder_parameter.enable_jetbrains.value ? 1 : 0 + agent_id = coder_agent.main.id + slug = "jetbrains-gateway" + display_name = "JetBrains Gateway" + icon = "/icon/intellij.svg" + command = "bash -c 'echo \"🚀 JetBrains Gateway Integration\"; echo \"========================\"; echo \"\"; echo \"📍 Project Folder: /workspaces\"; echo \"🔧 Available IDEs: IntelliJ IDEA Ultimate, WebStorm, PyCharm Professional, GoLand\"; echo \"🌐 Default IDE: IntelliJ IDEA Ultimate\"; echo \"\"; echo \"💡 To connect:\"; echo \" 1. Install JetBrains Gateway on your local machine\"; echo \" 2. Connect to this workspace using Coder Gateway plugin\"; echo \" 3. Select your preferred IDE from the available options\"; echo \"\"; echo \"📚 Documentation: https://coder.com/docs/ides/gateway\"; echo \"\"; read -p \"Press Enter to continue...\";'" + order = 3 +} + +# File Manager +resource "coder_app" "file_manager" { + agent_id = coder_agent.main.id + slug = "files" + display_name = "File Manager" + icon = "/icon/folder.svg" + command = "bash -c 'export TERM=xterm-256color && cd /workspaces && ranger'" + order = 5 +} \ No newline at end of file diff --git a/tf/main.tf b/tf/main.tf new file mode 100644 index 0000000..5c9313a --- /dev/null +++ b/tf/main.tf @@ -0,0 +1,236 @@ +terraform { + required_version = ">= 1.0" + required_providers { + coder = { + source = "coder/coder" + version = "~> 2.0" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 2.25" + } + envbuilder = { + source = "coder/envbuilder" + version = "~> 1.0" + } + } +} + +provider "coder" {} +provider "docker" { + host = var.docker_socket != "" ? var.docker_socket : null +} +provider "envbuilder" {} + +# Data Sources +data "coder_provisioner" "me" {} +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +# Parameters +data "coder_parameter" "repo" { + name = "repo" + display_name = "Repository" + description = "Select a repository to clone" + mutable = true + order = 1 + + option { + name = "Custom Development Environment" + description = "Full-stack development with all services" + value = "custom" + } + option { + name = "vercel/next.js" + description = "The React Framework" + value = "https://github.com/vercel/next.js" + } + option { + name = "Custom URL" + description = "Specify a custom repo URL below" + value = "custom-url" + } +} + +data "coder_parameter" "custom_repo_url" { + name = "custom_repo_url" + display_name = "Custom Repository URL" + description = "Enter a custom repository URL" + default = "" + mutable = true + order = 2 +} + +data "coder_parameter" "enable_services" { + name = "enable_services" + display_name = "Enable Database Services" + description = "Enable PostgreSQL, Redis, Qdrant, and Docker Registry" + type = "bool" + default = "true" + mutable = true + order = 3 +} + +data "coder_parameter" "enable_ai_tools" { + name = "enable_ai_tools" + display_name = "Enable AI Assistant Tools" + description = "Install Claude Code and AI development tools" + type = "bool" + default = "true" + mutable = true + order = 4 +} + +data "coder_parameter" "enable_claude_code" { + name = "enable_claude_code" + display_name = "Enable Claude Code CLI" + description = "Install Claude Code command-line interface" + type = "bool" + default = "true" + mutable = true + order = 5 +} + +data "coder_parameter" "enable_cursor_support" { + name = "enable_cursor_support" + display_name = "Enable Cursor IDE Support" + description = "Install Cursor IDE configuration and settings" + type = "bool" + default = "true" + mutable = true + order = 6 +} + +data "coder_parameter" "enable_windsurf_support" { + name = "enable_windsurf_support" + display_name = "Enable Windsurf IDE Support" + description = "Install Windsurf IDE configuration and settings" + type = "bool" + default = "true" + mutable = true + order = 7 +} + +data "coder_parameter" "enable_jetbrains" { + name = "enable_jetbrains" + display_name = "Enable JetBrains Gateway" + description = "Enable JetBrains Gateway integration for remote development" + type = "bool" + default = "true" + mutable = true + order = 8 +} + +data "coder_parameter" "enable_jupyter" { + name = "enable_jupyter" + display_name = "Enable Jupyter Lab" + description = "Enable Jupyter Lab for data science and notebook development" + type = "bool" + default = "false" + mutable = true + order = 9 +} + +data "coder_parameter" "enable_pgadmin" { + name = "enable_pgadmin" + display_name = "Enable pgAdmin" + description = "Enable pgAdmin web interface for PostgreSQL management" + type = "bool" + default = "true" + mutable = true + order = 10 +} + +# Local Variables +locals { + # Container and workspace naming + container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" + workspace_id = data.coder_workspace.me.id + + # Git configuration + 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 + + # Repository URL logic + repo_url = ( + data.coder_parameter.repo.value == "custom" ? "https://github.com/coder/envbuilder" : + data.coder_parameter.repo.value == "custom-url" ? data.coder_parameter.custom_repo_url.value : + data.coder_parameter.repo.value + ) + + # Development container image with all required tools + devcontainer_image = var.devcontainer_image + + # Environment variables for the development container + dev_environment = { + # Git configuration + "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 + + # Development tools + "NODE_VERSION" = var.node_version + "PYTHON_VERSION" = var.python_version + "RUST_VERSION" = "stable" + + # Service URLs (when services are enabled) + "POSTGRES_URL" = data.coder_parameter.enable_services.value ? "postgresql://postgres:${var.postgres_password}@postgres-${local.workspace_id}:5432/postgres" : "" + "REDIS_URL" = data.coder_parameter.enable_services.value ? "redis://:${var.redis_password}@redis-${local.workspace_id}:6379" : "" + "QDRANT_URL" = data.coder_parameter.enable_services.value ? "http://qdrant-${local.workspace_id}:6333" : "" + + # Development configuration + "EDITOR" = "code" + "PYTHONPATH" = "/workspaces" + "CARGO_HOME" = "/home/coder/.cargo" + "RUSTUP_HOME" = "/home/coder/.rustup" + } + + # Legacy service URLs for backward compatibility + postgres_url = "postgresql://postgres:${var.postgres_password}@postgres-${local.workspace_id}:5432/postgres" + redis_url = "redis://:${var.redis_password}@redis-${local.workspace_id}:6379" + qdrant_url = "http://qdrant-${local.workspace_id}:6333" +} + +# Docker Network +resource "docker_network" "workspace" { + name = "coder-${local.workspace_id}" + driver = "bridge" + + labels { + label = "coder.workspace_id" + value = local.workspace_id + } + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.project" + value = var.project_name + } +} + +# Workspace Volume +resource "docker_volume" "workspaces" { + name = "workspaces-${local.workspace_id}" + + labels { + label = "coder.workspace_id" + value = local.workspace_id + } + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.type" + value = "workspace-data" + } +} + +# Development Container Image +resource "docker_image" "devcontainer" { + name = local.devcontainer_image + keep_locally = true +} \ No newline at end of file diff --git a/tf/outputs.tf b/tf/outputs.tf new file mode 100644 index 0000000..ab5a202 --- /dev/null +++ b/tf/outputs.tf @@ -0,0 +1,118 @@ +# ============================================================================= +# Terraform Outputs +# Expose important values for reference and external use +# ============================================================================= + +# ============================================================================= +# Workspace Information +# ============================================================================= + +output "workspace_id" { + description = "Unique identifier for the Coder workspace" + value = local.workspace_id +} + +output "workspace_name" { + description = "Name of the Coder workspace" + value = data.coder_workspace.me.name +} + +output "workspace_owner" { + description = "Owner of the Coder workspace" + value = data.coder_workspace_owner.me.name +} + +output "container_name" { + description = "Name of the main development container" + value = local.container_name +} + +# ============================================================================= +# Git Configuration +# ============================================================================= + +output "git_author_name" { + description = "Git author name configured for the workspace" + value = local.git_author_name +} + +output "git_author_email" { + description = "Git author email configured for the workspace" + value = local.git_author_email + sensitive = true +} + +output "repository_url" { + description = "Repository URL that will be cloned" + value = local.repo_url +} + +# ============================================================================= +# Service Connection Information +# ============================================================================= + +output "postgres_connection_url" { + description = "PostgreSQL connection URL for applications" + value = data.coder_parameter.enable_services.value ? local.postgres_url : null + sensitive = true +} + +output "redis_connection_url" { + description = "Redis connection URL for applications" + value = data.coder_parameter.enable_services.value ? local.redis_url : null + sensitive = true +} + +output "qdrant_api_url" { + description = "Qdrant vector database API URL" + value = data.coder_parameter.enable_services.value ? local.qdrant_url : null +} + + +# ============================================================================= +# Network Information +# ============================================================================= + +output "docker_network_name" { + description = "Name of the Docker network for service communication" + value = docker_network.workspace.name +} + +output "workspace_volume_name" { + description = "Name of the persistent workspace volume" + value = docker_volume.workspaces.name +} + +# ============================================================================= +# Development Environment Information +# ============================================================================= + +output "development_tools" { + description = "Information about installed development tools" + value = { + node_version = var.node_version + python_version = var.python_version + container_image = local.devcontainer_image + } +} + +output "service_status" { + description = "Status of optional development services" + value = { + services_enabled = data.coder_parameter.enable_services.value + ai_tools_enabled = data.coder_parameter.enable_ai_tools.value + pgadmin_enabled = var.enable_pgadmin + jupyter_enabled = var.enable_jupyter + } +} + +# ============================================================================= +# Access Information +# ============================================================================= + +output "external_ports" { + description = "External ports exposed for services" + value = data.coder_parameter.enable_services.value ? { + pgadmin = var.enable_pgadmin ? var.pgadmin_port : null + } : {} +} \ No newline at end of file diff --git a/tf/scripts.tf b/tf/scripts.tf new file mode 100644 index 0000000..9208531 --- /dev/null +++ b/tf/scripts.tf @@ -0,0 +1,72 @@ +# ============================================================================= +# Provisioning Scripts - AI Development Tools and Extensions +# Installation scripts for Cursor, Claude Code, Windsurf support +# ============================================================================= + +# ============================================================================= +# Claude Code CLI Installation +# ============================================================================= + +resource "coder_script" "claude_code_setup" { + count = data.coder_parameter.enable_ai_tools.value && data.coder_parameter.enable_claude_code.value ? 1 : 0 + agent_id = coder_agent.main.id + display_name = "Install Claude Code CLI" + icon = "/icon/ai.svg" + run_on_start = true + + script = "bash /home/coder/resources/tf/scripts/claude-install.sh" +} + +# ============================================================================= +# Cursor IDE Support Setup +# ============================================================================= + +resource "coder_script" "cursor_setup" { + count = data.coder_parameter.enable_ai_tools.value && data.coder_parameter.enable_cursor_support.value ? 1 : 0 + agent_id = coder_agent.main.id + display_name = "Configure Cursor IDE Support" + icon = "/icon/code.svg" + run_on_start = true + + script = "bash /home/coder/resources/tf/scripts/cursor-setup.sh" +} + +# ============================================================================= +# Windsurf IDE Support Setup +# ============================================================================= + +resource "coder_script" "windsurf_setup" { + count = data.coder_parameter.enable_ai_tools.value && data.coder_parameter.enable_windsurf_support.value ? 1 : 0 + agent_id = coder_agent.main.id + display_name = "Configure Windsurf IDE Support" + icon = "/icon/code.svg" + run_on_start = true + + script = "bash /home/coder/resources/tf/scripts/windsurf-setup.sh" +} + +# ============================================================================= +# Development Tools and Extensions +# ============================================================================= + +resource "coder_script" "dev_extensions" { + agent_id = coder_agent.main.id + display_name = "Install Development Tools" + icon = "/icon/tools.svg" + run_on_start = true + + script = "bash /home/coder/resources/tf/scripts/dev-tools.sh" +} + +# ============================================================================= +# Git Hooks and Metadata Capture Setup +# ============================================================================= + +resource "coder_script" "git_hooks_setup" { + agent_id = coder_agent.main.id + display_name = "Setup Git Hooks" + icon = "/icon/git.svg" + run_on_start = true + + script = "bash /home/coder/resources/tf/scripts/git-hooks.sh" +} \ No newline at end of file diff --git a/tf/services.tf b/tf/services.tf new file mode 100644 index 0000000..af9b4e9 --- /dev/null +++ b/tf/services.tf @@ -0,0 +1,328 @@ +# ============================================================================= +# Service Containers - Database and Development Services +# PostgreSQL, Redis, Qdrant, Docker Registry +# ============================================================================= + +# ============================================================================= +# PostgreSQL Database Service +# ============================================================================= + +# PostgreSQL data volume for persistence +resource "docker_volume" "postgres_data" { + count = data.coder_parameter.enable_services.value ? 1 : 0 + name = "postgres-data-${local.workspace_id}" + + labels { + label = "coder.service" + value = "postgres" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} + +# PostgreSQL container +resource "docker_container" "postgres" { + count = data.coder_parameter.enable_services.value ? 1 : 0 + image = "postgres:${var.postgres_version}-alpine" + name = "postgres-${local.workspace_id}" + + # PostgreSQL configuration + env = [ + "POSTGRES_DB=postgres", + "POSTGRES_USER=postgres", + "POSTGRES_PASSWORD=${var.postgres_password}", + "POSTGRES_INITDB_ARGS=--auth-local=trust --auth-host=md5", + "POSTGRES_SHARED_PRELOAD_LIBRARIES=pg_stat_statements", + "POSTGRES_MAX_CONNECTIONS=${var.postgres_max_connections}" + ] + + # Network configuration - internal only, accessible via Coder port forwarding + networks_advanced { + name = docker_network.workspace.name + } + + # Data persistence + volumes { + volume_name = docker_volume.postgres_data[0].name + container_path = "/var/lib/postgresql/data" + } + + # Health check + healthcheck { + test = ["CMD-SHELL", "pg_isready -U postgres"] + interval = "15s" + timeout = "5s" + retries = 5 + } + + restart = "unless-stopped" + + labels { + label = "coder.service" + value = "postgres" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} + +# ============================================================================= +# Redis Cache Service +# ============================================================================= + +# Redis data volume for persistence +resource "docker_volume" "redis_data" { + count = data.coder_parameter.enable_services.value ? 1 : 0 + name = "redis-data-${local.workspace_id}" + + labels { + label = "coder.service" + value = "redis" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} + +# Redis container +resource "docker_container" "redis" { + count = data.coder_parameter.enable_services.value ? 1 : 0 + image = "redis:${var.redis_version}-alpine" + name = "redis-${local.workspace_id}" + + # Redis configuration with authentication + command = [ + "redis-server", + "--requirepass", var.redis_password, + "--appendonly", "yes", + "--appendfsync", "everysec", + "--maxmemory", var.redis_max_memory, + "--maxmemory-policy", "allkeys-lru" + ] + + networks_advanced { + name = docker_network.workspace.name + } + + # Data persistence + volumes { + volume_name = docker_volume.redis_data[0].name + container_path = "/data" + } + + # Health check with authentication + healthcheck { + test = ["CMD", "redis-cli", "-a", var.redis_password, "ping"] + interval = "15s" + timeout = "3s" + retries = 5 + } + + restart = "unless-stopped" + + labels { + label = "coder.service" + value = "redis" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} + +# ============================================================================= +# Qdrant Vector Database Service +# ============================================================================= + +# Qdrant data volume for persistence +resource "docker_volume" "qdrant_data" { + count = data.coder_parameter.enable_services.value ? 1 : 0 + name = "qdrant-data-${local.workspace_id}" + + labels { + label = "coder.service" + value = "qdrant" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} + +# Qdrant container +resource "docker_container" "qdrant" { + count = data.coder_parameter.enable_services.value ? 1 : 0 + image = "qdrant/qdrant:${var.qdrant_version}" + name = "qdrant-${local.workspace_id}" + + # Qdrant configuration + env = [ + "QDRANT__SERVICE__HTTP_PORT=6333", + "QDRANT__SERVICE__GRPC_PORT=6334", + "QDRANT__SERVICE__HOST=0.0.0.0", + "QDRANT__LOG_LEVEL=INFO" + ] + + networks_advanced { + name = docker_network.workspace.name + } + + # Data persistence + volumes { + volume_name = docker_volume.qdrant_data[0].name + container_path = "/qdrant/storage" + } + + # Health check using simple file existence check (Qdrant creates lock files when running) + healthcheck { + test = ["CMD-SHELL", "test -d /qdrant/storage && test -f /qdrant/qdrant || exit 1"] + interval = "20s" + timeout = "5s" + retries = 5 + } + + restart = "unless-stopped" + + labels { + label = "coder.service" + value = "qdrant" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} + + +# ============================================================================= +# pgAdmin for PostgreSQL Management (Optional) +# ============================================================================= + +# pgAdmin data volume +resource "docker_volume" "pgadmin_data" { + count = data.coder_parameter.enable_services.value && data.coder_parameter.enable_pgadmin.value ? 1 : 0 + name = "pgadmin-data-${local.workspace_id}" + + labels { + label = "coder.service" + value = "pgadmin" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} + +# pgAdmin container +resource "docker_container" "pgadmin" { + count = data.coder_parameter.enable_services.value && data.coder_parameter.enable_pgadmin.value ? 1 : 0 + image = "dpage/pgadmin4:latest" + name = "pgadmin-${local.workspace_id}" + + # pgAdmin configuration + env = [ + "PGADMIN_DEFAULT_EMAIL=${var.pgadmin_email}", + "PGADMIN_DEFAULT_PASSWORD=${var.pgadmin_password}", + "PGADMIN_CONFIG_SERVER_MODE=False", + "PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False", + "PGADMIN_LISTEN_PORT=80" + ] + + networks_advanced { + name = docker_network.workspace.name + } + + # Data persistence + volumes { + volume_name = docker_volume.pgadmin_data[0].name + container_path = "/var/lib/pgadmin" + } + + # Health check for pgAdmin web interface + healthcheck { + test = ["CMD-SHELL", "curl -f http://localhost:80/misc/ping || wget --no-verbose --tries=1 --spider http://localhost:80/misc/ping || exit 1"] + interval = "30s" + timeout = "10s" + retries = 3 + start_period = "60s" + } + + restart = "unless-stopped" + + labels { + label = "coder.service" + value = "pgadmin" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} + +# ============================================================================= +# Jupyter Lab Service (Optional for Data Science) +# ============================================================================= + +# Jupyter data volume +resource "docker_volume" "jupyter_data" { + count = data.coder_parameter.enable_jupyter.value ? 1 : 0 + name = "jupyter-data-${local.workspace_id}" + + labels { + label = "coder.service" + value = "jupyter" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} + +# Jupyter Lab container +resource "docker_container" "jupyter" { + count = data.coder_parameter.enable_jupyter.value ? 1 : 0 + image = "jupyter/scipy-notebook:latest" + name = "jupyter-${local.workspace_id}" + + # Jupyter configuration + env = [ + "JUPYTER_ENABLE_LAB=yes", + "JUPYTER_TOKEN=", + "RESTARTABLE=yes", + "JUPYTER_PORT=8888" + ] + + networks_advanced { + name = docker_network.workspace.name + } + + # Port accessible within workspace network - no host exposure needed + + # Data persistence + volumes { + volume_name = docker_volume.jupyter_data[0].name + container_path = "/home/jovyan/work" + } + + # Share workspace volume + volumes { + volume_name = docker_volume.workspaces.name + container_path = "/home/jovyan/workspaces" + read_only = false + } + + restart = "unless-stopped" + + labels { + label = "coder.service" + value = "jupyter" + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } +} \ No newline at end of file diff --git a/tf/terraform.tfvars b/tf/terraform.tfvars new file mode 100644 index 0000000..1612ffe --- /dev/null +++ b/tf/terraform.tfvars @@ -0,0 +1,105 @@ +# ============================================================================= +# Terraform Variables Configuration +# Variable assignments for the development environment +# ============================================================================= + +# ============================================================================= +# Project Configuration +# ============================================================================= +project_name = "dev-environment" +environment = "dev" + +# ============================================================================= +# Docker Configuration +# ============================================================================= +docker_socket = "" +devcontainer_image = "mcr.microsoft.com/devcontainers/universal:2-linux" + +# ============================================================================= +# Development Tool Versions +# ============================================================================= +node_version = "20" +python_version = "3.12" +postgres_version = "17" +redis_version = "7" +qdrant_version = "latest" + +# ============================================================================= +# Service Configuration +# ============================================================================= +postgres_password = "devpassword" +redis_password = "devpassword" +postgres_max_connections = 100 +redis_max_memory = "512mb" + +# ============================================================================= +# Network Configuration +# ============================================================================= +pgadmin_port = 5050 +pgadmin_email = "admin@dev.local" +pgadmin_password = "adminpassword" + +# ============================================================================= +# Development Packages +# ============================================================================= +npm_packages = [ + "repomix", + "create-next-app", + "nodemon", + "concurrently", + "@types/node", + "typescript", + "eslint", + "prettier" +] + +python_packages = [ + "fastapi", + "uvicorn", + "requests", + "pandas", + "numpy", + "psycopg2-binary", + "redis", + "qdrant-client", + "python-dotenv" +] + +system_packages = [ + "make", + "tree", + "jq", + "curl", + "wget", + "unzip", + "build-essential" +] + +# ============================================================================= +# AI Development Tools +# ============================================================================= +install_claude_code = true +install_cursor_support = true +install_windsurf_support = true + +# ============================================================================= +# Performance Configuration +# ============================================================================= +workspace_memory_limit = 16384 +workspace_cpu_limit = 4 + +# ============================================================================= +# Feature Toggles +# ============================================================================= +enable_pgadmin = true +enable_monitoring = true +enable_jupyter = false + +# ============================================================================= +# Common Tags +# ============================================================================= +common_tags = { + Environment = "development" + ManagedBy = "terraform" + Purpose = "remote-development" +} \ No newline at end of file diff --git a/tf/variables.tf b/tf/variables.tf new file mode 100644 index 0000000..1cbd775 --- /dev/null +++ b/tf/variables.tf @@ -0,0 +1,322 @@ +# ============================================================================= +# Variable Definitions +# Modular Development Environment Configuration +# ============================================================================= + +# ============================================================================= +# Project Configuration +# ============================================================================= + +variable "project_name" { + description = "Name of the project for resource tagging and identification" + type = string + default = "dev-environment" + + validation { + condition = can(regex("^[a-z0-9-]+$", var.project_name)) + error_message = "Project name must contain only lowercase letters, numbers, and hyphens." + } +} + +variable "environment" { + description = "Environment designation (dev, staging, prod)" + type = string + default = "dev" + + validation { + condition = contains(["dev", "staging", "prod"], var.environment) + error_message = "Environment must be one of: dev, staging, prod." + } +} + +# ============================================================================= +# Docker Configuration +# ============================================================================= + +variable "docker_socket" { + description = "Docker daemon socket URI (empty for default)" + type = string + default = "" +} + +variable "devcontainer_image" { + description = "Development container image with all required tools pre-installed" + type = string + default = "mcr.microsoft.com/devcontainers/universal:2-linux" +} + +# ============================================================================= +# Development Tool Versions +# ============================================================================= + +variable "node_version" { + description = "Node.js version to install" + type = string + default = "20" + + validation { + condition = contains(["18", "20", "21"], var.node_version) + error_message = "Node.js version must be one of: 18, 20, 21." + } +} + +variable "python_version" { + description = "Python version to install" + type = string + default = "3.12" + + validation { + condition = contains(["3.10", "3.11", "3.12"], var.python_version) + error_message = "Python version must be 3.10, 3.11, or 3.12." + } +} + +variable "postgres_version" { + description = "PostgreSQL version" + type = string + default = "17" + + validation { + condition = contains(["13", "14", "15", "16", "17"], var.postgres_version) + error_message = "PostgreSQL version must be one of: 13, 14, 15, 16, 17." + } +} + +variable "redis_version" { + description = "Redis version" + type = string + default = "7" + + validation { + condition = contains(["6", "7"], var.redis_version) + error_message = "Redis version must be 6 or 7." + } +} + +variable "qdrant_version" { + description = "Qdrant vector database version" + type = string + default = "latest" +} + +# ============================================================================= +# Service Configuration +# ============================================================================= + +variable "postgres_password" { + description = "PostgreSQL postgres user password" + type = string + default = "devpassword" + sensitive = true + + validation { + condition = length(var.postgres_password) >= 8 + error_message = "PostgreSQL password must be at least 8 characters long." + } +} + +variable "redis_password" { + description = "Redis authentication password" + type = string + default = "devpassword" + sensitive = true + + validation { + condition = length(var.redis_password) >= 8 + error_message = "Redis password must be at least 8 characters long." + } +} + +variable "postgres_max_connections" { + description = "Maximum PostgreSQL connections" + type = number + default = 100 + + validation { + condition = var.postgres_max_connections >= 20 && var.postgres_max_connections <= 1000 + error_message = "PostgreSQL max connections must be between 20 and 1000." + } +} + +variable "redis_max_memory" { + description = "Redis maximum memory (e.g., '256mb', '1gb')" + type = string + default = "512mb" + + validation { + condition = can(regex("^[0-9]+[kmg]b$", var.redis_max_memory)) + error_message = "Redis max memory must be in format like '256mb' or '1gb'." + } +} + +# ============================================================================= +# Network Configuration +# ============================================================================= + + +variable "pgadmin_port" { + description = "pgAdmin web interface port" + type = number + default = 5050 + + validation { + condition = var.pgadmin_port >= 1024 && var.pgadmin_port <= 65535 + error_message = "pgAdmin port must be between 1024 and 65535." + } +} + +variable "pgadmin_email" { + description = "pgAdmin login email" + type = string + default = "admin@example.com" + + validation { + condition = can(regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", var.pgadmin_email)) + error_message = "pgAdmin email must be a valid email address." + } +} + +variable "pgadmin_password" { + description = "pgAdmin login password" + type = string + default = "adminpassword" + sensitive = true + + validation { + condition = length(var.pgadmin_password) >= 8 + error_message = "pgAdmin password must be at least 8 characters long." + } +} + +# ============================================================================= +# Development Packages +# ============================================================================= + +variable "npm_packages" { + description = "Global npm packages to install" + type = list(string) + default = [ + "repomix", # Repository packaging tool + "create-next-app", # Next.js app generator + "nodemon", # Development server auto-reload + "concurrently", # Run multiple commands + "@types/node", # Node.js TypeScript types + "typescript", # TypeScript compiler + "eslint", # JavaScript linter + "prettier" # Code formatter + ] +} + +variable "python_packages" { + description = "Python packages to install via uv" + type = list(string) + default = [ + "fastapi", # Modern web framework + "uvicorn", # ASGI server + "requests", # HTTP library + "pandas", # Data manipulation + "numpy", # Numerical computing + "psycopg2-binary", # PostgreSQL adapter + "redis", # Redis client + "qdrant-client", # Qdrant vector database client + "python-dotenv" # Environment variable loading + ] +} + +variable "system_packages" { + description = "Additional system packages to install" + type = list(string) + default = [ + "make", # Build tool + "tree", # Directory tree viewer + "jq", # JSON processor + "curl", # HTTP client + "wget", # File downloader + "unzip", # Archive extractor + "build-essential" # Compilation tools + ] +} + +# ============================================================================= +# AI Development Tools +# ============================================================================= + +variable "install_claude_code" { + description = "Install Claude Code CLI for AI assistance" + type = bool + default = true +} + +variable "install_cursor_support" { + description = "Install Cursor IDE support and extensions" + type = bool + default = true +} + +variable "install_windsurf_support" { + description = "Install Windsurf IDE support" + type = bool + default = false +} + +# ============================================================================= +# Performance Configuration +# ============================================================================= + +variable "workspace_memory_limit" { + description = "Memory limit for workspace container (MB)" + type = number + default = 8192 + + validation { + condition = var.workspace_memory_limit >= 2048 && var.workspace_memory_limit <= 32768 + error_message = "Workspace memory limit must be between 2048MB (2GB) and 32768MB (32GB)." + } +} + +variable "workspace_cpu_limit" { + description = "CPU limit for workspace container (cores)" + type = number + default = 4 + + validation { + condition = var.workspace_cpu_limit >= 1 && var.workspace_cpu_limit <= 16 + error_message = "Workspace CPU limit must be between 1 and 16 cores." + } +} + +# ============================================================================= +# Feature Toggles +# ============================================================================= + +variable "enable_pgadmin" { + description = "Enable pgAdmin web interface (resource intensive)" + type = bool + default = true +} + +variable "enable_monitoring" { + description = "Enable container monitoring and metrics collection" + type = bool + default = false +} + +variable "enable_jupyter" { + description = "Enable Jupyter Lab for data science workflows" + type = bool + default = false +} + +# ============================================================================= +# Common Tags +# ============================================================================= + +variable "common_tags" { + description = "Common tags to apply to all resources" + type = map(string) + default = { + Environment = "development" + ManagedBy = "terraform" + Purpose = "remote-development" + } +} \ No newline at end of file diff --git a/tf/workspace.tf b/tf/workspace.tf new file mode 100644 index 0000000..b159fe2 --- /dev/null +++ b/tf/workspace.tf @@ -0,0 +1,224 @@ +# ============================================================================= +# Development Workspace Container +# Main development environment with all required tools +# ============================================================================= + +# ============================================================================= +# Coder Agent - Workspace Management +# ============================================================================= + +resource "coder_agent" "main" { + arch = data.coder_provisioner.me.arch + os = "linux" + dir = "/workspaces" + + # Environment variables for development + 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 + "NODE_VERSION" = var.node_version + "PYTHON_VERSION" = var.python_version + "PATH" = "$PATH:/home/coder/.cargo/bin:/home/coder/.local/bin:/usr/local/bin" + "HOME" = "/home/coder" + "USER" = "coder" + # Service URLs for development + "POSTGRES_URL" = data.coder_parameter.enable_services.value ? "postgresql://postgres:${var.postgres_password}@postgres-${local.workspace_id}:5432/postgres" : "" + "REDIS_URL" = data.coder_parameter.enable_services.value ? "redis://:${var.redis_password}@redis-${local.workspace_id}:6379" : "" + "QDRANT_URL" = data.coder_parameter.enable_services.value ? "http://qdrant-${local.workspace_id}:6333" : "" + # Additional environment variables for scripts + "ENABLE_SERVICES" = tostring(data.coder_parameter.enable_services.value) + } + + # Reference bind-mounted startup script plus service port forwarding + startup_script = <<-EOT + bash /home/coder/resources/tf/scripts/workspace-setup.sh + + # Register JetBrains Gateway backend location if enabled + if [ "${data.coder_parameter.enable_jetbrains.value}" = "true" ] && [ -d ~/JetBrains ]; then + ~/JetBrains/*/bin/remote-dev-server.sh registerBackendLocationForGateway 2>/dev/null || echo "JetBrains Gateway registration skipped" + fi + EOT + + # Performance and resource monitoring + metadata { + display_name = "CPU Usage" + key = "cpu_usage" + script = "coder stat cpu" + interval = 60 + timeout = 10 + } + + metadata { + display_name = "RAM Usage" + key = "ram_usage" + script = "coder stat mem" + interval = 60 + timeout = 10 + } + + metadata { + display_name = "Disk Usage" + key = "disk_usage" + script = "coder stat disk --path /workspaces" + interval = 300 + timeout = 10 + } + + metadata { + display_name = "Git Branch" + key = "git_branch" + script = "cd /workspaces && git branch --show-current 2>/dev/null || echo 'no-repo'" + interval = 300 + timeout = 5 + } +} + +# ============================================================================= +# Main Development Container +# ============================================================================= + +resource "docker_container" "workspace" { + count = data.coder_workspace.me.start_count + image = docker_image.devcontainer.image_id + name = local.container_name + hostname = data.coder_workspace.me.name + + # Container resource limits + memory = var.workspace_memory_limit * 1024 * 1024 # Convert MB to bytes + + # Environment variables + 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}", + "NODE_VERSION=${var.node_version}", + "PYTHON_VERSION=${var.python_version}", + "CODER_AGENT_TOKEN=${coder_agent.main.token}" + ] + + # Network configuration + networks_advanced { + name = docker_network.workspace.name + } + + # Host networking for Docker-in-Docker and reverse proxy support + host { + host = "host.docker.internal" + ip = "host-gateway" + } + + # No port mappings needed - reverse proxy will handle routing + # All services run within the isolated workspace network + # Coder's port forwarding and apps will provide access via reverse proxy + + + # Volume mounts + volumes { + container_path = "/workspaces" + volume_name = docker_volume.workspaces.name + read_only = false + } + + # Mount the existing coder-home volume for user data persistence + volumes { + container_path = "/home/coder" + volume_name = "bwk8ckcok8o84cc0o4os4sso_coder-home" + read_only = false + } + + # Bind mount code-tools directory for live script updates + volumes { + host_path = "/home/trav/code-tools" + container_path = "/home/coder/resources" + read_only = true + } + + # Docker socket for Docker-in-Docker + volumes { + host_path = "/var/run/docker.sock" + container_path = "/var/run/docker.sock" + } + + # Working directory + working_dir = "/workspaces" + + # Keep container running + command = ["/bin/bash", "-c", "${coder_agent.main.init_script} && sleep infinity"] + + # Container labels for management + labels { + label = "coder.owner" + value = data.coder_workspace_owner.me.name + } + labels { + label = "coder.workspace_id" + value = local.workspace_id + } + labels { + label = "coder.project" + value = var.project_name + } + + # Dependencies + depends_on = [ + docker_network.workspace, + docker_volume.workspaces, + docker_image.devcontainer + ] +} + +# ============================================================================= +# JetBrains Gateway Integration +# ============================================================================= + +module "jetbrains_gateway" { + count = data.coder_parameter.enable_jetbrains.value && data.coder_workspace.me.start_count > 0 ? 1 : 0 + source = "registry.coder.com/modules/jetbrains-gateway/coder" + version = "1.0.29" + agent_id = coder_agent.main.id + folder = "/workspaces" + jetbrains_ides = ["IU", "WS", "PY", "GO"] + default = "IU" + latest = false + jetbrains_ide_versions = { + "IU" = { + build_number = "251.25410.129" + version = "2025.1" + } + "WS" = { + build_number = "251.25410.129" + version = "2025.1" + } + "PY" = { + build_number = "251.25410.129" + version = "2025.1" + } + "GO" = { + build_number = "251.25410.129" + version = "2025.1" + } + "CL" = { + build_number = "251.25410.129" + version = "2025.1" + } + "PS" = { + build_number = "251.25410.129" + version = "2025.1" + } + "RR" = { + build_number = "251.25410.129" + version = "2025.1" + } + "RM" = { + build_number = "251.25410.129" + version = "2025.1" + } + "RD" = { + build_number = "251.25410.129" + version = "2025.1" + } + } +} \ No newline at end of file diff --git a/ui.png b/ui.png new file mode 100644 index 0000000..9ad814b Binary files /dev/null and b/ui.png differ