Compare commits
1 Commits
main
...
cat/restic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d638371a85 |
590
.icons/restic.svg
Normal file
590
.icons/restic.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 202 KiB |
523
registry/coder/modules/restic/README.md
Normal file
523
registry/coder/modules/restic/README.md
Normal file
@ -0,0 +1,523 @@
|
|||||||
|
---
|
||||||
|
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)
|
||||||
75
registry/coder/modules/restic/main.test.ts
Normal file
75
registry/coder/modules/restic/main.test.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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
registry/coder/modules/restic/main.tf
Normal file
271
registry/coder/modules/restic/main.tf
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
333
registry/coder/modules/restic/restic.tftest.hcl
Normal file
333
registry/coder/modules/restic/restic.tftest.hcl
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
104
registry/coder/modules/restic/scripts/backup.sh
Normal file
104
registry/coder/modules/restic/scripts/backup.sh
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#!/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"
|
||||||
296
registry/coder/modules/restic/scripts/run.sh
Normal file
296
registry/coder/modules/restic/scripts/run.sh
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
#!/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
|
||||||
Loading…
x
Reference in New Issue
Block a user