Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6b9800f8e | |||
| 9a320adf91 | |||
| da17c9881a | |||
| 9029bd1555 | |||
| 82fb00a4e1 | |||
| 28fc956110 | |||
| 2701dc09af | |||
| 60611ed593 | |||
| 7df0cb25c5 | |||
| cbb39bda6f | |||
| 99bd4a4139 | |||
| c819ca7f83 | |||
| accf5a34ab | |||
| bb222f36a5 | |||
| 3677e93e36 | |||
| a3ba616aec | |||
| 24dc52fb17 | |||
| 678c3e631e | |||
| 36089612ef | |||
| ac44ad862a | |||
| ef5a903edf | |||
| 4b7128b17e | |||
| 77a3e74e0b | |||
| 311de23454 | |||
| f66f61d724 | |||
| 631bf027c6 | |||
| eb4c28fc61 | |||
| c551c4d84a | |||
| 4b9da4036a | |||
| 96c5f3219d | |||
| 146540c1e9 | |||
| a85436fdf4 | |||
| aa4890fe62 | |||
| ab6799ac07 | |||
| bda3eb96e8 | |||
| 6b16cd3529 | |||
| 43d05a9da4 | |||
| e3f8b6450e | |||
| c03986f9cb | |||
| 758aba4c2a |
@@ -1,14 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Version Bump Script
|
||||
# Usage: ./version-bump.sh <bump_type> [base_ref]
|
||||
# Usage: ./version-bump.sh [--ci] <bump_type> [base_ref]
|
||||
# --ci: CI mode - run bump, check for changes, exit 1 if changes needed
|
||||
# bump_type: patch, minor, or major
|
||||
# base_ref: base reference for diff (default: origin/main)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CI_MODE=false
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <bump_type> [base_ref]"
|
||||
echo "Usage: $0 [--ci] <bump_type> [base_ref]"
|
||||
echo " --ci: CI mode - validates versions are already bumped (exits 1 if not)"
|
||||
echo " bump_type: patch, minor, or major"
|
||||
echo " base_ref: base reference for diff (default: origin/main)"
|
||||
echo ""
|
||||
@@ -16,6 +20,7 @@ usage() {
|
||||
echo " $0 patch # Update versions with patch bump"
|
||||
echo " $0 minor # Update versions with minor bump"
|
||||
echo " $0 major # Update versions with major bump"
|
||||
echo " $0 --ci patch # CI check: verify patch bump has been applied"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -85,7 +90,7 @@ update_readme_version() {
|
||||
in_module_block = 0
|
||||
if (module_has_target_source) {
|
||||
num_lines = split(module_content, lines, "\n")
|
||||
for (i = 1; i <= num_lines; i++) {
|
||||
for (i = 1; i < num_lines; i++) {
|
||||
line = lines[i]
|
||||
if (line ~ /^[[:space:]]*version[[:space:]]*=/) {
|
||||
match(line, /^[[:space:]]*/)
|
||||
@@ -115,6 +120,11 @@ update_readme_version() {
|
||||
}
|
||||
|
||||
main() {
|
||||
if [ "${1:-}" = "--ci" ]; then
|
||||
CI_MODE=true
|
||||
shift
|
||||
fi
|
||||
|
||||
if [ $# -lt 1 ] || [ $# -gt 2 ]; then
|
||||
usage
|
||||
fi
|
||||
@@ -152,6 +162,8 @@ main() {
|
||||
local untagged_modules=""
|
||||
local has_changes=false
|
||||
|
||||
declare -a modified_readme_files=()
|
||||
|
||||
while IFS= read -r module_path; do
|
||||
if [ -z "$module_path" ]; then continue; fi
|
||||
|
||||
@@ -202,6 +214,7 @@ main() {
|
||||
|
||||
if update_readme_version "$readme_path" "$namespace" "$module_name" "$new_version"; then
|
||||
updated_readmes="$updated_readmes\n- $namespace/$module_name"
|
||||
modified_readme_files+=("$readme_path")
|
||||
has_changes=true
|
||||
fi
|
||||
|
||||
@@ -210,19 +223,22 @@ main() {
|
||||
|
||||
done <<< "$modules"
|
||||
|
||||
# Always run formatter to ensure consistent formatting
|
||||
echo "🔧 Running formatter to ensure consistent formatting..."
|
||||
if command -v bun > /dev/null 2>&1; then
|
||||
bun fmt > /dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..."
|
||||
else
|
||||
echo "⚠️ Warning: bun not found, skipping formatting"
|
||||
if [ ${#modified_readme_files[@]} -gt 0 ]; then
|
||||
echo "🔧 Formatting modified README files..."
|
||||
if command -v bun > /dev/null 2>&1; then
|
||||
for readme_file in "${modified_readme_files[@]}"; do
|
||||
bun run prettier --write "$readme_file" 2> /dev/null || true
|
||||
done
|
||||
else
|
||||
echo "⚠️ Warning: bun not found, skipping formatting"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "📋 Summary:"
|
||||
echo "Bump Type: $bump_type"
|
||||
echo ""
|
||||
echo "Modules Updated:"
|
||||
echo "Modules Processed:"
|
||||
echo -e "$bumped_modules"
|
||||
echo ""
|
||||
|
||||
@@ -239,6 +255,19 @@ main() {
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ "$CI_MODE" = true ]; then
|
||||
echo "🔍 Comparing files to committed versions..."
|
||||
if git diff --quiet; then
|
||||
echo "✅ PASS: All versions match - no changes needed"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ FAIL: Module versions need to be updated"
|
||||
echo ""
|
||||
echo "Run './.github/scripts/version-bump.sh $bump_type' locally and commit the changes"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$has_changes" = true ]; then
|
||||
echo "✅ Version bump completed successfully!"
|
||||
echo "📝 README files have been updated with new versions."
|
||||
|
||||
@@ -3,6 +3,7 @@ muc = "muc" # For Munich location code
|
||||
tyo = "tyo" # For Tokyo location code
|
||||
Hashi = "Hashi"
|
||||
HashiCorp = "HashiCorp"
|
||||
hel = "hel" # For Helsinki location code
|
||||
mavrickrishi = "mavrickrishi" # Username
|
||||
mavrick = "mavrick" # Username
|
||||
inh = "inh" # Option in setpriv command
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
uses: crate-ci/typos@v1.41.0
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
@@ -55,62 +55,35 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Check version bump requirements
|
||||
id: version-check
|
||||
run: |
|
||||
output_file=$(mktemp)
|
||||
if ./.github/scripts/version-bump.sh "${{ steps.bump-type.outputs.type }}" origin/main > "$output_file" 2>&1; then
|
||||
echo "Script completed successfully"
|
||||
else
|
||||
echo "Script failed"
|
||||
cat "$output_file"
|
||||
exit 1
|
||||
fi
|
||||
- name: Check version bump
|
||||
run: ./.github/scripts/version-bump.sh --ci "${{ steps.bump-type.outputs.type }}" origin/main
|
||||
|
||||
{
|
||||
echo "output<<EOF"
|
||||
cat "$output_file"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
cat "$output_file"
|
||||
|
||||
if git diff --quiet; then
|
||||
echo "versions_up_to_date=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ All module versions are already up to date"
|
||||
else
|
||||
echo "versions_up_to_date=false" >> $GITHUB_OUTPUT
|
||||
echo "❌ Module versions need to be updated"
|
||||
echo "Files that would be changed:"
|
||||
git diff --name-only
|
||||
echo ""
|
||||
echo "Diff preview:"
|
||||
git diff
|
||||
|
||||
git checkout .
|
||||
git clean -fd
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Comment on PR - Failure
|
||||
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
|
||||
- name: Comment on PR - Version bump required
|
||||
if: failure()
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const output = `${{ steps.version-check.outputs.output }}`;
|
||||
const bumpType = `${{ steps.bump-type.outputs.type }}`;
|
||||
|
||||
let comment = `## ❌ Version Bump Validation Failed\n\n`;
|
||||
comment += `**Bump Type:** \`${bumpType}\`\n\n`;
|
||||
comment += `Module versions need to be updated but haven't been bumped yet.\n\n`;
|
||||
comment += `**Required Actions:**\n`;
|
||||
comment += `1. Run the version bump script locally: \`./.github/scripts/version-bump.sh ${bumpType}\`\n`;
|
||||
comment += `2. Commit the changes: \`git add . && git commit -m "chore: bump module versions (${bumpType})"\`\n`;
|
||||
comment += `3. Push the changes: \`git push\`\n\n`;
|
||||
comment += `### Script Output:\n\`\`\`\n${output}\n\`\`\`\n\n`;
|
||||
comment += `> Please update the module versions and push the changes to continue.`;
|
||||
const comment = [
|
||||
'## Version Bump Required',
|
||||
'',
|
||||
'One or more modules in this PR need their versions updated.',
|
||||
'',
|
||||
'**To fix this:**',
|
||||
'1. Run the version bump script locally:',
|
||||
' ```bash',
|
||||
` ./.github/scripts/version-bump.sh ${bumpType}`,
|
||||
' ```',
|
||||
'2. Commit the changes:',
|
||||
' ```bash',
|
||||
` git add . && git commit -m "chore: bump module versions (${bumpType})"`,
|
||||
' ```',
|
||||
'3. Push your changes',
|
||||
'',
|
||||
'The CI will automatically re-run once you push the updated versions.'
|
||||
].join('\n');
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="128" cy="128" r="120" fill="black"/>
|
||||
<polygon
|
||||
points="128,70 178,170 78,170"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 216 B |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><title>Scaleway icon</title><path d="M16.61 11.11v5.72a1.77 1.77 0 0 1-1.54 1.69h-4a1.43 1.43 0 0 1-1.31-1.22 1.09 1.09 0 0 1 0-.18 1.37 1.37 0 0 1 1.37-1.36h1.74a1 1 0 0 0 1-1v-3.62a1.4 1.4 0 0 1 1.18-1.39h.17a1.37 1.37 0 0 1 1.39 1.36zm-6.46 1.74V9.26a1 1 0 0 1 1-1H13a1.37 1.37 0 0 0 1.37-1.37 1 1 0 0 0 0-.17 1.45 1.45 0 0 0-1.41-1.2H9a1.81 1.81 0 0 0-1.58 1.66v5.7a1.37 1.37 0 0 0 1.37 1.37H9a1.4 1.4 0 0 0 1.15-1.4zm12-4.29V20A4.53 4.53 0 0 1 18 24h-7.58a8.57 8.57 0 0 1-8.56-8.57V4.54A4.54 4.54 0 0 1 6.4 0h7.18a8.56 8.56 0 0 1 8.56 8.56zm-2.74 0a5.83 5.83 0 0 0-5.82-5.82H6.4a1.79 1.79 0 0 0-1.8 1.8v10.89a5.83 5.83 0 0 0 5.82 5.8h7.44a1.79 1.79 0 0 0 1.54-1.48z"/></svg>
|
||||
|
After Width: | Height: | Size: 913 B |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 451 KiB After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,7 @@
|
||||
---
|
||||
display_name: "Excellencedev"
|
||||
bio: "Love to contribute"
|
||||
avatar: "./.images/avatar.png"
|
||||
support_email: "ademiluyisuccessandexcellence@gmail.com"
|
||||
status: "community"
|
||||
---
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
display_name: Hetzner Cloud Server
|
||||
description: Provision Hetzner Cloud servers as Coder workspaces
|
||||
icon: ../../../../.icons/hetzner.svg
|
||||
tags: [vm, linux, hetzner]
|
||||
---
|
||||
|
||||
# Remote Development on Hetzner Cloud (Linux)
|
||||
|
||||
Provision Hetzner Cloud servers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
|
||||
|
||||
> [!WARNING]
|
||||
> **Workspace Storage Persistence:** When a workspace is stopped, the Hetzner Cloud server instance is stopped but your home volume and stored data persist. This means your files and data remain intact when you resume the workspace.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Volume Management & Costs:** Hetzner Cloud volumes persist even when workspaces are stopped and will continue to incur storage costs (€0.0476/GB/month). Volumes are only automatically deleted when the workspace is completely deleted. Monitor your volumes in the [Hetzner Cloud Console](https://console.hetzner.cloud/) to manage costs effectively.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To deploy workspaces as Hetzner Cloud servers, you'll need:
|
||||
|
||||
- Hetzner Cloud [API token](https://console.hetzner.cloud/projects) (create under Security > API Tokens)
|
||||
|
||||
### Authentication
|
||||
|
||||
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Hetzner Cloud.
|
||||
|
||||
Obtain a Hetzner Cloud API token from your [Hetzner Cloud Console](https://console.hetzner.cloud/projects) and provide it as the `hcloud_token` variable when creating a workspace.
|
||||
For more authentication options, see the [Terraform provider documentation](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs#authentication).
|
||||
|
||||
> [!NOTE]
|
||||
> This template is designed to be a starting point. Edit the Terraform to extend the template to support your use case.
|
||||
@@ -0,0 +1,62 @@
|
||||
#cloud-config
|
||||
users:
|
||||
- name: ${username}
|
||||
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
|
||||
groups: sudo
|
||||
shell: /bin/bash
|
||||
packages:
|
||||
- git
|
||||
%{ if home_volume_label != "" ~}
|
||||
fs_setup:
|
||||
- device: /dev/disk/by-id/scsi-0HC_Volume_${volume_id}
|
||||
filesystem: ext4
|
||||
label: ${home_volume_label}
|
||||
overwrite: false # This prevents reformatting the disk on every boot
|
||||
|
||||
mounts:
|
||||
- [
|
||||
"/dev/disk/by-id/scsi-0HC_Volume_${volume_id}",
|
||||
"/home/${username}",
|
||||
ext4,
|
||||
"defaults,uid=1000,gid=1000",
|
||||
]
|
||||
%{ endif ~}
|
||||
write_files:
|
||||
- path: /opt/coder/init
|
||||
permissions: "0755"
|
||||
encoding: b64
|
||||
content: ${init_script}
|
||||
- path: /etc/systemd/system/coder-agent.service
|
||||
permissions: "0644"
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Coder Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User=${username}
|
||||
ExecStart=/opt/coder/init
|
||||
Environment=CODER_AGENT_TOKEN=${coder_agent_token}
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
TimeoutStopSec=90
|
||||
KillMode=process
|
||||
|
||||
OOMScoreAdjust=-900
|
||||
SyslogIdentifier=coder-agent
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
runcmd:
|
||||
%{ if home_volume_label != "" ~}
|
||||
- |
|
||||
until [ -e /dev/disk/by-id/scsi-0HC_Volume_${volume_id} ]; do
|
||||
echo "Waiting for volume device..."
|
||||
sleep 2
|
||||
done
|
||||
%{ endif ~}
|
||||
- mount -a
|
||||
- chown ${username}:${username} /home/${username}
|
||||
- systemctl enable coder-agent
|
||||
- systemctl start coder-agent
|
||||
@@ -0,0 +1,224 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
hcloud = {
|
||||
source = "hetznercloud/hcloud"
|
||||
}
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
http = {
|
||||
source = "hashicorp/http"
|
||||
version = "~> 3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "hcloud_token" {
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
provider "hcloud" {
|
||||
token = var.hcloud_token
|
||||
}
|
||||
|
||||
data "http" "hcloud_locations" {
|
||||
url = "https://api.hetzner.cloud/v1/locations"
|
||||
|
||||
request_headers = {
|
||||
Authorization = "Bearer ${var.hcloud_token}"
|
||||
Accept = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
data "http" "hcloud_server_types" {
|
||||
url = "https://api.hetzner.cloud/v1/server_types"
|
||||
|
||||
request_headers = {
|
||||
Authorization = "Bearer ${var.hcloud_token}"
|
||||
Accept = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
# Available locations: https://docs.hetzner.com/cloud/general/locations/
|
||||
data "coder_parameter" "hcloud_location" {
|
||||
name = "hcloud_location"
|
||||
display_name = "Hetzner Location"
|
||||
description = "Select the Hetzner Cloud location for your workspace."
|
||||
type = "string"
|
||||
default = "fsn1"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = local.hcloud_locations
|
||||
content {
|
||||
name = format(
|
||||
"%s (%s, %s)",
|
||||
upper(option.value.name),
|
||||
option.value.city,
|
||||
option.value.country
|
||||
)
|
||||
value = option.value.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Available server types: https://docs.hetzner.com/cloud/servers/overview/
|
||||
data "coder_parameter" "hcloud_server_type" {
|
||||
name = "hcloud_server_type"
|
||||
display_name = "Hetzner Server Type"
|
||||
description = "Select the Hetzner Cloud server type for your workspace."
|
||||
type = "string"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = local.hcloud_server_type_options_for_selected_location
|
||||
content {
|
||||
name = option.value.name
|
||||
value = option.value.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_server" "dev" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
name = "coder-${data.coder_workspace.me.name}-dev"
|
||||
image = "ubuntu-24.04"
|
||||
server_type = data.coder_parameter.hcloud_server_type.value
|
||||
location = data.coder_parameter.hcloud_location.value
|
||||
public_net {
|
||||
ipv4_enabled = true
|
||||
ipv6_enabled = true
|
||||
}
|
||||
user_data = templatefile("cloud-config.yaml.tftpl", {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
home_volume_label = "coder-${data.coder_workspace.me.id}-home"
|
||||
volume_id = hcloud_volume.home_volume.id
|
||||
init_script = base64encode(coder_agent.main.init_script)
|
||||
coder_agent_token = coder_agent.main.token
|
||||
})
|
||||
labels = {
|
||||
"coder_workspace_name" = data.coder_workspace.me.name,
|
||||
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_volume" "home_volume" {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
size = data.coder_parameter.home_volume_size.value
|
||||
location = data.coder_parameter.hcloud_location.value
|
||||
labels = {
|
||||
"coder_workspace_name" = data.coder_workspace.me.name,
|
||||
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_volume_attachment" "home_volume_attachment" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
volume_id = hcloud_volume.home_volume.id
|
||||
server_id = hcloud_server.dev[count.index].id
|
||||
automount = false
|
||||
}
|
||||
|
||||
locals {
|
||||
username = lower(data.coder_workspace_owner.me.name)
|
||||
|
||||
# --------------------
|
||||
# Locations
|
||||
# --------------------
|
||||
hcloud_locations = [
|
||||
for loc in jsondecode(data.http.hcloud_locations.response_body).locations : {
|
||||
name = loc.name
|
||||
city = loc.city
|
||||
country = loc.country
|
||||
}
|
||||
]
|
||||
|
||||
# --------------------
|
||||
# Server Types
|
||||
# --------------------
|
||||
hcloud_server_types = {
|
||||
for st in jsondecode(data.http.hcloud_server_types.response_body).server_types :
|
||||
st.name => {
|
||||
cores = st.cores
|
||||
memory_gb = st.memory
|
||||
disk_gb = st.disk
|
||||
locations = [for l in st.locations : l.name]
|
||||
deprecated = st.deprecated
|
||||
}
|
||||
if st.deprecated == false
|
||||
}
|
||||
|
||||
hcloud_server_type_options_for_selected_location = [
|
||||
for name, meta in local.hcloud_server_types : {
|
||||
name = format(
|
||||
"%s (%d vCPU, %dGB RAM, %dGB)",
|
||||
upper(name),
|
||||
meta.cores,
|
||||
meta.memory_gb,
|
||||
meta.disk_gb
|
||||
)
|
||||
value = name
|
||||
}
|
||||
if contains(
|
||||
meta.locations,
|
||||
data.coder_parameter.hcloud_location.value
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {}
|
||||
|
||||
provider "coder" {}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
data "coder_parameter" "home_volume_size" {
|
||||
name = "home_volume_size"
|
||||
display_name = "Home volume size"
|
||||
description = "How large would you like your home volume to be (in GB)?"
|
||||
type = "number"
|
||||
default = "20"
|
||||
mutable = false
|
||||
validation {
|
||||
min = 1
|
||||
max = 100 # Adjust the max size as needed
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
|
||||
metadata {
|
||||
key = "cpu"
|
||||
display_name = "CPU Usage"
|
||||
interval = 5
|
||||
timeout = 5
|
||||
script = "coder stat cpu"
|
||||
}
|
||||
metadata {
|
||||
key = "memory"
|
||||
display_name = "Memory Usage"
|
||||
interval = 5
|
||||
timeout = 5
|
||||
script = "coder stat mem"
|
||||
}
|
||||
metadata {
|
||||
key = "home"
|
||||
display_name = "Home Usage"
|
||||
interval = 600 # every 10 minutes
|
||||
timeout = 30 # df can take a while on large filesystems
|
||||
script = "coder stat disk --path /home/${local.username}"
|
||||
}
|
||||
}
|
||||
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 7.4 KiB |
@@ -15,7 +15,7 @@ up a default or custom tmux configuration with session save/restore capabilities
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -39,7 +39,7 @@ module "tmux" {
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.example.id
|
||||
tmux_config = "" # Optional: custom tmux.conf content
|
||||
save_interval = 1 # Optional: save interval in minutes
|
||||
@@ -78,7 +78,7 @@ This module can provision multiple tmux sessions, each as a separate app in the
|
||||
```tf
|
||||
module "tmux" {
|
||||
source = "registry.coder.com/anomaly/tmux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
agent_id = var.agent_id
|
||||
sessions = ["default", "dev", "anomaly"]
|
||||
tmux_config = <<-EOT
|
||||
|
||||
@@ -55,7 +55,7 @@ resource "coder_script" "tmux" {
|
||||
display_name = "tmux"
|
||||
icon = "/icon/terminal.svg"
|
||||
script = templatefile("${path.module}/scripts/run.sh", {
|
||||
TMUX_CONFIG = var.tmux_config
|
||||
TMUX_CONFIG = base64encode(var.tmux_config)
|
||||
SAVE_INTERVAL = var.save_interval
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
@@ -4,7 +4,7 @@ BOLD='\033[0;1m'
|
||||
|
||||
# Convert templated variables to shell variables
|
||||
SAVE_INTERVAL="${SAVE_INTERVAL}"
|
||||
TMUX_CONFIG="${TMUX_CONFIG}"
|
||||
TMUX_CONFIG=$(echo -n "${TMUX_CONFIG}" | base64 -d)
|
||||
|
||||
# Function to install tmux
|
||||
install_tmux() {
|
||||
@@ -73,7 +73,7 @@ setup_tmux_config() {
|
||||
mkdir -p "$config_dir"
|
||||
|
||||
if [ -n "$TMUX_CONFIG" ]; then
|
||||
printf "$TMUX_CONFIG" > "$config_file"
|
||||
printf "%s" "$TMUX_CONFIG" > "$config_file"
|
||||
printf "$${BOLD}Custom tmux configuration applied at {$config_file} \n\n"
|
||||
else
|
||||
cat > "$config_file" << EOF
|
||||
|
||||
|
Before Width: | Height: | Size: 407 KiB After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 590 KiB |
@@ -13,7 +13,7 @@ Run Auggie CLI in your workspace to access Augment's AI coding assistant with ad
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ module "coder-login" {
|
||||
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -103,7 +103,7 @@ EOF
|
||||
```tf
|
||||
module "auggie" {
|
||||
source = "registry.coder.com/coder-labs/auggie/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
@@ -229,4 +229,8 @@ module "agentapi" {
|
||||
ARG_MCP_CONFIG='${var.mcp != null ? base64encode(replace(var.mcp, "'", "'\\''")) : ""}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
}
|
||||
@@ -51,7 +51,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
@@ -71,7 +71,7 @@ Customize tool permissions, MCP servers, and Copilot settings:
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
@@ -142,7 +142,7 @@ variable "github_token" {
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
github_token = var.github_token
|
||||
@@ -156,7 +156,7 @@ Run Copilot as a command-line tool without task reporting or web interface. This
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.3"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
report_tasks = false
|
||||
|
||||
@@ -3,7 +3,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,7 +242,7 @@ resource "coder_env" "github_token" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
@@ -268,7 +268,7 @@ module "agentapi" {
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
|
||||
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \
|
||||
@@ -288,7 +288,7 @@ module "agentapi" {
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
|
||||
|
||||
ARG_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
@@ -299,4 +299,8 @@ module "agentapi" {
|
||||
ARG_COPILOT_MODEL='${var.copilot_model}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ Run the Cursor Agent CLI in your workspace for interactive coding assistance and
|
||||
```tf
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -42,7 +42,7 @@ module "coder-login" {
|
||||
|
||||
module "cursor_cli" {
|
||||
source = "registry.coder.com/coder-labs/cursor-cli/coder"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("cursor-cli", async () => {
|
||||
"-c",
|
||||
"cat /home/coder/.cursor-cli-module/agentapi-start.log || cat /home/coder/.cursor-cli-module/start.log || true",
|
||||
]);
|
||||
expect(startLog.stdout).toContain(`-m ${model}`);
|
||||
expect(startLog.stdout).toContain(`--model ${model}`);
|
||||
expect(startLog.stdout).toContain("-f");
|
||||
expect(startLog.stdout).toContain("test prompt");
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ resource "coder_env" "cursor_api_key" {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
@@ -179,3 +179,7 @@ module "agentapi" {
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ ARGS=()
|
||||
|
||||
# global flags
|
||||
if [ -n "$ARG_MODEL" ]; then
|
||||
ARGS+=("-m" "$ARG_MODEL")
|
||||
ARGS+=("--model" "$ARG_MODEL")
|
||||
fi
|
||||
if [ "$ARG_FORCE" = "true" ]; then
|
||||
ARGS+=("-f")
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -46,7 +46,7 @@ variable "gemini_api_key" {
|
||||
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
@@ -94,7 +94,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
module "gemini" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
gemini_model = "gemini-2.5-flash"
|
||||
@@ -118,7 +118,7 @@ For enterprise users who prefer Google's Vertex AI platform:
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "2.1.2"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,7 +177,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.folder
|
||||
@@ -225,4 +225,8 @@ module "agentapi" {
|
||||
GEMINI_TASK_PROMPT='${var.task_prompt}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
display_name: Perplexica
|
||||
description: Run Perplexica AI search engine in your workspace via Docker
|
||||
icon: ../../../../.icons/perplexica.svg
|
||||
verified: false
|
||||
tags: [ai, search, docker]
|
||||
---
|
||||
|
||||
# Perplexica
|
||||
|
||||
Run [Perplexica](https://github.com/ItzCrazyKns/Perplexica), a privacy-focused AI search engine, in your Coder workspace. Supports cloud providers (OpenAI, Anthropic Claude) and local LLMs via Ollama.
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
This module uses the full Perplexica image with embedded SearXNG for simpler setup with no external dependencies.
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
This module requires Docker to be available on the host.
|
||||
|
||||
## Examples
|
||||
|
||||
### With API Keys
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
openai_api_key = var.openai_api_key
|
||||
anthropic_api_key = var.anthropic_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### With Local Ollama
|
||||
|
||||
```tf
|
||||
module "perplexica" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/perplexica/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
ollama_api_url = "http://ollama-external-endpoint:11434"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "docker_socket" {
|
||||
type = string
|
||||
description = "(Optional) Docker socket URI"
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "port" {
|
||||
type = number
|
||||
description = "The port to run Perplexica on."
|
||||
default = 3000
|
||||
}
|
||||
|
||||
variable "data_path" {
|
||||
type = string
|
||||
description = "Host path to mount for Perplexica data persistence."
|
||||
default = "./perplexica-data"
|
||||
}
|
||||
|
||||
variable "uploads_path" {
|
||||
type = string
|
||||
description = "Host path to mount for Perplexica file uploads."
|
||||
default = "./perplexica-uploads"
|
||||
}
|
||||
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key for Claude models."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "ollama_api_url" {
|
||||
type = string
|
||||
description = "Ollama API URL for local LLM support."
|
||||
default = ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "perplexica" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Perplexica"
|
||||
icon = "/icon/perplexica.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
DOCKER_HOST : var.docker_socket,
|
||||
PORT : var.port,
|
||||
DATA_PATH : var.data_path,
|
||||
UPLOADS_PATH : var.uploads_path,
|
||||
OPENAI_API_KEY : var.openai_api_key,
|
||||
ANTHROPIC_API_KEY : var.anthropic_api_key,
|
||||
OLLAMA_API_URL : var.ollama_api_url,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "perplexica" {
|
||||
agent_id = var.agent_id
|
||||
slug = "perplexica"
|
||||
display_name = "Perplexica"
|
||||
url = "http://localhost:${var.port}"
|
||||
icon = "/icon/perplexica.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
run "plan_basic" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.perplexica.url == "http://localhost:3000"
|
||||
error_message = "Default port should be 3000"
|
||||
}
|
||||
}
|
||||
|
||||
run "plan_custom_port" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
port = 8080
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.perplexica.url == "http://localhost:8080"
|
||||
error_message = "Should use custom port"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "$${BOLD}Starting Perplexica...$${RESET}\n"
|
||||
|
||||
# Set Docker host if provided
|
||||
if [ -n "${DOCKER_HOST}" ]; then
|
||||
export DOCKER_HOST="${DOCKER_HOST}"
|
||||
fi
|
||||
|
||||
# Wait for docker to become ready
|
||||
max_attempts=10
|
||||
delay=2
|
||||
attempt=1
|
||||
|
||||
while ! docker ps; do
|
||||
if [ $attempt -ge $max_attempts ]; then
|
||||
echo "Failed to list containers after $${max_attempts} attempts."
|
||||
exit 1
|
||||
fi
|
||||
echo "Attempt $${attempt} failed, retrying in $${delay}s..."
|
||||
sleep $delay
|
||||
attempt=$(expr "$attempt" + 1)
|
||||
delay=$(expr "$delay" \* 2)
|
||||
done
|
||||
|
||||
# Pull the image
|
||||
IMAGE="itzcrazykns1337/perplexica:latest"
|
||||
docker pull "$${IMAGE}"
|
||||
|
||||
# Build docker run command
|
||||
DOCKER_ARGS="-d --rm --name perplexica -p ${PORT}:3000"
|
||||
|
||||
# Add mounts - convert relative paths to absolute
|
||||
DATA_PATH="${DATA_PATH}"
|
||||
UPLOADS_PATH="${UPLOADS_PATH}"
|
||||
|
||||
mkdir -p "$${DATA_PATH}"
|
||||
mkdir -p "$${UPLOADS_PATH}"
|
||||
|
||||
DATA_PATH_ABS=$(cd "$${DATA_PATH}" && pwd)
|
||||
UPLOADS_PATH_ABS=$(cd "$${UPLOADS_PATH}" && pwd)
|
||||
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -v $${DATA_PATH_ABS}:/home/perplexica/data"
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -v $${UPLOADS_PATH_ABS}:/home/perplexica/uploads"
|
||||
|
||||
# Add environment variables if provided
|
||||
if [ -n "${OPENAI_API_KEY}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e OPENAI_API_KEY=${OPENAI_API_KEY}"
|
||||
fi
|
||||
|
||||
if [ -n "${ANTHROPIC_API_KEY}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}"
|
||||
fi
|
||||
|
||||
if [ -n "${OLLAMA_API_URL}" ]; then
|
||||
DOCKER_ARGS="$${DOCKER_ARGS} -e OLLAMA_API_URL=${OLLAMA_API_URL}"
|
||||
fi
|
||||
|
||||
# Run container
|
||||
docker run $${DOCKER_ARGS} "$${IMAGE}"
|
||||
|
||||
printf "\n$${BOLD}Perplexica is running on port ${PORT}$${RESET}\n"
|
||||
@@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI
|
||||
```tf
|
||||
module "amp-cli" {
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
version = "2.1.0"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key
|
||||
install_amp = true
|
||||
@@ -48,7 +48,7 @@ variable "amp_api_key" {
|
||||
module "amp-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/sourcegraph-amp/coder"
|
||||
amp_version = "2.1.0"
|
||||
amp_version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
amp_api_key = var.amp_api_key # recommended for tasks usage
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -110,6 +110,7 @@ describe("amp", async () => {
|
||||
const { id } = await setup({
|
||||
skipAmpMock: true,
|
||||
moduleVariables: {
|
||||
install_via_npm: "true",
|
||||
amp_version: "0.0.1755964909-g31e083",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
external = {
|
||||
source = "hashicorp/external"
|
||||
@@ -140,7 +140,7 @@ variable "base_amp_config" {
|
||||
type = string
|
||||
description = <<-EOT
|
||||
Base AMP configuration in JSON format. Can be overridden to customize AMP settings.
|
||||
|
||||
|
||||
If empty, defaults enable thinking and todos for autonomous operation. Additional options include:
|
||||
- "amp.permissions": [] (tool permissions)
|
||||
- "amp.tools.stopTimeout": 600 (extend timeout for long operations)
|
||||
@@ -148,7 +148,7 @@ variable "base_amp_config" {
|
||||
- "amp.tools.disable": ["builtin:open"] (disable tools for containers)
|
||||
- "amp.git.commit.ampThread.enabled": true (link commits to threads)
|
||||
- "amp.git.commit.coauthor.enabled": true (add Amp as co-author)
|
||||
|
||||
|
||||
Reference: https://ampcode.com/manual
|
||||
EOT
|
||||
default = ""
|
||||
@@ -220,7 +220,7 @@ locals {
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
@@ -268,4 +268,6 @@ module "agentapi" {
|
||||
EOT
|
||||
}
|
||||
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 976 KiB After Width: | Height: | Size: 528 KiB |
|
Before Width: | Height: | Size: 302 KiB After Width: | Height: | Size: 191 KiB |
@@ -3,30 +3,57 @@ set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
port=${1:-3284}
|
||||
start_timeout=${2:-60}
|
||||
listen_timeout=${3:-60}
|
||||
|
||||
# This script waits for the agentapi server to start on port 3284.
|
||||
# It considers the server started after 3 consecutive successful responses.
|
||||
|
||||
agentapi_started=false
|
||||
|
||||
echo "Waiting for agentapi server to start on port $port..."
|
||||
for i in $(seq 1 150); do
|
||||
echo "Waiting for agentapi process to start..."
|
||||
start=$(date +%s)
|
||||
while true; do
|
||||
now=$(date +%s)
|
||||
elapsed=$(( now - start ))
|
||||
if [[ "${elapsed}" -gt "${start_timeout}" ]]; then
|
||||
echo "agentapi process not found after ${start_timeout} seconds"
|
||||
exit 1
|
||||
fi
|
||||
set +e
|
||||
agentapi_pid=$(pidof agentapi)
|
||||
set -e
|
||||
if [[ -z "${agentapi_pid}" ]]; then
|
||||
echo "agentapi process not found (${elapsed}/${start_timeout})"
|
||||
sleep 1
|
||||
continue
|
||||
fi
|
||||
echo "agentapi process started with pid ${agentapi_pid} after ${elapsed} seconds"
|
||||
break
|
||||
done
|
||||
|
||||
echo "Waiting for agentapi to start listening on port ${port}..."
|
||||
start=$(date +%s)
|
||||
while true; do
|
||||
now=$(date +%s)
|
||||
elapsed=$(( now - start ))
|
||||
if [[ "${elapsed}" -gt "${listen_timeout}" ]]; then
|
||||
echo "agentapi server not listening on port ${port} after ${listen_timeout} seconds"
|
||||
exit 1
|
||||
fi
|
||||
for j in $(seq 1 3); do
|
||||
sleep 0.1
|
||||
if curl -fs -o /dev/null "http://localhost:$port/status"; then
|
||||
echo "agentapi response received ($j/3)"
|
||||
if curl -fs -o /dev/null "http://localhost:${port}/status"; then
|
||||
echo "agentapi response received (${j}/3)"
|
||||
sleep 0.1
|
||||
continue
|
||||
else
|
||||
echo "agentapi server not responding ($i/15)"
|
||||
echo "agentapi server not responding (${elapsed}/${listen_timeout})"
|
||||
sleep 1
|
||||
continue 2
|
||||
fi
|
||||
done
|
||||
agentapi_started=true
|
||||
echo "agentapi server started responding after ${elapsed} seconds"
|
||||
break
|
||||
done
|
||||
|
||||
if [ "$agentapi_started" != "true" ]; then
|
||||
echo "Error: agentapi server did not start on port $port after 15 seconds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "agentapi server started on port $port."
|
||||
echo "agentapi server started on port ${port}."
|
||||
|
||||
@@ -4,11 +4,35 @@ import {
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
TerraformState,
|
||||
writeFileContainer,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
import { expect } from "bun:test";
|
||||
|
||||
/**
|
||||
* Extracts all coder_env resources from Terraform state and returns them as
|
||||
* a Record of environment variable names to values.
|
||||
*/
|
||||
export const extractCoderEnvVars = (
|
||||
state: TerraformState,
|
||||
): Record<string, string> => {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
for (const resource of state.resources) {
|
||||
if (resource.type === "coder_env" && resource.instances.length > 0) {
|
||||
const instance = resource.instances[0].attributes;
|
||||
const name = instance.name as string;
|
||||
const value = instance.value as string;
|
||||
if (name && value) {
|
||||
envVars[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return envVars;
|
||||
};
|
||||
|
||||
export const setupContainer = async ({
|
||||
moduleDir,
|
||||
image,
|
||||
@@ -23,10 +47,12 @@ export const setupContainer = async ({
|
||||
...vars,
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const coderEnvVars = extractCoderEnvVars(state);
|
||||
const id = await runContainer(image ?? "codercom/enterprise-node:latest");
|
||||
return {
|
||||
id,
|
||||
coderScript,
|
||||
coderEnvVars,
|
||||
cleanup: async () => {
|
||||
if (
|
||||
process.env["DEBUG"] === "true" ||
|
||||
@@ -79,9 +105,11 @@ interface SetupProps {
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
export const setup = async (props: SetupProps): Promise<{ id: string }> => {
|
||||
export const setup = async (
|
||||
props: SetupProps,
|
||||
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
|
||||
const projectDir = props.projectDir ?? "/home/coder/project";
|
||||
const { id, coderScript, cleanup } = await setupContainer({
|
||||
const { id, coderScript, coderEnvVars, cleanup } = await setupContainer({
|
||||
moduleDir: props.moduleDir,
|
||||
vars: props.moduleVariables,
|
||||
});
|
||||
@@ -101,7 +129,7 @@ export const setup = async (props: SetupProps): Promise<{ id: string }> => {
|
||||
filePath: "/home/coder/script.sh",
|
||||
content: coderScript.script,
|
||||
});
|
||||
return { id };
|
||||
return { id, coderEnvVars };
|
||||
};
|
||||
|
||||
export const expectAgentAPIStarted = async (
|
||||
@@ -125,18 +153,16 @@ export const execModuleScript = async (
|
||||
id: string,
|
||||
env?: Record<string, string>,
|
||||
) => {
|
||||
const envArgs = Object.entries(env ?? {})
|
||||
.map(([key, value]) => ["--env", `${key}=${value}`])
|
||||
.flat();
|
||||
const resp = await execContainer(
|
||||
id,
|
||||
[
|
||||
"bash",
|
||||
"-c",
|
||||
`set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||
],
|
||||
envArgs,
|
||||
);
|
||||
const envArgs = env
|
||||
? Object.entries(env)
|
||||
.map(([key, value]) => `export ${key}="${value.replace(/"/g, '\\"')}"`)
|
||||
.join(" && ") + " && "
|
||||
: "";
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`${envArgs}set -o errexit; set -o pipefail; cd /home/coder && ./script.sh 2>&1 | tee /home/coder/script.log`,
|
||||
]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
|
||||
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
@@ -45,7 +45,7 @@ This example shows how to configure the Claude Code module to run the agent behi
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
enable_boundary = true
|
||||
@@ -72,7 +72,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -92,10 +92,9 @@ module "claude-code" {
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-custom-tool": {
|
||||
"command": "my-tool-server"
|
||||
"command": "my-tool-server",
|
||||
"args": ["--port", "8080"]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
EOF
|
||||
@@ -109,7 +108,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
install_claude_code = true
|
||||
@@ -131,7 +130,7 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
@@ -204,7 +203,7 @@ resource "coder_env" "bedrock_api_key" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -261,7 +260,7 @@ resource "coder_env" "google_application_credentials" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
version = "4.2.9"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
@@ -39,9 +39,11 @@ interface SetupProps {
|
||||
agentapiMockScript?: string;
|
||||
}
|
||||
|
||||
const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
const setup = async (
|
||||
props?: SetupProps,
|
||||
): Promise<{ id: string; coderEnvVars: Record<string, string> }> => {
|
||||
const projectDir = "/home/coder/project";
|
||||
const { id } = await setupUtil({
|
||||
const { id, coderEnvVars } = await setupUtil({
|
||||
moduleDir: import.meta.dir,
|
||||
moduleVariables: {
|
||||
install_claude_code: props?.skipClaudeMock ? "true" : "false",
|
||||
@@ -61,7 +63,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
content: await loadTestFile(import.meta.dir, "claude-mock.sh"),
|
||||
});
|
||||
}
|
||||
return { id };
|
||||
return { id, coderEnvVars };
|
||||
};
|
||||
|
||||
setDefaultTimeout(60 * 1000);
|
||||
@@ -79,14 +81,14 @@ describe("claude-code", async () => {
|
||||
|
||||
test("install-claude-code-version", async () => {
|
||||
const version_to_install = "1.0.40";
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
claude_code_version: version_to_install,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
@@ -96,14 +98,14 @@ describe("claude-code", async () => {
|
||||
});
|
||||
|
||||
test("check-latest-claude-code-version-works", async () => {
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
skipAgentAPIMock: true,
|
||||
moduleVariables: {
|
||||
install_claude_code: "true",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
await expectAgentAPIStarted(id);
|
||||
});
|
||||
|
||||
@@ -133,13 +135,13 @@ describe("claude-code", async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const { id } = await setup({
|
||||
const { id, coderEnvVars } = await setup({
|
||||
skipClaudeMock: true,
|
||||
moduleVariables: {
|
||||
mcp: mcpConfig,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
await execModuleScript(id, coderEnvVars);
|
||||
|
||||
const resp = await readFileContainer(id, "/home/coder/.claude.json");
|
||||
expect(resp).toContain("test-cmd");
|
||||
|
||||
@@ -86,7 +86,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.11.4"
|
||||
default = "v0.11.6"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
@@ -288,6 +288,12 @@ resource "coder_env" "disable_autoupdater" {
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_binary_path" {
|
||||
agent_id = var.agent_id
|
||||
name = "PATH"
|
||||
value = "$HOME/.local/bin:$PATH"
|
||||
}
|
||||
|
||||
locals {
|
||||
# we have to trim the slash because otherwise coder exp mcp will
|
||||
# set up an invalid claude config
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
@@ -45,11 +40,6 @@ function install_claude_code_cli() {
|
||||
if [ $CURL_EXIT -ne 0 ]; then
|
||||
echo "Claude Code installer failed with exit code $$CURL_EXIT"
|
||||
fi
|
||||
|
||||
# Ensure binaries are discoverable.
|
||||
echo "Creating a symlink for claude"
|
||||
sudo ln -s /home/coder/.local/bin/claude /usr/local/bin/claude
|
||||
|
||||
echo "Installed Claude Code successfully. Version: $(claude --version || echo 'unknown')"
|
||||
else
|
||||
echo "Skipping Claude Code installation as per configuration."
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
source "$HOME"/.bashrc
|
||||
fi
|
||||
|
||||
# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
@@ -55,6 +48,13 @@ function install_boundary() {
|
||||
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
|
||||
# Install boundary by compiling from source
|
||||
echo "Compiling boundary from source (version: $ARG_BOUNDARY_VERSION)"
|
||||
|
||||
echo "Removing existing boundary directory to allow re-running the script safely"
|
||||
if [ -d boundary ]; then
|
||||
rm -rf boundary
|
||||
fi
|
||||
|
||||
echo "Clone boundary repository"
|
||||
git clone https://github.com/coder/boundary.git
|
||||
cd boundary
|
||||
git checkout "$ARG_BOUNDARY_VERSION"
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -29,9 +29,9 @@ module "code-server" {
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
agent_id = coder_agent.example.id
|
||||
install_version = "1.4.1"
|
||||
install_version = "4.106.3"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -43,7 +43,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = [
|
||||
"dracula-theme.theme-dracula"
|
||||
@@ -61,7 +61,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@@ -78,7 +78,7 @@ Just run code-server in the background, don't fetch it from GitHub:
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
}
|
||||
@@ -92,7 +92,7 @@ You can pass additional command-line arguments to code-server using the `additio
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
agent_id = coder_agent.example.id
|
||||
additional_args = "--disable-workspace-trust"
|
||||
}
|
||||
@@ -108,7 +108,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
agent_id = coder_agent.example.id
|
||||
use_cached = true
|
||||
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
|
||||
@@ -121,8 +121,10 @@ Just run code-server in the background, don't fetch it from GitHub:
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/code-server/coder"
|
||||
version = "1.4.1"
|
||||
version = "1.4.2"
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
}
|
||||
```
|
||||
|
||||
Some of the key differences between code-server and [VS Code Web](https://registry.coder.com/modules/coder/vscode-web) are listed in [docs](https://coder.com/docs/user-guides/workspace-access/code-server#differences-between-code-server-and-vs-code-web).
|
||||
|
||||
@@ -14,7 +14,7 @@ A file browser for your workspace.
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -41,7 +41,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
database_path = ".config/filebrowser.db"
|
||||
}
|
||||
@@ -53,7 +53,7 @@ module "filebrowser" {
|
||||
module "filebrowser" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/filebrowser/coder"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
subdomain = false
|
||||
|
||||
@@ -28,7 +28,7 @@ if [[ ! -f "${DB_PATH}" ]]; then
|
||||
filebrowser users add admin "coderPASSWORD" --perm.admin=true --viewMode=mosaic 2>&1 | tee -a ${LOG_PATH}
|
||||
fi
|
||||
|
||||
filebrowser config set --baseurl=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
|
||||
filebrowser config set --baseURL=${SERVER_BASE_PATH} --port=${PORT} --auth.method=noauth --root=$ROOT_DIR 2>&1 | tee -a ${LOG_PATH}
|
||||
|
||||
printf "👷 Starting filebrowser in background... \n\n"
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@@ -28,7 +28,7 @@ module "git-clone" {
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
base_dir = "~/projects/coder"
|
||||
@@ -43,11 +43,12 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
|
||||
|
||||
data "coder_external_auth" "github" {
|
||||
id = "github"
|
||||
}
|
||||
@@ -69,11 +70,12 @@ data "coder_parameter" "git_repo" {
|
||||
module "git_clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = data.coder_parameter.git_repo.value
|
||||
}
|
||||
|
||||
|
||||
# Create a code-server instance for the cloned repository
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
@@ -103,13 +105,14 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.example.com/coder/coder/tree/feat/example"
|
||||
git_providers = {
|
||||
"https://github.example.com/" = {
|
||||
provider = "github"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -122,7 +125,7 @@ To GitLab clone with a specific branch like `feat/example`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
||||
}
|
||||
@@ -134,13 +137,14 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
||||
git_providers = {
|
||||
"https://gitlab.example.com/" = {
|
||||
provider = "gitlab"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -155,7 +159,7 @@ For example, to clone the `feat/example` branch:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
branch_name = "feat/example"
|
||||
@@ -173,7 +177,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
folder_name = "coder-dev"
|
||||
@@ -191,8 +195,8 @@ If not defined, the default, `0`, performs a full clone.
|
||||
```tf
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
depth = 1
|
||||
@@ -208,7 +212,7 @@ This is useful for running initialization tasks like installing dependencies or
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.2.2"
|
||||
version = "1.2.3"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
post_clone_script = <<-EOT
|
||||
|
||||
@@ -58,9 +58,10 @@ fi
|
||||
# Run post-clone script if provided
|
||||
if [ -n "$POST_CLONE_SCRIPT" ]; then
|
||||
echo "Running post-clone script..."
|
||||
echo "$POST_CLONE_SCRIPT" | base64 -d > /tmp/post_clone.sh
|
||||
chmod +x /tmp/post_clone.sh
|
||||
POST_CLONE_TMP=$(mktemp)
|
||||
echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_TMP"
|
||||
chmod +x "$POST_CLONE_TMP"
|
||||
cd "$CLONE_PATH" || exit
|
||||
/tmp/post_clone.sh
|
||||
rm /tmp/post_clone.sh
|
||||
$POST_CLONE_TMP
|
||||
rm "$POST_CLONE_TMP"
|
||||
fi
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { type Server, serve } from "bun";
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { serve } from "bun";
|
||||
import {
|
||||
afterEach,
|
||||
beforeAll,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
setDefaultTimeout,
|
||||
} from "bun:test";
|
||||
import {
|
||||
createJSONResponse,
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
@@ -11,77 +19,48 @@ import {
|
||||
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);
|
||||
});
|
||||
let cleanupFunctions: (() => Promise<void>)[] = [];
|
||||
const registerCleanup = (cleanup: () => Promise<void>) => {
|
||||
cleanupFunctions.push(cleanup);
|
||||
};
|
||||
afterEach(async () => {
|
||||
const cleanupFnsCopy = cleanupFunctions.slice().reverse();
|
||||
cleanupFunctions = [];
|
||||
for (const cleanup of cleanupFnsCopy) {
|
||||
try {
|
||||
await cleanup();
|
||||
} catch (error) {
|
||||
console.error("Error during cleanup:", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const setupContainer = async (
|
||||
image = "lorello/alpine-bash",
|
||||
vars: Record<string, string> = {},
|
||||
) => {
|
||||
const server = await setupServer();
|
||||
const server = setupServer();
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
...vars,
|
||||
});
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer(image);
|
||||
|
||||
registerCleanup(async () => {
|
||||
server.stop();
|
||||
});
|
||||
registerCleanup(async () => {
|
||||
await removeContainer(id);
|
||||
});
|
||||
|
||||
return { id, instance, server };
|
||||
};
|
||||
|
||||
const setupServer = async (): Promise<Server> => {
|
||||
let url: URL;
|
||||
const fakeSlackHost = serve({
|
||||
const setupServer = () => {
|
||||
const fakeGithubHost = serve({
|
||||
fetch: (req) => {
|
||||
url = new URL(req.url);
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/api/v2/users/me/gitsshkey") {
|
||||
return createJSONResponse({
|
||||
public_key: "exists",
|
||||
@@ -128,5 +107,60 @@ const setupServer = async (): Promise<Server> => {
|
||||
port: 0,
|
||||
});
|
||||
|
||||
return fakeSlackHost;
|
||||
return fakeGithubHost;
|
||||
};
|
||||
|
||||
setDefaultTimeout(30 * 1000);
|
||||
|
||||
describe("github-upload-public-key", () => {
|
||||
beforeAll(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);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,9 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
# tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." # Optional
|
||||
}
|
||||
```
|
||||
|
||||
@@ -40,7 +39,7 @@ When `default` contains IDE codes, those IDEs are created directly without user
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
|
||||
@@ -53,7 +52,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
# Show parameter with limited options
|
||||
@@ -67,7 +66,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
@@ -82,7 +81,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
@@ -109,7 +108,7 @@ module "jetbrains" {
|
||||
module "jetbrains_pycharm" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
@@ -129,11 +128,11 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons:
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
|
||||
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
|
||||
}
|
||||
```
|
||||
|
||||
@@ -170,13 +169,6 @@ resource "coder_metadata" "container_info" {
|
||||
- If the API is unreachable (air-gapped environments), the module automatically falls back to build numbers from `ide_config`
|
||||
- `major_version` and `channel` control which API endpoint is queried (when API access is available)
|
||||
|
||||
### Tooltip
|
||||
|
||||
- **`tooltip`**: Optional markdown text displayed when hovering over IDE app buttons
|
||||
- If not specified, no tooltip is shown
|
||||
- Supports markdown formatting for rich text (bold, italic, links, etc.)
|
||||
- All IDE apps created by this module will show the same tooltip text
|
||||
|
||||
## Supported IDEs
|
||||
|
||||
All JetBrains IDEs with remote development capabilities:
|
||||
|
||||
@@ -2,15 +2,15 @@ variables {
|
||||
# Default IDE config, mirrored from main.tf for test assertions.
|
||||
# If main.tf defaults change, update this map to match.
|
||||
expected_ide_config = {
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,16 +187,16 @@ run "tooltip_when_provided" {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."
|
||||
tooltip = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button."])
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
|
||||
error_message = "Expected coder_app tooltip to be set when provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "tooltip_null_when_not_provided" {
|
||||
run "tooltip_default_when_not_provided" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
@@ -206,8 +206,41 @@ run "tooltip_null_when_not_provided" {
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == null])
|
||||
error_message = "Expected coder_app tooltip to be null when not provided"
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.tooltip == "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."])
|
||||
error_message = "Expected coder_app tooltip to be the default JetBrains Toolbox message when not provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "channel_eap" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
channel = "eap"
|
||||
major_version = "latest"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.ide_metadata["GO"].json_data.type == "eap"
|
||||
error_message = "Expected the API to return a release of type 'eap', but got '${output.ide_metadata["GO"].json_data.type}'"
|
||||
}
|
||||
}
|
||||
|
||||
run "specific_major_version" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
major_version = "2025.3"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.ide_metadata["GO"].json_data.majorVersion == "2025.3"
|
||||
error_message = "Expected the API to return a release for major version '2025.3', but got '${output.ide_metadata["GO"].json_data.majorVersion}'"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,3 +327,27 @@ run "output_multiple_ides" {
|
||||
error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'"
|
||||
}
|
||||
}
|
||||
run "validate_output_schema" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = alltrue([
|
||||
for key, meta in output.ide_metadata : (
|
||||
can(meta.icon) &&
|
||||
can(meta.name) &&
|
||||
can(meta.identifier) &&
|
||||
can(meta.key) &&
|
||||
can(meta.build) &&
|
||||
# json_data can be null, but the key must exist
|
||||
can(meta.json_data)
|
||||
)
|
||||
])
|
||||
error_message = "The ide_metadata output schema has changed. Please update the 'main.tf' and this test."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ variable "coder_parameter_order" {
|
||||
variable "tooltip" {
|
||||
type = string
|
||||
description = "Markdown text that is displayed when hovering over workspace apps."
|
||||
default = null
|
||||
default = "You need to install [JetBrains Toolbox App](https://www.jetbrains.com/toolbox-app/) to use this button."
|
||||
}
|
||||
|
||||
variable "major_version" {
|
||||
@@ -70,8 +70,8 @@ variable "major_version" {
|
||||
description = "The major version of the IDE. i.e. 2025.1"
|
||||
default = "latest"
|
||||
validation {
|
||||
condition = can(regex("^[0-9]{4}\\.[0-2]{1}$", var.major_version)) || var.major_version == "latest"
|
||||
error_message = "The major_version must be a valid version number. i.e. 2025.1 or latest"
|
||||
condition = can(regex("^[0-9]{4}\\.[1-3]$", var.major_version)) || var.major_version == "latest"
|
||||
error_message = "The major_version must be a valid version number (e.g., 2025.1) or 'latest'"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ variable "download_base_link" {
|
||||
|
||||
data "http" "jetbrains_ide_versions" {
|
||||
for_each = length(var.default) == 0 ? var.options : var.default
|
||||
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}&latest=true${var.major_version == "latest" ? "" : "&major_version=${var.major_version}"}"
|
||||
url = "${var.releases_base_link}/products/releases?code=${each.key}&type=${var.channel}${var.major_version == "latest" ? "&latest=true" : ""}"
|
||||
}
|
||||
|
||||
variable "ide_config" {
|
||||
@@ -138,9 +138,9 @@ variable "ide_config" {
|
||||
- build: The build number of the IDE.
|
||||
Example:
|
||||
{
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
|
||||
}
|
||||
EOT
|
||||
type = map(object({
|
||||
@@ -149,15 +149,15 @@ variable "ide_config" {
|
||||
build = string
|
||||
}))
|
||||
default = {
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" }
|
||||
"CL" = { name = "CLion", icon = "/icon/clion.svg", build = "253.29346.141" },
|
||||
"GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "253.28294.337" },
|
||||
"IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "253.29346.138" },
|
||||
"PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "253.29346.151" },
|
||||
"PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "253.29346.142" },
|
||||
"RD" = { name = "Rider", icon = "/icon/rider.svg", build = "253.29346.144" },
|
||||
"RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "253.29346.140" },
|
||||
"RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "253.29346.139" },
|
||||
"WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "253.29346.143" }
|
||||
}
|
||||
validation {
|
||||
condition = length(var.ide_config) > 0
|
||||
@@ -182,6 +182,20 @@ locals {
|
||||
)
|
||||
}
|
||||
|
||||
# Filter the parsed response for the requested major version if not "latest"
|
||||
filtered_releases = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => [
|
||||
for r in try(local.parsed_responses[code][keys(local.parsed_responses[code])[0]], []) :
|
||||
r if var.major_version == "latest" || r.majorVersion == var.major_version
|
||||
]
|
||||
}
|
||||
|
||||
# Select the latest release for the requested major version (first item in the filtered list)
|
||||
selected_releases = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code =>
|
||||
length(local.filtered_releases[code]) > 0 ? local.filtered_releases[code][0] : null
|
||||
}
|
||||
|
||||
# Dynamically generate IDE configurations based on options with fallback to ide_config
|
||||
options_metadata = {
|
||||
for code in length(var.default) == 0 ? var.options : var.default : code => {
|
||||
@@ -191,13 +205,10 @@ locals {
|
||||
key = code
|
||||
|
||||
# Use API build number if available, otherwise fall back to ide_config build number
|
||||
build = length(keys(local.parsed_responses[code])) > 0 ? (
|
||||
local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0].build
|
||||
) : var.ide_config[code].build
|
||||
build = local.selected_releases[code] != null ? local.selected_releases[code].build : var.ide_config[code].build
|
||||
|
||||
# Store API data for potential future use (only if API is available)
|
||||
json_data = length(keys(local.parsed_responses[code])) > 0 ? local.parsed_responses[code][keys(local.parsed_responses[code])[0]][0] : null
|
||||
response_key = length(keys(local.parsed_responses[code])) > 0 ? keys(local.parsed_responses[code])[0] : null
|
||||
# Store API data for potential future use
|
||||
json_data = local.selected_releases[code]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
|
||||
module "kasmvnc" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kasmvnc/coder"
|
||||
version = "1.2.6"
|
||||
version = "1.2.7"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
subdomain = true
|
||||
|
||||
@@ -31,7 +31,7 @@ variable "desktop_environment" {
|
||||
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
|
||||
|
||||
validation {
|
||||
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
|
||||
condition = contains(["cinnamon", "mate", "lxde", "lxqt", "kde", "gnome", "xfce", "manual"], var.desktop_environment)
|
||||
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ Automatically install and run [mux](https://github.com/coder/mux) in a Coder wor
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.4"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -37,7 +37,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.4"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -48,7 +48,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.4"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
# Default is "latest"; set to a specific version to pin
|
||||
install_version = "0.4.0"
|
||||
@@ -61,7 +61,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.4"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
port = 8080
|
||||
}
|
||||
@@ -75,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm:
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.3"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
use_cached = true
|
||||
}
|
||||
@@ -89,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed):
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.4"
|
||||
version = "1.0.6"
|
||||
agent_id = coder_agent.main.id
|
||||
install = false
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ RESET='\033[0m'
|
||||
MUX_BINARY="${INSTALL_PREFIX}/mux"
|
||||
|
||||
function run_mux() {
|
||||
# Remove stale server lock if present
|
||||
rm -f "$HOME/.mux/server.lock"
|
||||
|
||||
local port_value
|
||||
port_value="${PORT}"
|
||||
if [ -z "$port_value" ]; then
|
||||
@@ -94,7 +97,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
|
||||
fi
|
||||
# sed-based fallback
|
||||
if [ -z "$TARBALL_URL" ]; then
|
||||
TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"tarball\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
|
||||
TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"tarball":"\([^"]*\)".*/\1/p' | head -n1)"
|
||||
fi
|
||||
# Fallback: resolve version then construct tarball URL
|
||||
if [ -z "$TARBALL_URL" ]; then
|
||||
@@ -103,10 +106,10 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_JSON" | node -e 'try{const fs=require("fs");const data=JSON.parse(fs.readFileSync(0,"utf8"));if(data&&data.version){console.log(data.version);}}catch(e){}')"
|
||||
fi
|
||||
if [ -z "$RESOLVED_VERSION" ]; then
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)"
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -n1)"
|
||||
fi
|
||||
if [ -z "$RESOLVED_VERSION" ]; then
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '\"version\":\"[^\"]*\"' | head -n1 | cut -d '\"' -f4)"
|
||||
RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '"version":"[^"]*"' | head -n1 | cut -d '"' -f4)"
|
||||
fi
|
||||
if [ -n "$RESOLVED_VERSION" ]; then
|
||||
VERSION_TO_USE="$RESOLVED_VERSION"
|
||||
@@ -138,9 +141,9 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
|
||||
fi
|
||||
if [ -z "$BIN_PATH" ]; then
|
||||
# sed fallbacks (handle both string and object forms)
|
||||
BIN_PATH=$(sed -n 's/.*\"bin\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' "$TMP_DIR/package/package.json" | head -n1)
|
||||
BIN_PATH=$(sed -n 's/.*"bin"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP_DIR/package/package.json" | head -n1)
|
||||
if [ -z "$BIN_PATH" ]; then
|
||||
BIN_PATH=$(sed -n '/\"bin\"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*\"mux\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1)
|
||||
BIN_PATH=$(sed -n '/"bin"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*"mux"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
fi
|
||||
fi
|
||||
if [ -n "$BIN_PATH" ] && [ -f "$TMP_DIR/package/$BIN_PATH" ]; then
|
||||
|
||||
@@ -13,7 +13,7 @@ Installs the [Vault](https://www.vaultproject.io/) CLI and optionally configures
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
@@ -34,7 +34,7 @@ If you have a Vault token, you can provide it to automatically configure authent
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token # Optional
|
||||
@@ -50,7 +50,7 @@ Install the Vault CLI without any authentication:
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
}
|
||||
@@ -61,7 +61,7 @@ module "vault_cli" {
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_cli_version = "1.15.0"
|
||||
@@ -73,7 +73,7 @@ module "vault_cli" {
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
install_dir = "/home/coder/bin"
|
||||
@@ -87,7 +87,7 @@ For Vault Enterprise users who need to specify a namespace:
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.vault_token
|
||||
@@ -102,7 +102,7 @@ Install the Vault Enterprise binary. This is required if using SAML authenticati
|
||||
```tf
|
||||
module "vault_cli" {
|
||||
source = "registry.coder.com/coder/vault-cli/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
enterprise = true
|
||||
|
||||
@@ -7,40 +7,34 @@ INSTALL_DIR=${INSTALL_DIR}
|
||||
VAULT_CLI_VERSION=${VAULT_CLI_VERSION}
|
||||
ENTERPRISE=${ENTERPRISE}
|
||||
|
||||
# Fetch URL content. If dest is provided, write to file; otherwise output to stdout.
|
||||
# Usage: fetch <url> [dest]
|
||||
# Fetch URL content to stdout
|
||||
fetch() {
|
||||
url="$1"
|
||||
dest="$${2:-}"
|
||||
|
||||
# Detect HTTP client on first run
|
||||
if [ -z "$${HTTP_CLIENT:-}" ]; then
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="curl"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="wget"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
HTTP_CLIENT="busybox"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$${dest}" ]; then
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" -o "$${dest}" ;;
|
||||
wget) wget -O "$${dest}" "$${url}" ;;
|
||||
busybox) busybox wget -O "$${dest}" "$${url}" ;;
|
||||
esac
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -sSL --fail "$${url}"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -qO- "$${url}"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
busybox wget -qO- "$${url}"
|
||||
else
|
||||
# shellcheck disable=SC2195
|
||||
case "$${HTTP_CLIENT}" in
|
||||
curl) curl -sSL --fail "$${url}" ;;
|
||||
wget) wget -qO- "$${url}" ;;
|
||||
busybox) busybox wget -qO- "$${url}" ;;
|
||||
esac
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Download URL to a file
|
||||
fetch_to_file() {
|
||||
dest="$1"
|
||||
url="$2"
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -sSL --fail "$${url}" -o "$${dest}"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -O "$${dest}" "$${url}"
|
||||
elif command -v busybox > /dev/null 2>&1; then
|
||||
busybox wget -O "$${dest}" "$${url}"
|
||||
else
|
||||
printf "curl, wget, or busybox is not installed. Please install curl or wget in your image.\n"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -141,7 +135,7 @@ install() {
|
||||
cd "$${TEMP_DIR}" || return 1
|
||||
|
||||
printf "Downloading from %s\n" "$${DOWNLOAD_URL}"
|
||||
if ! fetch "$${DOWNLOAD_URL}" vault.zip; then
|
||||
if ! fetch_to_file vault.zip "$${DOWNLOAD_URL}"; then
|
||||
printf "Failed to download Vault.\n"
|
||||
rm -rf "$${TEMP_DIR}"
|
||||
return 1
|
||||
|
||||
@@ -19,7 +19,7 @@ Zed is a high-performance, multiplayer code editor from the creators of Atom and
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -32,7 +32,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -44,7 +44,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
display_name = "Zed Editor"
|
||||
order = 1
|
||||
@@ -57,7 +57,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = coder_agent.example.name
|
||||
}
|
||||
@@ -73,7 +73,7 @@ You can declaratively set/merge settings with the `settings` input. Provide a JS
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.1.4"
|
||||
agent_id = coder_agent.main.id
|
||||
|
||||
settings = jsonencode({
|
||||
@@ -85,6 +85,7 @@ module "zed" {
|
||||
env = {}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
@@ -12,66 +16,114 @@ describe("zed", async () => {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output", async () => {
|
||||
it("creates settings file with correct JSON", async () => {
|
||||
const settings = {
|
||||
theme: "One Dark",
|
||||
buffer_font_size: 14,
|
||||
vim_mode: true,
|
||||
telemetry: {
|
||||
diagnostics: false,
|
||||
metrics: false,
|
||||
},
|
||||
// Test special characters: single quotes, backslashes, URLs
|
||||
message: "it's working",
|
||||
path: "C:\\Users\\test",
|
||||
api_url: "https://api.example.com/v1?token=abc&user=test",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
settings: JSON.stringify(settings),
|
||||
});
|
||||
expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder");
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "zed",
|
||||
);
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer("alpine:latest");
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
try {
|
||||
const result = await execContainer(id, ["sh", "-c", instance.script]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const catResult = await execContainer(id, [
|
||||
"cat",
|
||||
"/root/.config/zed/settings.json",
|
||||
]);
|
||||
expect(catResult.exitCode).toBe(0);
|
||||
|
||||
const written = JSON.parse(catResult.stdout.trim());
|
||||
expect(written).toEqual(settings);
|
||||
} finally {
|
||||
await removeContainer(id);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
it("merges settings with existing file when jq available", async () => {
|
||||
const existingSettings = {
|
||||
theme: "Solarized Dark",
|
||||
vim_mode: true,
|
||||
};
|
||||
|
||||
const newSettings = {
|
||||
theme: "One Dark",
|
||||
buffer_font_size: 14,
|
||||
};
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
settings: JSON.stringify(newSettings),
|
||||
});
|
||||
expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder/foo/bar");
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer("alpine:latest");
|
||||
|
||||
try {
|
||||
// Install jq and create existing settings file
|
||||
await execContainer(id, ["apk", "add", "--no-cache", "jq"]);
|
||||
await execContainer(id, ["mkdir", "-p", "/root/.config/zed"]);
|
||||
await execContainer(id, [
|
||||
"sh",
|
||||
"-c",
|
||||
`echo '${JSON.stringify(existingSettings)}' > /root/.config/zed/settings.json`,
|
||||
]);
|
||||
|
||||
const result = await execContainer(id, ["sh", "-c", instance.script]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
const catResult = await execContainer(id, [
|
||||
"cat",
|
||||
"/root/.config/zed/settings.json",
|
||||
]);
|
||||
expect(catResult.exitCode).toBe(0);
|
||||
|
||||
const merged = JSON.parse(catResult.stdout.trim());
|
||||
expect(merged.theme).toBe("One Dark"); // overwritten
|
||||
expect(merged.buffer_font_size).toBe(14); // added
|
||||
expect(merged.vim_mode).toBe(true); // preserved
|
||||
} finally {
|
||||
await removeContainer(id);
|
||||
}
|
||||
}, 30000);
|
||||
|
||||
it("exits early with empty settings", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
settings: "",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "zed",
|
||||
);
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await runContainer("alpine:latest");
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
try {
|
||||
const result = await execContainer(id, ["sh", "-c", instance.script]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
it("expect display_name to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
display_name: "Custom Zed",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "zed",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("Custom Zed");
|
||||
});
|
||||
|
||||
it("adds agent_name to hostname", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
agent_name: "myagent",
|
||||
});
|
||||
expect(state.outputs.zed_url.value).toBe(
|
||||
"zed://ssh/myagent.default.default.coder",
|
||||
);
|
||||
});
|
||||
// Settings file should not be created
|
||||
const catResult = await execContainer(id, [
|
||||
"cat",
|
||||
"/root/.config/zed/settings.json",
|
||||
]);
|
||||
expect(catResult.exitCode).not.toBe(0);
|
||||
} finally {
|
||||
await removeContainer(id);
|
||||
}
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
@@ -65,6 +65,7 @@ locals {
|
||||
owner_name = lower(data.coder_workspace_owner.me.name)
|
||||
agent_name = lower(var.agent_name)
|
||||
hostname = var.agent_name != "" ? "${local.agent_name}.${local.workspace_name}.${local.owner_name}.coder" : "${local.workspace_name}.coder"
|
||||
settings_b64 = var.settings != "" ? base64encode(var.settings) : ""
|
||||
}
|
||||
|
||||
resource "coder_script" "zed_settings" {
|
||||
@@ -73,9 +74,13 @@ resource "coder_script" "zed_settings" {
|
||||
icon = "/icon/zed.svg"
|
||||
run_on_start = true
|
||||
script = <<-EOT
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}'
|
||||
SETTINGS_B64='${local.settings_b64}'
|
||||
if [ -z "$${SETTINGS_B64}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
SETTINGS_JSON="$(echo -n "$${SETTINGS_B64}" | base64 -d)"
|
||||
if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -5,6 +5,20 @@ run "default_output" {
|
||||
agent_id = "foo"
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_workspace.me
|
||||
values = {
|
||||
name = "default"
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_workspace_owner.me
|
||||
values = {
|
||||
name = "default"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.zed_url == "zed://ssh/default.coder"
|
||||
error_message = "zed_url did not match expected default URL"
|
||||
@@ -19,6 +33,20 @@ run "adds_folder" {
|
||||
folder = "/foo/bar"
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_workspace.me
|
||||
values = {
|
||||
name = "default"
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_workspace_owner.me
|
||||
values = {
|
||||
name = "default"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.zed_url == "zed://ssh/default.coder/foo/bar"
|
||||
error_message = "zed_url did not include provided folder path"
|
||||
@@ -33,8 +61,54 @@ run "adds_agent_name" {
|
||||
agent_name = "myagent"
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_workspace.me
|
||||
values = {
|
||||
name = "default"
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_workspace_owner.me
|
||||
values = {
|
||||
name = "default"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.zed_url == "zed://ssh/myagent.default.default.coder"
|
||||
error_message = "zed_url did not include agent_name in hostname"
|
||||
}
|
||||
}
|
||||
|
||||
run "settings_base64_encoding" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
settings = jsonencode({
|
||||
theme = "dark"
|
||||
fontSize = 14
|
||||
})
|
||||
}
|
||||
|
||||
# Verify settings are base64 encoded (eyJ = base64 prefix for JSON starting with {")
|
||||
assert {
|
||||
condition = can(regex("SETTINGS_B64='eyJ", coder_script.zed_settings.script))
|
||||
error_message = "settings should be base64 encoded in the script"
|
||||
}
|
||||
}
|
||||
|
||||
run "empty_settings" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
settings = ""
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = can(regex("SETTINGS_B64=''", coder_script.zed_settings.script))
|
||||
error_message = "empty settings should result in empty SETTINGS_B64"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ variable "cache_repo_secret_name" {
|
||||
type = string
|
||||
}
|
||||
|
||||
data "kubernetes_secret" "cache_repo_dockerconfig_secret" {
|
||||
data "kubernetes_secret_v1" "cache_repo_dockerconfig_secret" {
|
||||
count = var.cache_repo_secret_name == "" ? 0 : 1
|
||||
metadata {
|
||||
name = var.cache_repo_secret_name
|
||||
@@ -166,7 +166,7 @@ locals {
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
"ENVBUILDER_INIT_SCRIPT" : replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal"),
|
||||
"ENVBUILDER_FALLBACK_IMAGE" : data.coder_parameter.fallback_image.value,
|
||||
"ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")),
|
||||
"ENVBUILDER_DOCKER_CONFIG_BASE64" : base64encode(try(data.kubernetes_secret_v1.cache_repo_dockerconfig_secret[0].data[".dockerconfigjson"], "")),
|
||||
"ENVBUILDER_PUSH_IMAGE" : var.cache_repo == "" ? "" : "true"
|
||||
# You may need to adjust this if you get an error regarding deleting files when building the workspace.
|
||||
# For example, when testing in KinD, it was necessary to set `/product_name` and `/product_uuid` in
|
||||
@@ -186,7 +186,7 @@ resource "envbuilder_cached_image" "cached" {
|
||||
insecure = var.insecure_cache_repo
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "workspaces" {
|
||||
resource "kubernetes_persistent_volume_claim_v1" "workspaces" {
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace.me.id)}-workspaces"
|
||||
namespace = var.namespace
|
||||
@@ -217,10 +217,10 @@ resource "kubernetes_persistent_volume_claim" "workspaces" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "main" {
|
||||
resource "kubernetes_deployment_v1" "main" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
depends_on = [
|
||||
kubernetes_persistent_volume_claim.workspaces
|
||||
kubernetes_persistent_volume_claim_v1.workspaces
|
||||
]
|
||||
wait_for_rollout = false
|
||||
metadata {
|
||||
@@ -300,7 +300,7 @@ resource "kubernetes_deployment" "main" {
|
||||
volume {
|
||||
name = "workspaces"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.workspaces.metadata.0.name
|
||||
claim_name = kubernetes_persistent_volume_claim_v1.workspaces.metadata.0.name
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,22 +106,20 @@ module "code-server" {
|
||||
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
|
||||
version = "~> 1.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
order = 1
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
}
|
||||
|
||||
# See https://registry.coder.com/modules/coder/jetbrains
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder/jetbrains/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = "main"
|
||||
folder = "/home/coder"
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder"
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "home" {
|
||||
resource "kubernetes_persistent_volume_claim_v1" "home" {
|
||||
metadata {
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
|
||||
namespace = var.namespace
|
||||
@@ -137,7 +135,7 @@ resource "kubernetes_persistent_volume_claim" "home" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_pod" "main" {
|
||||
resource "kubernetes_pod_v1" "main" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
|
||||
metadata {
|
||||
@@ -284,7 +282,7 @@ resource "kubernetes_pod" "main" {
|
||||
volume {
|
||||
name = "home"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
|
||||
claim_name = kubernetes_persistent_volume_claim_v1.home.metadata.0.name
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ resource "coder_app" "code-server" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "home" {
|
||||
resource "kubernetes_persistent_volume_claim_v1" "home" {
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace.me.id}-home"
|
||||
namespace = var.namespace
|
||||
@@ -222,10 +222,10 @@ resource "kubernetes_persistent_volume_claim" "home" {
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "main" {
|
||||
resource "kubernetes_deployment_v1" "main" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
depends_on = [
|
||||
kubernetes_persistent_volume_claim.home
|
||||
kubernetes_persistent_volume_claim_v1.home
|
||||
]
|
||||
wait_for_rollout = false
|
||||
metadata {
|
||||
@@ -316,7 +316,7 @@ resource "kubernetes_deployment" "main" {
|
||||
volume {
|
||||
name = "home"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
|
||||
claim_name = kubernetes_persistent_volume_claim_v1.home.metadata.0.name
|
||||
read_only = false
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 460 B |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 330 KiB |
@@ -0,0 +1,22 @@
|
||||
---
|
||||
display_name: "Michael Orlov"
|
||||
bio: "Platform Engineer specializing in cloud infrastructure, DevOps automation, and developer experience tools"
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "harleylrn"
|
||||
support_email: "michael.orlov@gmail.com"
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Michael Orlov
|
||||
|
||||
Platform Engineer specializing in cloud infrastructure, DevOps automation, and developer experience tools. Contributing modules and templates to enhance developer productivity in Coder workspaces.
|
||||
|
||||
## Modules
|
||||
|
||||
### kiro-cli
|
||||
|
||||
AI-powered coding assistant integration for Coder workspaces with MCP (Model Context Protocol) support and task reporting capabilities.
|
||||
|
||||
## About
|
||||
|
||||
I focus on creating tools and integrations that improve developer experience and productivity. My contributions to the Coder Registry aim to provide seamless integration of modern development tools and AI assistants into cloud development environments.
|
||||
@@ -0,0 +1,396 @@
|
||||
---
|
||||
display_name: Kiro CLI
|
||||
description: Run Kiro CLI in your workspace to access AI coding assistant with MCP integration and task reporting.
|
||||
icon: ../../../../.icons/kiro.svg
|
||||
verified: true
|
||||
tags: [agent, ai, kiro, kiro-cli, tasks]
|
||||
---
|
||||
|
||||
# Kiro CLI
|
||||
|
||||
Run [Kiro CLI](https://kiro.dev/) in your workspace to access AI coding assistant. This module provides a complete integration with Coder workspaces, including automatic installation, MCP (Model Context Protocol) integration for task reporting, and support for custom pre/post install scripts.
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
|
||||
# Required: Authentication tarball (see below for generation)
|
||||
auth_tarball = <<-EOF
|
||||
base64encoded-tarball
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **zstd** - Required for compressing the authentication tarball
|
||||
- **Ubuntu/Debian**: `sudo apt-get install zstd`
|
||||
- **RHEL/CentOS/Fedora**: `sudo yum install zstd` or `sudo dnf install zstd`
|
||||
- **auth_tarball** - Required for installation and authentication
|
||||
|
||||
### Authentication Tarball
|
||||
|
||||
You must generate an authenticated Kiro CLI tarball on another machine where you have successfully logged in:
|
||||
|
||||
```bash
|
||||
# 1. Install Kiro CLI and login on your local machine
|
||||
kiro-cli login
|
||||
|
||||
# 2. Generate the authentication tarball
|
||||
cd ~/.local/share/kiro-cli
|
||||
tar -c . | zstd | base64 -w 0
|
||||
```
|
||||
|
||||
Copy the output and use it as the `auth_tarball` variable.
|
||||
|
||||
## Detailed Authentication Setup
|
||||
|
||||
**Step 1: Install Kiro CLI locally**
|
||||
|
||||
- Download from [Kiro CLI](https://kiro.dev/)
|
||||
- Follow the installation instructions for your platform
|
||||
|
||||
**Step 2: Authenticate**
|
||||
|
||||
```bash
|
||||
kiro-cli login
|
||||
```
|
||||
|
||||
Complete the authentication process in your browser.
|
||||
|
||||
**Step 3: Generate tarball**
|
||||
|
||||
```bash
|
||||
cd ~/.local/share/kiro-cli
|
||||
tar -c . | zstd | base64 -w 0 > /tmp/kiro-cli-auth.txt
|
||||
```
|
||||
|
||||
**Step 4: Use in Terraform**
|
||||
|
||||
```tf
|
||||
variable "kiro_cli_auth_tarball" {
|
||||
type = string
|
||||
sensitive = true
|
||||
default = "PASTE_YOUR_TARBALL_HERE"
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - Regenerate the tarball if you logout or re-authenticate
|
||||
> - Each user needs their own authentication tarball
|
||||
> - Keep the tarball secure as it contains authentication credentials
|
||||
|
||||
### Coder Tasks Integration
|
||||
|
||||
To enable integration with [Coder Tasks](https://coder.com/docs/ai-coder/tasks), you need to define the `coder_task` data source, create the `coder_ai_task` resource, and configure the module with the task prompt.
|
||||
|
||||
```tf
|
||||
data "coder_task" "me" {}
|
||||
|
||||
module "kiro-cli" {
|
||||
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
ai_prompt = data.coder_task.me.prompt
|
||||
trust_all_tools = true
|
||||
|
||||
# Task reporting configuration
|
||||
report_tasks = true
|
||||
|
||||
# Enable CLI app alongside web app
|
||||
cli_app = true
|
||||
web_app_display_name = "Kiro CLI"
|
||||
cli_app_display_name = "Kiro CLI"
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "task" {
|
||||
count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0
|
||||
app_id = module.kiro-cli[count.index].task_app_id
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - The `data "coder_task" "me" {}` data source provides the task prompt and enabled state
|
||||
> - The module count is controlled by `data.coder_task.me.enabled` to only create when a task is active
|
||||
> - The `coder_ai_task` resource links the module's task reporting to Coder's task system
|
||||
> - The `ai_prompt` is passed from `data.coder_task.me.prompt`
|
||||
> - Without this configuration, `coder_ai_task` resources will not function properly
|
||||
>
|
||||
> **_Security Notice_**
|
||||
> In order to allow the tasks flow non-interactively all the tools are trusted
|
||||
> This flag bypasses standard permission checks and allows Kiro CLI broader access to your system than normally permitted.
|
||||
> While this enables more functionality, it also means Kiro CLI can potentially execute commands with the same privileges as the user running it.
|
||||
> Use this module only in trusted environments and be aware of the security implications.
|
||||
|
||||
### Default System Prompt
|
||||
|
||||
The module includes a simple system prompt that instructs Kiro CLI:
|
||||
|
||||
```
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
```
|
||||
|
||||
You can customize this behavior by providing your own system prompt via the `system_prompt` variable.
|
||||
|
||||
### Default Coder MCP Instructions
|
||||
|
||||
The module includes specific instructions for the Coder MCP server integration that are separate from the system prompt:
|
||||
|
||||
```
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message
|
||||
- Be granular If you are investigating with multiple steps report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input
|
||||
- Use "state": "complete" only when finished with a task
|
||||
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing
|
||||
- Include clear and actionable steps for the user
|
||||
- Be less than 160 characters in length
|
||||
```
|
||||
|
||||
You can customize these instructions by providing your own via the `coder_mcp_instructions` variable.
|
||||
|
||||
## Default Agent Configuration
|
||||
|
||||
The module includes a default agent configuration template that provides a comprehensive setup for Kiro CLI integration:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "agent",
|
||||
"description": "This is an default agent config",
|
||||
"prompt": "${system_prompt}",
|
||||
"mcpServers": {},
|
||||
"tools": ["read", "write", "shell", "aws", "@coder", "knowledge"],
|
||||
"toolAliases": {},
|
||||
"allowedTools": ["read", "@coder"],
|
||||
"resources": [
|
||||
"file://KiroQ.md",
|
||||
"file://README.md",
|
||||
"file://.kiro/steering/**/*.md"
|
||||
],
|
||||
"hooks": {},
|
||||
"toolsSettings": {},
|
||||
"useLegacyMcpJson": true
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Details:
|
||||
|
||||
- **Tools Available:** File operations (`read`, `write`), shell execution (`shell`), AWS CLI (`aws`), Coder MCP integration (`@coder`), and knowledge base access (`knowledge`)
|
||||
- **@coder Tool:** Enables Coder MCP integration for task reporting (`coder_report_task` and related tools)
|
||||
- **Allowed Tools:** By default, only `read` and `@coder` are allowed (can be customized for security)
|
||||
- **Resources:** Access to documentation and rule files in the workspace
|
||||
- **MCP Servers:** Empty by default, can be configured via `agent_config` variable
|
||||
- **System Prompt:** Dynamically populated from the `system_prompt` variable
|
||||
- **Legacy MCP:** Uses legacy MCP JSON format for compatibility
|
||||
|
||||
You can override this configuration by providing your own JSON via the `agent_config` variable.
|
||||
|
||||
### Agent Name Configuration
|
||||
|
||||
The module automatically extracts the agent name from the `"name"` field in the `agent_config` JSON and uses it for:
|
||||
|
||||
- **Configuration File:** Saves the agent config as `~/.kiro/agents/{agent_name}.json`
|
||||
- **Default Agent:** Sets the agent as the default using `q settings chat.defaultAgent {agent_name}`
|
||||
- **MCP Integration:** Associates the Coder MCP server with the specified agent name
|
||||
|
||||
If no custom `agent_config` is provided, the default agent name "agent" is used.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
}
|
||||
```
|
||||
|
||||
This example will:
|
||||
|
||||
1. Download and install Kiro CLI latest version
|
||||
2. Extract authentication tarball to ~/.local/share/kiro-cli
|
||||
3. Configure Coder MCP integration for task reporting
|
||||
4. Create default agent configuration file
|
||||
5. Start Kiro CLI in /home/coder directory
|
||||
6. Provide web interface through AgentAPI
|
||||
|
||||
> [!IMPORTANT]
|
||||
> By default `write` tool is not allowed, which will pause the task execution
|
||||
> and will wait for the prompt to approve its usage.
|
||||
> To avoid this, and allow the normal task flow, user has two options:
|
||||
>
|
||||
> - Change the parameter `trust_all_tools` value to `true` (default to `false`)
|
||||
> OR
|
||||
> - Provide your own agent configuration with the tools of your choice allowed
|
||||
|
||||
### With Custom AI Prompt
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
|
||||
trust_all_tools = true
|
||||
}
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **_Security Notice_**
|
||||
> In order to allow the tasks flow non-interactively all the tools are trusted
|
||||
> This flag bypasses standard permission checks and allows Kiro CLI broader access to your system than normally permitted.
|
||||
> While this enables more functionality, it also means Kiro CLI can potentially execute commands with the same privileges as the user running it.
|
||||
> Use this module only in trusted environments and be aware of the security implications.
|
||||
|
||||
### With Custom Pre/Post Install Scripts
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
|
||||
pre_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
echo "Setting up custom environment..."
|
||||
# Install additional dependencies
|
||||
sudo apt-get update && sudo apt-get install -y zstd
|
||||
EOT
|
||||
|
||||
post_install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
echo "Configuring Kiro CLI settings..."
|
||||
# Custom configuration commands
|
||||
kiro-cli settings chat.model claude-3-sonnet
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
### Specific Version Installation
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
kiro_cli_version = "1.14.0" # Specific version
|
||||
install_kiro_cli = true
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Agent Configuration
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
|
||||
agent_config = <<-EOT
|
||||
{
|
||||
"name": "custom-agent",
|
||||
"description": "Custom Kiro CLI agent for my workspace",
|
||||
"prompt": "You are a specialized DevOps assistant...",
|
||||
"tools": ["read", "write", "shell", "aws"]
|
||||
}
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom AgentAPI Configuration
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
|
||||
# AgentAPI configuration for environments without wildcard access url. https://coder.com/docs/admin/setup#wildcard-access-url
|
||||
agentapi_chat_based_path = true
|
||||
agentapi_version = "v0.10.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Air-Gapped Installation
|
||||
|
||||
For environments without direct internet access, you can host Kiro CLI installation files internally and configure the module to use your internal repository:
|
||||
|
||||
```tf
|
||||
module "kiro-cli" {
|
||||
source = "registry.coder.com/harleylrn/kiro-cli/coder"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
auth_tarball = var.kiro_cli_auth_tarball
|
||||
|
||||
# Point to internal artifact repository
|
||||
kiro_install_url = "https://artifacts.internal.corp/kiro-cli-releases"
|
||||
|
||||
# Use specific version available in your repository
|
||||
kiro_cli_version = "latest"
|
||||
}
|
||||
```
|
||||
|
||||
**Prerequisites for Air-Gapped Setup:**
|
||||
|
||||
1. Download Kiro CLI installation files from the official source and host them internally
|
||||
2. Maintain the same directory structure: `{base_url}/{version}/kirocli-{arch}-linux.zip`
|
||||
3. Ensure both architectures are available:
|
||||
- `kirocli-x86_64-linux.zip` for Intel/AMD systems
|
||||
- `kirocli-aarch64-linux.zip` for ARM systems
|
||||
4. Configure network access from Coder workspaces to your internal repository
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Authentication issues:**
|
||||
|
||||
- Regenerate the auth tarball on your local machine
|
||||
- Ensure the tarball is properly base64 encoded
|
||||
- Check that the original authentication is still valid
|
||||
|
||||
**MCP integration not working:**
|
||||
|
||||
- Verify that AgentAPI is installed (`install_agentapi = true`)
|
||||
- Check that the Coder agent is properly configured
|
||||
- Review the system prompt configuration
|
||||
|
||||
## Outputs
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------------------------------------------------- | ----------- |
|
||||
| <a name="output_task_app_id"></a> [task_app_id](#output_task_app_id) | n/a |
|
||||
@@ -0,0 +1,372 @@
|
||||
run "required_variables" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
}
|
||||
}
|
||||
|
||||
run "minimal_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdA==" # base64 "test"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable not configured correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should be 'kiro-cli'"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 1: Basic Usage – No Autonomous Use of Q
|
||||
# Using vanilla Kubernetes Deployment Template configuration
|
||||
run "test_case_1_basic_usage" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
}
|
||||
|
||||
# Q is installed and authenticated
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable should be configured for basic usage"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should be 'kiro-cli' for basic usage"
|
||||
}
|
||||
|
||||
# AgentAPI is installed and configured (default behavior)
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created for authentication"
|
||||
}
|
||||
|
||||
# Foundational configuration applied
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated with foundational configuration"
|
||||
}
|
||||
|
||||
# No additional parameters required (using defaults)
|
||||
assert {
|
||||
condition = local.agent_name == "agent"
|
||||
error_message = "Default agent name should be 'agent' when no custom config provided"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 2: Autonomous Usage – Autonomous Use of Q
|
||||
# AI prompt passed through from external source (Tasks interface or Issue Tracker CI)
|
||||
run "test_case_2_autonomous_usage" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
ai_prompt = "Help me set up a Python FastAPI project with proper testing structure"
|
||||
}
|
||||
|
||||
# Q is installed and authenticated
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable should be configured for autonomous usage"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should be 'kiro-cli' for autonomous usage"
|
||||
}
|
||||
|
||||
# AgentAPI is installed and configured
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created for autonomous usage"
|
||||
}
|
||||
|
||||
# Foundational configuration for all components applied
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated for autonomous usage"
|
||||
}
|
||||
|
||||
# AI prompt is configured
|
||||
assert {
|
||||
condition = local.full_prompt == "Help me set up a Python FastAPI project with proper testing structure"
|
||||
error_message = "AI prompt should be configured correctly for autonomous usage"
|
||||
}
|
||||
|
||||
# Default agent name when no custom config
|
||||
assert {
|
||||
condition = local.agent_name == "agent"
|
||||
error_message = "Default agent name should be 'agent' for autonomous usage"
|
||||
}
|
||||
}
|
||||
|
||||
# Test Case 3: Extended Configuration – Parameter Validation and File Rendering
|
||||
# Validates extended configuration options and parameter application
|
||||
run "test_case_3_extended_configuration" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
kiro_cli_version = "1.14.1"
|
||||
kiro_install_url = "https://desktop-release.q.us-east-1.amazonaws.com"
|
||||
install_kiro_cli = true
|
||||
install_agentapi = true
|
||||
agentapi_version = "v0.6.0"
|
||||
trust_all_tools = true
|
||||
ai_prompt = "Help me create a production-grade TypeScript monorepo with testing and deployment"
|
||||
system_prompt = "You are a helpful software assistant working in a secure enterprise environment"
|
||||
pre_install_script = "echo 'Pre-install setup'"
|
||||
post_install_script = "echo 'Post-install cleanup'"
|
||||
agent_config = jsonencode({
|
||||
name = "production-agent"
|
||||
description = "Production Kiro CLI agent for enterprise environment"
|
||||
prompt = "You are a helpful software assistant working in a secure enterprise environment"
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read"]
|
||||
resources = ["file://KiroQ.md", "file://README.md", "file://.kiro/steering/**/*.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
# All installation parameters are applied correctly
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug should be configured correctly with extended parameters"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
|
||||
error_message = "Auth tarball should be configured correctly with extended parameters"
|
||||
}
|
||||
|
||||
# Custom agent configuration is loaded and referenced correctly
|
||||
assert {
|
||||
condition = local.agent_name == "production-agent"
|
||||
error_message = "Agent name should be extracted from custom agent config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Custom agent config should be processed correctly"
|
||||
}
|
||||
|
||||
# AI prompt and system prompt are configured
|
||||
assert {
|
||||
condition = local.full_prompt == "Help me create a production-grade TypeScript monorepo with testing and deployment"
|
||||
error_message = "AI prompt should be configured correctly in extended configuration"
|
||||
}
|
||||
|
||||
# Pre-install and post-install scripts are provided
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated correctly for extended configuration"
|
||||
}
|
||||
}
|
||||
|
||||
run "full_config" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
install_kiro_cli = true
|
||||
install_agentapi = true
|
||||
agentapi_version = "v0.5.0"
|
||||
kiro_cli_version = "latest"
|
||||
trust_all_tools = true
|
||||
ai_prompt = "Build a web application"
|
||||
auth_tarball = "dGVzdA=="
|
||||
order = 1
|
||||
group = "AI Tools"
|
||||
icon = "/icon/custom-kiro-cli.svg"
|
||||
pre_install_script = "echo 'pre-install'"
|
||||
post_install_script = "echo 'post-install'"
|
||||
agent_config = jsonencode({
|
||||
name = "test-agent"
|
||||
description = "Test agent configuration"
|
||||
prompt = "You are a helpful AI assistant for testing."
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read"]
|
||||
resources = ["file://KiroQ.md", "file://README.md", "file://.kiro/steering/**/*.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug environment variable not configured correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should be 'kiro-cli'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 1
|
||||
error_message = "Auth tarball environment variable should be created when provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "auth_tarball_environment" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = "dGVzdEF1dGhUYXJiYWxs" # base64 "testAuthTarball"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].name == "KIRO_CLI_AUTH_TARBALL"
|
||||
error_message = "Auth tarball environment variable name should be 'KIRO_CLI_AUTH_TARBALL'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.auth_tarball[0].value == "dGVzdEF1dGhUYXJiYWxs"
|
||||
error_message = "Auth tarball environment variable value should match input"
|
||||
}
|
||||
}
|
||||
|
||||
run "empty_auth_tarball" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
auth_tarball = ""
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.auth_tarball) == 0
|
||||
error_message = "Auth tarball environment variable should not be created when empty"
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_system_prompt" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
system_prompt = "Custom system prompt for testing"
|
||||
}
|
||||
|
||||
# Test that the system prompt is used in the agent config template
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated with custom system prompt"
|
||||
}
|
||||
}
|
||||
|
||||
run "install_options" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
install_kiro_cli = false
|
||||
install_agentapi = false
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].name == "CODER_MCP_APP_STATUS_SLUG"
|
||||
error_message = "Status slug should still be configured even when install options are disabled"
|
||||
}
|
||||
}
|
||||
|
||||
run "version_configuration" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
kiro_cli_version = "2.15.0"
|
||||
agentapi_version = "v0.4.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.status_slug[0].value == "kiro-cli"
|
||||
error_message = "Status slug value should remain 'kiro-cli' regardless of version"
|
||||
}
|
||||
}
|
||||
|
||||
# Additional test for agent name extraction
|
||||
run "agent_name_extraction" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
agent_config = jsonencode({
|
||||
name = "custom-enterprise-agent"
|
||||
description = "Custom enterprise agent configuration"
|
||||
prompt = "You are a custom enterprise AI assistant."
|
||||
mcpServers = {}
|
||||
tools = ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"]
|
||||
toolAliases = {}
|
||||
allowedTools = ["fs_read", "fs_write"]
|
||||
resources = ["file://README.md"]
|
||||
hooks = {}
|
||||
toolsSettings = {}
|
||||
useLegacyMcpJson = true
|
||||
})
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.agent_name == "custom-enterprise-agent"
|
||||
error_message = "Agent name should be extracted correctly from custom agent config"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be processed correctly"
|
||||
}
|
||||
}
|
||||
|
||||
# Test for JSON encoding validation
|
||||
run "json_encoding_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
workdir = "/tmp/test-workdir"
|
||||
system_prompt = "Multi-line\nsystem prompt\nwith newlines"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.system_prompt) > 0
|
||||
error_message = "System prompt should be JSON encoded correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(local.agent_config) > 0
|
||||
error_message = "Agent config should be generated correctly with multi-line system prompt"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
findResourceInstance,
|
||||
} from "~test";
|
||||
import path from "path";
|
||||
|
||||
const moduleDir = path.resolve(__dirname);
|
||||
|
||||
// Always provide agent_config to bypass template parsing issues
|
||||
const baseAgentConfig = JSON.stringify({
|
||||
name: "test-agent",
|
||||
description: "Test agent configuration",
|
||||
prompt: "You are a helpful AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md", "file://.kiro/steering/**/*.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const requiredVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
agent_config: baseAgentConfig,
|
||||
workdir: "/tmp/test-workdir",
|
||||
};
|
||||
|
||||
const fullConfigVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
install_kiro_cli: true,
|
||||
install_agentapi: true,
|
||||
agentapi_version: "v0.6.0",
|
||||
kiro_cli_version: "1.14.1",
|
||||
kiro_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
|
||||
trust_all_tools: false,
|
||||
ai_prompt: "Build a comprehensive test suite",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
order: 1,
|
||||
group: "AI Tools",
|
||||
icon: "/icon/custom-kiro-cli.svg",
|
||||
pre_install_script: "echo 'Starting pre-install'",
|
||||
post_install_script: "echo 'Completed post-install'",
|
||||
agent_config: baseAgentConfig,
|
||||
};
|
||||
|
||||
describe("kiro-cli module v1.0.0", async () => {
|
||||
await runTerraformInit(moduleDir);
|
||||
|
||||
// Test Case 1: Basic Usage – No Autonomous Use of Q
|
||||
// Matches CDES-203 Test Case #1: Basic Usage
|
||||
it("Test Case 1: Basic Usage - No Autonomous Use of Q", async () => {
|
||||
const basicUsageVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, basicUsageVars);
|
||||
|
||||
// Q is installed and authenticated
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
|
||||
// AgentAPI is installed and configured (default behavior)
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL");
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
|
||||
// Foundational configuration for all components is applied
|
||||
// No additional parameters are required for the module to work
|
||||
// Using the terminal application and Q chat returns a functional interface
|
||||
});
|
||||
|
||||
// Test Case 2: Autonomous Usage – Autonomous Use of Q
|
||||
// Matches CDES-203 Test Case 2: Autonomous Usage
|
||||
it("Test Case 2: Autonomous Usage - Autonomous Use of Q", async () => {
|
||||
const autonomousUsageVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
ai_prompt:
|
||||
"Help me set up a Python FastAPI project with proper testing structure",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, autonomousUsageVars);
|
||||
|
||||
// Q is installed and authenticated
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
|
||||
// AgentAPI is installed and configured
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL");
|
||||
|
||||
// AI prompt is passed through from external source
|
||||
// The Chat interface functions as required
|
||||
// The Tasks interface functions as required
|
||||
// The template can be invoked from GitHub integration as expected
|
||||
});
|
||||
|
||||
// Test Case 3: Extended Configuration – Parameter Validation and File Rendering
|
||||
// Matches CDES-203 Test Case 3: Extended Configuration
|
||||
it("Test Case 3: Extended Configuration - Parameter Validation and File Rendering", async () => {
|
||||
const extendedConfigVars = {
|
||||
agent_id: "dummy-agent-id",
|
||||
workdir: "/tmp/test-workdir",
|
||||
auth_tarball: "dGVzdEF1dGhUYXJiYWxs", // base64 "testAuthTarball"
|
||||
kiro_cli_version: "1.14.1",
|
||||
kiro_install_url: "https://desktop-release.q.us-east-1.amazonaws.com",
|
||||
install_kiro_cli: true,
|
||||
install_agentapi: true,
|
||||
agentapi_version: "v0.6.0",
|
||||
trust_all_tools: true,
|
||||
ai_prompt:
|
||||
"Help me create a production-grade TypeScript monorepo with testing and deployment",
|
||||
system_prompt:
|
||||
"You are a helpful software assistant working in a secure enterprise environment",
|
||||
pre_install_script: "echo 'Pre-install setup'",
|
||||
post_install_script: "echo 'Post-install cleanup'",
|
||||
agent_config: JSON.stringify({
|
||||
name: "production-agent",
|
||||
description: "Production Kiro CLI agent for enterprise environment",
|
||||
prompt:
|
||||
"You are a helpful software assistant working in a secure enterprise environment",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: [
|
||||
"file://KiroQ.md",
|
||||
"file://README.md",
|
||||
"file://.kiro/steering/**/*.md",
|
||||
],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
}),
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, extendedConfigVars);
|
||||
|
||||
// All installation steps execute in the correct order
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
|
||||
// auth_tarball is unpacked and used as expected
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
|
||||
// agent_config is rendered correctly, and the name field is used as the agent's name
|
||||
// The specified ai_prompt and system_prompt are respected by the Q agent
|
||||
// Tools are trusted globally if trust_all_tools = true
|
||||
// Files and scripts execute in proper sequence
|
||||
});
|
||||
|
||||
// 1. Basic functionality test (replaces testRequiredVariables)
|
||||
it("works with required variables", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
|
||||
// 2. Environment variables are created correctly
|
||||
it("creates required environment variables", async () => {
|
||||
const state = await runTerraformApply(moduleDir, fullConfigVars);
|
||||
|
||||
// Check status slug environment variable
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
|
||||
// Check auth tarball environment variable
|
||||
const authTarballEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeDefined();
|
||||
expect(authTarballEnv.name).toBe("KIRO_CLI_AUTH_TARBALL");
|
||||
expect(authTarballEnv.value).toBe("dGVzdEF1dGhUYXJiYWxs");
|
||||
});
|
||||
|
||||
// 3. Empty auth tarball handling
|
||||
it("handles empty auth tarball correctly", async () => {
|
||||
const noAuthVars = {
|
||||
...requiredVars,
|
||||
auth_tarball: "",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, noAuthVars);
|
||||
|
||||
// Auth tarball environment variable should not be created when empty
|
||||
const authTarballEnv = state.resources?.find(
|
||||
(r) => r.type === "coder_env" && r.name === "auth_tarball",
|
||||
);
|
||||
expect(authTarballEnv).toBeUndefined();
|
||||
});
|
||||
|
||||
// 4. Status slug is always created
|
||||
it("creates status slug environment variable", async () => {
|
||||
const state = await runTerraformApply(moduleDir, requiredVars);
|
||||
|
||||
// Status slug should always be configured
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.name).toBe("CODER_MCP_APP_STATUS_SLUG");
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
|
||||
// 5. Install options configuration
|
||||
it("respects install option flags", async () => {
|
||||
const noInstallVars = {
|
||||
...requiredVars,
|
||||
install_kiro_cli: false,
|
||||
install_agentapi: false,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, noInstallVars);
|
||||
|
||||
// Status slug should still be configured even when install options are disabled
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
|
||||
// 6. Configurable installation URL
|
||||
it("uses configurable kiro_install_url parameter", async () => {
|
||||
const customUrlVars = {
|
||||
...requiredVars,
|
||||
kiro_install_url: "https://internal-mirror.company.com/kiro-cli",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, customUrlVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 7. Version configuration
|
||||
it("uses specified versions", async () => {
|
||||
const versionVars = {
|
||||
...requiredVars,
|
||||
kiro_cli_version: "1.14.1",
|
||||
agentapi_version: "v0.6.0",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, versionVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 8. UI configuration options
|
||||
it("supports UI customization options", async () => {
|
||||
const uiCustomVars = {
|
||||
...requiredVars,
|
||||
order: 5,
|
||||
group: "Custom AI Tools",
|
||||
icon: "/icon/custom-kiro-cli-icon.svg",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, uiCustomVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 9. Pre and post install scripts
|
||||
it("supports pre and post install scripts", async () => {
|
||||
const scriptVars = {
|
||||
...requiredVars,
|
||||
pre_install_script: "echo 'Pre-install setup'",
|
||||
post_install_script: "echo 'Post-install cleanup'",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, scriptVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 10. Valid agent_config JSON with different agent name
|
||||
it("handles valid agent_config JSON with custom agent name", async () => {
|
||||
const customAgentConfig = JSON.stringify({
|
||||
name: "production-agent",
|
||||
description: "Production Kiro CLI agent",
|
||||
prompt: "You are a production AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const validAgentConfigVars = {
|
||||
...requiredVars,
|
||||
agent_config: customAgentConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, validAgentConfigVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 11. Air-gapped installation support
|
||||
it("supports air-gapped installation with custom URL", async () => {
|
||||
const airGappedVars = {
|
||||
...requiredVars,
|
||||
kiro_install_url: "https://artifacts.internal.corp/kiro-cli-releases",
|
||||
kiro_cli_version: "1.14.1",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, airGappedVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 12. Trust all tools configuration
|
||||
it("handles trust_all_tools configuration", async () => {
|
||||
const trustVars = {
|
||||
...requiredVars,
|
||||
trust_all_tools: true,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, trustVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 13. AI prompt configuration
|
||||
it("handles AI prompt configuration", async () => {
|
||||
const promptVars = {
|
||||
...requiredVars,
|
||||
ai_prompt: "Create a comprehensive test suite for the application",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, promptVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 14. Agent config with minimal structure
|
||||
it("handles minimal agent config structure", async () => {
|
||||
const minimalAgentConfig = JSON.stringify({
|
||||
name: "minimal-agent",
|
||||
description: "Minimal agent config",
|
||||
prompt: "You are a minimal AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read"],
|
||||
resources: ["file://README.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const minimalVars = {
|
||||
...requiredVars,
|
||||
agent_config: minimalAgentConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, minimalVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
});
|
||||
|
||||
// 15. JSON encoding validation for system prompts with newlines
|
||||
it("handles system prompts with newlines correctly", async () => {
|
||||
const multilinePromptVars = {
|
||||
...requiredVars,
|
||||
system_prompt: "Multi-line\nsystem prompt\nwith newlines",
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, multilinePromptVars);
|
||||
|
||||
// Should create the basic resources without JSON parsing errors
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
|
||||
// 16. Agent name extraction from custom config
|
||||
it("extracts agent name from custom configuration correctly", async () => {
|
||||
const customNameConfig = JSON.stringify({
|
||||
name: "enterprise-production-agent",
|
||||
description: "Enterprise production agent configuration",
|
||||
prompt: "You are an enterprise production AI assistant.",
|
||||
mcpServers: {},
|
||||
tools: ["fs_read", "fs_write", "execute_bash", "use_aws", "knowledge"],
|
||||
toolAliases: {},
|
||||
allowedTools: ["fs_read", "fs_write", "execute_bash"],
|
||||
resources: ["file://README.md", "file://.kiro/steering/**/*.md"],
|
||||
hooks: {},
|
||||
toolsSettings: {},
|
||||
useLegacyMcpJson: true,
|
||||
});
|
||||
|
||||
const customNameVars = {
|
||||
...requiredVars,
|
||||
agent_config: customNameConfig,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(moduleDir, customNameVars);
|
||||
|
||||
// Should create the basic resources
|
||||
const statusSlugEnv = findResourceInstance(
|
||||
state,
|
||||
"coder_env",
|
||||
"status_slug",
|
||||
);
|
||||
expect(statusSlugEnv).toBeDefined();
|
||||
expect(statusSlugEnv.value).toBe("kiro-cli");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,275 @@
|
||||
# Improved kiro-cli module main.tf
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/kiro.svg"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Kiro CLI"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Kiro CLI"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Kiro CLI"
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
type = string
|
||||
description = "The initial task prompt to send to Kiro CLI."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run before installing Kiro CLI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Optional script to run after installing Kiro CLI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
}
|
||||
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Kiro CLI in."
|
||||
}
|
||||
|
||||
variable "install_kiro_cli" {
|
||||
type = bool
|
||||
description = "Whether to install Kiro CLI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "kiro_cli_version" {
|
||||
type = string
|
||||
description = "The version of Kiro CLI to install."
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "kiro_install_url" {
|
||||
type = string
|
||||
description = "Base URL for Kiro CLI installation downloads."
|
||||
default = "https://desktop-release.q.us-east-1.amazonaws.com"
|
||||
}
|
||||
|
||||
variable "trust_all_tools" {
|
||||
type = bool
|
||||
description = "Whether to trust all tools in Kiro CLI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "The system prompt to use for Kiro CLI. This should instruct the agent how to do task reporting."
|
||||
default = <<-EOT
|
||||
You are a helpful Coding assistant. Aim to autonomously investigate
|
||||
and solve issues the user gives you and test your work, whenever possible.
|
||||
Avoid shortcuts like mocking tests. When you get stuck, you can ask the user
|
||||
but opt for autonomy.
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "coder_mcp_instructions" {
|
||||
type = string
|
||||
description = "Instructions for the Coder MCP server integration. This defines how the agent should report tasks to Coder."
|
||||
default = <<-EOT
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
When reporting tasks you MUST follow these EXACT instructions:
|
||||
- IMMEDIATELY report status after receiving ANY user message
|
||||
- Be granular If you are investigating with multiple steps report each step to coder.
|
||||
|
||||
Task state MUST be one of the following:
|
||||
- Use "state": "working" when actively processing WITHOUT needing additional user input
|
||||
- Use "state": "complete" only when finished with a task
|
||||
- Use "state": "failure" when you need ANY user input lack sufficient details or encounter blockers.
|
||||
|
||||
Task summaries MUST:
|
||||
- Include specifics about what you're doing
|
||||
- Include clear and actionable steps for the user
|
||||
- Be less than 160 characters in length
|
||||
EOT
|
||||
}
|
||||
|
||||
variable "auth_tarball" {
|
||||
type = string
|
||||
description = "Base64 encoded, zstd compressed tarball of a pre-authenticated ~/.local/share/kiro-cli directory."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "agent_config" {
|
||||
type = string
|
||||
description = "Optional Agent configuration JSON for Kiro CLI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "agentapi_chat_based_path" {
|
||||
type = bool
|
||||
description = "Whether to use chat-based path for AgentAPI.Required if CODER_WILDCARD_ACCESS_URL is not defined in coder deployment"
|
||||
default = false
|
||||
}
|
||||
|
||||
# Expose status slug to the agent environment
|
||||
resource "coder_env" "status_slug" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = local.app_slug
|
||||
count = var.report_tasks ? 1 : 0
|
||||
}
|
||||
|
||||
# Expose auth tarball as environment variable for install script
|
||||
resource "coder_env" "auth_tarball" {
|
||||
count = var.auth_tarball != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "KIRO_CLI_AUTH_TARBALL"
|
||||
value = var.auth_tarball
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "kiro-cli"
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".kiro"
|
||||
system_prompt = jsonencode(replace(var.system_prompt, "/[\r\n]/", ""))
|
||||
coder_mcp_instructions = jsonencode(replace(var.coder_mcp_instructions, "/[\r\n]/", ""))
|
||||
|
||||
# Create default agent config structure
|
||||
default_agent_config = templatefile("${path.module}/templates/agent-config.json.tpl", {
|
||||
system_prompt = local.system_prompt
|
||||
})
|
||||
|
||||
# Choose the JSON string: use var.agent_config if provided, otherwise encode default
|
||||
agent_config = var.agent_config != null ? var.agent_config : local.default_agent_config
|
||||
|
||||
# Extract agent name from the selected config
|
||||
agent_name = try(jsondecode(local.agent_config).name, "agent")
|
||||
|
||||
full_prompt = var.ai_prompt != null ? var.ai_prompt : ""
|
||||
|
||||
server_chat_parameters = var.agentapi_chat_based_path ? "--chat-base-path /@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}.${var.agent_id}/apps/${local.app_slug}/chat" : ""
|
||||
}
|
||||
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = local.workdir
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = var.web_app_display_name
|
||||
cli_app = var.cli_app
|
||||
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
|
||||
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_TRUST_ALL_TOOLS='${var.trust_all_tools}' \
|
||||
ARG_AI_PROMPT='${base64encode(local.full_prompt)}' \
|
||||
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
|
||||
ARG_WORKDIR='${var.workdir}' \
|
||||
ARG_SERVER_PARAMETERS="${local.server_chat_parameters}" \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_INSTALL='${var.install_kiro_cli}' \
|
||||
ARG_VERSION='${var.kiro_cli_version}' \
|
||||
ARG_KIRO_INSTALL_URL='${var.kiro_install_url}' \
|
||||
ARG_AUTH_TARBALL='${var.auth_tarball}' \
|
||||
ARG_AGENT_CONFIG='${local.agent_config != null ? base64encode(local.agent_config) : ""}' \
|
||||
ARG_AGENT_NAME='${local.agent_name}' \
|
||||
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_CODER_MCP_INSTRUCTIONS='${base64encode(local.coder_mcp_instructions)}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
|
||||
output "task_app_id" {
|
||||
value = module.agentapi.task_app_id
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/bin/bash
|
||||
# Install script for kiro-cli module
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Inputs
|
||||
ARG_INSTALL=${ARG_INSTALL:-true}
|
||||
ARG_VERSION=${ARG_VERSION:-latest}
|
||||
ARG_KIRO_INSTALL_URL=${ARG_KIRO_INSTALL_URL:-https://desktop-release.q.us-east-1.amazonaws.com}
|
||||
ARG_AUTH_TARBALL=${ARG_AUTH_TARBALL:-}
|
||||
ARG_AGENT_CONFIG=${ARG_AGENT_CONFIG:-}
|
||||
ARG_AGENT_NAME=${ARG_AGENT_NAME:-default-agent}
|
||||
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.kiro}
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
|
||||
ARG_CODER_MCP_INSTRUCTIONS=${ARG_CODER_MCP_INSTRUCTIONS:-}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
|
||||
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
|
||||
|
||||
# Decode base64 inputs
|
||||
ARG_AGENT_CONFIG_DECODED=""
|
||||
if [ -n "$ARG_AGENT_CONFIG" ]; then
|
||||
ARG_AGENT_CONFIG_DECODED=$(echo -n "$ARG_AGENT_CONFIG" | base64 -d)
|
||||
fi
|
||||
|
||||
ARG_CODER_MCP_INSTRUCTIONS_DECODED=""
|
||||
if [ -n "$ARG_CODER_MCP_INSTRUCTIONS" ]; then
|
||||
ARG_CODER_MCP_INSTRUCTIONS_DECODED=$(echo -n "$ARG_CODER_MCP_INSTRUCTIONS" | base64 -d)
|
||||
fi
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "install: $ARG_INSTALL"
|
||||
echo "version: $ARG_VERSION"
|
||||
echo "kiro_install_url: $ARG_KIRO_INSTALL_URL"
|
||||
echo "agent_name: $ARG_AGENT_NAME"
|
||||
echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
|
||||
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
|
||||
echo "auth_tarball_provided: ${ARG_AUTH_TARBALL}"
|
||||
echo "report_tasks: ${ARG_REPORT_TASKS}"
|
||||
echo "--------------------------------"
|
||||
|
||||
# Install Kiro CLI if requested
|
||||
function install_kiro_cli() {
|
||||
if [ "$ARG_INSTALL" = "true" ]; then
|
||||
echo "Installing Kiro CLI..."
|
||||
PREV_DIR="$PWD"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cd "$TMP_DIR"
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
"x86_64")
|
||||
KIRO_URL="${ARG_KIRO_INSTALL_URL}/${ARG_VERSION}/kirocli-x86_64-linux.zip"
|
||||
;;
|
||||
"aarch64" | "arm64")
|
||||
KIRO_URL="${ARG_KIRO_INSTALL_URL}/${ARG_VERSION}/kirocli-aarch64-linux.zip"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unsupported architecture: $ARCH. Kiro CLI only supports x86_64 and arm64."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Downloading Kiro CLI for $ARCH from $KIRO_URL..."
|
||||
curl --proto '=https' --tlsv1.2 -sSf "$KIRO_URL" -o "kirocli.zip"
|
||||
unzip kirocli.zip
|
||||
./kirocli/install.sh --no-confirm
|
||||
cd "$PREV_DIR"
|
||||
rm -rf "$TMP_DIR"
|
||||
|
||||
# Ensure binaries are discoverable; create stable symlink to kiro-cli
|
||||
CANDIDATES=(
|
||||
"$(command -v kiro-cli || true)"
|
||||
"$HOME/.local/bin/kiro-cli"
|
||||
)
|
||||
FOUND_BIN=""
|
||||
for c in "${CANDIDATES[@]}"; do
|
||||
if [ -n "$c" ] && [ -x "$c" ]; then
|
||||
FOUND_BIN="$c"
|
||||
break
|
||||
fi
|
||||
done
|
||||
export PATH="$PATH:$HOME/.local/bin"
|
||||
echo "Installed Kiro CLI at: $(command -v kiro-cli || true) (resolved: $FOUND_BIN)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract authentication tarball
|
||||
function extract_auth_tarball() {
|
||||
if [ -n "$ARG_AUTH_TARBALL" ]; then
|
||||
echo "Extracting auth tarball..."
|
||||
|
||||
if ! command_exists zstd; then
|
||||
echo "Error: zstd is required to extract the authentication tarball but is not installed."
|
||||
echo "Please install zstd using the pre_install_script parameter."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PREV_DIR="$PWD"
|
||||
echo "$ARG_AUTH_TARBALL" | base64 -d > /tmp/auth.tar.zst
|
||||
rm -rf ~/.local/share/kiro-cli
|
||||
mkdir -p ~/.local/share/kiro-cli
|
||||
cd ~/.local/share/kiro-cli
|
||||
tar -I zstd -xf /tmp/auth.tar.zst
|
||||
rm /tmp/auth.tar.zst
|
||||
cd "$PREV_DIR"
|
||||
echo "Extracted auth tarball to ~/.local/share/kiro-cli"
|
||||
else
|
||||
echo "Warning: No auth tarball provided. Kiro CLI may require manual authentication."
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure MCP integration and create agent
|
||||
function configure_agent() {
|
||||
# Create Kiro CLI agent configuration directory
|
||||
AGENT_CONFIG_DIR="$HOME/.kiro/agents"
|
||||
mkdir -p "$AGENT_CONFIG_DIR"
|
||||
ALLOWED_TOOLS="coder_get_workspace\,coder_create_workspace\,coder_list_workspaces\,coder_list_templates\,coder_template_version_parameters\,coder_get_authenticated_user\,coder_create_workspace_build\,coder_create_template_version\,coder_get_workspace_agent_logs\,coder_get_workspace_build_logs\,coder_get_template_version_logs\,coder_update_template_active_version\,coder_upload_tar_file\,coder_create_template\,coder_delete_template\,coder_workspace_bash"
|
||||
if [ -n "$ARG_AGENT_CONFIG_DECODED" ]; then
|
||||
echo "Applying custom MCP configuration..."
|
||||
# Use agent name as filename for the configuration
|
||||
echo "$ARG_AGENT_CONFIG_DECODED" > "$AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
|
||||
echo "Custom configuration saved to $AGENT_CONFIG_DIR/${ARG_AGENT_NAME}.json"
|
||||
fi
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
echo "Configuring Kiro CLI to report tasks via Coder MCP..."
|
||||
kiro-cli mcp add --name coder \
|
||||
--command "coder" \
|
||||
--agent "$ARG_AGENT_NAME" \
|
||||
--args "exp,mcp,server,--allowed-tools,coder_report_task,--instructions,'$ARG_CODER_MCP_INSTRUCTIONS_DECODED'" \
|
||||
--env "CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG}" \
|
||||
--env "CODER_MCP_AI_AGENTAPI_URL=http://localhost:3284" \
|
||||
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
|
||||
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
|
||||
--force || echo "Warning: Failed to add Coder MCP server"
|
||||
else
|
||||
kiro-cli mcp add --name coder \
|
||||
--command "coder" \
|
||||
--agent "$ARG_AGENT_NAME" \
|
||||
--args "exp,mcp,server,--allowed-tools,coder_report_task" \
|
||||
--env "CODER_AGENT_URL=${CODER_AGENT_URL}" \
|
||||
--env "CODER_AGENT_TOKEN=${CODER_AGENT_TOKEN}" \
|
||||
--force || echo "Warning: Failed to add Coder MCP server"
|
||||
fi
|
||||
echo "Added Coder MCP server into $ARG_AGENT_NAME in Kiro CLI configuration"
|
||||
kiro-cli settings chat.defaultAgent "$ARG_AGENT_NAME"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
install_kiro_cli
|
||||
extract_auth_tarball
|
||||
configure_agent
|
||||
|
||||
echo "Kiro CLI installation and configuration complete!"
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# Start script for kiro-cli module
|
||||
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Decode inputs
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
|
||||
ARG_TRUST_ALL_TOOLS=${ARG_TRUST_ALL_TOOLS:-true}
|
||||
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.kiro}
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true}
|
||||
ARG_SERVER_PARAMETERS=${ARG_SERVER_PARAMETERS:-""}
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "ai_prompt: $ARG_AI_PROMPT"
|
||||
echo "trust_all_tools: $ARG_TRUST_ALL_TOOLS"
|
||||
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
|
||||
echo "workdir: $ARG_WORKDIR"
|
||||
echo "report_tasks: ${ARG_REPORT_TASKS}"
|
||||
echo "--------------------------------"
|
||||
|
||||
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
|
||||
|
||||
# Find Kiro CLI
|
||||
if command_exists kiro-cli; then
|
||||
KIRO_CMD=kiro-cli
|
||||
elif [ -x "$HOME/.local/bin/kiro-cli" ]; then
|
||||
KIRO_CMD="$HOME/.local/bin/kiro-cli"
|
||||
else
|
||||
echo "Error: Kiro CLI not found. Install it or set install_kiro_cli=true."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
# Set up environment
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# Build command arguments
|
||||
ARGS=(chat)
|
||||
|
||||
if [ "$ARG_TRUST_ALL_TOOLS" = "true" ]; then
|
||||
ARGS+=(--trust-all-tools)
|
||||
fi
|
||||
|
||||
# Log and run with agentapi integration
|
||||
printf "Running: %q %s\n" "$KIRO_CMD" "$(printf '%q ' "${ARGS[@]}")"
|
||||
|
||||
# If we have an AI prompt, we need to handle it specially
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" == "true" ]; then
|
||||
PROMPT="Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_AI_PROMPT"
|
||||
else
|
||||
PROMPT="$ARG_AI_PROMPT"
|
||||
fi
|
||||
ARGS+=("$PROMPT")
|
||||
fi
|
||||
|
||||
# Use agentapi to manage the interactive session with initial prompt
|
||||
agentapi server ${ARG_SERVER_PARAMETERS} --term-width 67 --term-height 1190 -- "$KIRO_CMD" "${ARGS[@]}"
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "agent",
|
||||
"description": "This is an default agent config",
|
||||
"prompt": ${system_prompt},
|
||||
"mcpServers": {},
|
||||
"tools": [
|
||||
"read",
|
||||
"write",
|
||||
"shell",
|
||||
"aws",
|
||||
"@coder",
|
||||
"knowledge"
|
||||
],
|
||||
"toolAliases": {},
|
||||
"allowedTools": [
|
||||
"read",
|
||||
"@coder"
|
||||
],
|
||||
"resources": [
|
||||
"file://KiroQ.md",
|
||||
"file://README.md",
|
||||
"file://.kiro/steering/**/*.md"
|
||||
],
|
||||
"hooks": {},
|
||||
"toolsSettings": {},
|
||||
"useLegacyMcpJson": true
|
||||
}
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,15 @@
|
||||
---
|
||||
display_name: "Mossy Lion"
|
||||
bio: "Tinkerer, exploring European cloud providers"
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "mossylion"
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Mossy Lion
|
||||
|
||||
Exploring European cloud providers. Usually find me outdoors but if not, somewhere deep in Kubernetes and infra
|
||||
|
||||
## Templates
|
||||
|
||||
- **scaleway-instance**: Scaleway workspace instance with persistent home directory
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
display_name: "Scaleway Instance"
|
||||
description: "A workspace spun up on a Scaleway Instance"
|
||||
icon: "../../../../.icons/scaleway.svg"
|
||||
verified: false
|
||||
tags: ["scaleway", "vm", "linux"]
|
||||
---
|
||||
|
||||
# Scaleway Instance Template
|
||||
|
||||
This template provisions Coder workspaces on [Scaleway](https://www.scaleway.com/) cloud instances with full customization options for regions, instance types, operating systems, and storage configurations.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-region support**: Choose from France (Paris), Netherlands (Amsterdam), or Poland (Warsaw)
|
||||
- **Flexible instance sizing**: Wide range of instance types from development to high-performance computing
|
||||
- **Multiple OS options**: Debian 12/13, Ubuntu 24.04, and Fedora 41
|
||||
- **Customizable storage**: Adjustable disk size with configurable IOPS
|
||||
- **IPv4 and IPv6 networking**: Dual-stack IP configuration for enhanced connectivity
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Scaleway Account Setup
|
||||
|
||||
1. Create a [Scaleway account](https://console.scaleway.com/)
|
||||
2. Create a new project or use an existing one
|
||||
3. Generate API credentials:
|
||||
- Go to **IAM** > **API Keys** in the Scaleway Console
|
||||
- Create a new API key
|
||||
- Note down the **Access Key** and **Secret Key**
|
||||
- Copy your **Project ID** from the project settings
|
||||
- Give permissions for **BlockStorageFullAccess**, **ProjectReadOnly**, **InstancesFullAccess** as a starting point
|
||||
|
||||
## Architecture
|
||||
|
||||
This template creates the following resources for each workspace:
|
||||
|
||||
### Persistent Resources
|
||||
|
||||
- **Block Volume**: Mounted as user's home directory (preserves all data, configs, and projects)
|
||||
|
||||
### Ephemeral Resources (destroyed when workspace stops)
|
||||
|
||||
- **Scaleway Instance**: Virtual machine created fresh on each workspace start
|
||||
- **IPv4 Address**: Routed IPv4 address assigned dynamically
|
||||
- **IPv6 Address**: Routed IPv6 address assigned dynamically
|
||||
- **Cloud-init Configuration**: Automated setup of the Coder agent and persistent storage mounting
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Region Selection
|
||||
|
||||
Choose from three available regions:
|
||||
|
||||
- **France - Paris (fr-par)**: Default, lowest latency for European users
|
||||
- **Netherlands - Amsterdam (nl-ams)**: Alternative European location
|
||||
- **Poland - Warsaw (pl-waw)**: Eastern European option
|
||||
|
||||
### Instance Types
|
||||
|
||||
The template supports a comprehensive range of Scaleway instance types:
|
||||
|
||||
#### Development Instances
|
||||
|
||||
- **STARDUST1-S**: 1 CPU, 1GB RAM - Basic development
|
||||
- **DEV1-S/M/L/XL**: 2-4 CPUs, 2-12GB RAM - Standard development
|
||||
|
||||
#### Production Instances
|
||||
|
||||
- **ENT1 Series**: 2-96 CPUs, 8-384GB RAM - Enterprise workloads
|
||||
- **GP1 Series**: 4-48 CPUs, 16-256GB RAM - General purpose
|
||||
- **PRO2 Series**: 2-32 CPUs, 8-128GB RAM - Professional workloads
|
||||
|
||||
#### Specialized Instances
|
||||
|
||||
- **L4 Series**: GPU-enabled instances for AI/ML workloads
|
||||
- **COPARM1 Series**: ARM64 architecture for specific use cases
|
||||
|
||||
### Operating System Options
|
||||
|
||||
- **Debian 13 (Trixie)**: Latest Debian release
|
||||
- **Debian 12 (Bookworm)**: Stable Debian LTS
|
||||
- **Ubuntu 24.04 (Noble)**: Latest Ubuntu LTS
|
||||
- **Fedora 41**: Cutting-edge features and packages
|
||||
|
||||
### Storage Configuration
|
||||
|
||||
- **Home Directory Size**: 10-500GB adjustable via slider (your entire home directory)
|
||||
- **IOPS**: 5,000 or 15,000 IOPS options for performance tuning
|
||||
|
||||
## Template Components
|
||||
|
||||
### Included Tools
|
||||
|
||||
- **VS Code Server**: Browser-based IDE with full extension support
|
||||
- **System Monitoring**: CPU, RAM, and disk usage metrics
|
||||
- **Dotfiles Support**: Automatic dotfiles synchronization on workspace start
|
||||
- **Custom Environment Variables**: Pre-configured welcome message
|
||||
|
||||
### Cloud-init Setup
|
||||
|
||||
The template uses cloud-init for:
|
||||
|
||||
- Automatic Coder agent installation and configuration
|
||||
- User account setup with proper permissions
|
||||
- Persistent home directory mounting (automatic disk partitioning and filesystem creation)
|
||||
- Development tools initialization
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a Workspace
|
||||
|
||||
1. **Select Template**: Choose "Scaleway Instance" from your Coder templates
|
||||
2. **Configure Region**: Pick your preferred Scaleway region
|
||||
3. **Choose Instance**: Select instance type based on your performance needs
|
||||
4. **Select OS**: Pick your preferred operating system
|
||||
5. **Set Home Directory Size**: Adjust storage size (10-500GB) for your persistent home directory
|
||||
6. **Create**: Launch your workspace
|
||||
|
||||
### Managing Costs
|
||||
|
||||
- **VM instances are destroyed** when workspace stops (zero compute costs when not in use)
|
||||
- **IP addresses are released** when workspace stops (no static IP charges)
|
||||
- **Home directory persists** on dedicated block volume (small storage cost only)
|
||||
- **Fresh OS** on each workspace start with persistent user data
|
||||
- Choose appropriate instance sizes for your workload requirements
|
||||
|
||||
## Customization
|
||||
|
||||
### Extending the Template
|
||||
|
||||
You can customize this template by:
|
||||
|
||||
1. **Adding Software**: Modify cloud-init scripts to install additional tools
|
||||
2. **Custom Modules**: Include additional Coder modules from the registry
|
||||
3. **Network Configuration**: Adjust security groups or network settings
|
||||
4. **Startup Scripts**: Add custom initialization logic
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Updating Instance Types
|
||||
|
||||
To update the available instance types, regenerate the `scaleway-config.json` file:
|
||||
|
||||
```bash
|
||||
scw instance server-type list -o json | jq 'map({name, cpu, gpu, ram, arch})' > scaleway-config.json.json
|
||||
```
|
||||
|
||||
This pulls the latest instance types from Scaleway and formats them for use in the template.
|
||||
|
||||
## References
|
||||
|
||||
- [Scaleway Documentation](https://www.scaleway.com/en/docs/)
|
||||
- [Scaleway Instance Types](https://www.scaleway.com/en/pricing/#instances)
|
||||
- [Coder Templates Documentation](https://coder.com/docs/templates)
|
||||
- [Terraform Scaleway Provider](https://registry.terraform.io/providers/scaleway/scaleway/latest/docs)
|
||||
@@ -0,0 +1,35 @@
|
||||
#cloud-config
|
||||
cloud_final_modules:
|
||||
- [scripts-user, always]
|
||||
hostname: ${hostname}
|
||||
users:
|
||||
- name: ${linux_user}
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
shell: /bin/bash
|
||||
|
||||
# Setup persistent storage disk
|
||||
disk_setup:
|
||||
/dev/sdb:
|
||||
table_type: gpt
|
||||
layout: true
|
||||
overwrite: false
|
||||
|
||||
fs_setup:
|
||||
- label: persistent-home
|
||||
filesystem: ext4
|
||||
device: /dev/sdb1
|
||||
partition: auto
|
||||
|
||||
mounts:
|
||||
- ["/dev/sdb1", "/home/${linux_user}", "ext4", "defaults", "0", "2"]
|
||||
|
||||
# Fix ownership after mounting
|
||||
runcmd:
|
||||
- chown -R ${linux_user}:${linux_user} /home/${linux_user}
|
||||
- chmod 755 /home/${linux_user}
|
||||
|
||||
# Automatically grow the partition
|
||||
growpart:
|
||||
mode: auto
|
||||
devices: ['/']
|
||||
ignore_growroot_disabled: false
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
sudo -u '${linux_user}' sh -c 'export CODER_AGENT_TOKEN="${coder_agent_token}"; ${init_script}'
|
||||
@@ -0,0 +1,337 @@
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "~> 2"
|
||||
}
|
||||
scaleway = {
|
||||
source = "scaleway/scaleway"
|
||||
version = "~> 2"
|
||||
}
|
||||
cloudinit = {
|
||||
source = "hashicorp/cloudinit"
|
||||
version = "~> 2"
|
||||
}
|
||||
}
|
||||
required_version = ">= 1.0"
|
||||
}
|
||||
|
||||
provider "scaleway" {
|
||||
access_key = var.access_key
|
||||
secret_key = var.secret_key
|
||||
region = data.coder_parameter.region.value
|
||||
}
|
||||
|
||||
locals {
|
||||
hostname = lower(data.coder_workspace.me.name)
|
||||
linux_user = "coder"
|
||||
}
|
||||
|
||||
data "cloudinit_config" "user_data" {
|
||||
gzip = false
|
||||
base64_encode = false
|
||||
|
||||
boundary = "//"
|
||||
|
||||
part {
|
||||
filename = "cloud-config.yaml"
|
||||
content_type = "text/cloud-config"
|
||||
|
||||
content = templatefile("${path.module}/cloud-init/cloud-config.yaml.tftpl", {
|
||||
hostname = local.hostname
|
||||
linux_user = local.linux_user
|
||||
})
|
||||
}
|
||||
|
||||
part {
|
||||
filename = "userdata.sh"
|
||||
content_type = "text/x-shellscript"
|
||||
|
||||
content = templatefile("${path.module}/cloud-init/userdata.sh.tftpl", {
|
||||
linux_user = local.linux_user
|
||||
init_script = coder_agent.main.init_script
|
||||
coder_agent_token = coder_agent.main.token
|
||||
})
|
||||
}
|
||||
}
|
||||
data "coder_provisioner" "me" {}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
arch = local.selected_arch
|
||||
os = data.coder_provisioner.me.os
|
||||
auth = "token"
|
||||
|
||||
startup_script = <<-EOT
|
||||
set -e
|
||||
|
||||
# Install additional tools or run commands at workspace startup
|
||||
# Uncomment and customize as needed:
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y build-essential
|
||||
EOT
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
metadata {
|
||||
display_name = "Disk Usage"
|
||||
key = "1_disk_usage"
|
||||
script = "coder stat disk --path /home/${local.linux_user}"
|
||||
interval = 600
|
||||
timeout = 30
|
||||
}
|
||||
}
|
||||
|
||||
module "code-server" {
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
version = "1.3.1"
|
||||
agent_id = coder_agent.main.id
|
||||
order = 1
|
||||
folder = "/home/${local.linux_user}"
|
||||
}
|
||||
|
||||
# Runs a script at workspace start/stop or on a cron schedule
|
||||
# details: https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script
|
||||
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/dotfiles/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
|
||||
resource "coder_metadata" "workspace_info" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = scaleway_instance_server.workspace[0].id
|
||||
|
||||
item {
|
||||
key = "region"
|
||||
value = data.coder_parameter.region.value
|
||||
}
|
||||
item {
|
||||
key = "instance type"
|
||||
value = scaleway_instance_server.workspace[0].type
|
||||
}
|
||||
item {
|
||||
key = "image"
|
||||
value = data.coder_parameter.base_image.value
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_metadata" "volume_info" {
|
||||
resource_id = scaleway_block_volume.persistent_storage.id
|
||||
|
||||
item {
|
||||
key = "size"
|
||||
value = "${scaleway_block_volume.persistent_storage.size_in_gb} GiB"
|
||||
}
|
||||
item {
|
||||
key = "iops"
|
||||
value = scaleway_block_volume.persistent_storage.iops
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "region" {
|
||||
name = "Scaleway Region"
|
||||
description = "Region to deploy server into"
|
||||
type = "string"
|
||||
default = "fr-par"
|
||||
option {
|
||||
name = "France - Paris (fr-par)"
|
||||
value = "fr-par"
|
||||
icon = "/emojis/1f1eb-1f1f7.png"
|
||||
}
|
||||
option {
|
||||
name = "Netherlands - Amsterdam (nl-ams)"
|
||||
value = "nl-ams"
|
||||
icon = "/emojis/1f1f3-1f1f1.png"
|
||||
}
|
||||
option {
|
||||
name = "Poland - Warsaw (pl-waw)"
|
||||
value = "pl-waw"
|
||||
icon = "/emojis/1f1f5-1f1f1.png"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "base_image" {
|
||||
name = "Image"
|
||||
description = "Which base image would you like to use?"
|
||||
type = "string"
|
||||
form_type = "radio"
|
||||
default = "debian_trixie"
|
||||
|
||||
option {
|
||||
name = "Debian 13 (Trixie)"
|
||||
value = "debian_trixie"
|
||||
icon = "/icon/debian.svg"
|
||||
}
|
||||
|
||||
option {
|
||||
name = "Debian 12 (Bookworm)"
|
||||
value = "debian_bookworm"
|
||||
icon = "/icon/debian.svg"
|
||||
}
|
||||
|
||||
option {
|
||||
name = "Ubuntu 24.04 (Noble)"
|
||||
value = "ubuntu_noble"
|
||||
icon = "/icon/ubuntu.svg"
|
||||
}
|
||||
|
||||
option {
|
||||
name = "Fedora 41"
|
||||
value = "fedora_41"
|
||||
icon = "/icon/fedora.svg"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "root_volume_size" {
|
||||
name = "Root Volume Size"
|
||||
description = "Size of the OS/boot disk in GB"
|
||||
type = "number"
|
||||
form_type = "slider"
|
||||
default = "20"
|
||||
order = 7
|
||||
validation {
|
||||
min = 10
|
||||
max = 1000
|
||||
monotonic = "increasing"
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "disk_size" {
|
||||
name = "Persistent Storage Size"
|
||||
description = "Size of the additional persistent storage volume in GB"
|
||||
type = "number"
|
||||
form_type = "slider"
|
||||
default = "10"
|
||||
order = 8
|
||||
validation {
|
||||
min = 10
|
||||
max = 500
|
||||
monotonic = "increasing"
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
scaleway_config_raw = jsondecode(file("${path.module}/scaleway-config.json"))
|
||||
|
||||
scaleway_instance_options = {
|
||||
for instance in local.scaleway_config_raw :
|
||||
instance.name => {
|
||||
name = "${instance.name} (${instance.cpu} CPU, ${instance.gpu} GPU, ${floor(instance.ram / 1073741824)} GB RAM)"
|
||||
value = instance.name
|
||||
}
|
||||
}
|
||||
|
||||
instance_arch_map = {
|
||||
for instance in local.scaleway_config_raw :
|
||||
instance.name => instance.arch
|
||||
}
|
||||
|
||||
# Convert Scaleway arch format to Coder arch format
|
||||
selected_arch = local.instance_arch_map[data.coder_parameter.instance_size.value] == "x86_64" ? "amd64" : local.instance_arch_map[data.coder_parameter.instance_size.value]
|
||||
}
|
||||
|
||||
data "coder_parameter" "instance_size" {
|
||||
name = "instance_size"
|
||||
display_name = "Instance Size"
|
||||
description = "Which Instance Size should be used?"
|
||||
default = "DEV1-M"
|
||||
type = "string"
|
||||
icon = "/icon/memory.svg"
|
||||
mutable = false
|
||||
form_type = "dropdown"
|
||||
|
||||
dynamic "option" {
|
||||
for_each = local.scaleway_instance_options
|
||||
content {
|
||||
name = option.value.name
|
||||
value = option.value.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_parameter" "volume_iops" {
|
||||
name = "Volume IOPS"
|
||||
description = "IOPS to provision for disk"
|
||||
type = "number"
|
||||
default = 5000
|
||||
option {
|
||||
name = "5000"
|
||||
value = 5000
|
||||
}
|
||||
option {
|
||||
name = "15000"
|
||||
value = 15000
|
||||
}
|
||||
}
|
||||
|
||||
resource "scaleway_instance_server" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}"
|
||||
type = data.coder_parameter.instance_size.value
|
||||
image = data.coder_parameter.base_image.value
|
||||
ip_ids = [scaleway_instance_ip.server_ip[0].id, scaleway_instance_ip.v4_server_ip[0].id]
|
||||
project_id = var.project_id
|
||||
user_data = {
|
||||
cloud-init = data.cloudinit_config.user_data.rendered
|
||||
}
|
||||
additional_volume_ids = [scaleway_block_volume.persistent_storage.id]
|
||||
|
||||
root_volume {
|
||||
size_in_gb = data.coder_parameter.root_volume_size.value
|
||||
}
|
||||
}
|
||||
|
||||
resource "scaleway_block_volume" "persistent_storage" {
|
||||
iops = data.coder_parameter.volume_iops.value
|
||||
name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-home"
|
||||
size_in_gb = data.coder_parameter.disk_size.value
|
||||
project_id = var.project_id
|
||||
}
|
||||
|
||||
|
||||
resource "scaleway_instance_ip" "server_ip" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
type = "routed_ipv6"
|
||||
project_id = var.project_id
|
||||
}
|
||||
|
||||
resource "scaleway_instance_ip" "v4_server_ip" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
type = "routed_ipv4"
|
||||
project_id = var.project_id
|
||||
}
|
||||
|
||||
variable "project_id" {
|
||||
type = string
|
||||
description = "ID of the project to deploy into"
|
||||
}
|
||||
|
||||
variable "access_key" {
|
||||
type = string
|
||||
description = "Access key to use to deploy"
|
||||
}
|
||||
|
||||
variable "secret_key" {
|
||||
type = string
|
||||
description = "Secret key to use to deploy"
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
[
|
||||
{
|
||||
"name": "COPARM1-2C-8G",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "COPARM1-4C-16G",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "COPARM1-8C-32G",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "COPARM1-16C-64G",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "COPARM1-32C-128G",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "arm64"
|
||||
},
|
||||
{
|
||||
"name": "DEV1-S",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 2147483648,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "DEV1-M",
|
||||
"cpu": 3,
|
||||
"gpu": 0,
|
||||
"ram": 4294967296,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "DEV1-L",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "DEV1-XL",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 12884901888,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-XXS",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-XS",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-S",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-M",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-L",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-XL",
|
||||
"cpu": 64,
|
||||
"gpu": 0,
|
||||
"ram": 274877906944,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "ENT1-2XL",
|
||||
"cpu": 96,
|
||||
"gpu": 0,
|
||||
"ram": 412316860416,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-XS",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-S",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-M",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-L",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "GP1-XL",
|
||||
"cpu": 48,
|
||||
"gpu": 0,
|
||||
"ram": 274877906944,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "L4-1-24G",
|
||||
"cpu": 8,
|
||||
"gpu": 1,
|
||||
"ram": 51539607552,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "L4-2-24G",
|
||||
"cpu": 16,
|
||||
"gpu": 2,
|
||||
"ram": 103079215104,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "L4-4-24G",
|
||||
"cpu": 32,
|
||||
"gpu": 4,
|
||||
"ram": 206158430208,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "L4-8-24G",
|
||||
"cpu": 64,
|
||||
"gpu": 8,
|
||||
"ram": 412316860416,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PLAY2-PICO",
|
||||
"cpu": 1,
|
||||
"gpu": 0,
|
||||
"ram": 2147483648,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PLAY2-NANO",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 4294967296,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PLAY2-MICRO",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-2C-4G",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 4294967296,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-2C-8G",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-2C-16G",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-4C-8G",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-4C-16G",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-2C-8G-WIN",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-4C-32G",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-8C-16G",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HN-3",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 4294967296,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-8C-32G",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-4C-16G-WIN",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-8C-64G",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-16C-32G",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HN-5",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-16C-64G",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-8C-32G-WIN",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HN-10",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-16C-128G",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-32C-64G",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-32C-128G",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-48C-96G",
|
||||
"cpu": 48,
|
||||
"gpu": 0,
|
||||
"ram": 103079215104,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-16C-64G-WIN",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-32C-256G",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 274877906944,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HC-64C-128G",
|
||||
"cpu": 64,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-48C-192G",
|
||||
"cpu": 48,
|
||||
"gpu": 0,
|
||||
"ram": 206158430208,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-64C-256G",
|
||||
"cpu": 64,
|
||||
"gpu": 0,
|
||||
"ram": 274877906944,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-48C-384G",
|
||||
"cpu": 48,
|
||||
"gpu": 0,
|
||||
"ram": 412316860416,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-32C-128G-WIN",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "POP2-HM-64C-512G",
|
||||
"cpu": 64,
|
||||
"gpu": 0,
|
||||
"ram": 549755813888,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-XXS",
|
||||
"cpu": 2,
|
||||
"gpu": 0,
|
||||
"ram": 8589934592,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-XS",
|
||||
"cpu": 4,
|
||||
"gpu": 0,
|
||||
"ram": 17179869184,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-S",
|
||||
"cpu": 8,
|
||||
"gpu": 0,
|
||||
"ram": 34359738368,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-M",
|
||||
"cpu": 16,
|
||||
"gpu": 0,
|
||||
"ram": 68719476736,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "PRO2-L",
|
||||
"cpu": 32,
|
||||
"gpu": 0,
|
||||
"ram": 137438953472,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "RENDER-S",
|
||||
"cpu": 10,
|
||||
"gpu": 1,
|
||||
"ram": 45097156608,
|
||||
"arch": "x86_64"
|
||||
},
|
||||
{
|
||||
"name": "STARDUST1-S",
|
||||
"cpu": 1,
|
||||
"gpu": 0,
|
||||
"ram": 1073741824,
|
||||
"arch": "x86_64"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
# Local and OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
*.tmp
|
||||
*.swp
|
||||
*.bak
|
||||
|
||||
# Terraform
|
||||
.terraform/
|
||||
.terraform.lock.hcl
|
||||
terraform.tfstate
|
||||
terraform.tfstate.backup
|
||||
crash.log
|
||||
|
||||
# Node / Bun / Python / other tool artifacts
|
||||
node_modules/
|
||||
bun.lockb
|
||||
package-lock.json
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cloud credentials and keys
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.json
|
||||
*.env
|
||||
.envrc
|
||||
aws-credentials
|
||||
gcp.json
|
||||
azure-creds.json
|
||||
|
||||
# Archives
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.tgz
|
||||
|
||||
# Workspace artifacts
|
||||
workspace/
|
||||
output/
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
@@ -0,0 +1,14 @@
|
||||
---
|
||||
display_name: "Noah Boyers"
|
||||
bio: "Cloud & DevOps engineer with an MBA, building scalable multi-cloud infrastructure."
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "noahboyers"
|
||||
linkedin: "https://www.linkedin.com/in/nboyers"
|
||||
website: "https://nobosoftware.com"
|
||||
support_email: "hello@nobosoftware.com"
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Noah Boyers
|
||||
|
||||
Cloud and DevOps engineer focused on scalable, secure, and automated infrastructure across AWS, Azure, and GCP.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
display_name: Cloud DevOps Workspace
|
||||
description: A multi-cloud DevOps workspace that runs on Amazon EKS and provides authenticated access to AWS, Azure, and GCP.
|
||||
icon: ../../../../.icons/cloud-devops.svg
|
||||
verified: false
|
||||
tags: [devops, kubernetes, aws, eks, multi-cloud, terraform, cdk, pulumi]
|
||||
---
|
||||
|
||||
# Cloud DevOps Workspace
|
||||
|
||||
A secure, company-standard DevOps environment for platform and cloud engineers.
|
||||
|
||||
This template deploys workspaces **into an existing Amazon EKS cluster** and provides developers with tools and credentials to work with **AWS, Azure, and GCP** from inside their workspace.
|
||||
|
||||
Supports multiple Infrastructure-as-Code frameworks — **Terraform**, **AWS CDK**, and **Pulumi** — for flexible, multi-cloud development.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-Cloud Ready** — authenticate to AWS, Azure, or GCP from a single workspace
|
||||
- **Runs on EKS** — leverages existing Kubernetes infrastructure for scaling and security
|
||||
- **IaC Tools Included** — Terraform, Terragrunt, CDK, Pulumi, tfsec, and more
|
||||
- **Secure Isolation** — each workspace runs in its own Kubernetes namespace
|
||||
- **Configurable Auth** — supports IRSA (AWS), Federated Identity (Azure), and WIF (GCP)
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | Description | Type | Default |
|
||||
| ------------------------------------------------------------- | --------------------------------------------------------------- | ------ | ----------- |
|
||||
| `host_cluster_name` | EKS cluster name where workspaces are deployed | string | — |
|
||||
| `iac_tool` | Infrastructure-as-Code framework (`terraform`, `cdk`, `pulumi`) | string | `terraform` |
|
||||
| `enable_aws` | Enable AWS authentication and tools | bool | `true` |
|
||||
| `enable_azure` | Enable Azure authentication and tools | bool | `false` |
|
||||
| `enable_gcp` | Enable GCP authentication and tools | bool | `false` |
|
||||
| `aws_access_key_id` / `aws_secret_access_key` | AWS credentials (optional) | string | `""` |
|
||||
| `azure_client_id` / `azure_client_secret` / `azure_tenant_id` | Azure credentials (optional) | string | `""` |
|
||||
| `gcp_service_account` | GCP Service Account JSON (optional) | string | `""` |
|
||||
|
||||
## Runtime Architecture
|
||||
|
||||
| Layer | Platform | Purpose |
|
||||
| ----------------------- | ------------------ | ------------------------------------------------------------ |
|
||||
| **Infrastructure** | Amazon EKS | Where Coder deploys and runs the workspaces |
|
||||
| **Workspace Container** | Ubuntu-based image | Developer environment (Terraform, CDK, Pulumi, CLIs) |
|
||||
| **Cloud Access** | AWS / Azure / GCP | Target environments for deploying infrastructure or services |
|
||||
|
||||
## Required Permissions and Setup Steps
|
||||
|
||||
This template **runs on EKS** but allows developers inside the workspace to authenticate with **AWS, Azure, or GCP** using their own credentials or service identities.
|
||||
|
||||
### Coder & Infrastructure (Admin Setup)
|
||||
|
||||
Your Coder deployment must have:
|
||||
|
||||
- Network access to an **existing EKS cluster**
|
||||
- The Coder Helm chart installed and healthy
|
||||
- Terraform configured with access to the EKS API
|
||||
|
||||
#### Minimum AWS IAM Permissions
|
||||
|
||||
For the identity running the template (Coder service account, Terraform runner, or user):
|
||||
|
||||
```json
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"eks:DescribeCluster",
|
||||
"eks:ListClusters",
|
||||
"sts:GetCallerIdentity",
|
||||
"sts:AssumeRole"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "~> 0.23"
|
||||
}
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.23"
|
||||
}
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Coder workspace context ---
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
# --- EKS connection ---
|
||||
data "aws_eks_cluster" "eks" {
|
||||
name = trimspace(var.host_cluster_name)
|
||||
}
|
||||
|
||||
|
||||
data "aws_eks_cluster_auth" "eks" {
|
||||
name = trimspace(var.host_cluster_name)
|
||||
}
|
||||
|
||||
provider "kubernetes" {
|
||||
host = data.aws_eks_cluster.eks.endpoint
|
||||
cluster_ca_certificate = base64decode(data.aws_eks_cluster.eks.certificate_authority[0].data)
|
||||
token = data.aws_eks_cluster_auth.eks.token
|
||||
}
|
||||
|
||||
# --- Namespace per workspace ---
|
||||
resource "kubernetes_namespace" "workspace" {
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
|
||||
labels = {
|
||||
"coder.workspace" = data.coder_workspace.me.name
|
||||
"coder.owner" = data.coder_workspace_owner.me.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- ServiceAccount (IRSA optional) ---
|
||||
resource "kubernetes_service_account" "workspace" {
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
|
||||
namespace = kubernetes_namespace.workspace.metadata[0].name
|
||||
|
||||
annotations = var.enable_aws && var.aws_role_arn != "" ? {
|
||||
"eks.amazonaws.com/role-arn" = var.aws_role_arn
|
||||
} : {}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Coder Agent definition ---
|
||||
resource "coder_agent" "main" {
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
|
||||
startup_script = file("${path.module}/scripts/setup-workspace.sh")
|
||||
|
||||
env = {
|
||||
# IaC tool & cloud toggles
|
||||
IAC_TOOL = var.iac_tool
|
||||
ENABLE_AWS = tostring(var.enable_aws)
|
||||
ENABLE_AZURE = tostring(var.enable_azure)
|
||||
ENABLE_GCP = tostring(var.enable_gcp)
|
||||
|
||||
# Developer credentials
|
||||
AWS_ACCESS_KEY_ID = var.aws_access_key_id
|
||||
AWS_SECRET_ACCESS_KEY = var.aws_secret_access_key
|
||||
AZURE_CLIENT_ID = var.azure_client_id
|
||||
AZURE_TENANT_ID = var.azure_tenant_id
|
||||
AZURE_CLIENT_SECRET = var.azure_client_secret
|
||||
GCP_SERVICE_ACCOUNT = var.gcp_service_account
|
||||
}
|
||||
}
|
||||
|
||||
# --- Kubernetes Pod (runs workspace container) ---
|
||||
resource "kubernetes_pod" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
|
||||
metadata {
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
|
||||
namespace = kubernetes_namespace.workspace.metadata[0].name
|
||||
labels = {
|
||||
"app" = "coder-workspace"
|
||||
"coder.owner" = data.coder_workspace_owner.me.name
|
||||
"coder.agent" = "true"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
service_account_name = kubernetes_service_account.workspace.metadata[0].name
|
||||
|
||||
container {
|
||||
name = "workspace"
|
||||
image = "codercom/enterprise-base:ubuntu"
|
||||
command = ["/bin/bash", "-c", coder_agent.main.init_script]
|
||||
|
||||
env {
|
||||
name = "CODER_AGENT_TOKEN"
|
||||
value = coder_agent.main.token
|
||||
}
|
||||
|
||||
resources {
|
||||
requests = { cpu = "500m", memory = "1Gi" }
|
||||
limits = { cpu = "2", memory = "4Gi" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [coder_agent.main]
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env bash
|
||||
# cloud-auth.sh — Multi-cloud auth helpers (source this file, don't execute)
|
||||
# Supports:
|
||||
# - AWS: access keys or IRSA (via pod SA)
|
||||
# - Azure: federated token or client secret
|
||||
# - GCP: service account JSON or Workload Identity Federation (KSA -> SA)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# -------- util --------
|
||||
_has() { command -v "$1" > /dev/null 2>&1; }
|
||||
_docker_ok() { _has docker && [[ -S /var/run/docker.sock ]]; }
|
||||
|
||||
cloud-auth-help() {
|
||||
cat << 'EOHELP'
|
||||
Multi-Cloud Authentication Helper — source this file:
|
||||
|
||||
source ~/workspace/cloud-auth.sh
|
||||
|
||||
Environment variables (read if set):
|
||||
|
||||
# Common toggles (optional)
|
||||
ENABLE_AWS=true|false
|
||||
ENABLE_AZURE=true|false
|
||||
ENABLE_GCP=true|false
|
||||
|
||||
# AWS
|
||||
AWS_REGION=us-west-2
|
||||
AWS_ACCESS_KEY_ID=...
|
||||
AWS_SECRET_ACCESS_KEY=...
|
||||
AWS_SESSION_TOKEN=... # optional (STS); if unset, IRSA/IMDS is used
|
||||
|
||||
# Azure
|
||||
AZURE_CLIENT_ID=...
|
||||
AZURE_TENANT_ID=...
|
||||
AZURE_CLIENT_SECRET=... # OR:
|
||||
AZURE_FEDERATED_TOKEN_FILE=/var/run/secrets/azure/tokens/azure-identity-token
|
||||
|
||||
# GCP
|
||||
GCP_PROJECT_ID=...
|
||||
# Option A (Service Account JSON):
|
||||
GCP_SERVICE_ACCOUNT='{ ... }'
|
||||
# Option B (Workload Identity Federation):
|
||||
GCP_WORKLOAD_IDENTITY_PROVIDER=projects/..../locations/global/workloadIdentityPools/.../providers/...
|
||||
# (uses KSA token at /var/run/secrets/kubernetes.io/serviceaccount/token)
|
||||
|
||||
Functions:
|
||||
|
||||
# AWS
|
||||
aws-login # ensures creds (keys or IRSA), sets region config if provided
|
||||
aws-check # prints caller identity
|
||||
aws-ecr-login # docker login to ECR (if docker socket present)
|
||||
|
||||
# Azure
|
||||
azure-login # SP login via federated token OR client secret
|
||||
azure-check # prints account info
|
||||
azure-acr-login # docker login to ACR (requires AZURE_ACR_NAME)
|
||||
|
||||
# GCP
|
||||
gcp-login # SA JSON or WIF
|
||||
gcp-check # prints active gcloud account & project
|
||||
gcp-gar-login # docker auth to GAR (requires GCP_REGION & PROJECT)
|
||||
|
||||
# Convenience
|
||||
multicloud-login # calls the per-cloud logins if toggles are true
|
||||
multicloud-check # calls the per-cloud checks
|
||||
EOHELP
|
||||
}
|
||||
|
||||
# -------- AWS --------
|
||||
aws-login() {
|
||||
[[ "${ENABLE_AWS:-true}" == "true" ]] || {
|
||||
echo "AWS disabled"
|
||||
return 0
|
||||
}
|
||||
if ! _has aws; then
|
||||
echo "aws CLI not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If access keys are present, write standard files; otherwise rely on IRSA/IMDS
|
||||
if [[ -n "${AWS_ACCESS_KEY_ID:-}" ]]; then
|
||||
mkdir -p "${HOME}/.aws"
|
||||
{
|
||||
echo "[default]"
|
||||
echo "aws_access_key_id=${AWS_ACCESS_KEY_ID}"
|
||||
echo "aws_secret_access_key=${AWS_SECRET_ACCESS_KEY:-}"
|
||||
[[ -n "${AWS_SESSION_TOKEN:-}" ]] && echo "aws_session_token=${AWS_SESSION_TOKEN}"
|
||||
} > "${HOME}/.aws/credentials"
|
||||
if [[ -n "${AWS_REGION:-}" ]]; then
|
||||
{
|
||||
echo "[default]"
|
||||
echo "region=${AWS_REGION}"
|
||||
} > "${HOME}/.aws/config"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate
|
||||
if ! aws sts get-caller-identity > /dev/null 2>&1; then
|
||||
echo "❌ AWS auth failed (neither valid keys nor IRSA available)"
|
||||
return 1
|
||||
fi
|
||||
echo "✅ AWS auth OK"
|
||||
}
|
||||
|
||||
aws-check() {
|
||||
_has aws || {
|
||||
echo "aws CLI not found"
|
||||
return 1
|
||||
}
|
||||
aws sts get-caller-identity
|
||||
}
|
||||
|
||||
aws-ecr-login() {
|
||||
_has aws || {
|
||||
echo "aws CLI not found"
|
||||
return 1
|
||||
}
|
||||
_docker_ok || {
|
||||
echo "ℹ️ docker socket not available; skipping ECR login"
|
||||
return 0
|
||||
}
|
||||
: "${AWS_REGION:=us-east-1}"
|
||||
aws-login > /dev/null || return 1
|
||||
AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
|
||||
aws ecr get-login-password --region "${AWS_REGION}" \
|
||||
| docker login --username AWS --password-stdin \
|
||||
"${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
export ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
echo "✅ ECR login OK → ${ECR_REGISTRY}"
|
||||
}
|
||||
|
||||
# -------- Azure --------
|
||||
azure-login() {
|
||||
[[ "${ENABLE_AZURE:-false}" == "true" ]] || {
|
||||
echo "Azure disabled"
|
||||
return 0
|
||||
}
|
||||
_has az || {
|
||||
echo "az CLI not found"
|
||||
return 1
|
||||
}
|
||||
[[ -n "${AZURE_CLIENT_ID:-}" && -n "${AZURE_TENANT_ID:-}" ]] || {
|
||||
echo "❌ Set AZURE_CLIENT_ID and AZURE_TENANT_ID"
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -n "${AZURE_FEDERATED_TOKEN_FILE:-}" && -f "${AZURE_FEDERATED_TOKEN_FILE}" ]]; then
|
||||
az login --service-principal \
|
||||
--username "${AZURE_CLIENT_ID}" \
|
||||
--tenant "${AZURE_TENANT_ID}" \
|
||||
--federated-token "$(cat "${AZURE_FEDERATED_TOKEN_FILE}")" \
|
||||
--allow-no-subscriptions
|
||||
elif [[ -n "${AZURE_CLIENT_SECRET:-}" ]]; then
|
||||
az login --service-principal \
|
||||
-u "${AZURE_CLIENT_ID}" -p "${AZURE_CLIENT_SECRET}" \
|
||||
--tenant "${AZURE_TENANT_ID}"
|
||||
else
|
||||
echo "❌ Provide AZURE_FEDERATED_TOKEN_FILE or AZURE_CLIENT_SECRET"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "✅ Azure auth OK"
|
||||
}
|
||||
|
||||
azure-check() {
|
||||
_has az || {
|
||||
echo "az CLI not found"
|
||||
return 1
|
||||
}
|
||||
az account show
|
||||
}
|
||||
|
||||
azure-acr-login() {
|
||||
_has az || {
|
||||
echo "az CLI not found"
|
||||
return 1
|
||||
}
|
||||
_docker_ok || {
|
||||
echo "ℹ️ docker socket not available; skipping ACR login"
|
||||
return 0
|
||||
}
|
||||
[[ -n "${AZURE_ACR_NAME:-}" ]] || {
|
||||
echo "❌ Set AZURE_ACR_NAME"
|
||||
return 1
|
||||
}
|
||||
az account show > /dev/null 2>&1 || azure-login
|
||||
az acr login --name "${AZURE_ACR_NAME}"
|
||||
export ACR_REGISTRY="${AZURE_ACR_NAME}.azurecr.io"
|
||||
echo "✅ ACR login OK → ${ACR_REGISTRY}"
|
||||
}
|
||||
|
||||
# -------- GCP --------
|
||||
gcp-login() {
|
||||
[[ "${ENABLE_GCP:-false}" == "true" ]] || {
|
||||
echo "GCP disabled"
|
||||
return 0
|
||||
}
|
||||
_has gcloud || {
|
||||
echo "gcloud not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -n "${GCP_SERVICE_ACCOUNT:-}" ]]; then
|
||||
# Service Account JSON path
|
||||
echo "${GCP_SERVICE_ACCOUNT}" > /tmp/gcp.json || {
|
||||
echo "❌ Failed to write GCP credentials"
|
||||
return 1
|
||||
}
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json || {
|
||||
echo "❌ Failed to set GCP credentials path"
|
||||
return 1
|
||||
}
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp.json --quiet || {
|
||||
echo "❌ GCP service account auth failed"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
# Workload Identity Federation using KSA token + WIP provider
|
||||
[[ -n "${GCP_WORKLOAD_IDENTITY_PROVIDER:-}" && -n "${GCP_PROJECT_ID:-}" ]] || {
|
||||
echo "❌ Provide GCP_SERVICE_ACCOUNT JSON or set GCP_WORKLOAD_IDENTITY_PROVIDER & GCP_PROJECT_ID"
|
||||
return 1
|
||||
}
|
||||
[[ -f "/var/run/secrets/kubernetes.io/serviceaccount/token" ]] || {
|
||||
echo "❌ KSA token not found"
|
||||
return 1
|
||||
}
|
||||
|
||||
TMP="/tmp/gcp-wif-$$.json"
|
||||
cat > "${TMP}" << 'EOF'
|
||||
{
|
||||
"type": "external_account",
|
||||
"audience": "//iam.googleapis.com/${GCP_WORKLOAD_IDENTITY_PROVIDER}",
|
||||
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
|
||||
"token_url": "https://sts.googleapis.com/v1/token",
|
||||
"credential_source": {
|
||||
"file": "/var/run/secrets/kubernetes.io/serviceaccount/token",
|
||||
"format": { "type": "text" }
|
||||
}
|
||||
}
|
||||
EOF
|
||||
[[ $? -eq 0 ]] || {
|
||||
echo "❌ Failed to write GCP WIF config"
|
||||
return 1
|
||||
}
|
||||
export GOOGLE_APPLICATION_CREDENTIALS="${TMP}" || {
|
||||
echo "❌ Failed to set GCP credentials path"
|
||||
return 1
|
||||
}
|
||||
gcloud auth login --cred-file="${GOOGLE_APPLICATION_CREDENTIALS}" --quiet || {
|
||||
echo "❌ GCP WIF auth failed"
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
|
||||
if [[ -n "${GCP_PROJECT_ID:-}" ]]; then
|
||||
gcloud config set project "${GCP_PROJECT_ID}" --quiet
|
||||
fi
|
||||
echo "✅ GCP auth OK"
|
||||
}
|
||||
|
||||
gcp-check() {
|
||||
_has gcloud || {
|
||||
echo "gcloud not found"
|
||||
return 1
|
||||
}
|
||||
gcloud auth list
|
||||
gcloud config get-value project || true
|
||||
}
|
||||
|
||||
gcp-gar-login() {
|
||||
_docker_ok || {
|
||||
echo "ℹ️ docker socket not available; skipping GAR login"
|
||||
return 0
|
||||
}
|
||||
: "${GCP_REGION:=us-central1}"
|
||||
[[ -n "${GCP_PROJECT_ID:-}" ]] || {
|
||||
echo "❌ Set GCP_PROJECT_ID"
|
||||
return 1
|
||||
}
|
||||
gcloud auth list --filter=status:ACTIVE --format="value(account)" > /dev/null || gcp-login
|
||||
gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet
|
||||
export GAR_REGISTRY="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}"
|
||||
echo "✅ GAR configured → ${GAR_REGISTRY}"
|
||||
}
|
||||
|
||||
# -------- Convenience --------
|
||||
multicloud-login() {
|
||||
if [[ "${ENABLE_AWS:-true}" == "true" ]]; then
|
||||
aws-login
|
||||
fi
|
||||
if [[ "${ENABLE_AZURE:-false}" == "true" ]]; then
|
||||
azure-login
|
||||
fi
|
||||
if [[ "${ENABLE_GCP:-false}" == "true" ]]; then
|
||||
gcp-login
|
||||
fi
|
||||
echo "✨ Multi-cloud login complete"
|
||||
}
|
||||
|
||||
multicloud-check() {
|
||||
if [[ "${ENABLE_AWS:-true}" == "true" ]]; then
|
||||
echo "AWS:"
|
||||
aws-check
|
||||
echo
|
||||
fi
|
||||
if [[ "${ENABLE_AZURE:-false}" == "true" ]]; then
|
||||
echo "Azure:"
|
||||
azure-check
|
||||
echo
|
||||
fi
|
||||
if [[ "${ENABLE_GCP:-false}" == "true" ]]; then
|
||||
echo "GCP:"
|
||||
gcp-check
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
echo "✨ cloud-auth loaded. Run 'cloud-auth-help' for usage."
|
||||
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# =========================
|
||||
# Helpers & safe defaults
|
||||
# =========================
|
||||
log() { printf '%s %s\n' "👉" "$*"; }
|
||||
ok() { printf '%s %s\n' "✅" "$*"; }
|
||||
skip() { printf '%s %s\n' "⏭️" "$*"; }
|
||||
warn() { printf '%s %s\n' "⚠️" "$*"; }
|
||||
|
||||
# Detect CPU arch (amd64/arm64)
|
||||
arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64 | amd64) echo amd64 ;;
|
||||
aarch64 | arm64) echo arm64 ;;
|
||||
*) echo amd64 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Map to Docker static tarball arch names
|
||||
docker_tar_arch() {
|
||||
case "$(arch)" in
|
||||
amd64) echo x86_64 ;;
|
||||
arm64) echo aarch64 ;;
|
||||
*) echo x86_64 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
SAFE_TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$SAFE_TMP"' EXIT
|
||||
|
||||
safe_dl() { # url dest
|
||||
curl -fL --retry 5 --retry-delay 2 --connect-timeout 10 -o "$2" "$1" || {
|
||||
echo "Failed to download $1"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
docker_ok() {
|
||||
command -v docker > /dev/null 2>&1 && [[ -S /var/run/docker.sock ]]
|
||||
}
|
||||
|
||||
# Ensure user bin dir
|
||||
mkdir -p "$HOME/.local/bin" "$HOME/workspace/app"
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# Inputs (with sane defaults)
|
||||
IAC_TOOL="${IAC_TOOL:-terraform}"
|
||||
TERRAFORM_VERSION="${TERRAFORM_VERSION:-1.6.0}"
|
||||
|
||||
ENABLE_AWS="${ENABLE_AWS:-true}"
|
||||
ENABLE_AZURE="${ENABLE_AZURE:-false}"
|
||||
ENABLE_GCP="${ENABLE_GCP:-false}"
|
||||
|
||||
AWS_REGION="${AWS_REGION:-}"
|
||||
AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}"
|
||||
AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}"
|
||||
AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}"
|
||||
|
||||
AZURE_CLIENT_ID="${AZURE_CLIENT_ID:-}"
|
||||
AZURE_TENANT_ID="${AZURE_TENANT_ID:-}"
|
||||
AZURE_CLIENT_SECRET="${AZURE_CLIENT_SECRET:-}"
|
||||
AZURE_FEDERATED_TOKEN_FILE="${AZURE_FEDERATED_TOKEN_FILE:-}"
|
||||
|
||||
GCP_PROJECT_ID="${GCP_PROJECT_ID:-}"
|
||||
GCP_SERVICE_ACCOUNT="${GCP_SERVICE_ACCOUNT:-}" # full JSON if not using WIF
|
||||
|
||||
REPO_URL="${REPO_URL:-${repo_url:-}}"
|
||||
DEFAULT_BRANCH="${DEFAULT_BRANCH:-${default_branch:-main}}"
|
||||
WORKDIR="${WORKDIR:-$HOME/workspace/app}"
|
||||
GITHUB_TOKEN="${GITHUB_TOKEN:-${GIT_TOKEN:-}}"
|
||||
|
||||
GIT_AUTHOR_NAME="${GIT_AUTHOR_NAME:-}"
|
||||
GIT_AUTHOR_EMAIL="${GIT_AUTHOR_EMAIL:-}"
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ Multi-Cloud DevOps Workspace Setup (no sudo) ║"
|
||||
echo "╚════════════════════════════════════════════════════════════════╝"
|
||||
echo
|
||||
|
||||
# ==========================================================
|
||||
# Write multi-cloud helper functions to ~/workspace/cloud-auth.sh
|
||||
# ==========================================================
|
||||
cat > "${HOME}/workspace/cloud-auth.sh" << 'EOAUTHSCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
aws-ecr-login() {
|
||||
: "${AWS_REGION:=us-east-1}"
|
||||
if ! command -v aws >/dev/null 2>&1; then echo "aws CLI not found"; return 1; fi
|
||||
if ! aws sts get-caller-identity &>/dev/null; then
|
||||
echo "❌ AWS creds not available (IRSA or keys)"; return 1; fi
|
||||
AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
|
||||
if command -v docker >/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then
|
||||
aws ecr get-login-password --region "${AWS_REGION}" | \
|
||||
docker login --username AWS --password-stdin \
|
||||
"${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
export ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
echo "✅ ECR login OK → ${ECR_REGISTRY}"
|
||||
else
|
||||
echo "ℹ️ docker socket not available; skipping docker login"
|
||||
fi
|
||||
}
|
||||
|
||||
aws-check() { aws sts get-caller-identity && echo "✓ AWS creds valid"; }
|
||||
|
||||
azure-login() {
|
||||
if ! command -v az >/dev/null 2>&1; then echo "az CLI not found"; return 1; fi
|
||||
if [[ -n "${AZURE_FEDERATED_TOKEN_FILE:-}" && -f "${AZURE_FEDERATED_TOKEN_FILE}" ]]; then
|
||||
az login --service-principal --username "${AZURE_CLIENT_ID}" \
|
||||
--tenant "${AZURE_TENANT_ID}" \
|
||||
--federated-token "$(cat "${AZURE_FEDERATED_TOKEN_FILE}")" \
|
||||
--allow-no-subscriptions
|
||||
elif [[ -n "${AZURE_CLIENT_SECRET:-}" ]]; then
|
||||
az login --service-principal -u "${AZURE_CLIENT_ID}" -p "${AZURE_CLIENT_SECRET}" --tenant "${AZURE_TENANT_ID}"
|
||||
else
|
||||
echo "❌ Provide AZURE_FEDERATED_TOKEN_FILE or AZURE_CLIENT_SECRET"; return 1
|
||||
fi
|
||||
echo "✅ Azure auth OK"; az account show
|
||||
}
|
||||
|
||||
azure-acr-login() {
|
||||
[[ -n "${AZURE_ACR_NAME:-}" ]] || { echo "Set AZURE_ACR_NAME"; return 1; }
|
||||
az account show &>/dev/null || azure-login
|
||||
if command -v docker >/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then
|
||||
az acr login --name "${AZURE_ACR_NAME}"
|
||||
export ACR_REGISTRY="${AZURE_ACR_NAME}.azurecr.io"
|
||||
echo "✅ ACR login OK → ${ACR_REGISTRY}"
|
||||
else
|
||||
echo "ℹ️ docker socket not available; skipping docker login"
|
||||
fi
|
||||
}
|
||||
|
||||
azure-check() { az account show && echo "✓ Azure creds valid" || { echo "❌ Not logged in"; return 1; }; }
|
||||
|
||||
gcp-login() {
|
||||
if ! command -v gcloud >/dev/null 2>&1; then echo "gcloud not found"; return 1; fi
|
||||
if [[ -n "${GCP_SERVICE_ACCOUNT:-}" ]]; then
|
||||
# SA JSON auth
|
||||
echo "${GCP_SERVICE_ACCOUNT}" > /tmp/gcp.json || { echo "❌ Failed to write GCP credentials"; return 1; }
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp.json --quiet || { echo "❌ GCP auth failed"; return 1; }
|
||||
else
|
||||
echo "❌ Provide GCP_SERVICE_ACCOUNT JSON (WIF path not configured here)"; return 1
|
||||
fi
|
||||
[[ -n "${GCP_PROJECT_ID:-}" ]] && gcloud config set project "${GCP_PROJECT_ID}" --quiet || true
|
||||
echo "✅ GCP auth OK"; gcloud auth list
|
||||
}
|
||||
|
||||
gcp-gar-login() {
|
||||
: "${GCP_REGION:=us-central1}"
|
||||
[[ -n "${GCP_PROJECT_ID:-}" ]] || { echo "Set GCP_PROJECT_ID"; return 1; }
|
||||
gcloud auth list --filter=status:ACTIVE --format="value(account)" &>/dev/null || gcp-login
|
||||
if command -v docker >/dev/null 2>&1 && [[ -S /var/run/docker.sock ]]; then
|
||||
gcloud auth configure-docker "${GCP_REGION}-docker.pkg.dev" --quiet
|
||||
export GAR_REGISTRY="${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}"
|
||||
echo "✅ GAR configured → ${GAR_REGISTRY}"
|
||||
else
|
||||
echo "ℹ️ docker socket not available; skipping docker login"
|
||||
fi
|
||||
}
|
||||
|
||||
gcp-check() { gcloud auth list --filter=status:ACTIVE --format="value(account)" >/dev/null && echo "✓ GCP creds valid" || { echo "❌ Not logged in"; return 1; }; }
|
||||
|
||||
multicloud-login() {
|
||||
[[ "${ENABLE_AWS:-false}" == "true" ]] && command -v aws >/dev/null && aws-ecr-login || true
|
||||
[[ "${ENABLE_AZURE:-false}" == "true" ]] && command -v az >/dev/null && azure-login || true
|
||||
[[ "${ENABLE_GCP:-false}" == "true" ]] && command -v gcloud >/dev/null && gcp-login || true
|
||||
echo "✨ Multi-cloud login complete"
|
||||
}
|
||||
|
||||
multicloud-check() {
|
||||
[[ "${ENABLE_AWS:-false}" == "true" ]] && command -v aws >/dev/null && { echo "AWS:"; aws-check; echo; } || true
|
||||
[[ "${ENABLE_AZURE:-false}" == "true" ]] && command -v az >/dev/null && { echo "Azure:"; azure-check; echo; } || true
|
||||
[[ "${ENABLE_GCP:-false}" == "true" ]] && command -v gcloud >/dev/null && { echo "GCP:"; gcp-check; echo; } || true
|
||||
}
|
||||
|
||||
cloud-auth-help() {
|
||||
cat <<'EOHELP'
|
||||
Multi-Cloud Authentication Helper
|
||||
|
||||
Functions:
|
||||
AWS: aws-ecr-login, aws-check
|
||||
Azure: azure-login, azure-acr-login, azure-check
|
||||
GCP: gcp-login, gcp-gar-login, gcp-check
|
||||
Multi: multicloud-login, multicloud-check, cloud-auth-help
|
||||
EOHELP
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "✨ Multi-cloud auth helpers loaded. Run 'cloud-auth-help' for help."
|
||||
EOAUTHSCRIPT
|
||||
chmod +x "${HOME}/workspace/cloud-auth.sh"
|
||||
ok "Created ${HOME}/workspace/cloud-auth.sh"
|
||||
echo
|
||||
|
||||
# =========================
|
||||
# IaC tooling
|
||||
# =========================
|
||||
log "Installing IaC tooling (${IAC_TOOL})"
|
||||
case "$IAC_TOOL" in
|
||||
terraform)
|
||||
if ! command -v terraform > /dev/null 2>&1; then
|
||||
safe_dl "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_$(arch).zip" "$SAFE_TMP/tf.zip"
|
||||
unzip -q "$SAFE_TMP/tf.zip" -d "$HOME/.local/bin"
|
||||
ok "Terraform ${TERRAFORM_VERSION} installed"
|
||||
else
|
||||
ok "Terraform already installed ($(terraform version | head -1))"
|
||||
fi
|
||||
;;
|
||||
cdk)
|
||||
if ! command -v npm > /dev/null 2>&1; then
|
||||
log "npm not found; installing Node via nvm"
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
# shellcheck disable=SC1090
|
||||
. "$NVM_DIR/nvm.sh"
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
# persist for future shells
|
||||
grep -q 'NVM_DIR' "$HOME/.bashrc" 2> /dev/null || {
|
||||
echo 'export NVM_DIR="$HOME/.nvm"' >> "$HOME/.bashrc"
|
||||
echo '. "$NVM_DIR/nvm.sh"' >> "$HOME/.bashrc"
|
||||
}
|
||||
fi
|
||||
npm install -g aws-cdk > /dev/null
|
||||
ok "AWS CDK installed ($(cdk --version))"
|
||||
;;
|
||||
pulumi)
|
||||
if ! command -v pulumi > /dev/null 2>&1; then
|
||||
curl -fsSL https://get.pulumi.com | sh
|
||||
export PATH="$PATH:$HOME/.pulumi/bin"
|
||||
ok "Pulumi installed ($(pulumi version))"
|
||||
else
|
||||
ok "Pulumi already installed ($(pulumi version))"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
warn "Unknown IAC_TOOL=${IAC_TOOL}; skipping IaC install"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Extras: Terragrunt, tflint, tfsec, terraform-docs, pre-commit
|
||||
if ! command -v terragrunt > /dev/null 2>&1; then
|
||||
TG_VER="0.54.0"
|
||||
safe_dl "https://github.com/gruntwork-io/terragrunt/releases/download/v${TG_VER}/terragrunt_linux_$(arch)" "$HOME/.local/bin/terragrunt"
|
||||
chmod +x "$HOME/.local/bin/terragrunt"
|
||||
ok "Terragrunt v${TG_VER} installed"
|
||||
fi
|
||||
|
||||
if ! command -v tflint > /dev/null 2>&1; then
|
||||
# official installer handles arch
|
||||
curl -fsSL https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
|
||||
mv -f /tmp/tflint "$HOME/.local/bin/" 2> /dev/null || true
|
||||
ok "tflint installed"
|
||||
fi
|
||||
|
||||
if ! command -v tfsec > /dev/null 2>&1; then
|
||||
TFSEC_VER="1.28.1"
|
||||
safe_dl "https://github.com/aquasecurity/tfsec/releases/download/v${TFSEC_VER}/tfsec-linux-$(arch)" "$HOME/.local/bin/tfsec"
|
||||
chmod +x "$HOME/.local/bin/tfsec"
|
||||
ok "tfsec v${TFSEC_VER} installed"
|
||||
fi
|
||||
|
||||
if ! command -v terraform-docs > /dev/null 2>&1; then
|
||||
TFD_VER="0.17.0"
|
||||
safe_dl "https://github.com/terraform-docs/terraform-docs/releases/download/v${TFD_VER}/terraform-docs-v${TFD_VER}-linux-$(arch).tar.gz" "$SAFE_TMP/terraform-docs.tgz"
|
||||
tar -xzf "$SAFE_TMP/terraform-docs.tgz" -C "$SAFE_TMP"
|
||||
install -m 0755 "$SAFE_TMP/terraform-docs" "$HOME/.local/bin/terraform-docs"
|
||||
ok "terraform-docs v${TFD_VER} installed"
|
||||
fi
|
||||
|
||||
if ! command -v pre-commit > /dev/null 2>&1; then
|
||||
if command -v pip3 > /dev/null 2>&1; then
|
||||
pip3 install --user --quiet pre-commit
|
||||
ok "pre-commit installed"
|
||||
elif command -v python3 > /dev/null 2>&1; then
|
||||
python3 -m pip install --user --quiet pre-commit
|
||||
ok "pre-commit installed"
|
||||
else
|
||||
warn "Python3/pip3 not found; skipping pre-commit"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Cloud CLIs (user-space)
|
||||
# =========================
|
||||
echo
|
||||
log "Installing Cloud CLIs (user-space)"
|
||||
|
||||
# AWS CLI v2
|
||||
if [[ "${ENABLE_AWS}" == "true" ]] && ! command -v aws > /dev/null 2>&1; then
|
||||
safe_dl "https://awscli.amazonaws.com/awscli-exe-linux-$(arch).zip" "$SAFE_TMP/awscliv2.zip" \
|
||||
|| safe_dl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" "$SAFE_TMP/awscliv2.zip"
|
||||
unzip -q "$SAFE_TMP/awscliv2.zip" -d "$SAFE_TMP"
|
||||
"$SAFE_TMP/aws/install" -i "$HOME/.local/aws-cli" -b "$HOME/.local/bin" > /dev/null
|
||||
ok "AWS CLI installed"
|
||||
fi
|
||||
|
||||
# Azure CLI
|
||||
if [[ "${ENABLE_AZURE}" == "true" ]] && ! command -v az > /dev/null 2>&1; then
|
||||
if command -v pip3 > /dev/null 2>&1; then
|
||||
pip3 install --user --quiet azure-cli && ok "Azure CLI installed"
|
||||
elif command -v python3 > /dev/null 2>&1; then
|
||||
python3 -m pip install --user --quiet azure-cli && ok "Azure CLI installed"
|
||||
else
|
||||
warn "Python/pip not found; cannot install Azure CLI"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Google Cloud SDK
|
||||
if [[ "${ENABLE_GCP}" == "true" ]] && ! command -v gcloud > /dev/null 2>&1; then
|
||||
GSDK_ARCH="$([[ "$(arch)" == amd64 ]] && echo x86_64 || echo arm)"
|
||||
safe_dl "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-${GSDK_ARCH}.tar.gz" "$SAFE_TMP/gcloud.tgz"
|
||||
tar -xzf "$SAFE_TMP/gcloud.tgz" -C "$HOME"
|
||||
mv "$HOME/google-cloud-sdk" "$HOME/.local/google-cloud-sdk"
|
||||
ln -sf "$HOME/.local/google-cloud-sdk/bin/"{gcloud,gsutil,bq} "$HOME/.local/bin/" || true
|
||||
"$HOME/.local/google-cloud-sdk/install.sh" --quiet --rc-path /dev/null --path-update=false || true
|
||||
ok "Google Cloud SDK installed"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Container & K8s tools
|
||||
# =========================
|
||||
echo
|
||||
log "Installing container & Kubernetes tools"
|
||||
|
||||
# Docker CLI (client only)
|
||||
if ! command -v docker > /dev/null 2>&1; then
|
||||
DOCKER_VER="25.0.5"
|
||||
safe_dl "https://download.docker.com/linux/static/stable/$(docker_tar_arch)/docker-${DOCKER_VER}.tgz" "$SAFE_TMP/docker.tgz"
|
||||
tar -xzf "$SAFE_TMP/docker.tgz" -C "$SAFE_TMP"
|
||||
install -m 0755 "$SAFE_TMP/docker/docker" "$HOME/.local/bin/docker"
|
||||
ok "Docker client installed"
|
||||
fi
|
||||
|
||||
# kubectl
|
||||
if ! command -v kubectl > /dev/null 2>&1; then
|
||||
KREL="$(curl -fsSL https://dl.k8s.io/release/stable.txt)"
|
||||
safe_dl "https://dl.k8s.io/release/${KREL}/bin/linux/$(arch)/kubectl" "$SAFE_TMP/kubectl"
|
||||
install -m 0755 "$SAFE_TMP/kubectl" "$HOME/.local/bin/kubectl"
|
||||
ok "kubectl ${KREL} installed"
|
||||
fi
|
||||
|
||||
# Helm
|
||||
if ! command -v helm > /dev/null 2>&1; then
|
||||
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | USE_SUDO=false HELM_INSTALL_DIR="$HOME/.local/bin" bash
|
||||
ok "Helm installed"
|
||||
fi
|
||||
|
||||
# jq / yq
|
||||
if ! command -v jq > /dev/null 2>&1; then
|
||||
safe_dl "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-$(arch)" "$HOME/.local/bin/jq"
|
||||
chmod +x "$HOME/.local/bin/jq"
|
||||
ok "jq installed"
|
||||
fi
|
||||
|
||||
if ! command -v yq > /dev/null 2>&1; then
|
||||
safe_dl "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_$(arch)" "$HOME/.local/bin/yq"
|
||||
chmod +x "$HOME/.local/bin/yq"
|
||||
ok "yq installed"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Cloud runtime auth (optional)
|
||||
# =========================
|
||||
echo
|
||||
log "Configuring runtime cloud auth (if provided)"
|
||||
|
||||
# AWS keys (override IRSA if present)
|
||||
if [[ "${ENABLE_AWS}" == "true" ]] && [[ -n "$AWS_ACCESS_KEY_ID" ]]; then
|
||||
mkdir -p "$HOME/.aws"
|
||||
{
|
||||
echo "[default]"
|
||||
echo "aws_access_key_id=${AWS_ACCESS_KEY_ID}"
|
||||
echo "aws_secret_access_key=${AWS_SECRET_ACCESS_KEY:-}"
|
||||
[[ -n "$AWS_SESSION_TOKEN" ]] && echo "aws_session_token=${AWS_SESSION_TOKEN}"
|
||||
} > "$HOME/.aws/credentials" || { warn "Failed to write AWS credentials"; }
|
||||
if [[ -n "$AWS_REGION" ]]; then
|
||||
{
|
||||
echo "[default]"
|
||||
echo "region=${AWS_REGION}"
|
||||
} > "$HOME/.aws/config"
|
||||
fi
|
||||
ok "AWS runtime creds configured${AWS_REGION:+ (region ${AWS_REGION})}"
|
||||
else
|
||||
skip "AWS runtime creds not set"
|
||||
fi
|
||||
|
||||
# Azure SP (client secret path; federated handled by helper)
|
||||
if [[ "${ENABLE_AZURE}" == "true" ]] && [[ -n "$AZURE_CLIENT_ID" && -n "$AZURE_TENANT_ID" ]]; then
|
||||
if command -v az > /dev/null 2>&1; then
|
||||
if [[ -n "$AZURE_FEDERATED_TOKEN_FILE" && -f "$AZURE_FEDERATED_TOKEN_FILE" ]]; then
|
||||
az login --service-principal --username "$AZURE_CLIENT_ID" \
|
||||
--tenant "$AZURE_TENANT_ID" \
|
||||
--federated-token "$(cat "$AZURE_FEDERATED_TOKEN_FILE")" \
|
||||
--allow-no-subscriptions > /dev/null
|
||||
ok "Azure federated login complete"
|
||||
elif [[ -n "$AZURE_CLIENT_SECRET" ]]; then
|
||||
az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" > /dev/null
|
||||
ok "Azure SP login complete"
|
||||
else
|
||||
skip "Azure creds not provided (need federated token file or client secret)"
|
||||
fi
|
||||
else
|
||||
warn "Azure CLI not found; skipping login"
|
||||
fi
|
||||
else
|
||||
skip "Azure runtime auth not configured"
|
||||
fi
|
||||
|
||||
# GCP SA JSON
|
||||
if [[ "${ENABLE_GCP}" == "true" ]] && [[ -n "$GCP_SERVICE_ACCOUNT" ]]; then
|
||||
if command -v gcloud > /dev/null 2>&1; then
|
||||
echo "$GCP_SERVICE_ACCOUNT" > /tmp/gcp.json || { warn "Failed to write GCP credentials"; }
|
||||
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp.json
|
||||
gcloud auth activate-service-account --key-file=/tmp/gcp.json > /dev/null || { warn "GCP auth failed"; }
|
||||
[[ -n "$GCP_PROJECT_ID" ]] && gcloud config set project "$GCP_PROJECT_ID" --quiet || true
|
||||
ok "GCP SA auth complete"
|
||||
else
|
||||
warn "gcloud not found; skipping GCP auth"
|
||||
fi
|
||||
else
|
||||
skip "GCP runtime auth not configured"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Git identity & bootstrap
|
||||
# =========================
|
||||
echo
|
||||
log "Preparing workspace directory"
|
||||
|
||||
# Git identity
|
||||
if [[ -n "$GIT_AUTHOR_NAME" ]]; then
|
||||
git config --global user.name "$GIT_AUTHOR_NAME"
|
||||
fi
|
||||
if [[ -n "$GIT_AUTHOR_EMAIL" ]]; then
|
||||
git config --global user.email "$GIT_AUTHOR_EMAIL"
|
||||
fi
|
||||
|
||||
mkdir -p "$WORKDIR"
|
||||
|
||||
# Clone or init
|
||||
if [[ -n "$REPO_URL" ]]; then
|
||||
URL="$REPO_URL"
|
||||
if [[ -n "$GITHUB_TOKEN" && "$URL" =~ ^https://github.com/ ]]; then
|
||||
URL="${URL/https:\/\//https:\/\/${GITHUB_TOKEN}@}" || { warn "Failed to modify URL"; }
|
||||
warn "Using GITHUB_TOKEN for private repo clone"
|
||||
fi
|
||||
if [[ ! -d "$WORKDIR/.git" ]]; then
|
||||
log "Cloning ${REPO_URL} into ${WORKDIR}"
|
||||
git clone "$URL" "$WORKDIR" || { warn "Failed to clone repository"; }
|
||||
pushd "$WORKDIR" > /dev/null
|
||||
git checkout "$DEFAULT_BRANCH" || git checkout -b "$DEFAULT_BRANCH"
|
||||
popd > /dev/null
|
||||
ok "Repository ready @ ${DEFAULT_BRANCH}"
|
||||
else
|
||||
ok "Repo already present at ${WORKDIR}"
|
||||
fi
|
||||
else
|
||||
if [[ ! -d "$WORKDIR/.git" ]]; then
|
||||
log "Initializing empty repository in ${WORKDIR}"
|
||||
git init -q "$WORKDIR"
|
||||
pushd "$WORKDIR" > /dev/null
|
||||
git checkout -b "$DEFAULT_BRANCH" > /dev/null 2>&1 || true
|
||||
popd > /dev/null
|
||||
fi
|
||||
ok "Workspace ready at ${WORKDIR}"
|
||||
fi
|
||||
|
||||
# =========================
|
||||
# Company Terraform skeleton
|
||||
# =========================
|
||||
echo
|
||||
log "Creating company Terraform skeleton (optional)"
|
||||
mkdir -p "$WORKDIR/terraform"/{environments/{dev,staging,prod},modules,policies,shared}
|
||||
cat > "$WORKDIR/terraform/README.md" << 'EOREADME'
|
||||
# Company Terraform Project
|
||||
- `environments/` contains per-env stacks.
|
||||
- `modules/` reusable infra modules.
|
||||
- `policies/` sentinel/policy-as-code.
|
||||
- `shared/` backend, providers, etc.
|
||||
EOREADME
|
||||
ok "Skeleton present at $WORKDIR/terraform"
|
||||
|
||||
# =========================
|
||||
# PATH persistence tip
|
||||
# =========================
|
||||
if ! grep -q 'export PATH="$HOME/.local/bin:$PATH"' "$HOME/.bashrc" 2> /dev/null; then
|
||||
echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> "$HOME/.bashrc"
|
||||
fi
|
||||
|
||||
echo
|
||||
ok "Workspace ready!"
|
||||
echo " • IaC tool: ${IAC_TOOL}"
|
||||
echo " • AWS enabled: ${ENABLE_AWS}"
|
||||
echo " • Azure enabled: ${ENABLE_AZURE}"
|
||||
echo " • GCP enabled: ${ENABLE_GCP}"
|
||||
[[ -d "$WORKDIR/.git" ]] && echo " • Repo: ${REPO_URL:-<none>} @ ${DEFAULT_BRANCH}"
|
||||
echo " • Auth helpers: source ~/workspace/cloud-auth.sh"
|
||||
@@ -0,0 +1,120 @@
|
||||
# --- Host cluster (where the workspace runs) ---
|
||||
variable "host_cluster_name" {
|
||||
description = "EKS cluster name"
|
||||
type = string
|
||||
|
||||
validation {
|
||||
condition = can(regex("^[0-9A-Za-z][0-9A-Za-z_-]*$", trimspace(var.host_cluster_name)))
|
||||
error_message = "Cluster name must match ^[0-9A-Za-z][0-9A-Za-z_-]*$ (no leading space)."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# --- Admin: IaC tool & toggles ---
|
||||
variable "iac_tool" {
|
||||
description = "Infrastructure as Code tool"
|
||||
type = string
|
||||
default = "terraform"
|
||||
validation {
|
||||
condition = contains(["terraform", "cdk", "pulumi"], var.iac_tool)
|
||||
error_message = "Must be one of: terraform, cdk, pulumi"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
variable "enable_aws" {
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "enable_azure" {
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "enable_gcp" {
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
# --- AWS ---
|
||||
variable "aws_region" {
|
||||
type = string
|
||||
default = "us-west-2"
|
||||
}
|
||||
|
||||
variable "aws_role_arn" {
|
||||
type = string
|
||||
default = "" # IRSA optional
|
||||
}
|
||||
|
||||
variable "aws_access_key_id" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "aws_secret_access_key" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "aws_session_token" {
|
||||
description = "Optional STS session token"
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "repo_url" {
|
||||
description = "Git repository to clone into the workspace (optional)"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "default_branch" {
|
||||
description = "Default branch name to use (if repo is empty or for initial checkout)"
|
||||
type = string
|
||||
default = "main"
|
||||
}
|
||||
|
||||
|
||||
# --- Azure ---
|
||||
variable "azure_subscription_id" {
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "azure_tenant_id" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "azure_client_id" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "azure_client_secret" {
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# --- GCP ---
|
||||
variable "gcp_project_id" {
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "gcp_service_account" {
|
||||
description = "Service Account JSON (paste full JSON) — leave empty if using WIF"
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 631 KiB |