mirror of
https://github.com/coder/registry.git
synced 2026-06-03 13:08:14 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1786a09ea | |||
| a35986d7df |
@@ -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
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 202 KiB |
@@ -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.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
@@ -49,7 +49,7 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.1.1"
|
||||
version = "3.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@@ -85,7 +85,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.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -108,7 +108,7 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "3.1.1"
|
||||
version = "3.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
@@ -181,7 +181,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.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -238,7 +238,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.0"
|
||||
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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user