Compare commits
2 Commits
main
...
slackme/ex
Author | SHA1 | Date |
---|---|---|
![]() |
cd3d42d54e | 2 years ago |
![]() |
66f1c3b97c | 2 years ago |
@ -1,6 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
@ -1,203 +0,0 @@
|
||||
#!/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}"
|
@ -1,23 +0,0 @@
|
||||
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 }}
|
@ -1,37 +0,0 @@
|
||||
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
|
||||
|
@ -1,7 +1,4 @@
|
||||
.terraform*
|
||||
node_modules
|
||||
*.tfstate
|
||||
*.tfstate.lock.info
|
||||
|
||||
# Ignore generated credentials from google-github-actions/auth
|
||||
gha-creds-*.json
|
||||
*.tfstate.lock.info
|
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.5 MiB |
@ -1 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.7 KiB |
@ -1,5 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 540 B |
@ -1 +0,0 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68.03 68.03"><defs><style>.cls-1{fill:#da291c;}</style></defs><title>Artboard 1</title><polygon class="cls-1" points="34.02 13.31 11.27 52.72 14.52 52.72 34.02 18.94 34.02 24.57 17.77 52.72 21.02 52.72 34.02 30.2 34.02 35.83 24.27 52.72 27.52 52.72 34.02 41.46 34.02 47.09 30.77 52.72 34.02 52.72 34.02 52.72 56.77 52.72 34.02 13.31"/></svg>
|
Before Width: | Height: | Size: 427 B |
@ -1 +0,0 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
Before Width: | Height: | Size: 960 B |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 2.2 KiB |
@ -1 +0,0 @@
|
||||
<svg width="2270" height="2500" viewBox="0 0 256 282" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><g fill="#8CC84B"><path d="M116.504 3.58c6.962-3.985 16.03-4.003 22.986 0 34.995 19.774 70.001 39.517 104.99 59.303 6.581 3.707 10.983 11.031 10.916 18.614v118.968c.049 7.897-4.788 15.396-11.731 19.019-34.88 19.665-69.742 39.354-104.616 59.019-7.106 4.063-16.356 3.75-23.24-.646-10.457-6.062-20.932-12.094-31.39-18.15-2.137-1.274-4.546-2.288-6.055-4.36 1.334-1.798 3.719-2.022 5.657-2.807 4.365-1.388 8.374-3.616 12.384-5.778 1.014-.694 2.252-.428 3.224.193 8.942 5.127 17.805 10.403 26.777 15.481 1.914 1.105 3.852-.362 5.488-1.274 34.228-19.345 68.498-38.617 102.72-57.968 1.268-.61 1.969-1.956 1.866-3.345.024-39.245.006-78.497.012-117.742.145-1.576-.767-3.025-2.192-3.67-34.759-19.575-69.5-39.18-104.253-58.76a3.621 3.621 0 0 0-4.094-.006C91.2 39.257 56.465 58.88 21.712 78.454c-1.42.646-2.373 2.071-2.204 3.653.006 39.245 0 78.497 0 117.748a3.329 3.329 0 0 0 1.89 3.303c9.274 5.259 18.56 10.481 27.84 15.722 5.228 2.814 11.647 4.486 17.407 2.33 5.083-1.823 8.646-7.01 8.549-12.407.048-39.016-.024-78.038.036-117.048-.127-1.732 1.516-3.163 3.2-3 4.456-.03 8.918-.06 13.374.012 1.86-.042 3.14 1.823 2.91 3.568-.018 39.263.048 78.527-.03 117.79.012 10.464-4.287 21.85-13.966 26.97-11.924 6.177-26.662 4.867-38.442-1.056-10.198-5.09-19.93-11.097-29.947-16.55C5.368 215.886.555 208.357.604 200.466V81.497c-.073-7.74 4.504-15.197 11.29-18.85C46.768 42.966 81.636 23.27 116.504 3.58z"/><path d="M146.928 85.99c15.21-.979 31.493-.58 45.18 6.913 10.597 5.742 16.472 17.793 16.659 29.566-.296 1.588-1.956 2.464-3.472 2.355-4.413-.006-8.827.06-13.24-.03-1.872.072-2.96-1.654-3.195-3.309-1.268-5.633-4.34-11.212-9.642-13.929-8.139-4.075-17.576-3.87-26.451-3.785-6.479.344-13.446.905-18.935 4.715-4.214 2.886-5.494 8.712-3.99 13.404 1.418 3.369 5.307 4.456 8.489 5.458 18.33 4.794 37.754 4.317 55.734 10.626 7.444 2.572 14.726 7.572 17.274 15.366 3.333 10.446 1.872 22.932-5.56 31.318-6.027 6.901-14.805 10.657-23.56 12.697-11.647 2.597-23.734 2.663-35.562 1.51-11.122-1.268-22.696-4.19-31.282-11.768-7.342-6.375-10.928-16.308-10.572-25.895.085-1.619 1.697-2.748 3.248-2.615 4.444-.036 8.888-.048 13.332.006 1.775-.127 3.091 1.407 3.182 3.08.82 5.367 2.837 11 7.517 14.182 9.032 5.827 20.365 5.428 30.707 5.591 8.568-.38 18.186-.495 25.178-6.158 3.689-3.23 4.782-8.634 3.785-13.283-1.08-3.925-5.186-5.754-8.712-6.95-18.095-5.724-37.736-3.647-55.656-10.12-7.275-2.571-14.31-7.432-17.105-14.906-3.9-10.578-2.113-23.662 6.098-31.765 8.006-8.06 19.563-11.164 30.551-12.275z"/></g></svg>
|
Before Width: | Height: | Size: 2.5 KiB |
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#FFD814" d="M0 0l7.971 15.516L16 0H0zm6.732 6.16h-1.27V4.89h1.27v1.27zm0-1.906h-1.27V2.985h1.27v1.269zm1.904 3.81h-1.27v-1.27h1.27v1.27zm0-1.905h-1.27V4.89h1.27v1.27zm0-1.905h-1.27V2.985h1.27v1.269zm1.894 1.905H9.26V4.89h1.27v1.27zM9.26 4.254V2.985h1.27v1.269H9.26z"/></svg>
|
Before Width: | Height: | Size: 506 B |
Before Width: | Height: | Size: 603 KiB |
Before Width: | Height: | Size: 3.3 MiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 174 KiB |
Before Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 205 KiB |
@ -1,75 +1,26 @@
|
||||
# Contributing
|
||||
|
||||
## 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
|
||||
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:
|
||||
To create a new module, clone this repository and run:
|
||||
|
||||
```shell
|
||||
./new.sh NAME_OF_NEW_MODULE
|
||||
./new.sh MOUDLE_NAME
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
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 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:
|
||||
Reference existing `*.test.ts` files for implementation.
|
||||
|
||||
```shell
|
||||
# Run tests for a specific 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
|
||||
|
||||
```tf
|
||||
module "example" {
|
||||
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.
|
||||
}
|
||||
```hcl
|
||||
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
|
||||
```
|
||||
|
||||
## 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.
|
||||
> **Note:** This is the responsibility of the module author to implement tests for their module. and test the module locally before submitting a PR.
|
||||
|
@ -1,49 +0,0 @@
|
||||
---
|
||||
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.
|
@ -1,170 +0,0 @@
|
||||
# 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"
|
@ -1,85 +0,0 @@
|
||||
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
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
---
|
||||
display_name: airflow
|
||||
description: A module that adds Apache Airflow in your Coder template
|
||||
icon: ../.icons/airflow.svg
|
||||
maintainer_github: coder
|
||||
partner_github: nataindata
|
||||
verified: true
|
||||
tags: [airflow, idea, web, helper]
|
||||
---
|
||||
|
||||
# airflow
|
||||
|
||||
A module that adds Apache Airflow in your Coder template.
|
||||
|
||||
```tf
|
||||
module "airflow" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/apache-airflow/coder"
|
||||
version = "1.0.13"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||

|
@ -1,65 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Add required variables for your modules and remove any unneeded variables
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "log_path" {
|
||||
type = string
|
||||
description = "The path to log airflow to."
|
||||
default = "/tmp/airflow.log"
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "The port to run airflow on."
|
||||
default = 8080
|
||||
}
|
||||
|
||||
variable "share" {
|
||||
type = string
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "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
|
||||
}
|
||||
|
||||
resource "coder_script" "airflow" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "airflow"
|
||||
icon = "/icon/apache-guacamole.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
LOG_PATH : var.log_path,
|
||||
PORT : var.port
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "airflow" {
|
||||
agent_id = var.agent_id
|
||||
slug = "airflow"
|
||||
display_name = "airflow"
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = "/icon/apache-guacamole.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
PATH=$PATH:~/.local/bin
|
||||
pip install --upgrade apache-airflow
|
||||
|
||||
filename=~/airflow/airflow.db
|
||||
if ! [ -f $filename ] || ! [ -s $filename ]; then
|
||||
airflow db init
|
||||
fi
|
||||
|
||||
export AIRFLOW__CORE__LOAD_EXAMPLES=false
|
||||
|
||||
airflow webserver > ${LOG_PATH} 2>&1 &
|
||||
|
||||
airflow scheduler >> /tmp/airflow_scheduler.log 2>&1 &
|
||||
|
||||
airflow users create -u admin -p admin -r Admin -e admin@admin.com -f Coder -l User
|
@ -1,37 +0,0 @@
|
||||
---
|
||||
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"
|
||||
}
|
||||
```
|
@ -1,88 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
@ -1,62 +0,0 @@
|
||||
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."
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DOTFILES_URI="${DOTFILES_URI}"
|
||||
DOTFILES_USER="${DOTFILES_USER}"
|
||||
|
||||
if [ -n "$${DOTFILES_URI// }" ]; then
|
||||
if [ -z "$DOTFILES_USER" ]; then
|
||||
DOTFILES_USER="$USER"
|
||||
fi
|
||||
|
||||
echo "✨ Applying dotfiles for user $DOTFILES_USER"
|
||||
|
||||
if [ "$DOTFILES_USER" = "$USER" ]; then
|
||||
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log
|
||||
else
|
||||
# The `eval echo ~"$DOTFILES_USER"` part is used to dynamically get the home directory of the user, see https://superuser.com/a/484280
|
||||
# eval echo ~coder -> "/home/coder"
|
||||
# eval echo ~root -> "/root"
|
||||
|
||||
CODER_BIN=$(which coder)
|
||||
DOTFILES_USER_HOME=$(eval echo ~"$DOTFILES_USER")
|
||||
sudo -u "$DOTFILES_USER" sh -c "'$CODER_BIN' dotfiles '$DOTFILES_URI' -y 2>&1 | tee '$DOTFILES_USER_HOME'/.dotfiles.log"
|
||||
fi
|
||||
fi
|
@ -1,117 +0,0 @@
|
||||
---
|
||||
display_name: exoscale-instance-type
|
||||
description: A parameter with human readable exoscale instance names
|
||||
icon: ../.icons/exoscale.svg
|
||||
maintainer_github: WhizUs
|
||||
verified: false
|
||||
tags: [helper, parameter, instances, exoscale]
|
||||
---
|
||||
|
||||
# exoscale-instance-type
|
||||
|
||||
A parameter with all Exoscale instance types. This allows developers to select
|
||||
their desired virtual machine for the workspace.
|
||||
|
||||
Customize the preselected parameter value:
|
||||
|
||||
```tf
|
||||
module "exoscale-instance-type" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||
version = "1.0.12"
|
||||
default = "standard.medium"
|
||||
}
|
||||
|
||||
resource "exoscale_compute_instance" "instance" {
|
||||
type = module.exoscale-instance-type.value
|
||||
# ...
|
||||
}
|
||||
|
||||
resource "coder_metadata" "workspace_info" {
|
||||
item {
|
||||
key = "instance type"
|
||||
value = module.exoscale-instance-type.name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Examples
|
||||
|
||||
### Customize type
|
||||
|
||||
Change the display name a type using the corresponding maps:
|
||||
|
||||
```tf
|
||||
module "exoscale-instance-type" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||
version = "1.0.12"
|
||||
default = "standard.medium"
|
||||
|
||||
custom_names = {
|
||||
"standard.medium" : "Mittlere Instanz" # German translation
|
||||
}
|
||||
|
||||
custom_descriptions = {
|
||||
"standard.medium" : "4 GB Arbeitsspeicher, 2 Kerne, 10 - 400 GB Festplatte" # German translation
|
||||
}
|
||||
}
|
||||
|
||||
resource "exoscale_compute_instance" "instance" {
|
||||
type = module.exoscale-instance-type.value
|
||||
# ...
|
||||
}
|
||||
|
||||
resource "coder_metadata" "workspace_info" {
|
||||
item {
|
||||
key = "instance type"
|
||||
value = module.exoscale-instance-type.name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Use category and exclude type
|
||||
|
||||
Show only gpu1 types
|
||||
|
||||
```tf
|
||||
module "exoscale-instance-type" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-instance-type/coder"
|
||||
version = "1.0.12"
|
||||
default = "gpu.large"
|
||||
type_category = ["gpu"]
|
||||
exclude = [
|
||||
"gpu2.small",
|
||||
"gpu2.medium",
|
||||
"gpu2.large",
|
||||
"gpu2.huge",
|
||||
"gpu3.small",
|
||||
"gpu3.medium",
|
||||
"gpu3.large",
|
||||
"gpu3.huge"
|
||||
]
|
||||
}
|
||||
|
||||
resource "exoscale_compute_instance" "instance" {
|
||||
type = module.exoscale-instance-type.value
|
||||
# ...
|
||||
}
|
||||
|
||||
resource "coder_metadata" "workspace_info" {
|
||||
item {
|
||||
key = "instance type"
|
||||
value = module.exoscale-instance-type.name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Related templates
|
||||
|
||||
A related exoscale template will be provided soon.
|
@ -1,43 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "../test";
|
||||
|
||||
describe("exoscale-instance-type", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {});
|
||||
expect(state.outputs.value.value).toBe("");
|
||||
});
|
||||
|
||||
it("customized default", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
default: "gpu3.huge",
|
||||
type_category: `["gpu", "cpu"]`,
|
||||
});
|
||||
expect(state.outputs.value.value).toBe("gpu3.huge");
|
||||
});
|
||||
|
||||
it("fails because of wrong categroy definition", async () => {
|
||||
expect(async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
default: "gpu3.huge",
|
||||
// type_category: ["standard"] is standard
|
||||
});
|
||||
}).toThrow('default value "gpu3.huge" must be defined as one of options');
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
const order = 99;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
expect(state.resources).toHaveLength(1);
|
||||
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
||||
});
|
||||
});
|
@ -1,286 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
default = "Exoscale instance type"
|
||||
description = "The display name of the parameter."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "description" {
|
||||
default = "Select the exoscale instance type to use for the workspace. Check out the pricing page for more information: https://www.exoscale.com/pricing"
|
||||
description = "The description of the parameter."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "default" {
|
||||
default = ""
|
||||
description = "The default instance type to use if no type is specified. One of [\"standard.micro\", \"standard.tiny\", \"standard.small\", \"standard.medium\", \"standard.large\", \"standard.extra\", \"standard.huge\", \"standard.mega\", \"standard.titan\", \"standard.jumbo\", \"standard.colossus\", \"cpu.extra\", \"cpu.huge\", \"cpu.mega\", \"cpu.titan\", \"memory.extra\", \"memory.huge\", \"memory.mega\", \"memory.titan\", \"storage.extra\", \"storage.huge\", \"storage.mega\", \"storage.titan\", \"storage.jumbo\", \"gpu.small\", \"gpu.medium\", \"gpu.large\", \"gpu.huge\", \"gpu2.small\", \"gpu2.medium\", \"gpu2.large\", \"gpu2.huge\", \"gpu3.small\", \"gpu3.medium\", \"gpu3.large\", \"gpu3.huge\"]"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "mutable" {
|
||||
default = false
|
||||
description = "Whether the parameter can be changed after creation."
|
||||
type = bool
|
||||
}
|
||||
|
||||
variable "custom_names" {
|
||||
default = {}
|
||||
description = "A map of custom display names for instance type IDs."
|
||||
type = map(string)
|
||||
}
|
||||
variable "custom_descriptions" {
|
||||
default = {}
|
||||
description = "A map of custom descriptions for instance type IDs."
|
||||
type = map(string)
|
||||
}
|
||||
|
||||
variable "type_category" {
|
||||
default = ["standard"]
|
||||
description = "A list of instance type categories the user is allowed to choose. One of [\"standard\", \"cpu\", \"memory\", \"storage\", \"gpu\"]"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "exclude" {
|
||||
default = []
|
||||
description = "A list of instance type IDs to exclude. One of [\"standard.micro\", \"standard.tiny\", \"standard.small\", \"standard.medium\", \"standard.large\", \"standard.extra\", \"standard.huge\", \"standard.mega\", \"standard.titan\", \"standard.jumbo\", \"standard.colossus\", \"cpu.extra\", \"cpu.huge\", \"cpu.mega\", \"cpu.titan\", \"memory.extra\", \"memory.huge\", \"memory.mega\", \"memory.titan\", \"storage.extra\", \"storage.huge\", \"storage.mega\", \"storage.titan\", \"storage.jumbo\", \"gpu.small\", \"gpu.medium\", \"gpu.large\", \"gpu.huge\", \"gpu2.small\", \"gpu2.medium\", \"gpu2.large\", \"gpu2.huge\", \"gpu3.small\", \"gpu3.medium\", \"gpu3.large\", \"gpu3.huge\"]"
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
# https://www.exoscale.com/pricing/
|
||||
|
||||
standard_instances = [
|
||||
{
|
||||
value = "standard.micro",
|
||||
name = "Standard Micro",
|
||||
description = "512 MB RAM, 1 Core, 10 - 200 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.tiny",
|
||||
name = "Standard Tiny",
|
||||
description = "1 GB RAM, 1 Core, 10 - 400 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.small",
|
||||
name = "Standard Small",
|
||||
description = "2 GB RAM, 2 Cores, 10 - 400 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.medium",
|
||||
name = "Standard Medium",
|
||||
description = "4 GB RAM, 2 Cores, 10 - 400 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.large",
|
||||
name = "Standard Large",
|
||||
description = "8 GB RAM, 4 Cores, 10 - 400 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.extra",
|
||||
name = "Standard Extra",
|
||||
description = "rge",
|
||||
description = "16 GB RAM, 4 Cores, 10 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.huge",
|
||||
name = "Standard Huge",
|
||||
description = "32 GB RAM, 8 Cores, 10 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.mega",
|
||||
name = "Standard Mega",
|
||||
description = "64 GB RAM, 12 Cores, 10 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.titan",
|
||||
name = "Standard Titan",
|
||||
description = "128 GB RAM, 16 Cores, 10 - 1.6 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.jumbo",
|
||||
name = "Standard Jumbo",
|
||||
description = "256 GB RAM, 24 Cores, 10 - 1.6 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "standard.colossus",
|
||||
name = "Standard Colossus",
|
||||
description = "320 GB RAM, 40 Cores, 10 - 1.6 TB Disk"
|
||||
}
|
||||
]
|
||||
cpu_instances = [
|
||||
{
|
||||
value = "cpu.extra",
|
||||
name = "CPU Extra-Large",
|
||||
description = "16 GB RAM, 8 Cores, 10 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "cpu.huge",
|
||||
name = "CPU Huge",
|
||||
description = "32 GB RAM, 16 Cores, 10 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "cpu.mega",
|
||||
name = "CPU Mega",
|
||||
description = "64 GB RAM, 32 Cores, 10 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "cpu.titan",
|
||||
name = "CPU Titan",
|
||||
description = "128 GB RAM, 40 Cores, 0.1 - 1.6 TB Disk"
|
||||
}
|
||||
]
|
||||
memory_instances = [
|
||||
{
|
||||
value = "memory.extra",
|
||||
name = "Memory Extra-Large",
|
||||
description = "16 GB RAM, 2 Cores, 10 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "memory.huge",
|
||||
name = "Memory Huge",
|
||||
description = "32 GB RAM, 4 Cores, 10 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "memory.mega",
|
||||
name = "Memory Mega",
|
||||
description = "64 GB RAM, 8 Cores, 10 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "memory.titan",
|
||||
name = "Memory Titan",
|
||||
description = "128 GB RAM, 12 Cores, 0.1 - 1.6 TB Disk"
|
||||
}
|
||||
]
|
||||
storage_instances = [
|
||||
{
|
||||
value = "storage.extra",
|
||||
name = "Storage Extra-Large",
|
||||
description = "16 GB RAM, 4 Cores, 1 - 2 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "storage.huge",
|
||||
name = "Storage Huge",
|
||||
description = "32 GB RAM, 8 Cores, 2 - 3 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "storage.mega",
|
||||
name = "Storage Mega",
|
||||
description = "64 GB RAM, 12 Cores, 3 - 5 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "storage.titan",
|
||||
name = "Storage Titan",
|
||||
description = "128 GB RAM, 16 Cores, 5 - 10 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "storage.jumbo",
|
||||
name = "Storage Jumbo",
|
||||
description = "225 GB RAM, 24 Cores, 10 - 15 TB Disk"
|
||||
}
|
||||
]
|
||||
gpu_instances = [
|
||||
{
|
||||
value = "gpu.small",
|
||||
name = "GPU1 Small",
|
||||
description = "56 GB RAM, 12 Cores, 1 GPU, 100 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu.medium",
|
||||
name = "GPU1 Medium",
|
||||
description = "90 GB RAM, 16 Cores, 2 GPU, 0.1 - 1.2 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu.large",
|
||||
name = "GPU1 Large",
|
||||
description = "120 GB RAM, 24 Cores, 3 GPU, 0.1 - 1.6 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu.huge",
|
||||
name = "GPU1 Huge",
|
||||
description = "225 GB RAM, 48 Cores, 4 GPU, 0.1 - 1.6 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu2.small",
|
||||
name = "GPU2 Small",
|
||||
description = "56 GB RAM, 12 Cores, 1 GPU, 100 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu2.medium",
|
||||
name = "GPU2 Medium",
|
||||
description = "90 GB RAM, 16 Cores, 2 GPU, 0.1 - 1.2 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu2.large",
|
||||
name = "GPU2 Large",
|
||||
description = "120 GB RAM, 24 Cores, 3 GPU, 0.1 - 1.6 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu2.huge",
|
||||
name = "GPU2 Huge",
|
||||
description = "225 GB RAM, 48 Cores, 4 GPU, 0.1 - 1.6 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu3.small",
|
||||
name = "GPU3 Small",
|
||||
description = "56 GB RAM, 12 Cores, 1 GPU, 100 - 800 GB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu3.medium",
|
||||
name = "GPU3 Medium",
|
||||
description = "120 GB RAM, 24 Cores, 2 GPU, 0.1 - 1.2 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu3.large",
|
||||
name = "GPU3 Large",
|
||||
description = "224 GB RAM, 48 Cores, 4 GPU, 0.1 - 1.6 TB Disk"
|
||||
},
|
||||
{
|
||||
value = "gpu3.huge",
|
||||
name = "GPU3 Huge",
|
||||
description = "448 GB RAM, 96 Cores, 8 GPU, 0.1 - 1.6 TB Disk"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
data "coder_parameter" "instance_type" {
|
||||
name = "exoscale_instance_type"
|
||||
display_name = var.display_name
|
||||
description = var.description
|
||||
default = var.default == "" ? null : var.default
|
||||
order = var.coder_parameter_order
|
||||
mutable = var.mutable
|
||||
dynamic "option" {
|
||||
for_each = [for k, v in concat(
|
||||
contains(var.type_category, "standard") ? local.standard_instances : [],
|
||||
contains(var.type_category, "cpu") ? local.cpu_instances : [],
|
||||
contains(var.type_category, "memory") ? local.memory_instances : [],
|
||||
contains(var.type_category, "storage") ? local.storage_instances : [],
|
||||
contains(var.type_category, "gpu") ? local.gpu_instances : []
|
||||
) : v if !(contains(var.exclude, v.value))]
|
||||
content {
|
||||
name = try(var.custom_names[option.value.value], option.value.name)
|
||||
description = try(var.custom_descriptions[option.value.value], option.value.description)
|
||||
value = option.value.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "value" {
|
||||
value = data.coder_parameter.instance_type.value
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
---
|
||||
display_name: exoscale-zone
|
||||
description: A parameter with human zone names and icons
|
||||
icon: ../.icons/exoscale.svg
|
||||
maintainer_github: WhizUs
|
||||
verified: false
|
||||
tags: [helper, parameter, zones, regions, exoscale]
|
||||
---
|
||||
|
||||
# exoscale-zone
|
||||
|
||||
A parameter with all Exoscale zones. This allows developers to select
|
||||
the zone closest to them.
|
||||
|
||||
Customize the preselected parameter value:
|
||||
|
||||
```tf
|
||||
module "exoscale-zone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||
version = "1.0.12"
|
||||
default = "ch-dk-2"
|
||||
}
|
||||
|
||||
|
||||
data "exoscale_compute_template" "my_template" {
|
||||
zone = module.exoscale-zone.value
|
||||
name = "Linux Ubuntu 22.04 LTS 64-bit"
|
||||
}
|
||||
|
||||
resource "exoscale_compute_instance" "instance" {
|
||||
zone = module.exoscale-zone.value
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Examples
|
||||
|
||||
### Customize zones
|
||||
|
||||
Change the display name and icon for a zone using the corresponding maps:
|
||||
|
||||
```tf
|
||||
module "exoscale-zone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||
version = "1.0.12"
|
||||
default = "at-vie-1"
|
||||
|
||||
custom_names = {
|
||||
"at-vie-1" : "Home Vienna"
|
||||
}
|
||||
|
||||
custom_icons = {
|
||||
"at-vie-1" : "/emojis/1f3e0.png"
|
||||
}
|
||||
}
|
||||
|
||||
data "exoscale_compute_template" "my_template" {
|
||||
zone = module.exoscale-zone.value
|
||||
name = "Linux Ubuntu 22.04 LTS 64-bit"
|
||||
}
|
||||
|
||||
resource "exoscale_compute_instance" "instance" {
|
||||
zone = module.exoscale-zone.value
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Exclude regions
|
||||
|
||||
Hide the Switzerland zones Geneva and Zurich
|
||||
|
||||
```tf
|
||||
module "exoscale-zone" {
|
||||
source = "registry.coder.com/modules/exoscale-zone/coder"
|
||||
version = "1.0.12"
|
||||
exclude = ["ch-gva-2", "ch-dk-2"]
|
||||
}
|
||||
|
||||
data "exoscale_compute_template" "my_template" {
|
||||
zone = module.exoscale-zone.value
|
||||
name = "Linux Ubuntu 22.04 LTS 64-bit"
|
||||
}
|
||||
|
||||
resource "exoscale_compute_instance" "instance" {
|
||||
zone = module.exoscale-zone.value
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Related templates
|
||||
|
||||
An exoscale sample template will be delivered soon.
|
@ -1,33 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "../test";
|
||||
|
||||
describe("exoscale-zone", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {});
|
||||
expect(state.outputs.value.value).toBe("");
|
||||
});
|
||||
|
||||
it("customized default", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
default: "at-vie-1",
|
||||
});
|
||||
expect(state.outputs.value.value).toBe("at-vie-1");
|
||||
});
|
||||
|
||||
it("set custom order for coder_parameter", async () => {
|
||||
const order = 99;
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_parameter_order: order.toString(),
|
||||
});
|
||||
expect(state.resources).toHaveLength(1);
|
||||
expect(state.resources[0].instances[0].attributes.order).toBe(order);
|
||||
});
|
||||
});
|
@ -1,116 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
default = "Exoscale Region"
|
||||
description = "The display name of the parameter."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "description" {
|
||||
default = "The region to deploy workspace infrastructure."
|
||||
description = "The description of the parameter."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "default" {
|
||||
default = ""
|
||||
description = "The default region to use if no region is specified."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "mutable" {
|
||||
default = false
|
||||
description = "Whether the parameter can be changed after creation."
|
||||
type = bool
|
||||
}
|
||||
|
||||
variable "custom_names" {
|
||||
default = {}
|
||||
description = "A map of custom display names for region IDs."
|
||||
type = map(string)
|
||||
}
|
||||
|
||||
variable "custom_icons" {
|
||||
default = {}
|
||||
description = "A map of custom icons for region IDs."
|
||||
type = map(string)
|
||||
}
|
||||
|
||||
variable "exclude" {
|
||||
default = []
|
||||
description = "A list of region IDs to exclude."
|
||||
type = list(string)
|
||||
}
|
||||
|
||||
variable "coder_parameter_order" {
|
||||
type = number
|
||||
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
locals {
|
||||
# This is a static list because the zones don't change _that_
|
||||
# frequently and including the `exoscale_zones` data source requires
|
||||
# the provider, which requires a zone.
|
||||
# https://www.exoscale.com/datacenters/
|
||||
zones = {
|
||||
"de-fra-1" = {
|
||||
name = "Frankfurt - Germany"
|
||||
icon = "/emojis/1f1e9-1f1ea.png"
|
||||
}
|
||||
"at-vie-1" = {
|
||||
name = "Vienna 1 - Austria"
|
||||
icon = "/emojis/1f1e6-1f1f9.png"
|
||||
}
|
||||
"at-vie-2" = {
|
||||
name = "Vienna 2 - Austria"
|
||||
icon = "/emojis/1f1e6-1f1f9.png"
|
||||
}
|
||||
"ch-gva-2" = {
|
||||
name = "Geneva - Switzerland"
|
||||
icon = "/emojis/1f1e8-1f1ed.png"
|
||||
}
|
||||
"ch-dk-2" = {
|
||||
name = "Zurich - Switzerland"
|
||||
icon = "/emojis/1f1e8-1f1ed.png"
|
||||
}
|
||||
"bg-sof-1" = {
|
||||
name = "Sofia - Bulgaria"
|
||||
icon = "/emojis/1f1e7-1f1ec.png"
|
||||
}
|
||||
"de-muc-1" = {
|
||||
name = "Munich - Germany"
|
||||
icon = "/emojis/1f1e9-1f1ea.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "zone" {
|
||||
name = "exoscale_zone"
|
||||
display_name = var.display_name
|
||||
description = var.description
|
||||
default = var.default == "" ? null : var.default
|
||||
order = var.coder_parameter_order
|
||||
mutable = var.mutable
|
||||
dynamic "option" {
|
||||
for_each = { for k, v in local.zones : k => v if !(contains(var.exclude, k)) }
|
||||
content {
|
||||
name = try(var.custom_names[option.key], option.value.name)
|
||||
icon = try(var.custom_icons[option.key], option.value.icon)
|
||||
value = option.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "value" {
|
||||
value = data.coder_parameter.zone.value
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
---
|
||||
display_name: Git commit signing
|
||||
description: Configures Git to sign commits using your Coder SSH key
|
||||
icon: ../.icons/git.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, git]
|
||||
---
|
||||
|
||||
# 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.
|
||||
It requires `curl` and `jq` to be installed inside your workspace.
|
||||
|
||||
Please observe that using the SSH key that's part of your Coder account for commit signing, means that in the event of a breach of your Coder account, or a malicious admin, someone could perform commit signing pretending to be you.
|
||||
|
||||
This module has a chance of conflicting with the user's dotfiles / the personalize module if one of those has configuration directives that overwrite this module's / each other's git configuration.
|
||||
|
||||
```tf
|
||||
module "git-commit-signing" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-commit-signing/coder"
|
||||
version = "1.0.11"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
@ -1,25 +0,0 @@
|
||||
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."
|
||||
}
|
||||
|
||||
resource "coder_script" "git-commit-signing" {
|
||||
display_name = "Git commit signing"
|
||||
icon = "/icon/git.svg"
|
||||
|
||||
script = file("${path.module}/run.sh")
|
||||
run_on_start = true
|
||||
|
||||
agent_id = var.agent_id
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
if ! command -v git > /dev/null; then
|
||||
echo "git is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v curl > /dev/null; then
|
||||
echo "curl is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq > /dev/null; then
|
||||
echo "jq is not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p ~/.ssh/git-commit-signing
|
||||
|
||||
echo "Downloading SSH key"
|
||||
|
||||
ssh_key=$(curl --request GET \
|
||||
--url "${CODER_AGENT_URL}api/v2/workspaceagents/me/gitsshkey" \
|
||||
--header "Coder-Session-Token: ${CODER_AGENT_TOKEN}" \
|
||||
--silent --show-error)
|
||||
|
||||
jq --raw-output ".public_key" > ~/.ssh/git-commit-signing/coder.pub << EOF
|
||||
$ssh_key
|
||||
EOF
|
||||
|
||||
jq --raw-output ".private_key" > ~/.ssh/git-commit-signing/coder << EOF
|
||||
$ssh_key
|
||||
EOF
|
||||
|
||||
chmod -R 600 ~/.ssh/git-commit-signing/coder
|
||||
chmod -R 644 ~/.ssh/git-commit-signing/coder.pub
|
||||
|
||||
echo "Configuring git to use the SSH key"
|
||||
|
||||
git config --global gpg.format ssh
|
||||
git config --global commit.gpgsign true
|
||||
git config --global user.signingkey ~/.ssh/git-commit-signing/coder
|
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
printf "$${BOLD}Checking git-config!\n"
|
||||
|
||||
# Check if git is installed
|
||||
command -v git >/dev/null 2>&1 || {
|
||||
echo "Git is not installed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Set git username and email if missing
|
||||
if [ -z $(git config --get user.email) ]; then
|
||||
printf "git-config: No user.email found, setting to ${GIT_EMAIL}\n"
|
||||
git config --global user.email "${GIT_EMAIL}"
|
||||
fi
|
||||
|
||||
if [ -z $(git config --get user.name) ]; then
|
||||
printf "git-config: No user.name found, setting to ${GIT_USERNAME}\n"
|
||||
git config --global user.name "${GIT_USERNAME}"
|
||||
fi
|
||||
|
||||
printf "\n$${BOLD}git-config: using email: $(git config --get user.email)\n"
|
||||
printf "$${BOLD}git-config: using username: $(git config --get user.name)\n\n"
|
@ -1,55 +0,0 @@
|
||||
---
|
||||
display_name: Github Upload Public Key
|
||||
description: Automates uploading Coder public key to Github so users don't have to.
|
||||
icon: ../.icons/github.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, git]
|
||||
---
|
||||
|
||||
# github-upload-public-key
|
||||
|
||||
Templates that utilize Github External Auth can automatically ensure that the Coder public key is uploaded to Github so that users can clone repositories without needing to upload the public key themselves.
|
||||
|
||||
```tf
|
||||
module "github-upload-public-key" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
# Requirements
|
||||
|
||||
This module requires `curl` and `jq` to be installed inside your workspace.
|
||||
|
||||
Github External Auth must be enabled in the workspace for this module to work. The Github app that is configured for external auth must have both read and write permissions to "Git SSH keys" in order to upload the public key. Additionally, a Coder admin must also have the `admin:public_key` scope added to the external auth configuration of the Coder deployment. For example:
|
||||
|
||||
```
|
||||
CODER_EXTERNAL_AUTH_0_ID="USER_DEFINED_ID"
|
||||
CODER_EXTERNAL_AUTH_0_TYPE=github
|
||||
CODER_EXTERNAL_AUTH_0_CLIENT_ID=xxxxxx
|
||||
CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=xxxxxxx
|
||||
CODER_EXTERNAL_AUTH_0_SCOPES="repo,workflow,admin:public_key"
|
||||
```
|
||||
|
||||
Note that the default scopes if not provided are `repo,workflow`. If the module is failing to complete after updating the external auth configuration, instruct users of the module to "Unlink" and "Link" their Github account in the External Auth user settings page to get the new scopes.
|
||||
|
||||
# Example
|
||||
|
||||
Using a coder github external auth with a non-default id: (default is `github`)
|
||||
|
||||
```tf
|
||||
data "coder_external_auth" "github" {
|
||||
id = "myauthid"
|
||||
}
|
||||
|
||||
module "github-upload-public-key" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/github-upload-public-key/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
external_auth_id = data.coder_external_auth.github.id
|
||||
}
|
||||
```
|
@ -1,132 +0,0 @@
|
||||
import { type Server, serve } from "bun";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
createJSONResponse,
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
writeCoder,
|
||||
} from "../test";
|
||||
|
||||
describe("github-upload-public-key", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("creates new key if one does not exist", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
await writeCoder(id, "echo foo");
|
||||
|
||||
const url = server.url.toString().slice(0, -1);
|
||||
const exec = await execContainer(id, [
|
||||
"env",
|
||||
`CODER_ACCESS_URL=${url}`,
|
||||
`GITHUB_API_URL=${url}`,
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key has been added to GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
// we need to increase timeout to pull the container
|
||||
}, 15000);
|
||||
|
||||
it("does nothing if one already exists", async () => {
|
||||
const { instance, id, server } = await setupContainer();
|
||||
// use keyword to make server return a existing key
|
||||
await writeCoder(id, "echo findkey");
|
||||
|
||||
const url = server.url.toString().slice(0, -1);
|
||||
const exec = await execContainer(id, [
|
||||
"env",
|
||||
`CODER_ACCESS_URL=${url}`,
|
||||
`GITHUB_API_URL=${url}`,
|
||||
"CODER_OWNER_SESSION_TOKEN=foo",
|
||||
"CODER_EXTERNAL_AUTH_ID=github",
|
||||
"bash",
|
||||
"-c",
|
||||
instance.script,
|
||||
]);
|
||||
expect(exec.stdout).toContain(
|
||||
"Your Coder public key is already on GitHub!",
|
||||
);
|
||||
expect(exec.exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const setupContainer = async (
|
||||
image = "lorello/alpine-bash",
|
||||
vars: Record<string, string> = {},
|
||||
) => {
|
||||
const server = await setupServer();
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
return { id, instance, server };
|
||||
};
|
||||
|
||||
const setupServer = async (): Promise<Server> => {
|
||||
let url: URL;
|
||||
const fakeSlackHost = serve({
|
||||
fetch: (req) => {
|
||||
url = new URL(req.url);
|
||||
if (url.pathname === "/api/v2/users/me/gitsshkey") {
|
||||
return createJSONResponse({
|
||||
public_key: "exists",
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname === "/user/keys") {
|
||||
if (req.method === "POST") {
|
||||
return createJSONResponse(
|
||||
{
|
||||
key: "created",
|
||||
},
|
||||
201,
|
||||
);
|
||||
}
|
||||
|
||||
// case: key already exists
|
||||
if (req.headers.get("Authorization") === "Bearer findkey") {
|
||||
return createJSONResponse([
|
||||
{
|
||||
key: "foo",
|
||||
},
|
||||
{
|
||||
key: "exists",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
// case: key does not exist
|
||||
return createJSONResponse([
|
||||
{
|
||||
key: "foo",
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return createJSONResponse(
|
||||
{
|
||||
error: "not_found",
|
||||
},
|
||||
404,
|
||||
);
|
||||
},
|
||||
port: 0,
|
||||
});
|
||||
|
||||
return fakeSlackHost;
|
||||
};
|
@ -1,43 +0,0 @@
|
||||
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 "external_auth_id" {
|
||||
type = string
|
||||
description = "The ID of the GitHub external auth."
|
||||
default = "github"
|
||||
}
|
||||
|
||||
variable "github_api_url" {
|
||||
type = string
|
||||
description = "The URL of the GitHub instance."
|
||||
default = "https://api.github.com"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_script" "github_upload_public_key" {
|
||||
agent_id = var.agent_id
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
CODER_OWNER_SESSION_TOKEN : data.coder_workspace_owner.me.session_token,
|
||||
CODER_ACCESS_URL : data.coder_workspace.me.access_url,
|
||||
CODER_EXTERNAL_AUTH_ID : var.external_auth_id,
|
||||
GITHUB_API_URL : var.github_api_url,
|
||||
})
|
||||
display_name = "Github Upload Public Key"
|
||||
icon = "/icon/github.svg"
|
||||
run_on_start = true
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if [ -z "$CODER_ACCESS_URL" ]; then
|
||||
if [ -z "${CODER_ACCESS_URL}" ]; then
|
||||
echo "CODER_ACCESS_URL is empty!"
|
||||
exit 1
|
||||
fi
|
||||
CODER_ACCESS_URL=${CODER_ACCESS_URL}
|
||||
fi
|
||||
|
||||
if [ -z "$CODER_OWNER_SESSION_TOKEN" ]; then
|
||||
if [ -z "${CODER_OWNER_SESSION_TOKEN}" ]; then
|
||||
echo "CODER_OWNER_SESSION_TOKEN is empty!"
|
||||
exit 1
|
||||
fi
|
||||
CODER_OWNER_SESSION_TOKEN=${CODER_OWNER_SESSION_TOKEN}
|
||||
fi
|
||||
|
||||
if [ -z "$CODER_EXTERNAL_AUTH_ID" ]; then
|
||||
if [ -z "${CODER_EXTERNAL_AUTH_ID}" ]; then
|
||||
echo "CODER_EXTERNAL_AUTH_ID is empty!"
|
||||
exit 1
|
||||
fi
|
||||
CODER_EXTERNAL_AUTH_ID=${CODER_EXTERNAL_AUTH_ID}
|
||||
fi
|
||||
|
||||
if [ -z "$GITHUB_API_URL" ]; then
|
||||
if [ -z "${GITHUB_API_URL}" ]; then
|
||||
echo "GITHUB_API_URL is empty!"
|
||||
exit 1
|
||||
fi
|
||||
GITHUB_API_URL=${GITHUB_API_URL}
|
||||
fi
|
||||
|
||||
echo "Fetching GitHub token..."
|
||||
GITHUB_TOKEN=$(coder external-auth access-token $CODER_EXTERNAL_AUTH_ID)
|
||||
if [ $? -ne 0 ]; then
|
||||
printf "Authenticate with Github to automatically upload Coder public key:\n$GITHUB_TOKEN\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fetching public key from Coder..."
|
||||
PUBLIC_KEY_RESPONSE=$(
|
||||
curl -L -s \
|
||||
-w "\n%%{http_code}" \
|
||||
-H 'accept: application/json' \
|
||||
-H "cookie: coder_session_token=$CODER_OWNER_SESSION_TOKEN" \
|
||||
"$CODER_ACCESS_URL/api/v2/users/me/gitsshkey"
|
||||
)
|
||||
PUBLIC_KEY_RESPONSE_STATUS=$(tail -n1 <<< "$PUBLIC_KEY_RESPONSE")
|
||||
PUBLIC_KEY_BODY=$(sed \$d <<< "$PUBLIC_KEY_RESPONSE")
|
||||
|
||||
if [ "$PUBLIC_KEY_RESPONSE_STATUS" -ne 200 ]; then
|
||||
echo "Failed to fetch Coder public SSH key with status code $PUBLIC_KEY_RESPONSE_STATUS!"
|
||||
echo "$PUBLIC_KEY_BODY"
|
||||
exit 1
|
||||
fi
|
||||
PUBLIC_KEY=$(jq -r '.public_key' <<< "$PUBLIC_KEY_BODY")
|
||||
if [ -z "$PUBLIC_KEY" ]; then
|
||||
echo "No Coder public SSH key found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Fetching public keys from GitHub..."
|
||||
GITHUB_KEYS_RESPONSE=$(
|
||||
curl -L -s \
|
||||
-w "\n%%{http_code}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
$GITHUB_API_URL/user/keys
|
||||
)
|
||||
GITHUB_KEYS_RESPONSE_STATUS=$(tail -n1 <<< "$GITHUB_KEYS_RESPONSE")
|
||||
GITHUB_KEYS_RESPONSE_BODY=$(sed \$d <<< "$GITHUB_KEYS_RESPONSE")
|
||||
|
||||
if [ "$GITHUB_KEYS_RESPONSE_STATUS" -ne 200 ]; then
|
||||
echo "Failed to fetch Coder public SSH key with status code $GITHUB_KEYS_RESPONSE_STATUS!"
|
||||
echo "$GITHUB_KEYS_RESPONSE_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GITHUB_MATCH=$(jq -r --arg PUBLIC_KEY "$PUBLIC_KEY" '.[] | select(.key == $PUBLIC_KEY) | .key' <<< "$GITHUB_KEYS_RESPONSE_BODY")
|
||||
|
||||
if [ "$PUBLIC_KEY" = "$GITHUB_MATCH" ]; then
|
||||
echo "Your Coder public key is already on GitHub!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Your Coder public key is not in GitHub. Adding it now..."
|
||||
CODER_PUBLIC_KEY_NAME="$CODER_ACCESS_URL Workspaces"
|
||||
UPLOAD_RESPONSE=$(
|
||||
curl -L -s \
|
||||
-X POST \
|
||||
-w "\n%%{http_code}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
$GITHUB_API_URL/user/keys \
|
||||
-d "{\"title\":\"$CODER_PUBLIC_KEY_NAME\",\"key\":\"$PUBLIC_KEY\"}"
|
||||
)
|
||||
UPLOAD_RESPONSE_STATUS=$(tail -n1 <<< "$UPLOAD_RESPONSE")
|
||||
UPLOAD_RESPONSE_BODY=$(sed \$d <<< "$UPLOAD_RESPONSE")
|
||||
|
||||
if [ "$UPLOAD_RESPONSE_STATUS" -ne 201 ]; then
|
||||
echo "Failed to upload Coder public SSH key with status code $UPLOAD_RESPONSE_STATUS!"
|
||||
echo "$UPLOAD_RESPONSE_BODY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Your Coder public key has been added to GitHub!"
|
@ -1,80 +0,0 @@
|
||||
---
|
||||
display_name: "HCP Vault Secrets"
|
||||
description: "Fetch secrets from HCP Vault"
|
||||
icon: ../.icons/vault.svg
|
||||
maintainer_github: coder
|
||||
partner_github: hashicorp
|
||||
verified: true
|
||||
tags: [helper, integration, vault, hashicorp, hvs]
|
||||
---
|
||||
|
||||
# HCP Vault Secrets
|
||||
|
||||
This module lets you fetch all or selective secrets from a [HCP Vault Secrets](https://developer.hashicorp.com/hcp/docs/vault-secrets) app into your [Coder](https://coder.com) workspaces. It makes use of the [`hcp_vault_secrets_app`](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/data-sources/vault_secrets_app) data source from the [HCP provider](https://registry.terraform.io/providers/hashicorp/hcp/latest).
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
project_id = "aaa-bbb-ccc"
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
To configure the HCP Vault Secrets module, follow these steps,
|
||||
|
||||
1. [Create secrets in HCP Vault Secrets](https://developer.hashicorp.com/vault/tutorials/hcp-vault-secrets-get-started/hcp-vault-secrets-create-secret)
|
||||
2. Create an HCP Service Principal from the HCP Vault Secrets app in the HCP console. This will give you the `HCP_CLIENT_ID` and `HCP_CLIENT_SECRET` that you need to authenticate with HCP Vault Secrets.
|
||||

|
||||
3. Set `HCP_CLIENT_ID` and `HCP_CLIENT_SECRET` variables on the coder provisioner (recommended) or supply them as input to the module.
|
||||
4. Set the `project_id`. This is the ID of the project where the HCP Vault Secrets app is running.
|
||||
|
||||
> See the [HCP Vault Secrets documentation](https://developer.hashicorp.com/hcp/docs/vault-secrets) for more information.
|
||||
|
||||
## Fetch All Secrets
|
||||
|
||||
To fetch all secrets from the HCP Vault Secrets app, skip the `secrets` input.
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
project_id = "aaa-bbb-ccc"
|
||||
}
|
||||
```
|
||||
|
||||
## Fetch Selective Secrets
|
||||
|
||||
To fetch selective secrets from the HCP Vault Secrets app, set the `secrets` input.
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
project_id = "aaa-bbb-ccc"
|
||||
secrets = ["MY_SECRET_1", "MY_SECRET_2"]
|
||||
}
|
||||
```
|
||||
|
||||
## Set Client ID and Client Secret as Inputs
|
||||
|
||||
Set `client_id` and `client_secret` as module inputs.
|
||||
|
||||
```tf
|
||||
module "vault" {
|
||||
source = "registry.coder.com/modules/hcp-vault-secrets/coder"
|
||||
version = "1.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
app_name = "demo-app"
|
||||
project_id = "aaa-bbb-ccc"
|
||||
client_id = "HCP_CLIENT_ID"
|
||||
client_secret = "HCP_CLIENT_SECRET"
|
||||
}
|
||||
```
|
@ -1,73 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12.4"
|
||||
}
|
||||
hcp = {
|
||||
source = "hashicorp/hcp"
|
||||
version = ">= 0.82.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "hcp" {
|
||||
client_id = var.client_id
|
||||
client_secret = var.client_secret
|
||||
project_id = var.project_id
|
||||
}
|
||||
|
||||
provider "coder" {}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "project_id" {
|
||||
type = string
|
||||
description = "The ID of the HCP project."
|
||||
}
|
||||
|
||||
variable "client_id" {
|
||||
type = string
|
||||
description = <<-EOF
|
||||
The client ID for the HCP Vault Secrets service principal. (Optional if HCP_CLIENT_ID is set as an environment variable.)
|
||||
EOF
|
||||
default = null
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "client_secret" {
|
||||
type = string
|
||||
description = <<-EOF
|
||||
The client secret for the HCP Vault Secrets service principal. (Optional if HCP_CLIENT_SECRET is set as an environment variable.)
|
||||
EOF
|
||||
default = null
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "app_name" {
|
||||
type = string
|
||||
description = "The name of the secrets app in HCP Vault Secrets"
|
||||
}
|
||||
|
||||
variable "secrets" {
|
||||
type = list(string)
|
||||
description = "The names of the secrets to retrieve from HCP Vault Secrets"
|
||||
default = null
|
||||
}
|
||||
|
||||
data "hcp_vault_secrets_app" "secrets" {
|
||||
app_name = var.app_name
|
||||
}
|
||||
|
||||
resource "coder_env" "hvs_secrets" {
|
||||
# https://support.hashicorp.com/hc/en-us/articles/4538432032787-Variable-has-a-sensitive-value-and-cannot-be-used-as-for-each-arguments
|
||||
for_each = var.secrets != null ? toset(var.secrets) : nonsensitive(toset(keys(data.hcp_vault_secrets_app.secrets.secrets)))
|
||||
agent_id = var.agent_id
|
||||
name = each.key
|
||||
value = data.hcp_vault_secrets_app.secrets.secrets[each.key]
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
email=${ARTIFACTORY_EMAIL}
|
||||
%{ for REPO in REPOS ~}
|
||||
${REPO.SCOPE}registry=${JFROG_URL}/artifactory/api/npm/${REPO.NAME}
|
||||
//${JFROG_HOST}/artifactory/api/npm/${REPO.NAME}/:_authToken=${ARTIFACTORY_ACCESS_TOKEN}
|
||||
%{ endfor ~}
|
@ -1,107 +0,0 @@
|
||||
---
|
||||
display_name: JFrog (OAuth)
|
||||
description: Install the JF CLI and authenticate with Artifactory using OAuth.
|
||||
icon: ../.icons/jfrog.svg
|
||||
maintainer_github: coder
|
||||
partner_github: jfrog
|
||||
verified: true
|
||||
tags: [integration, jfrog]
|
||||
---
|
||||
|
||||
# JFrog
|
||||
|
||||
Install the JF CLI and authenticate package managers with Artifactory using OAuth configured via the Coder [`external-auth`](https://coder.com/docs/v2/latest/admin/external-auth) feature.
|
||||
|
||||

|
||||
|
||||
```tf
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
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"
|
||||
|
||||
package_managers = {
|
||||
npm = ["npm", "@scoped:npm-scoped"]
|
||||
go = ["go", "another-go-repo"]
|
||||
pypi = ["pypi", "extra-index-pypi"]
|
||||
docker = ["example-docker-staging.jfrog.io", "example-docker-production.jfrog.io"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Note
|
||||
> This module does not install `npm`, `go`, `pip`, etc but only configure them. You need to handle the installation of these tools yourself.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This module is usable by JFrog self-hosted (on-premises) Artifactory as it requires configuring a custom integration. This integration benefits from Coder's [external-auth](https://coder.com/docs/v2/latest/admin/external-auth) feature and allows each user to authenticate with Artifactory using an OAuth flow and issues user-scoped tokens to each user. For configuration instructions, see this [guide](https://coder.com/docs/v2/latest/guides/artifactory-integration#jfrog-oauth) on the Coder documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
Configure the Python pip package manager to fetch packages from Artifactory while mapping the Coder email to the Artifactory username.
|
||||
|
||||
```tf
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "email"
|
||||
|
||||
package_managers = {
|
||||
pypi = ["pypi"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You should now be able to install packages from Artifactory using both the `jf pip` and `pip` command.
|
||||
|
||||
```shell
|
||||
jf pip install requests
|
||||
```
|
||||
|
||||
```shell
|
||||
pip install requests
|
||||
```
|
||||
|
||||
### Configure code-server with JFrog extension
|
||||
|
||||
The [JFrog extension](https://open-vsx.org/extension/JFrog/jfrog-vscode-extension) for VS Code allows you to interact with Artifactory from within the IDE.
|
||||
|
||||
```tf
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/jfrog-oauth/coder"
|
||||
version = "1.0.19"
|
||||
agent_id = coder_agent.example.id
|
||||
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"
|
||||
configure_code_server = true # Add JFrog extension configuration for code-server
|
||||
package_managers = {
|
||||
npm = ["npm"]
|
||||
go = ["go"]
|
||||
pypi = ["pypi"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using the access token in other terraform resources
|
||||
|
||||
JFrog Access token is also available as a terraform output. You can use it in other terraform resources. For example, you can use it to configure an [Artifactory docker registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/docker-registry) with the [docker terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs).
|
||||
|
||||
```tf
|
||||
provider "docker" {
|
||||
# ...
|
||||
registry_auth {
|
||||
address = "https://example.jfrog.io/artifactory/api/docker/REPO-KEY"
|
||||
username = try(module.jfrog[0].username, "")
|
||||
password = try(module.jfrog[0].access_token, "")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Here `REPO_KEY` is the name of docker repository in Artifactory.
|
@ -1,129 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformInit,
|
||||
runTerraformApply,
|
||||
testRequiredVariables,
|
||||
} from "../test";
|
||||
|
||||
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);
|
||||
|
||||
const fakeFrogApi = "localhost:8081/artifactory/api";
|
||||
const fakeFrogUrl = "http://localhost:8081";
|
||||
const user = "default";
|
||||
|
||||
it("can run apply with required variables", async () => {
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
@ -1,173 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.23"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "jfrog_url" {
|
||||
type = string
|
||||
description = "JFrog instance URL. e.g. https://myartifactory.jfrog.io"
|
||||
# ensue the URL is HTTPS or HTTP
|
||||
validation {
|
||||
condition = can(regex("^(https|http)://", var.jfrog_url))
|
||||
error_message = "jfrog_url must be a valid URL starting with either 'https://' or 'http://'"
|
||||
}
|
||||
}
|
||||
|
||||
variable "jfrog_server_id" {
|
||||
type = string
|
||||
description = "The server ID of the JFrog instance for JFrog CLI configuration"
|
||||
default = "0"
|
||||
}
|
||||
|
||||
variable "username_field" {
|
||||
type = string
|
||||
description = "The field to use for the artifactory username. i.e. Coder username or email."
|
||||
default = "username"
|
||||
validation {
|
||||
condition = can(regex("^(email|username)$", var.username_field))
|
||||
error_message = "username_field must be either 'email' or 'username'"
|
||||
}
|
||||
}
|
||||
|
||||
variable "external_auth_id" {
|
||||
type = string
|
||||
description = "JFrog external auth ID. Default: 'jfrog'"
|
||||
default = "jfrog"
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "configure_code_server" {
|
||||
type = bool
|
||||
description = "Set to true to configure code-server to use JFrog."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "package_managers" {
|
||||
type = object({
|
||||
npm = optional(list(string), [])
|
||||
go = optional(list(string), [])
|
||||
pypi = optional(list(string), [])
|
||||
docker = optional(list(string), [])
|
||||
})
|
||||
description = <<-EOF
|
||||
A map of package manager names to their respective artifactory repositories. Unused package managers can be omitted.
|
||||
For example:
|
||||
{
|
||||
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 {
|
||||
# The username field to use for artifactory
|
||||
username = var.username_field == "email" ? data.coder_workspace_owner.me.email : data.coder_workspace_owner.me.name
|
||||
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_owner" "me" {}
|
||||
|
||||
data "coder_external_auth" "jfrog" {
|
||||
id = var.external_auth_id
|
||||
}
|
||||
|
||||
resource "coder_script" "jfrog" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "jfrog"
|
||||
icon = "/icon/jfrog.svg"
|
||||
script = templatefile("${path.module}/run.sh", merge(
|
||||
local.common_values,
|
||||
{
|
||||
CONFIGURE_CODE_SERVER = var.configure_code_server
|
||||
HAS_NPM = length(var.package_managers.npm) == 0 ? "" : "YES"
|
||||
NPMRC = local.npmrc
|
||||
REPOSITORY_NPM = try(element(var.package_managers.npm, 0), "")
|
||||
HAS_GO = length(var.package_managers.go) == 0 ? "" : "YES"
|
||||
REPOSITORY_GO = try(element(var.package_managers.go, 0), "")
|
||||
HAS_PYPI = length(var.package_managers.pypi) == 0 ? "" : "YES"
|
||||
PIP_CONF = local.pip_conf
|
||||
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
|
||||
}
|
||||
|
||||
resource "coder_env" "jfrog_ide_url" {
|
||||
count = var.configure_code_server ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "JFROG_IDE_URL"
|
||||
value = var.jfrog_url
|
||||
}
|
||||
|
||||
resource "coder_env" "jfrog_ide_access_token" {
|
||||
count = var.configure_code_server ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "JFROG_IDE_ACCESS_TOKEN"
|
||||
value = data.coder_external_auth.jfrog.access_token
|
||||
}
|
||||
|
||||
resource "coder_env" "jfrog_ide_store_connection" {
|
||||
count = var.configure_code_server ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "JFROG_IDE_STORE_CONNECTION"
|
||||
value = true
|
||||
}
|
||||
|
||||
resource "coder_env" "goproxy" {
|
||||
count = length(var.package_managers.go) == 0 ? 0 : 1
|
||||
agent_id = var.agent_id
|
||||
name = "GOPROXY"
|
||||
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" {
|
||||
description = "value of the JFrog access token"
|
||||
value = data.coder_external_auth.jfrog.access_token
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "username" {
|
||||
description = "value of the JFrog username"
|
||||
value = local.username
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
[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 ~}
|
@ -1,131 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
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
|
||||
if command -v jf > /dev/null 2>&1; then
|
||||
echo "✅ JFrog CLI is already installed, skipping installation."
|
||||
else
|
||||
echo "📦 Installing JFrog CLI..."
|
||||
curl -fL https://install-cli.jfrog.io | sudo sh
|
||||
sudo chmod 755 /usr/local/bin/jf
|
||||
fi
|
||||
|
||||
# The jf CLI checks $CI when determining whether to use interactive
|
||||
# flows.
|
||||
export CI=true
|
||||
# Authenticate JFrog CLI with Artifactory.
|
||||
echo "${ARTIFACTORY_ACCESS_TOKEN}" | jf c add --access-token-stdin --url "${JFROG_URL}" --overwrite "${JFROG_SERVER_ID}"
|
||||
# Set the configured server as the default.
|
||||
jf c use "${JFROG_SERVER_ID}"
|
||||
|
||||
# Configure npm to use the Artifactory "npm" repository.
|
||||
if [ -z "${HAS_NPM}" ]; then
|
||||
not_configured npm
|
||||
else
|
||||
echo "📦 Configuring npm..."
|
||||
jf npmc --global --repo-resolve "${REPOSITORY_NPM}"
|
||||
cat << EOF > ~/.npmrc
|
||||
${NPMRC}
|
||||
EOF
|
||||
config_complete
|
||||
fi
|
||||
|
||||
# Configure the `pip` to use the Artifactory "python" repository.
|
||||
if [ -z "${HAS_PYPI}" ]; then
|
||||
not_configured pypi
|
||||
else
|
||||
echo "🐍 Configuring pip..."
|
||||
jf pipc --global --repo-resolve "${REPOSITORY_PYPI}"
|
||||
mkdir -p ~/.pip
|
||||
cat << EOF > ~/.pip/pip.conf
|
||||
${PIP_CONF}
|
||||
EOF
|
||||
config_complete
|
||||
fi
|
||||
|
||||
# Configure Artifactory "go" repository.
|
||||
if [ -z "${HAS_GO}" ]; then
|
||||
not_configured go
|
||||
else
|
||||
echo "🐹 Configuring go..."
|
||||
jf goc --global --repo-resolve "${REPOSITORY_GO}"
|
||||
config_complete
|
||||
fi
|
||||
|
||||
# Configure the JFrog CLI to use the Artifactory "docker" repository.
|
||||
if [ -z "${HAS_DOCKER}" ]; then
|
||||
not_configured docker
|
||||
else
|
||||
if command -v docker > /dev/null 2>&1; then
|
||||
echo "🔑 Configuring 🐳 docker credentials..."
|
||||
mkdir -p ~/.docker
|
||||
${REGISTER_DOCKER}
|
||||
else
|
||||
echo "🤔 no docker is installed, skipping docker configuration."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install the JFrog vscode extension for code-server.
|
||||
if [ "${CONFIGURE_CODE_SERVER}" == "true" ]; then
|
||||
while ! [ -x /tmp/code-server/bin/code-server ]; do
|
||||
counter=0
|
||||
if [ $counter -eq 60 ]; then
|
||||
echo "Timed out waiting for /tmp/code-server/bin/code-server to be installed."
|
||||
exit 1
|
||||
fi
|
||||
echo "Waiting for /tmp/code-server/bin/code-server to be installed..."
|
||||
sleep 1
|
||||
((counter++))
|
||||
done
|
||||
echo "📦 Installing JFrog extension..."
|
||||
/tmp/code-server/bin/code-server --install-extension jfrog.jfrog-vscode-extension
|
||||
echo "🥳 JFrog extension installed!"
|
||||
else
|
||||
echo "🤔 Skipping JFrog extension installation. Set configure_code_server to true to install the JFrog extension."
|
||||
fi
|
||||
|
||||
# Configure the JFrog CLI completion
|
||||
echo "📦 Configuring JFrog CLI completion..."
|
||||
# Get the user's shell
|
||||
SHELLNAME=$(grep "^$USER" /etc/passwd | awk -F':' '{print $7}' | awk -F'/' '{print $NF}')
|
||||
# Generate the completion script
|
||||
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
|
||||
if [ "$SHELLNAME" == "bash" ] && [ -f ~/.bashrc ]; then
|
||||
if ! grep -q "$begin_stanza" ~/.bashrc; then
|
||||
printf "%s\n" "$begin_stanza" >> ~/.bashrc
|
||||
echo 'source "$HOME/.jfrog/jfrog_bash_completion"' >> ~/.bashrc
|
||||
echo "# END: jf CLI shell completion" >> ~/.bashrc
|
||||
else
|
||||
echo "🥳 ~/.bashrc already contains jf CLI shell completion configuration, skipping."
|
||||
fi
|
||||
elif [ "$SHELLNAME" == "zsh" ] && [ -f ~/.zshrc ]; then
|
||||
if ! grep -q "$begin_stanza" ~/.zshrc; then
|
||||
printf "\n%s\n" "$begin_stanza" >> ~/.zshrc
|
||||
echo "autoload -Uz compinit" >> ~/.zshrc
|
||||
echo "compinit" >> ~/.zshrc
|
||||
echo 'source "$HOME/.jfrog/jfrog_zsh_completion"' >> ~/.zshrc
|
||||
echo "# END: jf CLI shell completion" >> ~/.zshrc
|
||||
else
|
||||
echo "🥳 ~/.zshrc already contains jf CLI shell completion configuration, skipping."
|
||||
fi
|
||||
else
|
||||
echo "🤔 ~/.bashrc or ~/.zshrc does not exist, skipping jf CLI shell completion configuration."
|
||||
fi
|
@ -1,5 +0,0 @@
|
||||
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 ~}
|