forked from Nixius/authelia
Archive customer volumes on expiry, restore on resubscribe
When a subscription is deleted, all Docker volumes for the customer
stack are tarred to /data/archives/{stackName}/ before the stack is
removed and volumes pruned. On resubscribe or reactivation, volumes
are restored from the archive before deploying the stack.
Made-with: Cursor
This commit is contained in:
parent
75b63ca923
commit
7e40fea6f3
|
|
@ -20,6 +20,7 @@ type Config struct {
|
|||
TraefikNetwork string
|
||||
TemplatePath string
|
||||
CustomerDomain string
|
||||
ArchivePath string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
|
|
@ -41,6 +42,7 @@ func Load() *Config {
|
|||
TraefikNetwork: envOrDefault("TRAEFIK_NETWORK", "authelia_dev"),
|
||||
TemplatePath: envOrDefault("TEMPLATE_PATH", "/app/templates"),
|
||||
CustomerDomain: envOrDefault("CUSTOMER_DOMAIN", "app.a250.ca"),
|
||||
ArchivePath: envOrDefault("ARCHIVE_PATH", "/archives"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ func (a *App) handleActivatePost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
stackName := fmt.Sprintf("customer-%s", remoteUser)
|
||||
if err := a.swarm.RestoreVolumes(stackName, a.cfg.ArchivePath); err != nil {
|
||||
log.Printf("activate: volume restore failed for %s: %v", remoteUser, err)
|
||||
}
|
||||
if err := a.swarm.DeployStack(stackName, remoteUser, a.cfg.TraefikDomain); err != nil {
|
||||
log.Printf("activate: stack deploy failed for %s: %v", remoteUser, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,9 @@ func (a *App) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
|||
stackName := fmt.Sprintf("customer-%s", result.Username)
|
||||
exists, _ := a.swarm.StackExists(stackName)
|
||||
if !exists {
|
||||
if err := a.swarm.RestoreVolumes(stackName, a.cfg.ArchivePath); err != nil {
|
||||
log.Printf("resubscribe: volume restore failed for %s: %v", result.Username, err)
|
||||
}
|
||||
if err := a.swarm.DeployStack(stackName, result.Username, a.cfg.TraefikDomain); err != nil {
|
||||
log.Printf("resubscribe: stack deploy failed for %s: %v", result.Username, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,11 +83,15 @@ func (a *App) onSubscriptionDeleted(event stripego.Event) {
|
|||
}
|
||||
|
||||
stackName := "customer-" + username
|
||||
if err := a.swarm.ArchiveVolumes(stackName, a.cfg.ArchivePath); err != nil {
|
||||
log.Printf("archive failed for %s: %v", stackName, err)
|
||||
}
|
||||
|
||||
if err := a.swarm.RemoveStack(stackName); err != nil {
|
||||
log.Printf("stack remove failed for %s: %v", stackName, err)
|
||||
}
|
||||
|
||||
log.Printf("deprovisioned stack for customer %s (%s)", customerID, username)
|
||||
log.Printf("deprovisioned stack for customer %s (%s), volumes archived", customerID, username)
|
||||
}
|
||||
|
||||
func (a *App) onSubscriptionUpdated(event stripego.Event) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
package swarm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ArchiveVolumes tars each Docker volume belonging to stackName into archiveDir
|
||||
// and removes the volumes afterwards. Archive layout:
|
||||
//
|
||||
// {archiveDir}/{stackName}/{volumeName}.tar.gz
|
||||
func (c *Client) ArchiveVolumes(stackName, archiveDir string) error {
|
||||
vols, err := c.listVolumes(stackName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list volumes: %w", err)
|
||||
}
|
||||
if len(vols) == 0 {
|
||||
log.Printf("archive: no volumes found for %s, nothing to archive", stackName)
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Join(archiveDir, stackName)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("create archive dir: %w", err)
|
||||
}
|
||||
|
||||
for _, vol := range vols {
|
||||
tarName := vol + ".tar.gz"
|
||||
log.Printf("archive: backing up volume %s -> %s/%s", vol, dir, tarName)
|
||||
|
||||
cmd := exec.Command("docker", "run", "--rm",
|
||||
"-v", vol+":/data:ro",
|
||||
"-v", dir+":/backup",
|
||||
"alpine",
|
||||
"tar", "czf", "/backup/"+tarName, "-C", "/data", ".",
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
|
||||
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
log.Printf("archive: failed to tar %s: %s: %v", vol, strings.TrimSpace(string(out)), err)
|
||||
continue
|
||||
}
|
||||
|
||||
rmCmd := exec.Command("docker", "volume", "rm", vol)
|
||||
rmCmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
|
||||
if out, err := rmCmd.CombinedOutput(); err != nil {
|
||||
log.Printf("archive: failed to remove volume %s: %s: %v", vol, strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("archive: completed for %s (%d volumes)", stackName, len(vols))
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestoreVolumes recreates Docker volumes from a previous archive. No-op if
|
||||
// no archive directory exists for the stack (fresh customer).
|
||||
func (c *Client) RestoreVolumes(stackName, archiveDir string) error {
|
||||
dir := filepath.Join(archiveDir, stackName)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("read archive dir: %w", err)
|
||||
}
|
||||
|
||||
restored := 0
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if !strings.HasSuffix(name, ".tar.gz") {
|
||||
continue
|
||||
}
|
||||
vol := strings.TrimSuffix(name, ".tar.gz")
|
||||
|
||||
log.Printf("restore: extracting %s -> volume %s", name, vol)
|
||||
|
||||
createCmd := exec.Command("docker", "volume", "create", vol)
|
||||
createCmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
|
||||
if out, err := createCmd.CombinedOutput(); err != nil {
|
||||
log.Printf("restore: volume create %s failed: %s: %v", vol, strings.TrimSpace(string(out)), err)
|
||||
continue
|
||||
}
|
||||
|
||||
cmd := exec.Command("docker", "run", "--rm",
|
||||
"-v", vol+":/data",
|
||||
"-v", dir+":/backup:ro",
|
||||
"alpine",
|
||||
"tar", "xzf", "/backup/"+name, "-C", "/data",
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
|
||||
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
log.Printf("restore: extract %s failed: %s: %v", name, strings.TrimSpace(string(out)), err)
|
||||
continue
|
||||
}
|
||||
|
||||
os.Remove(filepath.Join(dir, name))
|
||||
restored++
|
||||
}
|
||||
|
||||
if restored > 0 {
|
||||
os.Remove(dir)
|
||||
log.Printf("restore: completed for %s (%d volumes)", stackName, restored)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) listVolumes(stackName string) ([]string, error) {
|
||||
cmd := exec.Command("docker", "volume", "ls",
|
||||
"--filter", "name="+stackName+"_",
|
||||
"--format", "{{.Name}}",
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("docker volume ls: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
|
||||
var vols []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if line != "" {
|
||||
vols = append(vols, line)
|
||||
}
|
||||
}
|
||||
return vols, nil
|
||||
}
|
||||
|
|
@ -195,8 +195,10 @@ services:
|
|||
- TRAEFIK_NETWORK=authelia_dev
|
||||
- CUSTOMER_DOMAIN=app.a250.ca
|
||||
- TEMPLATE_PATH=/app/templates
|
||||
- ARCHIVE_PATH=/archives
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- atlas_archives:/archives
|
||||
networks:
|
||||
- authelia_dev
|
||||
deploy:
|
||||
|
|
@ -230,3 +232,4 @@ volumes:
|
|||
redis_data:
|
||||
authelia_data:
|
||||
lldap_data:
|
||||
atlas_archives:
|
||||
|
|
|
|||
Loading…
Reference in New Issue