mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3ff43c0a6 | |||
| 30123e7ea3 | |||
| f7c1be71f7 | |||
| 19519a0a13 | |||
| 63e42283ce | |||
| 0c5a8a2354 | |||
| 51ec6e3212 | |||
| 843b1f1e5a | |||
| 583918bfef | |||
| a1786a09ea | |||
| a35986d7df | |||
| e34320cb0b | |||
| ca7bc42946 |
@@ -5,6 +5,8 @@ Hashi = "Hashi"
|
||||
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
|
||||
@@ -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 |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M7.05 40q-1.2 0-2.1-.925-.9-.925-.9-2.075V11q0-1.15.9-2.075Q5.85 8 7.05 8h14l3 3h17q1.15 0 2.075.925.925.925.925 2.075v23q0 1.15-.925 2.075Q42.2 40 41.05 40Zm0-29v26h34V14H22.8l-3-3H7.05Zm0 0v26Z"/></svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,163 @@
|
||||
---
|
||||
display_name: Archive
|
||||
description: Create automated and user-invocable scripts that archive and extract selected files/directories with optional compression (gzip or zstd).
|
||||
icon: ../../../../.icons/folder.svg
|
||||
verified: false
|
||||
tags: [backup, archive, tar, helper]
|
||||
---
|
||||
|
||||
# Archive
|
||||
|
||||
This module installs small, robust scripts in your workspace to create and extract tar archives from a list of files and directories. It supports optional compression (gzip or zstd). The create command prints only the resulting archive path to stdout; operational logs go to stderr. An optional stop hook can also create an archive automatically when the workspace stops, and an optional start hook can wait for an archive on-disk and extract it on start.
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
paths = ["./projects", "./code"]
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Installs two commands into the workspace `$PATH`: `coder-archive-create` and `coder-archive-extract`.
|
||||
- Creates a single `.tar`, `.tar.gz`, or `.tar.zst` containing selected paths (depends on `tar`).
|
||||
- Optional compression: `gzip`, `zstd` (depends on `gzip` or `zstd`).
|
||||
- Stores defaults so commands can be run without arguments (supports overriding via CLI flags).
|
||||
- Logs and status messages go to stderr, the create command prints only the final archive path to stdout.
|
||||
- Optional:
|
||||
- `create_on_stop` to create an archive automatically when the workspace stops.
|
||||
- `extract_on_start` to wait for an archive to appear and extract it on start.
|
||||
|
||||
> [!WARNING]
|
||||
> The `create_on_stop` feature uses the `coder_script` `run_on_stop` which may not work as expected on certain templates without additional provider configuration. The agent may be terminated before the script completes. See [coder/coder#6174](https://github.com/coder/coder/issues/6174) for provider-specific workarounds and [coder/coder#6175](https://github.com/coder/coder/issues/6175) for tracking a fix.
|
||||
|
||||
## Usage
|
||||
|
||||
Basic example:
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
# Paths to include in the archive (files or directories).
|
||||
directory = "~"
|
||||
paths = [
|
||||
"./projects",
|
||||
"./code",
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Customize compression and output:
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
directory = "/"
|
||||
paths = ["/etc", "/home"]
|
||||
compression = "zstd" # "gzip" | "zstd" | "none"
|
||||
output_dir = "/tmp/backup" # defaults to /tmp
|
||||
archive_name = "my-backup" # base name (extension is inferred from compression)
|
||||
}
|
||||
```
|
||||
|
||||
Enable auto-archive on stop:
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
# Creates /tmp/coder-archive.tar.gz of the users home directory (defaults).
|
||||
create_on_stop = true
|
||||
}
|
||||
```
|
||||
|
||||
Extract on start:
|
||||
|
||||
```tf
|
||||
module "archive" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/archive/coder"
|
||||
version = "0.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
# Where to look for the archive file to extract:
|
||||
output_dir = "/tmp"
|
||||
archive_name = "my-archive"
|
||||
compression = "gzip"
|
||||
|
||||
# Waits up to 5 minutes for /tmp/my-archive.tar.gz to be present, note that
|
||||
# using a long timeout will delay every workspace start by this much until the
|
||||
# archive is present.
|
||||
extract_on_start = true
|
||||
extract_wait_timeout_seconds = 300
|
||||
}
|
||||
```
|
||||
|
||||
## Command usage
|
||||
|
||||
The installer writes the following files:
|
||||
|
||||
- `$CODER_SCRIPT_DATA_DIR/archive-lib.sh`
|
||||
- `$CODER_SCRIPT_BIN_DIR/coder-archive-create`
|
||||
- `$CODER_SCRIPT_BIN_DIR/coder-archive-extract`
|
||||
|
||||
Create usage:
|
||||
|
||||
```console
|
||||
coder-archive-create [OPTIONS] [PATHS...]
|
||||
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
|
||||
-C, --directory <DIRECTORY> Change to directory for archiving (default from module)
|
||||
-f, --file <ARCHIVE> Output archive file (default from module)
|
||||
-h, --help Show help
|
||||
```
|
||||
|
||||
Extract usage:
|
||||
|
||||
```console
|
||||
coder-archive-extract [OPTIONS]
|
||||
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
|
||||
-C, --directory <DIRECTORY> Extract into directory (default from module)
|
||||
-f, --file <ARCHIVE> Archive file to extract (default from module)
|
||||
-h, --help Show help
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
- Use Terraform defaults:
|
||||
|
||||
```
|
||||
coder-archive-create
|
||||
```
|
||||
|
||||
- Override compression and output file at runtime:
|
||||
|
||||
```
|
||||
coder-archive-create --compression zstd --file /tmp/backups/archive.tar.zst
|
||||
```
|
||||
|
||||
- Add extra paths on the fly (in addition to the Terraform defaults):
|
||||
|
||||
```
|
||||
coder-archive-create /etc/hosts
|
||||
```
|
||||
|
||||
- Extract an archive into a directory:
|
||||
|
||||
```
|
||||
coder-archive-extract --file /tmp/backups/archive.tar.gz --directory /tmp/restore
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
mock_provider "coder" {}
|
||||
|
||||
run "apply_defaults" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "agent-123"
|
||||
paths = ["~/project", "/etc/hosts"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.archive_path == "/tmp/coder-archive.tar.gz"
|
||||
error_message = "archive_path should be empty when archive_name is not set"
|
||||
}
|
||||
}
|
||||
|
||||
run "apply_with_name" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "agent-123"
|
||||
paths = ["/etc/hosts"]
|
||||
archive_name = "nightly"
|
||||
output_dir = "/tmp/backups"
|
||||
compression = "zstd"
|
||||
create_archive_on_stop = true
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.archive_path == "/tmp/backups/nightly.tar.zst"
|
||||
error_message = "archive_path should be computed from archive_name + output_dir + extension"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
import { describe, expect, it, beforeAll } from "bun:test";
|
||||
import {
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
type TerraformState,
|
||||
} from "~test";
|
||||
|
||||
const USE_XTRACE =
|
||||
process.env.ARCHIVE_TEST_XTRACE === "1" || process.env.XTRACE === "1";
|
||||
|
||||
const IMAGE = "alpine";
|
||||
const BIN_DIR = "/tmp/coder-script-data/bin";
|
||||
const DATA_DIR = "/tmp/coder-script-data";
|
||||
|
||||
type ExecResult = {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
const ensureRunOk = (label: string, res: ExecResult) => {
|
||||
if (res.exitCode !== 0) {
|
||||
console.error(
|
||||
`[${label}] non-zero exit code: ${res.exitCode}\n--- stdout ---\n${res.stdout.trim()}\n--- stderr ---\n${res.stderr.trim()}\n--------------`,
|
||||
);
|
||||
}
|
||||
expect(res.exitCode).toBe(0);
|
||||
};
|
||||
|
||||
const sh = async (id: string, cmd: string): Promise<ExecResult> => {
|
||||
const res = await execContainer(id, ["sh", "-c", cmd]);
|
||||
return res;
|
||||
};
|
||||
|
||||
const bashRun = async (id: string, cmd: string): Promise<ExecResult> => {
|
||||
const injected = USE_XTRACE ? `/bin/bash -x ${cmd}` : cmd;
|
||||
return sh(id, injected);
|
||||
};
|
||||
|
||||
const prepareContainer = async (image = IMAGE) => {
|
||||
const id = await runContainer(image);
|
||||
// Prepare script dirs and deps.
|
||||
ensureRunOk(
|
||||
"mkdirs",
|
||||
await sh(id, `mkdir -p ${BIN_DIR} ${DATA_DIR} /tmp/backup`),
|
||||
);
|
||||
|
||||
// Install tools used by tests.
|
||||
ensureRunOk(
|
||||
"apk add",
|
||||
await sh(id, "apk add --no-cache bash tar gzip zstd coreutils"),
|
||||
);
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
const installArchive = async (
|
||||
state: TerraformState,
|
||||
opts?: { env?: string[] },
|
||||
) => {
|
||||
const instance = findResourceInstance(state, "coder_script");
|
||||
const id = await prepareContainer();
|
||||
// Run installer script with correct env for CODER_SCRIPT paths.
|
||||
const args = ["bash"];
|
||||
if (USE_XTRACE) args.push("-x");
|
||||
args.push("-c", instance.script);
|
||||
|
||||
const resp = await execContainer(id, args, [
|
||||
"--env",
|
||||
`CODER_SCRIPT_BIN_DIR=${BIN_DIR}`,
|
||||
"--env",
|
||||
`CODER_SCRIPT_DATA_DIR=${DATA_DIR}`,
|
||||
...(opts?.env ?? []),
|
||||
]);
|
||||
|
||||
return {
|
||||
id,
|
||||
install: {
|
||||
exitCode: resp.exitCode,
|
||||
stdout: resp.stdout.trim(),
|
||||
stderr: resp.stderr.trim(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const fileExists = async (id: string, path: string) => {
|
||||
const res = await sh(id, `test -f ${path} && echo yes || echo no`);
|
||||
return res.stdout.trim() === "yes";
|
||||
};
|
||||
|
||||
const isExecutable = async (id: string, path: string) => {
|
||||
const res = await sh(id, `test -x ${path} && echo yes || echo no`);
|
||||
return res.stdout.trim() === "yes";
|
||||
};
|
||||
|
||||
const listTar = async (id: string, path: string) => {
|
||||
// Try to autodetect compression flags from extension.
|
||||
let cmd = "";
|
||||
if (path.endsWith(".tar.gz")) {
|
||||
cmd = `tar -tzf ${path}`;
|
||||
} else if (path.endsWith(".tar.zst")) {
|
||||
// validate with zstd and ask tar to list via --zstd.
|
||||
cmd = `zstd -t -q ${path} && tar --zstd -tf ${path}`;
|
||||
} else {
|
||||
cmd = `tar -tf ${path}`;
|
||||
}
|
||||
return sh(id, cmd);
|
||||
};
|
||||
|
||||
describe("archive", () => {
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
// Ensure required variables are enforced.
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
});
|
||||
|
||||
it("installs wrapper scripts to BIN_DIR and library to DATA_DIR", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
});
|
||||
|
||||
// The Terraform output should reflect defaults from main.tf.
|
||||
expect(state.outputs.archive_path.value).toEqual(
|
||||
"/tmp/coder-archive.tar.gz",
|
||||
);
|
||||
|
||||
const { id, install } = await installArchive(state);
|
||||
ensureRunOk("install", install);
|
||||
|
||||
expect(install.stdout).toContain(
|
||||
`Installed archive library to: ${DATA_DIR}/archive-lib.sh`,
|
||||
);
|
||||
expect(install.stdout).toContain(
|
||||
`Installed create script to: ${BIN_DIR}/coder-archive-create`,
|
||||
);
|
||||
expect(install.stdout).toContain(
|
||||
`Installed extract script to: ${BIN_DIR}/coder-archive-extract`,
|
||||
);
|
||||
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-create`)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-extract`)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses sane defaults: creates gzip archive at the default path and logs to stderr", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
// Keep defaults: compression=gzip, output_dir=/tmp, archive_name=coder-archive.
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const createTestdata = await bashRun(
|
||||
id,
|
||||
`mkdir ~/gzip; touch ~/gzip/defaults.txt`,
|
||||
);
|
||||
ensureRunOk("create testdata", createTestdata);
|
||||
|
||||
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
|
||||
ensureRunOk("archive-create default run", run);
|
||||
|
||||
// Only the archive path should print to stdout.
|
||||
expect(run.stdout.trim()).toEqual("/tmp/coder-archive.tar.gz");
|
||||
expect(await fileExists(id, "/tmp/coder-archive.tar.gz")).toBe(true);
|
||||
|
||||
// Some useful diagnostics should be on stderr.
|
||||
expect(run.stderr).toContain("Creating archive:");
|
||||
expect(run.stderr).toContain("Compression: gzip");
|
||||
|
||||
const list = await listTar(id, "/tmp/coder-archive.tar.gz");
|
||||
ensureRunOk("list default archive", list);
|
||||
expect(list.stdout).toContain("gzip/defaults.txt");
|
||||
}, 20000);
|
||||
|
||||
it("creates a gzip archive with explicit -f and includes extra CLI paths", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
// Provide a simple default path so we can assert contents.
|
||||
paths: `["~/gzip"]`,
|
||||
compression: "gzip",
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const createTestdata = await bashRun(
|
||||
id,
|
||||
`mkdir ~/gzip; touch ~/gzip/test.txt; touch ~/gziptest.txt`,
|
||||
);
|
||||
ensureRunOk("create testdata", createTestdata);
|
||||
|
||||
const out = "/tmp/backup/test-archive.tar.gz";
|
||||
const run = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-create -f ${out} ~/gziptest.txt`,
|
||||
);
|
||||
ensureRunOk("archive-create gzip explicit -f", run);
|
||||
|
||||
expect(run.stdout.trim()).toEqual(out);
|
||||
expect(await fileExists(id, out)).toBe(true);
|
||||
|
||||
const list = await sh(id, `tar -tzf ${out}`);
|
||||
ensureRunOk("tar -tzf contents (gzip)", list);
|
||||
expect(list.stdout).toContain("gzip/test.txt");
|
||||
expect(list.stdout).toContain("gziptest.txt");
|
||||
}, 20000);
|
||||
|
||||
it("creates a zstd-compressed archive when requested via CLI override", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
paths: `["/etc/hostname"]`,
|
||||
// Module default is gzip, override at runtime to zstd.
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const out = "/tmp/backup/zstd-archive.tar.zst";
|
||||
const run = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-create --compression zstd -f ${out}`,
|
||||
);
|
||||
ensureRunOk("archive-create zstd", run);
|
||||
|
||||
expect(run.stdout.trim()).toEqual(out);
|
||||
|
||||
// Check integrity via zstd and that tar can list it.
|
||||
ensureRunOk("zstd -t", await sh(id, `test -f ${out} && zstd -t -q ${out}`));
|
||||
ensureRunOk("tar --zstd -tf", await sh(id, `tar --zstd -tf ${out}`));
|
||||
}, 30000);
|
||||
|
||||
it("creates an uncompressed tar when compression=none", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
// Keep module defaults but override at runtime.
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const out = "/tmp/backup/raw-archive.tar";
|
||||
const run = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-create --compression none -f ${out}`,
|
||||
);
|
||||
ensureRunOk("archive-create none", run);
|
||||
|
||||
expect(run.stdout.trim()).toEqual(out);
|
||||
ensureRunOk("tar -tf (none)", await sh(id, `tar -tf ${out} >/dev/null`));
|
||||
}, 20000);
|
||||
|
||||
it("applies exclude patterns from Terraform", async () => {
|
||||
// Include a file, but also exclude it via Terraform defaults to ensure
|
||||
// exclusion flows through.
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
paths: `["/etc/hostname"]`,
|
||||
exclude_patterns: `["/etc/hostname"]`,
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const out = "/tmp/backup/excluded.tar.gz";
|
||||
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create -f ${out}`);
|
||||
ensureRunOk("archive-create with exclude_patterns", run);
|
||||
|
||||
const list = await sh(id, `tar -tzf ${out}`);
|
||||
ensureRunOk("tar -tzf contents (exclude)", list);
|
||||
expect(list.stdout).not.toContain("etc/hostname"); // Excluded by Terraform default.
|
||||
}, 20000);
|
||||
|
||||
it("adds a run_on_stop script when enabled", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
create_on_stop: true,
|
||||
});
|
||||
|
||||
const coderScripts = state.resources.filter(
|
||||
(r) => r.type === "coder_script",
|
||||
);
|
||||
// Installer (run_on_start) + run_on_stop.
|
||||
expect(coderScripts.length).toBe(2);
|
||||
});
|
||||
|
||||
it("extracts a previously created archive into a target directory", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
paths: `["/etc/hostname"]`,
|
||||
compression: "gzip",
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
// Create archive.
|
||||
const out = "/tmp/backup/extract-test.tar.gz";
|
||||
const created = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-create -f ${out} /etc/hosts`,
|
||||
);
|
||||
ensureRunOk("create for extract", created);
|
||||
|
||||
// Extract archive.
|
||||
const extractDir = "/tmp/extract";
|
||||
const extract = await bashRun(
|
||||
id,
|
||||
`${BIN_DIR}/coder-archive-extract -f ${out} -C ${extractDir}`,
|
||||
);
|
||||
ensureRunOk("archive-extract", extract);
|
||||
|
||||
// Verify a known file exists after extraction.
|
||||
const exists = await sh(
|
||||
id,
|
||||
`test -f ${extractDir}/etc/hosts && echo ok || echo no`,
|
||||
);
|
||||
expect(exists.stdout.trim()).toEqual("ok");
|
||||
}, 20000);
|
||||
|
||||
it("honors Terraform defaults without CLI args (compression, name, output_dir)", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "agent-123",
|
||||
compression: "zstd",
|
||||
archive_name: "my-default",
|
||||
output_dir: "/tmp/defout",
|
||||
});
|
||||
|
||||
const { id } = await installArchive(state);
|
||||
|
||||
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
|
||||
ensureRunOk("archive-create terraform defaults", run);
|
||||
expect(run.stdout.trim()).toEqual("/tmp/defout/my-default.tar.zst");
|
||||
expect(run.stderr).toContain("Creating archive:");
|
||||
expect(run.stderr).toContain("Compression: zstd");
|
||||
ensureRunOk(
|
||||
"zstd -t",
|
||||
await sh(id, "zstd -t -q /tmp/defout/my-default.tar.zst"),
|
||||
);
|
||||
ensureRunOk(
|
||||
"tar --zstd -tf",
|
||||
await sh(id, "tar --zstd -tf /tmp/defout/my-default.tar.zst"),
|
||||
);
|
||||
}, 30000);
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
description = "The ID of a Coder agent."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "paths" {
|
||||
description = "List of files/directories to include in the archive. Defaults to the current directory."
|
||||
type = list(string)
|
||||
default = ["."]
|
||||
}
|
||||
|
||||
variable "exclude_patterns" {
|
||||
description = "Exclude patterns for the archive."
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "compression" {
|
||||
description = "Compression algorithm for the archive. Supported: gzip, zstd, none."
|
||||
type = string
|
||||
default = "gzip"
|
||||
validation {
|
||||
condition = contains(["gzip", "zstd", "none"], var.compression)
|
||||
error_message = "compression must be one of: gzip, zstd, none."
|
||||
}
|
||||
}
|
||||
|
||||
variable "archive_name" {
|
||||
description = "Optional archive base name without extension. If empty, defaults to \"coder-archive\"."
|
||||
type = string
|
||||
default = "coder-archive"
|
||||
}
|
||||
|
||||
variable "output_dir" {
|
||||
description = "Optional output directory where the archive will be written. Defaults to \"/tmp\"."
|
||||
type = string
|
||||
default = "/tmp"
|
||||
}
|
||||
|
||||
variable "directory" {
|
||||
description = "Change current directory to this path before creating or extracting the archive. Defaults to the user's home directory."
|
||||
type = string
|
||||
default = "~"
|
||||
}
|
||||
|
||||
variable "create_on_stop" {
|
||||
description = "If true, also create a run_on_stop script that creates the archive automatically on workspace stop."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "extract_on_start" {
|
||||
description = "If true, the installer will wait for an archive and extract it on start."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "extract_wait_timeout_seconds" {
|
||||
description = "Timeout (seconds) to wait for an archive when extract_on_start is true."
|
||||
type = number
|
||||
default = 5
|
||||
}
|
||||
|
||||
# Provide a stable script filename and sensible defaults.
|
||||
locals {
|
||||
extension = var.compression == "gzip" ? ".tar.gz" : var.compression == "zstd" ? ".tar.zst" : ".tar"
|
||||
|
||||
# Ensure ~ is expanded because it cannot be expanded inside quotes in a
|
||||
# templated shell script.
|
||||
paths = [for v in var.paths : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
|
||||
exclude_patterns = [for v in var.exclude_patterns : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
|
||||
directory = replace(var.directory, "/^~(\\/|$)/", "$$HOME$1")
|
||||
output_dir = replace(var.output_dir, "/^~(\\/|$)/", "$$HOME$1")
|
||||
|
||||
archive_path = "${local.output_dir}/${var.archive_name}${local.extension}"
|
||||
}
|
||||
|
||||
output "archive_path" {
|
||||
description = "Full path to the archive file that will be created, extracted, or both."
|
||||
value = local.archive_path
|
||||
}
|
||||
|
||||
# This script installs the user-facing archive script into $CODER_SCRIPT_BIN_DIR.
|
||||
# The installed script can be run manually by the user to create an archive.
|
||||
resource "coder_script" "archive_start_script" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Archive"
|
||||
icon = "/icon/folder.svg"
|
||||
run_on_start = true
|
||||
start_blocks_login = var.extract_on_start
|
||||
|
||||
# Render the user-facing archive script with Terraform defaults, then write it to $CODER_SCRIPT_BIN_DIR
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
TF_LIB_B64 = base64encode(file("${path.module}/scripts/archive-lib.sh")),
|
||||
TF_PATHS = join(" ", formatlist("%q", local.paths)),
|
||||
TF_EXCLUDE_PATTERNS = join(" ", formatlist("%q", local.exclude_patterns)),
|
||||
TF_COMPRESSION = var.compression,
|
||||
TF_ARCHIVE_PATH = local.archive_path,
|
||||
TF_DIRECTORY = local.directory,
|
||||
TF_EXTRACT_ON_START = var.extract_on_start,
|
||||
TF_EXTRACT_WAIT_TIMEOUT = var.extract_wait_timeout_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
# Optionally, also register a run_on_stop script that creates the archive automatically
|
||||
# when the workspace stops. It simply invokes the installed archive script.
|
||||
resource "coder_script" "archive_stop_script" {
|
||||
count = var.create_on_stop ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
display_name = "Archive"
|
||||
icon = "/icon/folder.svg"
|
||||
run_on_stop = true
|
||||
start_blocks_login = false
|
||||
|
||||
# Call the installed script. It will log to stderr and print the archive path to stdout.
|
||||
# We redirect stdout to stderr to avoid surfacing the path in system logs if undesired.
|
||||
# Remove the redirection if you want the path to appear in stdout on stop as well.
|
||||
script = <<-EOT
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
"$CODER_SCRIPT_BIN_DIR/coder-archive-create"
|
||||
EOT
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
LIB_B64="${TF_LIB_B64}"
|
||||
EXTRACT_ON_START="${TF_EXTRACT_ON_START}"
|
||||
EXTRACT_WAIT_TIMEOUT="${TF_EXTRACT_WAIT_TIMEOUT}"
|
||||
|
||||
# Set script defaults from Terraform.
|
||||
DEFAULT_PATHS=(${TF_PATHS})
|
||||
DEFAULT_EXCLUDE_PATTERNS=(${TF_EXCLUDE_PATTERNS})
|
||||
DEFAULT_COMPRESSION="${TF_COMPRESSION}"
|
||||
DEFAULT_ARCHIVE_PATH="${TF_ARCHIVE_PATH}"
|
||||
DEFAULT_DIRECTORY="${TF_DIRECTORY}"
|
||||
|
||||
# 1) Decode the library into $CODER_SCRIPT_DATA_DIR/archive-lib.sh (static, sourceable).
|
||||
LIB_PATH="$CODER_SCRIPT_DATA_DIR/archive-lib.sh"
|
||||
lib_tmp="$(mktemp -t coder-module-archive.XXXXXX))"
|
||||
trap 'rm -f "$lib_tmp" 2>/dev/null || true' EXIT
|
||||
|
||||
# Decode the base64 content safely.
|
||||
if ! printf '%s' "$LIB_B64" | base64 -d > "$lib_tmp"; then
|
||||
echo "ERROR: Failed to decode archive library from base64." >&2
|
||||
exit 1
|
||||
fi
|
||||
chmod 0644 "$lib_tmp"
|
||||
mv "$lib_tmp" "$LIB_PATH"
|
||||
|
||||
# 2) Generate the wrapper scripts (create and extract).
|
||||
create_wrapper() {
|
||||
tmp="$(mktemp -t coder-module-archive.XXXXXX)"
|
||||
trap 'rm -f "$tmp" 2>/dev/null || true' EXIT
|
||||
cat > "$tmp" << EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
. "$LIB_PATH"
|
||||
|
||||
# Set defaults from Terraform (through installer).
|
||||
$(
|
||||
declare -p \
|
||||
DEFAULT_PATHS \
|
||||
DEFAULT_EXCLUDE_PATTERNS \
|
||||
DEFAULT_COMPRESSION \
|
||||
DEFAULT_ARCHIVE_PATH \
|
||||
DEFAULT_DIRECTORY
|
||||
)
|
||||
|
||||
$1 "\$@"
|
||||
EOF
|
||||
chmod 0755 "$tmp"
|
||||
mv "$tmp" "$2"
|
||||
}
|
||||
|
||||
CREATE_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-create"
|
||||
EXTRACT_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-extract"
|
||||
create_wrapper archive_create "$CREATE_WRAPPER_PATH"
|
||||
create_wrapper archive_extract "$EXTRACT_WRAPPER_PATH"
|
||||
|
||||
echo "Installed archive library to: $LIB_PATH"
|
||||
echo "Installed create script to: $CREATE_WRAPPER_PATH"
|
||||
echo "Installed extract script to: $EXTRACT_WRAPPER_PATH"
|
||||
|
||||
# 3) Optionally wait for and extract an archive on start.
|
||||
if [[ $EXTRACT_ON_START = true ]]; then
|
||||
. "$LIB_PATH"
|
||||
|
||||
archive_wait_and_extract "$EXTRACT_WAIT_TIMEOUT" quiet || {
|
||||
exit_code=$?
|
||||
if [[ $exit_code -eq 2 ]]; then
|
||||
echo "WARNING: Archive not found in backup path (this is expected with new workspaces)."
|
||||
else
|
||||
exit $exit_code
|
||||
fi
|
||||
}
|
||||
fi
|
||||
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
printf '%s\n' "$@" >&2
|
||||
}
|
||||
warn() {
|
||||
printf 'WARNING: %s\n' "$1" >&2
|
||||
}
|
||||
error() {
|
||||
printf 'ERROR: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
load_defaults() {
|
||||
DEFAULT_PATHS=("${DEFAULT_PATHS[@]:-.}")
|
||||
DEFAULT_EXCLUDE_PATTERNS=("${DEFAULT_EXCLUDE_PATTERNS[@]:-}")
|
||||
DEFAULT_COMPRESSION="${DEFAULT_COMPRESSION:-gzip}"
|
||||
DEFAULT_ARCHIVE_PATH="${DEFAULT_ARCHIVE_PATH:-/tmp/coder-archive.tar.gz}"
|
||||
DEFAULT_DIRECTORY="${DEFAULT_DIRECTORY:-$HOME}"
|
||||
}
|
||||
|
||||
ensure_tools() {
|
||||
command -v tar > /dev/null 2>&1 || error "tar is required"
|
||||
case "$1" in
|
||||
gzip)
|
||||
command -v gzip > /dev/null 2>&1 || error "gzip is required for gzip compression"
|
||||
;;
|
||||
zstd)
|
||||
command -v zstd > /dev/null 2>&1 || error "zstd is required for zstd compression"
|
||||
;;
|
||||
none) ;;
|
||||
*)
|
||||
error "Unsupported compression algorithm: $1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
usage_archive_create() {
|
||||
load_defaults
|
||||
|
||||
cat >&2 << USAGE
|
||||
Usage: coder-archive-create [OPTIONS] [[PATHS] ...]
|
||||
Options:
|
||||
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
|
||||
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
|
||||
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
|
||||
-h, --help Show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
archive_create() {
|
||||
load_defaults
|
||||
|
||||
local compression="${DEFAULT_COMPRESSION}"
|
||||
local directory="${DEFAULT_DIRECTORY}"
|
||||
local file="${DEFAULT_ARCHIVE_PATH}"
|
||||
local paths=("${DEFAULT_PATHS[@]}")
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-c | --compression)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_create
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
compression="$2"
|
||||
shift 2
|
||||
;;
|
||||
-C | --directory)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_create
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
directory="$2"
|
||||
shift 2
|
||||
;;
|
||||
-f | --file)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_create
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
file="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage_archive_create
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
while [[ $# -gt 0 ]]; do
|
||||
paths+=("$1")
|
||||
shift
|
||||
done
|
||||
;;
|
||||
-*)
|
||||
usage_archive_create
|
||||
error "Unknown option: $1"
|
||||
;;
|
||||
*)
|
||||
paths+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_tools "$compression"
|
||||
|
||||
local -a tar_opts=(-c -f "$file" -C "$directory")
|
||||
case "$compression" in
|
||||
gzip)
|
||||
tar_opts+=(-z)
|
||||
;;
|
||||
zstd)
|
||||
tar_opts+=(--zstd)
|
||||
;;
|
||||
none) ;;
|
||||
*)
|
||||
error "Unsupported compression algorithm: $compression"
|
||||
;;
|
||||
esac
|
||||
|
||||
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
|
||||
if [[ -n $path ]]; then
|
||||
tar_opts+=(--exclude "$path")
|
||||
fi
|
||||
done
|
||||
|
||||
# Ensure destination directory exists.
|
||||
dest="$(dirname "$file")"
|
||||
mkdir -p "$dest" 2> /dev/null || error "Failed to create output dir: $dest"
|
||||
|
||||
log "Creating archive:"
|
||||
log " Compression: $compression"
|
||||
log " Directory: $directory"
|
||||
log " Archive: $file"
|
||||
log " Paths: ${paths[*]}"
|
||||
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
|
||||
|
||||
umask 077
|
||||
tar "${tar_opts[@]}" "${paths[@]}"
|
||||
|
||||
printf '%s\n' "$file"
|
||||
}
|
||||
|
||||
usage_archive_extract() {
|
||||
load_defaults
|
||||
|
||||
cat >&2 << USAGE
|
||||
Usage: coder-archive-extract [OPTIONS]
|
||||
Options:
|
||||
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
|
||||
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
|
||||
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
|
||||
-h, --help Show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
archive_extract() {
|
||||
load_defaults
|
||||
|
||||
local compression="${DEFAULT_COMPRESSION}"
|
||||
local directory="${DEFAULT_DIRECTORY}"
|
||||
local file="${DEFAULT_ARCHIVE_PATH}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-c | --compression)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_extract
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
compression="$2"
|
||||
shift 2
|
||||
;;
|
||||
-C | --directory)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_extract
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
directory="$2"
|
||||
shift 2
|
||||
;;
|
||||
-f | --file)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage_archive_extract
|
||||
error "Missing value for $1"
|
||||
fi
|
||||
file="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage_archive_extract
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
done
|
||||
;;
|
||||
-*)
|
||||
usage_archive_extract
|
||||
error "Unknown option: $1"
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_tools "$compression"
|
||||
|
||||
local -a tar_opts=(-x -f "$file" -C "$directory")
|
||||
case "$compression" in
|
||||
gzip)
|
||||
tar_opts+=(-z)
|
||||
;;
|
||||
zstd)
|
||||
tar_opts+=(--zstd)
|
||||
;;
|
||||
none) ;;
|
||||
*)
|
||||
error "Unsupported compression algorithm: $compression"
|
||||
;;
|
||||
esac
|
||||
|
||||
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
|
||||
if [[ -n $path ]]; then
|
||||
tar_opts+=(--exclude "$path")
|
||||
fi
|
||||
done
|
||||
|
||||
# Ensure destination directory exists.
|
||||
mkdir -p "$directory" || error "Failed to create directory: $directory"
|
||||
|
||||
log "Extracting archive:"
|
||||
log " Compression: $compression"
|
||||
log " Directory: $directory"
|
||||
log " Archive: $file"
|
||||
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
|
||||
|
||||
umask 077
|
||||
tar "${tar_opts[@]}" "${paths[@]}"
|
||||
|
||||
printf 'Extracted %s into %s\n' "$file" "$directory"
|
||||
}
|
||||
|
||||
archive_wait_and_extract() {
|
||||
load_defaults
|
||||
|
||||
local timeout="${1:-300}"
|
||||
local quiet="${2:-}"
|
||||
local file="${DEFAULT_ARCHIVE_PATH}"
|
||||
|
||||
local start now
|
||||
start=$(date +%s)
|
||||
while true; do
|
||||
if [[ -f "$file" ]]; then
|
||||
archive_extract -f "$file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ((timeout <= 0)); then
|
||||
break
|
||||
fi
|
||||
now=$(date +%s)
|
||||
if ((now - start >= timeout)); then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ -z $quiet ]]; then
|
||||
printf 'ERROR: Timed out waiting for archive: %s\n' "$file" >&2
|
||||
fi
|
||||
return 2
|
||||
}
|
||||
@@ -13,10 +13,10 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.1.1"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = var.openai_api_key
|
||||
folder = "/home/coder/project"
|
||||
workdir = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -33,10 +33,11 @@ module "codex" {
|
||||
module "codex" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.1.1"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
folder = "/home/coder/project"
|
||||
workdir = "/home/coder/project"
|
||||
report_tasks = false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -60,11 +61,11 @@ module "coder-login" {
|
||||
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.1.1"
|
||||
version = "3.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
openai_api_key = "..."
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
folder = "/home/coder/project"
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
# Custom configuration for full auto mode
|
||||
base_config_toml = <<-EOT
|
||||
@@ -75,7 +76,7 @@ module "codex" {
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified folder. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications.
|
||||
> This module configures Codex with a `workspace-write` sandbox that allows AI tasks to read/write files in the specified workdir. While the sandbox provides security boundaries, Codex can still modify files within the workspace. Use this module _only_ in trusted environments and be aware of the security implications.
|
||||
|
||||
## How it Works
|
||||
|
||||
@@ -106,7 +107,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se
|
||||
```tf
|
||||
module "codex" {
|
||||
source = "registry.coder.com/coder-labs/codex/coder"
|
||||
version = "2.1.1"
|
||||
version = "3.0.0"
|
||||
# ... other variables ...
|
||||
|
||||
# Override default configuration
|
||||
@@ -137,7 +138,7 @@ module "codex" {
|
||||
> [!IMPORTANT]
|
||||
> To use tasks with Codex CLI, ensure you have the `openai_api_key` variable set, and **you create a `coder_parameter` named `"AI Prompt"` and pass its value to the codex module's `ai_prompt` variable**. [Tasks Template Example](https://registry.coder.com/templates/coder-labs/tasks-docker).
|
||||
> The module automatically configures Codex with your API key and model preferences.
|
||||
> folder is a required variable for the module to function correctly.
|
||||
> workdir is a required variable for the module to function correctly.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
install_codex: props?.skipCodexMock ? "true" : "false",
|
||||
install_agentapi: props?.skipAgentAPIMock ? "true" : "false",
|
||||
codex_model: "gpt-4-turbo",
|
||||
folder: "/home/coder",
|
||||
workdir: "/home/coder",
|
||||
...props?.moduleVariables,
|
||||
},
|
||||
registerCleanup,
|
||||
@@ -166,12 +166,12 @@ describe("codex", async () => {
|
||||
expect(postInstallLog).toContain("post-install-script");
|
||||
});
|
||||
|
||||
test("folder-variable", async () => {
|
||||
const folder = "/tmp/codex-test-folder";
|
||||
test("workdir-variable", async () => {
|
||||
const workdir = "/tmp/codex-test-workdir";
|
||||
const { id } = await setup({
|
||||
skipCodexMock: false,
|
||||
moduleVariables: {
|
||||
folder,
|
||||
workdir,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
@@ -179,7 +179,7 @@ describe("codex", async () => {
|
||||
id,
|
||||
"/home/coder/.codex-module/install.log",
|
||||
);
|
||||
expect(resp).toContain(folder);
|
||||
expect(resp).toContain(workdir);
|
||||
});
|
||||
|
||||
test("additional-mcp-servers", async () => {
|
||||
|
||||
@@ -36,11 +36,41 @@ variable "icon" {
|
||||
default = "/icon/openai.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
variable "workdir" {
|
||||
type = string
|
||||
description = "The folder to run Codex in."
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
type = bool
|
||||
description = "Whether to enable task reporting to Coder UI via AgentAPI"
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "cli_app" {
|
||||
type = bool
|
||||
description = "Whether to create a CLI app for Codex"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the web app"
|
||||
default = "Codex"
|
||||
}
|
||||
|
||||
variable "cli_app_display_name" {
|
||||
type = string
|
||||
description = "Display name for the CLI app"
|
||||
default = "Codex CLI"
|
||||
}
|
||||
|
||||
variable "install_codex" {
|
||||
type = bool
|
||||
description = "Whether to install Codex."
|
||||
@@ -120,6 +150,7 @@ resource "coder_env" "openai_api_key" {
|
||||
}
|
||||
|
||||
locals {
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "codex"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
@@ -131,16 +162,18 @@ module "agentapi" {
|
||||
version = "1.2.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
folder = var.folder
|
||||
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 = "Codex"
|
||||
cli_app_slug = "${local.app_slug}-cli"
|
||||
cli_app_display_name = "Codex CLI"
|
||||
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_subdomain = var.subdomain
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
@@ -152,8 +185,9 @@ module "agentapi" {
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_CODEX_MODEL='${var.codex_model}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.folder}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.workdir}' \
|
||||
ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
@@ -165,12 +199,14 @@ module "agentapi" {
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
ARG_OPENAI_API_KEY='${var.openai_api_key}' \
|
||||
ARG_REPORT_TASKS='${var.report_tasks}' \
|
||||
ARG_INSTALL='${var.install_codex}' \
|
||||
ARG_CODEX_VERSION='${var.codex_version}' \
|
||||
ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \
|
||||
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.folder}' \
|
||||
ARG_CODEX_START_DIRECTORY='${var.workdir}' \
|
||||
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
|
||||
@@ -22,6 +22,8 @@ printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
|
||||
printf "Has Base Config: %s\n" "$([ -n "$ARG_BASE_CONFIG_TOML" ] && echo "Yes" || echo "No")"
|
||||
printf "Has Additional MCP: %s\n" "$([ -n "$ARG_ADDITIONAL_MCP_SERVERS" ] && echo "Yes" || echo "No")"
|
||||
printf "Has System Prompt: %s\n" "$([ -n "$ARG_CODEX_INSTRUCTION_PROMPT" ] && echo "Yes" || echo "No")"
|
||||
printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")"
|
||||
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
|
||||
echo "======================================"
|
||||
|
||||
set +o nounset
|
||||
@@ -100,13 +102,20 @@ EOF
|
||||
append_mcp_servers_section() {
|
||||
local config_path="$1"
|
||||
|
||||
if [ "${ARG_REPORT_TASKS}" == "false" ]; then
|
||||
ARG_CODER_MCP_APP_STATUS_SLUG=""
|
||||
CODER_MCP_AI_AGENTAPI_URL=""
|
||||
else
|
||||
CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
fi
|
||||
|
||||
cat << EOF >> "$config_path"
|
||||
|
||||
# MCP Servers Configuration
|
||||
[mcp_servers.Coder]
|
||||
command = "coder"
|
||||
args = ["exp", "mcp", "server"]
|
||||
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "http://localhost:3284", "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
|
||||
env = { "CODER_MCP_APP_STATUS_SLUG" = "${ARG_CODER_MCP_APP_STATUS_SLUG}", "CODER_MCP_AI_AGENTAPI_URL" = "${CODER_MCP_AI_AGENTAPI_URL}" , "CODER_AGENT_URL" = "${CODER_AGENT_URL}", "CODER_AGENT_TOKEN" = "${CODER_AGENT_TOKEN}" }
|
||||
description = "Report ALL tasks and statuses (in progress, done, failed) you are working on."
|
||||
type = "stdio"
|
||||
|
||||
@@ -159,7 +168,21 @@ function add_instruction_prompt_if_exists() {
|
||||
fi
|
||||
}
|
||||
|
||||
function add_auth_json() {
|
||||
AUTH_JSON_PATH="$HOME/.codex/auth.json"
|
||||
mkdir -p "$(dirname "$AUTH_JSON_PATH")"
|
||||
AUTH_JSON=$(
|
||||
cat << EOF
|
||||
{
|
||||
"OPENAI_API_KEY": "${ARG_OPENAI_API_KEY}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
echo "$AUTH_JSON" > "$AUTH_JSON_PATH"
|
||||
}
|
||||
|
||||
install_codex
|
||||
codex --version
|
||||
populate_config_toml
|
||||
add_instruction_prompt_if_exists
|
||||
add_auth_json
|
||||
|
||||
@@ -22,6 +22,7 @@ printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided"
|
||||
printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}"
|
||||
printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY"
|
||||
printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")"
|
||||
printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS"
|
||||
echo "======================================"
|
||||
set +o nounset
|
||||
CODEX_ARGS=()
|
||||
@@ -57,7 +58,11 @@ fi
|
||||
|
||||
if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then
|
||||
printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT"
|
||||
PROMPT="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_CODEX_TASK_PROMPT"
|
||||
if [ "${ARG_REPORT_TASKS}" == "true" ]; then
|
||||
PROMPT="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_CODEX_TASK_PROMPT"
|
||||
else
|
||||
PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT"
|
||||
fi
|
||||
CODEX_ARGS+=("$PROMPT")
|
||||
else
|
||||
printf "No task prompt given.\n"
|
||||
|
||||
@@ -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.1"
|
||||
version = "0.2.2"
|
||||
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.1"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
@@ -71,12 +71,12 @@ Customize tool permissions, MCP servers, and Copilot settings:
|
||||
```tf
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/projects"
|
||||
|
||||
# Version pinning (defaults to "0.0.334", use "latest" for newest version)
|
||||
copilot_version = "latest"
|
||||
# Version pinning (defaults to "latest", use specific version if desired)
|
||||
copilot_version = "0.0.334"
|
||||
|
||||
# Tool permissions
|
||||
allow_tools = ["shell(git)", "shell(npm)", "write"]
|
||||
@@ -142,7 +142,7 @@ variable "github_token" {
|
||||
|
||||
module "copilot" {
|
||||
source = "registry.coder.com/coder-labs/copilot/coder"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
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.1"
|
||||
version = "0.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
report_tasks = false
|
||||
|
||||
@@ -104,7 +104,7 @@ variable "agentapi_version" {
|
||||
variable "copilot_version" {
|
||||
type = string
|
||||
description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'."
|
||||
default = "0.0.334"
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "report_tasks" {
|
||||
|
||||
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.2.0"
|
||||
version = "2.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
version = ">= 2.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,8 +239,6 @@ resource "coder_app" "agentapi_cli" {
|
||||
group = var.cli_app_group
|
||||
}
|
||||
|
||||
resource "coder_ai_task" "agentapi" {
|
||||
sidebar_app {
|
||||
id = coder_app.agentapi_web.id
|
||||
}
|
||||
output "task_app_id" {
|
||||
value = coder_app.agentapi_web.id
|
||||
}
|
||||
|
||||
@@ -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.1.1"
|
||||
version = "3.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
@@ -32,8 +32,29 @@ module "claude-code" {
|
||||
- You can get the API key from the [Anthropic Console](https://console.anthropic.com/dashboard).
|
||||
- You can get the Session Token using the `claude setup-token` command. This is a long-lived authentication token (requires Claude subscription)
|
||||
|
||||
### Session Resumption Behavior
|
||||
|
||||
By default, Claude Code automatically resumes existing conversations when your workspace restarts. Sessions are tracked per workspace directory, so conversations continue where you left off. If no session exists (first start), your `ai_prompt` will run normally. To disable this behavior and always start fresh, set `continue = false`
|
||||
|
||||
## Examples
|
||||
|
||||
### Usage with Agent Boundaries
|
||||
|
||||
This example shows how to configure the Claude Code module to run the agent behind a process-level boundary that restricts its network access.
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
enable_boundary = true
|
||||
boundary_version = "main"
|
||||
boundary_log_dir = "/tmp/boundary_logs"
|
||||
boundary_log_level = "WARN"
|
||||
boundary_additional_allowed_urls = ["GET *google.com"]
|
||||
boundary_proxy_port = "8087"
|
||||
version = "3.3.0"
|
||||
}
|
||||
```
|
||||
|
||||
### Usage with Tasks and Advanced Configuration
|
||||
|
||||
This example shows how to configure the Claude Code module with an AI prompt, API key shared by all users of the template, and other custom settings.
|
||||
@@ -49,7 +70,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.1.1"
|
||||
version = "3.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -85,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.1.1"
|
||||
version = "3.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -108,7 +129,7 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.1.1"
|
||||
version = "3.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
@@ -181,7 +202,7 @@ resource "coder_env" "bedrock_api_key" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.1.1"
|
||||
version = "3.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -238,7 +259,7 @@ resource "coder_env" "google_application_credentials" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.1.1"
|
||||
version = "3.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
@@ -167,7 +167,7 @@ describe("claude-code", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
permission_mode: mode,
|
||||
task_prompt: "test prompt",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
@@ -185,7 +185,7 @@ describe("claude-code", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
model: model,
|
||||
task_prompt: "test prompt",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
await execModuleScript(id);
|
||||
@@ -198,13 +198,24 @@ describe("claude-code", async () => {
|
||||
expect(startLog.stdout).toContain(`--model ${model}`);
|
||||
});
|
||||
|
||||
test("claude-continue-previous-conversation", async () => {
|
||||
test("claude-continue-resume-existing-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
task_prompt: "test prompt",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
// Create a mock session file with the predefined task session ID
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
@@ -212,7 +223,9 @@ describe("claude-code", async () => {
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
expect(startLog.stdout).toContain("--resume");
|
||||
expect(startLog.stdout).toContain(taskSessionId);
|
||||
expect(startLog.stdout).toContain("Resuming existing task session");
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
|
||||
@@ -134,8 +134,8 @@ variable "resume_session_id" {
|
||||
|
||||
variable "continue" {
|
||||
type = bool
|
||||
description = "Load the most recent conversation in the current directory. Task will fail in a new workspace with no conversation/session to continue"
|
||||
default = false
|
||||
description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "dangerously_skip_permissions" {
|
||||
@@ -192,6 +192,54 @@ variable "claude_md_path" {
|
||||
default = "$HOME/.claude/CLAUDE.md"
|
||||
}
|
||||
|
||||
variable "enable_boundary" {
|
||||
type = bool
|
||||
description = "Whether to enable coder boundary for network filtering"
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "boundary_version" {
|
||||
type = string
|
||||
description = "Boundary version, valid git reference should be provided (tag, commit, branch)"
|
||||
default = "main"
|
||||
}
|
||||
|
||||
variable "boundary_log_dir" {
|
||||
type = string
|
||||
description = "Directory for boundary logs"
|
||||
default = "/tmp/boundary_logs"
|
||||
}
|
||||
|
||||
variable "boundary_log_level" {
|
||||
type = string
|
||||
description = "Log level for boundary process"
|
||||
default = "WARN"
|
||||
}
|
||||
|
||||
variable "boundary_additional_allowed_urls" {
|
||||
type = list(string)
|
||||
description = "Additional URLs to allow through boundary (in addition to default allowed URLs)"
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "boundary_proxy_port" {
|
||||
type = string
|
||||
description = "Port for HTTP Proxy used by Boundary"
|
||||
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
|
||||
|
||||
@@ -229,6 +277,8 @@ locals {
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".claude-module"
|
||||
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
|
||||
# Extract hostname from access_url for boundary --allow flag
|
||||
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
|
||||
|
||||
# Required prompts for the module to properly report task status to Coder
|
||||
report_tasks_system_prompt = <<-EOT
|
||||
@@ -299,6 +349,15 @@ module "agentapi" {
|
||||
ARG_PERMISSION_MODE='${var.permission_mode}' \
|
||||
ARG_WORKDIR='${local.workdir}' \
|
||||
ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \
|
||||
ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \
|
||||
ARG_BOUNDARY_VERSION='${var.boundary_version}' \
|
||||
ARG_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \
|
||||
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
|
||||
|
||||
|
||||
@@ -188,6 +188,32 @@ run "test_claude_code_permission_mode_validation" {
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_with_boundary" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-boundary"
|
||||
workdir = "/home/coder/boundary-test"
|
||||
enable_boundary = true
|
||||
boundary_log_dir = "/tmp/test-boundary-logs"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.enable_boundary == true
|
||||
error_message = "Boundary should be enabled"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.boundary_log_dir == "/tmp/test-boundary-logs"
|
||||
error_message = "Boundary log dir should be set correctly"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = local.coder_host != ""
|
||||
error_message = "Coder host should be extracted from access URL"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_claude_code_system_prompt" {
|
||||
command = plan
|
||||
|
||||
@@ -267,4 +293,4 @@ run "test_claude_report_tasks_disabled" {
|
||||
condition = endswith(trimspace(coder_env.claude_code_system_prompt.value), "</system>")
|
||||
error_message = "System prompt should end with </system>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@ ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-}
|
||||
ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-}
|
||||
ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"}
|
||||
ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d)
|
||||
ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false}
|
||||
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 "--------------------------------"
|
||||
|
||||
@@ -27,6 +35,12 @@ printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIO
|
||||
printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE"
|
||||
printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT"
|
||||
printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR"
|
||||
printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY"
|
||||
printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION"
|
||||
printf "ARG_BOUNDARY_LOG_DIR: %s\n" "$ARG_BOUNDARY_LOG_DIR"
|
||||
printf "ARG_BOUNDARY_LOG_LEVEL: %s\n" "$ARG_BOUNDARY_LOG_LEVEL"
|
||||
printf "ARG_BOUNDARY_PROXY_PORT: %s\n" "$ARG_BOUNDARY_PROXY_PORT"
|
||||
printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
@@ -35,6 +49,14 @@ echo "--------------------------------"
|
||||
# avoid exiting if the script fails
|
||||
bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true
|
||||
|
||||
function install_boundary() {
|
||||
# Install boundary from public github repo
|
||||
git clone https://github.com/coder/boundary
|
||||
cd boundary
|
||||
git checkout $ARG_BOUNDARY_VERSION
|
||||
go install ./cmd/...
|
||||
}
|
||||
|
||||
function validate_claude_installation() {
|
||||
if command_exists claude; then
|
||||
printf "Claude Code is installed\n"
|
||||
@@ -44,41 +66,119 @@ function validate_claude_installation() {
|
||||
fi
|
||||
}
|
||||
|
||||
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
|
||||
|
||||
task_session_exists() {
|
||||
if find "$HOME/.claude" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
ARGS=()
|
||||
|
||||
function build_claude_args() {
|
||||
function start_agentapi() {
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
|
||||
if [ -n "$ARG_MODEL" ]; then
|
||||
ARGS+=(--model "$ARG_MODEL")
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
|
||||
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
|
||||
fi
|
||||
|
||||
if [ "$ARG_CONTINUE" = "true" ]; then
|
||||
ARGS+=(--continue)
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_PERMISSION_MODE" ]; then
|
||||
ARGS+=(--permission-mode "$ARG_PERMISSION_MODE")
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
function start_agentapi() {
|
||||
mkdir -p "$ARG_WORKDIR"
|
||||
cd "$ARG_WORKDIR"
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ -n "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" ]; then
|
||||
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
|
||||
echo "Using explicit resume_session_id: $ARG_RESUME_SESSION_ID"
|
||||
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
elif [ "$ARG_CONTINUE" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
echo "Task session detected (ID: $TASK_SESSION_ID)"
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Resuming existing task session"
|
||||
else
|
||||
echo "No existing task session found"
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
|
||||
echo "Starting new task session with prompt"
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting new task session"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Continue disabled, starting fresh session"
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT")
|
||||
echo "Starting new session with prompt"
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting claude code session"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
|
||||
|
||||
if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then
|
||||
install_boundary
|
||||
|
||||
mkdir -p "$ARG_BOUNDARY_LOG_DIR"
|
||||
printf "Starting with coder boundary enabled\n"
|
||||
|
||||
# Build boundary args with conditional --unprivileged flag
|
||||
BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR")
|
||||
# Add default allowed URLs
|
||||
BOUNDARY_ARGS+=(--allow "*anthropic.com" --allow "registry.npmjs.org" --allow "*sentry.io" --allow "claude.ai" --allow "$ARG_CODER_HOST")
|
||||
|
||||
# Add any additional allowed URLs from the variable
|
||||
if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then
|
||||
IFS=' ' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS"
|
||||
for url in "${ADDITIONAL_URLS[@]}"; do
|
||||
BOUNDARY_ARGS+=(--allow "$url")
|
||||
done
|
||||
fi
|
||||
|
||||
# Set HTTP Proxy port used by Boundary
|
||||
BOUNDARY_ARGS+=(--proxy-port $ARG_BOUNDARY_PROXY_PORT)
|
||||
|
||||
# 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=()
|
||||
for arg in "${ARGS[@]}"; do
|
||||
if [ "$arg" != "--dangerously-skip-permissions" ]; then
|
||||
CLAUDE_ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \
|
||||
sudo -E env PATH=$PATH setpriv --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \
|
||||
claude "${CLAUDE_ARGS[@]}"
|
||||
else
|
||||
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
validate_claude_installation
|
||||
build_claude_args
|
||||
start_agentapi
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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" {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 -->
|
||||
|
||||

|
||||
|
||||
## 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,
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
Executable
+100
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user