Compare commits

..

3 Commits

Author SHA1 Message Date
Jiachen Jiang 583918bfef added example of boundary to claude code module (#500) 2025-10-21 16:33:15 -05:00
Benjamin Peinhardt a1786a09ea update claude-code module version (#498)
The version for the claude-code module should have been updated in
https://github.com/coder/registry/pull/455. This PR updates the module
version so we can cut a release 😎
2025-10-21 13:46:32 -05:00
Benjamin Peinhardt a35986d7df feat: initial boundary integration with claude code (#455)
Closes #

## Description

<!-- Briefly describe what this PR does and why -->

## Type of Change

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

## Module Information

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

**Path:** `registry/[namespace]/modules/[module-name]`  
**New version:** `v1.0.0`  
**Breaking change:** [ ] Yes [ ] No

## Testing & Validation

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

## Related Issues

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

---------

Co-authored-by: YEVHENII SHCHERBINA <yevhenii@coder.com>
2025-10-21 13:44:26 -04:00
12 changed files with 157 additions and 2200 deletions
+1
View File
@@ -5,6 +5,7 @@ Hashi = "Hashi"
HashiCorp = "HashiCorp"
mavrickrishi = "mavrickrishi" # Username
mavrick = "mavrick" # Username
inh = "inh" # Option in setpriv command
[files]
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
-590
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 202 KiB

+23 -6
View File
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.1.1"
version = "3.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -34,6 +34,23 @@ module "claude-code" {
## 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.2.1"
}
```
### 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 +66,7 @@ data "coder_parameter" "ai_prompt" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.1.1"
version = "3.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
@@ -85,7 +102,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.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder"
install_claude_code = true
@@ -108,7 +125,7 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.1.1"
version = "3.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
@@ -181,7 +198,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.1.1"
version = "3.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@@ -238,7 +255,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "3.1.1"
version = "3.2.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
@@ -192,6 +192,42 @@ 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"
}
resource "coder_env" "claude_code_md_path" {
count = var.claude_md_path == "" ? 0 : 1
@@ -229,6 +265,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 +337,13 @@ 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_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,12 @@ 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_CODER_HOST=${ARG_CODER_HOST:-}
echo "--------------------------------"
@@ -27,6 +33,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 +47,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"
@@ -76,7 +96,47 @@ function start_agentapi() {
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)
# 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
-523
View File
@@ -1,523 +0,0 @@
---
display_name: Restic Backup
description: Cloud-backed ephemeral workspaces with automatic backup on stop and restore on start using Restic
icon: ../../../../.icons/restic.svg
verified: false
tags: [backup, restore, cloud, restic, s3, b2]
---
# Restic Backup
Automatic cloud backups for Coder workspaces. Backs up on stop, restores on start.
## Features
- Auto backup/restore on workspace stop/start
- Works with S3, B2, Azure, GCS, SFTP, local storage
- Encrypted and deduplicated
- Workspace-aware tagging for easy browsing
- Configurable retention policies
- Clone backups between workspaces
## Quick Start
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "s3:s3.amazonaws.com/my-workspace-backups"
password = var.restic_password
env = {
AWS_ACCESS_KEY_ID = var.aws_access_key
AWS_SECRET_ACCESS_KEY = var.aws_secret_key
}
}
```
## How It Works
1. Workspace stops → automatic backup to cloud
2. Workspace starts → automatic restore from backup
3. Backups are tagged with `workspace-id`, `workspace-owner`, `workspace-name`
4. Auto-restore uses `workspace-id` to find the correct backup
5. Manually restore any backup using `snapshot_id`
## Storage Backend Configuration
### AWS S3
[Official Restic S3 Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#amazon-s3)
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "s3:s3.amazonaws.com/my-bucket/workspace-backups"
password = var.restic_password
env = {
AWS_ACCESS_KEY_ID = var.aws_access_key
AWS_SECRET_ACCESS_KEY = var.aws_secret_key
AWS_DEFAULT_REGION = "us-east-1"
}
}
```
### Backblaze B2 (Cost-Effective)
[Official Restic B2 Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#backblaze-b2)
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "b2:my-bucket:workspace-backups"
password = var.restic_password
env = {
B2_ACCOUNT_ID = var.b2_account_id
B2_ACCOUNT_KEY = var.b2_account_key
}
}
```
### Azure Blob Storage
[Official Restic Azure Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#microsoft-azure-blob-storage)
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "azure:container-name:/workspace-backups"
password = var.restic_password
env = {
AZURE_ACCOUNT_NAME = var.azure_account_name
AZURE_ACCOUNT_KEY = var.azure_account_key
}
}
```
### Google Cloud Storage
[Official Restic GCS Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#google-cloud-storage)
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "gs:my-bucket:/workspace-backups"
password = var.restic_password
env = {
GOOGLE_PROJECT_ID = var.gcp_project_id
GOOGLE_APPLICATION_CREDENTIALS = "/path/to/service-account.json"
}
}
```
### MinIO or S3-Compatible Storage
[Official Restic Minio Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#minio-server) | [S3-Compatible](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#s3-compatible-storage)
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "s3:http://minio.company.com:9000/workspace-backups"
password = var.restic_password
env = {
AWS_ACCESS_KEY_ID = var.minio_access_key
AWS_SECRET_ACCESS_KEY = var.minio_secret_key
}
}
```
### SFTP
[Official Restic SFTP Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#sftp)
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "sftp:user@backup-server.com:/backups/restic"
password = var.restic_password
# SSH key should be at ~/.ssh/id_rsa
# Or configure custom SSH command:
env = {
RESTIC_SFTP_COMMAND = "ssh user@host -i /path/to/key -s sftp"
}
}
```
### Local Directory (Testing)
[Official Restic Local Documentation](https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#local)
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "/backup/restic-repo"
password = var.restic_password
}
```
**Note:** Use persistent storage (Docker volume, PV) for local repositories.
## Advanced Configuration
### Selective Backup Paths
Only backup specific directories:
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "s3:s3.amazonaws.com/backups"
password = var.restic_password
backup_paths = [
"/home/coder/projects",
"/home/coder/.config",
"/home/coder/data",
]
exclude_patterns = [
"**/.git",
"**/node_modules",
"**/__pycache__",
"**/target",
"**/.venv",
"**/tmp",
]
env = {
AWS_ACCESS_KEY_ID = var.aws_access_key
AWS_SECRET_ACCESS_KEY = var.aws_secret_key
}
}
```
### Periodic Backups While Running
Backup every N minutes while workspace is active:
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "b2:workspace-backups"
password = var.restic_password
# Backup every 30 minutes while workspace is running
backup_interval_minutes = 30
env = {
B2_ACCOUNT_ID = var.b2_account_id
B2_ACCOUNT_KEY = var.b2_account_key
}
}
```
### Custom Stop Script
Run cleanup before backup:
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "s3:s3.amazonaws.com/backups"
password = var.restic_password
custom_stop_script = <<-EOF
#!/bin/bash
echo "Cleaning up before backup..."
rm -rf /tmp/*
docker system prune -f
find /home/coder -name "*.log" -delete
EOF
env = {
AWS_ACCESS_KEY_ID = var.aws_access_key
AWS_SECRET_ACCESS_KEY = var.aws_secret_key
}
}
```
### Clone Another Workspace's Backup
Restore from a specific snapshot:
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "s3:s3.amazonaws.com/backups"
password = var.restic_password
# Restore from specific snapshot (find ID using: restic snapshots)
restore_on_start = true
snapshot_id = "abc123def" # The snapshot ID to restore
env = {
AWS_ACCESS_KEY_ID = var.aws_access_key
AWS_SECRET_ACCESS_KEY = var.aws_secret_key
}
}
```
To find snapshot IDs from another workspace:
```bash
# List all snapshots grouped by workspace
restic snapshots --group-by tags
# Or filter by specific workspace
restic snapshots --tag workspace-owner:john --tag workspace-name:dev-workspace
```
### Custom Retention Policies
Control how many backups to keep:
```tf
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "s3:s3.amazonaws.com/backups"
password = var.restic_password
# Keep last 10 backups
retention_keep_last = 10
# Keep daily backups for 14 days
retention_keep_daily = 14
# Keep weekly backups for 8 weeks
retention_keep_weekly = 8
# Keep monthly backups for 6 months
retention_keep_monthly = 6
# Apply retention automatically
auto_forget = true
# Don't prune on stop (too slow)
auto_prune = false
env = {
AWS_ACCESS_KEY_ID = var.aws_access_key
AWS_SECRET_ACCESS_KEY = var.aws_secret_key
}
}
```
### Using HCP Vault Secrets
Store credentials securely:
```tf
module "vault_secrets" {
source = "registry.coder.com/coder/hcp-vault-secrets/coder"
version = "1.0.34"
agent_id = coder_agent.main.id
app_name = "workspace-backups"
project_id = var.hcp_project_id
secrets = ["RESTIC_PASSWORD", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
}
module "restic" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/restic/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
repository = "s3:s3.amazonaws.com/backups"
password = "" # Will use RESTIC_PASSWORD from vault
depends_on = [module.vault_secrets]
}
```
## Manual Operations
### Trigger Manual Backup
Click the **"Backup Now"** button in the Coder UI, or run from terminal:
```bash
restic-backup --tag manual-backup
```
### List Your Workspace's Backups
```bash
restic snapshots --tag workspace-id:$RESTIC_WORKSPACE_ID
```
Or view all snapshots:
```bash
restic snapshots
```
### List All Workspace Backups in Repository
```bash
restic snapshots --group-by tags
```
This shows snapshots grouped by workspace, making it easy to see all workspace backups in the repository.
### Restore Specific Snapshot
```bash
# List snapshots for this workspace
restic snapshots --tag workspace-id:$RESTIC_WORKSPACE_ID
# Restore to temporary location for inspection
restic restore /tmp/restore < snapshot-id > --target
# Or restore to original location
restic restore / < snapshot-id > --target
```
### Check Repository Health
```bash
restic check
```
### Manual Cleanup
```bash
# Remove old snapshots for this workspace
restic forget --tag workspace-id:$RESTIC_WORKSPACE_ID --keep-last 3
# Reclaim space (removes unreferenced data)
restic prune
```
## Important Considerations
### Stop Backup Limitations
> **Warning**: The `backup_on_stop` feature may not work on all template types if the agent is terminated before backup completes. See [coder/coder#6174](https://github.com/coder/coder/issues/6174) for details.
**Recommendations**:
- Test stop backups with your specific template
- Keep backups fast (use selective paths and exclusions)
- Use `backup_interval_minutes` for important data
- Set `auto_prune = false` for stop backups (prune is slow)
### Repository Organization
**Single Shared Repository** (Recommended):
- All workspaces share one repository
- Backups are tagged with workspace metadata
- Deduplication saves space
- Easy credential management
**Per-Workspace Repositories**:
- Each workspace uses separate repository
- More isolation but more complex
- No cross-workspace restore
### Security
- Repository password encrypts ALL backups
- Use Coder parameters or external secrets for credentials
- Backend credentials should have minimal permissions
- Consider separate repositories for different teams
### Performance Tips
- **Use exclusions**: Skip `.git`, `node_modules`, caches
- **Selective paths**: Only backup what you need
- **Interval backups**: Balance frequency vs performance
- **Retention policies**: Keep low retention to save storage costs
- **Prune manually**: Don't enable `auto_prune` on stop (too slow)
## Troubleshooting
### Backup Fails on Stop
The workspace might be terminating before backup completes. Try:
- Reducing backup size with selective paths
- Using interval backups instead
- Testing with a local repository first
### Restore Blocks Login Too Long
- Reduce restore size with selective backup paths
- Set `start_blocks_login = false` to allow login during restore
- Use faster storage backend
### Repository Not Found
Ensure:
- Repository URL is correct
- Backend credentials are valid
- Network connectivity to storage backend
- Repository has been initialized (`auto_init_repo = true`)
### Permission Denied
Check:
- Backend credentials have write permissions
- Local directory (if used) is writable
- SSH key (for SFTP) is accessible
### Out of Storage Space
Run cleanup:
```bash
restic forget --tag workspace-id:$RESTIC_WORKSPACE_ID --keep-last 2
restic prune
```
## Links
- [Restic Documentation](https://restic.readthedocs.io/)
- [Restic GitHub](https://github.com/restic/restic)
- [Coder Documentation](https://coder.com/docs)
@@ -1,75 +0,0 @@
import { describe, expect, it } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
describe("restic", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "test-agent-id",
repository: "s3:s3.amazonaws.com/test-bucket",
password: "test-password",
});
it("installs restic successfully", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
repository: "/tmp/restic-repo",
password: "test-password",
install_restic: "true",
auto_init_repo: "false",
restore_on_start: "false",
});
const output = await executeScriptInContainer(
state,
"alpine",
"sh",
"apk add --no-cache curl bzip2",
);
if (output.exitCode !== 0) {
console.log("Exit code:", output.exitCode);
console.log("STDOUT:", output.stdout.join("\n"));
console.log("STDERR:", output.stderr.join("\n"));
}
expect(output.exitCode).toBe(0);
const stdout = output.stdout.join("\n");
expect(stdout).toContain("Restic Backup Module Setup");
expect(stdout).toContain("Installing Restic...");
expect(stdout).toContain("Detected OS: linux");
expect(stdout).toContain("Architecture:");
expect(stdout).toContain("Fetching latest version");
expect(stdout).toContain("Version:");
expect(stdout).toContain("Downloading Restic");
expect(stdout).toContain("Restic installed:");
expect(stdout).toContain("Restic verified:");
expect(stdout).toContain("restic");
expect(stdout).toContain("Restic setup complete");
});
it("creates backup helper script in workspace", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "test-agent",
repository: "/tmp/restic-repo",
password: "test-password",
install_restic: "false",
auto_init_repo: "false",
restore_on_start: "false",
});
const output = await executeScriptInContainer(state, "alpine");
const stdout = output.stdout.join("\n");
expect(stdout).toContain("Installing backup helper script");
expect(stdout).toContain("Backup helper installed:");
expect(stdout).toContain("/restic-backup");
expect(stdout).toContain("Backup helper verified as executable");
});
});
-271
View File
@@ -1,271 +0,0 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.12"
}
}
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "repository" {
type = string
description = "Restic repository location (e.g., 's3:s3.amazonaws.com/bucket', 'b2:bucket-name', '/local/path')."
}
variable "password" {
type = string
description = "Password for encrypting the Restic repository. Keep this secure!"
sensitive = true
}
variable "install_restic" {
type = bool
description = "Whether to install Restic binary."
default = true
}
variable "restic_version" {
type = string
description = "Version of Restic to install (e.g., '0.16.4' or 'latest')."
default = "latest"
}
variable "backup_paths" {
type = list(string)
description = "List of paths to backup. Can be absolute or relative to 'directory'."
default = ["/home/coder"]
}
variable "exclude_patterns" {
type = list(string)
description = "Patterns to exclude from backup (e.g., ['**/.git', '**/node_modules'])."
default = []
}
variable "backup_tags" {
type = list(string)
description = "Additional tags to apply to all snapshots."
default = []
}
variable "directory" {
type = string
description = "Working directory for backup operations."
default = "~"
}
variable "backup_on_stop" {
type = bool
description = "Whether to automatically backup when workspace stops."
default = true
}
variable "backup_interval_minutes" {
type = number
description = "Backup every N minutes while workspace is running (0 = disabled)."
default = 0
}
variable "restore_on_start" {
type = bool
description = "Whether to restore from backup when workspace starts."
default = true
}
variable "snapshot_id" {
type = string
description = "Specific snapshot ID to restore. If empty and restore_on_start is true, restores latest backup of this workspace. If set, restores that specific snapshot (useful for cloning workspaces)."
default = ""
}
variable "restore_target" {
type = string
description = "Target directory for restore ('/' restores to original paths)."
default = "/"
}
variable "start_blocks_login" {
type = bool
description = "Whether to block login until restore completes."
default = true
}
variable "custom_stop_script" {
type = string
description = "Custom script to run before stop backup."
default = ""
}
variable "retention_keep_last" {
type = number
description = "Keep last N snapshots per workspace."
default = 10
}
variable "retention_keep_daily" {
type = number
description = "Keep daily snapshots for N days."
default = 14
}
variable "retention_keep_weekly" {
type = number
description = "Keep weekly snapshots for N weeks."
default = 8
}
variable "retention_keep_monthly" {
type = number
description = "Keep monthly snapshots for N months."
default = 6
}
variable "auto_forget" {
type = bool
description = "Apply retention policies automatically after backup."
default = false
}
variable "auto_prune" {
type = bool
description = "Run prune after forget to reclaim space (slower but frees storage)."
default = false
}
variable "auto_init_repo" {
type = bool
description = "Automatically initialize repository if it doesn't exist."
default = true
}
variable "env" {
type = map(string)
description = "Environment variables for backend configuration (e.g., AWS_ACCESS_KEY_ID, B2_ACCOUNT_KEY). See README for backend-specific examples."
default = {}
sensitive = true
}
variable "icon" {
type = string
description = "Icon to use for Restic apps."
default = "/icon/restic.svg"
}
variable "order" {
type = number
description = "Order of apps in UI."
default = null
}
variable "group" {
type = string
description = "Group name for apps."
default = null
}
resource "coder_env" "restic_repository" {
agent_id = var.agent_id
name = "RESTIC_REPOSITORY"
value = var.repository
}
resource "coder_env" "restic_password" {
agent_id = var.agent_id
name = "RESTIC_PASSWORD"
value = var.password
}
resource "coder_env" "backend_env" {
for_each = nonsensitive(var.env)
agent_id = var.agent_id
name = each.key
value = each.value
}
resource "coder_env" "workspace_owner" {
agent_id = var.agent_id
name = "RESTIC_WORKSPACE_OWNER"
value = data.coder_workspace_owner.me.name
}
resource "coder_env" "workspace_name" {
agent_id = var.agent_id
name = "RESTIC_WORKSPACE_NAME"
value = data.coder_workspace.me.name
}
resource "coder_env" "workspace_id" {
agent_id = var.agent_id
name = "RESTIC_WORKSPACE_ID"
value = data.coder_workspace.me.id
}
resource "coder_script" "install_and_restore" {
agent_id = var.agent_id
display_name = "Restic Setup"
icon = var.icon
run_on_start = true
start_blocks_login = var.restore_on_start && var.start_blocks_login
script = templatefile("${path.module}/scripts/run.sh", {
INSTALL_RESTIC = var.install_restic
RESTIC_VERSION = var.restic_version
AUTO_INIT = var.auto_init_repo
RESTORE_ON_START = var.restore_on_start
SNAPSHOT_ID = var.snapshot_id
RESTORE_TARGET = var.restore_target
BACKUP_INTERVAL = var.backup_interval_minutes
BACKUP_PATHS = jsonencode(var.backup_paths)
EXCLUDE_PATTERNS = jsonencode(var.exclude_patterns)
BACKUP_TAGS = jsonencode(var.backup_tags)
DIRECTORY = var.directory
RETENTION_LAST = var.retention_keep_last
RETENTION_DAILY = var.retention_keep_daily
RETENTION_WEEKLY = var.retention_keep_weekly
RETENTION_MONTHLY = var.retention_keep_monthly
AUTO_FORGET = var.auto_forget
AUTO_PRUNE = var.auto_prune
BACKUP_SCRIPT_B64 = base64encode(file("${path.module}/scripts/backup.sh"))
})
}
resource "coder_script" "stop_backup" {
count = var.backup_on_stop ? 1 : 0
agent_id = var.agent_id
display_name = "Restic Backup"
icon = var.icon
run_on_stop = true
start_blocks_login = false
script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
${var.custom_stop_script}
"$CODER_SCRIPT_BIN_DIR/restic-backup" --tag "stop-backup"
EOT
}
resource "coder_app" "restic_backup" {
agent_id = var.agent_id
slug = "restic-backup"
display_name = "Backup Now"
icon = var.icon
order = var.order
group = var.group
command = "$CODER_SCRIPT_BIN_DIR/restic-backup --tag manual-backup"
}
@@ -1,333 +0,0 @@
run "required_variables" {
command = plan
variables {
agent_id = "test-agent"
repository = "s3:s3.amazonaws.com/test-bucket"
password = "test-password"
}
}
run "stop_backup_script_created_when_enabled" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
backup_on_stop = true
}
assert {
condition = coder_script.stop_backup[0].run_on_stop == true
error_message = "Stop backup script should have run_on_stop enabled"
}
assert {
condition = coder_script.stop_backup[0].agent_id == "test-agent"
error_message = "Stop backup script should use correct agent_id"
}
}
run "stop_backup_script_not_created_when_disabled" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
backup_on_stop = false
}
assert {
condition = length(coder_script.stop_backup) == 0
error_message = "Stop backup script should not be created when backup_on_stop is false"
}
}
run "restore_blocks_login_by_default" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
restore_on_start = true
}
assert {
condition = coder_script.install_and_restore.start_blocks_login == true
error_message = "Install script should block login when restore_on_start and start_blocks_login are true"
}
}
run "restore_does_not_block_login_when_disabled" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
restore_on_start = true
start_blocks_login = false
}
assert {
condition = coder_script.install_and_restore.start_blocks_login == false
error_message = "Install script should not block login when start_blocks_login is false"
}
}
run "workspace_metadata_env_vars_created" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
}
assert {
condition = coder_env.workspace_owner.name == "RESTIC_WORKSPACE_OWNER"
error_message = "Workspace owner env var should be RESTIC_WORKSPACE_OWNER"
}
assert {
condition = coder_env.workspace_name.name == "RESTIC_WORKSPACE_NAME"
error_message = "Workspace name env var should be RESTIC_WORKSPACE_NAME"
}
assert {
condition = coder_env.workspace_id.name == "RESTIC_WORKSPACE_ID"
error_message = "Workspace ID env var should be RESTIC_WORKSPACE_ID"
}
}
run "core_env_vars_created" {
command = plan
variables {
agent_id = "test-agent"
repository = "s3:s3.amazonaws.com/bucket"
password = "secure-password"
}
assert {
condition = coder_env.restic_repository.name == "RESTIC_REPOSITORY"
error_message = "Repository env var should be RESTIC_REPOSITORY"
}
assert {
condition = coder_env.restic_repository.value == "s3:s3.amazonaws.com/bucket"
error_message = "Repository env var should match input"
}
assert {
condition = coder_env.restic_password.name == "RESTIC_PASSWORD"
error_message = "Password env var should be RESTIC_PASSWORD"
}
}
run "safe_retention_defaults" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
}
# Verify auto_forget is false by default (safe)
assert {
condition = var.auto_forget == false
error_message = "auto_forget should be false by default for safety"
}
# Verify reasonable retention defaults
assert {
condition = var.retention_keep_last == 10
error_message = "Default retention_keep_last should be 10"
}
assert {
condition = var.retention_keep_daily == 14
error_message = "Default retention_keep_daily should be 14"
}
}
run "manual_backup_app_created" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
}
assert {
condition = coder_app.restic_backup.slug == "restic-backup"
error_message = "Backup app should have slug restic-backup"
}
assert {
condition = coder_app.restic_backup.display_name == "Backup Now"
error_message = "Backup app should display 'Backup Now'"
}
assert {
condition = can(regex("restic-backup", coder_app.restic_backup.command))
error_message = "Backup app command should call restic-backup helper"
}
}
run "install_restic_enabled_in_script" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
install_restic = true
}
assert {
condition = can(regex("INSTALL_RESTIC=\"true\"", coder_script.install_and_restore.script))
error_message = "Script should have INSTALL_RESTIC set to true"
}
}
run "install_restic_disabled_in_script" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
install_restic = false
}
assert {
condition = can(regex("INSTALL_RESTIC=\"false\"", coder_script.install_and_restore.script))
error_message = "Script should have INSTALL_RESTIC set to false"
}
}
run "auto_init_repo_configuration" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
auto_init_repo = false
}
assert {
condition = can(regex("AUTO_INIT=\"false\"", coder_script.install_and_restore.script))
error_message = "Script should have AUTO_INIT set to false"
}
}
run "restore_on_start_configuration" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
restore_on_start = true
snapshot_id = "abc123"
}
assert {
condition = can(regex("RESTORE_ON_START=\"true\"", coder_script.install_and_restore.script))
error_message = "Script should have RESTORE_ON_START set to true"
}
assert {
condition = can(regex("SNAPSHOT_ID=\"abc123\"", coder_script.install_and_restore.script))
error_message = "Script should have SNAPSHOT_ID set to abc123"
}
}
run "interval_backup_configuration" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
backup_interval_minutes = 30
}
assert {
condition = can(regex("BACKUP_INTERVAL=\"30\"", coder_script.install_and_restore.script))
error_message = "Script should have BACKUP_INTERVAL set to 30"
}
}
run "interval_backup_disabled_by_default" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
}
assert {
condition = can(regex("BACKUP_INTERVAL=\"0\"", coder_script.install_and_restore.script))
error_message = "Script should have BACKUP_INTERVAL set to 0 by default"
}
}
run "backup_paths_and_exclusions_configuration" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
backup_paths = ["/home/coder", "/workspace"]
exclude_patterns = ["*.log", "node_modules"]
backup_tags = ["production", "daily"]
}
assert {
condition = can(regex("/home/coder", coder_script.install_and_restore.script))
error_message = "Script should contain backup path /home/coder"
}
assert {
condition = can(regex("/workspace", coder_script.install_and_restore.script))
error_message = "Script should contain backup path /workspace"
}
assert {
condition = can(regex("\\*.log", coder_script.install_and_restore.script))
error_message = "Script should contain exclude pattern *.log"
}
assert {
condition = can(regex("production", coder_script.install_and_restore.script))
error_message = "Script should contain backup tag production"
}
}
run "custom_stop_script_included" {
command = plan
variables {
agent_id = "test-agent"
repository = "/tmp/restic-repo"
password = "test-password"
backup_on_stop = true
custom_stop_script = "echo 'Pre-backup cleanup'"
}
assert {
condition = can(regex("echo 'Pre-backup cleanup'", coder_script.stop_backup[0].script))
error_message = "Stop script should contain custom stop script"
}
}
@@ -1,104 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
CONF_FILE="$CODER_SCRIPT_DATA_DIR/restic-backup.conf"
if [ -f "$CONF_FILE" ]; then
# shellcheck source=/dev/null
source "$CONF_FILE"
else
echo "Error: Configuration file not found: $CONF_FILE" >&2
exit 1
fi
EXTRA_TAGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--tag)
EXTRA_TAGS+=("$2")
shift 2
;;
*)
echo "Unknown argument: $1" >&2
echo "Usage: restic-backup [--tag TAG]" >&2
exit 1
;;
esac
done
echo "--------------------------------"
echo "Restic Backup"
echo "--------------------------------"
DIRECTORY="${DIRECTORY/#\~/$HOME}"
PATHS=$(echo "$BACKUP_PATHS" | python3 -c "import json, sys; print(' '.join(json.load(sys.stdin)))" 2> /dev/null || echo ".")
EXCLUDES=$(echo "$EXCLUDE_PATTERNS" | python3 -c "import json, sys; [print(f'--exclude={p}') for p in json.load(sys.stdin)]" 2> /dev/null || echo "")
TAGS=$(echo "$BACKUP_TAGS" | python3 -c "import json, sys; [print(f'--tag={t}') for t in json.load(sys.stdin)]" 2> /dev/null || echo "")
TAG_ARGS=(
"--tag=workspace-id:$RESTIC_WORKSPACE_ID"
"--tag=workspace-owner:$RESTIC_WORKSPACE_OWNER"
"--tag=workspace-name:$RESTIC_WORKSPACE_NAME"
)
if [ -n "$TAGS" ]; then
while IFS= read -r tag; do
[ -n "$tag" ] && TAG_ARGS+=("$tag")
done <<< "$TAGS"
fi
for tag in "${EXTRA_TAGS[@]}"; do
TAG_ARGS+=("--tag=$tag")
done
EXCLUDE_ARGS=()
if [ -n "$EXCLUDES" ]; then
while IFS= read -r exclude; do
[ -n "$exclude" ] && EXCLUDE_ARGS+=("$exclude")
done <<< "$EXCLUDES"
fi
cd "$DIRECTORY" || {
echo "Error: Failed to change to directory: $DIRECTORY" >&2
exit 1
}
echo "Working directory: $(pwd)"
echo "Backup paths: $PATHS"
echo "Tags: ${TAG_ARGS[*]}"
[ ${#EXCLUDE_ARGS[@]} -gt 0 ] && echo "Exclusions: ${EXCLUDE_ARGS[*]}"
echo "Starting backup..."
# shellcheck disable=SC2086
if restic backup $PATHS "${TAG_ARGS[@]}" "${EXCLUDE_ARGS[@]}"; then
echo "Backup completed successfully"
else
echo "Error: Backup failed" >&2
exit 1
fi
if [ "$AUTO_FORGET" = "true" ]; then
echo "Applying retention policies..."
FORGET_ARGS=(
"--tag=workspace-id:$RESTIC_WORKSPACE_ID"
"--keep-last=$RETENTION_LAST"
)
[ "$RETENTION_DAILY" -gt 0 ] && FORGET_ARGS+=("--keep-daily=$RETENTION_DAILY")
[ "$RETENTION_WEEKLY" -gt 0 ] && FORGET_ARGS+=("--keep-weekly=$RETENTION_WEEKLY")
[ "$RETENTION_MONTHLY" -gt 0 ] && FORGET_ARGS+=("--keep-monthly=$RETENTION_MONTHLY")
if [ "$AUTO_PRUNE" = "true" ]; then
FORGET_ARGS+=("--prune")
echo "Pruning unreferenced data..."
fi
if restic forget "${FORGET_ARGS[@]}"; then
echo "Retention policies applied"
else
echo "Warning: Failed to apply retention policies" >&2
fi
fi
echo "Backup process complete"
@@ -1,296 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
: $${CODER_SCRIPT_BIN_DIR:=$HOME/.local/bin}
: $${CODER_SCRIPT_DATA_DIR:=$HOME/.local/share/coder}
mkdir -p "$CODER_SCRIPT_BIN_DIR"
mkdir -p "$CODER_SCRIPT_DATA_DIR"
export PATH="$HOME/.local/bin:$PATH"
INSTALL_RESTIC="${INSTALL_RESTIC}"
RESTIC_VERSION="${RESTIC_VERSION}"
AUTO_INIT="${AUTO_INIT}"
RESTORE_ON_START="${RESTORE_ON_START}"
SNAPSHOT_ID="${SNAPSHOT_ID}"
RESTORE_TARGET="${RESTORE_TARGET}"
BACKUP_INTERVAL="${BACKUP_INTERVAL}"
BACKUP_PATHS='${BACKUP_PATHS}'
EXCLUDE_PATTERNS='${EXCLUDE_PATTERNS}'
BACKUP_TAGS='${BACKUP_TAGS}'
DIRECTORY="${DIRECTORY}"
RETENTION_LAST="${RETENTION_LAST}"
RETENTION_DAILY="${RETENTION_DAILY}"
RETENTION_WEEKLY="${RETENTION_WEEKLY}"
RETENTION_MONTHLY="${RETENTION_MONTHLY}"
AUTO_FORGET="${AUTO_FORGET}"
AUTO_PRUNE="${AUTO_PRUNE}"
BACKUP_SCRIPT_B64='${BACKUP_SCRIPT_B64}'
echo "--------------------------------"
echo "Restic Backup Module Setup"
echo "--------------------------------"
detect_os_arch() {
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
ARCH="amd64"
;;
aarch64 | arm64)
ARCH="arm64"
;;
armv7l)
ARCH="arm"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac
case "$OS" in
linux | darwin) ;;
*)
echo "Unsupported OS: $OS"
exit 1
;;
esac
echo "Detected OS: $OS, Architecture: $ARCH"
}
install_restic() {
if [ "$INSTALL_RESTIC" != "true" ]; then
echo "Skipping Restic installation (install_restic=false)"
return
fi
if command -v restic > /dev/null 2>&1; then
INSTALLED_VERSION=$(restic version | head -n1 | awk '{print $2}')
echo "Restic already installed: $INSTALLED_VERSION"
if [ "$RESTIC_VERSION" != "latest" ] && [ "$INSTALLED_VERSION" != "$RESTIC_VERSION" ]; then
echo "Warning: Version mismatch (installed: $INSTALLED_VERSION, requested: $RESTIC_VERSION)"
fi
return
fi
echo "Installing Restic..."
detect_os_arch
if [ "$RESTIC_VERSION" = "latest" ]; then
echo "Fetching latest version..."
LATEST_VERSION=$(curl -fsSL https://api.github.com/repos/restic/restic/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
if [ -z "$LATEST_VERSION" ]; then
echo "Error: Failed to fetch latest version"
exit 1
fi
echo "Version: $LATEST_VERSION"
DOWNLOAD_URL="https://github.com/restic/restic/releases/download/v$${LATEST_VERSION}/restic_$${LATEST_VERSION}_$${OS}_$${ARCH}.bz2"
else
DOWNLOAD_URL="https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_$${OS}_$${ARCH}.bz2"
fi
echo "Downloading Restic..."
mkdir -p "$HOME/.local/bin"
TMP_FILE=$(mktemp)
if curl -fsSL "$DOWNLOAD_URL" -o "$TMP_FILE"; then
bunzip2 -c "$TMP_FILE" > "$HOME/.local/bin/restic"
chmod +x "$HOME/.local/bin/restic"
rm "$TMP_FILE"
echo "Restic installed: $($HOME/.local/bin/restic version)"
else
echo "Error: Download failed"
rm -f "$TMP_FILE"
exit 1
fi
}
verify_installation() {
if ! command -v restic > /dev/null 2>&1; then
echo "Error: restic command not found in PATH"
echo "PATH: $PATH"
if [ "$INSTALL_RESTIC" = "true" ]; then
exit 1
else
echo "Warning: restic not found but install_restic=false, continuing anyway"
return
fi
fi
echo "Restic verified: $(restic version | head -n1)"
}
init_repository() {
if [ "$AUTO_INIT" != "true" ]; then
echo "Skipping repository initialization (auto_init_repo=false)"
return
fi
echo "Checking repository..."
if restic snapshots > /dev/null 2>&1; then
echo "Repository already initialized"
return
fi
echo "Initializing repository..."
if restic init; then
echo "Repository initialized"
else
echo "Error: Failed to initialize repository"
exit 1
fi
}
install_backup_helper() {
echo "Installing backup helper script..."
HELPER_SCRIPT="$CODER_SCRIPT_BIN_DIR/restic-backup"
echo -n "$BACKUP_SCRIPT_B64" | base64 -d > "$HELPER_SCRIPT"
chmod +x "$HELPER_SCRIPT"
cat > "$CODER_SCRIPT_DATA_DIR/restic-backup.conf" << EOF
BACKUP_PATHS='$BACKUP_PATHS'
EXCLUDE_PATTERNS='$EXCLUDE_PATTERNS'
BACKUP_TAGS='$BACKUP_TAGS'
DIRECTORY='$DIRECTORY'
RETENTION_LAST='$RETENTION_LAST'
RETENTION_DAILY='$RETENTION_DAILY'
RETENTION_WEEKLY='$RETENTION_WEEKLY'
RETENTION_MONTHLY='$RETENTION_MONTHLY'
AUTO_FORGET='$AUTO_FORGET'
AUTO_PRUNE='$AUTO_PRUNE'
EOF
if [ ! -x "$HELPER_SCRIPT" ]; then
echo "Error: Backup helper is not executable"
exit 1
fi
echo "Backup helper installed: $HELPER_SCRIPT"
echo "Backup helper verified as executable"
}
find_latest_snapshot() {
local TAG_FILTER="$1"
SNAPSHOTS_JSON=$(restic snapshots --tag "$TAG_FILTER" --json 2> /dev/null || echo "[]")
LATEST_SNAPSHOT=$(echo "$SNAPSHOTS_JSON" | python3 -c "
import json, sys
snapshots = json.load(sys.stdin)
if snapshots:
latest = max(snapshots, key=lambda s: s['time'])
print(latest['short_id'])
else:
print('')
" 2> /dev/null || echo "")
echo "$LATEST_SNAPSHOT"
}
restore_on_start() {
if [ "$RESTORE_ON_START" != "true" ]; then
echo "Skipping restore (restore_on_start=false)"
return
fi
echo "--------------------------------"
echo "Restore Configuration"
echo "--------------------------------"
SNAPSHOT_TO_RESTORE=""
if [ -n "$SNAPSHOT_ID" ]; then
echo "Restoring specific snapshot: $SNAPSHOT_ID"
SNAPSHOT_TO_RESTORE="$SNAPSHOT_ID"
else
echo "Finding latest backup for this workspace..."
SNAPSHOT_TO_RESTORE=$(find_latest_snapshot "workspace-id:$RESTIC_WORKSPACE_ID")
if [ -z "$SNAPSHOT_TO_RESTORE" ]; then
echo "No previous backup found"
echo "Starting with fresh workspace"
return
fi
echo "Found snapshot: $SNAPSHOT_TO_RESTORE"
fi
echo "Restoring to $RESTORE_TARGET..."
if restic restore "$SNAPSHOT_TO_RESTORE" --target "$RESTORE_TARGET"; then
echo "Restore completed successfully"
else
echo "Error: Restore failed"
exit 1
fi
}
setup_interval_backup() {
if [ "$BACKUP_INTERVAL" -eq 0 ]; then
return
fi
echo "Setting up interval backup (every $BACKUP_INTERVAL minutes)..."
cat > "$CODER_SCRIPT_DATA_DIR/interval-backup.sh" << 'EOFSCRIPT'
#!/usr/bin/env bash
set -euo pipefail
INTERVAL_MINUTES="$1"
INTERVAL_SECONDS=$((INTERVAL_MINUTES * 60))
echo "Starting interval backup loop (every $INTERVAL_MINUTES minutes)"
while true; do
sleep "$INTERVAL_SECONDS"
echo "Running scheduled backup..."
if "$CODER_SCRIPT_BIN_DIR/restic-backup" --tag "interval-backup"; then
echo "Scheduled backup completed"
else
echo "Scheduled backup failed"
fi
done
EOFSCRIPT
chmod +x "$CODER_SCRIPT_DATA_DIR/interval-backup.sh"
nohup "$CODER_SCRIPT_DATA_DIR/interval-backup.sh" "$BACKUP_INTERVAL" \
>> "$CODER_SCRIPT_DATA_DIR/interval-backup.log" 2>&1 &
echo "Interval backup started in background (PID: $!)"
}
main() {
install_restic
verify_installation
init_repository
install_backup_helper
restore_on_start
setup_interval_backup
echo "--------------------------------"
echo "Restic setup complete"
echo "--------------------------------"
echo "Available commands:"
echo " restic-backup - Run manual backup"
echo " restic snapshots - List all snapshots"
echo " restic restore <id> - Restore specific snapshot"
echo ""
echo "Repository: $${RESTIC_REPOSITORY:-not set}"
}
main