forked from toolshed/abra
Compare commits
31 Commits
0.4.0-alph
...
0.4.1-alph
Author | SHA1 | Date | |
---|---|---|---|
4ba15df9b7
|
|||
5721b357a2
|
|||
6140abbcac | |||
996255188b | |||
11d78234b2
|
|||
c214937e4a
|
|||
3a3f41988b
|
|||
f6690a80bd
|
|||
2337c4648b
|
|||
a1190f1352
|
|||
e421922f5b
|
|||
10d5705d1a
|
|||
a4f1634b24
|
|||
cbd924060f
|
|||
3c4bb6a55e
|
|||
a0d7a76f9d
|
|||
c71efb46ba
|
|||
ce69967ec5
|
|||
1a04439b1f | |||
979f417a63
|
|||
b27acb2f61
|
|||
622ecc4885
|
|||
ed5bbda811
|
|||
7b627ea518
|
|||
1ac66da83f
|
|||
061de96b62 | |||
6998298d32
|
|||
323f4467c8
|
|||
e8e41850b5
|
|||
0e23ec53d7
|
|||
b943a8b9b1
|
@ -7,7 +7,6 @@ gitea_urls:
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
- go generate ./...
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
@ -15,6 +14,15 @@ builds:
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
ldflags:
|
||||
- "-X 'main.Commit={{ .Commit }}'"
|
||||
- "-X 'main.Version={{ .Version }}'"
|
||||
|
15
LICENSE
Normal file
15
LICENSE
Normal file
@ -0,0 +1,15 @@
|
||||
Abra: The Co-op Cloud utility belt
|
||||
Copyright (C) 2022 Co-op Cloud <helo@coopcloud.tech>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
10
README.md
10
README.md
@ -1,12 +1,12 @@
|
||||
# abra
|
||||
|
||||
> https://coopcloud.tech
|
||||
# `abra`
|
||||
|
||||
[](https://build.coopcloud.tech/coop-cloud/abra)
|
||||
[](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
|
||||
|
||||
The Co-op Cloud utility belt 🎩🐇
|
||||
|
||||
`abra` is our flagship client & command-line tool which has been developed specifically in the context of the Co-op Cloud project for the purpose of making day-to-day operations for [operators](https://docs.coopcloud.tech/operators/) and [maintainers](https://docs.coopcloud.tech/maintainers/) as convenient as possible. It is libre software, written in [Go](https://go.dev) and maintained and extended by the community ❤
|
||||
<a href="https://github.com/egonelbre/gophers"><img align="right" width="150" src="https://github.com/egonelbre/gophers/raw/master/.thumb/sketch/adventure/poking-fire.png"/></a>
|
||||
|
||||
Please see [docs.coopcloud.tech/abra/](https://docs.coopcloud.tech/abra/) for help on install, upgrade, hacking, troubleshooting & more!
|
||||
`abra` is our flagship client & command-line tool which has been developed specifically in the context of the Co-op Cloud project for the purpose of making the day-to-day operations of [operators](https://docs.coopcloud.tech/operators/) and [maintainers](https://docs.coopcloud.tech/maintainers/) pleasant & convenient. It is libre software, written in [Go](https://go.dev) and maintained and extended by the community :heart:
|
||||
|
||||
Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more!
|
||||
|
@ -30,5 +30,7 @@ var AppCommand = cli.Command{
|
||||
appVersionCommand,
|
||||
appErrorsCommand,
|
||||
appCmdCommand,
|
||||
appBackupCommand,
|
||||
appRestoreCommand,
|
||||
},
|
||||
}
|
||||
|
389
cli/app/backup.go
Normal file
389
cli/app/backup.go
Normal file
@ -0,0 +1,389 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
containerPkg "coopcloud.tech/abra/pkg/container"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/upstream/container"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
"github.com/klauspost/pgzip"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type backupConfig struct {
|
||||
preHookCmd string
|
||||
postHookCmd string
|
||||
backupPaths []string
|
||||
}
|
||||
|
||||
var appBackupCommand = cli.Command{
|
||||
Name: "backup",
|
||||
Aliases: []string{"bk"},
|
||||
Usage: "Run app backup",
|
||||
ArgsUsage: "<domain> [<service>]",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Description: `
|
||||
This command runs an app backup.
|
||||
|
||||
A backup command and pre/post hook commands are defined in the recipe
|
||||
configuration. Abra reads this configuration and run the comands in the context
|
||||
of the deployed services. Pass <service> if you only want to back up a single
|
||||
service. All backups are placed in the ~/.abra/backups directory.
|
||||
|
||||
A single backup file is produced for all backup paths specified for a service.
|
||||
If we have the following backup configuration:
|
||||
|
||||
- "backupbot.backup.path=/var/lib/foo,/var/lib/bar"
|
||||
|
||||
And we run "abra app backup example.com app", Abra will produce a file that
|
||||
looks like:
|
||||
|
||||
~/.abra/backups/example_com_app_609341138.tar.gz
|
||||
|
||||
This file is a compressed archive which contains all backup paths. To see paths, run:
|
||||
|
||||
tar -tf ~/.abra/backups/example_com_app_609341138.tar.gz
|
||||
|
||||
(Make sure to change the name of the backup file)
|
||||
|
||||
This single file can be used to restore your app. See "abra app restore" for more.
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
recipe, err := recipe.Get(app.Recipe)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
backupConfigs := make(map[string]backupConfig)
|
||||
for _, service := range recipe.Config.Services {
|
||||
if backupsEnabled, ok := service.Deploy.Labels["backupbot.backup"]; ok {
|
||||
if backupsEnabled == "true" {
|
||||
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
|
||||
bkConfig := backupConfig{}
|
||||
|
||||
logrus.Debugf("backup config detected for %s", fullServiceName)
|
||||
|
||||
if paths, ok := service.Deploy.Labels["backupbot.backup.path"]; ok {
|
||||
logrus.Debugf("detected backup paths for %s: %s", fullServiceName, paths)
|
||||
bkConfig.backupPaths = strings.Split(paths, ",")
|
||||
}
|
||||
|
||||
if preHookCmd, ok := service.Deploy.Labels["backupbot.backup.pre-hook"]; ok {
|
||||
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
|
||||
bkConfig.preHookCmd = preHookCmd
|
||||
}
|
||||
|
||||
if postHookCmd, ok := service.Deploy.Labels["backupbot.backup.post-hook"]; ok {
|
||||
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
|
||||
bkConfig.postHookCmd = postHookCmd
|
||||
}
|
||||
|
||||
backupConfigs[service.Name] = bkConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serviceName := c.Args().Get(1)
|
||||
if serviceName != "" {
|
||||
backupConfig, ok := backupConfigs[serviceName]
|
||||
if !ok {
|
||||
logrus.Fatalf("no backup config for %s? does %s exist?", serviceName, serviceName)
|
||||
}
|
||||
|
||||
logrus.Infof("running backup for the %s service", serviceName)
|
||||
|
||||
if err := runBackup(app, serviceName, backupConfig); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
for serviceName, backupConfig := range backupConfigs {
|
||||
logrus.Infof("running backup for the %s service", serviceName)
|
||||
|
||||
if err := runBackup(app, serviceName, backupConfig); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// runBackup does the actual backup logic.
|
||||
func runBackup(app config.App, serviceName string, bkConfig backupConfig) error {
|
||||
if len(bkConfig.backupPaths) == 0 {
|
||||
return fmt.Errorf("backup paths are empty for %s?", serviceName)
|
||||
}
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: avoid instantiating a new CLI
|
||||
dcli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
|
||||
|
||||
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
|
||||
if bkConfig.preHookCmd != "" {
|
||||
splitCmd := internal.SafeSplit(bkConfig.preHookCmd)
|
||||
|
||||
logrus.Debugf("split pre-hook command for %s into %s", fullServiceName, splitCmd)
|
||||
|
||||
preHookExecOpts := types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
Cmd: splitCmd,
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
}
|
||||
|
||||
if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
|
||||
return fmt.Errorf("failed to run %s on %s: %s", bkConfig.preHookCmd, targetContainer.ID, err.Error())
|
||||
}
|
||||
|
||||
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, bkConfig.preHookCmd)
|
||||
}
|
||||
|
||||
var tempBackupPaths []string
|
||||
for _, remoteBackupPath := range bkConfig.backupPaths {
|
||||
timestamp := strconv.Itoa(time.Now().Nanosecond())
|
||||
sanitisedPath := strings.ReplaceAll(remoteBackupPath, "/", "_")
|
||||
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s%s_%s.tar.gz", fullServiceName, sanitisedPath, timestamp))
|
||||
logrus.Debugf("temporarily backing up %s:%s to %s", fullServiceName, remoteBackupPath, localBackupPath)
|
||||
|
||||
logrus.Infof("backing up %s:%s", fullServiceName, remoteBackupPath)
|
||||
|
||||
content, _, err := cl.CopyFromContainer(context.Background(), targetContainer.ID, remoteBackupPath)
|
||||
if err != nil {
|
||||
logrus.Debugf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
|
||||
if err := cleanupTempArchives(tempBackupPaths); err != nil {
|
||||
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
|
||||
}
|
||||
return fmt.Errorf("failed to copy %s from container: %s", remoteBackupPath, err.Error())
|
||||
}
|
||||
defer content.Close()
|
||||
|
||||
_, srcBase := archive.SplitPathDirEntry(remoteBackupPath)
|
||||
preArchive := archive.RebaseArchiveEntries(content, srcBase, remoteBackupPath)
|
||||
if err := copyToFile(localBackupPath, preArchive); err != nil {
|
||||
logrus.Debugf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
|
||||
if err := cleanupTempArchives(tempBackupPaths); err != nil {
|
||||
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
|
||||
}
|
||||
return fmt.Errorf("failed to create tar archive (%s): %s", localBackupPath, err.Error())
|
||||
}
|
||||
|
||||
tempBackupPaths = append(tempBackupPaths, localBackupPath)
|
||||
}
|
||||
|
||||
logrus.Infof("compressing and merging archives...")
|
||||
|
||||
if err := mergeArchives(tempBackupPaths, fullServiceName); err != nil {
|
||||
logrus.Debugf("failed to merge archive files: %s", err.Error())
|
||||
if err := cleanupTempArchives(tempBackupPaths); err != nil {
|
||||
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
|
||||
}
|
||||
return fmt.Errorf("failed to merge archive files: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := cleanupTempArchives(tempBackupPaths); err != nil {
|
||||
return fmt.Errorf("failed to clean up temporary archives: %s", err.Error())
|
||||
}
|
||||
|
||||
if bkConfig.postHookCmd != "" {
|
||||
splitCmd := internal.SafeSplit(bkConfig.postHookCmd)
|
||||
|
||||
logrus.Debugf("split post-hook command for %s into %s", fullServiceName, splitCmd)
|
||||
|
||||
postHookExecOpts := types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
Cmd: splitCmd,
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
}
|
||||
|
||||
if err := container.RunExec(dcli, cl, targetContainer.ID, &postHookExecOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("succesfully ran %s post-hook command: %s", fullServiceName, bkConfig.postHookCmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyToFile(outfile string, r io.Reader) error {
|
||||
tmpFile, err := system.TempFileSequential(filepath.Dir(outfile), ".tar_temp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpPath := tmpFile.Name()
|
||||
|
||||
_, err = io.Copy(tmpFile, r)
|
||||
tmpFile.Close()
|
||||
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Rename(tmpPath, outfile); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanupTempArchives(tarPaths []string) error {
|
||||
for _, tarPath := range tarPaths {
|
||||
if err := os.RemoveAll(tarPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("remove temporary archive file %s", tarPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeArchives(tarPaths []string, serviceName string) error {
|
||||
var out io.Writer
|
||||
var cout *pgzip.Writer
|
||||
|
||||
timestamp := strconv.Itoa(time.Now().Nanosecond())
|
||||
localBackupPath := filepath.Join(config.BACKUP_DIR, fmt.Sprintf("%s_%s.tar.gz", serviceName, timestamp))
|
||||
|
||||
fout, err := os.Create(localBackupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to open %s: %s", localBackupPath, err)
|
||||
}
|
||||
|
||||
defer fout.Close()
|
||||
out = fout
|
||||
|
||||
cout = pgzip.NewWriter(out)
|
||||
out = cout
|
||||
|
||||
tw := tar.NewWriter(out)
|
||||
|
||||
for _, tarPath := range tarPaths {
|
||||
if err := addTar(tw, tarPath); err != nil {
|
||||
return fmt.Errorf("failed to merge %s: %v", tarPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tw.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close tar writer %v", err)
|
||||
}
|
||||
|
||||
if cout != nil {
|
||||
if err := cout.Flush(); err != nil {
|
||||
return fmt.Errorf("failed to flush: %s", err)
|
||||
} else if err = cout.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close compressed writer: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("backed up %s to %s", serviceName, localBackupPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addTar(tw *tar.Writer, pth string) (err error) {
|
||||
var tr *tar.Reader
|
||||
var rc io.ReadCloser
|
||||
var hdr *tar.Header
|
||||
|
||||
if tr, rc, err = openTarFile(pth); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
if hdr, err = tr.Next(); err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
if err = tw.WriteHeader(hdr); err != nil {
|
||||
break
|
||||
} else if _, err = io.Copy(tw, tr); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
err = rc.Close()
|
||||
} else {
|
||||
rc.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func openTarFile(pth string) (tr *tar.Reader, rc io.ReadCloser, err error) {
|
||||
var fin *os.File
|
||||
var n int
|
||||
buff := make([]byte, 1024)
|
||||
|
||||
if fin, err = os.Open(pth); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if n, err = fin.Read(buff); err != nil {
|
||||
fin.Close()
|
||||
return
|
||||
} else if n == 0 {
|
||||
fin.Close()
|
||||
err = fmt.Errorf("%s is empty", pth)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = fin.Seek(0, 0); err != nil {
|
||||
fin.Close()
|
||||
return
|
||||
}
|
||||
|
||||
rc = fin
|
||||
tr = tar.NewReader(rc)
|
||||
|
||||
return tr, rc, nil
|
||||
}
|
@ -32,6 +32,14 @@ var localCmdFlag = &cli.BoolFlag{
|
||||
Destination: &localCmd,
|
||||
}
|
||||
|
||||
var remoteUser string
|
||||
var remoteUserFlag = &cli.StringFlag{
|
||||
Name: "user, u",
|
||||
Value: "",
|
||||
Usage: "User to run command within a service context",
|
||||
Destination: &remoteUser,
|
||||
}
|
||||
|
||||
var appCmdCommand = cli.Command{
|
||||
Name: "command",
|
||||
Aliases: []string{"cmd"},
|
||||
@ -52,18 +60,15 @@ Example:
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
localCmdFlag,
|
||||
remoteUserFlag,
|
||||
},
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Before: internal.SubCommandBefore,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
if len(c.Args()) <= 2 && !localCmd {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>/<command>? did you mean to pass --local?"))
|
||||
}
|
||||
|
||||
if len(c.Args()) > 2 && localCmd {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("cannot specify <service> and --local together"))
|
||||
if localCmd && remoteUser != "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & <user> together"))
|
||||
}
|
||||
|
||||
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
|
||||
@ -74,6 +79,20 @@ Example:
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
var parsedCmdArgs string
|
||||
var cmdArgsIdx int
|
||||
var hasCmdArgs bool
|
||||
for idx, arg := range c.Args() {
|
||||
if arg == "--" {
|
||||
cmdArgsIdx = idx
|
||||
hasCmdArgs = true
|
||||
}
|
||||
|
||||
if hasCmdArgs && idx > cmdArgsIdx {
|
||||
parsedCmdArgs += fmt.Sprintf("%s ", c.Args().Get(idx))
|
||||
}
|
||||
}
|
||||
|
||||
if localCmd {
|
||||
cmdName := c.Args().Get(1)
|
||||
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
|
||||
@ -82,8 +101,17 @@ Example:
|
||||
|
||||
logrus.Debugf("--local detected, running %s on local work station", cmdName)
|
||||
|
||||
sourceAndExec := fmt.Sprintf("TARGET=local; APP_NAME=%s; . %s; %s", app.StackName(), abraSh, cmdName)
|
||||
var sourceAndExec string
|
||||
if hasCmdArgs {
|
||||
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
|
||||
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; . %s; %s %s", app.StackName(), abraSh, cmdName, parsedCmdArgs)
|
||||
} else {
|
||||
logrus.Debug("did not detect any command arguments")
|
||||
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; . %s; %s", app.StackName(), abraSh, cmdName)
|
||||
}
|
||||
|
||||
cmd := exec.Command("/bin/sh", "-c", sourceAndExec)
|
||||
|
||||
if err := internal.RunCmd(cmd); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -113,20 +141,6 @@ Example:
|
||||
|
||||
logrus.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
|
||||
|
||||
var parsedCmdArgs string
|
||||
var cmdArgsIdx int
|
||||
var hasCmdArgs bool
|
||||
for idx, arg := range c.Args() {
|
||||
if arg == "--" {
|
||||
cmdArgsIdx = idx
|
||||
hasCmdArgs = true
|
||||
}
|
||||
|
||||
if hasCmdArgs && idx > cmdArgsIdx {
|
||||
parsedCmdArgs += fmt.Sprintf("%s ", c.Args().Get(idx))
|
||||
}
|
||||
}
|
||||
|
||||
if hasCmdArgs {
|
||||
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
|
||||
} else {
|
||||
@ -200,6 +214,11 @@ func runCmdRemote(app config.App, abraSh, serviceName, cmdName, cmdArgs string)
|
||||
Tty: true,
|
||||
}
|
||||
|
||||
if remoteUser != "" {
|
||||
logrus.Debugf("running command with user %s", remoteUser)
|
||||
execCreateOpts.User = remoteUser
|
||||
}
|
||||
|
||||
// FIXME: avoid instantiating a new CLI
|
||||
dcli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
|
@ -30,7 +30,7 @@ var logOpts = types.ContainerLogsOptions{
|
||||
|
||||
// stackLogs lists logs for all stack services
|
||||
func stackLogs(c *cli.Context, app config.App, client *dockerClient.Client) {
|
||||
filters, err := app.Filters()
|
||||
filters, err := app.Filters(true, false)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -103,7 +103,7 @@ var appLogsCommand = cli.Command{
|
||||
|
||||
func tailServiceLogs(c *cli.Context, cl *dockerClient.Client, app config.App, serviceName string) error {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
|
||||
filters.Add("name", fmt.Sprintf("%s_%s", app.StackName(), serviceName))
|
||||
|
||||
chosenService, err := service.GetService(context.Background(), cl, filters, internal.NoInput)
|
||||
if err != nil {
|
||||
|
@ -66,7 +66,7 @@ var appPsCommand = cli.Command{
|
||||
|
||||
// showPSOutput renders ps output.
|
||||
func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
|
||||
filters, err := app.Filters()
|
||||
filters, err := app.Filters(true, true)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ var appRemoveCommand = cli.Command{
|
||||
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
|
||||
}
|
||||
|
||||
fs, err := app.Filters()
|
||||
fs, err := app.Filters(false, false)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -114,6 +114,11 @@ var appRemoveCommand = cli.Command{
|
||||
logrus.Info("no secrets to remove")
|
||||
}
|
||||
|
||||
fs, err = app.Filters(false, true)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
volumeListOKBody, err := cl.VolumeList(context.Background(), fs)
|
||||
volumeList := volumeListOKBody.Volumes
|
||||
if err != nil {
|
||||
|
201
cli/app/restore.go
Normal file
201
cli/app/restore.go
Normal file
@ -0,0 +1,201 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
containerPkg "coopcloud.tech/abra/pkg/container"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/upstream/container"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type restoreConfig struct {
|
||||
preHookCmd string
|
||||
postHookCmd string
|
||||
}
|
||||
|
||||
var appRestoreCommand = cli.Command{
|
||||
Name: "restore",
|
||||
Aliases: []string{"rs"},
|
||||
Usage: "Run app restore",
|
||||
ArgsUsage: "<domain> <service> <file>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
BashComplete: autocomplete.AppNameComplete,
|
||||
Description: `
|
||||
This command runs an app restore.
|
||||
|
||||
Pre/post hook commands are defined in the recipe configuration. Abra reads this
|
||||
configuration and run the comands in the context of the service before
|
||||
restoring the backup.
|
||||
|
||||
Unlike "abra app backup", restore must be run on a per-service basis. You can
|
||||
not restore all services in one go. Backup files produced by Abra are
|
||||
compressed archives which use absolute paths. This allows Abra to restore
|
||||
according to standard tar command logic.
|
||||
|
||||
Example:
|
||||
|
||||
abra app restore example.com app ~/.abra/backups/example_com_app_609341138.tar.gz
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
serviceName := c.Args().Get(1)
|
||||
if serviceName == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>?"))
|
||||
}
|
||||
|
||||
backupPath := c.Args().Get(2)
|
||||
if backupPath == "" {
|
||||
internal.ShowSubcommandHelpAndError(c, errors.New("missing <file>?"))
|
||||
}
|
||||
|
||||
if _, err := os.Stat(backupPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logrus.Fatalf("%s doesn't exist?", backupPath)
|
||||
}
|
||||
}
|
||||
|
||||
recipe, err := recipe.Get(app.Recipe)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
restoreConfigs := make(map[string]restoreConfig)
|
||||
for _, service := range recipe.Config.Services {
|
||||
if restoreEnabled, ok := service.Deploy.Labels["backupbot.restore"]; ok {
|
||||
if restoreEnabled == "true" {
|
||||
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), service.Name)
|
||||
rsConfig := restoreConfig{}
|
||||
|
||||
logrus.Debugf("restore config detected for %s", fullServiceName)
|
||||
|
||||
if preHookCmd, ok := service.Deploy.Labels["backupbot.restore.pre-hook"]; ok {
|
||||
logrus.Debugf("detected pre-hook command for %s: %s", fullServiceName, preHookCmd)
|
||||
rsConfig.preHookCmd = preHookCmd
|
||||
}
|
||||
|
||||
if postHookCmd, ok := service.Deploy.Labels["backupbot.restore.post-hook"]; ok {
|
||||
logrus.Debugf("detected post-hook command for %s: %s", fullServiceName, postHookCmd)
|
||||
rsConfig.postHookCmd = postHookCmd
|
||||
}
|
||||
|
||||
restoreConfigs[service.Name] = rsConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rsConfig, ok := restoreConfigs[serviceName]
|
||||
if !ok {
|
||||
rsConfig = restoreConfig{}
|
||||
}
|
||||
if err := runRestore(app, backupPath, serviceName, rsConfig); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// runRestore does the actual restore logic.
|
||||
func runRestore(app config.App, backupPath, serviceName string, rsConfig restoreConfig) error {
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// FIXME: avoid instantiating a new CLI
|
||||
dcli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
|
||||
|
||||
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fullServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
|
||||
if rsConfig.preHookCmd != "" {
|
||||
splitCmd := internal.SafeSplit(rsConfig.preHookCmd)
|
||||
|
||||
logrus.Debugf("split pre-hook command for %s into %s", fullServiceName, splitCmd)
|
||||
|
||||
preHookExecOpts := types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
Cmd: splitCmd,
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
}
|
||||
|
||||
if err := container.RunExec(dcli, cl, targetContainer.ID, &preHookExecOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("succesfully ran %s pre-hook command: %s", fullServiceName, rsConfig.preHookCmd)
|
||||
}
|
||||
|
||||
backupReader, err := os.Open(backupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := archive.DecompressStream(backupReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we use absolute paths so tar knows what to do. it will restore files
|
||||
// according to the paths set in the compresed archive
|
||||
restorePath := "/"
|
||||
|
||||
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, restorePath, content, copyOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("restored %s to %s", backupPath, fullServiceName)
|
||||
|
||||
if rsConfig.postHookCmd != "" {
|
||||
splitCmd := internal.SafeSplit(rsConfig.postHookCmd)
|
||||
|
||||
logrus.Debugf("split post-hook command for %s into %s", fullServiceName, splitCmd)
|
||||
|
||||
postHookExecOpts := types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
Cmd: splitCmd,
|
||||
Detach: false,
|
||||
Tty: true,
|
||||
}
|
||||
|
||||
if err := container.RunExec(dcli, cl, targetContainer.ID, &postHookExecOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("succesfully ran %s post-hook command: %s", fullServiceName, rsConfig.postHookCmd)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -216,7 +216,7 @@ Example:
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
filters, err := app.Filters()
|
||||
filters, err := app.Filters(false, false)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -293,7 +293,7 @@ var appSecretLsCommand = cli.Command{
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
filters, err := app.Filters()
|
||||
filters, err := app.Filters(false, false)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ var appVolumeListCommand = cli.Command{
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
filters, err := app.Filters()
|
||||
filters, err := app.Filters(false, true)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
@ -80,7 +80,7 @@ Passing "--force/-f" will select all volumes for removal. Be careful.
|
||||
Action: func(c *cli.Context) error {
|
||||
app := internal.ValidateApp(c)
|
||||
|
||||
filters, err := app.Filters()
|
||||
filters, err := app.Filters(false, true)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
@ -172,6 +172,7 @@ func newAbraApp(version, commit string) *cli.App {
|
||||
path.Join(config.SERVERS_DIR),
|
||||
path.Join(config.RECIPES_DIR),
|
||||
path.Join(config.VENDOR_DIR),
|
||||
path.Join(config.BACKUP_DIR),
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
|
35
cli/internal/backup.go
Normal file
35
cli/internal/backup.go
Normal file
@ -0,0 +1,35 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SafeSplit splits up a string into a list of commands safely.
|
||||
func SafeSplit(s string) []string {
|
||||
split := strings.Split(s, " ")
|
||||
|
||||
var result []string
|
||||
var inquote string
|
||||
var block string
|
||||
for _, i := range split {
|
||||
if inquote == "" {
|
||||
if strings.HasPrefix(i, "'") || strings.HasPrefix(i, "\"") {
|
||||
inquote = string(i[0])
|
||||
block = strings.TrimPrefix(i, inquote) + " "
|
||||
} else {
|
||||
result = append(result, i)
|
||||
}
|
||||
} else {
|
||||
if !strings.HasSuffix(i, inquote) {
|
||||
block += i + " "
|
||||
} else {
|
||||
block += strings.TrimSuffix(i, inquote)
|
||||
inquote = ""
|
||||
result = append(result, block)
|
||||
block = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
@ -393,15 +393,15 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
|
||||
}
|
||||
|
||||
if err := commitRelease(recipe, tagString); err != nil {
|
||||
logrus.Fatal(err)
|
||||
logrus.Fatalf("failed to commit changes: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := tagRelease(tagString, repo); err != nil {
|
||||
logrus.Fatal(err)
|
||||
logrus.Fatalf("failed to tag release: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := pushRelease(recipe, tagString); err != nil {
|
||||
logrus.Fatal(err)
|
||||
logrus.Fatalf("failed to publish new release: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -61,6 +61,10 @@ You may invoke this command in "wizard" mode and be prompted for input:
|
||||
Action: func(c *cli.Context) error {
|
||||
recipe := internal.ValidateRecipeWithPrompt(c, true)
|
||||
|
||||
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
|
5
go.mod
5
go.mod
@ -36,8 +36,9 @@ require (
|
||||
github.com/gliderlabs/ssh v0.3.3
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0
|
||||
github.com/kevinburke/ssh_config v1.1.0
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1
|
||||
github.com/kevinburke/ssh_config v1.2.0
|
||||
github.com/klauspost/pgzip v1.2.5
|
||||
github.com/libdns/gandi v1.0.2
|
||||
github.com/libdns/libdns v0.2.1
|
||||
github.com/moby/sys/mount v0.2.0 // indirect
|
||||
|
10
go.sum
10
go.sum
@ -583,8 +583,8 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
@ -648,15 +648,17 @@ github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o=
|
||||
github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.14.2 h1:S0OHlFk/Gbon/yauFJ4FfJJF5V0fc5HbBTJazi28pRw=
|
||||
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
@ -64,8 +64,13 @@ func (a App) StackName() string {
|
||||
return stackName
|
||||
}
|
||||
|
||||
// Filters retrieves exact app filters for querying the container runtime.
|
||||
func (a App) Filters() (filters.Args, error) {
|
||||
// Filters retrieves exact app filters for querying the container runtime. Due
|
||||
// to upstream issues, filtering works different depending on what you're
|
||||
// querying. So, for example, secrets don't work with regex! The caller needs
|
||||
// to implement their own validation that the right secrets are matched. In
|
||||
// order to handle these cases, we provide the `appendServiceNames` /
|
||||
// `exactMatch` modifiers.
|
||||
func (a App) Filters(appendServiceNames, exactMatch bool) (filters.Args, error) {
|
||||
filters := filters.NewArgs()
|
||||
|
||||
composeFiles, err := GetAppComposeFiles(a.Recipe, a.Env)
|
||||
@ -80,7 +85,22 @@ func (a App) Filters() (filters.Args, error) {
|
||||
}
|
||||
|
||||
for _, service := range compose.Services {
|
||||
filter := fmt.Sprintf("^%s_%s", a.StackName(), service.Name)
|
||||
var filter string
|
||||
|
||||
if appendServiceNames {
|
||||
if exactMatch {
|
||||
filter = fmt.Sprintf("^%s_%s", a.StackName(), service.Name)
|
||||
} else {
|
||||
filter = fmt.Sprintf("%s_%s", a.StackName(), service.Name)
|
||||
}
|
||||
} else {
|
||||
if exactMatch {
|
||||
filter = fmt.Sprintf("^%s", a.StackName())
|
||||
} else {
|
||||
filter = fmt.Sprintf("%s", a.StackName())
|
||||
}
|
||||
}
|
||||
|
||||
filters.Add("name", filter)
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
|
||||
var SERVERS_DIR = path.Join(ABRA_DIR, "servers")
|
||||
var RECIPES_DIR = path.Join(ABRA_DIR, "recipes")
|
||||
var VENDOR_DIR = path.Join(ABRA_DIR, "vendor")
|
||||
var BACKUP_DIR = path.Join(ABRA_DIR, "backups")
|
||||
var RECIPES_JSON = path.Join(ABRA_DIR, "catalogue", "recipes.json")
|
||||
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
|
||||
var CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
|
||||
|
@ -572,7 +572,7 @@ func EnsureUpToDate(recipeName string) error {
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
return fmt.Errorf("%s has locally unstaged changes", recipeName)
|
||||
return fmt.Errorf("%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding", recipeName, recipeDir)
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(recipeDir)
|
||||
|
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ABRA_VERSION="0.3.0-alpha"
|
||||
ABRA_VERSION="0.4.1-alpha"
|
||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
|
||||
RC_VERSION="0.4.0-alpha-rc7"
|
||||
RC_VERSION="0.4.1-alpha"
|
||||
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
|
||||
|
||||
for arg in "$@"; do
|
||||
@ -44,8 +44,17 @@ function install_abra_release {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m)
|
||||
ARCH=$(uname -m)
|
||||
if [[ $ARCH =~ "aarch64" ]]; then
|
||||
ARCH="arm64"
|
||||
elif [[ $ARCH =~ "armv5l" ]]; then
|
||||
ARCH="armv5"
|
||||
elif [[ $ARCH =~ "armv6l" ]]; then
|
||||
ARCH="armv6"
|
||||
elif [[ $ARCH =~ "armv7l" ]]; then
|
||||
ARCH="armv7"
|
||||
fi
|
||||
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$ARCH
|
||||
FILENAME="abra_"$ABRA_VERSION"_"$PLATFORM""
|
||||
sed_command_rel='s/.*"assets":\[\{[^]]*"name":"'$FILENAME'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
|
||||
sed_command_checksums='s/.*"assets":\[\{[^\]*"name":"checksums.txt"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p'
|
||||
|
5
scripts/release/upx.sh
Executable file
5
scripts/release/upx.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
upx ./dist/abra_*/abra
|
Reference in New Issue
Block a user