Compare commits

...

6 Commits

Author SHA1 Message Date
DevelopmentCats ae24c2da97 feat: add session resumption 2025-10-23 16:23:03 -05:00
Yevhenii Shcherbina 30123e7ea3 feat: add boundary pprof server in claude-code module (#503) 2025-10-23 14:18:30 -04:00
djarbz f7c1be71f7 Add [copyparty] module (#486)
## Description

This PR adds a module to install Copyparty as an alternative to
Filebrowser.

## Type of Change

- [x] New module
- [ ] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/djarbz/modules/copyparty`  
**New version:** `v0.1.0`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [N/A] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
None

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-23 11:19:05 -05:00
DevCats 19519a0a13 fix: add shebang to zed coder_script (#504)
## Description

Add `#!/bin/sh` to zed_settings coder_script
<!-- Briefly describe what this PR does and why -->

## Type of Change

- [ ] New module
- [ ] New template
- [X] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/zed`  
**New version:** `v1.1.1`  
**Breaking change:** [ ] Yes [X] No

## Testing & Validation

- [X] Tests pass (`bun test`)
- [X] Code formatted (`bun fmt`)
- [X] Changes tested locally

## Related Issues

https://github.com/coder/registry/issues/482
<!-- Link related issues or write "None" if not applicable -->
2025-10-23 07:39:27 -05:00
Rowan Smith 63e42283ce chore: Update templates from Always to IfNotPresent for image_pull_policy (#501)
## Description

Change `image_pull_policy` from `Always` to `IfNotPresent` on Coder
owned templates. Given these are a reference point for users and
customers and they copy them into their own templates I think it makes
sense to encourage the use of caching of images.

## Type of Change

- [ ] New module
- [ ] New template
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Template Information

**Path:** 


https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-devcontainer

https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-envbox

https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues

None
2025-10-23 04:28:58 +00:00
Eric Paulsen 0c5a8a2354 add nfs-deployment template (#502)
## Description

this PR adds a new template to the registry, which shows how to mount an
NFS share to a K8s deployment workspace.

## Type of Change

- [ ] New module
- [x] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Template Information

<!-- Delete this section if not applicable -->

**Path:** `registry/ericpaulsen/templates/nfs-deployment`

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues

None

---------

Co-authored-by: DevCats <christofer@coder.com>
2025-10-22 11:51:58 -05:00
23 changed files with 1269 additions and 22 deletions
+1
View File
@@ -6,6 +6,7 @@ HashiCorp = "HashiCorp"
mavrickrishi = "mavrickrishi" # Username
mavrick = "mavrick" # Username
inh = "inh" # Option in setpriv command
exportfs = "exportfs" # nfs related binary
[files]
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
+210
View File
@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="300mm"
height="207mm"
viewBox="0 0 300 207"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<title
id="title1">copyparty_logo</title>
<defs
id="defs1">
<linearGradient
inkscape:collect="always"
id="linearGradient1">
<stop
style="stop-color:#ffcc55;stop-opacity:1"
offset="0"
id="stop1" />
<stop
style="stop-color:#ffcc00;stop-opacity:1"
offset="0.2"
id="stop2" />
<stop
style="stop-color:#ff8800;stop-opacity:1"
offset="1"
id="stop3" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="15"
y1="15"
x2="15"
y2="143"
gradientUnits="userSpaceOnUse" />
</defs>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>copyparty_logo</dc:title>
<dc:source>github.com/9001/copyparty</dc:source>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="kassett">
<rect
style="fill:#333333"
id="rect1"
width="300"
height="205"
x="0"
y="0"
rx="12"
ry="12" />
<rect
style="fill:url(#linearGradient2)"
id="rect2"
width="270"
height="128"
x="15"
y="15"
rx="8"
ry="8" />
<rect
style="fill:#333333"
id="rect3"
width="172"
height="52"
x="64"
y="72"
rx="26"
ry="26" />
<circle
style="fill:#cccccc"
id="circle1"
cx="91"
cy="98"
r="18" />
<circle
style="fill:#cccccc"
id="circle2"
cx="209"
cy="98"
r="18" />
<path
style="fill:#737373;stroke-width:1px"
d="m 48,207 10,-39 c 1.79,-6.2 5.6,-7.8 12,-8 60,-1 100,-1 160,0 6.4,0.2 10,1.8 12,8 l 10,39 z"
id="path1"
sodipodi:nodetypes="ccccccc" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="tekst"
style="display:none">
<text
xml:space="preserve"
style="font-size:38.8056px;line-height:1.25;font-family:Akbar;-inkscape-font-specification:Akbar;letter-spacing:3.70417px;word-spacing:0px;fill:#333333"
x="47.153069"
y="55.548954"
id="text1"><tspan
sodipodi:role="line"
id="tspan1"
x="47.153069"
y="55.548954"
style="-inkscape-font-specification:Akbar"
rotate="0 0">copyparty</tspan></text>
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="stensatt">
<path
d="m 63.5,50.9 q -0.85,0.93 -4.73,2.3 -3.6,1.3 -4.4,1.3 -3.3,0 -5.1,-2.1 -1.75,-2 -1.75,-5.36 0,-4.6 3.76,-7.64 3.3,-2.7 7.3,-2.7 0.4,0 0.93,0.74 0.54,0.7 0.54,1.16 0,2.06 -2.2,2.7 -1.36,0.4 -4.04,1.16 -2.2,1.16 -2.2,4.4 0,3.2 2.9,3.2 0.85,0 0.85,0 0.54,0 1.44,-0.16 1.1,-0.23 2.9,-0.74 1.8,-0.54 2.13,-0.54 0.4,0 1.75,0.6 z"
style="fill:#333333"
id="path11" />
<path
d="m 87.6,45 q 0,4.2 -3.7,6.95 -3.2,2.3 -6.87,2.3 -3.4,0 -6,-2.6 -2.5,-2.6 -2.5,-6 0,-3.6 3.14,-6.64 3.2,-3 6.8,-3 3.5,0 6.3,2.76 2.83,2.76 2.83,6.25 z m -3.4,0.16 q 0,-2.25 -1.75,-3.7 -1.7,-1.5 -4,-1.5 -0.1,0 -1.6,1.6 -1.44,1.55 -2.44,1.55 -0.6,0 -0.8,-0.3 -1.16,2.3 -1.16,3 0,2.25 2.13,3.4 1.6,0.9 3.6,0.9 2,0 3.76,-1.1 2.25,-1.4 2.25,-3.84 z"
style="fill:#333333"
id="path12" />
<path
d="m 112.8,46.8 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2.1,0 -2.1,2.64 0,0.85 0.23,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.77,2.83 -1.44,0 -3,-0.85 -1.46,-9.5 -1.46,-12 0,-3.65 1.75,-8.1 2.37,-6.05 6.45,-6.05 3.7,0 7.3,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.33,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.38,1.24 0.43,0.8 0.85,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
style="fill:#333333"
id="path13" />
<path
d="m 133,40 q -2.1,4.1 -3.2,7 -0.1,0.3 -1.6,4.5 -0.4,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.3,2.64 -1.4,-0.2 -1.6,-1.6 0,-0.2 0,-0.5 0,-0.16 0.3,-1.5 1,-5.04 1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.5,-1.36 2.1,-1.36 0.4,0 1.1,0.6 0.6,0.6 0.7,1.1 0.8,6.2 4.9,11.1 1,-1.8 1.8,-4.04 0.5,-1.4 1.6,-4.15 1.9,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.9,0.3 1.3,2.8 z"
style="fill:#333333"
id="path14" />
<path
d="m 157.5,48 q 0,2.8 -1.9,4.4 -1.8,1.5 -4.7,1.5 -0.7,0 -2.7,-0.4 -1.9,-0.4 -2.6,-0.4 -2,0 -2,2.64 0,0.85 0.2,2.6 0.2,1.75 0.2,2.6 0,1.9 -0.7,2.83 -1.5,0 -3,-0.85 -1.5,-9.5 -1.5,-11.95 0,-3.65 1.8,-8.1 2.3,-6.05 6.4,-6.05 3.7,0 7.2,4.1 3.3,3.84 3.3,7.14 z m -3.8,0.2 q -0.6,-2.2 -2.6,-4.4 -2.3,-2.5 -4.3,-2.5 -1.3,0 -2.3,2.2 -0.9,1.8 -0.9,3.26 0,0.47 0.4,1.24 0.4,0.8 0.8,0.8 1.1,0 3.2,0.3 2.1,0.3 3.2,0.3 0.3,0 1.3,-0.4 1,-0.47 1.3,-0.74 z"
style="fill:#333333"
id="path15" />
<path
d="m 182,53.3 q 0,0.9 -0.6,1.5 -0.6,0.6 -1.4,0.6 -1.6,0 -3,-0.9 -1.4,-0.93 -2.1,-2.3 -0.7,-0.1 -1.5,0.85 -0.9,1.16 -1.1,1.24 -1.2,0.54 -3.9,0.54 -2.2,0 -3.9,-2.44 -1.5,-2.13 -1.5,-4 0,-3.4 3.4,-6.4 3.2,-2.9 6.7,-2.9 0.9,0 1.7,0.6 0.8,0.6 0.8,1.44 0,0.54 -0.4,1.1 2.4,0.9 2.4,2.83 0,0.35 -0.1,1.05 -0.1,0.7 -0.1,1.05 0,0.4 0.1,0.6 0.5,1.3 2.5,3.4 1.9,1.9 1.9,2.2 z m -8.1,-10.1 q -0.4,0 -1.1,-0.1 -0.8,-0.16 -1.1,-0.16 -1.3,0 -3.2,1.94 -1.9,1.94 -1.9,3.3 0,0.8 0.7,1.8 0.9,1.3 2.2,1.3 2.6,0 3.5,-2.9 0.5,-2.6 1,-5.16 z"
style="fill:#333333"
id="path16" />
<path
d="m 203.8,42.4 q -0.4,0.4 -1.5,0.4 -0.9,0 -2.5,-0.3 -1.7,-0.3 -2.5,-0.3 -4.7,0 -5.5,6.9 -0.3,3.1 -0.4,3.3 -0.4,1 -1.7,2.3 h -1.1 q -0.7,-1.2 -1.3,-4.1 -0.6,-2.76 -0.6,-4.27 0,-1.16 0.1,-1.5 0.2,-0.54 1,-0.54 0.3,0 0.6,0.3 0.4,0.3 0.4,0.3 1.9,-3.53 3.1,-4.6 1.8,-1.7 5.1,-1.7 1.4,0 3.6,0.9 2.8,1.16 3.3,2.8 z"
style="fill:#333333"
id="path17" />
<path
d="m 229.5,37.16 q 0.3,0.8 0.3,1.44 0,1.86 -2.4,1.86 -1,0 -3.5,-0.5 -2.5,-0.54 -3.4,-0.54 -1.3,0 -1.5,0.1 -0.4,0.2 -0.4,1.2 0,2.2 0.6,6.9 0.7,5.86 1.6,6.13 -0.4,0.35 -0.4,1.1 -1.2,0.7 -2.6,0.7 -1.4,0 -2,-3.9 -0.2,-1.36 -0.5,-7.76 -0.2,-4.6 -0.8,-5.5 -0.3,-0.47 -4.3,-0.35 -1,0 -1.6,0.1 -0.5,0 -0.3,0 -0.8,0 -1.2,-0.7 -0.5,-1.3 -0.5,-1.4 0,-1.44 4.1,-2 1.6,-0.16 4.7,-0.5 0,-0.85 -0.1,-2.56 0,-1.75 0,-2.6 0,-4.35 2.1,-4.35 0.5,0 1.1,0.6 0.6,0.6 0.6,1.1 v 7.9 q 1.1,1.2 5,1.7 3.9,0.5 5.3,1.86 z"
style="fill:#333333"
id="path18" />
<path
d="m 251.2,40.2 q -2,4.1 -3.2,7 -0.1,0.3 -1.5,4.5 -0.5,1.36 -1,4.2 -0.5,2.83 -1,4.2 -1,2.83 -2.4,2.64 -1.4,-0.2 -1.5,-1.6 -0.1,-0.2 -0.1,-0.5 0,-0.16 0.3,-1.5 1.1,-5.04 1.1,-6.44 0,-0.54 -0.1,-0.74 -1.4,-2.44 -4.1,-7.4 -2.7,-4.97 -2.4,-7.7 1.4,-1.36 2.1,-1.36 0.4,0 1,0.6 0.6,0.6 0.7,1.1 0.9,6.2 4.9,11.1 1,-1.8 1.9,-4.04 0.5,-1.4 1.6,-4.15 1.8,-4.46 3.4,-4.46 0.2,0 0.4,0.1 0.8,0.3 1.2,2.8 z"
style="fill:#333333"
id="path19" />
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="tagger">
<g
id="g1">
<path
id="path4"
style="fill:#333333"
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path5"
style="fill:#333333"
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path6"
style="fill:#333333"
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
<g
id="g2"
transform="rotate(30,150,318.19)">
<path
id="path7"
style="fill:#333333"
d="m 111.4,83.335 -9.526,5.5 2.5,4.33 9.526,-5.5 z m -33.775,19.5 -9.526,5.5 2.5,4.33 9.526,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path8"
style="fill:#333333"
d="M 88.5,73 V 84 h 5 V 73 Z m 0,39 v 11 h 5 V 112 Z"
sodipodi:nodetypes="cccccccccc" />
<path
id="path9"
style="fill:#333333"
d="m 68.1,87.665 9.526,5.5 2.5,-4.33 -9.526,-5.5 z m 33.775,19.5 9.527,5.5 2.5,-4.33 -9.527,-5.5 z"
sodipodi:nodetypes="cccccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

@@ -108,6 +108,18 @@ variable "post_install_script" {
default = null
}
variable "continue" {
type = bool
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise creates new session. When false, always starts fresh."
default = true
}
variable "resume_session_id" {
type = string
description = "Resume a specific session by ID. Takes precedence over automatic session detection."
default = ""
}
locals {
app_slug = "cursorcli"
install_script = file("${path.module}/scripts/install.sh")
@@ -158,6 +170,8 @@ module "agentapi" {
ARG_FORCE='${var.force}' \
ARG_MODEL='${var.model}' \
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
ARG_CONTINUE='${var.continue}' \
ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
ARG_MODULE_DIR_NAME='${local.module_dir_name}' \
ARG_FOLDER='${var.folder}' \
/tmp/start.sh
@@ -9,6 +9,7 @@ command_exists() {
# Inputs
ARG_INSTALL=${ARG_INSTALL:-true}
ARG_VERSION=${ARG_VERSION:-latest}
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.cursor-cli-module}
ARG_FOLDER=${ARG_FOLDER:-$HOME}
ARG_CODER_MCP_APP_STATUS_SLUG=${ARG_CODER_MCP_APP_STATUS_SLUG:-}
@@ -20,6 +21,7 @@ ARG_WORKSPACE_RULES_JSON=$(echo -n "$ARG_WORKSPACE_RULES_JSON" | base64 -d)
echo "--------------------------------"
echo "install: $ARG_INSTALL"
echo "version: $ARG_VERSION"
echo "folder: $ARG_FOLDER"
echo "coder_mcp_app_status_slug: $ARG_CODER_MCP_APP_STATUS_SLUG"
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
@@ -10,24 +10,36 @@ command_exists() {
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
ARG_FORCE=${ARG_FORCE:-false}
ARG_MODEL=${ARG_MODEL:-}
ARG_OUTPUT_FORMAT=${ARG_OUTPUT_FORMAT:-json}
ARG_CONTINUE=${ARG_CONTINUE:-true}
ARG_RESUME_SESSION_ID=${ARG_RESUME_SESSION_ID:-}
ARG_MODULE_DIR_NAME=${ARG_MODULE_DIR_NAME:-.cursor-cli-module}
ARG_FOLDER=${ARG_FOLDER:-$HOME}
echo "--------------------------------"
echo "install: $ARG_INSTALL"
echo "version: $ARG_VERSION"
echo "folder: $ARG_FOLDER"
echo "ai_prompt: $ARG_AI_PROMPT"
echo "force: $ARG_FORCE"
echo "model: $ARG_MODEL"
echo "output_format: $ARG_OUTPUT_FORMAT"
echo "continue: $ARG_CONTINUE"
echo "resume_session_id: ${ARG_RESUME_SESSION_ID:-<none>}"
echo "module_dir_name: $ARG_MODULE_DIR_NAME"
echo "folder: $ARG_FOLDER"
echo "--------------------------------"
mkdir -p "$HOME/$ARG_MODULE_DIR_NAME"
SESSION_FILE="$HOME/$ARG_MODULE_DIR_NAME/session_id.txt"
get_stored_session_id() {
if [ -f "$SESSION_FILE" ]; then
cat "$SESSION_FILE" | tr -d '\n' || echo ""
fi
}
store_session_id() {
echo "$1" > "$SESSION_FILE"
echo "Stored session ID: $1"
}
# Find cursor agent cli
if command_exists cursor-agent; then
CURSOR_CMD=cursor-agent
@@ -56,6 +68,34 @@ if [ "$ARG_FORCE" = "true" ]; then
ARGS+=("-f")
fi
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
echo "Using explicit resume_session_id: $ARG_RESUME_SESSION_ID"
ARGS+=("--resume" "$ARG_RESUME_SESSION_ID")
elif [ "$ARG_CONTINUE" = "true" ]; then
STORED_SESSION=$(get_stored_session_id)
if [ -n "$STORED_SESSION" ]; then
echo "Found existing session: $STORED_SESSION"
echo "Resuming conversation..."
ARGS+=("--resume" "$STORED_SESSION")
else
echo "No existing session found"
echo "Creating new session for this workspace..."
NEW_SESSION=$($CURSOR_CMD create-chat 2>&1 | tr -d '\n')
if [ -n "$NEW_SESSION" ]; then
store_session_id "$NEW_SESSION"
ARGS+=("--resume" "$NEW_SESSION")
else
echo "Warning: Failed to create session, continuing without session resume"
fi
fi
else
echo "Continue disabled, starting fresh session"
rm -f "$SESSION_FILE"
fi
if [ -n "$ARG_AI_PROMPT" ]; then
printf "AI prompt provided\n"
ARGS+=("Complete the task at hand in one go. 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")
@@ -6,6 +6,11 @@ if [[ "$1" == "--version" ]]; then
exit 0
fi
if [[ "$1" == "create-chat" ]]; then
echo "12345678-1234-1234-1234-123456789abc"
exit 0
fi
set -e
while true; do
+7 -7
View File
@@ -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 = "3.2.2"
version = "3.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -51,7 +51,7 @@ module "claude-code" {
boundary_log_level = "WARN"
boundary_additional_allowed_urls = ["GET *google.com"]
boundary_proxy_port = "8087"
version = "3.2.1"
version = "3.3.0"
}
```
@@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.2.2"
version = "3.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
@@ -106,7 +106,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 = "3.2.2"
version = "3.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder"
install_claude_code = true
@@ -129,7 +129,7 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.2.2"
version = "3.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
@@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.2.2"
version = "3.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.2.2"
version = "3.3.0"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
@@ -228,6 +228,18 @@ variable "boundary_proxy_port" {
default = "8087"
}
variable "enable_boundary_pprof" {
type = bool
description = "Whether to enable coder boundary pprof server"
default = false
}
variable "boundary_pprof_port" {
type = string
description = "Port for pprof server used by Boundary"
default = "6067"
}
resource "coder_env" "claude_code_md_path" {
count = var.claude_md_path == "" ? 0 : 1
@@ -343,6 +355,8 @@ module "agentapi" {
ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \
ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join(" ", var.boundary_additional_allowed_urls)}' \
ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \
ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \
ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \
ARG_CODER_HOST='${local.coder_host}' \
/tmp/start.sh
EOT
@@ -22,6 +22,8 @@ ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"}
ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"}
ARG_BOUNDARY_LOG_LEVEL=${ARG_BOUNDARY_LOG_LEVEL:-"WARN"}
ARG_BOUNDARY_PROXY_PORT=${ARG_BOUNDARY_PROXY_PORT:-"8087"}
ARG_ENABLE_BOUNDARY_PPROF=${ARG_ENABLE_BOUNDARY_PPROF:-false}
ARG_BOUNDARY_PPROF_PORT=${ARG_BOUNDARY_PPROF_PORT:-"6067"}
ARG_CODER_HOST=${ARG_CODER_HOST:-}
echo "--------------------------------"
@@ -155,6 +157,12 @@ function start_agentapi() {
# Set log level for boundary
BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL)
if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then
# Enable boundary pprof server on specified port
BOUNDARY_ARGS+=(--pprof)
BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT})
fi
# Remove --dangerously-skip-permissions from ARGS when using boundary (it doesn't work with elevated permissions)
# Create a new array without the dangerous permissions flag
CLAUDE_ARGS=()
+5 -5
View File
@@ -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.0"
version = "1.1.1"
agent_id = coder_agent.example.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.0"
version = "1.1.1"
agent_id = coder_agent.example.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.0"
version = "1.1.1"
agent_id = coder_agent.example.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.0"
version = "1.1.1"
agent_id = coder_agent.example.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.0"
version = "1.1.1"
agent_id = coder_agent.example.id
settings = jsonencode({
+1
View File
@@ -73,6 +73,7 @@ resource "coder_script" "zed_settings" {
icon = "/icon/zed.svg"
run_on_start = true
script = <<-EOT
#!/bin/sh
set -eu
SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}'
if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then
@@ -264,7 +264,7 @@ resource "kubernetes_deployment" "main" {
container {
name = "dev"
image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image
image_pull_policy = "Always"
image_pull_policy = "IfNotPresent"
security_context {
privileged = true
}
@@ -455,4 +455,4 @@ resource "coder_metadata" "container_info" {
key = "cache repo"
value = var.cache_repo == "" ? "not enabled" : var.cache_repo
}
}
}
@@ -152,7 +152,7 @@ resource "kubernetes_pod" "main" {
name = "dev"
# We highly recommend pinning this to a specific release of envbox, as the latest tag may change.
image = "ghcr.io/coder/envbox:latest"
image_pull_policy = "Always"
image_pull_policy = "IfNotPresent"
command = ["/envbox", "docker"]
security_context {
@@ -310,4 +310,4 @@ resource "kubernetes_pod" "main" {
}
}
}
}
}
+1 -1
View File
@@ -287,7 +287,7 @@ resource "kubernetes_deployment" "main" {
container {
name = "dev"
image = "codercom/enterprise-base:ubuntu"
image_pull_policy = "Always"
image_pull_policy = "IfNotPresent"
command = ["sh", "-c", coder_agent.main.init_script]
security_context {
run_as_user = "1000"
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+11
View File
@@ -0,0 +1,11 @@
---
display_name: "Austin"
bio: "IT Pro by day, script kiddie at night."
avatar: "./.images/avatar.png"
github: "djarbz"
status: "community"
---
# Austin
I like to program as a hobby.
@@ -0,0 +1,68 @@
---
display_name: copyparty
description: A web based file explorer alternative to Filebrowser.
icon: ../../../../.icons/copyparty.svg
verified: false
tags: [files, filebrowser, web, copyparty]
---
# copyparty
<!-- Describes what this module does -->
This module installs Copyparty, an alternative to Filebrowser.
[Copyparty](https://github.com/9001/copyparty) is a portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps
```tf
module "copyparty" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/djarbz/copyparty/coder"
version = "1.0.0"
}
```
<!-- Add a screencast or screenshot here put them in .images directory -->
![copyparty-browser-fs8](../../.images/copyparty_screenshot.png)
## Examples
### Example 1
Some basic command line options:
```tf
module "copyparty" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/djarbz/copyparty/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
arguments = [
"-v", "/home/coder/:/home:r", # Share home directory (read-only)
"-v", "${local.repo_dir}:/repo:rw", # Share project directory (read-write)
"-e2dsa", # Enables general file indexing"
]
}
```
### Example 2
```tf
module "copyparty" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/djarbz/copyparty/coder"
version = "1.0.0"
agent_id = coder_agent.example.id
subdomain = true
arguments = [
"-v", "/tmp:/tmp:r", # Share tmp directory (read-only)
"-v", "/home/coder/:/home:rw", # Share home directory (read-write)
"-v", "${local.root_dir}:/work:A:c,dotsrch", # Share work directory (All Perms)
"-e2dsa", # Enables general file indexing"
"--re-maxage", "900", # Rescan filesystem for changes every SEC
"--see-dots", # Show dotfiles by default if user has correct permissions on volume
"--xff-src=lan", # List of trusted reverse-proxy CIDRs (comma-separated) or `lan` for private IPs.
"--rproxy", "1", # Which ip to associate clients with, index of X-FWD IP.
]
}
```
@@ -0,0 +1,181 @@
# --- Test Case 1: Required Variables ---
run "plan_with_required_vars" {
command = plan
variables {
agent_id = "example-agent-id"
}
}
# --- Test Case 2: Coder App URL uses custom port ---
run "app_url_uses_port" {
command = plan
variables {
agent_id = "example-agent-id"
port = 19999
}
assert {
condition = resource.coder_app.copyparty.url == "http://localhost:19999"
error_message = "Expected copyparty app URL to include configured port"
}
}
# --- Test Case 3: Default Values ---
run "test_defaults" {
# This run block applies the module with default values
# (except for the required 'agent_id' provided above).
variables {
agent_id = "example-agent-id"
}
# --- Asserts for coder_app "copyparty" ---
assert {
condition = resource.coder_app.copyparty.display_name == "copyparty"
error_message = "Default display_name is incorrect"
}
assert {
condition = resource.coder_app.copyparty.slug == "copyparty"
error_message = "Default slug is incorrect"
}
assert {
condition = resource.coder_app.copyparty.url == "http://localhost:3923"
error_message = "Default URL is incorrect, expected port 3923"
}
assert {
condition = resource.coder_app.copyparty.subdomain == false
error_message = "Default subdomain should be false"
}
assert {
condition = resource.coder_app.copyparty.share == "owner"
error_message = "Default share value should be 'owner'"
}
assert {
condition = resource.coder_app.copyparty.open_in == "slim-window"
error_message = "Default open_in value should be 'slim-window'"
}
# --- Asserts for coder_script "copyparty" ---
assert {
condition = coder_script.copyparty.display_name == "copyparty"
error_message = "Script display_name is incorrect"
}
# Check rendered script content (this assumes your run.sh uses the variables)
assert {
condition = strcontains(coder_script.copyparty.script, "PORT=\"3923\"")
error_message = "Script content does not reflect default port"
}
assert {
condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/tmp/copyparty.log\"")
error_message = "Script content does not reflect default log_path"
}
assert {
condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"\"")
error_message = "Script content does not reflect default empty arguments"
}
}
# --- Test Case 4: Custom Values ---
run "test_custom_values" {
# Override default variables for this specific run
variables {
agent_id = "example-agent-id"
port = 8080
slug = "my-custom-app"
display_name = "My Custom App"
share = "authenticated"
open_in = "tab"
pinned_version = "v1.2.3"
arguments = ["--verbose", "-v"]
log_path = "/var/log/custom.log"
}
# --- Asserts for coder_app "copyparty" ---
assert {
condition = resource.coder_app.copyparty.display_name == "My Custom App"
error_message = "Custom display_name was not applied"
}
assert {
condition = resource.coder_app.copyparty.slug == "my-custom-app"
error_message = "Custom slug was not applied"
}
assert {
condition = resource.coder_app.copyparty.url == "http://localhost:8080"
error_message = "Custom port was not applied to URL"
}
assert {
condition = resource.coder_app.copyparty.share == "authenticated"
error_message = "Custom share value was not applied"
}
assert {
condition = resource.coder_app.copyparty.open_in == "tab"
error_message = "Custom open_in value was not applied"
}
# --- Asserts for coder_script "copyparty" ---
assert {
condition = strcontains(coder_script.copyparty.script, "PORT=\"8080\"")
error_message = "Script content does not reflect custom port"
}
assert {
condition = strcontains(coder_script.copyparty.script, "PINNED_VERSION=\"v1.2.3\"")
error_message = "Script content does not reflect custom pinned_version"
}
assert {
condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"--verbose,-v\"")
error_message = "Script content does not reflect custom arguments"
}
assert {
condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/var/log/custom.log\"")
error_message = "Script content does not reflect custom log_path"
}
}
# --- Test Case 5: Validation Failure (open_in) ---
run "test_invalid_open_in" {
# This is a 'plan' test that expects a failure
command = plan
variables {
agent_id = "example-agent-id"
open_in = "invalid-value"
}
# Expect this plan to fail due to the validation rule in 'var.open_in'
expect_failures = [
var.open_in,
]
}
# --- Test Case 6: Validation Failure (share) ---
run "test_invalid_share" {
# This is a 'plan' test that expects a failure
command = plan
variables {
agent_id = "example-agent-id"
share = "everyone" # This is not 'owner', 'authenticated', or 'public'
}
# Expect this plan to fail due to the validation rule in 'var.share'
expect_failures = [
var.share,
]
}
+174
View File
@@ -0,0 +1,174 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
locals {
# A built-in icon like "/icon/code.svg" or a full URL of icon
icon_url = "/icon/copyparty.svg"
# a map of all possible values
# options = {
# "Option 1" = {
# "name" = "Option 1",
# "value" = "1"
# "icon" = "/emojis/1.png"
# }
# "Option 2" = {
# "name" = "Option 2",
# "value" = "2"
# "icon" = "/emojis/2.png"
# }
# }
}
# Add required variables for your modules and remove any unneeded variables
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "log_path" {
type = string
description = "The path to log copyparty to."
default = "/tmp/copyparty.log"
}
variable "port" {
type = number
description = "ports to listen on (comma/range); ignored for unix-sockets (default: 3923)"
default = 3923
}
variable "slug" {
type = string
description = "The slug for the copyparty application."
default = "copyparty"
}
variable "display_name" {
type = string
description = "The display name for the copyparty application."
default = "copyparty"
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "open_in" {
type = string
description = <<-EOT
Determines where the app will be opened. Valid values are `"tab"` and `"slim-window" (default)`.
`"tab"` opens in a new tab in the same browser window.
`"slim-window"` opens a new browser window without navigation controls.
EOT
default = "slim-window"
validation {
condition = contains(["tab", "slim-window"], var.open_in)
error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'."
}
}
variable "subdomain" {
type = bool
description = <<-EOT
Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder.
If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible.
EOT
default = false
}
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 "mutable" {
# type = bool
# description = "Whether the parameter is mutable."
# default = true
# }
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
}
# Add other variables here
variable "pinned_version" {
type = string
description = "Install a specific version in semver format (v1.19.16)."
default = ""
}
variable "arguments" {
type = list(string)
description = "A list of arguments to pass to the application."
default = []
}
resource "coder_script" "copyparty" {
agent_id = var.agent_id
display_name = "copyparty"
icon = local.icon_url
script = templatefile("${path.module}/run.sh", {
LOG_PATH : var.log_path,
PORT : var.port,
PINNED_VERSION : var.pinned_version,
ARGUMENTS : join(",", var.arguments),
})
run_on_start = true
run_on_stop = false
}
resource "coder_app" "copyparty" {
agent_id = var.agent_id
slug = var.slug
display_name = var.display_name
url = "http://localhost:${var.port}"
icon = local.icon_url
subdomain = var.subdomain
share = var.share
order = var.order
group = var.group
open_in = var.open_in
# Remove if the app does not have a healthcheck endpoint
healthcheck {
url = "http://localhost:${var.port}"
interval = 5
threshold = 6
}
}
# data "coder_parameter" "copyparty" {
# type = "list(string)"
# name = "copyparty"
# display_name = "copyparty"
# icon = local.icon_url
# mutable = var.mutable
# default = local.options["Option 1"]["value"]
# dynamic "option" {
# for_each = local.options
# content {
# icon = option.value.icon
# name = option.value.name
# value = option.value.value
# }
# }
# }
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# Convert templated variables to shell variables
# This variable is assigned to itself, so the assignment does nothing.
# shellcheck disable=SC2269
LOG_PATH="${LOG_PATH}"
# Ports to listen on (comma/range); ignored for unix-sockets (default: 3923)
PORT="${PORT}"
# Pinned version (e.g., v1.19.16); overrides latest release discovery if set
PINNED_VERSION="${PINNED_VERSION}"
# Custom CLI Arguments# The variable from Terraform is a single, comma-separated string.
# We need to split it into a proper bash array using the comma (,) as the delimiter.
IFS=',' read -r -a ARGUMENTS <<< "${ARGUMENTS}"
# VARIABLE appears unused. Verify use (or export if used externally).
# shellcheck disable=SC2034
MODULE_NAME="Copyparty"
# VARIABLE appears unused. Verify use (or export if used externally).
# shellcheck disable=SC2034
BOLD='\033[0;1m'
printf '%sInstalling %s ...\n\n' "$${BOLD}" "$${MODULE_NAME}"
# Add code here
# Use variables from the templatefile function in main.tf
# e.g. LOG_PATH, PORT, etc.
printf "🐍 Verifying Python 3 installation...\n"
if ! command -v python3 &> /dev/null; then
printf "❌ Python3 could not be found. Please install it to continue.\n"
exit 1
fi
printf "✅ Python3 is installed.\n\n"
RELEASE_TO_INSTALL=""
# Install provided version to pin, otherwise discover latest github release from `https://github.com/9001/copyparty`.
if [[ -n "$${PINNED_VERSION}" ]]; then
printf "📌 Pinned version specified: %s\n" "$${PINNED_VERSION}"
# Verify that it is in v#.#.# format
if [[ ! "$${PINNED_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
printf "❌ Invalid format for PINNED_VERSION. Expected 'v#.#.#' (e.g., v1.19.16).\n"
exit 1
fi
RELEASE_TO_INSTALL="$${PINNED_VERSION}"
printf "✅ Using pinned version %s.\n\n" "$${RELEASE_TO_INSTALL}"
else
printf "🔎 Discovering latest release from GitHub...\n"
# Use curl to get the latest release tag from the GitHub API and sed to parse it
LATEST_RELEASE=$(curl -fsSL https://api.github.com/repos/9001/copyparty/releases/latest | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\1/')
if [[ -z "$${LATEST_RELEASE}" ]]; then
printf "❌ Could not determine the latest release. Please check your internet connection.\n"
exit 1
fi
RELEASE_TO_INSTALL="$${LATEST_RELEASE}"
printf "🏷️ Latest release is %s.\n\n" "$${RELEASE_TO_INSTALL}"
fi
# Download appropriate release version assets: `copyparty-sfx.py` and `helptext.html`.
printf "🚀 Downloading copyparty v%s...\n" "$${RELEASE_TO_INSTALL}"
DOWNLOAD_URL="https://github.com/9001/copyparty/releases/download/$${RELEASE_TO_INSTALL}"
printf "⏬ Downloading copyparty-sfx.py...\n"
if ! curl -fsSL -o /tmp/copyparty-sfx.py "$${DOWNLOAD_URL}/copyparty-sfx.py"; then
printf "❌ Failed to download copyparty-sfx.py.\n"
exit 1
fi
printf "⏬ Downloading helptext.html...\n"
if ! curl -fsSL -o /tmp/helptext.html "$${DOWNLOAD_URL}/helptext.html"; then
# This is not a fatal error, just a warning.
printf "⚠️ Could not download helptext.html. The application will still work.\n"
fi
chmod +x /tmp/copyparty-sfx.py
printf "✅ Download complete.\n\n"
printf "🥳 Installation complete!\n\n"
# Build a clean, quoted string of the command for logging purposes only.
log_command="python3 /tmp/copyparty-sfx.py -p '$${PORT}'"
for arg in "$${ARGUMENTS[@]}"; do
# printf "DEBUG: ARG [$${arg}]\n"
log_command+=" '$${arg}'"
done
# Clear the log file and write the header and command string using printf.
{
printf "=== Starting copyparty at %s ===\n" "$(date)"
printf "EXECUTING: %s\n" "$${log_command}"
} > "$${LOG_PATH}"
printf "👷 Starting %s in background...\n\n" "$${MODULE_NAME}"
# Execute the actual command using the robust array expansion.
# Then, append its output (stdout and stderr) to the log file.
python3 /tmp/copyparty-sfx.py -p "$${PORT}" "$${ARGUMENTS[@]}" >> "$${LOG_PATH}" 2>&1 &
printf "✅ Service started. Check logs at %s\n\n" "$${LOG_PATH}"
@@ -0,0 +1,70 @@
---
display_name: "NFS K8s Deployment"
description: "Mount an NFS share to a Coder K8s workspace"
icon: "../../../../.icons/folder.svg"
verified: false
tags: ["kubernetes", "shared-dir", "nfs"]
---
# NFS K8s Deployment
This template provisions a Coder workspace as a Kubernetes Deployment, with an NFS share mounted
as a volume. The NFS share will synchronize the server-side files onto the client (Coder workspace)
When you stop the Coder workspace and rebuild, the NFS share will be re-mounted, and the changes persisted.
Note the `volume` and `volume_mount` blocks in the deployment and container spec,
respectively:
```terraform
resource "kubernetes_deployment" "main" {
spec {
template {
spec {
container {
volume_mount {
mount_path = data.coder_parameter.nfs_mount_path.value # mount path in the container
name = "nfs-share"
}
}
volume {
name = "nfs-share"
nfs {
path = data.coder_parameter.nfs_mount_path.value # path to be exported from the server
server = data.coder_parameter.nfs_server.value # server IP address
}
}
}
}
}
}
```
## server-side configuration
1. Create an NFS mount on the server for the clients to access:
```console
export NFS_MNT_PATH=/mnt/nfs_share
# Create directory to shaare
sudo mkdir -p $NFS_MNT_PATH
# Assign UID & GIDs access
sudo chown -R uid:gid $NFS_MNT_PATH
sudo chmod 777 $NFS_MNT_PATH
```
1. Grant access to the client by updating the `/etc/exports` file, which
controls the directories shared with remote clients. See
[Red Hat's docs for more information about the configuration options](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/5/html/deployment_guide/s1-nfs-server-config-exports).
```console
# Provides read/write access to clients accessing the NFS from any IP address.
/mnt/nfs_share *(rw,sync,no_subtree_check)
```
1. Export the NFS file share directory. You must do this every time you change
`/etc/exports`.
```console
sudo exportfs -a
sudo systemctl restart <nfs-package>
```
@@ -0,0 +1,348 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
}
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
provider "coder" {
}
provider "kubernetes" {
config_path = var.use_kubeconfig == true ? "~/.kube/config" : null
}
variable "use_kubeconfig" {
type = bool
description = <<-EOF
Use host kubeconfig? (true/false)
Set this to false if the Coder host is itself running as a Pod on the same
Kubernetes cluster as you are deploying workspaces to.
Set this to true if the Coder host is running outside the Kubernetes cluster
for workspaces. A valid "~/.kube/config" must be present on the Coder host.
EOF
default = false
}
variable "namespace" {
type = string
description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace."
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
data "coder_parameter" "cpu" {
name = "cpu"
display_name = "CPU"
description = "The number of CPU cores"
default = "2"
icon = "/icon/memory.svg"
mutable = true
option {
name = "2 Cores"
value = "2"
}
option {
name = "4 Cores"
value = "4"
}
option {
name = "6 Cores"
value = "6"
}
option {
name = "8 Cores"
value = "8"
}
}
data "coder_parameter" "memory" {
name = "memory"
display_name = "Memory"
description = "The amount of memory in GB"
default = "2"
icon = "/icon/memory.svg"
mutable = true
option {
name = "2 GB"
value = "2"
}
option {
name = "4 GB"
value = "4"
}
option {
name = "6 GB"
value = "6"
}
option {
name = "8 GB"
value = "8"
}
}
data "coder_parameter" "home_disk_size" {
name = "home_disk_size"
display_name = "Home disk size"
description = "The size of the home disk in GB"
default = "10"
type = "number"
icon = "/emojis/1f4be.png"
mutable = false
validation {
min = 1
max = 99999
}
}
data "coder_parameter" "nfs_server" {
name = "nfs_server"
type = "string"
display_name = "NFS Server IP"
description = "The NFS server IP address to use for the workspace"
}
data "coder_parameter" "nfs_mount_path" {
name = "nfs_mount_path"
type = "string"
display_name = "NFS Mount Path"
description = "The path in your workspace container to mount the NFS share to"
default = "/mnt/nfs-share"
validation {
regex = "^/[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*$"
error = "NFS mount path must be a valid path in your workspace container"
}
}
resource "coder_agent" "coder" {
os = "linux"
arch = "amd64"
# The following metadata blocks are optional. They are used to display
# information about your workspace in the dashboard. You can remove them
# if you don't want to display any information.
# For basic resources, you can use the `coder stat` command.
# If you need more control, you can write your own script.
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 = "Home Disk"
key = "3_home_disk"
script = "coder stat disk --path $${HOME}"
interval = 60
timeout = 1
}
metadata {
display_name = "CPU Usage (Host)"
key = "4_cpu_usage_host"
script = "coder stat cpu --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Memory Usage (Host)"
key = "5_mem_usage_host"
script = "coder stat mem --host"
interval = 10
timeout = 1
}
metadata {
display_name = "Load Average (Host)"
key = "6_load_host"
# get load avg scaled by number of cores
script = <<EOT
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
EOT
interval = 60
timeout = 1
}
}
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.3.1"
agent_id = coder_agent.coder.id
accept_license = true
}
resource "kubernetes_deployment" "main" {
count = data.coder_workspace.me.start_count
depends_on = [
kubernetes_persistent_volume_claim.home
]
wait_for_rollout = false
metadata {
name = "coder-${data.coder_workspace.me.id}"
namespace = var.namespace
labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
"app.kubernetes.io/part-of" = "coder"
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
annotations = {
"com.coder.user.email" = data.coder_workspace_owner.me.email
}
}
spec {
replicas = 1
selector {
match_labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
"app.kubernetes.io/part-of" = "coder"
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
}
strategy {
type = "Recreate"
}
template {
metadata {
labels = {
"app.kubernetes.io/name" = "coder-workspace"
"app.kubernetes.io/instance" = "coder-workspace-${data.coder_workspace.me.id}"
"app.kubernetes.io/part-of" = "coder"
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
}
spec {
container {
name = "dev"
image = "codercom/enterprise-base:ubuntu"
image_pull_policy = "Always"
command = ["sh", "-c", coder_agent.coder.init_script]
env {
name = "CODER_AGENT_TOKEN"
value = coder_agent.coder.token
}
resources {
requests = {
"cpu" = "250m"
"memory" = "512Mi"
}
limits = {
"cpu" = "${data.coder_parameter.cpu.value}"
"memory" = "${data.coder_parameter.memory.value}Gi"
}
}
volume_mount {
mount_path = "/home/${lower(data.coder_workspace_owner.me.name)}"
name = "home"
read_only = false
}
volume_mount {
mount_path = data.coder_parameter.nfs_mount_path.value
name = "nfs-share"
}
}
volume {
name = "home"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.home.metadata.0.name
read_only = false
}
}
volume {
name = "nfs-share"
nfs {
path = data.coder_parameter.nfs_mount_path.value
server = data.coder_parameter.nfs_server.value
}
}
affinity {
// This affinity attempts to spread out all workspace pods evenly across
// nodes.
pod_anti_affinity {
preferred_during_scheduling_ignored_during_execution {
weight = 1
pod_affinity_term {
topology_key = "kubernetes.io/hostname"
label_selector {
match_expressions {
key = "app.kubernetes.io/name"
operator = "In"
values = ["coder-workspace"]
}
}
}
}
}
}
}
}
}
}
resource "kubernetes_persistent_volume_claim" "home" {
metadata {
name = "${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace_owner.me.name)}-home"
namespace = var.namespace
labels = {
"app.kubernetes.io/name" = "coder-pvc"
"app.kubernetes.io/instance" = "coder-pvc-${data.coder_workspace.me.id}"
"app.kubernetes.io/part-of" = "coder"
//Coder-specific labels.
"com.coder.resource" = "true"
"com.coder.workspace.id" = data.coder_workspace.me.id
"com.coder.workspace.name" = data.coder_workspace.me.name
"com.coder.user.id" = data.coder_workspace_owner.me.id
"com.coder.user.username" = data.coder_workspace_owner.me.name
}
annotations = {
"com.coder.user.email" = data.coder_workspace_owner.me.email
}
}
wait_until_bound = false
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "${data.coder_parameter.home_disk_size.value}Gi"
}
}
}
}