Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd6747f9bc | ||
|
|
fb81c8969f | ||
|
|
162808760d | ||
|
|
ad1189afff | ||
|
|
94e126f248 | ||
|
|
04535a9cd7 | ||
|
|
7a9f553564 | ||
|
|
e11b19d33e | ||
|
|
93c4fb3a8d | ||
|
|
86038f8d37 | ||
|
|
120a0e342e | ||
|
|
b51932d7ac | ||
|
|
834ffde032 | ||
|
|
831f64da56 | ||
|
|
236022f870 | ||
|
|
4c45d69994 | ||
|
|
310d0262bd | ||
|
|
f446fbd667 | ||
|
|
982c75e86f | ||
|
|
523ad9fe23 | ||
|
|
096cd214ce | ||
|
|
6a87fd18e5 | ||
|
|
fa4b84e8d1 | ||
|
|
7e0eacf1f4 | ||
|
|
cbe48aa072 | ||
|
|
89bb023fa5 | ||
|
|
66472b0105 | ||
|
|
cd010baac8 | ||
|
|
f7fa145855 | ||
|
|
f7f9c8b7ef | ||
|
|
889186d553 | ||
|
|
352577b833 | ||
|
|
4e59ecc606 | ||
|
|
a40f2b86c3 | ||
|
|
a2c29ace0a | ||
|
|
da4a561cb5 | ||
|
|
d77ad8ac63 | ||
|
|
b1f81afa7f | ||
|
|
883741244b |
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
19
.github/workflows/ci.yaml
vendored
19
.github/workflows/ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: oven-sh/setup-bun@v1
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
- name: Setup
|
- name: Setup
|
||||||
@@ -27,7 +27,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: oven-sh/setup-bun@v1
|
with:
|
||||||
|
fetch-depth: 0 # Needed to get tags
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
- name: Setup
|
- name: Setup
|
||||||
@@ -38,3 +40,16 @@ jobs:
|
|||||||
uses: crate-ci/typos@v1.17.2
|
uses: crate-ci/typos@v1.17.2
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: bun lint
|
run: bun lint
|
||||||
|
- name: Check version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# check for version changes
|
||||||
|
./update-version.sh
|
||||||
|
# Check if any changes were made in README.md files
|
||||||
|
if [[ -n "$(git status --porcelain -- '**/README.md')" ]]; then
|
||||||
|
echo "Version mismatch detected. Please run ./update-version.sh and commit the updated README.md files."
|
||||||
|
git diff -- '**/README.md'
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "No version mismatch detected. All versions are up to date."
|
||||||
|
fi
|
||||||
|
|||||||
42
.github/workflows/update-readme.yaml
vendored
42
.github/workflows/update-readme.yaml
vendored
@@ -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'
|
|
||||||
1
.icons/cursor.svg
Normal file
1
.icons/cursor.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 1.5 MiB |
@@ -1,24 +1,47 @@
|
|||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
To create a new module, clone this repository and run:
|
## Getting started
|
||||||
|
|
||||||
|
This repo uses the [Bun runtime](https://bun.sh/) to to run all code and tests. To install Bun, you can run this command on Linux/MacOS:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./new.sh MODULE_NAME
|
curl -fsSL https://bun.sh/install | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Or this command on Windows:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the instructions to ensure that Bun is available globally. Once Bun has been installed, clone this repository. From there, run this script to create a new module:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./new.sh NAME_OF_NEW_MODULE
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing a Module
|
## Testing a Module
|
||||||
|
|
||||||
|
> **Note:** It is the responsibility of the module author to implement tests for their module. The author must test the module locally before submitting a PR.
|
||||||
|
|
||||||
A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers.
|
A suite of test-helpers exists to run `terraform apply` on modules with variables, and test script output against containers.
|
||||||
|
|
||||||
The testing suite must be able to run docker containers with the `--network=host` flag, which typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS and Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop.
|
The testing suite must be able to run docker containers with the `--network=host` flag. This typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS and Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop.
|
||||||
|
|
||||||
Reference existing `*.test.ts` files for implementation.
|
Reference the existing `*.test.ts` files to get an idea for how to set up tests.
|
||||||
|
|
||||||
|
You can run all tests in a specific file with this command:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Run tests for a specific module!
|
|
||||||
$ bun test -t '<module>'
|
$ bun test -t '<module>'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or run all tests by running this command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ bun test
|
||||||
|
```
|
||||||
|
|
||||||
You can test a module locally by updating the source as follows
|
You can test a module locally by updating the source as follows
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
@@ -27,4 +50,25 @@ module "example" {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR.
|
## Releases
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> When creating a new release, make sure that your new version number is fully accurate. If a version number is incorrect or does not exist, we may end up serving incorrect/old data for our various tools and providers.
|
||||||
|
|
||||||
|
Much of our release process is automated. To cut a new release:
|
||||||
|
|
||||||
|
1. Navigate to [GitHub's Releases page](https://github.com/coder/modules/releases)
|
||||||
|
2. Click "Draft a new release"
|
||||||
|
3. Click the "Choose a tag" button and type a new release number in the format `v<major>.<minor>.<patch>` (e.g., `v1.18.0`). Then click "Create new tag".
|
||||||
|
4. Click the "Generate release notes" button, and clean up the resulting README. Be sure to remove any notes that would not be relevant to end-users (e.g., bumping dependencies).
|
||||||
|
5. Once everything looks good, click the "Publish release" button.
|
||||||
|
|
||||||
|
Once the release has been cut, a script will run to check whether there are any modules that will require that the new release number be published to Terraform. If there are any, a new pull request will automatically be generated. Be sure to approve this PR and merge it into the `main` branch.
|
||||||
|
|
||||||
|
Following that, our automated processes will handle publishing new data for [`registry.coder.com`](https://github.com/coder/registry.coder.com/):
|
||||||
|
|
||||||
|
1. Publishing new versions to Coder's [Terraform Registry](https://registry.terraform.io/providers/coder/coder/latest)
|
||||||
|
2. Publishing new data to the [Coder Registry](https://registry.coder.com)
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Some data in `registry.coder.com` is fetched on demand from the Module repo's main branch. This data should be updated almost immediately after a new release, but other changes will take some time to propagate.
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
Modules
|
Modules
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
[Registry](https://registry.coder.com) | [Coder Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Coder Enterprise](https://coder.com/docs/v2/latest/enterprise)
|
[Module Registry](https://registry.coder.com) | [Coder Docs](https://coder.com/docs) | [Why Coder](https://coder.com/why) | [Coder Enterprise](https://coder.com/docs/v2/latest/enterprise)
|
||||||
|
|
||||||
[](https://discord.gg/coder)
|
[](https://discord.gg/coder)
|
||||||
[](./LICENSE)
|
[](./LICENSE)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Modules extend Templates to create reusable components for your development environment.
|
Modules extend Coder Templates to create reusable components for your development environment.
|
||||||
|
|
||||||
e.g.
|
e.g.
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
|
|||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -28,7 +28,7 @@ module "code-server" {
|
|||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
install_version = "4.8.3"
|
install_version = "4.8.3"
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
|||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
extensions = [
|
extensions = [
|
||||||
"dracula-theme.theme-dracula"
|
"dracula-theme.theme-dracula"
|
||||||
@@ -58,7 +58,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
|||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
extensions = ["dracula-theme.theme-dracula"]
|
extensions = ["dracula-theme.theme-dracula"]
|
||||||
settings = {
|
settings = {
|
||||||
@@ -74,7 +74,7 @@ Just run code-server in the background, don't fetch it from GitHub:
|
|||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
|
|||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
use_cached = true
|
use_cached = true
|
||||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||||
@@ -101,7 +101,7 @@ Just run code-server in the background, don't fetch it from GitHub:
|
|||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
offline = true
|
offline = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,15 @@ variable "auto_install_extensions" {
|
|||||||
default = false
|
default = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "subdomain" {
|
||||||
|
type = bool
|
||||||
|
description = <<-EOT
|
||||||
|
Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder.
|
||||||
|
If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible.
|
||||||
|
EOT
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
resource "coder_script" "code-server" {
|
resource "coder_script" "code-server" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "code-server"
|
display_name = "code-server"
|
||||||
@@ -154,7 +163,7 @@ resource "coder_app" "code-server" {
|
|||||||
display_name = var.display_name
|
display_name = var.display_name
|
||||||
url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}"
|
url = "http://localhost:${var.port}/${var.folder != "" ? "?folder=${urlencode(var.folder)}" : ""}"
|
||||||
icon = "/icon/code.svg"
|
icon = "/icon/code.svg"
|
||||||
subdomain = false
|
subdomain = var.subdomain
|
||||||
share = var.share
|
share = var.share
|
||||||
order = var.order
|
order = var.order
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ CODE_SERVER="${INSTALL_PREFIX}/bin/code-server"
|
|||||||
EXTENSION_ARG=""
|
EXTENSION_ARG=""
|
||||||
if [ -n "${EXTENSIONS_DIR}" ]; then
|
if [ -n "${EXTENSIONS_DIR}" ]; then
|
||||||
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
|
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
|
||||||
|
mkdir -p "${EXTENSIONS_DIR}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
function run_code_server() {
|
function run_code_server() {
|
||||||
|
|||||||
35
cursor/README.md
Normal file
35
cursor/README.md
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
```
|
||||||
89
cursor/main.test.ts
Normal file
89
cursor/main.test.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
executeScriptInContainer,
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
62
cursor/main.tf
Normal file
62
cursor/main.tf
Normal file
@@ -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."
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
|
|||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -30,7 +30,7 @@ module "dotfiles" {
|
|||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -40,7 +40,7 @@ module "dotfiles" {
|
|||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
user = "root"
|
user = "root"
|
||||||
}
|
}
|
||||||
@@ -51,13 +51,13 @@ module "dotfiles" {
|
|||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
|
|
||||||
module "dotfiles-root" {
|
module "dotfiles-root" {
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
user = "root"
|
user = "root"
|
||||||
dotfiles_uri = module.dotfiles.dotfiles_uri
|
dotfiles_uri = module.dotfiles.dotfiles_uri
|
||||||
@@ -71,7 +71,7 @@ You can set a default dotfiles repository for all users by setting the `default_
|
|||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,14 @@ variable "coder_parameter_order" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
data "coder_parameter" "dotfiles_uri" {
|
variable "manual_update" {
|
||||||
count = var.dotfiles_uri == null ? 1 : 0
|
type = bool
|
||||||
|
description = "If true, this adds a button to workspace page to refresh dotfiles on demand."
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_parameter" "dotfiles_uri" {
|
||||||
|
count = var.dotfiles_uri == null ? 1 : 0
|
||||||
type = "string"
|
type = "string"
|
||||||
name = "dotfiles_uri"
|
name = "dotfiles_uri"
|
||||||
display_name = "Dotfiles URL"
|
display_name = "Dotfiles URL"
|
||||||
@@ -68,6 +73,18 @@ resource "coder_script" "dotfiles" {
|
|||||||
run_on_start = true
|
run_on_start = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "coder_app" "dotfiles" {
|
||||||
|
count = var.manual_update ? 1 : 0
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "Refresh Dotfiles"
|
||||||
|
slug = "dotfiles"
|
||||||
|
icon = "/icon/dotfiles.svg"
|
||||||
|
command = templatefile("${path.module}/run.sh", {
|
||||||
|
DOTFILES_URI : local.dotfiles_uri,
|
||||||
|
DOTFILES_USER : local.user
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
output "dotfiles_uri" {
|
output "dotfiles_uri" {
|
||||||
description = "Dotfiles URI"
|
description = "Dotfiles URI"
|
||||||
value = local.dotfiles_uri
|
value = local.dotfiles_uri
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ A file browser for your workspace.
|
|||||||
```tf
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -28,7 +28,7 @@ module "filebrowser" {
|
|||||||
```tf
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
folder = "/home/coder/project"
|
folder = "/home/coder/project"
|
||||||
}
|
}
|
||||||
@@ -39,8 +39,19 @@ module "filebrowser" {
|
|||||||
```tf
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
database_path = ".config/filebrowser.db"
|
database_path = ".config/filebrowser.db"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Serve from the same domain (no subdomain)
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "filebrowser" {
|
||||||
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
agent_name = "main"
|
||||||
|
subdomain = false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -88,4 +88,27 @@ describe("filebrowser", async () => {
|
|||||||
"📝 Logs at /tmp/filebrowser.log",
|
"📝 Logs at /tmp/filebrowser.log",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("runs with subdomain=false", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
agent_name: "main",
|
||||||
|
subdomain: false,
|
||||||
|
});
|
||||||
|
const output = await executeScriptInContainer(state, "alpine");
|
||||||
|
expect(output.exitCode).toBe(0);
|
||||||
|
expect(output.stdout).toEqual([
|
||||||
|
"\u001B[0;1mInstalling filebrowser ",
|
||||||
|
"",
|
||||||
|
"🥳 Installation complete! ",
|
||||||
|
"",
|
||||||
|
"👷 Starting filebrowser in background... ",
|
||||||
|
"",
|
||||||
|
"📂 Serving /root at http://localhost:13339 ",
|
||||||
|
"",
|
||||||
|
"Running 'filebrowser --noauth --root /root --port 13339' ",
|
||||||
|
"",
|
||||||
|
"📝 Logs at /tmp/filebrowser.log",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,21 @@ variable "agent_id" {
|
|||||||
description = "The ID of a Coder agent."
|
description = "The ID of a Coder agent."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data "coder_workspace" "me" {}
|
||||||
|
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
|
variable "agent_name" {
|
||||||
|
type = string
|
||||||
|
description = "The name of the main deployment. (Used to build the subpath for coder_app.)"
|
||||||
|
default = ""
|
||||||
|
validation {
|
||||||
|
# If subdomain is false, then agent_name must be set.
|
||||||
|
condition = var.subdomain || var.agent_name != ""
|
||||||
|
error_message = "The agent_name must be set."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
variable "database_path" {
|
variable "database_path" {
|
||||||
type = string
|
type = string
|
||||||
description = "The path to the filebrowser database."
|
description = "The path to the filebrowser database."
|
||||||
@@ -58,6 +73,15 @@ variable "order" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "subdomain" {
|
||||||
|
type = bool
|
||||||
|
description = <<-EOT
|
||||||
|
Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder.
|
||||||
|
If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible.
|
||||||
|
EOT
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
resource "coder_script" "filebrowser" {
|
resource "coder_script" "filebrowser" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "File Browser"
|
display_name = "File Browser"
|
||||||
@@ -67,7 +91,9 @@ resource "coder_script" "filebrowser" {
|
|||||||
PORT : var.port,
|
PORT : var.port,
|
||||||
FOLDER : var.folder,
|
FOLDER : var.folder,
|
||||||
LOG_PATH : var.log_path,
|
LOG_PATH : var.log_path,
|
||||||
DB_PATH : var.database_path
|
DB_PATH : var.database_path,
|
||||||
|
SUBDOMAIN : var.subdomain,
|
||||||
|
SERVER_BASE_PATH : var.subdomain ? "" : format("/@%s/%s.%s/apps/filebrowser", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name),
|
||||||
})
|
})
|
||||||
run_on_start = true
|
run_on_start = true
|
||||||
}
|
}
|
||||||
@@ -78,7 +104,7 @@ resource "coder_app" "filebrowser" {
|
|||||||
display_name = "File Browser"
|
display_name = "File Browser"
|
||||||
url = "http://localhost:${var.port}"
|
url = "http://localhost:${var.port}"
|
||||||
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
|
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
|
||||||
subdomain = true
|
subdomain = var.subdomain
|
||||||
share = var.share
|
share = var.share
|
||||||
order = var.order
|
order = var.order
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ if [ "${DB_PATH}" != "filebrowser.db" ]; then
|
|||||||
DB_FLAG=" -d ${DB_PATH}"
|
DB_FLAG=" -d ${DB_PATH}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# set baseurl to be able to run if sudomain=false; if subdomain=true the SERVER_BASE_PATH value will be ""
|
||||||
|
filebrowser config set --baseurl "${SERVER_BASE_PATH}" > ${LOG_PATH} 2>&1
|
||||||
|
|
||||||
printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n"
|
printf "📂 Serving $${ROOT_DIR} at http://localhost:${PORT} \n\n"
|
||||||
|
|
||||||
printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG}' \n\n"
|
printf "Running 'filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG}' \n\n"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
|
|||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
source = "registry.coder.com/modules/git-clone/coder"
|
source = "registry.coder.com/modules/git-clone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
url = "https://github.com/coder/coder"
|
url = "https://github.com/coder/coder"
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ module "git-clone" {
|
|||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
source = "registry.coder.com/modules/git-clone/coder"
|
source = "registry.coder.com/modules/git-clone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
url = "https://github.com/coder/coder"
|
url = "https://github.com/coder/coder"
|
||||||
base_dir = "~/projects/coder"
|
base_dir = "~/projects/coder"
|
||||||
@@ -41,7 +41,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
|
|||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
source = "registry.coder.com/modules/git-clone/coder"
|
source = "registry.coder.com/modules/git-clone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
url = "https://github.com/coder/coder"
|
url = "https://github.com/coder/coder"
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ data "coder_parameter" "git_repo" {
|
|||||||
# Clone the repository for branch `feat/example`
|
# Clone the repository for branch `feat/example`
|
||||||
module "git_clone" {
|
module "git_clone" {
|
||||||
source = "registry.coder.com/modules/git-clone/coder"
|
source = "registry.coder.com/modules/git-clone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
url = data.coder_parameter.git_repo.value
|
url = data.coder_parameter.git_repo.value
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ module "git_clone" {
|
|||||||
# Create a code-server instance for the cloned repository
|
# Create a code-server instance for the cloned repository
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
order = 1
|
order = 1
|
||||||
folder = "/home/${local.username}/${module.git_clone.folder_name}"
|
folder = "/home/${local.username}/${module.git_clone.folder_name}"
|
||||||
@@ -98,7 +98,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
|
|||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
source = "registry.coder.com/modules/git-clone/coder"
|
source = "registry.coder.com/modules/git-clone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
url = "https://github.example.com/coder/coder/tree/feat/example"
|
url = "https://github.example.com/coder/coder/tree/feat/example"
|
||||||
git_providers = {
|
git_providers = {
|
||||||
@@ -116,7 +116,7 @@ To GitLab clone with a specific branch like `feat/example`
|
|||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
source = "registry.coder.com/modules/git-clone/coder"
|
source = "registry.coder.com/modules/git-clone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
|
|||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
source = "registry.coder.com/modules/git-clone/coder"
|
source = "registry.coder.com/modules/git-clone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
||||||
git_providers = {
|
git_providers = {
|
||||||
@@ -147,9 +147,26 @@ For example, to clone the `feat/example` branch:
|
|||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
source = "registry.coder.com/modules/git-clone/coder"
|
source = "registry.coder.com/modules/git-clone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.18"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
url = "https://github.com/coder/coder"
|
url = "https://github.com/coder/coder"
|
||||||
branch_name = "feat/example"
|
branch_name = "feat/example"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Git clone with different destination folder
|
||||||
|
|
||||||
|
By default, the repository will be cloned into a folder matching the repository name. You can use the `folder_name` attribute to change the name of the destination folder to something else.
|
||||||
|
|
||||||
|
For example, this will clone into the `~/projects/coder/coder-dev` folder:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "git-clone" {
|
||||||
|
source = "registry.coder.com/modules/git-clone/coder"
|
||||||
|
version = "1.0.18"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
url = "https://github.com/coder/coder"
|
||||||
|
folder_name = "coder-dev"
|
||||||
|
base_dir = "~/projects/coder"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -79,6 +79,22 @@ describe("git-clone", async () => {
|
|||||||
expect(state.outputs.branch_name.value).toEqual("");
|
expect(state.outputs.branch_name.value).toEqual("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("repo_dir should match base_dir/folder_name", async () => {
|
||||||
|
const url = "git@github.com:coder/coder.git";
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
base_dir: "/tmp",
|
||||||
|
folder_name: "foo",
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
expect(state.outputs.repo_dir.value).toEqual("/tmp/foo");
|
||||||
|
expect(state.outputs.folder_name.value).toEqual("foo");
|
||||||
|
expect(state.outputs.clone_url.value).toEqual(url);
|
||||||
|
const https_url = "https://github.com/coder/coder.git";
|
||||||
|
expect(state.outputs.web_url.value).toEqual(https_url);
|
||||||
|
expect(state.outputs.branch_name.value).toEqual("");
|
||||||
|
});
|
||||||
|
|
||||||
it("branch_name should not include query string", async () => {
|
it("branch_name should not include query string", async () => {
|
||||||
const state = await runTerraformApply(import.meta.dir, {
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
agent_id: "foo",
|
agent_id: "foo",
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ variable "branch_name" {
|
|||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "folder_name" {
|
||||||
|
description = "The destination folder to clone the repository into."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
# Remove query parameters and fragments from the URL
|
# Remove query parameters and fragments from the URL
|
||||||
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
|
url = replace(replace(var.url, "/\\?.*/", ""), "/#.*/", "")
|
||||||
@@ -64,7 +70,7 @@ locals {
|
|||||||
# Extract the branch name from the URL
|
# Extract the branch name from the URL
|
||||||
branch_name = var.branch_name == "" && local.tree_path != "" ? replace(replace(local.url, local.clone_url, ""), "/.*${local.tree_path}/", "") : var.branch_name
|
branch_name = var.branch_name == "" && local.tree_path != "" ? replace(replace(local.url, local.clone_url, ""), "/.*${local.tree_path}/", "") : var.branch_name
|
||||||
# Extract the folder name from the URL
|
# Extract the folder name from the URL
|
||||||
folder_name = replace(basename(local.clone_url), ".git", "")
|
folder_name = var.folder_name == "" ? replace(basename(local.clone_url), ".git", "") : var.folder_name
|
||||||
# Construct the path to clone the repository
|
# Construct the path to clone the repository
|
||||||
clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name])
|
clone_path = var.base_dir != "" ? join("/", [var.base_dir, local.folder_name]) : join("/", ["~", local.folder_name])
|
||||||
# Construct the web URL
|
# Construct the web URL
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
display_name: Git commit signing
|
display_name: Git commit signing
|
||||||
description: Configures Git to sign commits using your Coder SSH key
|
description: Configures Git to sign commits using your Coder SSH key
|
||||||
icon: ../.icons/git.svg
|
icon: ../.icons/git.svg
|
||||||
maintainer_github: phorcys420
|
maintainer_github: coder
|
||||||
verified: false
|
verified: true
|
||||||
tags: [helper, git]
|
tags: [helper, git]
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
5
jfrog-oauth/.npmrc.tftpl
Normal file
5
jfrog-oauth/.npmrc.tftpl
Normal file
@@ -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 ~}
|
||||||
@@ -17,15 +17,16 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
|||||||
```tf
|
```tf
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
jfrog_url = "https://example.jfrog.io"
|
jfrog_url = "https://example.jfrog.io"
|
||||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||||
|
|
||||||
package_managers = {
|
package_managers = {
|
||||||
"npm" : "npm",
|
npm = ["npm", "@scoped:npm-scoped"]
|
||||||
"go" : "go",
|
go = ["go", "another-go-repo"]
|
||||||
"pypi" : "pypi"
|
pypi = ["pypi", "extra-index-pypi"]
|
||||||
|
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -44,13 +45,13 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
|
|||||||
```tf
|
```tf
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
jfrog_url = "https://example.jfrog.io"
|
jfrog_url = "https://example.jfrog.io"
|
||||||
username_field = "email"
|
username_field = "email"
|
||||||
|
|
||||||
package_managers = {
|
package_managers = {
|
||||||
"pypi" : "pypi"
|
pypi = ["pypi"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -72,15 +73,15 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
|
|||||||
```tf
|
```tf
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
jfrog_url = "https://example.jfrog.io"
|
jfrog_url = "https://example.jfrog.io"
|
||||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||||
configure_code_server = true # Add JFrog extension configuration for code-server
|
configure_code_server = true # Add JFrog extension configuration for code-server
|
||||||
package_managers = {
|
package_managers = {
|
||||||
"npm" : "npm",
|
npm = ["npm"]
|
||||||
"go" : "go",
|
go = ["go"]
|
||||||
"pypi" : "pypi"
|
pypi = ["pypi"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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";
|
||||||
agent_id: "some-agent-id",
|
const fakeFrogUrl = "http://localhost:8081";
|
||||||
jfrog_url: "http://localhost:8081",
|
const user = "default";
|
||||||
package_managers: "{}",
|
|
||||||
|
it("can run apply with required variables", async () => {
|
||||||
|
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "some-agent-id",
|
||||||
|
jfrog_url: fakeFrogUrl,
|
||||||
|
package_managers: "{}",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO add more tests
|
|
||||||
|
|||||||
@@ -53,23 +53,51 @@ variable "configure_code_server" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "package_managers" {
|
variable "package_managers" {
|
||||||
type = map(string)
|
type = object({
|
||||||
description = <<EOF
|
npm = optional(list(string), [])
|
||||||
A map of package manager names to their respective artifactory repositories.
|
go = optional(list(string), [])
|
||||||
For example:
|
pypi = optional(list(string), [])
|
||||||
{
|
docker = optional(list(string), [])
|
||||||
"npm": "YOUR_NPM_REPO_KEY",
|
})
|
||||||
"go": "YOUR_GO_REPO_KEY",
|
description = <<-EOF
|
||||||
"pypi": "YOUR_PYPI_REPO_KEY",
|
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
|
||||||
"docker": "YOUR_DOCKER_REPO_KEY"
|
For example:
|
||||||
}
|
{
|
||||||
EOF
|
npm = ["GLOBAL_NPM_REPO_KEY", "@SCOPED:NPM_REPO_KEY"]
|
||||||
|
go = ["YOUR_GO_REPO_KEY", "ANOTHER_GO_REPO_KEY"]
|
||||||
|
pypi = ["YOUR_PYPI_REPO_KEY", "ANOTHER_PYPI_REPO_KEY"]
|
||||||
|
docker = ["YOUR_DOCKER_REPO_KEY", "ANOTHER_DOCKER_REPO_KEY"]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
# The username field to use for artifactory
|
# The username field to use for artifactory
|
||||||
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name
|
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name
|
||||||
jfrog_host = replace(var.jfrog_url, "https://", "")
|
jfrog_host = split("://", var.jfrog_url)[1]
|
||||||
|
common_values = {
|
||||||
|
JFROG_URL = var.jfrog_url
|
||||||
|
JFROG_HOST = local.jfrog_host
|
||||||
|
JFROG_SERVER_ID = var.jfrog_server_id
|
||||||
|
ARTIFACTORY_USERNAME = local.username
|
||||||
|
ARTIFACTORY_EMAIL = data.coder_workspace_owner.me.email
|
||||||
|
ARTIFACTORY_ACCESS_TOKEN = data.coder_external_auth.jfrog.access_token
|
||||||
|
}
|
||||||
|
npmrc = templatefile(
|
||||||
|
"${path.module}/.npmrc.tftpl",
|
||||||
|
merge(
|
||||||
|
local.common_values,
|
||||||
|
{
|
||||||
|
REPOS = [
|
||||||
|
for r in var.package_managers.npm :
|
||||||
|
strcontains(r, ":") ? zipmap(["SCOPE", "NAME"], ["${split(":", r)[0]}:", split(":", r)[1]]) : { SCOPE = "", NAME = r }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pip_conf = templatefile(
|
||||||
|
"${path.module}/pip.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.pypi })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data "coder_workspace" "me" {}
|
data "coder_workspace" "me" {}
|
||||||
@@ -83,19 +111,22 @@ resource "coder_script" "jfrog" {
|
|||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "jfrog"
|
display_name = "jfrog"
|
||||||
icon = "/icon/jfrog.svg"
|
icon = "/icon/jfrog.svg"
|
||||||
script = templatefile("${path.module}/run.sh", {
|
script = templatefile("${path.module}/run.sh", merge(
|
||||||
JFROG_URL : var.jfrog_url,
|
local.common_values,
|
||||||
JFROG_HOST : local.jfrog_host,
|
{
|
||||||
JFROG_SERVER_ID : var.jfrog_server_id,
|
CONFIGURE_CODE_SERVER = var.configure_code_server
|
||||||
ARTIFACTORY_USERNAME : local.username,
|
HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES"
|
||||||
ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.email,
|
NPMRC = local.npmrc
|
||||||
ARTIFACTORY_ACCESS_TOKEN : data.coder_external_auth.jfrog.access_token,
|
REPOSITORY_NPM = try(element(var.package_managers.npm, 0), "")
|
||||||
CONFIGURE_CODE_SERVER : var.configure_code_server,
|
HAS_GO = length(var.package_managers.go) == 0 ? "" : "YES"
|
||||||
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),
|
REPOSITORY_GO = try(element(var.package_managers.go, 0), "")
|
||||||
REPOSITORY_GO : lookup(var.package_managers, "go", ""),
|
HAS_PYPI = length(var.package_managers.pypi) == 0 ? "" : "YES"
|
||||||
REPOSITORY_PYPI : lookup(var.package_managers, "pypi", ""),
|
PIP_CONF = local.pip_conf
|
||||||
REPOSITORY_DOCKER : lookup(var.package_managers, "docker", ""),
|
REPOSITORY_PYPI = try(element(var.package_managers.pypi, 0), "")
|
||||||
})
|
HAS_DOCKER = length(var.package_managers.docker) == 0 ? "" : "YES"
|
||||||
|
REGISTER_DOCKER = join("\n", formatlist("register_docker \"%s\"", var.package_managers.docker))
|
||||||
|
}
|
||||||
|
))
|
||||||
run_on_start = true
|
run_on_start = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,10 +152,13 @@ resource "coder_env" "jfrog_ide_store_connection" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "coder_env" "goproxy" {
|
resource "coder_env" "goproxy" {
|
||||||
count = lookup(var.package_managers, "go", "") == "" ? 0 : 1
|
count = length(var.package_managers.go) == 0 ? 0 : 1
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
name = "GOPROXY"
|
name = "GOPROXY"
|
||||||
value = "https://${local.username}:${data.coder_external_auth.jfrog.access_token}@${local.jfrog_host}/artifactory/api/go/${lookup(var.package_managers, "go", "")}"
|
value = join(",", [
|
||||||
|
for repo in var.package_managers.go :
|
||||||
|
"https://${local.username}:${data.coder_external_auth.jfrog.access_token}@${local.jfrog_host}/artifactory/api/go/${repo}"
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
output "access_token" {
|
output "access_token" {
|
||||||
|
|||||||
6
jfrog-oauth/pip.conf.tftpl
Normal file
6
jfrog-oauth/pip.conf.tftpl
Normal file
@@ -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 ~}
|
||||||
@@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
BOLD='\033[0;1m'
|
BOLD='\033[0;1m'
|
||||||
|
|
||||||
|
not_configured() {
|
||||||
|
type=$1
|
||||||
|
echo "🤔 no $type repository is set, skipping $type configuration."
|
||||||
|
echo "You can configure a $type repository by providing a key for '$type' in the 'package_managers' input."
|
||||||
|
}
|
||||||
|
|
||||||
|
config_complete() {
|
||||||
|
echo "🥳 Configuration complete!"
|
||||||
|
}
|
||||||
|
|
||||||
|
register_docker() {
|
||||||
|
repo=$1
|
||||||
|
echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login "$repo" --username ${ARTIFACTORY_USERNAME} --password-stdin
|
||||||
|
}
|
||||||
|
|
||||||
# check if JFrog CLI is already installed
|
# check if JFrog CLI is already installed
|
||||||
if command -v jf > /dev/null 2>&1; then
|
if command -v jf > /dev/null 2>&1; then
|
||||||
echo "✅ JFrog CLI is already installed, skipping installation."
|
echo "✅ JFrog CLI is already installed, skipping installation."
|
||||||
@@ -20,52 +35,47 @@ echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFRO
|
|||||||
jf c use "${JFROG_SERVER_ID}"
|
jf c use "${JFROG_SERVER_ID}"
|
||||||
|
|
||||||
# Configure npm to use the Artifactory "npm" repository.
|
# Configure npm to use the Artifactory "npm" repository.
|
||||||
if [ -z "${REPOSITORY_NPM}" ]; then
|
if [ -z "${HAS_NPM}" ]; then
|
||||||
echo "🤔 no npm repository is set, skipping npm configuration."
|
not_configured npm
|
||||||
echo "You can configure an npm repository by providing the a key for 'npm' in the 'package_managers' input."
|
|
||||||
else
|
else
|
||||||
echo "📦 Configuring npm..."
|
echo "📦 Configuring npm..."
|
||||||
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
|
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
|
||||||
cat << EOF > ~/.npmrc
|
cat << EOF > ~/.npmrc
|
||||||
email=${ARTIFACTORY_EMAIL}
|
${NPMRC}
|
||||||
registry=${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}
|
|
||||||
EOF
|
EOF
|
||||||
echo "//${JFROG_HOST}/artifactory/api/npm/${REPOSITORY_NPM}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}" >> ~/.npmrc
|
config_complete
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Configure the `pip` to use the Artifactory "python" repository.
|
# Configure the `pip` to use the Artifactory "python" repository.
|
||||||
if [ -z "${REPOSITORY_PYPI}" ]; then
|
if [ -z "${HAS_PYPI}" ]; then
|
||||||
echo "🤔 no pypi repository is set, skipping pip configuration."
|
not_configured pypi
|
||||||
echo "You can configure a pypi repository by providing the a key for 'pypi' in the 'package_managers' input."
|
|
||||||
else
|
else
|
||||||
echo "📦 Configuring pip..."
|
echo "🐍 Configuring pip..."
|
||||||
jf pipc --global --repo-resolve "${REPOSITORY_PYPI}"
|
jf pipc --global --repo-resolve "${REPOSITORY_PYPI}"
|
||||||
mkdir -p ~/.pip
|
mkdir -p ~/.pip
|
||||||
cat << EOF > ~/.pip/pip.conf
|
cat << EOF > ~/.pip/pip.conf
|
||||||
[global]
|
${PIP_CONF}
|
||||||
index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple
|
|
||||||
EOF
|
EOF
|
||||||
|
config_complete
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Configure Artifactory "go" repository.
|
# Configure Artifactory "go" repository.
|
||||||
if [ -z "${REPOSITORY_GO}" ]; then
|
if [ -z "${HAS_GO}" ]; then
|
||||||
echo "🤔 no go repository is set, skipping go configuration."
|
not_configured go
|
||||||
echo "You can configure a go repository by providing the a key for 'go' in the 'package_managers' input."
|
|
||||||
else
|
else
|
||||||
echo "🐹 Configuring go..."
|
echo "🐹 Configuring go..."
|
||||||
jf goc --global --repo-resolve "${REPOSITORY_GO}"
|
jf goc --global --repo-resolve "${REPOSITORY_GO}"
|
||||||
|
config_complete
|
||||||
fi
|
fi
|
||||||
echo "🥳 Configuration complete!"
|
|
||||||
|
|
||||||
# Configure the JFrog CLI to use the Artifactory "docker" repository.
|
# Configure the JFrog CLI to use the Artifactory "docker" repository.
|
||||||
if [ -z "${REPOSITORY_DOCKER}" ]; then
|
if [ -z "${HAS_DOCKER}" ]; then
|
||||||
echo "🤔 no docker repository is set, skipping docker configuration."
|
not_configured docker
|
||||||
echo "You can configure a docker repository by providing the a key for 'docker' in the 'package_managers' input."
|
|
||||||
else
|
else
|
||||||
if command -v docker > /dev/null 2>&1; then
|
if command -v docker > /dev/null 2>&1; then
|
||||||
echo "🔑 Configuring 🐳 docker credentials..."
|
echo "🔑 Configuring 🐳 docker credentials..."
|
||||||
mkdir -p ~/.docker
|
mkdir -p ~/.docker
|
||||||
echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login ${JFROG_HOST} --username ${ARTIFACTORY_USERNAME} --password-stdin
|
${REGISTER_DOCKER}
|
||||||
else
|
else
|
||||||
echo "🤔 no docker is installed, skipping docker configuration."
|
echo "🤔 no docker is installed, skipping docker configuration."
|
||||||
fi
|
fi
|
||||||
@@ -96,20 +106,19 @@ echo "📦 Configuring JFrog CLI completion..."
|
|||||||
SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}')
|
SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}')
|
||||||
# Generate the completion script
|
# Generate the completion script
|
||||||
jf completion $SHELLNAME --install
|
jf completion $SHELLNAME --install
|
||||||
|
begin_stanza="# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)"
|
||||||
# Add the completion script to the user's shell profile
|
# Add the completion script to the user's shell profile
|
||||||
if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then
|
if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then
|
||||||
if ! grep -q "# jf CLI shell completion" ~/.bashrc; then
|
if ! grep -q "$begin_stanza" ~/.bashrc; then
|
||||||
echo "" >> ~/.bashrc
|
printf "%s\n" "$begin_stanza" >> ~/.bashrc
|
||||||
echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)" >> ~/.bashrc
|
|
||||||
echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc
|
echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc
|
||||||
echo "# END: jf CLI shell completion" >> ~/.bashrc
|
echo "# END: jf CLI shell completion" >> ~/.bashrc
|
||||||
else
|
else
|
||||||
echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping."
|
echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping."
|
||||||
fi
|
fi
|
||||||
elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then
|
elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then
|
||||||
if ! grep -q "# jf CLI shell completion" ~/.zshrc; then
|
if ! grep -q "$begin_stanza" ~/.zshrc; then
|
||||||
echo "" >> ~/.zshrc
|
printf "\n%s\n" "$begin_stanza" >> ~/.zshrc
|
||||||
echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-oauth)" >> ~/.zshrc
|
|
||||||
echo "autoload -Uz compinit" >> ~/.zshrc
|
echo "autoload -Uz compinit" >> ~/.zshrc
|
||||||
echo "compinit" >> ~/.zshrc
|
echo "compinit" >> ~/.zshrc
|
||||||
echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc
|
echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc
|
||||||
|
|||||||
5
jfrog-token/.npmrc.tftpl
Normal file
5
jfrog-token/.npmrc.tftpl
Normal file
@@ -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 ~}
|
||||||
@@ -15,14 +15,15 @@ Install the JF CLI and authenticate package managers with Artifactory using Arti
|
|||||||
```tf
|
```tf
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
jfrog_url = "https://XXXX.jfrog.io"
|
jfrog_url = "https://XXXX.jfrog.io"
|
||||||
artifactory_access_token = var.artifactory_access_token
|
artifactory_access_token = var.artifactory_access_token
|
||||||
package_managers = {
|
package_managers = {
|
||||||
"npm" : "npm",
|
npm = ["npm", "@scoped:npm-scoped"]
|
||||||
"go" : "go",
|
go = ["go", "another-go-repo"]
|
||||||
"pypi" : "pypi"
|
pypi = ["pypi", "extra-index-pypi"]
|
||||||
|
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -41,14 +42,14 @@ For detailed instructions, please see this [guide](https://coder.com/docs/v2/lat
|
|||||||
```tf
|
```tf
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
jfrog_url = "https://YYYY.jfrog.io"
|
jfrog_url = "https://YYYY.jfrog.io"
|
||||||
artifactory_access_token = var.artifactory_access_token # An admin access token
|
artifactory_access_token = var.artifactory_access_token # An admin access token
|
||||||
package_managers = {
|
package_managers = {
|
||||||
"npm" : "npm-local",
|
npm = ["npm-local"]
|
||||||
"go" : "go-local",
|
go = ["go-local"]
|
||||||
"pypi" : "pypi-local"
|
pypi = ["pypi-local"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -74,15 +75,15 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
|
|||||||
```tf
|
```tf
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
jfrog_url = "https://XXXX.jfrog.io"
|
jfrog_url = "https://XXXX.jfrog.io"
|
||||||
artifactory_access_token = var.artifactory_access_token
|
artifactory_access_token = var.artifactory_access_token
|
||||||
configure_code_server = true # Add JFrog extension configuration for code-server
|
configure_code_server = true # Add JFrog extension configuration for code-server
|
||||||
package_managers = {
|
package_managers = {
|
||||||
"npm" : "npm",
|
npm = ["npm"]
|
||||||
"go" : "go",
|
go = ["go"]
|
||||||
"pypi" : "pypi"
|
pypi = ["pypi"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -94,15 +95,13 @@ data "coder_workspace" "me" {}
|
|||||||
|
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
source = "registry.coder.com/modules/jfrog-token/coder"
|
source = "registry.coder.com/modules/jfrog-token/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
jfrog_url = "https://XXXX.jfrog.io"
|
jfrog_url = "https://XXXX.jfrog.io"
|
||||||
artifactory_access_token = var.artifactory_access_token
|
artifactory_access_token = var.artifactory_access_token
|
||||||
token_description = "Token for Coder workspace: ${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}"
|
token_description = "Token for Coder workspace: ${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}"
|
||||||
package_managers = {
|
package_managers = {
|
||||||
"npm" : "npm",
|
npm = ["npm"]
|
||||||
"go" : "go",
|
|
||||||
"pypi" : "pypi"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,12 +1,29 @@
|
|||||||
import { serve } from "bun";
|
import { serve } from "bun";
|
||||||
import { describe } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import {
|
import {
|
||||||
createJSONResponse,
|
createJSONResponse,
|
||||||
|
findResourceInstance,
|
||||||
runTerraformInit,
|
runTerraformInit,
|
||||||
|
runTerraformApply,
|
||||||
testRequiredVariables,
|
testRequiredVariables,
|
||||||
} from "../test";
|
} from "../test";
|
||||||
|
|
||||||
describe("jfrog-token", async () => {
|
describe("jfrog-token", async () => {
|
||||||
|
type TestVariables = {
|
||||||
|
agent_id: string;
|
||||||
|
jfrog_url: string;
|
||||||
|
artifactory_access_token: string;
|
||||||
|
package_managers: string;
|
||||||
|
|
||||||
|
token_description?: string;
|
||||||
|
check_license?: boolean;
|
||||||
|
refreshable?: boolean;
|
||||||
|
expires_in?: number;
|
||||||
|
username_field?: string;
|
||||||
|
jfrog_server_id?: string;
|
||||||
|
configure_code_server?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
await runTerraformInit(import.meta.dir);
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
// Run a fake JFrog server so the provider can initialize
|
// Run a fake JFrog server so the provider can initialize
|
||||||
@@ -32,10 +49,116 @@ describe("jfrog-token", async () => {
|
|||||||
port: 0,
|
port: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
testRequiredVariables(import.meta.dir, {
|
const fakeFrogApi = `${fakeFrogHost.hostname}:${fakeFrogHost.port}/artifactory/api`;
|
||||||
agent_id: "some-agent-id",
|
const fakeFrogUrl = `http://${fakeFrogHost.hostname}:${fakeFrogHost.port}`;
|
||||||
jfrog_url: "http://" + fakeFrogHost.hostname + ":" + fakeFrogHost.port,
|
const user = "default";
|
||||||
artifactory_access_token: "XXXX",
|
const token = "xxx";
|
||||||
package_managers: "{}",
|
|
||||||
|
it("can run apply with required variables", async () => {
|
||||||
|
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "some-agent-id",
|
||||||
|
jfrog_url: fakeFrogUrl,
|
||||||
|
artifactory_access_token: "XXXX",
|
||||||
|
package_managers: "{}",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates an npmrc with scoped repos", async () => {
|
||||||
|
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "some-agent-id",
|
||||||
|
jfrog_url: fakeFrogUrl,
|
||||||
|
artifactory_access_token: "XXXX",
|
||||||
|
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=xxx
|
||||||
|
@foo:registry=http://${fakeFrogApi}/npm/foo
|
||||||
|
//${fakeFrogApi}/npm/foo/:_authToken=xxx
|
||||||
|
@bar:registry=http://${fakeFrogApi}/npm/bar
|
||||||
|
//${fakeFrogApi}/npm/bar/:_authToken=xxx
|
||||||
|
|
||||||
|
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,
|
||||||
|
artifactory_access_token: "XXXX",
|
||||||
|
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}:${token}@${fakeFrogApi}/pypi/global/simple
|
||||||
|
extra-index-url =
|
||||||
|
https://${user}:${token}@${fakeFrogApi}/pypi/foo/simple
|
||||||
|
https://${user}:${token}@${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,
|
||||||
|
artifactory_access_token: "XXXX",
|
||||||
|
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,
|
||||||
|
artifactory_access_token: "XXXX",
|
||||||
|
package_managers: JSON.stringify({
|
||||||
|
go: ["foo", "bar", "baz"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
|
||||||
|
const proxies = ["foo", "bar", "baz"]
|
||||||
|
.map((r) => `https://${user}:${token}@${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',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -80,23 +80,51 @@ variable "configure_code_server" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "package_managers" {
|
variable "package_managers" {
|
||||||
type = map(string)
|
type = object({
|
||||||
description = <<EOF
|
npm = optional(list(string), [])
|
||||||
A map of package manager names to their respective artifactory repositories.
|
go = optional(list(string), [])
|
||||||
For example:
|
pypi = optional(list(string), [])
|
||||||
{
|
docker = optional(list(string), [])
|
||||||
"npm": "YOUR_NPM_REPO_KEY",
|
})
|
||||||
"go": "YOUR_GO_REPO_KEY",
|
description = <<-EOF
|
||||||
"pypi": "YOUR_PYPI_REPO_KEY",
|
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
|
||||||
"docker": "YOUR_DOCKER_REPO_KEY"
|
For example:
|
||||||
}
|
{
|
||||||
EOF
|
npm = ["GLOBAL_NPM_REPO_KEY", "@SCOPED:NPM_REPO_KEY"]
|
||||||
|
go = ["YOUR_GO_REPO_KEY", "ANOTHER_GO_REPO_KEY"]
|
||||||
|
pypi = ["YOUR_PYPI_REPO_KEY", "ANOTHER_PYPI_REPO_KEY"]
|
||||||
|
docker = ["YOUR_DOCKER_REPO_KEY", "ANOTHER_DOCKER_REPO_KEY"]
|
||||||
|
}
|
||||||
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
# The username field to use for artifactory
|
# The username field to use for artifactory
|
||||||
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name
|
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name
|
||||||
jfrog_host = replace(var.jfrog_url, "https://", "")
|
jfrog_host = split("://", var.jfrog_url)[1]
|
||||||
|
common_values = {
|
||||||
|
JFROG_URL = var.jfrog_url
|
||||||
|
JFROG_HOST = local.jfrog_host
|
||||||
|
JFROG_SERVER_ID = var.jfrog_server_id
|
||||||
|
ARTIFACTORY_USERNAME = local.username
|
||||||
|
ARTIFACTORY_EMAIL = data.coder_workspace_owner.me.email
|
||||||
|
ARTIFACTORY_ACCESS_TOKEN = artifactory_scoped_token.me.access_token
|
||||||
|
}
|
||||||
|
npmrc = templatefile(
|
||||||
|
"${path.module}/.npmrc.tftpl",
|
||||||
|
merge(
|
||||||
|
local.common_values,
|
||||||
|
{
|
||||||
|
REPOS = [
|
||||||
|
for r in var.package_managers.npm :
|
||||||
|
strcontains(r, ":") ? zipmap(["SCOPE", "NAME"], ["${split(":", r)[0]}:", split(":", r)[1]]) : { SCOPE = "", NAME = r }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pip_conf = templatefile(
|
||||||
|
"${path.module}/pip.conf.tftpl", merge(local.common_values, { REPOS = var.package_managers.pypi })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configure the Artifactory provider
|
# Configure the Artifactory provider
|
||||||
@@ -123,19 +151,22 @@ resource "coder_script" "jfrog" {
|
|||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "jfrog"
|
display_name = "jfrog"
|
||||||
icon = "/icon/jfrog.svg"
|
icon = "/icon/jfrog.svg"
|
||||||
script = templatefile("${path.module}/run.sh", {
|
script = templatefile("${path.module}/run.sh", merge(
|
||||||
JFROG_URL : var.jfrog_url,
|
local.common_values,
|
||||||
JFROG_HOST : local.jfrog_host,
|
{
|
||||||
JFROG_SERVER_ID : var.jfrog_server_id,
|
CONFIGURE_CODE_SERVER = var.configure_code_server
|
||||||
ARTIFACTORY_USERNAME : local.username,
|
HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES"
|
||||||
ARTIFACTORY_EMAIL : data.coder_workspace_owner.me.email,
|
NPMRC = local.npmrc
|
||||||
ARTIFACTORY_ACCESS_TOKEN : artifactory_scoped_token.me.access_token,
|
REPOSITORY_NPM = try(element(var.package_managers.npm, 0), "")
|
||||||
CONFIGURE_CODE_SERVER : var.configure_code_server,
|
HAS_GO = length(var.package_managers.go) == 0 ? "" : "YES"
|
||||||
REPOSITORY_NPM : lookup(var.package_managers, "npm", ""),
|
REPOSITORY_GO = try(element(var.package_managers.go, 0), "")
|
||||||
REPOSITORY_GO : lookup(var.package_managers, "go", ""),
|
HAS_PYPI = length(var.package_managers.pypi) == 0 ? "" : "YES"
|
||||||
REPOSITORY_PYPI : lookup(var.package_managers, "pypi", ""),
|
PIP_CONF = local.pip_conf
|
||||||
REPOSITORY_DOCKER : lookup(var.package_managers, "docker", ""),
|
REPOSITORY_PYPI = try(element(var.package_managers.pypi, 0), "")
|
||||||
})
|
HAS_DOCKER = length(var.package_managers.docker) == 0 ? "" : "YES"
|
||||||
|
REGISTER_DOCKER = join("\n", formatlist("register_docker \"%s\"", var.package_managers.docker))
|
||||||
|
}
|
||||||
|
))
|
||||||
run_on_start = true
|
run_on_start = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,10 +192,13 @@ resource "coder_env" "jfrog_ide_store_connection" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resource "coder_env" "goproxy" {
|
resource "coder_env" "goproxy" {
|
||||||
count = lookup(var.package_managers, "go", "") == "" ? 0 : 1
|
count = length(var.package_managers.go) == 0 ? 0 : 1
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
name = "GOPROXY"
|
name = "GOPROXY"
|
||||||
value = "https://${local.username}:${artifactory_scoped_token.me.access_token}@${local.jfrog_host}/artifactory/api/go/${lookup(var.package_managers, "go", "")}"
|
value = join(",", [
|
||||||
|
for repo in var.package_managers.go :
|
||||||
|
"https://${local.username}:${artifactory_scoped_token.me.access_token}@${local.jfrog_host}/artifactory/api/go/${repo}"
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
output "access_token" {
|
output "access_token" {
|
||||||
|
|||||||
6
jfrog-token/pip.conf.tftpl
Normal file
6
jfrog-token/pip.conf.tftpl
Normal file
@@ -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 ~}
|
||||||
@@ -2,6 +2,21 @@
|
|||||||
|
|
||||||
BOLD='\033[0;1m'
|
BOLD='\033[0;1m'
|
||||||
|
|
||||||
|
not_configured() {
|
||||||
|
type=$1
|
||||||
|
echo "🤔 no $type repository is set, skipping $type configuration."
|
||||||
|
echo "You can configure a $type repository by providing a key for '$type' in the 'package_managers' input."
|
||||||
|
}
|
||||||
|
|
||||||
|
config_complete() {
|
||||||
|
echo "🥳 Configuration complete!"
|
||||||
|
}
|
||||||
|
|
||||||
|
register_docker() {
|
||||||
|
repo=$1
|
||||||
|
echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login "$repo" --username ${ARTIFACTORY_USERNAME} --password-stdin
|
||||||
|
}
|
||||||
|
|
||||||
# check if JFrog CLI is already installed
|
# check if JFrog CLI is already installed
|
||||||
if command -v jf > /dev/null 2>&1; then
|
if command -v jf > /dev/null 2>&1; then
|
||||||
echo "✅ JFrog CLI is already installed, skipping installation."
|
echo "✅ JFrog CLI is already installed, skipping installation."
|
||||||
@@ -11,8 +26,7 @@ else
|
|||||||
sudo chmod 755 /usr/local/bin/jf
|
sudo chmod 755 /usr/local/bin/jf
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The jf CLI checks $CI when determining whether to use interactive
|
# The jf CLI checks $CI when determining whether to use interactive flows.
|
||||||
# flows.
|
|
||||||
export CI=true
|
export CI=true
|
||||||
# Authenticate JFrog CLI with Artifactory.
|
# Authenticate JFrog CLI with Artifactory.
|
||||||
echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}"
|
echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}"
|
||||||
@@ -20,52 +34,47 @@ echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFRO
|
|||||||
jf c use "${JFROG_SERVER_ID}"
|
jf c use "${JFROG_SERVER_ID}"
|
||||||
|
|
||||||
# Configure npm to use the Artifactory "npm" repository.
|
# Configure npm to use the Artifactory "npm" repository.
|
||||||
if [ -z "${REPOSITORY_NPM}" ]; then
|
if [ -z "${HAS_NPM}" ]; then
|
||||||
echo "🤔 no npm repository is set, skipping npm configuration."
|
not_configured npm
|
||||||
echo "You can configure an npm repository by providing the a key for 'npm' in the 'package_managers' input."
|
|
||||||
else
|
else
|
||||||
echo "📦 Configuring npm..."
|
echo "📦 Configuring npm..."
|
||||||
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
|
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
|
||||||
cat << EOF > ~/.npmrc
|
cat << EOF > ~/.npmrc
|
||||||
email=${ARTIFACTORY_EMAIL}
|
${NPMRC}
|
||||||
registry=${JFROG_URL}/artifactory/api/npm/${REPOSITORY_NPM}
|
|
||||||
EOF
|
EOF
|
||||||
echo "//${JFROG_HOST}/artifactory/api/npm/${REPOSITORY_NPM}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}" >> ~/.npmrc
|
config_complete
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Configure the `pip` to use the Artifactory "python" repository.
|
# Configure the `pip` to use the Artifactory "python" repository.
|
||||||
if [ -z "${REPOSITORY_PYPI}" ]; then
|
if [ -z "${HAS_PYPI}" ]; then
|
||||||
echo "🤔 no pypi repository is set, skipping pip configuration."
|
not_configured pypi
|
||||||
echo "You can configure a pypi repository by providing the a key for 'pypi' in the 'package_managers' input."
|
|
||||||
else
|
else
|
||||||
echo "🐍 Configuring pip..."
|
echo "🐍 Configuring pip..."
|
||||||
jf pipc --global --repo-resolve "${REPOSITORY_PYPI}"
|
jf pipc --global --repo-resolve "${REPOSITORY_PYPI}"
|
||||||
mkdir -p ~/.pip
|
mkdir -p ~/.pip
|
||||||
cat << EOF > ~/.pip/pip.conf
|
cat << EOF > ~/.pip/pip.conf
|
||||||
[global]
|
${PIP_CONF}
|
||||||
index-url = https://${ARTIFACTORY_USERNAME}:${ARTIFACTORY_ACCESS_TOKEN}@${JFROG_HOST}/artifactory/api/pypi/${REPOSITORY_PYPI}/simple
|
|
||||||
EOF
|
EOF
|
||||||
|
config_complete
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Configure Artifactory "go" repository.
|
# Configure Artifactory "go" repository.
|
||||||
if [ -z "${REPOSITORY_GO}" ]; then
|
if [ -z "${HAS_GO}" ]; then
|
||||||
echo "🤔 no go repository is set, skipping go configuration."
|
not_configured go
|
||||||
echo "You can configure a go repository by providing the a key for 'go' in the 'package_managers' input."
|
|
||||||
else
|
else
|
||||||
echo "🐹 Configuring go..."
|
echo "🐹 Configuring go..."
|
||||||
jf goc --global --repo-resolve "${REPOSITORY_GO}"
|
jf goc --global --repo-resolve "${REPOSITORY_GO}"
|
||||||
|
config_complete
|
||||||
fi
|
fi
|
||||||
echo "🥳 Configuration complete!"
|
|
||||||
|
|
||||||
# Configure the JFrog CLI to use the Artifactory "docker" repository.
|
# Configure the JFrog CLI to use the Artifactory "docker" repository.
|
||||||
if [ -z "${REPOSITORY_DOCKER}" ]; then
|
if [ -z "${HAS_DOCKER}" ]; then
|
||||||
echo "🤔 no docker repository is set, skipping docker configuration."
|
not_configured docker
|
||||||
echo "You can configure a docker repository by providing the a key for 'docker' in the 'package_managers' input."
|
|
||||||
else
|
else
|
||||||
if command -v docker > /dev/null 2>&1; then
|
if command -v docker > /dev/null 2>&1; then
|
||||||
echo "🔑 Configuring 🐳 docker credentials..."
|
echo "🔑 Configuring 🐳 docker credentials..."
|
||||||
mkdir -p ~/.docker
|
mkdir -p ~/.docker
|
||||||
echo -n "${ARTIFACTORY_ACCESS_TOKEN}" | docker login ${JFROG_HOST} --username ${ARTIFACTORY_USERNAME} --password-stdin
|
${REGISTER_DOCKER}
|
||||||
else
|
else
|
||||||
echo "🤔 no docker is installed, skipping docker configuration."
|
echo "🤔 no docker is installed, skipping docker configuration."
|
||||||
fi
|
fi
|
||||||
@@ -96,20 +105,19 @@ echo "📦 Configuring JFrog CLI completion..."
|
|||||||
SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}')
|
SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}')
|
||||||
# Generate the completion script
|
# Generate the completion script
|
||||||
jf completion $SHELLNAME --install
|
jf completion $SHELLNAME --install
|
||||||
|
begin_stanza="# BEGIN: jf CLI shell completion (added by coder module jfrog-token)"
|
||||||
# Add the completion script to the user's shell profile
|
# Add the completion script to the user's shell profile
|
||||||
if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then
|
if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then
|
||||||
if ! grep -q "# jf CLI shell completion" ~/.bashrc; then
|
if ! grep -q "$begin_stanza" ~/.bashrc; then
|
||||||
echo "" >> ~/.bashrc
|
printf "%s\n" "$begin_stanza" >> ~/.bashrc
|
||||||
echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-token)" >> ~/.bashrc
|
|
||||||
echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc
|
echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc
|
||||||
echo "# END: jf CLI shell completion" >> ~/.bashrc
|
echo "# END: jf CLI shell completion" >> ~/.bashrc
|
||||||
else
|
else
|
||||||
echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping."
|
echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping."
|
||||||
fi
|
fi
|
||||||
elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then
|
elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then
|
||||||
if ! grep -q "# jf CLI shell completion" ~/.zshrc; then
|
if ! grep -q "$begin_stanza" ~/.zshrc; then
|
||||||
echo "" >> ~/.zshrc
|
printf "\n%s\n" "$begin_stanza" >> ~/.zshrc
|
||||||
echo "# BEGIN: jf CLI shell completion (added by coder module jfrog-token)" >> ~/.zshrc
|
|
||||||
echo "autoload -Uz compinit" >> ~/.zshrc
|
echo "autoload -Uz compinit" >> ~/.zshrc
|
||||||
echo "compinit" >> ~/.zshrc
|
echo "compinit" >> ~/.zshrc
|
||||||
echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc
|
echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ A module that adds Jupyter Notebook in your Coder template.
|
|||||||
```tf
|
```tf
|
||||||
module "jupyter-notebook" {
|
module "jupyter-notebook" {
|
||||||
source = "registry.coder.com/modules/jupyter-notebook/coder"
|
source = "registry.coder.com/modules/jupyter-notebook/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ printf "$${BOLD}Installing jupyter-notebook!\n"
|
|||||||
# check if jupyter-notebook is installed
|
# check if jupyter-notebook is installed
|
||||||
if ! command -v jupyter-notebook > /dev/null 2>&1; then
|
if ! command -v jupyter-notebook > /dev/null 2>&1; then
|
||||||
# install jupyter-notebook
|
# install jupyter-notebook
|
||||||
# check if python3 pip is installed
|
# check if pipx is installed
|
||||||
if ! command -v pip3 > /dev/null 2>&1; then
|
if ! command -v pipx > /dev/null 2>&1; then
|
||||||
echo "pip3 is not installed"
|
echo "pipx is not installed"
|
||||||
echo "Please install pip3 in your Dockerfile/VM image before running this script"
|
echo "Please install pipx in your Dockerfile/VM image before using this module"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# install jupyter-notebook
|
# install jupyter notebook
|
||||||
pip3 install --upgrade --no-cache-dir --no-warn-script-location jupyter
|
pipx install -q notebook
|
||||||
echo "🥳 jupyter-notebook has been installed\n\n"
|
echo "🥳 jupyter-notebook has been installed\n\n"
|
||||||
else
|
else
|
||||||
echo "🥳 jupyter-notebook is already installed\n\n"
|
echo "🥳 jupyter-notebook is already installed\n\n"
|
||||||
@@ -22,4 +22,4 @@ fi
|
|||||||
|
|
||||||
echo "👷 Starting jupyter-notebook in background..."
|
echo "👷 Starting jupyter-notebook in background..."
|
||||||
echo "check logs at ${LOG_PATH}"
|
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 &
|
$HOME/.local/bin/jupyter-notebook --NotebookApp.ip='0.0.0.0' --ServerApp.port=${PORT} --no-browser --ServerApp.token='' --ServerApp.password='' > ${LOG_PATH} 2>&1 &
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ A module that adds JupyterLab in your Coder template.
|
|||||||
```tf
|
```tf
|
||||||
module "jupyterlab" {
|
module "jupyterlab" {
|
||||||
source = "registry.coder.com/modules/jupyterlab/coder"
|
source = "registry.coder.com/modules/jupyterlab/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.19"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const executeScriptInContainerWithPip = async (
|
|||||||
}> => {
|
}> => {
|
||||||
const instance = findResourceInstance(state, "coder_script");
|
const instance = findResourceInstance(state, "coder_script");
|
||||||
const id = await runContainer(image);
|
const id = await runContainer(image);
|
||||||
const respPip = await execContainer(id, [shell, "-c", "apk add py3-pip"]);
|
const respPipx = await execContainer(id, [shell, "-c", "apk add pipx"]);
|
||||||
const resp = await execContainer(id, [shell, "-c", instance.script]);
|
const resp = await execContainer(id, [shell, "-c", instance.script]);
|
||||||
const stdout = resp.stdout.trim().split("\n");
|
const stdout = resp.stdout.trim().split("\n");
|
||||||
const stderr = resp.stderr.trim().split("\n");
|
const stderr = resp.stderr.trim().split("\n");
|
||||||
@@ -40,7 +40,7 @@ describe("jupyterlab", async () => {
|
|||||||
agent_id: "foo",
|
agent_id: "foo",
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fails without pip3", async () => {
|
it("fails without pipx", async () => {
|
||||||
const state = await runTerraformApply(import.meta.dir, {
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
agent_id: "foo",
|
agent_id: "foo",
|
||||||
});
|
});
|
||||||
@@ -48,14 +48,14 @@ describe("jupyterlab", async () => {
|
|||||||
expect(output.exitCode).toBe(1);
|
expect(output.exitCode).toBe(1);
|
||||||
expect(output.stdout).toEqual([
|
expect(output.stdout).toEqual([
|
||||||
"\u001B[0;1mInstalling jupyterlab!",
|
"\u001B[0;1mInstalling jupyterlab!",
|
||||||
"pip3 is not installed",
|
"pipx is not installed",
|
||||||
"Please install pip3 in your Dockerfile/VM image before running this script",
|
"Please install pipx in your Dockerfile/VM image before running this script",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Add faster test to run with pip3.
|
// TODO: Add faster test to run with pipx.
|
||||||
// currently times out.
|
// currently times out.
|
||||||
// it("runs with pip3", async () => {
|
// it("runs with pipx", async () => {
|
||||||
// ...
|
// ...
|
||||||
// const output = await executeScriptInContainerWithPip(state, "alpine");
|
// const output = await executeScriptInContainerWithPip(state, "alpine");
|
||||||
// ...
|
// ...
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ printf "$${BOLD}Installing jupyterlab!\n"
|
|||||||
# check if jupyterlab is installed
|
# 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
|
# install jupyterlab
|
||||||
# check if python3 pip is installed
|
# check if pipx is installed
|
||||||
if ! command -v pip3 > /dev/null 2>&1; then
|
if ! command -v pipx > /dev/null 2>&1; then
|
||||||
echo "pip3 is not installed"
|
echo "pipx is not installed"
|
||||||
echo "Please install pip3 in your Dockerfile/VM image before running this script"
|
echo "Please install pipx in your Dockerfile/VM image before running this script"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# install jupyterlab
|
# install jupyterlab
|
||||||
pip3 install --upgrade --no-cache-dir --no-warn-script-location jupyterlab
|
pipx install -q jupyterlab
|
||||||
echo "🥳 jupyterlab has been installed\n\n"
|
echo "🥳 jupyterlab has been installed\n\n"
|
||||||
else
|
else
|
||||||
echo "🥳 jupyterlab is already installed\n\n"
|
echo "🥳 jupyterlab is already installed\n\n"
|
||||||
@@ -22,4 +22,4 @@ fi
|
|||||||
|
|
||||||
echo "👷 Starting jupyterlab in background..."
|
echo "👷 Starting jupyterlab in background..."
|
||||||
echo "check logs at ${LOG_PATH}"
|
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 &
|
||||||
|
|||||||
15
lint.ts
15
lint.ts
@@ -5,14 +5,15 @@ import grayMatter from "gray-matter";
|
|||||||
|
|
||||||
const files = await readdir(".", { withFileTypes: true });
|
const files = await readdir(".", { withFileTypes: true });
|
||||||
const dirs = files.filter(
|
const dirs = files.filter(
|
||||||
(f) => f.isDirectory() && !f.name.startsWith(".") && f.name !== "node_modules"
|
(f) =>
|
||||||
|
f.isDirectory() && !f.name.startsWith(".") && f.name !== "node_modules",
|
||||||
);
|
);
|
||||||
|
|
||||||
let badExit = false;
|
let badExit = false;
|
||||||
|
|
||||||
// error reports an error to the console and sets badExit to true
|
// error reports an error to the console and sets badExit to true
|
||||||
// so that the process will exit with a non-zero exit code.
|
// so that the process will exit with a non-zero exit code.
|
||||||
const error = (...data: any[]) => {
|
const error = (...data: unknown[]) => {
|
||||||
console.error(...data);
|
console.error(...data);
|
||||||
badExit = true;
|
badExit = true;
|
||||||
};
|
};
|
||||||
@@ -22,7 +23,7 @@ const verifyCodeBlocks = (
|
|||||||
res = {
|
res = {
|
||||||
codeIsTF: false,
|
codeIsTF: false,
|
||||||
codeIsHCL: false,
|
codeIsHCL: false,
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
// Check in-depth.
|
// Check in-depth.
|
||||||
@@ -30,7 +31,12 @@ const verifyCodeBlocks = (
|
|||||||
verifyCodeBlocks(token.items, res);
|
verifyCodeBlocks(token.items, res);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token.type === "list_item") {
|
if (token.type === "list_item") {
|
||||||
|
if (token.tokens === undefined) {
|
||||||
|
throw new Error("Tokens are missing for type list_item");
|
||||||
|
}
|
||||||
|
|
||||||
verifyCodeBlocks(token.tokens, res);
|
verifyCodeBlocks(token.tokens, res);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -80,8 +86,9 @@ for (const dir of dirs) {
|
|||||||
if (!data.maintainer_github) {
|
if (!data.maintainer_github) {
|
||||||
error(dir.name, "missing maintainer_github");
|
error(dir.name, "missing maintainer_github");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await stat(path.join(".", dir.name, data.icon));
|
await stat(path.join(".", dir.name, data.icon ?? ""));
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
error(dir.name, "icon does not exist", data.icon);
|
error(dir.name, "icon does not exist", data.icon);
|
||||||
}
|
}
|
||||||
|
|||||||
264
package-lock.json
generated
264
package-lock.json
generated
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,15 +8,15 @@
|
|||||||
"update-version": "./update-version.sh"
|
"update-version": "./update-version.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "^1.0.18",
|
"bun-types": "^1.1.23",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.2",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-sh": "^0.13.1",
|
"prettier-plugin-sh": "^0.13.1",
|
||||||
"prettier-plugin-terraform-formatter": "^1.2.1"
|
"prettier-plugin-terraform-formatter": "^1.2.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.5.4"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|||||||
@@ -126,7 +126,10 @@ const assertSlackMessage = async (opts: {
|
|||||||
durationMS?: number;
|
durationMS?: number;
|
||||||
output: string;
|
output: string;
|
||||||
}) => {
|
}) => {
|
||||||
let url: URL;
|
// Have to use non-null assertion because TS can't tell when the fetch
|
||||||
|
// function will run
|
||||||
|
let url!: URL;
|
||||||
|
|
||||||
const fakeSlackHost = serve({
|
const fakeSlackHost = serve({
|
||||||
fetch: (req) => {
|
fetch: (req) => {
|
||||||
url = new URL(req.url);
|
url = new URL(req.url);
|
||||||
@@ -138,15 +141,16 @@ const assertSlackMessage = async (opts: {
|
|||||||
},
|
},
|
||||||
port: 0,
|
port: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { instance, id } = await setupContainer(
|
const { instance, id } = await setupContainer(
|
||||||
"alpine/curl",
|
"alpine/curl",
|
||||||
opts.format && {
|
opts.format ? { slack_message: opts.format } : undefined,
|
||||||
slack_message: opts.format,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await writeCoder(id, "echo 'token'");
|
await writeCoder(id, "echo 'token'");
|
||||||
let exec = await execContainer(id, ["sh", "-c", instance.script]);
|
let exec = await execContainer(id, ["sh", "-c", instance.script]);
|
||||||
expect(exec.exitCode).toBe(0);
|
expect(exec.exitCode).toBe(0);
|
||||||
|
|
||||||
exec = await execContainer(id, [
|
exec = await execContainer(id, [
|
||||||
"sh",
|
"sh",
|
||||||
"-c",
|
"-c",
|
||||||
@@ -154,6 +158,7 @@ const assertSlackMessage = async (opts: {
|
|||||||
fakeSlackHost.hostname
|
fakeSlackHost.hostname
|
||||||
}:${fakeSlackHost.port}" slackme ${opts.command}`,
|
}:${fakeSlackHost.port}" slackme ${opts.command}`,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(exec.stderr.trim()).toBe("");
|
expect(exec.stderr.trim()).toBe("");
|
||||||
expect(url.pathname).toEqual("/api/chat.postMessage");
|
expect(url.pathname).toEqual("/api/chat.postMessage");
|
||||||
expect(url.searchParams.get("channel")).toEqual("token");
|
expect(url.searchParams.get("channel")).toEqual("token");
|
||||||
|
|||||||
@@ -4,25 +4,25 @@ set -euo pipefail
|
|||||||
|
|
||||||
# Function to run terraform init and validate in a directory
|
# Function to run terraform init and validate in a directory
|
||||||
run_terraform() {
|
run_terraform() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
echo "Running terraform init and validate in $dir"
|
echo "Running terraform init and validate in $dir"
|
||||||
pushd "$dir"
|
pushd "$dir"
|
||||||
terraform init -upgrade
|
terraform init -upgrade
|
||||||
terraform validate
|
terraform validate
|
||||||
popd
|
popd
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main script
|
# Main script
|
||||||
main() {
|
main() {
|
||||||
# Get the directory of the script
|
# Get the directory of the script
|
||||||
script_dir=$(dirname "$(readlink -f "$0")")
|
script_dir=$(dirname "$(readlink -f "$0")")
|
||||||
|
|
||||||
# Get all subdirectories in the repository
|
# Get all subdirectories in the repository
|
||||||
subdirs=$(find "$script_dir" -mindepth 1 -maxdepth 1 -type d -not -name ".*" | sort)
|
subdirs=$(find "$script_dir" -mindepth 1 -maxdepth 1 -type d -not -name ".*" | sort)
|
||||||
|
|
||||||
for dir in $subdirs; do
|
for dir in $subdirs; do
|
||||||
run_terraform "$dir"
|
run_terraform "$dir"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run the main script
|
# Run the main script
|
||||||
|
|||||||
40
test.ts
40
test.ts
@@ -90,20 +90,26 @@ type TerraformStateResource = {
|
|||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
instances: [{ attributes: Record<string, any> }];
|
|
||||||
|
instances: [
|
||||||
|
{
|
||||||
|
attributes: Record<string, JsonValue>;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TerraformOutput = {
|
||||||
|
type: string;
|
||||||
|
value: JsonValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TerraformState {
|
export interface TerraformState {
|
||||||
outputs: {
|
outputs: Record<string, TerraformOutput>;
|
||||||
[key: string]: {
|
|
||||||
type: string;
|
|
||||||
value: any;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
resources: [TerraformStateResource, ...TerraformStateResource[]];
|
resources: [TerraformStateResource, ...TerraformStateResource[]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TerraformVariables = Record<string, JsonValue>;
|
||||||
|
|
||||||
export interface CoderScriptAttributes {
|
export interface CoderScriptAttributes {
|
||||||
script: string;
|
script: string;
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
@@ -141,27 +147,33 @@ export const findResourceInstance = <T extends string>(
|
|||||||
* Creates a test-case for each variable provided and ensures that the apply
|
* Creates a test-case for each variable provided and ensures that the apply
|
||||||
* fails without it.
|
* fails without it.
|
||||||
*/
|
*/
|
||||||
export const testRequiredVariables = <TVars extends Record<string, string>>(
|
export const testRequiredVariables = <TVars extends TerraformVariables>(
|
||||||
dir: string,
|
dir: string,
|
||||||
vars: TVars,
|
vars: Readonly<TVars>,
|
||||||
) => {
|
) => {
|
||||||
// Ensures that all required variables are provided.
|
// Ensures that all required variables are provided.
|
||||||
it("required variables", async () => {
|
it("required variables", async () => {
|
||||||
await runTerraformApply(dir, vars);
|
await runTerraformApply(dir, vars);
|
||||||
});
|
});
|
||||||
|
|
||||||
const varNames = Object.keys(vars);
|
const varNames = Object.keys(vars);
|
||||||
varNames.forEach((varName) => {
|
varNames.forEach((varName) => {
|
||||||
// Ensures that every variable provided is required!
|
// Ensures that every variable provided is required!
|
||||||
it("missing variable " + varName, async () => {
|
it("missing variable " + varName, async () => {
|
||||||
const localVars = {};
|
const localVars: TerraformVariables = {};
|
||||||
varNames.forEach((otherVarName) => {
|
varNames.forEach((otherVarName) => {
|
||||||
if (otherVarName !== varName) {
|
if (otherVarName !== varName) {
|
||||||
localVars[otherVarName] = vars[otherVarName];
|
localVars[otherVarName] = vars[otherVarName];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await runTerraformApply(dir, localVars);
|
await runTerraformApply(dir, localVars);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
|
if (!(ex instanceof Error)) {
|
||||||
|
throw new Error("Unknown error generated");
|
||||||
|
}
|
||||||
|
|
||||||
expect(ex.message).toContain(
|
expect(ex.message).toContain(
|
||||||
`input variable \"${varName}\" is not set`,
|
`input variable \"${varName}\" is not set`,
|
||||||
);
|
);
|
||||||
@@ -177,11 +189,9 @@ export const testRequiredVariables = <TVars extends Record<string, string>>(
|
|||||||
* fine to run in parallel with other instances of this function, as it uses a
|
* fine to run in parallel with other instances of this function, as it uses a
|
||||||
* random state file.
|
* random state file.
|
||||||
*/
|
*/
|
||||||
export const runTerraformApply = async <
|
export const runTerraformApply = async <TVars extends TerraformVariables>(
|
||||||
TVars extends Readonly<Record<string, string | boolean>>,
|
|
||||||
>(
|
|
||||||
dir: string,
|
dir: string,
|
||||||
vars: TVars,
|
vars: Readonly<TVars>,
|
||||||
env?: Record<string, string>,
|
env?: Record<string, string>,
|
||||||
): Promise<TerraformState> => {
|
): Promise<TerraformState> => {
|
||||||
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
|
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"module": "esnext",
|
"module": "nodenext",
|
||||||
|
"strict": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "nodenext",
|
||||||
"types": ["bun-types"]
|
"types": ["bun-types"]
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# This script updates the version number in the README.md files of all modules
|
# This script increments the version number in the README.md files of all modules
|
||||||
# to the latest tag in the repository. It is intended to be run from the root
|
# by 1 patch version. It is intended to be run from the root
|
||||||
# of the repository or by using the `bun update-version` command.
|
# of the repository or by using the `bun update-version` command.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
current_tag=$(git describe --tags --abbrev=0)
|
current_tag=$(git describe --tags --abbrev=0)
|
||||||
previous_tag=$(git describe --tags --abbrev=0 $current_tag^)
|
|
||||||
mapfile -t changed_dirs < <(git diff --name-only "$previous_tag"..."$current_tag" -- ':!**/README.md' ':!**/*.test.ts' | xargs dirname | grep -v '^\.' | sort -u)
|
|
||||||
|
|
||||||
LATEST_TAG=$(git describe --abbrev=0 --tags | sed 's/^v//') || exit $?
|
# Increment the patch version
|
||||||
|
LATEST_TAG=$(echo "$current_tag" | sed 's/^v//' | awk -F. '{print $1"."$2"."$3+1}') || exit $?
|
||||||
|
|
||||||
|
# List directories with changes that are not README.md or test files
|
||||||
|
mapfile -t changed_dirs < <(git diff --name-only "$current_tag" -- ':!**/README.md' ':!**/*.test.ts' | xargs dirname | grep -v '^\.' | sort -u)
|
||||||
|
|
||||||
|
echo "Directories with changes: ${changed_dirs[*]}"
|
||||||
|
|
||||||
|
# Iterate over directories and update version in README.md
|
||||||
for dir in "${changed_dirs[@]}"; do
|
for dir in "${changed_dirs[@]}"; do
|
||||||
if [[ -f "$dir/README.md" ]]; then
|
if [[ -f "$dir/README.md" ]]; then
|
||||||
echo "Bumping version in $dir/README.md"
|
|
||||||
file="$dir/README.md"
|
file="$dir/README.md"
|
||||||
tmpfile=$(mktemp /tmp/tempfile.XXXXXX)
|
tmpfile=$(mktemp /tmp/tempfile.XXXXXX)
|
||||||
awk -v tag="$LATEST_TAG" '{
|
awk -v tag="$LATEST_TAG" '{
|
||||||
@@ -25,5 +29,12 @@ for dir in "${changed_dirs[@]}"; do
|
|||||||
print
|
print
|
||||||
}
|
}
|
||||||
}' "$file" > "$tmpfile" && mv "$tmpfile" "$file"
|
}' "$file" > "$tmpfile" && mv "$tmpfile" "$file"
|
||||||
|
|
||||||
|
# Check if the README.md file has changed
|
||||||
|
if ! git diff --quiet -- "$dir/README.md"; then
|
||||||
|
echo "Bumping version in $dir/README.md from $current_tag to $LATEST_TAG (incremented)"
|
||||||
|
else
|
||||||
|
echo "Version in $dir/README.md is already up to date"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
77
vault-jwt/README.md
Normal file
77
vault-jwt/README.md
Normal file
@@ -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.19"
|
||||||
|
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.19"
|
||||||
|
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.19"
|
||||||
|
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.19"
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
```
|
||||||
12
vault-jwt/main.test.ts
Normal file
12
vault-jwt/main.test.ts
Normal file
@@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
64
vault-jwt/main.tf
Normal file
64
vault-jwt/main.tf
Normal file
@@ -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" {}
|
||||||
112
vault-jwt/run.sh
Normal file
112
vault-jwt/run.sh
Normal file
@@ -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=$${VAULT_CLI_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"
|
||||||
@@ -24,9 +24,10 @@ describe("vscode-desktop", async () => {
|
|||||||
const coder_app = state.resources.find(
|
const coder_app = state.resources.find(
|
||||||
(res) => res.type == "coder_app" && res.name == "vscode",
|
(res) => res.type == "coder_app" && res.name == "vscode",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(coder_app).not.toBeNull();
|
expect(coder_app).not.toBeNull();
|
||||||
expect(coder_app.instances.length).toBe(1);
|
expect(coder_app?.instances.length).toBe(1);
|
||||||
expect(coder_app.instances[0].attributes.order).toBeNull();
|
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("adds folder", async () => {
|
it("adds folder", async () => {
|
||||||
@@ -80,8 +81,9 @@ describe("vscode-desktop", async () => {
|
|||||||
const coder_app = state.resources.find(
|
const coder_app = state.resources.find(
|
||||||
(res) => res.type == "coder_app" && res.name == "vscode",
|
(res) => res.type == "coder_app" && res.name == "vscode",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(coder_app).not.toBeNull();
|
expect(coder_app).not.toBeNull();
|
||||||
expect(coder_app.instances.length).toBe(1);
|
expect(coder_app?.instances.length).toBe(1);
|
||||||
expect(coder_app.instances[0].attributes.order).toBe(22);
|
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
|
|||||||
```tf
|
```tf
|
||||||
# AWS example. See below for examples of using this module with other providers
|
# AWS example. See below for examples of using this module with other providers
|
||||||
module "windows_rdp" {
|
module "windows_rdp" {
|
||||||
source = "registry.coder.com/coder/module/windows-rdp"
|
source = "registry.coder.com/modules/windows-rdp/coder"
|
||||||
version = "1.0.16"
|
version = "1.0.18"
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
agent_id = resource.coder_agent.main.id
|
agent_id = resource.coder_agent.main.id
|
||||||
resource_id = resource.aws_instance.dev.id
|
resource_id = resource.aws_instance.dev.id
|
||||||
@@ -24,7 +24,7 @@ module "windows_rdp" {
|
|||||||
|
|
||||||
## Video
|
## Video
|
||||||
|
|
||||||
https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02
|
[](https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a4be02)
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ https://github.com/coder/modules/assets/28937484/fb5f4a55-7b69-4550-ab62-301e13a
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "windows_rdp" {
|
module "windows_rdp" {
|
||||||
source = "registry.coder.com/coder/module/windows-rdp"
|
source = "registry.coder.com/modules/windows-rdp/coder"
|
||||||
version = "1.0.16"
|
version = "1.0.18"
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
agent_id = resource.coder_agent.main.id
|
agent_id = resource.coder_agent.main.id
|
||||||
resource_id = resource.aws_instance.dev.id
|
resource_id = resource.aws_instance.dev.id
|
||||||
@@ -44,8 +44,8 @@ module "windows_rdp" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "windows_rdp" {
|
module "windows_rdp" {
|
||||||
source = "registry.coder.com/coder/module/windows-rdp"
|
source = "registry.coder.com/modules/windows-rdp/coder"
|
||||||
version = "1.0.16"
|
version = "1.0.18"
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
agent_id = resource.coder_agent.main.id
|
agent_id = resource.coder_agent.main.id
|
||||||
resource_id = resource.google_compute_instance.dev[0].id
|
resource_id = resource.google_compute_instance.dev[0].id
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
type TestVariables = Readonly<{
|
type TestVariables = Readonly<{
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
resource_id: string;
|
resource_id: string;
|
||||||
|
share?: string;
|
||||||
admin_username?: string;
|
admin_username?: string;
|
||||||
admin_password?: string;
|
admin_password?: string;
|
||||||
}>;
|
}>;
|
||||||
@@ -23,7 +24,10 @@ function findWindowsRdpScript(state: TerraformState): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const instance of resource.instances) {
|
for (const instance of resource.instances) {
|
||||||
if (instance.attributes.display_name === "windows-rdp") {
|
if (
|
||||||
|
instance.attributes.display_name === "windows-rdp" &&
|
||||||
|
typeof instance.attributes.script === "string"
|
||||||
|
) {
|
||||||
return instance.attributes.script;
|
return instance.attributes.script;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,11 +103,11 @@ describe("Web RDP", async () => {
|
|||||||
const defaultRdpScript = findWindowsRdpScript(defaultState);
|
const defaultRdpScript = findWindowsRdpScript(defaultState);
|
||||||
expect(defaultRdpScript).toBeString();
|
expect(defaultRdpScript).toBeString();
|
||||||
|
|
||||||
const { username: defaultUsername, password: defaultPassword } =
|
const defaultResultsGroup =
|
||||||
formEntryValuesRe.exec(defaultRdpScript)?.groups ?? {};
|
formEntryValuesRe.exec(defaultRdpScript ?? "")?.groups ?? {};
|
||||||
|
|
||||||
expect(defaultUsername).toBe("Administrator");
|
expect(defaultResultsGroup.username).toBe("Administrator");
|
||||||
expect(defaultPassword).toBe("coderRDP!");
|
expect(defaultResultsGroup.password).toBe("coderRDP!");
|
||||||
|
|
||||||
// Test that custom usernames/passwords are also forwarded correctly
|
// Test that custom usernames/passwords are also forwarded correctly
|
||||||
const customAdminUsername = "crouton";
|
const customAdminUsername = "crouton";
|
||||||
@@ -121,10 +125,10 @@ describe("Web RDP", async () => {
|
|||||||
const customRdpScript = findWindowsRdpScript(customizedState);
|
const customRdpScript = findWindowsRdpScript(customizedState);
|
||||||
expect(customRdpScript).toBeString();
|
expect(customRdpScript).toBeString();
|
||||||
|
|
||||||
const { username: customUsername, password: customPassword } =
|
const customResultsGroup =
|
||||||
formEntryValuesRe.exec(customRdpScript)?.groups ?? {};
|
formEntryValuesRe.exec(customRdpScript ?? "")?.groups ?? {};
|
||||||
|
|
||||||
expect(customUsername).toBe(customAdminUsername);
|
expect(customResultsGroup.username).toBe(customAdminUsername);
|
||||||
expect(customPassword).toBe(customAdminPassword);
|
expect(customResultsGroup.password).toBe(customAdminPassword);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ terraform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "share" {
|
||||||
|
type = string
|
||||||
|
default = "owner"
|
||||||
|
validation {
|
||||||
|
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||||
|
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
variable "agent_id" {
|
variable "agent_id" {
|
||||||
type = string
|
type = string
|
||||||
description = "The ID of a Coder agent."
|
description = "The ID of a Coder agent."
|
||||||
@@ -53,6 +62,7 @@ resource "coder_script" "windows-rdp" {
|
|||||||
|
|
||||||
resource "coder_app" "windows-rdp" {
|
resource "coder_app" "windows-rdp" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
|
share = var.share
|
||||||
slug = "web-rdp"
|
slug = "web-rdp"
|
||||||
display_name = "Web RDP"
|
display_name = "Web RDP"
|
||||||
url = "http://localhost:7171"
|
url = "http://localhost:7171"
|
||||||
|
|||||||
BIN
windows-rdp/video-thumbnails/video-thumbnail.png
Normal file
BIN
windows-rdp/video-thumbnails/video-thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
Reference in New Issue
Block a user