Merge branch 'main' into stevenmasley/subdomain_vscode
commit
ce5af66257
@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
@ -1,42 +0,0 @@
|
|||||||
name: Update README on Tag
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-readme:
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Get the latest tag
|
|
||||||
id: get-latest-tag
|
|
||||||
run: echo "TAG=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Run update script
|
|
||||||
run: ./update-version.sh
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
id: create-pr
|
|
||||||
uses: peter-evans/create-pull-request@v5
|
|
||||||
with:
|
|
||||||
commit-message: 'chore: bump version to ${{ env.TAG }} in README.md files'
|
|
||||||
title: 'chore: bump version to ${{ env.TAG }} in README.md files'
|
|
||||||
body: 'This is an auto-generated PR to update README.md files of all modules with the new tag ${{ env.TAG }}'
|
|
||||||
branch: 'update-readme-branch'
|
|
||||||
base: 'main'
|
|
||||||
env:
|
|
||||||
TAG: ${{ steps.get-latest-tag.outputs.TAG }}
|
|
||||||
|
|
||||||
- name: Auto-approve
|
|
||||||
uses: hmarr/auto-approve-action@v4
|
|
||||||
if: github.ref == 'refs/heads/update-readme-branch'
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 1.5 MiB |
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
display_name: Cursor IDE
|
||||||
|
description: Add a one-click button to launch Cursor IDE
|
||||||
|
icon: ../.icons/cursor.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [ide, cursor, helper]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cursor IDE
|
||||||
|
|
||||||
|
Add a button to open any workspace with a single click in Cursor IDE.
|
||||||
|
|
||||||
|
Uses the [Coder Remote VS Code Extension](https://github.com/coder/cursor-coder).
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "cursor" {
|
||||||
|
source = "registry.coder.com/modules/cursor/coder"
|
||||||
|
version = "1.0.19"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Open in a specific directory
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "cursor" {
|
||||||
|
source = "registry.coder.com/modules/cursor/coder"
|
||||||
|
version = "1.0.19"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
folder = "/home/coder/project"
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,88 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("cursor", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("default output", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
|
||||||
|
const coder_app = state.resources.find(
|
||||||
|
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(coder_app).not.toBeNull();
|
||||||
|
expect(coder_app?.instances.length).toBe(1);
|
||||||
|
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds folder", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
folder: "/foo/bar",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds folder and open_recent", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
folder: "/foo/bar",
|
||||||
|
open_recent: "true",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds folder but not open_recent", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
folder: "/foo/bar",
|
||||||
|
openRecent: "false",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds open_recent", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
open_recent: "true",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("expect order to be set", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
order: "22",
|
||||||
|
});
|
||||||
|
|
||||||
|
const coder_app = state.resources.find(
|
||||||
|
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(coder_app).not.toBeNull();
|
||||||
|
expect(coder_app?.instances.length).toBe(1);
|
||||||
|
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,62 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "folder" {
|
||||||
|
type = string
|
||||||
|
description = "The folder to open in Cursor IDE."
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "open_recent" {
|
||||||
|
type = bool
|
||||||
|
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
|
resource "coder_app" "cursor" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
external = true
|
||||||
|
icon = "/icon/cursor.svg"
|
||||||
|
slug = "cursor"
|
||||||
|
display_name = "Cursor Desktop"
|
||||||
|
order = var.order
|
||||||
|
url = join("", [
|
||||||
|
"cursor://coder.coder-remote/open",
|
||||||
|
"?owner=",
|
||||||
|
data.coder_workspace_owner.me.name,
|
||||||
|
"&workspace=",
|
||||||
|
data.coder_workspace.me.name,
|
||||||
|
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||||
|
var.open_recent ? "&openRecent" : "",
|
||||||
|
"&url=",
|
||||||
|
data.coder_workspace.me.access_url,
|
||||||
|
"&token=$SESSION_TOKEN",
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
output "cursor_url" {
|
||||||
|
value = coder_app.cursor.url
|
||||||
|
description = "Cursor IDE Desktop URL."
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
email=${ARTIFACTORY_EMAIL}
|
||||||
|
%{ for REPO in REPOS ~}
|
||||||
|
${REPO.SCOPE}registry=${JFROG_URL}/artifactory/api/npm/${REPO.NAME}
|
||||||
|
//${JFROG_HOST}/artifactory/api/npm/${REPO.NAME}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}
|
||||||
|
%{ endfor ~}
|
@ -1,19 +1,129 @@
|
|||||||
import { serve } from "bun";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { describe } from "bun:test";
|
|
||||||
import {
|
import {
|
||||||
createJSONResponse,
|
findResourceInstance,
|
||||||
runTerraformInit,
|
runTerraformInit,
|
||||||
|
runTerraformApply,
|
||||||
testRequiredVariables,
|
testRequiredVariables,
|
||||||
} from "../test";
|
} from "../test";
|
||||||
|
|
||||||
describe("jfrog-oauth", async () => {
|
describe("jfrog-oauth", async () => {
|
||||||
|
type TestVariables = {
|
||||||
|
agent_id: string;
|
||||||
|
jfrog_url: string;
|
||||||
|
package_managers: string;
|
||||||
|
|
||||||
|
username_field?: string;
|
||||||
|
jfrog_server_id?: string;
|
||||||
|
external_auth_id?: string;
|
||||||
|
configure_code_server?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
await runTerraformInit(import.meta.dir);
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
testRequiredVariables(import.meta.dir, {
|
const fakeFrogApi = "localhost:8081/artifactory/api";
|
||||||
|
const fakeFrogUrl = "http://localhost:8081";
|
||||||
|
const user = "default";
|
||||||
|
|
||||||
|
it("can run apply with required variables", async () => {
|
||||||
|
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||||
agent_id: "some-agent-id",
|
agent_id: "some-agent-id",
|
||||||
jfrog_url: "http://localhost:8081",
|
jfrog_url: fakeFrogUrl,
|
||||||
package_managers: "{}",
|
package_managers: "{}",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO add more tests
|
it("generates an npmrc with scoped repos", async () => {
|
||||||
|
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "some-agent-id",
|
||||||
|
jfrog_url: fakeFrogUrl,
|
||||||
|
package_managers: JSON.stringify({
|
||||||
|
npm: ["global", "@foo:foo", "@bar:bar"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const coderScript = findResourceInstance(state, "coder_script");
|
||||||
|
const npmrcStanza = `cat << EOF > ~/.npmrc
|
||||||
|
email=${user}@example.com
|
||||||
|
registry=http://${fakeFrogApi}/npm/global
|
||||||
|
//${fakeFrogApi}/npm/global/:_authToken=
|
||||||
|
@foo:registry=http://${fakeFrogApi}/npm/foo
|
||||||
|
//${fakeFrogApi}/npm/foo/:_authToken=
|
||||||
|
@bar:registry=http://${fakeFrogApi}/npm/bar
|
||||||
|
//${fakeFrogApi}/npm/bar/:_authToken=
|
||||||
|
|
||||||
|
EOF`;
|
||||||
|
expect(coderScript.script).toContain(npmrcStanza);
|
||||||
|
expect(coderScript.script).toContain(
|
||||||
|
'jf npmc --global --repo-resolve "global"',
|
||||||
|
);
|
||||||
|
expect(coderScript.script).toContain(
|
||||||
|
'if [ -z "YES" ]; then\n not_configured npm',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates a pip config with extra-indexes", async () => {
|
||||||
|
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "some-agent-id",
|
||||||
|
jfrog_url: fakeFrogUrl,
|
||||||
|
package_managers: JSON.stringify({
|
||||||
|
pypi: ["global", "foo", "bar"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const coderScript = findResourceInstance(state, "coder_script");
|
||||||
|
const pipStanza = `cat << EOF > ~/.pip/pip.conf
|
||||||
|
[global]
|
||||||
|
index-url = https://${user}:@${fakeFrogApi}/pypi/global/simple
|
||||||
|
extra-index-url =
|
||||||
|
https://${user}:@${fakeFrogApi}/pypi/foo/simple
|
||||||
|
https://${user}:@${fakeFrogApi}/pypi/bar/simple
|
||||||
|
|
||||||
|
EOF`;
|
||||||
|
expect(coderScript.script).toContain(pipStanza);
|
||||||
|
expect(coderScript.script).toContain(
|
||||||
|
'jf pipc --global --repo-resolve "global"',
|
||||||
|
);
|
||||||
|
expect(coderScript.script).toContain(
|
||||||
|
'if [ -z "YES" ]; then\n not_configured pypi',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("registers multiple docker repos", async () => {
|
||||||
|
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "some-agent-id",
|
||||||
|
jfrog_url: fakeFrogUrl,
|
||||||
|
package_managers: JSON.stringify({
|
||||||
|
docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const coderScript = findResourceInstance(state, "coder_script");
|
||||||
|
const dockerStanza = ["foo", "bar", "baz"]
|
||||||
|
.map((r) => `register_docker "${r}.jfrog.io"`)
|
||||||
|
.join("\n");
|
||||||
|
expect(coderScript.script).toContain(dockerStanza);
|
||||||
|
expect(coderScript.script).toContain(
|
||||||
|
'if [ -z "YES" ]; then\n not_configured docker',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets goproxy with multiple repos", async () => {
|
||||||
|
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "some-agent-id",
|
||||||
|
jfrog_url: fakeFrogUrl,
|
||||||
|
package_managers: JSON.stringify({
|
||||||
|
go: ["foo", "bar", "baz"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
|
||||||
|
const proxies = ["foo", "bar", "baz"]
|
||||||
|
.map((r) => `https://${user}:@${fakeFrogApi}/go/${r}`)
|
||||||
|
.join(",");
|
||||||
|
expect(proxyEnv.value).toEqual(proxies);
|
||||||
|
|
||||||
|
const coderScript = findResourceInstance(state, "coder_script");
|
||||||
|
expect(coderScript.script).toContain(
|
||||||
|
'jf goc --global --repo-resolve "foo"',
|
||||||
|
);
|
||||||
|
expect(coderScript.script).toContain(
|
||||||
|
'if [ -z "YES" ]; then\n not_configured go',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
[global]
|
||||||
|
index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${try(element(REPOS, 0), "")}/simple
|
||||||
|
extra-index-url =
|
||||||
|
%{ for REPO in try(slice(REPOS, 1, length(REPOS)), []) ~}
|
||||||
|
https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPO}/simple
|
||||||
|
%{ endfor ~}
|
@ -0,0 +1,5 @@
|
|||||||
|
email=${ARTIFACTORY_EMAIL}
|
||||||
|
%{ for REPO in REPOS ~}
|
||||||
|
${REPO.SCOPE}registry=${JFROG_URL}/artifactory/api/npm/${REPO.NAME}
|
||||||
|
//${JFROG_HOST}/artifactory/api/npm/${REPO.NAME}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}
|
||||||
|
%{ endfor ~}
|
@ -0,0 +1,6 @@
|
|||||||
|
[global]
|
||||||
|
index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${try(element(REPOS, 0), "")}/simple
|
||||||
|
extra-index-url =
|
||||||
|
%{ for REPO in try(slice(REPOS, 1, length(REPOS)), []) ~}
|
||||||
|
https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPO}/simple
|
||||||
|
%{ endfor ~}
|
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
display_name: KasmVNC
|
||||||
|
description: A modern open source VNC server
|
||||||
|
icon: ../.icons/kasmvnc.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [helper, vnc, desktop]
|
||||||
|
---
|
||||||
|
|
||||||
|
# KasmVNC
|
||||||
|
|
||||||
|
Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and create an app to access it via the dashboard.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "kasmvnc" {
|
||||||
|
source = "registry.coder.com/modules/kasmvnc/coder"
|
||||||
|
version = "1.0.22"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
desktop_environment = "xfce"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** This module only works on workspaces with a pre-installed desktop environment. As an example base image you can use `codercom/enterprise-desktop` image.
|
@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
const allowedDesktopEnvs = ["xfce", "kde", "gnome", "lxde", "lxqt"] as const;
|
||||||
|
type AllowedDesktopEnv = (typeof allowedDesktopEnvs)[number];
|
||||||
|
|
||||||
|
type TestVariables = Readonly<{
|
||||||
|
agent_id: string;
|
||||||
|
desktop_environment: AllowedDesktopEnv;
|
||||||
|
port?: string;
|
||||||
|
kasm_version?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
describe("Kasm VNC", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
desktop_environment: "gnome",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Successfully installs for all expected Kasm desktop versions", async () => {
|
||||||
|
for (const v of allowedDesktopEnvs) {
|
||||||
|
const applyWithEnv = () => {
|
||||||
|
runTerraformApply<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
desktop_environment: v,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(applyWithEnv).not.toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,63 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "port" {
|
||||||
|
type = number
|
||||||
|
description = "The port to run KasmVNC on."
|
||||||
|
default = 6800
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "kasm_version" {
|
||||||
|
type = string
|
||||||
|
description = "Version of KasmVNC to install."
|
||||||
|
default = "1.3.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "desktop_environment" {
|
||||||
|
type = string
|
||||||
|
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
|
||||||
|
validation {
|
||||||
|
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
|
||||||
|
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_script" "kasm_vnc" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "KasmVNC"
|
||||||
|
icon = "/icon/kasmvnc.svg"
|
||||||
|
script = templatefile("${path.module}/run.sh", {
|
||||||
|
PORT : var.port,
|
||||||
|
DESKTOP_ENVIRONMENT : var.desktop_environment,
|
||||||
|
VERSION : var.kasm_version
|
||||||
|
})
|
||||||
|
run_on_start = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_app" "kasm_vnc" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
slug = "kasm-vnc"
|
||||||
|
display_name = "kasmVNC"
|
||||||
|
url = "http://localhost:${var.port}"
|
||||||
|
icon = "/icon/kasmvnc.svg"
|
||||||
|
subdomain = true
|
||||||
|
share = "owner"
|
||||||
|
healthcheck {
|
||||||
|
url = "http://localhost:${var.port}/app"
|
||||||
|
interval = 5
|
||||||
|
threshold = 5
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Function to check if vncserver is already installed
|
||||||
|
check_installed() {
|
||||||
|
if command -v vncserver &> /dev/null; then
|
||||||
|
echo "vncserver is already installed."
|
||||||
|
return 0 # Don't exit, just indicate it's installed
|
||||||
|
else
|
||||||
|
return 1 # Indicates not installed
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to download a file using wget, curl, or busybox as a fallback
|
||||||
|
download_file() {
|
||||||
|
local url=$1
|
||||||
|
local output=$2
|
||||||
|
if command -v wget &> /dev/null; then
|
||||||
|
wget $url -O $output
|
||||||
|
elif command -v curl &> /dev/null; then
|
||||||
|
curl -fsSL $url -o $output
|
||||||
|
elif command -v busybox &> /dev/null; then
|
||||||
|
busybox wget -O $output $url
|
||||||
|
else
|
||||||
|
echo "Neither wget, curl, nor busybox is installed. Please install one of them to proceed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install kasmvncserver for debian-based distros
|
||||||
|
install_deb() {
|
||||||
|
local url=$1
|
||||||
|
download_file $url /tmp/kasmvncserver.deb
|
||||||
|
sudo apt-get update
|
||||||
|
DEBIAN_FRONTEND=noninteractive sudo apt-get install --yes -qq --no-install-recommends --no-install-suggests /tmp/kasmvncserver.deb
|
||||||
|
sudo adduser $USER ssl-cert
|
||||||
|
rm /tmp/kasmvncserver.deb
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install kasmvncserver for Oracle 8
|
||||||
|
install_rpm_oracle8() {
|
||||||
|
local url=$1
|
||||||
|
download_file $url /tmp/kasmvncserver.rpm
|
||||||
|
sudo dnf config-manager --set-enabled ol8_codeready_builder
|
||||||
|
sudo dnf install oracle-epel-release-el8 -y
|
||||||
|
sudo dnf localinstall /tmp/kasmvncserver.rpm -y
|
||||||
|
sudo usermod -aG kasmvnc-cert $USER
|
||||||
|
rm /tmp/kasmvncserver.rpm
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install kasmvncserver for CentOS 7
|
||||||
|
install_rpm_centos7() {
|
||||||
|
local url=$1
|
||||||
|
download_file $url /tmp/kasmvncserver.rpm
|
||||||
|
sudo yum install epel-release -y
|
||||||
|
sudo yum install /tmp/kasmvncserver.rpm -y
|
||||||
|
sudo usermod -aG kasmvnc-cert $USER
|
||||||
|
rm /tmp/kasmvncserver.rpm
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install kasmvncserver for rpm-based distros
|
||||||
|
install_rpm() {
|
||||||
|
local url=$1
|
||||||
|
download_file $url /tmp/kasmvncserver.rpm
|
||||||
|
sudo rpm -i /tmp/kasmvncserver.rpm
|
||||||
|
rm /tmp/kasmvncserver.rpm
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install kasmvncserver for Alpine Linux
|
||||||
|
install_alpine() {
|
||||||
|
local url=$1
|
||||||
|
download_file $url /tmp/kasmvncserver.tgz
|
||||||
|
tar -xzf /tmp/kasmvncserver.tgz -C /usr/local/bin/
|
||||||
|
rm /tmp/kasmvncserver.tgz
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect system information
|
||||||
|
distro=$(grep "^ID=" /etc/os-release | awk -F= '{print $2}')
|
||||||
|
version=$(grep "^VERSION_ID=" /etc/os-release | awk -F= '{print $2}' | tr -d '"')
|
||||||
|
arch=$(uname -m)
|
||||||
|
|
||||||
|
echo "Detected Distribution: $distro"
|
||||||
|
echo "Detected Version: $version"
|
||||||
|
echo "Detected Architecture: $arch"
|
||||||
|
|
||||||
|
# Map arch to package arch
|
||||||
|
if [[ "$arch" == "x86_64" ]]; then
|
||||||
|
if [[ "$distro" == "ubuntu" || "$distro" == "debian" || "$distro" == "kali" ]]; then
|
||||||
|
arch="amd64"
|
||||||
|
else
|
||||||
|
arch="x86_64"
|
||||||
|
fi
|
||||||
|
elif [[ "$arch" == "aarch64" || "$arch" == "arm64" ]]; then
|
||||||
|
if [[ "$distro" == "ubuntu" || "$distro" == "debian" || "$distro" == "kali" ]]; then
|
||||||
|
arch="arm64"
|
||||||
|
else
|
||||||
|
arch="aarch64"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Unsupported architecture: $arch"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if vncserver is installed, and install if not
|
||||||
|
if ! check_installed; then
|
||||||
|
echo "Installing KASM version: ${VERSION}"
|
||||||
|
case $distro in
|
||||||
|
ubuntu | debian | kali)
|
||||||
|
case $version in
|
||||||
|
"20.04")
|
||||||
|
install_deb "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_focal_${VERSION}_$${arch}.deb"
|
||||||
|
;;
|
||||||
|
"22.04")
|
||||||
|
install_deb "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_jammy_${VERSION}_$${arch}.deb"
|
||||||
|
;;
|
||||||
|
"24.04")
|
||||||
|
install_deb "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_noble_${VERSION}_$${arch}.deb"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported Ubuntu/Debian/Kali version: $${version}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
oracle)
|
||||||
|
if [[ "$version" == "8" ]]; then
|
||||||
|
install_rpm_oracle8 "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_oracle_8_${VERSION}_$${arch}.rpm"
|
||||||
|
else
|
||||||
|
echo "Unsupported Oracle version: $${version}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
centos)
|
||||||
|
if [[ "$version" == "7" ]]; then
|
||||||
|
install_rpm_centos7 "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_centos_core_${VERSION}_$${arch}.rpm"
|
||||||
|
else
|
||||||
|
install_rpm "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_centos_core_${VERSION}_$${arch}.rpm"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
alpine)
|
||||||
|
if [[ "$version" == "3.17" || "$version" == "3.18" || "$version" == "3.19" || "$version" == "3.20" ]]; then
|
||||||
|
install_alpine "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvnc.alpine_$${version}_$${arch}.tgz"
|
||||||
|
else
|
||||||
|
echo "Unsupported Alpine version: $${version}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
fedora | opensuse)
|
||||||
|
install_rpm "https://github.com/kasmtech/KasmVNC/releases/download/v${VERSION}/kasmvncserver_$${distro}_$${version}_${VERSION}_$${arch}.rpm"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported distribution: $${distro}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "vncserver already installed. Skipping installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Coder port-forwarding from dashboard only supports HTTP
|
||||||
|
sudo bash -c "cat > /etc/kasmvnc/kasmvnc.yaml <<EOF
|
||||||
|
network:
|
||||||
|
protocol: http
|
||||||
|
websocket_port: ${PORT}
|
||||||
|
ssl:
|
||||||
|
require_ssl: false
|
||||||
|
udp:
|
||||||
|
public_ip: 127.0.0.1
|
||||||
|
EOF"
|
||||||
|
|
||||||
|
# This password is not used since we start the server without auth.
|
||||||
|
# The server is protected via the Coder session token / tunnel
|
||||||
|
# and does not listen publicly
|
||||||
|
echo -e "password\npassword\n" | vncpasswd -wo -u $USER
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
printf "🚀 Starting KasmVNC server...\n"
|
||||||
|
sudo -u $USER bash -c "vncserver -select-de ${DESKTOP_ENVIRONMENT} -disableBasicAuth" > /tmp/kasmvncserver.log 2>&1 &
|
@ -1,264 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "modules",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "modules",
|
|
||||||
"devDependencies": {
|
|
||||||
"bun-types": "^1.0.18",
|
|
||||||
"gray-matter": "^4.0.3",
|
|
||||||
"marked": "^12.0.0",
|
|
||||||
"prettier": "^3.2.5",
|
|
||||||
"prettier-plugin-sh": "^0.13.1",
|
|
||||||
"prettier-plugin-terraform-formatter": "^1.2.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.3.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "20.12.14",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
|
|
||||||
"integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~5.26.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/ws": {
|
|
||||||
"version": "8.5.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
|
|
||||||
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/argparse": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"sprintf-js": "~1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/bun-types": {
|
|
||||||
"version": "1.1.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.16.tgz",
|
|
||||||
"integrity": "sha512-LpAh8dQe4NKvhSW390Rkftw0ume0moSkRm575e1JZ1PwI/dXjbXyjpntq+2F0bVW1FV7V6B8EfWx088b+dNurw==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "~20.12.8",
|
|
||||||
"@types/ws": "~8.5.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esprima": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"esparse": "bin/esparse.js",
|
|
||||||
"esvalidate": "bin/esvalidate.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/extend-shallow": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"is-extendable": "^0.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gray-matter": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"js-yaml": "^3.13.1",
|
|
||||||
"kind-of": "^6.0.2",
|
|
||||||
"section-matter": "^1.0.0",
|
|
||||||
"strip-bom-string": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-extendable": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
|
||||||
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-yaml": {
|
|
||||||
"version": "3.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
|
||||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"argparse": "^1.0.7",
|
|
||||||
"esprima": "^4.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"js-yaml": "bin/js-yaml.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/kind-of": {
|
|
||||||
"version": "6.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
|
||||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/marked": {
|
|
||||||
"version": "12.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
|
||||||
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"marked": "bin/marked.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mvdan-sh": {
|
|
||||||
"version": "0.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/mvdan-sh/-/mvdan-sh-0.10.1.tgz",
|
|
||||||
"integrity": "sha512-kMbrH0EObaKmK3nVRKUIIya1dpASHIEusM13S4V1ViHFuxuNxCo+arxoa6j/dbV22YBGjl7UKJm9QQKJ2Crzhg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/prettier": {
|
|
||||||
"version": "3.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
|
|
||||||
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
|
|
||||||
"dev": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"prettier": "bin/prettier.cjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier-plugin-sh": {
|
|
||||||
"version": "0.13.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-sh/-/prettier-plugin-sh-0.13.1.tgz",
|
|
||||||
"integrity": "sha512-ytMcl1qK4s4BOFGvsc9b0+k9dYECal7U29bL/ke08FEUsF/JLN0j6Peo0wUkFDG4y2UHLMhvpyd6Sd3zDXe/eg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"mvdan-sh": "^0.10.1",
|
|
||||||
"sh-syntax": "^0.4.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/unts"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"prettier": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prettier-plugin-terraform-formatter": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier-plugin-terraform-formatter/-/prettier-plugin-terraform-formatter-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-rdzV61Bs/Ecnn7uAS/vL5usTX8xUWM+nQejNLZxt3I1kJH5WSeLEmq7LYu1wCoEQF+y7Uv1xGvPRfl3lIe6+tA==",
|
|
||||||
"dev": true,
|
|
||||||
"peerDependencies": {
|
|
||||||
"prettier": ">= 1.16.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"prettier": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/section-matter": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"extend-shallow": "^2.0.1",
|
|
||||||
"kind-of": "^6.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sh-syntax": {
|
|
||||||
"version": "0.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/sh-syntax/-/sh-syntax-0.4.2.tgz",
|
|
||||||
"integrity": "sha512-/l2UZ5fhGZLVZa16XQM9/Vq/hezGGbdHeVEA01uWjOL1+7Ek/gt6FquW0iKKws4a9AYPYvlz6RyVvjh3JxOteg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.6.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/unts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sprintf-js": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/strip-bom-string": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tslib": {
|
|
||||||
"version": "2.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
|
||||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz",
|
|
||||||
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==",
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "5.26.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,14 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
// If we were just compiling for the tests, we could safely target ESNext at
|
||||||
"module": "esnext",
|
// all times, but just because we've been starting to add more runtime logic
|
||||||
|
// files to some of the modules, erring on the side of caution by having a
|
||||||
|
// older compilation target
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "ESNext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "node",
|
||||||
"types": ["bun-types"]
|
"types": ["bun-types"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
display_name: Hashicorp Vault Integration (JWT)
|
||||||
|
description: Authenticates with Vault using a JWT from Coder's OIDC provider
|
||||||
|
icon: ../.icons/vault.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
partner_github: hashicorp
|
||||||
|
verified: true
|
||||||
|
tags: [helper, integration, vault, jwt, oidc]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hashicorp Vault Integration (JWT)
|
||||||
|
|
||||||
|
This module lets you authenticate with [Hashicorp Vault](https://www.vaultproject.io/) in your Coder workspaces by reusing the [OIDC](https://coder.com/docs/admin/auth#openid-connect) access token from Coder's OIDC authentication method. This requires configuring the Vault [JWT/OIDC](https://developer.hashicorp.com/vault/docs/auth/jwt#configuration) auth method.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "vault" {
|
||||||
|
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||||
|
version = "1.0.20"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
vault_addr = "https://vault.example.com"
|
||||||
|
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can use the Vault CLI in your workspaces to fetch secrets from Vault:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
vault kv get -namespace=coder -mount=secrets coder
|
||||||
|
```
|
||||||
|
|
||||||
|
or using the Vault API:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/data/coder"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Configure Vault integration with a non standard auth path (default is "jwt")
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "vault" {
|
||||||
|
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||||
|
version = "1.0.20"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
vault_addr = "https://vault.example.com"
|
||||||
|
vault_jwt_auth_path = "oidc"
|
||||||
|
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Map workspace owner's group to a Vault role
|
||||||
|
|
||||||
|
```tf
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
|
module "vault" {
|
||||||
|
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||||
|
version = "1.0.20"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
vault_addr = "https://vault.example.com"
|
||||||
|
vault_jwt_role = data.coder_workspace_owner.me.groups[0]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install a specific version of the Vault CLI
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "vault" {
|
||||||
|
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||||
|
version = "1.0.20"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
vault_addr = "https://vault.example.com"
|
||||||
|
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||||
|
vault_cli_version = "1.17.5"
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,12 @@
|
|||||||
|
import { describe } from "bun:test";
|
||||||
|
import { runTerraformInit, testRequiredVariables } from "../test";
|
||||||
|
|
||||||
|
describe("vault-jwt", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
vault_addr: "foo",
|
||||||
|
vault_jwt_role: "foo",
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,64 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.12.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add required variables for your modules and remove any unneeded variables
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vault_addr" {
|
||||||
|
type = string
|
||||||
|
description = "The address of the Vault server."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vault_jwt_auth_path" {
|
||||||
|
type = string
|
||||||
|
description = "The path to the Vault JWT auth method."
|
||||||
|
default = "jwt"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vault_jwt_role" {
|
||||||
|
type = string
|
||||||
|
description = "The name of the Vault role to use for authentication."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "vault_cli_version" {
|
||||||
|
type = string
|
||||||
|
description = "The version of Vault to install."
|
||||||
|
default = "latest"
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^(latest|[0-9]+\\.[0-9]+\\.[0-9]+)$", var.vault_cli_version))
|
||||||
|
error_message = "Vault version must be in the format 0.0.0 or latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_script" "vault" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "Vault (GitHub)"
|
||||||
|
icon = "/icon/vault.svg"
|
||||||
|
script = templatefile("${path.module}/run.sh", {
|
||||||
|
CODER_OIDC_ACCESS_TOKEN : data.coder_workspace_owner.me.oidc_access_token,
|
||||||
|
VAULT_JWT_AUTH_PATH : var.vault_jwt_auth_path,
|
||||||
|
VAULT_JWT_ROLE : var.vault_jwt_role,
|
||||||
|
VAULT_CLI_VERSION : var.vault_cli_version,
|
||||||
|
})
|
||||||
|
run_on_start = true
|
||||||
|
start_blocks_login = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_env" "vault_addr" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
name = "VAULT_ADDR"
|
||||||
|
value = var.vault_addr
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_workspace_owner" "me" {}
|
@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Convert all templated variables to shell variables
|
||||||
|
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
|
||||||
|
VAULT_JWT_AUTH_PATH=${VAULT_JWT_AUTH_PATH}
|
||||||
|
VAULT_JWT_ROLE=${VAULT_JWT_ROLE}
|
||||||
|
CODER_OIDC_ACCESS_TOKEN=${CODER_OIDC_ACCESS_TOKEN}
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
dest="$1"
|
||||||
|
url="$2"
|
||||||
|
if command -v curl > /dev/null 2>&1; then
|
||||||
|
curl -sSL --fail "$${url}" -o "$${dest}"
|
||||||
|
elif command -v wget > /dev/null 2>&1; then
|
||||||
|
wget -O "$${dest}" "$${url}"
|
||||||
|
elif command -v busybox > /dev/null 2>&1; then
|
||||||
|
busybox wget -O "$${dest}" "$${url}"
|
||||||
|
else
|
||||||
|
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
unzip_safe() {
|
||||||
|
if command -v unzip > /dev/null 2>&1; then
|
||||||
|
command unzip "$@"
|
||||||
|
elif command -v busybox > /dev/null 2>&1; then
|
||||||
|
busybox unzip "$@"
|
||||||
|
else
|
||||||
|
printf "unzip or busybox is not installed. Please install unzip in your image.\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install() {
|
||||||
|
# Get the architecture of the system
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
if [ "$${ARCH}" = "x86_64" ]; then
|
||||||
|
ARCH="amd64"
|
||||||
|
elif [ "$${ARCH}" = "aarch64" ]; then
|
||||||
|
ARCH="arm64"
|
||||||
|
else
|
||||||
|
printf "Unsupported architecture: $${ARCH}\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# Fetch the latest version of Vault if VAULT_CLI_VERSION is 'latest'
|
||||||
|
if [ "$${VAULT_CLI_VERSION}" = "latest" ]; then
|
||||||
|
LATEST_VERSION=$(curl -s https://releases.hashicorp.com/vault/ | grep -v 'rc' | grep -oE 'vault/[0-9]+\.[0-9]+\.[0-9]+' | sed 's/vault\///' | sort -V | tail -n 1)
|
||||||
|
printf "Latest version of Vault is %s.\n\n" "$${LATEST_VERSION}"
|
||||||
|
if [ -z "$${LATEST_VERSION}" ]; then
|
||||||
|
printf "Failed to determine the latest Vault version.\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
VAULT_CLI_VERSION=$${LATEST_VERSION}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the vault CLI is installed and has the correct version
|
||||||
|
installation_needed=1
|
||||||
|
if command -v vault > /dev/null 2>&1; then
|
||||||
|
CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
if [ "$${CURRENT_VERSION}" = "$${VAULT_CLI_VERSION}" ]; then
|
||||||
|
printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
|
||||||
|
installation_needed=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $${installation_needed} -eq 1 ]; then
|
||||||
|
# Download and install Vault
|
||||||
|
if [ -z "$${CURRENT_VERSION}" ]; then
|
||||||
|
printf "Installing Vault CLI ...\n\n"
|
||||||
|
else
|
||||||
|
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${VAULT_CLI_VERSION}"
|
||||||
|
fi
|
||||||
|
fetch vault.zip "https://releases.hashicorp.com/vault/$${VAULT_CLI_VERSION}/vault_$${VAULT_CLI_VERSION}_linux_$${ARCH}.zip"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
printf "Failed to download Vault.\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! unzip_safe vault.zip; then
|
||||||
|
printf "Failed to unzip Vault.\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm vault.zip
|
||||||
|
if sudo mv vault /usr/local/bin/vault 2> /dev/null; then
|
||||||
|
printf "Vault installed successfully!\n\n"
|
||||||
|
else
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
if ! mv vault ~/.local/bin/vault; then
|
||||||
|
printf "Failed to move Vault to local bin.\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
if ! (
|
||||||
|
cd "$TMP"
|
||||||
|
install
|
||||||
|
); then
|
||||||
|
echo "Failed to install Vault CLI."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -rf "$TMP"
|
||||||
|
|
||||||
|
# Authenticate with Vault
|
||||||
|
printf "🔑 Authenticating with Vault ...\n\n"
|
||||||
|
echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=-
|
||||||
|
printf "🥳 Vault authentication complete!\n\n"
|
||||||
|
printf "You can now use Vault CLI to access secrets.\n"
|
Loading…
Reference in New Issue