diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9ee005c..60a760b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,11 +21,14 @@ jobs: with: bun-version: latest - run: bun test - fmt: + pretty: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v1 with: bun-version: latest - - run: bun fmt:ci + - name: Format + run: bun fmt:ci + - name: Lint + run: bun install && bun lint diff --git a/.images/git-config-params.png b/.images/git-config-params.png new file mode 100644 index 0000000..55f24a7 Binary files /dev/null and b/.images/git-config-params.png differ diff --git a/.images/jupyter-notebook.png b/.images/jupyter-notebook.png new file mode 100644 index 0000000..dad85cc Binary files /dev/null and b/.images/jupyter-notebook.png differ diff --git a/.sample/README.md b/.sample/README.md index ebc3e49..387d45b 100644 --- a/.sample/README.md +++ b/.sample/README.md @@ -11,14 +11,14 @@ tags: [helper] - - ```hcl module "MODULE_NAME" { source = "https://registry.coder.com/modules/MODULE_NAME" } ``` + + ## Examples ### Example 1 diff --git a/.sample/run.sh b/.sample/run.sh index 79eb123..2f8fae1 100755 --- a/.sample/run.sh +++ b/.sample/run.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh BOLD='\033[0;1m' -echo "$${BOLD}Installing MODULE_NAME..." +printf "$${BOLD}Installing MODULE_NAME..." # Add code here # Use varibles from the templatefile function in main.tf # e.g. LOG_PATH, PORT, etc. diff --git a/bun.lockb b/bun.lockb index dfed919..0d30fe9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/code-server/main.tf b/code-server/main.tf index 0a8e4f4..837f0db 100644 --- a/code-server/main.tf +++ b/code-server/main.tf @@ -26,6 +26,12 @@ variable "port" { default = 13337 } +variable "display_name" { + type = string + description = "The display name for the code-server application." + default = "code-server" +} + variable "settings" { type = map(string) description = "A map of settings to apply to code-server." @@ -75,7 +81,7 @@ resource "coder_script" "code-server" { resource "coder_app" "code-server" { agent_id = var.agent_id slug = "code-server" - display_name = "code-server" + display_name = var.display_name url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}" icon = "/icon/code.svg" subdomain = false diff --git a/code-server/run.sh b/code-server/run.sh index 3e1a38b..6676aaa 100755 --- a/code-server/run.sh +++ b/code-server/run.sh @@ -41,9 +41,9 @@ done if [ ! -f ~/.local/share/code-server/Machine/settings.json ]; then echo "⚙️ Creating settings file..." mkdir -p ~/.local/share/code-server/Machine - echo "${SETTINGS}" > ~/.local/share/code-server/Machine/settings.json + echo "${SETTINGS}" >~/.local/share/code-server/Machine/settings.json fi echo "👷 Running code-server in the background..." echo "Check logs at ${LOG_PATH}!" -$CODE_SERVER --auth none --port ${PORT} >${LOG_PATH} 2>&1 & \ No newline at end of file +$CODE_SERVER --auth none --port ${PORT} >${LOG_PATH} 2>&1 & diff --git a/filebrowser/run.sh b/filebrowser/run.sh index 427c864..b13e919 100644 --- a/filebrowser/run.sh +++ b/filebrowser/run.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh BOLD='\033[0;1m' -echo "$${BOLD}Installing filebrowser \n\n" +printf "$${BOLD}Installing filebrowser \n\n" curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash diff --git a/git-clone/main.tf b/git-clone/main.tf index 0e0b23e..1d15dfe 100644 --- a/git-clone/main.tf +++ b/git-clone/main.tf @@ -26,12 +26,13 @@ variable "agent_id" { } resource "coder_script" "git_clone" { - agent_id = var.agent_id - display_name = "Git Clone" - icon = "/icons/git.svg" + agent_id = var.agent_id script = templatefile("${path.module}/run.sh", { CLONE_PATH : var.path != "" ? var.path : join("/", ["~", basename(var.url)]), REPO_URL : var.url, }) - run_on_start = true + display_name = "Git Clone" + icon = "/icons/git.svg" + run_on_start = true + start_blocks_login = true } diff --git a/git-config/README.md b/git-config/README.md new file mode 100644 index 0000000..1666374 --- /dev/null +++ b/git-config/README.md @@ -0,0 +1,48 @@ +--- +display_name: Git Config +description: Stores Git configuration from Coder credentials +icon: ../.icons/git.svg +maintainer_github: coder +verified: true +tags: [helper, git] +--- + +# git-config + +Runs a script that updates git credentials in the workspace to match the user's Coder credentials, optionally allowing to the developer to override the defaults. + +```hcl +module "git-config" { + source = "https://registry.coder.com/modules/git-config" + agent_id = coder_agent.example.id +} +``` + +TODO: Add screenshot + +## Examples + +### Allow users to override both username and email + +```hcl +module "git-config" { + source = "https://registry.coder.com/modules/git-config" + agent_id = coder_agent.example.id + allow_email_change = true +} +``` + +TODO: Add screenshot + +## Disallowing users from overriding both username and email + +```hcl +module "git-config" { + source = "https://registry.coder.com/modules/git-config" + agent_id = coder_agent.example.id + allow_username_change = false + allow_email_change = false +} +``` + +TODO: Add screenshot diff --git a/git-config/main.test.ts b/git-config/main.test.ts new file mode 100644 index 0000000..6fbdbc5 --- /dev/null +++ b/git-config/main.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "bun:test"; +import { + executeScriptInContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("git-config", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("fails without git", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + const output = await executeScriptInContainer(state, "alpine"); + expect(output.exitCode).toBe(1); + expect(output.stdout).toEqual([ + "\u001B[0;1mChecking git-config!", + "Git is not installed!", + ]); + }); + + it("runs with git", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + const output = await executeScriptInContainer(state, "alpine/git"); + expect(output.exitCode).toBe(0); + expect(output.stdout).toEqual([ + "\u001B[0;1mChecking git-config!", + "git-config: No user.email found, setting to ", + "git-config: No user.name found, setting to default", + "", + "\u001B[0;1mgit-config: using email: ", + "\u001B[0;1mgit-config: using username: default", + ]); + }); +}); diff --git a/git-config/main.tf b/git-config/main.tf new file mode 100644 index 0000000..55d9cca --- /dev/null +++ b/git-config/main.tf @@ -0,0 +1,61 @@ +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 "allow_username_change" { + type = bool + description = "Allow developers to change their git username." + default = true +} + +variable "allow_email_change" { + type = bool + description = "Allow developers to change their git email." + default = false +} + + +data "coder_workspace" "me" {} + +data "coder_parameter" "user_email" { + count = var.allow_email_change ? 1 : 0 + name = "user_email" + type = "string" + default = "" + description = "Git user.email to be used for commits. Leave empty to default to Coder username." + display_name = "Git config user.email" + mutable = true +} + +data "coder_parameter" "username" { + count = var.allow_username_change ? 1 : 0 + name = "username" + type = "string" + default = "" + description = "Git user.name to be used for commits. Leave empty to default to Coder username." + display_name = "Git config user.name" + mutable = true +} + +resource "coder_script" "git_config" { + agent_id = var.agent_id + script = templatefile("${path.module}/run.sh", { + GIT_USERNAME = try(data.coder_parameter.username[0].value, "") == "" ? data.coder_workspace.me.owner : try(data.coder_parameter.username[0].value, "") + GIT_EMAIL = try(data.coder_parameter.user_email[0].value, "") == "" ? data.coder_workspace.me.owner_email : try(data.coder_parameter.user_email[0].value, "") + }) + display_name = "Git Config" + icon = "/icon/git.svg" + run_on_start = true +} diff --git a/git-config/run.sh b/git-config/run.sh new file mode 100644 index 0000000..586be4d --- /dev/null +++ b/git-config/run.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env sh + +BOLD='\033[0;1m' +printf "$${BOLD}Checking git-config!\n" + +# Check if git is installed +command -v git >/dev/null 2>&1 || { + echo "Git is not installed!" + exit 1 +} + +# Set git username and email if missing +if [ -z $(git config --get user.email) ]; then + printf "git-config: No user.email found, setting to ${GIT_EMAIL}\n" + git config --global user.email "${GIT_EMAIL}" +fi + +if [ -z $(git config --get user.name) ]; then + printf "git-config: No user.name found, setting to ${GIT_USERNAME}\n" + git config --global user.name "${GIT_USERNAME}" +fi + +printf "\n$${BOLD}git-config: using email: $(git config --get user.email)\n" +printf "$${BOLD}git-config: using username: $(git config --get user.name)\n\n" diff --git a/jfrog/run.sh b/jfrog/run.sh index 7f58114..272eb1d 100644 --- a/jfrog/run.sh +++ b/jfrog/run.sh @@ -1,7 +1,7 @@ #!/usr/bin/env sh BOLD='\033[0;1m' -echo "$${BOLD}Installing JFrog CLI..." +printf "$${BOLD}Installing JFrog CLI..." # Install the JFrog CLI. curl -fL https://install-cli.jfrog.io | sudo sh @@ -20,11 +20,11 @@ if [ -z "${REPOSITORY_NPM}" ]; then else echo "📦 Configuring npm..." jf npmc --global --repo-resolve "${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}" - cat << EOF > ~/.npmrc + cat <~/.npmrc email = ${ARTIFACTORY_USERNAME} registry = ${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM} EOF - jf rt curl /api/npm/auth >> ~/.npmrc + jf rt curl /api/npm/auth >>~/.npmrc fi # Configure the `pip` to use the Artifactory "python" repository. @@ -33,7 +33,7 @@ if [ -z "${REPOSITORY_PYPI}" ]; then else echo "🐍 Configuring pip..." mkdir -p ~/.pip - cat << EOF > ~/.pip/pip.conf + cat <~/.pip/pip.conf [global] index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple EOF @@ -46,4 +46,4 @@ else echo "🐹 Configuring go..." export GOPROXY="https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/go/${REPOSITORY_GO}" fi -echo "🥳 Configuration complete!" \ No newline at end of file +echo "🥳 Configuration complete!" diff --git a/jupyter-notebook/README.md b/jupyter-notebook/README.md new file mode 100644 index 0000000..f3730b6 --- /dev/null +++ b/jupyter-notebook/README.md @@ -0,0 +1,21 @@ +--- +display_name: Jupyter Notebook +description: A module that adds Jupyter Notebook in your Coder template. +icon: ../.icons/jupyter.svg +maintainer_github: coder +verified: true +tags: [jupyter, helper, ide, web] +--- + +# Jupyter Notebook + +A module that adds Jupyter Notebook in your Coder template. + +![Jupyter Notebook](../.images/jupyter-notebook.png) + +```hcl +module "jupyter-notebook" { + source = "https://registry.coder.com/modules/jupyter-notebook" + agent_id = coder_agent.example.id +} +``` diff --git a/jupyter-notebook/main.tf b/jupyter-notebook/main.tf new file mode 100644 index 0000000..fb36b86 --- /dev/null +++ b/jupyter-notebook/main.tf @@ -0,0 +1,49 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.12" + } + } +} + +# Add required variables for your modules and remove any unneeded variables +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "log_path" { + type = string + description = "The path to log jupyter notebook to." + default = "/tmp/jupyter-notebook.log" +} + +variable "port" { + type = number + description = "The port to run jupyter-notebook on." + default = 19999 +} + +resource "coder_script" "jupyter-notebook" { + agent_id = var.agent_id + display_name = "jupyter-notebook" + icon = "/icon/jupyter.svg" + script = templatefile("${path.module}/run.sh", { + LOG_PATH : var.log_path, + PORT : var.port + }) + run_on_start = true +} + +resource "coder_app" "jupyter-notebook" { + agent_id = var.agent_id + slug = "jupyter-notebook" + display_name = "Jupyter Notebook" + url = "http://localhost:${var.port}" + icon = "/icon/jupyter.svg" + subdomain = true + share = "owner" +} diff --git a/jupyter-notebook/run.sh b/jupyter-notebook/run.sh new file mode 100755 index 0000000..1f948ab --- /dev/null +++ b/jupyter-notebook/run.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh + +BOLD='\033[0;1m' + +printf "$${BOLD}Installing jupyter-notebook!\n" + +# check if jupyter-notebook is installed +if ! command -v jupyter-notebook >/dev/null 2>&1; then + # install jupyter-notebook + # check if python3 pip is installed + if ! command -v pip3 >/dev/null 2>&1; then + echo "pip3 is not installed" + echo "Please install pip3 in your Dockerfile/VM image before running this script" + exit 1 + fi + # install jupyter-notebook + pip3 install --upgrade --no-cache-dir --no-warn-script-location jupyter + echo "🥳 jupyter-notebook has been installed\n\n" +else + echo "🥳 jupyter-notebook is already installed\n\n" +fi + +echo "👷 Starting jupyter-notebook in background..." +echo "check logs at ${LOG_PATH}" +$HOME/.local/bin/jupyter notebook --NotebookApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' >${LOG_PATH} 2>&1 & diff --git a/jupyterlab/run.sh b/jupyterlab/run.sh index ec7c28d..8f77cac 100755 --- a/jupyterlab/run.sh +++ b/jupyterlab/run.sh @@ -2,13 +2,13 @@ BOLD='\033[0;1m' -echo "$${BOLD}Installing jupyterlab!\n" +printf "$${BOLD}Installing jupyterlab!\n" # check if jupyterlab is installed -if ! command -v jupyterlab > /dev/null 2>&1; then +if ! command -v jupyterlab >/dev/null 2>&1; then # install jupyterlab # check if python3 pip is installed - if ! command -v pip3 > /dev/null 2>&1; then + if ! command -v pip3 >/dev/null 2>&1; then echo "pip3 is not installed" echo "Please install pip3 in your Dockerfile/VM image before running this script" exit 1 @@ -22,4 +22,4 @@ fi echo "👷 Starting jupyterlab in background..." echo "check logs at ${LOG_PATH}" -$HOME/.local/bin/jupyter lab --ServerApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' >${LOG_PATH} 2>&1 & +$HOME/.local/bin/jupyter lab --ServerApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' >${LOG_PATH} 2>&1 & diff --git a/lint.ts b/lint.ts new file mode 100644 index 0000000..12e733c --- /dev/null +++ b/lint.ts @@ -0,0 +1,96 @@ +import { readFile, readdir, stat } from "fs/promises"; +import * as path from "path"; +import * as marked from "marked"; +import grayMatter from "gray-matter"; + +const files = await readdir(".", { withFileTypes: true }); +const dirs = files.filter( + (f) => f.isDirectory() && !f.name.startsWith(".") && f.name !== "node_modules" +); + +let badExit = false; + +// error reports an error to the console and sets badExit to true +// so that the process will exit with a non-zero exit code. +const error = (...data: any[]) => { + console.error(...data); + badExit = true; +} + +// Ensures that each README has the proper format. +// Exits with 0 if all is good! +for (const dir of dirs) { + const readme = path.join(dir.name, "README.md"); + // Ensure exists + try { + await stat(readme); + } catch (ex) { + throw new Error(`Missing README.md in ${dir.name}`); + } + const content = await readFile(readme, "utf8"); + const matter = grayMatter(content); + const data = matter.data as { + display_name?: string; + description?: string; + icon?: string; + maintainer_github?: string; + partner_github?: string; + verified?: boolean; + tags?: string[]; + }; + if (!data.display_name) { + error(dir.name, "missing display_name"); + } + if (!data.description) { + error(dir.name, "missing description"); + } + if (!data.icon) { + error(dir.name, "missing icon"); + } + if (!data.maintainer_github) { + error(dir.name, "missing maintainer_github"); + } + try { + await stat(path.join(".", dir.name, data.icon)); + } catch (ex) { + error(dir.name, "icon does not exist", data.icon); + } + + const tokens = marked.lexer(content); + // Ensure there is an h1 and some text, then a code block + + let h1 = false; + let code = false; + let paragraph = false; + + for (const token of tokens) { + if (token.type === "heading" && token.depth === 1) { + h1 = true; + continue; + } + if (h1 && token.type === "heading") { + break; + } + if (token.type === "paragraph") { + paragraph = true; + continue; + } + if (token.type === "code") { + code = true; + continue; + } + } + if (!h1) { + error(dir.name, "missing h1"); + } + if (!paragraph) { + error(dir.name, "missing paragraph after h1"); + } + if (!code) { + error(dir.name, "missing example code block after paragraph"); + } +} + +if (badExit) { + process.exit(1); +} diff --git a/package.json b/package.json index c42b502..c9d3a81 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,13 @@ "scripts": { "test": "bun test", "fmt": "bun x prettier -w **/*.ts **/*.md *.md && terraform fmt **/*.tf", - "fmt:ci": "bun x prettier --check **/*.ts **/*.md *.md && terraform fmt -check **/*.tf" + "fmt:ci": "bun x prettier --check **/*.ts **/*.md *.md && terraform fmt -check **/*.tf", + "lint": "bun run lint.ts" }, "devDependencies": { - "bun-types": "^1.0.3" + "bun-types": "^1.0.3", + "gray-matter": "^4.0.3", + "marked": "^9.0.3" }, "peerDependencies": { "typescript": "^5.0.0" diff --git a/personalize/main.tf b/personalize/main.tf index 7235530..9de4b78 100644 --- a/personalize/main.tf +++ b/personalize/main.tf @@ -31,8 +31,9 @@ resource "coder_script" "personalize" { script = templatefile("${path.module}/run.sh", { PERSONALIZE_PATH : var.path, }) - display_name = "Personalize" - icon = "/icon/personalize.svg" - log_path = var.log_path - run_on_start = true + display_name = "Personalize" + icon = "/icon/personalize.svg" + log_path = var.log_path + run_on_start = true + start_blocks_login = true } diff --git a/tsconfig.json b/tsconfig.json index 86140a5..e7b89cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "target": "esnext", "module": "esnext", + "allowSyntheticDefaultImports": true, + "moduleResolution": "nodenext", "types": ["bun-types"] } }