Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63e23dd824 | |||
|
|
4d2531548f | ||
|
|
3d33656bcc | ||
|
|
c390ed005f | ||
|
|
d78925d05f | ||
|
|
22b2ad5fec | ||
|
|
6e66ff59a3 | ||
|
|
19cdb78319 | ||
|
|
4a93bf11e7 | ||
|
|
46a4113e51 | ||
|
|
7c8aa504ae | ||
|
|
e64f1ede52 | ||
|
|
c8a42f6202 | ||
|
|
e9238f107a | ||
|
|
e94f70a286 | ||
|
|
7654140330 | ||
|
|
bc6490f0d3 | ||
|
|
482ed84399 | ||
|
|
32b69016a0 | ||
|
|
6d2739131a | ||
|
|
cbd06b1135 | ||
|
|
675c82367a | ||
|
|
bf697e1fa4 | ||
|
|
b345e62ac1 | ||
|
|
6597a2d547 | ||
|
|
5101c27c83 | ||
|
|
90bfbfdc40 | ||
|
|
57d96ca27f | ||
|
|
f5ab7995d1 | ||
|
|
528a8a9fea | ||
|
|
87854707bc | ||
|
|
b53554b4e4 | ||
|
|
ce5a5b383a | ||
|
|
1b147ae90d | ||
|
|
7992d9d265 | ||
|
|
20d97a25dd | ||
|
|
8e0dfcd534 | ||
|
|
9752bf89a6 | ||
|
|
48c81c9ff4 | ||
|
|
acd5edffe7 | ||
|
|
4dcab99cb0 | ||
|
|
50a946df0f | ||
|
|
8a0ac3435c | ||
|
|
438c904567 | ||
|
|
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 | ||
|
|
c3eee866d1 | ||
|
|
bf175a1247 | ||
|
|
8fd54e0e78 | ||
|
|
e8ee02c044 | ||
|
|
aebdc9b434 | ||
|
|
d98bfcb20b | ||
|
|
894e507bb3 | ||
|
|
3f8f6181e0 | ||
|
|
b23d85327c | ||
|
|
a8580fe6b9 | ||
|
|
49f060549e | ||
|
|
b4153a6aaa | ||
|
|
13a8877791 | ||
|
|
fd2f91c043 | ||
|
|
c59eb0c0cc | ||
|
|
a381c3ee29 | ||
|
|
d9d1be08a3 | ||
|
|
7a8483d816 | ||
|
|
ec2c8edfb2 | ||
|
|
78f91a542a | ||
|
|
78c948094d | ||
|
|
16f96d3693 | ||
|
|
8262b29063 | ||
|
|
4ab72575ac | ||
|
|
f369697112 | ||
|
|
f82c7fd7a1 | ||
|
|
05a20a9e1f | ||
|
|
90e15cd90c | ||
|
|
5869eb86d4 | ||
|
|
25c90001f4 | ||
|
|
6409ee2bba | ||
|
|
7d366ff92a | ||
|
|
de00f6334f | ||
|
|
264584e673 | ||
|
|
83ecba2293 | ||
|
|
b2807640aa | ||
|
|
33d44fdf17 | ||
|
|
f335cd343d | ||
|
|
aebf095075 | ||
|
|
b283ac3129 | ||
|
|
5f418c3253 | ||
|
|
b09c4cb084 | ||
|
|
8aff87fdf7 | ||
|
|
f3c30abeb4 | ||
|
|
a9a75b675f | ||
|
|
ef4c87e48e | ||
|
|
1a0a8659cc | ||
|
|
c7a4fced4c | ||
|
|
5ec1b207d1 | ||
|
|
702271133f | ||
|
|
652fc6b84f | ||
|
|
8195cf4453 | ||
|
|
d5cfadb4e7 | ||
|
|
fba0f842a9 | ||
|
|
14e3fc5b6b | ||
|
|
0b6975c266 | ||
|
|
d530d68b12 | ||
|
|
047ccd67ca | ||
|
|
c7aa8253e3 | ||
|
|
452f41aa86 | ||
|
|
29209d546e | ||
|
|
aab5e55663 | ||
|
|
ff96b3f653 | ||
|
|
20795aa2b6 | ||
|
|
45456ab394 | ||
|
|
c652dbe320 | ||
|
|
4021d856ba | ||
|
|
72eaf8a9e1 | ||
|
|
249cb2fe9e | ||
|
|
49cff4b2aa | ||
|
|
c6b457e7fe | ||
|
|
beaa33b682 | ||
|
|
0d7bc37f9c | ||
|
|
dcd605c52e | ||
|
|
b93471a381 | ||
|
|
53083a5718 | ||
|
|
7de78d2ef5 | ||
|
|
89135671b2 | ||
|
|
ac648cc0a9 | ||
|
|
748a180ac3 | ||
|
|
ec922c7c3d | ||
|
|
9f8eee55b2 | ||
|
|
0e7644b284 | ||
|
|
bf06e8d3ac | ||
|
|
12fd16f701 | ||
|
|
1197e6bf0d | ||
|
|
c5c521fabd |
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"
|
||||||
203
.github/scripts/check.sh
vendored
Executable file
203
.github/scripts/check.sh
vendored
Executable file
@@ -0,0 +1,203 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -o pipefail
|
||||||
|
set -u
|
||||||
|
|
||||||
|
VERBOSE="${VERBOSE:-0}"
|
||||||
|
if [[ "${VERBOSE}" -ne "0" ]]; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List of required environment variables
|
||||||
|
required_vars=(
|
||||||
|
"INSTATUS_API_KEY"
|
||||||
|
"INSTATUS_PAGE_ID"
|
||||||
|
"INSTATUS_COMPONENT_ID"
|
||||||
|
"VERCEL_API_KEY"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if each required variable is set
|
||||||
|
for var in "${required_vars[@]}"; do
|
||||||
|
if [[ -z "${!var:-}" ]]; then
|
||||||
|
echo "Error: Environment variable '$var' is not set."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
REGISTRY_BASE_URL="${REGISTRY_BASE_URL:-https://registry.coder.com}"
|
||||||
|
|
||||||
|
status=0
|
||||||
|
declare -a modules=()
|
||||||
|
declare -a failures=()
|
||||||
|
|
||||||
|
# Collect all module directories containing a main.tf file
|
||||||
|
for path in $(find . -maxdepth 2 -not -path '*/.*' -type f -name main.tf | cut -d '/' -f 2 | sort -u); do
|
||||||
|
modules+=("${path}")
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Checking modules: ${modules[*]}"
|
||||||
|
|
||||||
|
# Function to update the component status on Instatus
|
||||||
|
update_component_status() {
|
||||||
|
local component_status=$1
|
||||||
|
# see https://instatus.com/help/api/components
|
||||||
|
(curl -X PUT "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/components/$INSTATUS_COMPONENT_ID" \
|
||||||
|
-H "Authorization: Bearer $INSTATUS_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"status\": \"$component_status\"}")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create an incident
|
||||||
|
create_incident() {
|
||||||
|
local incident_name="Degraded Service"
|
||||||
|
local message="The following modules are experiencing issues:\n"
|
||||||
|
for i in "${!failures[@]}"; do
|
||||||
|
message+="$((i + 1)). ${failures[$i]}\n"
|
||||||
|
done
|
||||||
|
|
||||||
|
component_status="PARTIALOUTAGE"
|
||||||
|
if (( ${#failures[@]} == ${#modules[@]} )); then
|
||||||
|
component_status="MAJOROUTAGE"
|
||||||
|
fi
|
||||||
|
# see https://instatus.com/help/api/incidents
|
||||||
|
incident_id=$(curl -s -X POST "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/incidents" \
|
||||||
|
-H "Authorization: Bearer $INSTATUS_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"name\": \"$incident_name\",
|
||||||
|
\"message\": \"$message\",
|
||||||
|
\"components\": [\"$INSTATUS_COMPONENT_ID\"],
|
||||||
|
\"status\": \"INVESTIGATING\",
|
||||||
|
\"notify\": true,
|
||||||
|
\"statuses\": [
|
||||||
|
{
|
||||||
|
\"id\": \"$INSTATUS_COMPONENT_ID\",
|
||||||
|
\"status\": \"PARTIALOUTAGE\"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}" | jq -r '.id')
|
||||||
|
|
||||||
|
echo "Created incident with ID: $incident_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check for existing unresolved incidents
|
||||||
|
check_existing_incident() {
|
||||||
|
# Fetch the latest incidents with status not equal to "RESOLVED"
|
||||||
|
local unresolved_incidents=$(curl -s -X GET "https://api.instatus.com/v1/$INSTATUS_PAGE_ID/incidents" \
|
||||||
|
-H "Authorization: Bearer $INSTATUS_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" | jq -r '.incidents[] | select(.status != "RESOLVED") | .id')
|
||||||
|
|
||||||
|
if [[ -n "$unresolved_incidents" ]]; then
|
||||||
|
echo "Unresolved incidents found: $unresolved_incidents"
|
||||||
|
return 0 # Indicate that there are unresolved incidents
|
||||||
|
else
|
||||||
|
echo "No unresolved incidents found."
|
||||||
|
return 1 # Indicate that no unresolved incidents exist
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
force_redeploy_registry () {
|
||||||
|
# These are not secret values; safe to just expose directly in script
|
||||||
|
local VERCEL_TEAM_SLUG="codercom"
|
||||||
|
local VERCEL_TEAM_ID="team_tGkWfhEGGelkkqUUm9nXq17r"
|
||||||
|
local VERCEL_APP="registry"
|
||||||
|
|
||||||
|
local latest_res
|
||||||
|
latest_res=$(curl "https://api.vercel.com/v6/deployments?app=$VERCEL_APP&limit=1&slug=$VERCEL_TEAM_SLUG&teamId=$VERCEL_TEAM_ID&target=production&state=BUILDING,INITIALIZING,QUEUED,READY" \
|
||||||
|
--fail \
|
||||||
|
--silent \
|
||||||
|
--header "Authorization: Bearer $VERCEL_API_KEY" \
|
||||||
|
--header "Content-Type: application/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# If we have zero deployments, something is VERY wrong. Make the whole
|
||||||
|
# script exit with a non-zero status code
|
||||||
|
local latest_id
|
||||||
|
latest_id=$(echo "${latest_res}" | jq -r '.deployments[0].uid')
|
||||||
|
if [[ "${latest_id}" = "null" ]]; then
|
||||||
|
echo "Unable to pull any previous deployments for redeployment"
|
||||||
|
echo "Please redeploy the latest deployment manually in Vercel."
|
||||||
|
echo "https://vercel.com/codercom/registry/deployments"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local latest_date_ts_seconds
|
||||||
|
latest_date_ts_seconds=$(echo "${latest_res}" | jq -r '.deployments[0].createdAt/1000|floor')
|
||||||
|
local current_date_ts_seconds
|
||||||
|
current_date_ts_seconds="$(date +%s)"
|
||||||
|
local max_redeploy_interval_seconds=7200 # 2 hours
|
||||||
|
if (( current_date_ts_seconds - latest_date_ts_seconds < max_redeploy_interval_seconds )); then
|
||||||
|
echo "The registry was deployed less than 2 hours ago."
|
||||||
|
echo "Not automatically re-deploying the regitstry."
|
||||||
|
echo "A human reading this message should decide if a redeployment is necessary."
|
||||||
|
echo "Please check the Vercel dashboard for more information."
|
||||||
|
echo "https://vercel.com/codercom/registry/deployments"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local latest_deployment_state
|
||||||
|
latest_deployment_state="$(echo "${latest_res}" | jq -r '.deployments[0].state')"
|
||||||
|
if [[ "${latest_deployment_state}" != "READY" ]]; then
|
||||||
|
echo "Last deployment was not in READY state. Skipping redeployment."
|
||||||
|
echo "A human reading this message should decide if a redeployment is necessary."
|
||||||
|
echo "Please check the Vercel dashboard for more information."
|
||||||
|
echo "https://vercel.com/codercom/registry/deployments"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "============================================================="
|
||||||
|
echo "!!! Redeploying registry with deployment ID: ${latest_id} !!!"
|
||||||
|
echo "============================================================="
|
||||||
|
|
||||||
|
if ! curl -X POST "https://api.vercel.com/v13/deployments?forceNew=1&skipAutoDetectionConfirmation=1&slug=$VERCEL_TEAM_SLUG&teamId=$VERCEL_TEAM_ID" \
|
||||||
|
--fail \
|
||||||
|
--header "Authorization: Bearer $VERCEL_API_KEY" \
|
||||||
|
--header "Content-Type: application/json" \
|
||||||
|
--data-raw "{ \"deploymentId\": \"${latest_id}\", \"name\": \"${VERCEL_APP}\", \"target\": \"production\" }"; then
|
||||||
|
echo "DEPLOYMENT FAILED! Please check the Vercel dashboard for more information."
|
||||||
|
echo "https://vercel.com/codercom/registry/deployments"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check each module's accessibility
|
||||||
|
for module in "${modules[@]}"; do
|
||||||
|
# Trim leading/trailing whitespace from module name
|
||||||
|
module=$(echo "${module}" | xargs)
|
||||||
|
url="${REGISTRY_BASE_URL}/modules/${module}"
|
||||||
|
printf "=== Checking module %s at %s\n" "${module}" "${url}"
|
||||||
|
status_code=$(curl --output /dev/null --head --silent --fail --location "${url}" --retry 3 --write-out "%{http_code}")
|
||||||
|
if (( status_code != 200 )); then
|
||||||
|
printf "==> FAIL(%s)\n" "${status_code}"
|
||||||
|
status=1
|
||||||
|
failures+=("${module}")
|
||||||
|
else
|
||||||
|
printf "==> OK(%s)\n" "${status_code}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Determine overall status and update Instatus component
|
||||||
|
if (( status == 0 )); then
|
||||||
|
echo "All modules are operational."
|
||||||
|
# set to
|
||||||
|
update_component_status "OPERATIONAL"
|
||||||
|
else
|
||||||
|
echo "The following modules have issues: ${failures[*]}"
|
||||||
|
# check if all modules are down
|
||||||
|
if (( ${#failures[@]} == ${#modules[@]} )); then
|
||||||
|
update_component_status "MAJOROUTAGE"
|
||||||
|
else
|
||||||
|
update_component_status "PARTIALOUTAGE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if there is an existing incident before creating a new one
|
||||||
|
if ! check_existing_incident; then
|
||||||
|
create_incident
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If a module is down, force a reployment to try getting things back online
|
||||||
|
# ASAP
|
||||||
|
# EDIT: registry.coder.com is no longer hosted on vercel
|
||||||
|
#force_redeploy_registry
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "${status}"
|
||||||
23
.github/workflows/check.yaml
vendored
Normal file
23
.github/workflows/check.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Health
|
||||||
|
# Check modules health on registry.coder.com
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0,15,30,45 * * * *" # Runs every 15 minutes
|
||||||
|
workflow_dispatch: # Allows manual triggering of the workflow if needed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-script:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run check.sh
|
||||||
|
run: |
|
||||||
|
./.github/scripts/check.sh
|
||||||
|
env:
|
||||||
|
INSTATUS_API_KEY: ${{ secrets.INSTATUS_API_KEY }}
|
||||||
|
INSTATUS_PAGE_ID: ${{ secrets.INSTATUS_PAGE_ID }}
|
||||||
|
INSTATUS_COMPONENT_ID: ${{ secrets.INSTATUS_COMPONENT_ID }}
|
||||||
|
VERCEL_API_KEY: ${{ secrets.VERCEL_API_KEY }}
|
||||||
36
.github/workflows/ci.yaml
vendored
36
.github/workflows/ci.yaml
vendored
@@ -16,18 +16,31 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Check out code
|
||||||
- uses: oven-sh/setup-bun@v1
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up Terraform
|
||||||
|
uses: coder/coder/.github/actions/setup-tf@main
|
||||||
|
- name: Set up Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
|
# We're using the latest version of Bun for now, but it might be worth
|
||||||
|
# reconsidering. They've pushed breaking changes in patch releases
|
||||||
|
# that have broken our CI.
|
||||||
|
# Our PR where issues started to pop up: https://github.com/coder/modules/pull/383
|
||||||
|
# The Bun PR that broke things: https://github.com/oven-sh/bun/pull/16067
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
- name: Setup
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
- run: bun test
|
- name: Run tests
|
||||||
|
run: bun test
|
||||||
pretty:
|
pretty:
|
||||||
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: coder/coder/.github/actions/setup-tf@main
|
||||||
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
- name: Setup
|
- name: Setup
|
||||||
@@ -38,3 +51,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
|
||||||
|
|||||||
37
.github/workflows/deploy-registry.yaml
vendored
Normal file
37
.github/workflows/deploy-registry.yaml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: deploy-registry
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Set id-token permission for gcloud
|
||||||
|
# Adding a comment because retriggering the build manually hung? I am the lord of devops and you will bend?
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Authenticate to Google Cloud
|
||||||
|
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935
|
||||||
|
with:
|
||||||
|
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
|
||||||
|
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
|
||||||
|
|
||||||
|
- name: Set up Google Cloud SDK
|
||||||
|
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a
|
||||||
|
|
||||||
|
# For the time being, let's have the first couple merges to main in modules deploy a new version
|
||||||
|
# to *dev*. Once we review and make sure everything's working, we can deploy a new version to *main*.
|
||||||
|
# Maybe in the future we could automate this based on the result of E2E tests.
|
||||||
|
- name: Deploy to dev.registry.coder.com
|
||||||
|
run: |
|
||||||
|
gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch dev
|
||||||
|
|
||||||
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'
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,3 +2,6 @@
|
|||||||
node_modules
|
node_modules
|
||||||
*.tfstate
|
*.tfstate
|
||||||
*.tfstate.lock.info
|
*.tfstate.lock.info
|
||||||
|
|
||||||
|
# Ignore generated credentials from google-github-actions/auth
|
||||||
|
gha-creds-*.json
|
||||||
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
.icons/dcv.svg
Normal file
1
.icons/dcv.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="82" height="80" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" overflow="hidden"><g transform="translate(-550 -124)"><g><g><g><g><path d="M551 124 631 124 631 204 551 204Z" fill="#ED7100" fill-rule="evenodd" fill-opacity="1"/><path d="M612.069 162.386C607.327 165.345 600.717 168.353 593.46 170.855 588.339 172.62 583.33 173.978 578.865 174.838 582.727 184.68 589.944 191.037 596.977 189.853 603.514 188.75 608.387 181.093 609.1 170.801L611.096 170.939C610.304 182.347 604.893 190.545 597.309 191.825 596.648 191.937 595.984 191.991 595.323 191.991 587.945 191.991 580.718 185.209 576.871 175.194 575.733 175.38 574.625 175.542 573.584 175.653 572.173 175.803 570.901 175.879 569.769 175.879 565.95 175.879 563.726 175.025 563.141 173.328 562.414 171.218 564.496 168.566 569.328 165.445L570.414 167.125C565.704 170.167 564.814 172.046 565.032 172.677 565.263 173.348 567.279 174.313 573.372 173.665 574.267 173.57 575.216 173.433 576.187 173.28 575.537 171.297 575.014 169.205 574.647 167.028 573.406 159.673 574.056 152.438 576.48 146.654 578.969 140.715 583.031 136.99 587.917 136.166 593.803 135.171 600.075 138.691 604.679 145.579L603.017 146.69C598.862 140.476 593.349 137.28 588.249 138.138 584.063 138.844 580.539 142.143 578.325 147.427 576.046 152.866 575.44 159.709 576.62 166.695 576.988 168.876 577.515 170.966 578.173 172.937 582.618 172.1 587.651 170.742 592.807 168.965 599.927 166.51 606.392 163.572 611.01 160.689 616.207 157.447 617.201 155.444 616.969 154.772 616.769 154.189 615.095 153.299 610.097 153.653L609.957 151.657C615.171 151.289 618.171 152.116 618.86 154.12 619.619 156.32 617.334 159.101 612.069 162.386" fill="#FFFFFF" fill-rule="evenodd" fill-opacity="1"/></g></g></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
5
.icons/desktop.svg
Normal file
5
.icons/desktop.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M31 6V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V6C1 4.35 2.35 3 4 3H28C29.65 3 31 4.35 31 6Z" fill="#2197F3"/>
|
||||||
|
<path d="M21 27H17V24C17 23.4478 16.5522 23 16 23C15.4478 23 15 23.4478 15 24V27H11C10.4478 27 10 27.4478 10 28C10 28.5522 10.4478 29 11 29H21C21.5522 29 22 28.5522 22 28C22 27.4478 21.5522 27 21 27Z" fill="#FFC10A"/>
|
||||||
|
<path d="M31 17V22C31 23.65 29.65 25 28 25H4C2.35 25 1 23.65 1 22V17H31Z" fill="#3F51B5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 540 B |
BIN
.images/amazon-dcv-windows.png
Normal file
BIN
.images/amazon-dcv-windows.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
@@ -13,6 +13,7 @@ tags: [helper]
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "MODULE_NAME" {
|
module "MODULE_NAME" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
}
|
}
|
||||||
@@ -28,6 +29,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "MODULE_NAME" {
|
module "MODULE_NAME" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
@@ -45,6 +47,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "MODULE_NAME" {
|
module "MODULE_NAME" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
source = "registry.coder.com/modules/MODULE_NAME/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
|
|||||||
@@ -1,30 +1,75 @@
|
|||||||
# 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
|
||||||
module "example" {
|
module "example" {
|
||||||
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
|
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
|
||||||
|
# You may need to remove the 'version' field, it is incompatible with some sources.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> **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,19 +3,21 @@
|
|||||||
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)
|
||||||
|
[](https://github.com/coder/modules/actions/workflows/check.yaml)
|
||||||
|
|
||||||
</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.
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
|
|||||||
49
amazon-dcv-windows/README.md
Normal file
49
amazon-dcv-windows/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
display_name: Amazon DCV Windows
|
||||||
|
description: Amazon DCV Server and Web Client for Windows
|
||||||
|
icon: ../.icons/dcv.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [windows, amazon, dcv, web, desktop]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Amazon DCV Windows
|
||||||
|
|
||||||
|
Amazon DCV is high performance remote display protocol that provides a secure way to deliver remote desktop and application streaming from any cloud or data center to any device, over varying network conditions.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Enable DCV Server and Web Client on Windows workspaces.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "dcv" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/amazon-dcv-windows/coder"
|
||||||
|
version = "1.0.24"
|
||||||
|
agent_id = resource.coder_agent.main.id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
resource "coder_metadata" "dcv" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
resource_id = aws_instance.dev.id # id of the instance resource
|
||||||
|
|
||||||
|
item {
|
||||||
|
key = "DCV client instructions"
|
||||||
|
value = "Run `coder port-forward ${data.coder_workspace.me.name} -p ${module.dcv[count.index].port}` and connect to **localhost:${module.dcv[count.index].port}${module.dcv[count.index].web_url_path}**"
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "username"
|
||||||
|
value = module.dcv[count.index].username
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
key = "password"
|
||||||
|
value = module.dcv[count.index].password
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Amazon DCV is free to use on AWS EC2 instances but requires a license for other cloud providers. Please see the instructions [here](https://docs.aws.amazon.com/dcv/latest/adminguide/setting-up-license.html#setting-up-license-ec2) for more information.
|
||||||
170
amazon-dcv-windows/install-dcv.ps1
Normal file
170
amazon-dcv-windows/install-dcv.ps1
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# Terraform variables
|
||||||
|
$adminPassword = "${admin_password}"
|
||||||
|
$port = "${port}"
|
||||||
|
$webURLPath = "${web_url_path}"
|
||||||
|
|
||||||
|
function Set-LocalAdminUser {
|
||||||
|
Write-Output "[INFO] Starting Set-LocalAdminUser function"
|
||||||
|
$securePassword = ConvertTo-SecureString $adminPassword -AsPlainText -Force
|
||||||
|
Write-Output "[DEBUG] Secure password created"
|
||||||
|
Get-LocalUser -Name Administrator | Set-LocalUser -Password $securePassword
|
||||||
|
Write-Output "[INFO] Administrator password set"
|
||||||
|
Get-LocalUser -Name Administrator | Enable-LocalUser
|
||||||
|
Write-Output "[INFO] User Administrator enabled successfully"
|
||||||
|
Read-Host "[DEBUG] Press Enter to proceed to the next step"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-VirtualDisplayDriverRequired {
|
||||||
|
Write-Output "[INFO] Starting Get-VirtualDisplayDriverRequired function"
|
||||||
|
$token = Invoke-RestMethod -Headers @{'X-aws-ec2-metadata-token-ttl-seconds' = '21600'} -Method PUT -Uri http://169.254.169.254/latest/api/token
|
||||||
|
Write-Output "[DEBUG] Token acquired: $token"
|
||||||
|
$instanceType = Invoke-RestMethod -Headers @{'X-aws-ec2-metadata-token' = $token} -Method GET -Uri http://169.254.169.254/latest/meta-data/instance-type
|
||||||
|
Write-Output "[DEBUG] Instance type: $instanceType"
|
||||||
|
$OSVersion = ((Get-ItemProperty -Path "Microsoft.PowerShell.Core\Registry::\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name ProductName).ProductName) -replace "[^0-9]", ''
|
||||||
|
Write-Output "[DEBUG] OS version: $OSVersion"
|
||||||
|
|
||||||
|
# Force boolean result
|
||||||
|
$result = (($OSVersion -ne "2019") -and ($OSVersion -ne "2022") -and ($OSVersion -ne "2025")) -and (($instanceType[0] -ne 'g') -and ($instanceType[0] -ne 'p'))
|
||||||
|
Write-Output "[INFO] VirtualDisplayDriverRequired result: $result"
|
||||||
|
Read-Host "[DEBUG] Press Enter to proceed to the next step"
|
||||||
|
return [bool]$result
|
||||||
|
}
|
||||||
|
|
||||||
|
function Download-DCV {
|
||||||
|
param (
|
||||||
|
[bool]$VirtualDisplayDriverRequired
|
||||||
|
)
|
||||||
|
Write-Output "[INFO] Starting Download-DCV function"
|
||||||
|
|
||||||
|
$downloads = @(
|
||||||
|
@{
|
||||||
|
Name = "DCV Display Driver"
|
||||||
|
Required = $VirtualDisplayDriverRequired
|
||||||
|
Path = "C:\Windows\Temp\DCVDisplayDriver.msi"
|
||||||
|
Uri = "https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-virtual-display-x64-Release.msi"
|
||||||
|
},
|
||||||
|
@{
|
||||||
|
Name = "DCV Server"
|
||||||
|
Required = $true
|
||||||
|
Path = "C:\Windows\Temp\DCVServer.msi"
|
||||||
|
Uri = "https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-server-x64-Release.msi"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($download in $downloads) {
|
||||||
|
if ($download.Required -and -not (Test-Path $download.Path)) {
|
||||||
|
try {
|
||||||
|
Write-Output "[INFO] Downloading $($download.Name)"
|
||||||
|
|
||||||
|
# Display progress manually (no events)
|
||||||
|
$progressActivity = "Downloading $($download.Name)"
|
||||||
|
$progressStatus = "Starting download..."
|
||||||
|
Write-Progress -Activity $progressActivity -Status $progressStatus -PercentComplete 0
|
||||||
|
|
||||||
|
# Synchronously download the file
|
||||||
|
$webClient = New-Object System.Net.WebClient
|
||||||
|
$webClient.DownloadFile($download.Uri, $download.Path)
|
||||||
|
|
||||||
|
# Update progress
|
||||||
|
Write-Progress -Activity $progressActivity -Status "Completed" -PercentComplete 100
|
||||||
|
|
||||||
|
Write-Output "[INFO] $($download.Name) downloaded successfully."
|
||||||
|
} catch {
|
||||||
|
Write-Output "[ERROR] Failed to download $($download.Name): $_"
|
||||||
|
throw
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Output "[INFO] $($download.Name) already exists. Skipping download."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "[INFO] All downloads completed"
|
||||||
|
Read-Host "[DEBUG] Press Enter to proceed to the next step"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-DCV {
|
||||||
|
param (
|
||||||
|
[bool]$VirtualDisplayDriverRequired
|
||||||
|
)
|
||||||
|
Write-Output "[INFO] Starting Install-DCV function"
|
||||||
|
|
||||||
|
if (-not (Get-Service -Name "dcvserver" -ErrorAction SilentlyContinue)) {
|
||||||
|
if ($VirtualDisplayDriverRequired) {
|
||||||
|
Write-Output "[INFO] Installing DCV Display Driver"
|
||||||
|
Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList "/I C:\Windows\Temp\DCVDisplayDriver.msi /quiet /norestart" -Wait
|
||||||
|
} else {
|
||||||
|
Write-Output "[INFO] DCV Display Driver installation skipped (not required)."
|
||||||
|
}
|
||||||
|
Write-Output "[INFO] Installing DCV Server"
|
||||||
|
Start-Process "C:\Windows\System32\msiexec.exe" -ArgumentList "/I C:\Windows\Temp\DCVServer.msi ADDLOCAL=ALL /quiet /norestart /l*v C:\Windows\Temp\dcv_install_msi.log" -Wait
|
||||||
|
} else {
|
||||||
|
Write-Output "[INFO] DCV Server already installed, skipping installation."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for the service to appear with a timeout
|
||||||
|
$timeout = 10 # seconds
|
||||||
|
$elapsed = 0
|
||||||
|
while (-not (Get-Service -Name "dcvserver" -ErrorAction SilentlyContinue) -and ($elapsed -lt $timeout)) {
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
$elapsed++
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($elapsed -ge $timeout) {
|
||||||
|
Write-Output "[WARNING] Timeout waiting for dcvserver service. A restart is required to complete installation."
|
||||||
|
Restart-SystemForDCV
|
||||||
|
} else {
|
||||||
|
Write-Output "[INFO] dcvserver service detected successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Restart-SystemForDCV {
|
||||||
|
Write-Output "[INFO] The system will restart in 10 seconds to finalize DCV installation."
|
||||||
|
Start-Sleep -Seconds 10
|
||||||
|
|
||||||
|
# Initiate restart
|
||||||
|
Restart-Computer -Force
|
||||||
|
|
||||||
|
# Exit the script after initiating restart
|
||||||
|
Write-Output "[INFO] Please wait for the system to restart..."
|
||||||
|
|
||||||
|
Exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function Configure-DCV {
|
||||||
|
Write-Output "[INFO] Starting Configure-DCV function"
|
||||||
|
$dcvPath = "Microsoft.PowerShell.Core\Registry::\HKEY_USERS\S-1-5-18\Software\GSettings\com\nicesoftware\dcv"
|
||||||
|
|
||||||
|
# Create the required paths
|
||||||
|
@("$dcvPath\connectivity", "$dcvPath\session-management", "$dcvPath\session-management\automatic-console-session", "$dcvPath\display") | ForEach-Object {
|
||||||
|
if (-not (Test-Path $_)) {
|
||||||
|
New-Item -Path $_ -Force | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set registry keys
|
||||||
|
New-ItemProperty -Path "$dcvPath\session-management" -Name create-session -PropertyType DWORD -Value 1 -Force
|
||||||
|
New-ItemProperty -Path "$dcvPath\session-management\automatic-console-session" -Name owner -Value Administrator -Force
|
||||||
|
New-ItemProperty -Path "$dcvPath\connectivity" -Name quic-port -PropertyType DWORD -Value $port -Force
|
||||||
|
New-ItemProperty -Path "$dcvPath\connectivity" -Name web-port -PropertyType DWORD -Value $port -Force
|
||||||
|
New-ItemProperty -Path "$dcvPath\connectivity" -Name web-url-path -PropertyType String -Value $webURLPath -Force
|
||||||
|
|
||||||
|
# Attempt to restart service
|
||||||
|
if (Get-Service -Name "dcvserver" -ErrorAction SilentlyContinue) {
|
||||||
|
Restart-Service -Name "dcvserver"
|
||||||
|
} else {
|
||||||
|
Write-Output "[WARNING] dcvserver service not found. Ensure the system was restarted properly."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "[INFO] DCV configuration completed"
|
||||||
|
Read-Host "[DEBUG] Press Enter to proceed to the next step"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main Script Execution
|
||||||
|
Write-Output "[INFO] Starting script"
|
||||||
|
$VirtualDisplayDriverRequired = [bool](Get-VirtualDisplayDriverRequired)
|
||||||
|
Set-LocalAdminUser
|
||||||
|
Download-DCV -VirtualDisplayDriverRequired $VirtualDisplayDriverRequired
|
||||||
|
Install-DCV -VirtualDisplayDriverRequired $VirtualDisplayDriverRequired
|
||||||
|
Configure-DCV
|
||||||
|
Write-Output "[INFO] Script completed"
|
||||||
85
amazon-dcv-windows/main.tf
Normal file
85
amazon-dcv-windows/main.tf
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "admin_password" {
|
||||||
|
type = string
|
||||||
|
default = "coderDCV!"
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "port" {
|
||||||
|
type = number
|
||||||
|
description = "The port number for the DCV server."
|
||||||
|
default = 8443
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "subdomain" {
|
||||||
|
type = bool
|
||||||
|
description = "Whether to use a subdomain for the DCV server."
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "slug" {
|
||||||
|
type = string
|
||||||
|
description = "The slug of the web-dcv coder_app resource."
|
||||||
|
default = "web-dcv"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_app" "web-dcv" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
slug = var.slug
|
||||||
|
display_name = "Web DCV"
|
||||||
|
url = "https://localhost:${var.port}${local.web_url_path}?username=${local.admin_username}&password=${var.admin_password}"
|
||||||
|
icon = "/icon/dcv.svg"
|
||||||
|
subdomain = var.subdomain
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_script" "install-dcv" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "Install DCV"
|
||||||
|
icon = "/icon/dcv.svg"
|
||||||
|
run_on_start = true
|
||||||
|
script = templatefile("${path.module}/install-dcv.ps1", {
|
||||||
|
admin_password : var.admin_password,
|
||||||
|
port : var.port,
|
||||||
|
web_url_path : local.web_url_path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
web_url_path = var.subdomain ? "/" : format("/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.slug)
|
||||||
|
admin_username = "Administrator"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "web_url_path" {
|
||||||
|
value = local.web_url_path
|
||||||
|
}
|
||||||
|
|
||||||
|
output "username" {
|
||||||
|
value = local.admin_username
|
||||||
|
}
|
||||||
|
|
||||||
|
output "password" {
|
||||||
|
value = var.admin_password
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "port" {
|
||||||
|
value = var.port
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ A module that adds Apache Airflow in your Coder template.
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "airflow" {
|
module "airflow" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/apache-airflow/coder"
|
source = "registry.coder.com/modules/apache-airflow/coder"
|
||||||
version = "1.0.13"
|
version = "1.0.13"
|
||||||
agent_id = coder_agent.main.id
|
agent_id = coder_agent.main.id
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Customize the preselected parameter value:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "aws-region" {
|
module "aws-region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/aws-region/coder"
|
source = "registry.coder.com/modules/aws-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
default = "us-east-1"
|
default = "us-east-1"
|
||||||
@@ -36,6 +37,7 @@ Change the display name and icon for a region using the corresponding maps:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "aws-region" {
|
module "aws-region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/aws-region/coder"
|
source = "registry.coder.com/modules/aws-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
default = "ap-south-1"
|
default = "ap-south-1"
|
||||||
@@ -62,6 +64,7 @@ Hide the Asia Pacific regions Seoul and Osaka:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "aws-region" {
|
module "aws-region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/aws-region/coder"
|
source = "registry.coder.com/modules/aws-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
exclude = ["ap-northeast-2", "ap-northeast-3"]
|
exclude = ["ap-northeast-2", "ap-northeast-3"]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import {
|
import {
|
||||||
executeScriptInContainer,
|
|
||||||
runTerraformApply,
|
runTerraformApply,
|
||||||
runTerraformInit,
|
runTerraformInit,
|
||||||
testRequiredVariables,
|
testRequiredVariables,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ This module adds a parameter with all Azure regions, allowing developers to sele
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "azure_region" {
|
module "azure_region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/azure-region/coder"
|
source = "registry.coder.com/modules/azure-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
default = "eastus"
|
default = "eastus"
|
||||||
@@ -33,6 +34,7 @@ Change the display name and icon for a region using the corresponding maps:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "azure-region" {
|
module "azure-region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/azure-region/coder"
|
source = "registry.coder.com/modules/azure-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
custom_names = {
|
custom_names = {
|
||||||
@@ -56,6 +58,7 @@ Hide all regions in Australia except australiacentral:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "azure-region" {
|
module "azure-region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/azure-region/coder"
|
source = "registry.coder.com/modules/azure-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import {
|
import {
|
||||||
executeScriptInContainer,
|
|
||||||
runTerraformApply,
|
runTerraformApply,
|
||||||
runTerraformInit,
|
runTerraformInit,
|
||||||
testRequiredVariables,
|
testRequiredVariables,
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -27,8 +28,9 @@ module "code-server" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
install_version = "4.8.3"
|
install_version = "4.8.3"
|
||||||
}
|
}
|
||||||
@@ -40,8 +42,9 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
extensions = [
|
extensions = [
|
||||||
"dracula-theme.theme-dracula"
|
"dracula-theme.theme-dracula"
|
||||||
@@ -53,12 +56,13 @@ Enter the `<author>.<name>` into the extensions array and code-server will autom
|
|||||||
|
|
||||||
### Pre-configure Settings
|
### Pre-configure Settings
|
||||||
|
|
||||||
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settingsjson) file:
|
Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarted/settings#_settings-json-file) file:
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
extensions = ["dracula-theme.theme-dracula"]
|
extensions = ["dracula-theme.theme-dracula"]
|
||||||
settings = {
|
settings = {
|
||||||
@@ -73,8 +77,9 @@ Just run code-server in the background, don't fetch it from GitHub:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.29"
|
||||||
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"]
|
||||||
}
|
}
|
||||||
@@ -88,8 +93,9 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.29"
|
||||||
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"]
|
||||||
@@ -100,8 +106,9 @@ Just run code-server in the background, don't fetch it from GitHub:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/code-server/coder"
|
source = "registry.coder.com/modules/code-server/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
offline = true
|
offline = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ variable "slug" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "settings" {
|
variable "settings" {
|
||||||
type = map(string)
|
type = any
|
||||||
description = "A map of settings to apply to code-server."
|
description = "A map of settings to apply to code-server."
|
||||||
default = {}
|
default = {}
|
||||||
}
|
}
|
||||||
@@ -95,6 +95,12 @@ variable "use_cached" {
|
|||||||
default = false
|
default = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "use_cached_extensions" {
|
||||||
|
type = bool
|
||||||
|
description = "Uses cached copy of extensions, otherwise do a forced upgrade"
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
variable "extensions_dir" {
|
variable "extensions_dir" {
|
||||||
type = string
|
type = string
|
||||||
description = "Override the directory to store extensions in."
|
description = "Override the directory to store extensions in."
|
||||||
@@ -107,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"
|
||||||
@@ -122,6 +137,7 @@ resource "coder_script" "code-server" {
|
|||||||
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
|
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
|
||||||
OFFLINE : var.offline,
|
OFFLINE : var.offline,
|
||||||
USE_CACHED : var.use_cached,
|
USE_CACHED : var.use_cached,
|
||||||
|
USE_CACHED_EXTENSIONS : var.use_cached_extensions,
|
||||||
EXTENSIONS_DIR : var.extensions_dir,
|
EXTENSIONS_DIR : var.extensions_dir,
|
||||||
FOLDER : var.folder,
|
FOLDER : var.folder,
|
||||||
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
|
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
|
||||||
@@ -147,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() {
|
||||||
@@ -25,36 +26,53 @@ if [ ! -f ~/.local/share/code-server/User/settings.json ]; then
|
|||||||
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
|
echo "${SETTINGS}" > ~/.local/share/code-server/User/settings.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if code-server is already installed for offline or cached mode
|
# Check if code-server is already installed for offline
|
||||||
if [ -f "$CODE_SERVER" ]; then
|
if [ "${OFFLINE}" = true ]; then
|
||||||
if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
|
if [ -f "$CODE_SERVER" ]; then
|
||||||
echo "🥳 Found a copy of code-server"
|
echo "🥳 Found a copy of code-server"
|
||||||
run_code_server
|
run_code_server
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
# Offline mode always expects a copy of code-server to be present
|
||||||
# Offline mode always expects a copy of code-server to be present
|
|
||||||
if [ "${OFFLINE}" = true ]; then
|
|
||||||
echo "Failed to find a copy of code-server"
|
echo "Failed to find a copy of code-server"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf "$${BOLD}Installing code-server!\n"
|
# If there is no cached install OR we don't want to use a cached install
|
||||||
|
if [ ! -f "$CODE_SERVER" ] || [ "${USE_CACHED}" != true ]; then
|
||||||
|
printf "$${BOLD}Installing code-server!\n"
|
||||||
|
|
||||||
ARGS=(
|
ARGS=(
|
||||||
"--method=standalone"
|
"--method=standalone"
|
||||||
"--prefix=${INSTALL_PREFIX}"
|
"--prefix=${INSTALL_PREFIX}"
|
||||||
)
|
)
|
||||||
if [ -n "${VERSION}" ]; then
|
if [ -n "${VERSION}" ]; then
|
||||||
ARGS+=("--version=${VERSION}")
|
ARGS+=("--version=${VERSION}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
output=$(curl -fsSL https://code-server.dev/install.sh | sh -s -- "$${ARGS[@]}")
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed to install code-server: $output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
output=$(curl -fsSL https://code-server.dev/install.sh | sh -s -- "$${ARGS[@]}")
|
# Get the list of installed extensions...
|
||||||
if [ $? -ne 0 ]; then
|
LIST_EXTENSIONS=$($CODE_SERVER --list-extensions $EXTENSION_ARG)
|
||||||
echo "Failed to install code-server: $output"
|
readarray -t EXTENSIONS_ARRAY <<< "$LIST_EXTENSIONS"
|
||||||
exit 1
|
function extension_installed() {
|
||||||
fi
|
if [ "${USE_CACHED_EXTENSIONS}" != true ]; then
|
||||||
printf "🥳 code-server has been installed in ${INSTALL_PREFIX}\n\n"
|
return 1
|
||||||
|
fi
|
||||||
|
for _extension in "$${EXTENSIONS_ARRAY[@]}"; do
|
||||||
|
if [ "$_extension" == "$1" ]; then
|
||||||
|
echo "Extension $1 was already installed."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# Install each extension...
|
# Install each extension...
|
||||||
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
||||||
@@ -62,8 +80,11 @@ for extension in "$${EXTENSIONLIST[@]}"; do
|
|||||||
if [ -z "$extension" ]; then
|
if [ -z "$extension" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
if extension_installed "$extension"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
|
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
|
||||||
output=$($CODE_SERVER "$EXTENSION_ARG" --install-extension "$extension")
|
output=$($CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension")
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Failed to install extension: $extension: $output"
|
echo "Failed to install extension: $extension: $output"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -83,9 +104,13 @@ if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
|
|||||||
|
|
||||||
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
|
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
|
||||||
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
|
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
|
||||||
extensions=$(jq -r '.recommendations[]' "$WORKSPACE_DIR"/.vscode/extensions.json)
|
# Use sed to remove single-line comments before parsing with jq
|
||||||
|
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR"/.vscode/extensions.json | jq -r '.recommendations[]')
|
||||||
for extension in $extensions; do
|
for extension in $extensions; do
|
||||||
$CODE_SERVER "$EXTENSION_ARG" --install-extension "$extension"
|
if extension_installed "$extension"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
$CODE_SERVER "$EXTENSION_ARG" --force --install-extension "$extension"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ Automatically logs the user into Coder when creating their workspace.
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "coder-login" {
|
module "coder-login" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/coder-login/coder"
|
source = "registry.coder.com/modules/coder-login/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.15"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe } from "bun:test";
|
||||||
import {
|
import { runTerraformInit, testRequiredVariables } from "../test";
|
||||||
executeScriptInContainer,
|
|
||||||
runTerraformApply,
|
|
||||||
runTerraformInit,
|
|
||||||
testRequiredVariables,
|
|
||||||
} from "../test";
|
|
||||||
|
|
||||||
describe("coder-login", async () => {
|
describe("coder-login", async () => {
|
||||||
await runTerraformInit(import.meta.dir);
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
coder = {
|
coder = {
|
||||||
source = "coder/coder"
|
source = "coder/coder"
|
||||||
version = ">= 0.12"
|
version = ">= 0.23"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,11 +15,12 @@ variable "agent_id" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data "coder_workspace" "me" {}
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
resource "coder_script" "coder-login" {
|
resource "coder_script" "coder-login" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
script = templatefile("${path.module}/run.sh", {
|
script = templatefile("${path.module}/run.sh", {
|
||||||
CODER_USER_TOKEN : data.coder_workspace.me.owner_session_token,
|
CODER_USER_TOKEN : data.coder_workspace_owner.me.session_token,
|
||||||
CODER_DEPLOYMENT_URL : data.coder_workspace.me.access_url
|
CODER_DEPLOYMENT_URL : data.coder_workspace.me.access_url
|
||||||
})
|
})
|
||||||
display_name = "Coder Login"
|
display_name = "Coder Login"
|
||||||
|
|||||||
37
cursor/README.md
Normal file
37
cursor/README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
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" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
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" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/cursor/coder"
|
||||||
|
version = "1.0.19"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
folder = "/home/coder/project"
|
||||||
|
}
|
||||||
|
```
|
||||||
88
cursor/main.test.ts
Normal file
88
cursor/main.test.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
describe("cursor", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("default output", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
|
||||||
|
const coder_app = state.resources.find(
|
||||||
|
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(coder_app).not.toBeNull();
|
||||||
|
expect(coder_app?.instances.length).toBe(1);
|
||||||
|
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds folder", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
folder: "/foo/bar",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds folder and open_recent", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
folder: "/foo/bar",
|
||||||
|
open_recent: "true",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds folder but not open_recent", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
folder: "/foo/bar",
|
||||||
|
openRecent: "false",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds open_recent", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
open_recent: "true",
|
||||||
|
});
|
||||||
|
expect(state.outputs.cursor_url.value).toBe(
|
||||||
|
"cursor://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("expect order to be set", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
order: "22",
|
||||||
|
});
|
||||||
|
|
||||||
|
const coder_app = state.resources.find(
|
||||||
|
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(coder_app).not.toBeNull();
|
||||||
|
expect(coder_app?.instances.length).toBe(1);
|
||||||
|
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||||
|
});
|
||||||
|
});
|
||||||
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."
|
||||||
|
}
|
||||||
@@ -17,8 +17,9 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -29,8 +30,9 @@ module "dotfiles" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.0"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -39,8 +41,9 @@ module "dotfiles" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.0"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
user = "root"
|
user = "root"
|
||||||
}
|
}
|
||||||
@@ -50,14 +53,16 @@ module "dotfiles" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.0"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
|
|
||||||
module "dotfiles-root" {
|
module "dotfiles-root" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.0"
|
version = "1.0.29"
|
||||||
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
|
||||||
@@ -70,8 +75,9 @@ You can set a default dotfiles repository for all users by setting the `default_
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "dotfiles" {
|
module "dotfiles" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/dotfiles/coder"
|
source = "registry.coder.com/modules/dotfiles/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.29"
|
||||||
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,12 +39,17 @@ 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 (optional)"
|
display_name = "Dotfiles URL"
|
||||||
order = var.coder_parameter_order
|
order = var.coder_parameter_order
|
||||||
default = var.default_dotfiles_uri
|
default = var.default_dotfiles_uri
|
||||||
description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
|
description = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
DOTFILES_URI="${DOTFILES_URI}"
|
DOTFILES_URI="${DOTFILES_URI}"
|
||||||
DOTFILES_USER="${DOTFILES_USER}"
|
DOTFILES_USER="${DOTFILES_USER}"
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Customize the preselected parameter value:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "exoscale-instance-type" {
|
module "exoscale-instance-type" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
default = "standard.medium"
|
default = "standard.medium"
|
||||||
@@ -44,6 +45,7 @@ Change the display name a type using the corresponding maps:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "exoscale-instance-type" {
|
module "exoscale-instance-type" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
default = "standard.medium"
|
default = "standard.medium"
|
||||||
@@ -78,6 +80,7 @@ Show only gpu1 types
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "exoscale-instance-type" {
|
module "exoscale-instance-type" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
default = "gpu.large"
|
default = "gpu.large"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Customize the preselected parameter value:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "exoscale-zone" {
|
module "exoscale-zone" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
default = "ch-dk-2"
|
default = "ch-dk-2"
|
||||||
@@ -43,6 +44,7 @@ Change the display name and icon for a zone using the corresponding maps:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "exoscale-zone" {
|
module "exoscale-zone" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
default = "at-vie-1"
|
default = "at-vie-1"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import {
|
import {
|
||||||
executeScriptInContainer,
|
|
||||||
runTerraformApply,
|
runTerraformApply,
|
||||||
runTerraformInit,
|
runTerraformInit,
|
||||||
testRequiredVariables,
|
testRequiredVariables,
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ A file browser for your workspace.
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -27,8 +28,9 @@ module "filebrowser" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.29"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
folder = "/home/coder/project"
|
folder = "/home/coder/project"
|
||||||
}
|
}
|
||||||
@@ -38,9 +40,23 @@ module "filebrowser" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.29"
|
||||||
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" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
|
version = "1.0.29"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
agent_name = "main"
|
||||||
|
subdomain = false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe("filebrowser", async () => {
|
|||||||
"",
|
"",
|
||||||
"📂 Serving /root at http://localhost:13339 ",
|
"📂 Serving /root at http://localhost:13339 ",
|
||||||
"",
|
"",
|
||||||
"Running 'filebrowser --noauth --root /root --port 13339' ",
|
"Running 'filebrowser --noauth --root /root --port 13339 --baseurl ' ",
|
||||||
"",
|
"",
|
||||||
"📝 Logs at /tmp/filebrowser.log",
|
"📝 Logs at /tmp/filebrowser.log",
|
||||||
]);
|
]);
|
||||||
@@ -61,7 +61,7 @@ describe("filebrowser", async () => {
|
|||||||
"",
|
"",
|
||||||
"📂 Serving /root at http://localhost:13339 ",
|
"📂 Serving /root at http://localhost:13339 ",
|
||||||
"",
|
"",
|
||||||
"Running 'filebrowser --noauth --root /root --port 13339 -d .config/filebrowser.db' ",
|
"Running 'filebrowser --noauth --root /root --port 13339 -d .config/filebrowser.db --baseurl ' ",
|
||||||
"",
|
"",
|
||||||
"📝 Logs at /tmp/filebrowser.log",
|
"📝 Logs at /tmp/filebrowser.log",
|
||||||
]);
|
]);
|
||||||
@@ -75,7 +75,7 @@ describe("filebrowser", async () => {
|
|||||||
const output = await executeScriptInContainer(state, "alpine");
|
const output = await executeScriptInContainer(state, "alpine");
|
||||||
expect(output.exitCode).toBe(0);
|
expect(output.exitCode).toBe(0);
|
||||||
expect(output.stdout).toEqual([
|
expect(output.stdout).toEqual([
|
||||||
"\u001B[0;1mInstalling filebrowser ",
|
"\u001b[0;1mInstalling filebrowser ",
|
||||||
"",
|
"",
|
||||||
"🥳 Installation complete! ",
|
"🥳 Installation complete! ",
|
||||||
"",
|
"",
|
||||||
@@ -83,7 +83,30 @@ describe("filebrowser", async () => {
|
|||||||
"",
|
"",
|
||||||
"📂 Serving /home/coder/project at http://localhost:13339 ",
|
"📂 Serving /home/coder/project at http://localhost:13339 ",
|
||||||
"",
|
"",
|
||||||
"Running 'filebrowser --noauth --root /home/coder/project --port 13339' ",
|
"Running 'filebrowser --noauth --root /home/coder/project --port 13339 --baseurl ' ",
|
||||||
|
"",
|
||||||
|
"📝 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 --baseurl /@default/default.main/apps/filebrowser' ",
|
||||||
"",
|
"",
|
||||||
"📝 Logs at /tmp/filebrowser.log",
|
"📝 Logs at /tmp/filebrowser.log",
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ 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 coder_agent resource. (Only required if subdomain is false and the template uses multiple agents.)"
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
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,27 +68,56 @@ variable "order" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "slug" {
|
||||||
|
type = string
|
||||||
|
description = "The slug of the coder_app resource."
|
||||||
|
default = "filebrowser"
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
|
icon = "/icon/filebrowser.svg"
|
||||||
script = templatefile("${path.module}/run.sh", {
|
script = templatefile("${path.module}/run.sh", {
|
||||||
LOG_PATH : var.log_path,
|
LOG_PATH : var.log_path,
|
||||||
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 : local.server_base_path
|
||||||
})
|
})
|
||||||
run_on_start = true
|
run_on_start = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "coder_app" "filebrowser" {
|
resource "coder_app" "filebrowser" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
slug = "filebrowser"
|
slug = var.slug
|
||||||
display_name = "File Browser"
|
display_name = "File Browser"
|
||||||
url = "http://localhost:${var.port}"
|
url = local.url
|
||||||
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
|
icon = "/icon/filebrowser.svg"
|
||||||
subdomain = true
|
subdomain = var.subdomain
|
||||||
share = var.share
|
share = var.share
|
||||||
order = var.order
|
order = var.order
|
||||||
|
|
||||||
|
healthcheck {
|
||||||
|
url = local.healthcheck_url
|
||||||
|
interval = 5
|
||||||
|
threshold = 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
server_base_path = var.subdomain ? "" : format("/@%s/%s%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name != null ? ".${var.agent_name}" : "", var.slug)
|
||||||
|
url = "http://localhost:${var.port}${local.server_base_path}"
|
||||||
|
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/health"
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
BOLD='\033[0;1m'
|
BOLD='\033[0;1m'
|
||||||
|
|
||||||
printf "$${BOLD}Installing filebrowser \n\n"
|
printf "$${BOLD}Installing filebrowser \n\n"
|
||||||
|
|
||||||
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
|
# Check if filebrowser is installed
|
||||||
|
if ! command -v filebrowser &> /dev/null; then
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash
|
||||||
|
fi
|
||||||
|
|
||||||
printf "🥳 Installation complete! \n\n"
|
printf "🥳 Installation complete! \n\n"
|
||||||
|
|
||||||
@@ -19,8 +23,8 @@ fi
|
|||||||
|
|
||||||
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} --baseurl ${SERVER_BASE_PATH}' \n\n"
|
||||||
|
|
||||||
filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} > ${LOG_PATH} 2>&1 &
|
filebrowser --noauth --root $ROOT_DIR --port ${PORT}$${DB_FLAG} --baseurl ${SERVER_BASE_PATH} > ${LOG_PATH} 2>&1 &
|
||||||
|
|
||||||
printf "📝 Logs at ${LOG_PATH} \n\n"
|
printf "📝 Logs at ${LOG_PATH} \n\n"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ We can use the simplest format here, only adding a default selection as the `atl
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "fly-region" {
|
module "fly-region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/fly-region/coder"
|
source = "registry.coder.com/modules/fly-region/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
default = "atl"
|
default = "atl"
|
||||||
@@ -31,6 +32,7 @@ The regions argument can be used to display only the desired regions in the Code
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "fly-region" {
|
module "fly-region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/fly-region/coder"
|
source = "registry.coder.com/modules/fly-region/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
default = "ams"
|
default = "ams"
|
||||||
@@ -46,6 +48,7 @@ Set custom icons and names with their respective maps.
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "fly-region" {
|
module "fly-region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/fly-region/coder"
|
source = "registry.coder.com/modules/fly-region/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
default = "ams"
|
default = "ams"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ This module adds Google Cloud Platform regions to your Coder template.
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "gcp_region" {
|
module "gcp_region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/gcp-region/coder"
|
source = "registry.coder.com/modules/gcp-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
regions = ["us", "europe"]
|
regions = ["us", "europe"]
|
||||||
@@ -33,6 +34,7 @@ Note: setting `gpu_only = true` and using a default region without GPU support,
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "gcp_region" {
|
module "gcp_region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/gcp-region/coder"
|
source = "registry.coder.com/modules/gcp-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
default = ["us-west1-a"]
|
default = ["us-west1-a"]
|
||||||
@@ -49,6 +51,7 @@ resource "google_compute_instance" "example" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "gcp_region" {
|
module "gcp_region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/gcp-region/coder"
|
source = "registry.coder.com/modules/gcp-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
regions = ["europe-west"]
|
regions = ["europe-west"]
|
||||||
@@ -64,6 +67,7 @@ resource "google_compute_instance" "example" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "gcp_region" {
|
module "gcp_region" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/gcp-region/coder"
|
source = "registry.coder.com/modules/gcp-region/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
regions = ["us", "europe"]
|
regions = ["us", "europe"]
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ This module allows you to automatically clone a repository by URL and skip if it
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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"
|
||||||
}
|
}
|
||||||
@@ -26,8 +27,9 @@ module "git-clone" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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"
|
||||||
@@ -40,8 +42,9 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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"
|
||||||
}
|
}
|
||||||
@@ -65,31 +68,33 @@ 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" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create a code-server instance for the cloned repository
|
# Create a code-server instance for the cloned repository
|
||||||
module "code-server" {
|
module "code-server" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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[count.index].folder_name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create a Coder app for the website
|
# Create a Coder app for the website
|
||||||
resource "coder_app" "website" {
|
resource "coder_app" "website" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
order = 2
|
order = 2
|
||||||
slug = "website"
|
slug = "website"
|
||||||
external = true
|
external = true
|
||||||
display_name = module.git_clone.folder_name
|
display_name = module.git_clone[count.index].folder_name
|
||||||
url = module.git_clone.web_url
|
url = module.git_clone[count.index].web_url
|
||||||
icon = module.git_clone.git_provider != "" ? "/icon/${module.git_clone.git_provider}.svg" : "/icon/git.svg"
|
icon = module.git_clone[count.index].git_provider != "" ? "/icon/${module.git_clone[count.index].git_provider}.svg" : "/icon/git.svg"
|
||||||
count = module.git_clone.web_url != "" ? 1 : 0
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -97,8 +102,9 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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 = {
|
||||||
@@ -115,8 +121,9 @@ To GitLab clone with a specific branch like `feat/example`
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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"
|
||||||
}
|
}
|
||||||
@@ -126,8 +133,9 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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 = {
|
||||||
@@ -146,10 +154,29 @@ For example, to clone the `feat/example` branch:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-clone" {
|
module "git-clone" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
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,13 +2,16 @@
|
|||||||
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]
|
||||||
---
|
---
|
||||||
|
|
||||||
# git-commit-signing
|
# git-commit-signing
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> This module will only work with Git versions >=2.34, prior versions [do not support signing commits via SSH keys](https://lore.kernel.org/git/xmqq8rxpgwki.fsf@gitster.g/).
|
||||||
|
|
||||||
This module downloads your SSH key from Coder and uses it to sign commits with Git.
|
This module downloads your SSH key from Coder and uses it to sign commits with Git.
|
||||||
It requires `curl` and `jq` to be installed inside your workspace.
|
It requires `curl` and `jq` to be installed inside your workspace.
|
||||||
|
|
||||||
@@ -18,6 +21,7 @@ This module has a chance of conflicting with the user's dotfiles / the personali
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-commit-signing" {
|
module "git-commit-signing" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/git-commit-signing/coder"
|
source = "registry.coder.com/modules/git-commit-signing/coder"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ Runs a script that updates git credentials in the workspace to match the user's
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-config" {
|
module "git-config" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/git-config/coder"
|
source = "registry.coder.com/modules/git-config/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.15"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -27,8 +28,9 @@ TODO: Add screenshot
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-config" {
|
module "git-config" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/git-config/coder"
|
source = "registry.coder.com/modules/git-config/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.15"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
allow_email_change = true
|
allow_email_change = true
|
||||||
}
|
}
|
||||||
@@ -40,8 +42,9 @@ TODO: Add screenshot
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "git-config" {
|
module "git-config" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/git-config/coder"
|
source = "registry.coder.com/modules/git-config/coder"
|
||||||
version = "1.0.12"
|
version = "1.0.15"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
allow_username_change = false
|
allow_username_change = false
|
||||||
allow_email_change = false
|
allow_email_change = false
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ describe("git-config", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resources = state.resources;
|
const resources = state.resources;
|
||||||
expect(resources).toHaveLength(3);
|
expect(resources).toHaveLength(6);
|
||||||
expect(resources).toMatchObject([
|
expect(resources).toMatchObject([
|
||||||
{ type: "coder_workspace", name: "me" },
|
{ type: "coder_workspace", name: "me" },
|
||||||
|
{ type: "coder_workspace_owner", name: "me" },
|
||||||
|
{ type: "coder_env", name: "git_author_email" },
|
||||||
{ type: "coder_env", name: "git_author_name" },
|
{ type: "coder_env", name: "git_author_name" },
|
||||||
|
{ type: "coder_env", name: "git_commmiter_email" },
|
||||||
{ type: "coder_env", name: "git_commmiter_name" },
|
{ type: "coder_env", name: "git_commmiter_name" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -35,12 +38,15 @@ describe("git-config", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resources = state.resources;
|
const resources = state.resources;
|
||||||
expect(resources).toHaveLength(5);
|
expect(resources).toHaveLength(8);
|
||||||
expect(resources).toMatchObject([
|
expect(resources).toMatchObject([
|
||||||
{ type: "coder_parameter", name: "user_email" },
|
{ type: "coder_parameter", name: "user_email" },
|
||||||
{ type: "coder_parameter", name: "username" },
|
{ type: "coder_parameter", name: "username" },
|
||||||
{ type: "coder_workspace", name: "me" },
|
{ type: "coder_workspace", name: "me" },
|
||||||
|
{ type: "coder_workspace_owner", name: "me" },
|
||||||
|
{ type: "coder_env", name: "git_author_email" },
|
||||||
{ type: "coder_env", name: "git_author_name" },
|
{ type: "coder_env", name: "git_author_name" },
|
||||||
|
{ type: "coder_env", name: "git_commmiter_email" },
|
||||||
{ type: "coder_env", name: "git_commmiter_name" },
|
{ type: "coder_env", name: "git_commmiter_name" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -53,13 +59,14 @@ describe("git-config", async () => {
|
|||||||
allow_username_change: "false",
|
allow_username_change: "false",
|
||||||
allow_email_change: "false",
|
allow_email_change: "false",
|
||||||
},
|
},
|
||||||
{ CODER_WORKSPACE_OWNER_EMAIL: "foo@emai.com" },
|
{ CODER_WORKSPACE_OWNER_EMAIL: "foo@email.com" },
|
||||||
);
|
);
|
||||||
|
|
||||||
const resources = state.resources;
|
const resources = state.resources;
|
||||||
expect(resources).toHaveLength(5);
|
expect(resources).toHaveLength(6);
|
||||||
expect(resources).toMatchObject([
|
expect(resources).toMatchObject([
|
||||||
{ type: "coder_workspace", name: "me" },
|
{ type: "coder_workspace", name: "me" },
|
||||||
|
{ type: "coder_workspace_owner", name: "me" },
|
||||||
{ type: "coder_env", name: "git_author_email" },
|
{ type: "coder_env", name: "git_author_email" },
|
||||||
{ type: "coder_env", name: "git_author_name" },
|
{ type: "coder_env", name: "git_author_name" },
|
||||||
{ type: "coder_env", name: "git_commmiter_email" },
|
{ type: "coder_env", name: "git_commmiter_email" },
|
||||||
@@ -75,12 +82,23 @@ describe("git-config", async () => {
|
|||||||
allow_email_change: "true",
|
allow_email_change: "true",
|
||||||
coder_parameter_order: order.toString(),
|
coder_parameter_order: order.toString(),
|
||||||
});
|
});
|
||||||
expect(state.resources).toHaveLength(5);
|
const resources = state.resources;
|
||||||
|
expect(resources).toHaveLength(8);
|
||||||
|
expect(resources).toMatchObject([
|
||||||
|
{ type: "coder_parameter", name: "user_email" },
|
||||||
|
{ type: "coder_parameter", name: "username" },
|
||||||
|
{ type: "coder_workspace", name: "me" },
|
||||||
|
{ type: "coder_workspace_owner", name: "me" },
|
||||||
|
{ type: "coder_env", name: "git_author_email" },
|
||||||
|
{ type: "coder_env", name: "git_author_name" },
|
||||||
|
{ type: "coder_env", name: "git_commmiter_email" },
|
||||||
|
{ type: "coder_env", name: "git_commmiter_name" },
|
||||||
|
]);
|
||||||
// user_email order is the same as the order
|
// user_email order is the same as the order
|
||||||
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
expect(resources[0].instances[0].attributes.order).toBe(order);
|
||||||
// username order is incremented by 1
|
// username order is incremented by 1
|
||||||
// @ts-ignore: Object is possibly 'null'.
|
// @ts-ignore: Object is possibly 'null'.
|
||||||
expect(state.resources[1].instances[0]?.attributes.order).toBe(order + 1);
|
expect(resources[1].instances[0]?.attributes.order).toBe(order + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("set custom order for coder_parameter for just username", async () => {
|
it("set custom order for coder_parameter for just username", async () => {
|
||||||
@@ -91,9 +109,19 @@ describe("git-config", async () => {
|
|||||||
allow_username_change: "true",
|
allow_username_change: "true",
|
||||||
coder_parameter_order: order.toString(),
|
coder_parameter_order: order.toString(),
|
||||||
});
|
});
|
||||||
expect(state.resources).toHaveLength(4);
|
const resources = state.resources;
|
||||||
|
expect(resources).toHaveLength(7);
|
||||||
|
expect(resources).toMatchObject([
|
||||||
|
{ type: "coder_parameter", name: "username" },
|
||||||
|
{ type: "coder_workspace", name: "me" },
|
||||||
|
{ type: "coder_workspace_owner", name: "me" },
|
||||||
|
{ type: "coder_env", name: "git_author_email" },
|
||||||
|
{ type: "coder_env", name: "git_author_name" },
|
||||||
|
{ type: "coder_env", name: "git_commmiter_email" },
|
||||||
|
{ type: "coder_env", name: "git_commmiter_name" },
|
||||||
|
]);
|
||||||
// user_email was not created
|
// user_email was not created
|
||||||
// username order is incremented by 1
|
// username order is incremented by 1
|
||||||
expect(state.resources[0].instances[0].attributes.order).toBe(order + 1);
|
expect(resources[0].instances[0].attributes.order).toBe(order + 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
coder = {
|
coder = {
|
||||||
source = "coder/coder"
|
source = "coder/coder"
|
||||||
version = ">= 0.13"
|
version = ">= 0.23"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,7 @@ variable "coder_parameter_order" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data "coder_workspace" "me" {}
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
data "coder_parameter" "user_email" {
|
data "coder_parameter" "user_email" {
|
||||||
count = var.allow_email_change ? 1 : 0
|
count = var.allow_email_change ? 1 : 0
|
||||||
@@ -59,25 +60,25 @@ data "coder_parameter" "username" {
|
|||||||
resource "coder_env" "git_author_name" {
|
resource "coder_env" "git_author_name" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
name = "GIT_AUTHOR_NAME"
|
name = "GIT_AUTHOR_NAME"
|
||||||
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
|
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "coder_env" "git_commmiter_name" {
|
resource "coder_env" "git_commmiter_name" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
name = "GIT_COMMITTER_NAME"
|
name = "GIT_COMMITTER_NAME"
|
||||||
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace.me.owner_name, data.coder_workspace.me.owner)
|
value = coalesce(try(data.coder_parameter.username[0].value, ""), data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "coder_env" "git_author_email" {
|
resource "coder_env" "git_author_email" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
name = "GIT_AUTHOR_EMAIL"
|
name = "GIT_AUTHOR_EMAIL"
|
||||||
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
|
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace_owner.me.email)
|
||||||
count = data.coder_workspace.me.owner_email != "" ? 1 : 0
|
count = data.coder_workspace_owner.me.email != "" ? 1 : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "coder_env" "git_commmiter_email" {
|
resource "coder_env" "git_commmiter_email" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
name = "GIT_COMMITTER_EMAIL"
|
name = "GIT_COMMITTER_EMAIL"
|
||||||
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace.me.owner_email)
|
value = coalesce(try(data.coder_parameter.user_email[0].value, ""), data.coder_workspace_owner.me.email)
|
||||||
count = data.coder_workspace.me.owner_email != "" ? 1 : 0
|
count = data.coder_workspace_owner.me.email != "" ? 1 : 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ Templates that utilize Github External Auth can automatically ensure that the Co
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "github-upload-public-key" {
|
module "github-upload-public-key" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||||
version = "1.0.13"
|
version = "1.0.15"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -45,8 +46,9 @@ data "coder_external_auth" "github" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module "github-upload-public-key" {
|
module "github-upload-public-key" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||||
version = "1.0.13"
|
version = "1.0.15"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
external_auth_id = data.coder_external_auth.github.id
|
external_auth_id = data.coder_external_auth.github.id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { type Server, serve } from "bun";
|
||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import {
|
import {
|
||||||
createJSONResponse,
|
createJSONResponse,
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
testRequiredVariables,
|
testRequiredVariables,
|
||||||
writeCoder,
|
writeCoder,
|
||||||
} from "../test";
|
} from "../test";
|
||||||
import { Server, serve } from "bun";
|
|
||||||
|
|
||||||
describe("github-upload-public-key", async () => {
|
describe("github-upload-public-key", async () => {
|
||||||
await runTerraformInit(import.meta.dir);
|
await runTerraformInit(import.meta.dir);
|
||||||
@@ -21,10 +21,12 @@ describe("github-upload-public-key", async () => {
|
|||||||
it("creates new key if one does not exist", async () => {
|
it("creates new key if one does not exist", async () => {
|
||||||
const { instance, id, server } = await setupContainer();
|
const { instance, id, server } = await setupContainer();
|
||||||
await writeCoder(id, "echo foo");
|
await writeCoder(id, "echo foo");
|
||||||
let exec = await execContainer(id, [
|
|
||||||
|
const url = server.url.toString().slice(0, -1);
|
||||||
|
const exec = await execContainer(id, [
|
||||||
"env",
|
"env",
|
||||||
"CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
|
`CODER_ACCESS_URL=${url}`,
|
||||||
"GITHUB_API_URL=" + server.url.toString().slice(0, -1),
|
`GITHUB_API_URL=${url}`,
|
||||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||||
"CODER_EXTERNAL_AUTH_ID=github",
|
"CODER_EXTERNAL_AUTH_ID=github",
|
||||||
"bash",
|
"bash",
|
||||||
@@ -42,10 +44,12 @@ describe("github-upload-public-key", async () => {
|
|||||||
const { instance, id, server } = await setupContainer();
|
const { instance, id, server } = await setupContainer();
|
||||||
// use keyword to make server return a existing key
|
// use keyword to make server return a existing key
|
||||||
await writeCoder(id, "echo findkey");
|
await writeCoder(id, "echo findkey");
|
||||||
let exec = await execContainer(id, [
|
|
||||||
|
const url = server.url.toString().slice(0, -1);
|
||||||
|
const exec = await execContainer(id, [
|
||||||
"env",
|
"env",
|
||||||
"CODER_ACCESS_URL=" + server.url.toString().slice(0, -1),
|
`CODER_ACCESS_URL=${url}`,
|
||||||
"GITHUB_API_URL=" + server.url.toString().slice(0, -1),
|
`GITHUB_API_URL=${url}`,
|
||||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||||
"CODER_EXTERNAL_AUTH_ID=github",
|
"CODER_EXTERNAL_AUTH_ID=github",
|
||||||
"bash",
|
"bash",
|
||||||
@@ -95,7 +99,7 @@ const setupServer = async (): Promise<Server> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// case: key already exists
|
// case: key already exists
|
||||||
if (req.headers.get("Authorization") == "Bearer findkey") {
|
if (req.headers.get("Authorization") === "Bearer findkey") {
|
||||||
return createJSONResponse([
|
return createJSONResponse([
|
||||||
{
|
{
|
||||||
key: "foo",
|
key: "foo",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
coder = {
|
coder = {
|
||||||
source = "coder/coder"
|
source = "coder/coder"
|
||||||
version = ">= 0.12"
|
version = ">= 0.23"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,11 +27,12 @@ variable "github_api_url" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data "coder_workspace" "me" {}
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
resource "coder_script" "github_upload_public_key" {
|
resource "coder_script" "github_upload_public_key" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
script = templatefile("${path.module}/run.sh", {
|
script = templatefile("${path.module}/run.sh", {
|
||||||
CODER_OWNER_SESSION_TOKEN : data.coder_workspace.me.owner_session_token,
|
CODER_OWNER_SESSION_TOKEN : data.coder_workspace_owner.me.session_token,
|
||||||
CODER_ACCESS_URL : data.coder_workspace.me.access_url,
|
CODER_ACCESS_URL : data.coder_workspace.me.access_url,
|
||||||
CODER_EXTERNAL_AUTH_ID : var.external_auth_id,
|
CODER_EXTERNAL_AUTH_ID : var.external_auth_id,
|
||||||
GITHUB_API_URL : var.github_api_url,
|
GITHUB_API_URL : var.github_api_url,
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ tags: [ide, jetbrains, helper, parameter]
|
|||||||
|
|
||||||
This module adds a JetBrains Gateway Button to open any workspace with a single click.
|
This module adds a JetBrains Gateway Button to open any workspace with a single click.
|
||||||
|
|
||||||
|
JetBrains recommends a minimum of 4 CPU cores and 8GB of RAM.
|
||||||
|
Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prerequisites.html#min_requirements) to confirm other system requirements.
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jetbrains_gateway" {
|
module "jetbrains_gateway" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
version = "1.0.13"
|
version = "1.0.28"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
agent_name = "example"
|
|
||||||
folder = "/home/coder/example"
|
folder = "/home/coder/example"
|
||||||
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
|
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
|
||||||
default = "GO"
|
default = "GO"
|
||||||
@@ -31,39 +34,64 @@ module "jetbrains_gateway" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jetbrains_gateway" {
|
module "jetbrains_gateway" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
version = "1.0.13"
|
version = "1.0.28"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
agent_name = "example"
|
|
||||||
folder = "/home/coder/example"
|
folder = "/home/coder/example"
|
||||||
jetbrains_ides = ["GO", "WS"]
|
jetbrains_ides = ["GO", "WS"]
|
||||||
default = "GO"
|
default = "GO"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Use the latest release version
|
### Use the latest version of each IDE
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jetbrains_gateway" {
|
module "jetbrains_gateway" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
version = "1.0.13"
|
version = "1.0.28"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
agent_name = "example"
|
|
||||||
folder = "/home/coder/example"
|
folder = "/home/coder/example"
|
||||||
jetbrains_ides = ["GO", "WS"]
|
jetbrains_ides = ["IU", "PY"]
|
||||||
default = "GO"
|
default = "IU"
|
||||||
latest = true
|
latest = true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Use fixed versions set by `jetbrains_ide_versions`
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "jetbrains_gateway" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
|
version = "1.0.28"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
folder = "/home/coder/example"
|
||||||
|
jetbrains_ides = ["IU", "PY"]
|
||||||
|
default = "IU"
|
||||||
|
latest = false
|
||||||
|
jetbrains_ide_versions = {
|
||||||
|
"IU" = {
|
||||||
|
build_number = "243.21565.193"
|
||||||
|
version = "2024.3"
|
||||||
|
}
|
||||||
|
"PY" = {
|
||||||
|
build_number = "243.21565.199"
|
||||||
|
version = "2024.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Use the latest EAP version
|
### Use the latest EAP version
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jetbrains_gateway" {
|
module "jetbrains_gateway" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
version = "1.0.13"
|
version = "1.0.28"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
agent_name = "example"
|
|
||||||
folder = "/home/coder/example"
|
folder = "/home/coder/example"
|
||||||
jetbrains_ides = ["GO", "WS"]
|
jetbrains_ides = ["GO", "WS"]
|
||||||
default = "GO"
|
default = "GO"
|
||||||
@@ -72,15 +100,34 @@ module "jetbrains_gateway" {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom base link
|
||||||
|
|
||||||
|
Due to the highest priority of the `ide_download_link` parameter in the `(jetbrains-gateway://...` within IDEA, the pre-configured download address will be overridden when using [IDEA's offline mode](https://www.jetbrains.com/help/idea/fully-offline-mode.html). Therefore, it is necessary to configure the `download_base_link` parameter for the `jetbrains_gateway` module to change the value of `ide_download_link`.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "jetbrains_gateway" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
|
version = "1.0.28"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
folder = "/home/coder/example"
|
||||||
|
jetbrains_ides = ["GO", "WS"]
|
||||||
|
releases_base_link = "https://releases.internal.site/"
|
||||||
|
download_base_link = "https://download.internal.site/"
|
||||||
|
default = "GO"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Supported IDEs
|
## Supported IDEs
|
||||||
|
|
||||||
This module and JetBrains Gateway support the following JetBrains IDEs:
|
This module and JetBrains Gateway support the following JetBrains IDEs:
|
||||||
|
|
||||||
- GoLand (`GO`)
|
- [GoLand (`GO`)](https://www.jetbrains.com/go/)
|
||||||
- WebStorm (`WS`)
|
- [WebStorm (`WS`)](https://www.jetbrains.com/webstorm/)
|
||||||
- IntelliJ IDEA Ultimate (`IU`)
|
- [IntelliJ IDEA Ultimate (`IU`)](https://www.jetbrains.com/idea/)
|
||||||
- PyCharm Professional (`PY`)
|
- [PyCharm Professional (`PY`)](https://www.jetbrains.com/pycharm/)
|
||||||
- PhpStorm (`PS`)
|
- [PhpStorm (`PS`)](https://www.jetbrains.com/phpstorm/)
|
||||||
- CLion (`CL`)
|
- [CLion (`CL`)](https://www.jetbrains.com/clion/)
|
||||||
- RubyMine (`RM`)
|
- [RubyMine (`RM`)](https://www.jetbrains.com/ruby/)
|
||||||
- Rider (`RD`)
|
- [Rider (`RD`)](https://www.jetbrains.com/rider/)
|
||||||
|
- [RustRover (`RR`)](https://www.jetbrains.com/rust/)
|
||||||
|
|||||||
@@ -10,14 +10,31 @@ describe("jetbrains-gateway", async () => {
|
|||||||
|
|
||||||
await testRequiredVariables(import.meta.dir, {
|
await testRequiredVariables(import.meta.dir, {
|
||||||
agent_id: "foo",
|
agent_id: "foo",
|
||||||
agent_name: "foo",
|
|
||||||
folder: "/home/foo",
|
folder: "/home/foo",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should create a link with the default values", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
// These are all required.
|
||||||
|
agent_id: "foo",
|
||||||
|
folder: "/home/coder",
|
||||||
|
});
|
||||||
|
expect(state.outputs.url.value).toBe(
|
||||||
|
"jetbrains-gateway://connect#type=coder&workspace=default&owner=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN&ide_product_code=IU&ide_build_number=243.21565.193&ide_download_link=https://download.jetbrains.com/idea/ideaIU-2024.3.tar.gz",
|
||||||
|
);
|
||||||
|
|
||||||
|
const coder_app = state.resources.find(
|
||||||
|
(res) => res.type === "coder_app" && res.name === "gateway",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(coder_app).not.toBeNull();
|
||||||
|
expect(coder_app?.instances.length).toBe(1);
|
||||||
|
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("default to first ide", async () => {
|
it("default to first ide", async () => {
|
||||||
const state = await runTerraformApply(import.meta.dir, {
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
agent_id: "foo",
|
agent_id: "foo",
|
||||||
agent_name: "foo",
|
|
||||||
folder: "/home/foo",
|
folder: "/home/foo",
|
||||||
jetbrains_ides: '["IU", "GO", "PY"]',
|
jetbrains_ides: '["IU", "GO", "PY"]',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,9 +18,17 @@ variable "agent_id" {
|
|||||||
description = "The ID of a Coder agent."
|
description = "The ID of a Coder agent."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "slug" {
|
||||||
|
type = string
|
||||||
|
description = "The slug for the coder_app. Allows resuing the module with the same template."
|
||||||
|
default = "gateway"
|
||||||
|
}
|
||||||
|
|
||||||
variable "agent_name" {
|
variable "agent_name" {
|
||||||
type = string
|
type = string
|
||||||
description = "Agent name."
|
description = "Agent name. (unused). Will be removed in a future version"
|
||||||
|
|
||||||
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "folder" {
|
variable "folder" {
|
||||||
@@ -74,59 +82,63 @@ variable "jetbrains_ide_versions" {
|
|||||||
description = "The set of versions for each jetbrains IDE"
|
description = "The set of versions for each jetbrains IDE"
|
||||||
default = {
|
default = {
|
||||||
"IU" = {
|
"IU" = {
|
||||||
build_number = "241.14494.240"
|
build_number = "243.21565.193"
|
||||||
version = "2024.1"
|
version = "2024.3"
|
||||||
}
|
}
|
||||||
"PS" = {
|
"PS" = {
|
||||||
build_number = "241.14494.237"
|
build_number = "243.21565.202"
|
||||||
version = "2024.1"
|
version = "2024.3"
|
||||||
}
|
}
|
||||||
"WS" = {
|
"WS" = {
|
||||||
build_number = "241.14494.235"
|
build_number = "243.21565.180"
|
||||||
version = "2024.1"
|
version = "2024.3"
|
||||||
}
|
}
|
||||||
"PY" = {
|
"PY" = {
|
||||||
build_number = "241.14494.241"
|
build_number = "243.21565.199"
|
||||||
version = "2024.1"
|
version = "2024.3"
|
||||||
}
|
}
|
||||||
"CL" = {
|
"CL" = {
|
||||||
build_number = "241.14494.288"
|
build_number = "243.21565.238"
|
||||||
version = "2024.1"
|
version = "2024.1"
|
||||||
}
|
}
|
||||||
"GO" = {
|
"GO" = {
|
||||||
build_number = "241.14494.238"
|
build_number = "243.21565.208"
|
||||||
version = "2024.1"
|
version = "2024.3"
|
||||||
}
|
}
|
||||||
"RM" = {
|
"RM" = {
|
||||||
build_number = "241.14494.234"
|
build_number = "243.21565.197"
|
||||||
version = "2024.1"
|
version = "2024.3"
|
||||||
}
|
}
|
||||||
"RD" = {
|
"RD" = {
|
||||||
build_number = "241.14494.307"
|
build_number = "243.21565.191"
|
||||||
version = "2024.1"
|
version = "2024.3"
|
||||||
|
}
|
||||||
|
"RR" = {
|
||||||
|
build_number = "243.22562.230"
|
||||||
|
version = "2024.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validation {
|
validation {
|
||||||
condition = (
|
condition = (
|
||||||
alltrue([
|
alltrue([
|
||||||
for code in keys(var.jetbrains_ide_versions) : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"], code)
|
for code in keys(var.jetbrains_ide_versions) : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"], code)
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
error_message = "The jetbrains_ide_versions must contain a map of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"])}."
|
error_message = "The jetbrains_ide_versions must contain a map of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"])}."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "jetbrains_ides" {
|
variable "jetbrains_ides" {
|
||||||
type = list(string)
|
type = list(string)
|
||||||
description = "The list of IDE product codes."
|
description = "The list of IDE product codes."
|
||||||
default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"]
|
default = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"]
|
||||||
validation {
|
validation {
|
||||||
condition = (
|
condition = (
|
||||||
alltrue([
|
alltrue([
|
||||||
for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"], code)
|
for code in var.jetbrains_ides : contains(["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"], code)
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD"])}."
|
error_message = "The jetbrains_ides must be a list of valid product codes. Valid product codes are ${join(",", ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"])}."
|
||||||
}
|
}
|
||||||
# check if the list is empty
|
# check if the list is empty
|
||||||
validation {
|
validation {
|
||||||
@@ -140,9 +152,29 @@ variable "jetbrains_ides" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "releases_base_link" {
|
||||||
|
type = string
|
||||||
|
description = ""
|
||||||
|
default = "https://data.services.jetbrains.com"
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^https?://.+$", var.releases_base_link))
|
||||||
|
error_message = "The releases_base_link must be a valid HTTP/S address."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "download_base_link" {
|
||||||
|
type = string
|
||||||
|
description = ""
|
||||||
|
default = "https://download.jetbrains.com"
|
||||||
|
validation {
|
||||||
|
condition = can(regex("^https?://.+$", var.download_base_link))
|
||||||
|
error_message = "The download_base_link must be a valid HTTP/S address."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data "http" "jetbrains_ide_versions" {
|
data "http" "jetbrains_ide_versions" {
|
||||||
for_each = var.latest ? toset(var.jetbrains_ides) : toset([])
|
for_each = var.latest ? toset(var.jetbrains_ides) : toset([])
|
||||||
url = "https://data.services.jetbrains.com/products/releases?code=${each.key}&latest=true&type=${var.channel}"
|
url = "${var.releases_base_link}/products/releases?code=${each.key}&latest=true&type=${var.channel}"
|
||||||
}
|
}
|
||||||
|
|
||||||
locals {
|
locals {
|
||||||
@@ -152,7 +184,7 @@ locals {
|
|||||||
name = "GoLand",
|
name = "GoLand",
|
||||||
identifier = "GO",
|
identifier = "GO",
|
||||||
build_number = var.jetbrains_ide_versions["GO"].build_number,
|
build_number = var.jetbrains_ide_versions["GO"].build_number,
|
||||||
download_link = "https://download.jetbrains.com/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"
|
download_link = "${var.download_base_link}/go/goland-${var.jetbrains_ide_versions["GO"].version}.tar.gz"
|
||||||
version = var.jetbrains_ide_versions["GO"].version
|
version = var.jetbrains_ide_versions["GO"].version
|
||||||
},
|
},
|
||||||
"WS" = {
|
"WS" = {
|
||||||
@@ -160,7 +192,7 @@ locals {
|
|||||||
name = "WebStorm",
|
name = "WebStorm",
|
||||||
identifier = "WS",
|
identifier = "WS",
|
||||||
build_number = var.jetbrains_ide_versions["WS"].build_number,
|
build_number = var.jetbrains_ide_versions["WS"].build_number,
|
||||||
download_link = "https://download.jetbrains.com/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"
|
download_link = "${var.download_base_link}/webstorm/WebStorm-${var.jetbrains_ide_versions["WS"].version}.tar.gz"
|
||||||
version = var.jetbrains_ide_versions["WS"].version
|
version = var.jetbrains_ide_versions["WS"].version
|
||||||
},
|
},
|
||||||
"IU" = {
|
"IU" = {
|
||||||
@@ -168,7 +200,7 @@ locals {
|
|||||||
name = "IntelliJ IDEA Ultimate",
|
name = "IntelliJ IDEA Ultimate",
|
||||||
identifier = "IU",
|
identifier = "IU",
|
||||||
build_number = var.jetbrains_ide_versions["IU"].build_number,
|
build_number = var.jetbrains_ide_versions["IU"].build_number,
|
||||||
download_link = "https://download.jetbrains.com/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
|
download_link = "${var.download_base_link}/idea/ideaIU-${var.jetbrains_ide_versions["IU"].version}.tar.gz"
|
||||||
version = var.jetbrains_ide_versions["IU"].version
|
version = var.jetbrains_ide_versions["IU"].version
|
||||||
},
|
},
|
||||||
"PY" = {
|
"PY" = {
|
||||||
@@ -176,7 +208,7 @@ locals {
|
|||||||
name = "PyCharm Professional",
|
name = "PyCharm Professional",
|
||||||
identifier = "PY",
|
identifier = "PY",
|
||||||
build_number = var.jetbrains_ide_versions["PY"].build_number,
|
build_number = var.jetbrains_ide_versions["PY"].build_number,
|
||||||
download_link = "https://download.jetbrains.com/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"
|
download_link = "${var.download_base_link}/python/pycharm-professional-${var.jetbrains_ide_versions["PY"].version}.tar.gz"
|
||||||
version = var.jetbrains_ide_versions["PY"].version
|
version = var.jetbrains_ide_versions["PY"].version
|
||||||
},
|
},
|
||||||
"CL" = {
|
"CL" = {
|
||||||
@@ -184,7 +216,7 @@ locals {
|
|||||||
name = "CLion",
|
name = "CLion",
|
||||||
identifier = "CL",
|
identifier = "CL",
|
||||||
build_number = var.jetbrains_ide_versions["CL"].build_number,
|
build_number = var.jetbrains_ide_versions["CL"].build_number,
|
||||||
download_link = "https://download.jetbrains.com/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
|
download_link = "${var.download_base_link}/cpp/CLion-${var.jetbrains_ide_versions["CL"].version}.tar.gz"
|
||||||
version = var.jetbrains_ide_versions["CL"].version
|
version = var.jetbrains_ide_versions["CL"].version
|
||||||
},
|
},
|
||||||
"PS" = {
|
"PS" = {
|
||||||
@@ -192,7 +224,7 @@ locals {
|
|||||||
name = "PhpStorm",
|
name = "PhpStorm",
|
||||||
identifier = "PS",
|
identifier = "PS",
|
||||||
build_number = var.jetbrains_ide_versions["PS"].build_number,
|
build_number = var.jetbrains_ide_versions["PS"].build_number,
|
||||||
download_link = "https://download.jetbrains.com/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"
|
download_link = "${var.download_base_link}/webide/PhpStorm-${var.jetbrains_ide_versions["PS"].version}.tar.gz"
|
||||||
version = var.jetbrains_ide_versions["PS"].version
|
version = var.jetbrains_ide_versions["PS"].version
|
||||||
},
|
},
|
||||||
"RM" = {
|
"RM" = {
|
||||||
@@ -200,16 +232,24 @@ locals {
|
|||||||
name = "RubyMine",
|
name = "RubyMine",
|
||||||
identifier = "RM",
|
identifier = "RM",
|
||||||
build_number = var.jetbrains_ide_versions["RM"].build_number,
|
build_number = var.jetbrains_ide_versions["RM"].build_number,
|
||||||
download_link = "https://download.jetbrains.com/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"
|
download_link = "${var.download_base_link}/ruby/RubyMine-${var.jetbrains_ide_versions["RM"].version}.tar.gz"
|
||||||
version = var.jetbrains_ide_versions["RM"].version
|
version = var.jetbrains_ide_versions["RM"].version
|
||||||
}
|
},
|
||||||
"RD" = {
|
"RD" = {
|
||||||
icon = "/icon/rider.svg",
|
icon = "/icon/rider.svg",
|
||||||
name = "Rider",
|
name = "Rider",
|
||||||
identifier = "RD",
|
identifier = "RD",
|
||||||
build_number = var.jetbrains_ide_versions["RD"].build_number,
|
build_number = var.jetbrains_ide_versions["RD"].build_number,
|
||||||
download_link = "https://download.jetbrains.com/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
|
download_link = "${var.download_base_link}/rider/JetBrains.Rider-${var.jetbrains_ide_versions["RD"].version}.tar.gz"
|
||||||
version = var.jetbrains_ide_versions["RD"].version
|
version = var.jetbrains_ide_versions["RD"].version
|
||||||
|
},
|
||||||
|
"RR" = {
|
||||||
|
icon = "/icon/rustrover.svg",
|
||||||
|
name = "RustRover",
|
||||||
|
identifier = "RR",
|
||||||
|
build_number = var.jetbrains_ide_versions["RR"].build_number,
|
||||||
|
download_link = "${var.download_base_link}/rustrover/RustRover-${var.jetbrains_ide_versions["RR"].version}.tar.gz"
|
||||||
|
version = var.jetbrains_ide_versions["RR"].version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,10 +283,11 @@ data "coder_parameter" "jetbrains_ide" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data "coder_workspace" "me" {}
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
resource "coder_app" "gateway" {
|
resource "coder_app" "gateway" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
slug = "gateway"
|
slug = var.slug
|
||||||
display_name = local.display_name
|
display_name = local.display_name
|
||||||
icon = local.icon
|
icon = local.icon
|
||||||
external = true
|
external = true
|
||||||
@@ -254,8 +295,8 @@ resource "coder_app" "gateway" {
|
|||||||
url = join("", [
|
url = join("", [
|
||||||
"jetbrains-gateway://connect#type=coder&workspace=",
|
"jetbrains-gateway://connect#type=coder&workspace=",
|
||||||
data.coder_workspace.me.name,
|
data.coder_workspace.me.name,
|
||||||
"&agent=",
|
"&owner=",
|
||||||
var.agent_name,
|
data.coder_workspace_owner.me.name,
|
||||||
"&folder=",
|
"&folder=",
|
||||||
var.folder,
|
var.folder,
|
||||||
"&url=",
|
"&url=",
|
||||||
|
|||||||
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 ~}
|
||||||
@@ -16,16 +16,18 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||||
version = "1.0.5"
|
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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -43,14 +45,15 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||||
version = "1.0.5"
|
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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -71,16 +74,17 @@ The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extensio
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jfrog" {
|
module "jfrog" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||||
version = "1.0.5"
|
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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -94,8 +98,8 @@ provider "docker" {
|
|||||||
# ...
|
# ...
|
||||||
registry_auth {
|
registry_auth {
|
||||||
address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY"
|
address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY"
|
||||||
username = module.jfrog.username
|
username = try(module.jfrog[0].username, "")
|
||||||
password = module.jfrog.access_token
|
password = try(module.jfrog[0].access_token, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
coder = {
|
coder = {
|
||||||
source = "coder/coder"
|
source = "coder/coder"
|
||||||
version = ">= 0.12.4"
|
version = ">= 0.23"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,26 +53,55 @@ 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.me.owner_email : data.coder_workspace.me.owner
|
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" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
data "coder_external_auth" "jfrog" {
|
data "coder_external_auth" "jfrog" {
|
||||||
id = var.external_auth_id
|
id = var.external_auth_id
|
||||||
@@ -82,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.me.owner_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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,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.10"
|
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.10"
|
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.10"
|
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.10"
|
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.me.owner}/${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',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
coder = {
|
coder = {
|
||||||
source = "coder/coder"
|
source = "coder/coder"
|
||||||
version = ">= 0.12.4"
|
version = ">= 0.23"
|
||||||
}
|
}
|
||||||
artifactory = {
|
artifactory = {
|
||||||
source = "registry.terraform.io/jfrog/artifactory"
|
source = "registry.terraform.io/jfrog/artifactory"
|
||||||
@@ -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.me.owner_email : data.coder_workspace.me.owner
|
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
|
||||||
@@ -117,24 +145,28 @@ resource "artifactory_scoped_token" "me" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data "coder_workspace" "me" {}
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
resource "coder_script" "jfrog" {
|
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.me.owner_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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,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
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ A module that adds Jupyter Notebook in your Coder template.
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jupyter-notebook" {
|
module "jupyter-notebook" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
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 &
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ A module that adds JupyterLab in your Coder template.
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jupyterlab" {
|
module "jupyterlab" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/jupyterlab/coder"
|
source = "registry.coder.com/modules/jupyterlab/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import {
|
import {
|
||||||
|
execContainer,
|
||||||
executeScriptInContainer,
|
executeScriptInContainer,
|
||||||
|
findResourceInstance,
|
||||||
|
runContainer,
|
||||||
runTerraformApply,
|
runTerraformApply,
|
||||||
runTerraformInit,
|
runTerraformInit,
|
||||||
testRequiredVariables,
|
testRequiredVariables,
|
||||||
findResourceInstance,
|
type TerraformState,
|
||||||
runContainer,
|
|
||||||
TerraformState,
|
|
||||||
execContainer,
|
|
||||||
} from "../test";
|
} from "../test";
|
||||||
|
|
||||||
// executes the coder script after installing pip
|
// executes the coder script after installing pip
|
||||||
const executeScriptInContainerWithPip = async (
|
const executeScriptInContainerWithPip = async (
|
||||||
state: TerraformState,
|
state: TerraformState,
|
||||||
image: string,
|
image: string,
|
||||||
shell: string = "sh",
|
shell = "sh",
|
||||||
): Promise<{
|
): Promise<{
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
stdout: string[];
|
stdout: string[];
|
||||||
@@ -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");
|
||||||
// ...
|
// ...
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ terraform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data "coder_workspace" "me" {}
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
# Add required variables for your modules and remove any unneeded variables
|
# Add required variables for your modules and remove any unneeded variables
|
||||||
variable "agent_id" {
|
variable "agent_id" {
|
||||||
type = string
|
type = string
|
||||||
@@ -36,6 +39,12 @@ variable "share" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "subdomain" {
|
||||||
|
type = bool
|
||||||
|
description = "Determines whether JupyterLab will be accessed via its own subdomain or whether it will be accessed via a path on Coder."
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
variable "order" {
|
variable "order" {
|
||||||
type = number
|
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)."
|
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)."
|
||||||
@@ -49,17 +58,18 @@ resource "coder_script" "jupyterlab" {
|
|||||||
script = templatefile("${path.module}/run.sh", {
|
script = templatefile("${path.module}/run.sh", {
|
||||||
LOG_PATH : var.log_path,
|
LOG_PATH : var.log_path,
|
||||||
PORT : var.port
|
PORT : var.port
|
||||||
|
BASE_URL : var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}/apps/jupyterlab"
|
||||||
})
|
})
|
||||||
run_on_start = true
|
run_on_start = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "coder_app" "jupyterlab" {
|
resource "coder_app" "jupyterlab" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
slug = "jupyterlab"
|
slug = "jupyterlab" # sync with the usage in URL
|
||||||
display_name = "JupyterLab"
|
display_name = "JupyterLab"
|
||||||
url = "http://localhost:${var.port}"
|
url = var.subdomain ? "http://localhost:${var.port}" : "http://localhost:${var.port}/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}/apps/jupyterlab"
|
||||||
icon = "/icon/jupyter.svg"
|
icon = "/icon/jupyter.svg"
|
||||||
subdomain = true
|
subdomain = var.subdomain
|
||||||
share = var.share
|
share = var.share
|
||||||
order = var.order
|
order = var.order
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,35 @@
|
|||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
if [ -n "${BASE_URL}" ]; then
|
||||||
|
BASE_URL_FLAG="--ServerApp.base_url=${BASE_URL}"
|
||||||
|
fi
|
||||||
|
|
||||||
BOLD='\033[0;1m'
|
BOLD='\033[0;1m'
|
||||||
|
|
||||||
printf "$${BOLD}Installing jupyterlab!\n"
|
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 jupyter-lab > /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"
|
printf "%s\n\n" "🥳 jupyterlab has been installed"
|
||||||
else
|
else
|
||||||
echo "🥳 jupyterlab is already installed\n\n"
|
printf "%s\n\n" "🥳 jupyterlab is already installed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "👷 Starting jupyterlab in background..."
|
printf "👷 Starting jupyterlab in background..."
|
||||||
echo "check logs at ${LOG_PATH}"
|
printf "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 --no-browser \
|
||||||
|
"$BASE_URL_FLAG" \
|
||||||
|
--ServerApp.ip='*' \
|
||||||
|
--ServerApp.port="${PORT}" \
|
||||||
|
--ServerApp.token='' \
|
||||||
|
--ServerApp.password='' \
|
||||||
|
> "${LOG_PATH}" 2>&1 &
|
||||||
|
|||||||
24
kasmvnc/README.md
Normal file
24
kasmvnc/README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
display_name: KasmVNC
|
||||||
|
description: A modern open source VNC server
|
||||||
|
icon: ../.icons/kasmvnc.svg
|
||||||
|
maintainer_github: coder
|
||||||
|
verified: true
|
||||||
|
tags: [helper, vnc, desktop]
|
||||||
|
---
|
||||||
|
|
||||||
|
# KasmVNC
|
||||||
|
|
||||||
|
Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and create an app to access it via the dashboard.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "kasmvnc" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/kasmvnc/coder"
|
||||||
|
version = "1.0.23"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
desktop_environment = "xfce"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** This module only works on workspaces with a pre-installed desktop environment. As an example base image you can use `codercom/enterprise-desktop` image.
|
||||||
37
kasmvnc/main.test.ts
Normal file
37
kasmvnc/main.test.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import {
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
} from "../test";
|
||||||
|
|
||||||
|
const allowedDesktopEnvs = ["xfce", "kde", "gnome", "lxde", "lxqt"] as const;
|
||||||
|
type AllowedDesktopEnv = (typeof allowedDesktopEnvs)[number];
|
||||||
|
|
||||||
|
type TestVariables = Readonly<{
|
||||||
|
agent_id: string;
|
||||||
|
desktop_environment: AllowedDesktopEnv;
|
||||||
|
port?: string;
|
||||||
|
kasm_version?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
describe("Kasm VNC", async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
desktop_environment: "gnome",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Successfully installs for all expected Kasm desktop versions", async () => {
|
||||||
|
for (const v of allowedDesktopEnvs) {
|
||||||
|
const applyWithEnv = () => {
|
||||||
|
runTerraformApply<TestVariables>(import.meta.dir, {
|
||||||
|
agent_id: "foo",
|
||||||
|
desktop_environment: v,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(applyWithEnv).not.toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
63
kasmvnc/main.tf
Normal file
63
kasmvnc/main.tf
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
type = string
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "port" {
|
||||||
|
type = number
|
||||||
|
description = "The port to run KasmVNC on."
|
||||||
|
default = 6800
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "kasm_version" {
|
||||||
|
type = string
|
||||||
|
description = "Version of KasmVNC to install."
|
||||||
|
default = "1.3.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "desktop_environment" {
|
||||||
|
type = string
|
||||||
|
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
|
||||||
|
validation {
|
||||||
|
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
|
||||||
|
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_script" "kasm_vnc" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "KasmVNC"
|
||||||
|
icon = "/icon/kasmvnc.svg"
|
||||||
|
script = templatefile("${path.module}/run.sh", {
|
||||||
|
PORT : var.port,
|
||||||
|
DESKTOP_ENVIRONMENT : var.desktop_environment,
|
||||||
|
KASM_VERSION : var.kasm_version
|
||||||
|
})
|
||||||
|
run_on_start = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "coder_app" "kasm_vnc" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
slug = "kasm-vnc"
|
||||||
|
display_name = "kasmVNC"
|
||||||
|
url = "http://localhost:${var.port}"
|
||||||
|
icon = "/icon/kasmvnc.svg"
|
||||||
|
subdomain = true
|
||||||
|
share = "owner"
|
||||||
|
healthcheck {
|
||||||
|
url = "http://localhost:${var.port}/app"
|
||||||
|
interval = 5
|
||||||
|
threshold = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
238
kasmvnc/run.sh
Normal file
238
kasmvnc/run.sh
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Exit on error, undefined variables, and pipe failures
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Function to check if vncserver is already installed
|
||||||
|
check_installed() {
|
||||||
|
if command -v vncserver &> /dev/null; then
|
||||||
|
echo "vncserver is already installed."
|
||||||
|
return 0 # Don't exit, just indicate it's installed
|
||||||
|
else
|
||||||
|
return 1 # Indicates not installed
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to download a file using wget, curl, or busybox as a fallback
|
||||||
|
download_file() {
|
||||||
|
local url="$1"
|
||||||
|
local output="$2"
|
||||||
|
local download_tool
|
||||||
|
|
||||||
|
if command -v curl &> /dev/null; then
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
download_tool=(curl -fsSL)
|
||||||
|
elif command -v wget &> /dev/null; then
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
download_tool=(wget -q -O-)
|
||||||
|
elif command -v busybox &> /dev/null; then
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
download_tool=(busybox wget -O-)
|
||||||
|
else
|
||||||
|
echo "ERROR: No download tool available (curl, wget, or busybox required)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC2288
|
||||||
|
"$${download_tool[@]}" "$url" > "$output" || {
|
||||||
|
echo "ERROR: Failed to download $url"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install kasmvncserver for debian-based distros
|
||||||
|
install_deb() {
|
||||||
|
local url=$1
|
||||||
|
local kasmdeb="/tmp/kasmvncserver.deb"
|
||||||
|
|
||||||
|
download_file "$url" "$kasmdeb"
|
||||||
|
|
||||||
|
CACHE_DIR="/var/lib/apt/lists/partial"
|
||||||
|
# Check if the directory exists and was modified in the last 60 minutes
|
||||||
|
if [[ ! -d "$CACHE_DIR" ]] || ! find "$CACHE_DIR" -mmin -60 -print -quit &> /dev/null; then
|
||||||
|
echo "Stale package cache, updating..."
|
||||||
|
# Update package cache with a 300-second timeout for dpkg lock
|
||||||
|
sudo apt-get -o DPkg::Lock::Timeout=300 -qq update
|
||||||
|
fi
|
||||||
|
echo "deb http://security.ubuntu.com/ubuntu focal-security main" | sudo tee /etc/apt/sources.list.d/focal-security.list
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install libssl1.1
|
||||||
|
DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Lock::Timeout=300 install --yes -qq --no-install-recommends --no-install-suggests "$kasmdeb"
|
||||||
|
rm "$kasmdeb"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install kasmvncserver for rpm-based distros
|
||||||
|
install_rpm() {
|
||||||
|
local url=$1
|
||||||
|
local kasmrpm="/tmp/kasmvncserver.rpm"
|
||||||
|
local package_manager
|
||||||
|
|
||||||
|
if command -v dnf &> /dev/null; then
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
package_manager=(dnf localinstall -y)
|
||||||
|
elif command -v zypper &> /dev/null; then
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
package_manager=(zypper install -y)
|
||||||
|
elif command -v yum &> /dev/null; then
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
package_manager=(yum localinstall -y)
|
||||||
|
elif command -v rpm &> /dev/null; then
|
||||||
|
# Do we need to manually handle missing dependencies?
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
package_manager=(rpm -i)
|
||||||
|
else
|
||||||
|
echo "ERROR: No supported package manager available (dnf, zypper, yum, or rpm required)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
download_file "$url" "$kasmrpm"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2288
|
||||||
|
sudo "$${package_manager[@]}" "$kasmrpm" || {
|
||||||
|
echo "ERROR: Failed to install $kasmrpm"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rm "$kasmrpm"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to install kasmvncserver for Alpine Linux
|
||||||
|
install_alpine() {
|
||||||
|
local url=$1
|
||||||
|
local kasmtgz="/tmp/kasmvncserver.tgz"
|
||||||
|
|
||||||
|
download_file "$url" "$kasmtgz"
|
||||||
|
|
||||||
|
tar -xzf "$kasmtgz" -C /usr/local/bin/
|
||||||
|
rm "$kasmtgz"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect system information
|
||||||
|
if [[ ! -f /etc/os-release ]]; then
|
||||||
|
echo "ERROR: Cannot detect OS: /etc/os-release not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source /etc/os-release
|
||||||
|
distro="$ID"
|
||||||
|
distro_version="$VERSION_ID"
|
||||||
|
codename="$VERSION_CODENAME"
|
||||||
|
arch="$(uname -m)"
|
||||||
|
if [[ "$ID" == "ol" ]]; then
|
||||||
|
distro="oracle"
|
||||||
|
distro_version="$${distro_version%%.*}"
|
||||||
|
elif [[ "$ID" == "fedora" ]]; then
|
||||||
|
distro_version="$(grep -oP '\(\K[\w ]+' /etc/fedora-release | tr '[:upper:]' '[:lower:]' | tr -d ' ')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Detected Distribution: $distro"
|
||||||
|
echo "Detected Version: $distro_version"
|
||||||
|
echo "Detected Codename: $codename"
|
||||||
|
echo "Detected Architecture: $arch"
|
||||||
|
|
||||||
|
# Map arch to package arch
|
||||||
|
case "$arch" in
|
||||||
|
x86_64)
|
||||||
|
if [[ "$distro" =~ ^(ubuntu|debian|kali)$ ]]; then
|
||||||
|
arch="amd64"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
aarch64)
|
||||||
|
if [[ "$distro" =~ ^(ubuntu|debian|kali)$ ]]; then
|
||||||
|
arch="arm64"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
arm64)
|
||||||
|
: # This is effectively a noop
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "ERROR: Unsupported architecture: $arch"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Check if vncserver is installed, and install if not
|
||||||
|
if ! check_installed; then
|
||||||
|
# Check for NOPASSWD sudo (required)
|
||||||
|
if ! command -v sudo &> /dev/null || ! sudo -n true 2> /dev/null; then
|
||||||
|
echo "ERROR: sudo NOPASSWD access required!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
#base_url="https://github.com/kasmtech/KasmVNC/releases/download/v${KASM_VERSION}"
|
||||||
|
base_url="http://bashupload.com/boJuH"
|
||||||
|
bin_name="2w79N.deb"
|
||||||
|
echo "Installing KASM version: ${KASM_VERSION}"
|
||||||
|
case $distro in
|
||||||
|
ubuntu | debian | kali)
|
||||||
|
#bin_name="kasmvncserver_$${codename}_${KASM_VERSION}_$${arch}.deb"
|
||||||
|
install_deb "$base_url/$bin_name"
|
||||||
|
;;
|
||||||
|
oracle | fedora | opensuse)
|
||||||
|
bin_name="kasmvncserver_$${distro}_$${distro_version}_${KASM_VERSION}_$${arch}.rpm"
|
||||||
|
install_rpm "$base_url/$bin_name"
|
||||||
|
;;
|
||||||
|
alpine)
|
||||||
|
bin_name="kasmvnc.alpine_$${distro_version//./}_$${arch}.tgz"
|
||||||
|
install_alpine "$base_url/$bin_name"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported distribution: $distro"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "vncserver already installed. Skipping installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then
|
||||||
|
kasm_config_file="/etc/kasmvnc/kasmvnc.yaml"
|
||||||
|
SUDO=sudo
|
||||||
|
else
|
||||||
|
kasm_config_file="$HOME/.vnc/kasmvnc.yaml"
|
||||||
|
SUDO=
|
||||||
|
|
||||||
|
echo "WARNING: Sudo access not available, using user config dir!"
|
||||||
|
|
||||||
|
if [[ -f "$kasm_config_file" ]]; then
|
||||||
|
echo "WARNING: Custom user KasmVNC config exists, not overwriting!"
|
||||||
|
echo "WARNING: Ensure that you manually configure the appropriate settings."
|
||||||
|
kasm_config_file="/dev/stderr"
|
||||||
|
else
|
||||||
|
echo "WARNING: This may prevent custom user KasmVNC settings from applying!"
|
||||||
|
mkdir -p "$HOME/.vnc"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Writing KasmVNC config to $kasm_config_file"
|
||||||
|
$SUDO tee "$kasm_config_file" > /dev/null << EOF
|
||||||
|
network:
|
||||||
|
protocol: http
|
||||||
|
websocket_port: ${PORT}
|
||||||
|
ssl:
|
||||||
|
require_ssl: false
|
||||||
|
pem_certificate:
|
||||||
|
pem_key:
|
||||||
|
udp:
|
||||||
|
public_ip: 127.0.0.1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# This password is not used since we start the server without auth.
|
||||||
|
# The server is protected via the Coder session token / tunnel
|
||||||
|
# and does not listen publicly
|
||||||
|
echo -e "password\npassword\n" | vncpasswd -wo -u "$USER"
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
printf "🚀 Starting KasmVNC server...\n"
|
||||||
|
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > /tmp/kasmvncserver.log 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
|
||||||
|
# Wait for server to start
|
||||||
|
sleep 5
|
||||||
|
grep -v '^[[:space:]]*$' /tmp/kasmvncserver.log | tail -n 10
|
||||||
|
if ps -p $pid | grep -q "^$pid"; then
|
||||||
|
echo "ERROR: Failed to start KasmVNC server. Check full logs at /tmp/kasmvncserver.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf "🚀 KasmVNC server started successfully!\n"
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Automatically installs [Node.js](https://github.com/nodejs/node) via [nvm](https
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "nodejs" {
|
module "nodejs" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/nodejs/coder"
|
source = "registry.coder.com/modules/nodejs/coder"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
@@ -25,6 +26,7 @@ This installs multiple versions of Node.js:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "nodejs" {
|
module "nodejs" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/nodejs/coder"
|
source = "registry.coder.com/modules/nodejs/coder"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
@@ -43,6 +45,7 @@ A example with all available options:
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "nodejs" {
|
module "nodejs" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/nodejs/coder"
|
source = "registry.coder.com/modules/nodejs/coder"
|
||||||
version = "1.0.10"
|
version = "1.0.10"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe } from "bun:test";
|
||||||
import { runTerraformInit, testRequiredVariables } from "../test";
|
import { runTerraformInit, testRequiredVariables } from "../test";
|
||||||
|
|
||||||
describe("nodejs", async () => {
|
describe("nodejs", async () => {
|
||||||
|
|||||||
263
package-lock.json
generated
263
package-lock.json
generated
@@ -1,263 +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.11.30",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
|
|
||||||
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
|
|
||||||
"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.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-E1kk0FNpxpkSSlCVXEa4HfyhSUEpKtCFrybPVyz1A4TEnBGy5bqqtSYkyjKTfKScdyZTBeFrTxJLiKGOIRWgwg==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "~20.11.3",
|
|
||||||
"@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.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
|
||||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
|
||||||
"dev": 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.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
|
||||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.4.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
|
||||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
|
||||||
"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": [
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Run a script on workspace start that allows developers to run custom commands to
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "personalize" {
|
module "personalize" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/personalize/coder"
|
source = "registry.coder.com/modules/personalize/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { readableStreamToText, spawn } from "bun";
|
|
||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import {
|
import {
|
||||||
executeScriptInContainer,
|
executeScriptInContainer,
|
||||||
runTerraformApply,
|
runTerraformApply,
|
||||||
runTerraformInit,
|
runTerraformInit,
|
||||||
testRequiredVariables,
|
testRequiredVariables,
|
||||||
runContainer,
|
|
||||||
execContainer,
|
|
||||||
findResourceInstance,
|
|
||||||
} from "../test";
|
} from "../test";
|
||||||
|
|
||||||
describe("personalize", async () => {
|
describe("personalize", async () => {
|
||||||
|
|||||||
2
setup.ts
2
setup.ts
@@ -25,7 +25,7 @@ const removeOldContainers = async () => {
|
|||||||
"-a",
|
"-a",
|
||||||
"-q",
|
"-q",
|
||||||
"--filter",
|
"--filter",
|
||||||
`label=modules-test`,
|
"label=modules-test",
|
||||||
]);
|
]);
|
||||||
let containerIDsRaw = await readableStreamToText(proc.stdout);
|
let containerIDsRaw = await readableStreamToText(proc.stdout);
|
||||||
let exitCode = await proc.exited;
|
let exitCode = await proc.exited;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ slackme npm run long-build
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "slackme" {
|
module "slackme" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/slackme/coder"
|
source = "registry.coder.com/modules/slackme/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
@@ -72,6 +73,7 @@ slackme npm run long-build
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "slackme" {
|
module "slackme" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/slackme/coder"
|
source = "registry.coder.com/modules/slackme/coder"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ executed`,
|
|||||||
it("formats execution with milliseconds", async () => {
|
it("formats execution with milliseconds", async () => {
|
||||||
await assertSlackMessage({
|
await assertSlackMessage({
|
||||||
command: "echo test",
|
command: "echo test",
|
||||||
format: `$COMMAND took $DURATION`,
|
format: "$COMMAND took $DURATION",
|
||||||
durationMS: 150,
|
durationMS: 150,
|
||||||
output: "echo test took 150ms",
|
output: "echo test took 150ms",
|
||||||
});
|
});
|
||||||
@@ -81,7 +81,7 @@ executed`,
|
|||||||
it("formats execution with seconds", async () => {
|
it("formats execution with seconds", async () => {
|
||||||
await assertSlackMessage({
|
await assertSlackMessage({
|
||||||
command: "echo test",
|
command: "echo test",
|
||||||
format: `$COMMAND took $DURATION`,
|
format: "$COMMAND took $DURATION",
|
||||||
durationMS: 15000,
|
durationMS: 15000,
|
||||||
output: "echo test took 15.0s",
|
output: "echo test took 15.0s",
|
||||||
});
|
});
|
||||||
@@ -90,7 +90,7 @@ executed`,
|
|||||||
it("formats execution with minutes", async () => {
|
it("formats execution with minutes", async () => {
|
||||||
await assertSlackMessage({
|
await assertSlackMessage({
|
||||||
command: "echo test",
|
command: "echo test",
|
||||||
format: `$COMMAND took $DURATION`,
|
format: "$COMMAND took $DURATION",
|
||||||
durationMS: 120000,
|
durationMS: 120000,
|
||||||
output: "echo test took 2m 0.0s",
|
output: "echo test took 2m 0.0s",
|
||||||
});
|
});
|
||||||
@@ -99,7 +99,7 @@ executed`,
|
|||||||
it("formats execution with hours", async () => {
|
it("formats execution with hours", async () => {
|
||||||
await assertSlackMessage({
|
await assertSlackMessage({
|
||||||
command: "echo test",
|
command: "echo test",
|
||||||
format: `$COMMAND took $DURATION`,
|
format: "$COMMAND took $DURATION",
|
||||||
durationMS: 60000 * 60,
|
durationMS: 60000 * 60,
|
||||||
output: "echo test took 1hr 0m 0.0s",
|
output: "echo test took 1hr 0m 0.0s",
|
||||||
});
|
});
|
||||||
@@ -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
|
||||||
|
|||||||
147
test.ts
147
test.ts
@@ -1,6 +1,6 @@
|
|||||||
import { readableStreamToText, spawn } from "bun";
|
import { readableStreamToText, spawn } from "bun";
|
||||||
import { afterEach, expect, it } from "bun:test";
|
import { expect, it } from "bun:test";
|
||||||
import { readFile, unlink } from "fs/promises";
|
import { readFile, unlink } from "node:fs/promises";
|
||||||
|
|
||||||
export const runContainer = async (
|
export const runContainer = async (
|
||||||
image: string,
|
image: string,
|
||||||
@@ -21,7 +21,8 @@ export const runContainer = async (
|
|||||||
"-c",
|
"-c",
|
||||||
init,
|
init,
|
||||||
]);
|
]);
|
||||||
let containerID = await readableStreamToText(proc.stdout);
|
|
||||||
|
const containerID = await readableStreamToText(proc.stdout);
|
||||||
const exitCode = await proc.exited;
|
const exitCode = await proc.exited;
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
throw new Error(containerID);
|
throw new Error(containerID);
|
||||||
@@ -29,12 +30,14 @@ export const runContainer = async (
|
|||||||
return containerID.trim();
|
return containerID.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
// executeScriptInContainer finds the only "coder_script"
|
/**
|
||||||
// resource in the given state and runs it in a container.
|
* Finds the only "coder_script" resource in the given state and runs it in a
|
||||||
|
* container.
|
||||||
|
*/
|
||||||
export const executeScriptInContainer = async (
|
export const executeScriptInContainer = async (
|
||||||
state: TerraformState,
|
state: TerraformState,
|
||||||
image: string,
|
image: string,
|
||||||
shell: string = "sh",
|
shell = "sh",
|
||||||
): Promise<{
|
): Promise<{
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
stdout: string[];
|
stdout: string[];
|
||||||
@@ -76,46 +79,56 @@ export const execContainer = async (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TerraformState {
|
type JsonValue =
|
||||||
outputs: {
|
| string
|
||||||
[key: string]: {
|
| number
|
||||||
type: string;
|
| boolean
|
||||||
value: any;
|
| null
|
||||||
};
|
| JsonValue[]
|
||||||
}
|
| { [key: string]: JsonValue };
|
||||||
resources: [
|
|
||||||
|
type TerraformStateResource = {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
provider: string;
|
||||||
|
|
||||||
|
instances: [
|
||||||
{
|
{
|
||||||
type: string;
|
attributes: Record<string, JsonValue>;
|
||||||
name: string;
|
|
||||||
provider: string;
|
|
||||||
instances: [
|
|
||||||
{
|
|
||||||
attributes: {
|
|
||||||
[key: string]: any;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
type TerraformOutput = {
|
||||||
|
type: string;
|
||||||
|
value: JsonValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TerraformState {
|
||||||
|
outputs: Record<string, TerraformOutput>;
|
||||||
|
resources: [TerraformStateResource, ...TerraformStateResource[]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TerraformVariables = Record<string, JsonValue>;
|
||||||
|
|
||||||
export interface CoderScriptAttributes {
|
export interface CoderScriptAttributes {
|
||||||
script: string;
|
script: string;
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// findResourceInstance finds the first instance of the given resource
|
export type ResourceInstance<T extends string = string> =
|
||||||
// type in the given state. If name is specified, it will only find
|
T extends "coder_script" ? CoderScriptAttributes : Record<string, string>;
|
||||||
// the instance with the given name.
|
|
||||||
export const findResourceInstance = <T extends "coder_script" | string>(
|
/**
|
||||||
|
* finds the first instance of the given resource type in the given state. If
|
||||||
|
* name is specified, it will only find the instance with the given name.
|
||||||
|
*/
|
||||||
|
export const findResourceInstance = <T extends string>(
|
||||||
state: TerraformState,
|
state: TerraformState,
|
||||||
type: T,
|
type: T,
|
||||||
name?: string,
|
name?: string,
|
||||||
// if type is "coder_script" return CoderScriptAttributes
|
): ResourceInstance<T> => {
|
||||||
): T extends "coder_script"
|
|
||||||
? CoderScriptAttributes
|
|
||||||
: Record<string, string> => {
|
|
||||||
const resource = state.resources.find(
|
const resource = state.resources.find(
|
||||||
(resource) =>
|
(resource) =>
|
||||||
resource.type === type && (name ? resource.name === name : true),
|
resource.type === type && (name ? resource.name === name : true),
|
||||||
@@ -128,33 +141,41 @@ export const findResourceInstance = <T extends "coder_script" | string>(
|
|||||||
`Resource ${type} has ${resource.instances.length} instances`,
|
`Resource ${type} has ${resource.instances.length} instances`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return resource.instances[0].attributes as any;
|
|
||||||
|
return resource.instances[0].attributes as ResourceInstance<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// testRequiredVariables creates a test-case
|
/**
|
||||||
// for each variable provided and ensures that
|
* Creates a test-case for each variable provided and ensures that the apply
|
||||||
// the apply fails without it.
|
* fails without it.
|
||||||
export const testRequiredVariables = (
|
*/
|
||||||
|
export const testRequiredVariables = <TVars extends TerraformVariables>(
|
||||||
dir: string,
|
dir: string,
|
||||||
vars: Record<string, string>,
|
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) => {
|
for (const varName of varNames) {
|
||||||
// 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) => {
|
for (const otherVarName of varNames) {
|
||||||
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`,
|
||||||
);
|
);
|
||||||
@@ -162,19 +183,31 @@ export const testRequiredVariables = (
|
|||||||
}
|
}
|
||||||
throw new Error(`${varName} is not a required variable!`);
|
throw new Error(`${varName} is not a required variable!`);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// runTerraformApply runs terraform apply in the given directory
|
/**
|
||||||
// with the given variables. It is fine to run in parallel with
|
* Runs terraform apply in the given directory with the given variables. It is
|
||||||
// other instances of this function, as it uses a random state file.
|
* fine to run in parallel with other instances of this function, as it uses a
|
||||||
export const runTerraformApply = async (
|
* random state file.
|
||||||
|
*/
|
||||||
|
export const runTerraformApply = async <TVars extends TerraformVariables>(
|
||||||
dir: string,
|
dir: string,
|
||||||
vars: Record<string, string>,
|
vars: Readonly<TVars>,
|
||||||
env: Record<string, string> = {},
|
customEnv?: Record<string, string>,
|
||||||
): Promise<TerraformState> => {
|
): Promise<TerraformState> => {
|
||||||
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
|
const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`;
|
||||||
Object.keys(vars).forEach((key) => (env[`TF_VAR_${key}`] = vars[key]));
|
|
||||||
|
const childEnv: Record<string, string | undefined> = {
|
||||||
|
...process.env,
|
||||||
|
...(customEnv ?? {}),
|
||||||
|
};
|
||||||
|
for (const [key, value] of Object.entries(vars) as [string, JsonValue][]) {
|
||||||
|
if (value !== null) {
|
||||||
|
childEnv[`TF_VAR_${key}`] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const proc = spawn(
|
const proc = spawn(
|
||||||
[
|
[
|
||||||
"terraform",
|
"terraform",
|
||||||
@@ -188,22 +221,26 @@ export const runTerraformApply = async (
|
|||||||
],
|
],
|
||||||
{
|
{
|
||||||
cwd: dir,
|
cwd: dir,
|
||||||
env,
|
env: childEnv,
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const text = await readableStreamToText(proc.stderr);
|
const text = await readableStreamToText(proc.stderr);
|
||||||
const exitCode = await proc.exited;
|
const exitCode = await proc.exited;
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
throw new Error(text);
|
throw new Error(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await readFile(stateFile, "utf8");
|
const content = await readFile(stateFile, "utf8");
|
||||||
await unlink(stateFile);
|
await unlink(stateFile);
|
||||||
return JSON.parse(content);
|
return JSON.parse(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
// runTerraformInit runs terraform init in the given directory.
|
/**
|
||||||
|
* Runs terraform init in the given directory.
|
||||||
|
*/
|
||||||
export const runTerraformInit = async (dir: string) => {
|
export const runTerraformInit = async (dir: string) => {
|
||||||
const proc = spawn(["terraform", "init"], {
|
const proc = spawn(["terraform", "init"], {
|
||||||
cwd: dir,
|
cwd: dir,
|
||||||
@@ -221,8 +258,8 @@ export const createJSONResponse = (obj: object, statusCode = 200): Response => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
status: statusCode,
|
status: statusCode,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
export const writeCoder = async (id: string, script: string) => {
|
export const writeCoder = async (id: string, script: string) => {
|
||||||
const exec = await execContainer(id, [
|
const exec = await execContainer(id, [
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
// If we were just compiling for the tests, we could safely target ESNext at
|
||||||
"module": "esnext",
|
// all times, but just because we've been starting to add more runtime logic
|
||||||
|
// files to some of the modules, erring on the side of caution by having a
|
||||||
|
// older compilation target
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "ESNext",
|
||||||
|
"strict": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "node",
|
||||||
"types": ["bun-types"]
|
"types": ["bun-types"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,65 @@
|
|||||||
#!/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" '
|
||||||
if ($1 == "version" && $2 == "=") {
|
BEGIN { in_code_block = 0; in_nested_block = 0 }
|
||||||
sub(/"[^"]*"/, "\"" tag "\"")
|
{
|
||||||
print
|
# Detect the start and end of Markdown code blocks.
|
||||||
} else {
|
if ($0 ~ /^```/) {
|
||||||
|
in_code_block = !in_code_block
|
||||||
|
# Reset nested block tracking when exiting a code block.
|
||||||
|
if (!in_code_block) {
|
||||||
|
in_nested_block = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle nested blocks within a code block.
|
||||||
|
if (in_code_block) {
|
||||||
|
# Detect the start of a nested block (skipping "module" blocks).
|
||||||
|
if ($0 ~ /{/ && !($1 == "module" || $1 ~ /^[a-zA-Z0-9_]+$/)) {
|
||||||
|
in_nested_block++
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect the end of a nested block.
|
||||||
|
if ($0 ~ /}/ && in_nested_block > 0) {
|
||||||
|
in_nested_block--
|
||||||
|
}
|
||||||
|
|
||||||
|
# Update "version" only if not in a nested block.
|
||||||
|
if (!in_nested_block && $1 == "version" && $2 == "=") {
|
||||||
|
sub(/"[^"]*"/, "\"" tag "\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ This module lets you authenticate with [Hashicorp Vault](https://www.vaultprojec
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "vault" {
|
module "vault" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/vault-github/coder"
|
source = "registry.coder.com/modules/vault-github/coder"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
@@ -45,6 +46,7 @@ To configure the Vault module, you must set up a Vault GitHub auth method. See t
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "vault" {
|
module "vault" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/vault-github/coder"
|
source = "registry.coder.com/modules/vault-github/coder"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
@@ -57,6 +59,7 @@ module "vault" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "vault" {
|
module "vault" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/vault-github/coder"
|
source = "registry.coder.com/modules/vault-github/coder"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
@@ -70,6 +73,7 @@ module "vault" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "vault" {
|
module "vault" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/vault-github/coder"
|
source = "registry.coder.com/modules/vault-github/coder"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
|
|||||||
81
vault-jwt/README.md
Normal file
81
vault-jwt/README.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
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/users/oidc-auth) 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" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||||
|
version = "1.0.20"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
vault_addr = "https://vault.example.com"
|
||||||
|
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can use the Vault CLI in your workspaces to fetch secrets from Vault:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
vault kv get -namespace=coder -mount=secrets coder
|
||||||
|
```
|
||||||
|
|
||||||
|
or using the Vault API:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -H "X-Vault-Token: ${VAULT_TOKEN}" -X GET "${VAULT_ADDR}/v1/coder/secrets/data/coder"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Configure Vault integration with a non standard auth path (default is "jwt")
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "vault" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||||
|
version = "1.0.20"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
vault_addr = "https://vault.example.com"
|
||||||
|
vault_jwt_auth_path = "oidc"
|
||||||
|
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Map workspace owner's group to a Vault role
|
||||||
|
|
||||||
|
```tf
|
||||||
|
data "coder_workspace_owner" "me" {}
|
||||||
|
|
||||||
|
module "vault" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||||
|
version = "1.0.20"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
vault_addr = "https://vault.example.com"
|
||||||
|
vault_jwt_role = data.coder_workspace_owner.me.groups[0]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install a specific version of the Vault CLI
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "vault" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/modules/vault-jwt/coder"
|
||||||
|
version = "1.0.20"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
vault_addr = "https://vault.example.com"
|
||||||
|
vault_jwt_role = "coder" # The Vault role to use for authentication
|
||||||
|
vault_cli_version = "1.17.5"
|
||||||
|
}
|
||||||
|
```
|
||||||
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=$${LATEST_VERSION}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the vault CLI is installed and has the correct version
|
||||||
|
installation_needed=1
|
||||||
|
if command -v vault > /dev/null 2>&1; then
|
||||||
|
CURRENT_VERSION=$(vault version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||||
|
if [ "$${CURRENT_VERSION}" = "$${VAULT_CLI_VERSION}" ]; then
|
||||||
|
printf "Vault version %s is already installed and up-to-date.\n\n" "$${CURRENT_VERSION}"
|
||||||
|
installation_needed=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $${installation_needed} -eq 1 ]; then
|
||||||
|
# Download and install Vault
|
||||||
|
if [ -z "$${CURRENT_VERSION}" ]; then
|
||||||
|
printf "Installing Vault CLI ...\n\n"
|
||||||
|
else
|
||||||
|
printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${VAULT_CLI_VERSION}"
|
||||||
|
fi
|
||||||
|
fetch vault.zip "https://releases.hashicorp.com/vault/$${VAULT_CLI_VERSION}/vault_$${VAULT_CLI_VERSION}_linux_$${ARCH}.zip"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
printf "Failed to download Vault.\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! unzip_safe vault.zip; then
|
||||||
|
printf "Failed to unzip Vault.\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
rm vault.zip
|
||||||
|
if sudo mv vault /usr/local/bin/vault 2> /dev/null; then
|
||||||
|
printf "Vault installed successfully!\n\n"
|
||||||
|
else
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
if ! mv vault ~/.local/bin/vault; then
|
||||||
|
printf "Failed to move Vault to local bin.\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
printf "Please add ~/.local/bin to your PATH to use vault CLI.\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
if ! (
|
||||||
|
cd "$TMP"
|
||||||
|
install
|
||||||
|
); then
|
||||||
|
echo "Failed to install Vault CLI."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
rm -rf "$TMP"
|
||||||
|
|
||||||
|
# Authenticate with Vault
|
||||||
|
printf "🔑 Authenticating with Vault ...\n\n"
|
||||||
|
echo "$${CODER_OIDC_ACCESS_TOKEN}" | vault write auth/"$${VAULT_JWT_AUTH_PATH}"/login role="$${VAULT_JWT_ROLE}" jwt=-
|
||||||
|
printf "🥳 Vault authentication complete!\n\n"
|
||||||
|
printf "You can now use Vault CLI to access secrets.\n"
|
||||||
@@ -15,8 +15,9 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "vscode" {
|
module "vscode" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/vscode-desktop/coder"
|
source = "registry.coder.com/modules/vscode-desktop/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.15"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -27,8 +28,9 @@ module "vscode" {
|
|||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "vscode" {
|
module "vscode" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
source = "registry.coder.com/modules/vscode-desktop/coder"
|
source = "registry.coder.com/modules/vscode-desktop/coder"
|
||||||
version = "1.0.8"
|
version = "1.0.15"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
folder = "/home/coder/project"
|
folder = "/home/coder/project"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,11 +18,58 @@ describe("vscode-desktop", async () => {
|
|||||||
agent_id: "foo",
|
agent_id: "foo",
|
||||||
});
|
});
|
||||||
expect(state.outputs.vscode_url.value).toBe(
|
expect(state.outputs.vscode_url.value).toBe(
|
||||||
"vscode://coder.coder-remote/open?owner=default&workspace=default&token=$SESSION_TOKEN",
|
"vscode://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
);
|
);
|
||||||
|
|
||||||
const resources: any = state.resources;
|
const coder_app = state.resources.find(
|
||||||
expect(resources[1].instances[0].attributes.order).toBeNull();
|
(res) => res.type === "coder_app" && res.name === "vscode",
|
||||||
|
);
|
||||||
|
|
||||||
|
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.vscode_url.value).toBe(
|
||||||
|
"vscode://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.vscode_url.value).toBe(
|
||||||
|
"vscode://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.vscode_url.value).toBe(
|
||||||
|
"vscode://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.vscode_url.value).toBe(
|
||||||
|
"vscode://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("expect order to be set", async () => {
|
it("expect order to be set", async () => {
|
||||||
@@ -31,7 +78,12 @@ describe("vscode-desktop", async () => {
|
|||||||
order: "22",
|
order: "22",
|
||||||
});
|
});
|
||||||
|
|
||||||
const resources: any = state.resources;
|
const coder_app = state.resources.find(
|
||||||
expect(resources[1].instances[0].attributes.order).toBe(22);
|
(res) => res.type === "coder_app" && res.name === "vscode",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(coder_app).not.toBeNull();
|
||||||
|
expect(coder_app?.instances.length).toBe(1);
|
||||||
|
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user