From ee910722cb8b8cf82879e661a8c41986e5c1bb27 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Sun, 17 Dec 2023 08:51:11 +0000 Subject: [PATCH] feat: add backup support for Code Flow Add support for taking on demand backups of Code Flow. Resolves flow/services#7 --- README.asciidoc | 27 ++++++++- config | 2 +- internal/actions/backup.go | 47 ++++++++++++++++ internal/actions/deploy.go | 16 +++++- internal/actions/prepare.go | 10 ++++ internal/actions/shutdown.go | 38 +++++++++++++ internal/actions/stop.go | 16 +++++- internal/config/config.go | 16 ++++-- magefiles/magefile.go | 59 ++++++++------------ templates/compose/docker-compose.yaml.gotmpl | 15 +++++ 10 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 internal/actions/backup.go create mode 100644 internal/actions/shutdown.go diff --git a/README.asciidoc b/README.asciidoc index c8cd54f..b8d7c76 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -35,8 +35,9 @@ The repository manages the services hosted on Flow Platform with the use of http ---- $ mage -l Targets: + backup creates a backup of the specified service. clean cleans the workspace. - deploy deploys the services to the Flow Platform. + deploy deploys the service(s) to the Flow Platform. prepare prepares the service's build directory. shutdown shuts down the Flow Platform in a given environment. stop stops a running service within the Flow Platform. @@ -59,3 +60,27 @@ mage prepare production forgejo ---- mage deploy production forgejo ---- + +== Backup Guide + +There is support for backing up the data for some of the services, e.g. `forgejo`. You can use mage to run the backup for a specific service. + +[source,console] +---- +mage backup production forgejo +---- + +Here mage will stop the running service and recreate the container running the specific backup script for that service. If there is no backup support for a particular service mage will inform you of this. + +[source,console] +---- +$ mage backup production traefik +Backup is not supported for "traefik". +---- + +Currently the service will not automatically resume after the backup is completed but you can achieve this with a chain command. + +[source,console] +---- +mage backup production forgejo && mage deploy production forgejo +---- diff --git a/config b/config index d332a51..1766c5e 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit d332a51218fc2b5cb152fd2439df061940c07dab +Subproject commit 1766c5ec179d869fc4999523cba036b75e0b4fd7 diff --git a/internal/actions/backup.go b/internal/actions/backup.go new file mode 100644 index 0000000..97be076 --- /dev/null +++ b/internal/actions/backup.go @@ -0,0 +1,47 @@ +package actions + +import ( + "fmt" + + "flow/services/internal/config" + "flow/services/internal/services" +) + +const containerRunModeBackup = "backup" + +func Backup(environment, service string) error { + cfg, err := config.NewConfig(environment) + if err != nil { + return fmt.Errorf("unable to load the configuration; %w", err) + } + + switch service { + case services.Forgejo: + cfg.Forgejo.ContainerRunMode = containerRunModeBackup + default: + fmt.Printf("Backup is not supported for %q.\n", service) + return nil + } + + if err := backup(cfg, environment, service); err != nil { + return fmt.Errorf("error performing a backup of %q; %w", service, err) + } + + return nil +} + +func backup(cfg config.Config, environment, service string) error { + if err := stop(cfg.Docker.Host, environment, service); err != nil { + return fmt.Errorf("unable to stop %q; %w", service, err) + } + + if err := prepareCompose(cfg, environment); err != nil { + return fmt.Errorf("unable to regenerate the compose file for the backup; %w", err) + } + + if err := deploy(cfg.Docker.Host, environment, service, false); err != nil { + return fmt.Errorf("error deploying the backup container; %w", err) + } + + return nil +} diff --git a/internal/actions/deploy.go b/internal/actions/deploy.go index 7b3b30e..96fddb7 100644 --- a/internal/actions/deploy.go +++ b/internal/actions/deploy.go @@ -2,13 +2,27 @@ package actions import ( "flow/services/internal" + "flow/services/internal/config" "fmt" "os" "github.com/magefile/mage/sh" ) -func Deploy(dockerHost, environment, service string, daemon bool) error { +func Deploy(environment, service string) error { + cfg, err := config.NewConfig(environment) + if err != nil { + return fmt.Errorf("unable to load the configuration; %w", err) + } + + if err := deploy(cfg.Docker.Host, environment, service, true); err != nil { + return fmt.Errorf("error deploying %q; %w", service, err) + } + + return nil +} + +func deploy(dockerHost, environment, service string, daemon bool) error { os.Setenv("DOCKER_HOST", dockerHost) command := []string{ diff --git a/internal/actions/prepare.go b/internal/actions/prepare.go index 4cca368..28661d7 100644 --- a/internal/actions/prepare.go +++ b/internal/actions/prepare.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "text/template" + "time" "flow/services/internal" "flow/services/internal/bundle" @@ -141,6 +142,7 @@ func render(cfg config.Config, environment, component string) error { funcMap := template.FuncMap{ "default": defaultValue, + "timestamp": timestamp(), } for _, f := range files { @@ -246,3 +248,11 @@ func defaultValue(value, defaultValue string) string { return value } + +func timestamp() func() string { + now := time.Now().Format("20060102-1504") + + return func() string { + return now + } +} diff --git a/internal/actions/shutdown.go b/internal/actions/shutdown.go new file mode 100644 index 0000000..214bda7 --- /dev/null +++ b/internal/actions/shutdown.go @@ -0,0 +1,38 @@ +package actions + +import ( + "fmt" + "os" + + "flow/services/internal" + "flow/services/internal/config" + + "github.com/magefile/mage/sh" +) + +func Shutdown(environment string) error { + cfg, err := config.NewConfig(environment) + if err != nil { + return fmt.Errorf("unable to load the configuration; %w", err) + } + + if err := shutdown(cfg.Docker.Host, environment); err != nil { + return fmt.Errorf("unable to shutdown the %q environment; %w", environment, err) + } + + return nil +} + +func shutdown(dockerHost, environment string) error { + os.Setenv("DOCKER_HOST", dockerHost) + + command := []string{ + "docker", + "compose", + "--project-directory", + fmt.Sprintf("%s/%s/compose", internal.RootBuildDir, environment), + "down", + } + + return sh.RunV(command[0], command[1:]...) +} diff --git a/internal/actions/stop.go b/internal/actions/stop.go index 3cdf12d..37d557b 100644 --- a/internal/actions/stop.go +++ b/internal/actions/stop.go @@ -2,13 +2,27 @@ package actions import ( "flow/services/internal" + "flow/services/internal/config" "fmt" "os" "github.com/magefile/mage/sh" ) -func Stop(dockerHost, environment, service string) error { +func Stop(environment, service string) error { + cfg, err := config.NewConfig(environment) + if err != nil { + return fmt.Errorf("unable to load the configuration; %w", err) + } + + if err := stop(cfg.Docker.Host, environment, service); err != nil { + return fmt.Errorf("unable to stop %q; %w", service, err) + } + + return nil +} + +func stop(dockerHost, environment, service string) error { os.Setenv("DOCKER_HOST", dockerHost) command := []string{ diff --git a/internal/config/config.go b/internal/config/config.go index ac476be..7e1395b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -78,6 +78,8 @@ type Forgejo struct { Oauth2JwtSigningAlgo string `json:"oauth2JwtSigningAlgo"` Oauth2JwtSecret string `json:"oauth2JwtSecret"` Actions ForgejoActions `json:"actions"` + Backups ForgejoBackups `json:"backups"` + ContainerRunMode string } type ForgejoActions struct { @@ -85,6 +87,10 @@ type ForgejoActions struct { DefaultActionsURL string `json:"defaultActionsURL"` } +type ForgejoBackups struct { + ContainerDirectory string `json:"containerDirectory"` +} + type Gotosocial struct { Version string `json:"version"` Name string `json:"name"` @@ -139,21 +145,21 @@ type LandingPageLinks struct { } func NewConfig(environment string) (Config, error) { - var c Config + var cfg Config path := filepath.Join(configDir, environment, configFileName) f, err := os.Open(path) if err != nil { - return c, fmt.Errorf("unable to open the file; %w", err) + return cfg, fmt.Errorf("unable to open the file; %w", err) } defer f.Close() decoder := json.NewDecoder(f) - if err = decoder.Decode(&c); err != nil { - return c, fmt.Errorf("unable to decode JSON data; %w", err) + if err = decoder.Decode(&cfg); err != nil { + return cfg, fmt.Errorf("unable to decode JSON data; %w", err) } - return c, nil + return cfg, nil } diff --git a/magefiles/magefile.go b/magefiles/magefile.go index 315b81a..ba18739 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -5,7 +5,6 @@ package main import ( "flow/services/internal" "flow/services/internal/actions" - "flow/services/internal/config" "fmt" "os" "path/filepath" @@ -14,6 +13,15 @@ import ( "github.com/magefile/mage/sh" ) +// Backup creates a backup of the specified service. +func Backup(environment, service string) error { + if err := actions.Backup(environment, service); err != nil { + return fmt.Errorf("error running Backup for %q; %w", service, err) + } + + return nil +} + // Clean cleans the workspace. func Clean(environment string) error { buildDir := filepath.Join(internal.RootBuildDir, environment) @@ -42,13 +50,8 @@ func Deploy(environment, service string) error { mg.F(Prepare, environment, service), ) - cfg, err := config.NewConfig(environment) - if err != nil { - return fmt.Errorf("unable to load the configuration; %w", err) - } - - if err := actions.Deploy(cfg.Docker.Host, environment, service, true); err != nil { - return fmt.Errorf("error deploying %q; %w", service, err) + if err := actions.Deploy(environment, service); err != nil { + return fmt.Errorf("error running Deploy for %q; %w", service, err) } return nil @@ -57,7 +60,7 @@ func Deploy(environment, service string) error { // Prepare prepares the service's build directory. func Prepare(environment, service string) error { if err := actions.Prepare(environment, service); err != nil { - return fmt.Errorf("error running preparations; %w", err) + return fmt.Errorf("error running Prepare for %q; %w", service, err) } return nil @@ -65,33 +68,17 @@ func Prepare(environment, service string) error { // Shutdown shuts down the Flow Platform in a given environment. func Shutdown(environment string) error { - cfg, err := config.NewConfig(environment) - if err != nil { - return fmt.Errorf("unable to load the configuration; %w", err) - } - - os.Setenv("DOCKER_HOST", cfg.Docker.Host) - - command := []string{ - "docker", - "compose", - "--project-directory", - fmt.Sprintf("%s/%s/compose", internal.RootBuildDir, environment), - "down", - } - - return sh.RunV(command[0], command[1:]...) -} - -// Stop stops a running service within the Flow Platform. -func Stop(environment, service string) error { - cfg, err := config.NewConfig(environment) - if err != nil { - return fmt.Errorf("unable to load the configuration; %w", err) - } - - if err := actions.Stop(cfg.Docker.Host, environment, service); err != nil { - return fmt.Errorf("error stopping %q; %w", service, err) + if err := actions.Shutdown(environment); err != nil { + return fmt.Errorf("error running Shutdown for %q; %w", environment, err) + } + + return nil +} + +// Stop stops a running service within the Flow Platform. +func Stop(environment, service string) error { + if err := actions.Stop(environment, service); err != nil { + return fmt.Errorf("error running Stop for %q; %w", service, err) } return nil diff --git a/templates/compose/docker-compose.yaml.gotmpl b/templates/compose/docker-compose.yaml.gotmpl index 01e2874..4ffea8f 100644 --- a/templates/compose/docker-compose.yaml.gotmpl +++ b/templates/compose/docker-compose.yaml.gotmpl @@ -55,13 +55,28 @@ services: image: "localhost/flow/forgejo:{{ .Forgejo.Version }}" build: context: "../forgejo" + {{- if eq .Forgejo.ContainerRunMode "backup" }} + command: + - forgejo + - dump + - --config + - {{ .Forgejo.AppIni }} + - --file + - {{ .Forgejo.Backups.ContainerDirectory }}/forgejo-backup-v{{ .Forgejo.Version }}-{{ timestamp }}.tar.gz + - --type + - tar.gz + {{- end }} expose: - "{{ .Forgejo.SshPort }}" - "{{ .Forgejo.HttpPort }}" networks: flow: ipv4_address: "{{ .Forgejo.ContainerIpv4Address }}" + {{- if eq .Forgejo.ContainerRunMode "backup" }} + restart: "no" + {{- else }} restart: "always" + {{- end }} volumes: {{- template "defaultVolumes" }} # Forgejo data volume