Compare commits

..

No commits in common. "main" and "0.4.0-alpha-rc5" have entirely different histories.

106 changed files with 1866 additions and 3060 deletions

View File

@ -3,17 +3,27 @@ kind: pipeline
name: coopcloud.tech/abra
steps:
- name: make check
image: golang:1.19
image: golang:1.17
commands:
- make check
- name: make static
image: golang:1.17
ignore: true # until we decide we all want this check
environment:
STATIC_CHECK_URL: honnef.co/go/tools/cmd/staticcheck
STATIC_CHECK_VERSION: v0.2.0
commands:
- go install $STATIC_CHECK_URL@$STATIC_CHECK_VERSION
- make static
- name: make build
image: golang:1.19
image: golang:1.17
commands:
- make build
- name: make test
image: golang:1.19
image: golang:1.17
commands:
- make test
@ -45,7 +55,7 @@ steps:
event: tag
- name: release
image: golang:1.19
image: golang:1.17
environment:
GITEA_TOKEN:
from_secret: goreleaser_gitea_token

1
.gitignore vendored
View File

@ -4,5 +4,4 @@
.vscode/
abra
dist/
tests/integration/.abra/catalogue
vendor/

View File

@ -7,6 +7,7 @@ gitea_urls:
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
@ -14,15 +15,6 @@ builds:
goos:
- linux
- darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"
@ -39,10 +31,8 @@ changelog:
sort: desc
filters:
exclude:
- "^Merge"
- "^Revert"
- "^WIP:"
- "^chore(deps):"
- "^style:"
- "^test:"
- "^tests:"
- "^Revert"

View File

@ -1,11 +0,0 @@
# authors
> If you're looking at this and you hack on `abra` and you're not listed here,
> please do add yourself! This is a community project, let's show some :heart:
- 3wordchant
- decentral1se
- frando
- kawaiipunk
- knoflook
- roxxers

15
LICENSE
View File

@ -1,15 +0,0 @@
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/>.

View File

@ -5,7 +5,7 @@ LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
export GOPRIVATE=coopcloud.tech
all: format check build test
all: format check static build test
run:
@go run -ldflags=$(LDFLAGS) $(ABRA)
@ -28,6 +28,9 @@ format:
check:
@test -z $$(gofmt -l .) || (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
static:
@staticcheck $(ABRA)
test:
@go test ./... -cover -v
@ -40,3 +43,15 @@ loc-author:
sort -f | \
uniq -ic | \
sort -n
int-core:
@docker run \
-v $$(pwd):/src \
--env-file .e2e.env \
debian:bullseye-slim \
sh -c "\
apt update && apt install -y wget curl git; echo ""; echo ""; \
git config --global user.email 'e2e@coopcloud.tech'; \
git config --global user.name 'e2e'; \
cd /src/tests/integration && bash core.sh -- --dev \
"

View File

@ -1,12 +1,73 @@
# `abra`
# abra
> https://coopcloud.tech
[![Build Status](https://build.coopcloud.tech/api/badges/coop-cloud/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/coop-cloud/abra)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/coop-cloud/abra)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
The Co-op Cloud utility belt 🎩🐇
<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>
`abra` is a command-line tool for managing your own [Co-op Cloud](https://coopcloud.tech). It can provision new servers, create apps, deploy them, run backup and restore operations and a whole lot of other things. Please see [docs.coopcloud.tech](https://docs.coopcloud.tech) for more extensive documentation.
`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:
## Quick install
Please see [docs.coopcloud.tech/abra](https://docs.coopcloud.tech/abra) for help on install, upgrade, hacking, troubleshooting & more!
```bash
curl https://install.abra.autonomic.zone | bash
```
Or using the latest release candidate (extra experimental!):
```bash
curl https://install.abra.autonomic.zone | bash -s -- --rc
```
Source for this script is in [scripts/installer/installer](./scripts/installer/installer).
## Hacking
### Getting started
Install [direnv](https://direnv.net), run `cp .envrc.sample .envrc`, then run `direnv allow` in this directory. This will set coopcloud repos as private due to [this bug.](https://git.coopcloud.tech/coop-cloud/coopcloud.tech/issues/20#issuecomment-8201). Or you can run `go env -w GOPRIVATE=coopcloud.tech` but I'm not sure how persistent this is.
Install [Go >= 1.16](https://golang.org/doc/install) and then:
- `make build` to build
- `./abra` to run commands
- `make test` will run tests
- `make install` will install it to `$GOPATH/bin`
- `go get <package>` and `go mod tidy` to add a new dependency
Our [Drone CI configuration](.drone.yml) runs a number of sanity on each pushed commit. See the [Makefile](./Makefile) for more handy targets.
Please use the [conventional commit format](https://www.conventionalcommits.org/en/v1.0.0/) for your commits so we can automate our change log.
### Versioning
We use [goreleaser](https://goreleaser.com) to help us automate releases. We use [semver](https://semver.org) for versioning all releases of the tool. While we are still in the public alpha release phase, we will maintain a `0.y.z-alpha` format. Change logs are generated from our commit logs. We are still working this out and aim to refine our release praxis as we go.
For developers, while using this `-alpha` format, the `y` part is the "major" version part. So, if you make breaking changes, you increment that and _not_ the `x` part. So, if you're on `0.1.0-alpha`, then you'd go to `0.1.1-alpha` for a backwards compatible change and `0.2.0-alpha` for a backwards incompatible change.
### Making a new release
- Change `ABRA_VERSION` to match the new tag in [`scripts`](./scripts/installer/installer) (use [semver](https://semver.org))
- Commit that change (e.g. `git commit -m 'chore: publish next tag x.y.z-alpha'`)
- Make a new tag (e.g. `git tag -a x.y.z-alpha`)
- Push the new tag (e.g. `git push && git push --tags`)
- Wait until the build finishes on [build.coopcloud.tech](https://build.coopcloud.tech/coop-cloud/abra)
- Deploy the new installer script (e.g. `cd ./scripts/installer && make`)
- Check the release worked, (e.g. `abra upgrade; abra -v`)
### Fork maintenance
#### `godotenv`
We maintain a fork of [godotenv](https://github.com/Autonomic-Cooperative/godotenv) for two features:
1. multi-line env var support
2. inline comment parsing
You can upgrade the version here by running `go get github.com/Autonomic-Cooperative/godotenv@<commit>` where `<commit>` is the latest commit you want to pin to. At time of writing, `go get github.com/Autonomic-Cooperative/godotenv@b031ea1211e7fd297af4c7747ffb562ebe00cd33` is the command you want to run to maintain the above functionality.
#### `docker/client`
A number of modules in [pkg/upstream](./pkg/upstream) are copy/pasta'd from the upstream [docker/docker/client](https://pkg.go.dev/github.com/docker/docker/client). We had to do this because upstream are not exposing their API as public.

View File

@ -1,22 +1,29 @@
package app
import (
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var AppCommand = cli.Command{
// AppCommand defines the `abra app` command and ets subcommands
var AppCommand = &cli.Command{
Name: "app",
Aliases: []string{"a"},
Usage: "Manage apps",
ArgsUsage: "<domain>",
Description: "Functionality for managing the life cycle of your apps",
Subcommands: []cli.Command{
Aliases: []string{"a"},
ArgsUsage: "<app>",
Description: `
This command provides all the functionality you need to manage the life cycle
of your apps. From initial deployment, day-2 operations (e.g. backup/restore)
to scaling apps up and spinning them down.
`,
Subcommands: []*cli.Command{
appNewCommand,
appConfigCommand,
appRestartCommand,
appDeployCommand,
appUpgradeCommand,
appUndeployCommand,
appBackupCommand,
appRestoreCommand,
appRemoveCommand,
appCheckCommand,
appListCommand,
@ -29,8 +36,5 @@ var AppCommand = cli.Command{
appVolumeCommand,
appVersionCommand,
appErrorsCommand,
appCmdCommand,
appBackupCommand,
appRestoreCommand,
},
}

View File

@ -1,389 +1,77 @@
package app
import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"os/exec"
"path"
"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"
"github.com/urfave/cli/v2"
)
type backupConfig struct {
preHookCmd string
postHookCmd string
backupPaths []string
var backupAllServices bool
var backupAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &backupAllServices,
Aliases: []string{"a"},
Usage: "Backup all services",
}
var appBackupCommand = cli.Command{
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: `
Run 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.
`,
Usage: "Backup an app",
Aliases: []string{"b"},
Flags: []cli.Flag{backupAllServicesFlag},
ArgsUsage: "<service>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
recipe, err := recipe.Get(app.Recipe)
if c.Args().Get(1) != "" && backupAllServices {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<service>' and '--all' together"))
}
abraSh := path.Join(config.RECIPES_DIR, app.Type, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s does not exist?", abraSh)
}
logrus.Fatal(err)
}
sourceCmd := fmt.Sprintf("source %s", abraSh)
execCmd := "abra_backup"
if !backupAllServices {
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no service(s) target provided"))
}
execCmd = fmt.Sprintf("abra_backup_%s", serviceName)
}
bytes, err := ioutil.ReadFile(abraSh)
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 !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a %s function", app.Type, execCmd)
}
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 {
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); 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
BashComplete: autocomplete.AppNameComplete,
}

View File

@ -9,22 +9,18 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appCheckCommand = cli.Command{
var appCheckCommand = &cli.Command{
Name: "check",
Aliases: []string{"chk"},
Usage: "Check if app is configured correctly",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Aliases: []string{"c"},
ArgsUsage: "<service>",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample")
envSamplePath := path.Join(config.RECIPES_DIR, app.Type, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s does not exist?", envSamplePath)

View File

@ -1,244 +0,0 @@
package app
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"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/formatter"
"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"
)
var appCmdCommand = cli.Command{
Name: "command",
Aliases: []string{"cmd"},
Usage: "Run app commands",
Description: `
Run an app specific command.
These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local". Arguments can be passed into these functions
using the "-- <args>" syntax.
Example:
abra app cmd example.com app create_user -- me@example.com
`,
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.LocalCmdFlag,
internal.RemoteUserFlag,
},
BashComplete: autocomplete.AppNameComplete,
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if internal.LocalCmd && internal.RemoteUser != "" {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
}
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
}
logrus.Fatal(err)
}
if internal.LocalCmd {
cmdName := c.Args().Get(1)
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("--local detected, running %s on local work station", cmdName)
var exportEnv string
for k, v := range app.Env {
exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v)
}
var sourceAndExec string
if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName, parsedCmdArgs)
} else {
logrus.Debug("did not detect any command arguments")
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName)
}
shell := "/bin/bash"
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
logrus.Debugf("%s does not exist locally, use /bin/sh as fallback", shell)
shell = "/bin/sh"
}
cmd := exec.Command(shell, "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
} else {
targetServiceName := c.Args().Get(1)
cmdName := c.Args().Get(2)
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil {
logrus.Fatal(err)
}
matchingServiceName := false
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
logrus.Fatalf("no service %s for %s?", targetServiceName, app.Name)
}
logrus.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
} else {
logrus.Debug("did not detect any command arguments")
}
if err := runCmdRemote(app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
func parseCmdArgs(args []string, isLocal bool) (bool, string) {
var (
parsedCmdArgs string
hasCmdArgs bool
)
if isLocal {
if len(args) > 2 {
return true, fmt.Sprintf("%s ", strings.Join(args[2:], " "))
}
} else {
if len(args) > 3 {
return true, fmt.Sprintf("%s ", strings.Join(args[3:], " "))
}
}
return hasCmdArgs, parsedCmdArgs
}
func ensureCommand(abraSh, recipeName, execCmd string) error {
bytes, err := ioutil.ReadFile(abraSh)
if err != nil {
return err
}
if !strings.Contains(string(bytes), execCmd) {
return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd)
}
return nil
}
func runCmdRemote(app config.App, abraSh, serviceName, cmdName, cmdArgs string) error {
cl, err := client.New(app.Server)
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
}
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(abraSh, toTarOpts)
if err != nil {
return err
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
return err
}
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
return err
}
shell := "/bin/bash"
findShell := []string{"test", "-e", shell}
execCreateOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,
AttachStdout: true,
Cmd: findShell,
Detach: false,
Tty: false,
}
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
shell = "/bin/sh"
}
var cmd []string
if cmdArgs != "" {
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s %s", serviceName, app.Name, app.StackName(), cmdName, cmdArgs)}
} else {
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
}
logrus.Debugf("running command: %s", strings.Join(cmd, " "))
if internal.RemoteUser != "" {
logrus.Debugf("running command with user %s", internal.RemoteUser)
execCreateOpts.User = internal.RemoteUser
}
execCreateOpts.Cmd = cmd
execCreateOpts.Tty = true
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
return err
}
return nil
}

View File

@ -1,31 +0,0 @@
package app
import (
"strings"
"testing"
)
func TestParseCmdArgs(t *testing.T) {
tests := []struct {
input []string
shouldParse bool
expectedOutput string
}{
// `--` is not parsed when passed in from the command-line e.g. -- foo bar baz
// so we need to eumlate that as missing when testing if bash args are passed in
// see https://git.coopcloud.tech/coop-cloud/organising/issues/336 for more
{[]string{"foo.com", "app", "test"}, false, ""},
{[]string{"foo.com", "app", "test", "foo"}, true, "foo "},
{[]string{"foo.com", "app", "test", "foo", "bar", "baz"}, true, "foo bar baz "},
}
for _, test := range tests {
ok, parsed := parseCmdArgs(test.input, false)
if ok != test.shouldParse {
t.Fatalf("[%s] should not parse", strings.Join(test.input, " "))
}
if parsed != test.expectedOutput {
t.Fatalf("%s does not match %s", parsed, test.expectedOutput)
}
}
}

View File

@ -10,18 +10,13 @@ import (
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appConfigCommand = cli.Command{
var appConfigCommand = &cli.Command{
Name: "config",
Aliases: []string{"cfg"},
Aliases: []string{"c"},
Usage: "Edit app config",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
appName := c.Args().First()

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"fmt"
"os"
"strings"
@ -16,29 +15,25 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appCpCommand = cli.Command{
var appCpCommand = &cli.Command{
Name: "cp",
Aliases: []string{"c"},
ArgsUsage: "<domain> <src> <dst>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<src> <dst>",
Usage: "Copy files to/from a running app service",
Description: `
Copy files to and from any app service file system.
This command supports copying files to and from any app service file system.
If you want to copy a myfile.txt to the root of the app service:
abra app cp <domain> myfile.txt app:/
abra app cp <app> myfile.txt app:/
And if you want to copy that file back to your current working directory locally:
abra app cp <domain> app:/myfile.txt .
abra app cp <app> app:/myfile.txt .
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
@ -105,15 +100,25 @@ func configureAndCp(
dstPath string,
service string,
isToContainer bool) error {
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, app.Name)
if err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service))
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
container, err := container.GetContainer(context.Background(), cl, filters, internal.NoInput)
container, err := container.GetContainer(c.Context, cl, filters, true)
if err != nil {
logrus.Fatal(err)
}
@ -132,11 +137,11 @@ func configureAndCp(
}
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), container.ID, dstPath, content, copyOpts); err != nil {
if err := cl.CopyToContainer(c.Context, container.ID, dstPath, content, copyOpts); err != nil {
logrus.Fatal(err)
}
} else {
content, _, err := cl.CopyFromContainer(context.Background(), container.ID, srcPath)
content, _, err := cl.CopyFromContainer(c.Context, container.ID, srcPath)
if err != nil {
logrus.Fatal(err)
}
@ -146,6 +151,5 @@ func configureAndCp(
logrus.Fatal(err)
}
}
return nil
}

View File

@ -3,26 +3,23 @@ package app
import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appDeployCommand = cli.Command{
var appDeployCommand = &cli.Command{
Name: "deploy",
Aliases: []string{"d"},
Usage: "Deploy an app",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
},
Before: internal.SubCommandBefore,
Description: `
Deploy an app. It does not support incrementing the version of a deployed app,
for this you need to look at the "abra app upgrade <domain>" command.
This command deploys an app. It does not support incrementing the version of a
deployed app, for this you need to look at the "abra app upgrade <app>"
command.
You may pass "--force" to re-deploy the same version again. This can be useful
if the container runtime has gotten into a weird state.

View File

@ -1,8 +1,6 @@
package app
import (
"context"
"fmt"
"strconv"
"strings"
"time"
@ -17,15 +15,14 @@ import (
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appErrorsCommand = cli.Command{
var appErrorsCommand = &cli.Command{
Name: "errors",
Usage: "List errors for a deployed app",
ArgsUsage: "<domain>",
Description: `
List errors for a deployed app.
This command lists errors for a deployed app.
This is a best-effort implementation and an attempt to gather a number of tips
& tricks for finding errors together into one convenient command. When an app
@ -42,16 +39,13 @@ Got any more ideas? Please let us know:
https://git.coopcloud.tech/coop-cloud/organising/issues/new/choose
This command is best accompanied by "abra app logs <domain>" which may reveal
This command is best accompanied by "abra app logs <app>" which may reveal
further information which can help you debug the cause of an app failure via
the logs.
`,
Aliases: []string{"e"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.WatchFlag,
},
Before: internal.SubCommandBefore,
Flags: []cli.Flag{internal.WatchFlag},
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
@ -61,7 +55,7 @@ the logs.
logrus.Fatal(err)
}
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
@ -89,16 +83,15 @@ the logs.
}
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
recipe, err := recipe.Get(app.Recipe)
recipe, err := recipe.Get(app.Type)
if err != nil {
return err
}
for _, service := range recipe.Config.Services {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
filters.Add("name", service.Name)
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil {
return err
}
@ -109,7 +102,7 @@ func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error
}
container := containers[0]
containerState, err := cl.ContainerInspect(context.Background(), container.ID)
containerState, err := cl.ContainerInspect(c.Context, container.ID)
if err != nil {
logrus.Fatal(err)
}

View File

@ -12,27 +12,31 @@ import (
"coopcloud.tech/abra/pkg/ssh"
"coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var status bool
var statusFlag = &cli.BoolFlag{
Name: "status, S",
Name: "status",
Aliases: []string{"S"},
Value: false,
Usage: "Show app deployment status",
Destination: &status,
}
var appRecipe string
var recipeFlag = &cli.StringFlag{
Name: "recipe, r",
var appType string
var typeFlag = &cli.StringFlag{
Name: "type",
Aliases: []string{"t"},
Value: "",
Usage: "Show apps of a specific recipe",
Destination: &appRecipe,
Usage: "Show apps of a specific type",
Destination: &appType,
}
var listAppServer string
var listAppServerFlag = &cli.StringFlag{
Name: "server, s",
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
@ -57,25 +61,23 @@ type serverStatus struct {
upgradeCount int
}
var appListCommand = cli.Command{
var appListCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List all managed apps",
Description: `
Read the local file system listing of apps and servers (e.g. ~/.abra/) to
generate a report of all your apps.
This command looks at your local file system listing of apps and servers (e.g.
in ~/.abra/) to generate a report of all your apps.
By passing the "--status/-S" flag, you can query all your servers for the
actual live deployment status. Depending on how many servers you manage, this
can take some time.
`,
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
statusFlag,
listAppServerFlag,
recipeFlag,
typeFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
appFiles, err := config.LoadAppFiles(listAppServer)
if err != nil {
@ -87,7 +89,7 @@ can take some time.
logrus.Fatal(err)
}
sort.Sort(config.ByServerAndRecipe(apps))
sort.Sort(config.ByServerAndType(apps))
statuses := make(map[string]map[string]string)
var catl recipe.RecipeCatalogue
@ -122,14 +124,14 @@ can take some time.
var ok bool
if stats, ok = allStats[app.Server]; !ok {
stats = serverStatus{}
if appRecipe == "" {
if appType == "" {
// count server, no filtering
totalServersCount++
}
}
if app.Recipe == appRecipe || appRecipe == "" {
if appRecipe != "" {
if app.Type == appType || appType == "" {
if appType != "" {
// only count server if matches filter
totalServersCount++
}
@ -143,10 +145,8 @@ can take some time.
version := "unknown"
if statusMeta, ok := statuses[app.StackName()]; ok {
if currentVersion, exists := statusMeta["version"]; exists {
if currentVersion != "" {
version = currentVersion
}
}
if statusMeta["status"] != "" {
status = statusMeta["status"]
}
@ -160,7 +160,7 @@ can take some time.
var newUpdates []string
if version != "unknown" {
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
updates, err := recipe.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil {
logrus.Fatal(err)
}
@ -197,7 +197,7 @@ can take some time.
}
appStats.server = app.Server
appStats.recipe = app.Recipe
appStats.recipe = app.Type
appStats.appName = app.Name
appStats.domain = app.Domain
@ -215,7 +215,7 @@ can take some time.
serverStat := allStats[app.Server]
tableCol := []string{"recipe", "domain"}
tableCol := []string{"recipe", "domain", "app name"}
if status {
tableCol = append(tableCol, []string{"status", "version", "upgrade"}...)
}
@ -223,7 +223,7 @@ can take some time.
table := formatter.CreateTable(tableCol)
for _, appStat := range serverStat.apps {
tableRow := []string{appStat.recipe, appStat.domain}
tableRow := []string{appStat.recipe, appStat.domain, appStat.appName}
if status {
tableRow = append(tableRow, []string{appStat.status, appStat.version, appStat.upgrade}...)
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"fmt"
"io"
"os"
@ -16,7 +15,7 @@ import (
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var logOpts = types.ContainerLogsOptions{
@ -29,14 +28,11 @@ 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(true, false)
if err != nil {
logrus.Fatal(err)
}
func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
filters := filters.NewArgs()
filters.Add("name", stackName)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := client.ServiceList(context.Background(), serviceOpts)
services, err := client.ServiceList(c.Context, serviceOpts)
if err != nil {
logrus.Fatal(err)
}
@ -49,7 +45,7 @@ func stackLogs(c *cli.Context, app config.App, client *dockerClient.Client) {
logOpts.ShowStdout = false
}
logs, err := client.ServiceLogs(context.Background(), s, logOpts)
logs, err := client.ServiceLogs(c.Context, s, logOpts)
if err != nil {
logrus.Fatal(err)
}
@ -67,16 +63,14 @@ func stackLogs(c *cli.Context, app config.App, client *dockerClient.Client) {
os.Exit(0)
}
var appLogsCommand = cli.Command{
var appLogsCommand = &cli.Command{
Name: "logs",
Aliases: []string{"l"},
ArgsUsage: "<domain> [<service>]",
ArgsUsage: "[<service>]",
Usage: "Tail app logs",
Flags: []cli.Flag{
internal.StdErrOnlyFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
@ -88,8 +82,8 @@ var appLogsCommand = cli.Command{
serviceName := c.Args().Get(1)
if serviceName == "" {
logrus.Debugf("tailing logs for all %s services", app.Recipe)
stackLogs(c, app, cl)
logrus.Debugf("tailing logs for all %s services", app.Type)
stackLogs(c, app.StackName(), cl)
} else {
logrus.Debugf("tailing logs for %s", serviceName)
if err := tailServiceLogs(c, cl, app, serviceName); err != nil {
@ -104,8 +98,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))
chosenService, err := service.GetService(context.Background(), cl, filters, internal.NoInput)
chosenService, err := service.GetService(c.Context, cl, filters, internal.NoInput)
if err != nil {
logrus.Fatal(err)
}
@ -114,7 +107,7 @@ func tailServiceLogs(c *cli.Context, cl *dockerClient.Client, app config.App, se
logOpts.ShowStdout = false
}
logs, err := cl.ServiceLogs(context.Background(), chosenService.ID, logOpts)
logs, err := cl.ServiceLogs(c.Context, chosenService.ID, logOpts)
if err != nil {
logrus.Fatal(err)
}

View File

@ -3,15 +3,15 @@ package app
import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appNewDescription = `
Take a recipe and uses it to create a new app. This new app configuration is
stored in your ~/.abra directory under the appropriate server.
This command takes a recipe and uses it to create a new app. This new app
configuration is stored in your ~/.abra directory under the appropriate server.
This command does not deploy your app for you. You will need to run "abra app
deploy <domain>" to do so.
deploy <app>" to do so.
You can see what recipes are available (i.e. values for the <recipe> argument)
by running "abra recipe ls".
@ -26,21 +26,19 @@ pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.
`
var appNewCommand = cli.Command{
var appNewCommand = &cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new app",
Aliases: []string{"n"},
Description: appNewDescription,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.NewAppServerFlag,
internal.DomainFlag,
internal.NewAppNameFlag,
internal.PassFlag,
internal.SecretsFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "[<recipe>]",
ArgsUsage: "<recipe>",
Action: internal.NewAction,
BashComplete: autocomplete.RecipeNameComplete,
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"strings"
"time"
@ -15,22 +14,20 @@ import (
"github.com/buger/goterm"
dockerFormatter "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appPsCommand = cli.Command{
var appPsCommand = &cli.Command{
Name: "ps",
Aliases: []string{"p"},
Usage: "Check app status",
ArgsUsage: "<domain>",
Description: "Show a more detailed status output of a specific deployed app",
Description: "This command shows a more detailed status output of a specific deployed app.",
Aliases: []string{"p"},
Flags: []cli.Flag{
internal.WatchFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
@ -40,7 +37,7 @@ var appPsCommand = cli.Command{
logrus.Fatal(err)
}
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
@ -66,12 +63,10 @@ var appPsCommand = cli.Command{
// showPSOutput renders ps output.
func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
filters, err := app.Filters(true, true)
if err != nil {
logrus.Fatal(err)
}
filters := filters.NewArgs()
filters.Add("name", app.StackName())
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"fmt"
"os"
@ -11,8 +10,9 @@ import (
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// Volumes stores the variable from VolumesFlag
@ -20,26 +20,23 @@ var Volumes bool
// VolumesFlag is used to specify if volumes should be deleted when deleting an app
var VolumesFlag = &cli.BoolFlag{
Name: "volumes, V",
Name: "volumes",
Value: false,
Destination: &Volumes,
}
var appRemoveCommand = cli.Command{
var appRemoveCommand = &cli.Command{
Name: "remove",
Aliases: []string{"rm"},
ArgsUsage: "<domain>",
Usage: "Remove an already undeployed app",
Aliases: []string{"rm"},
Flags: []cli.Flag{
VolumesFlag,
internal.ForceFlag,
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if !internal.Force && !internal.NoInput {
if !internal.Force {
response := false
prompt := &survey.Confirm{
Message: fmt.Sprintf("about to remove %s, are you sure?", app.Name),
@ -57,20 +54,17 @@ var appRemoveCommand = cli.Command{
logrus.Fatal(err)
}
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if isDeployed {
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s \" or pass --force", app.Name, app.Name)
}
fs, err := app.Filters(false, false)
if err != nil {
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
fs := filters.NewArgs()
fs.Add("name", app.StackName())
secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: fs})
if err != nil {
logrus.Fatal(err)
}
@ -86,7 +80,7 @@ var appRemoveCommand = cli.Command{
if len(secrets) > 0 {
var secretNamesToRemove []string
if !internal.Force && !internal.NoInput {
if !internal.Force {
secretsPrompt := &survey.MultiSelect{
Message: "which secrets do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
@ -99,12 +93,8 @@ var appRemoveCommand = cli.Command{
}
}
if internal.Force || internal.NoInput {
secretNamesToRemove = secretNames
}
for _, name := range secretNamesToRemove {
err := cl.SecretRemove(context.Background(), secrets[name])
err := cl.SecretRemove(c.Context, secrets[name])
if err != nil {
logrus.Fatal(err)
}
@ -114,12 +104,7 @@ 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)
volumeListOKBody, err := cl.VolumeList(c.Context, fs)
volumeList := volumeListOKBody.Volumes
if err != nil {
logrus.Fatal(err)
@ -133,7 +118,7 @@ var appRemoveCommand = cli.Command{
if len(vols) > 0 {
if Volumes {
var removeVols []string
if !internal.Force && !internal.NoInput {
if !internal.Force {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
@ -145,9 +130,8 @@ var appRemoveCommand = cli.Command{
logrus.Fatal(err)
}
}
for _, vol := range removeVols {
err := cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing
err := cl.VolumeRemove(c.Context, vol, internal.Force) // last argument is for force removing
if err != nil {
logrus.Fatal(err)
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"errors"
"fmt"
@ -11,18 +10,14 @@ import (
upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appRestartCommand = cli.Command{
var appRestartCommand = &cli.Command{
Name: "restart",
Aliases: []string{"re"},
Usage: "Restart an app",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Aliases: []string{"re"},
ArgsUsage: "<service>",
Description: `This command restarts a service within a deployed app.`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
@ -42,22 +37,22 @@ var appRestartCommand = cli.Command{
serviceName := fmt.Sprintf("%s_%s", app.StackName(), serviceNameShort)
logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 0); err != nil {
if err := upstream.RunServiceScale(c.Context, cl, serviceName, 0); err != nil {
logrus.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
if err := stack.WaitOnService(c.Context, cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("%s has been scaled to 0 (restart logic)", serviceName)
logrus.Debugf("attempting to scale %s to 1 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 1); err != nil {
if err := upstream.RunServiceScale(c.Context, cl, serviceName, 1); err != nil {
logrus.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
if err := stack.WaitOnService(c.Context, cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}

View File

@ -1,201 +1,79 @@
package app
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"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"
"github.com/urfave/cli/v2"
)
type restoreConfig struct {
preHookCmd string
postHookCmd string
var restoreAllServices bool
var restoreAllServicesFlag = &cli.BoolFlag{
Name: "all",
Value: false,
Destination: &restoreAllServices,
Aliases: []string{"a"},
Usage: "Restore all services",
}
var appRestoreCommand = cli.Command{
var appRestoreCommand = &cli.Command{
Name: "restore",
Usage: "Restore an app from a backup",
Aliases: []string{"rs"},
Usage: "Run app restore",
ArgsUsage: "<domain> <service> <file>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Description: `
Run 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
`,
Flags: []cli.Flag{restoreAllServicesFlag},
ArgsUsage: "<service> [<backup file>]",
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <service>?"))
if c.Args().Len() > 1 && restoreAllServices {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use <service>/<backup file> and '--all' together"))
}
backupPath := c.Args().Get(2)
if backupPath == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("missing <file>?"))
}
if _, err := os.Stat(backupPath); err != nil {
abraSh := path.Join(config.RECIPES_DIR, app.Type, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
logrus.Fatalf("%s doesn't exist?", backupPath)
logrus.Fatalf("%s does not exist?", abraSh)
}
}
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
sourceCmd := fmt.Sprintf("source %s", abraSh)
execCmd := "abra_restore"
if !restoreAllServices {
serviceName := c.Args().Get(1)
if serviceName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no service(s) target provided"))
}
execCmd = fmt.Sprintf("abra_restore_%s", serviceName)
}
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
bytes, err := ioutil.ReadFile(abraSh)
if err != nil {
logrus.Fatal(err)
}
if !strings.Contains(string(bytes), execCmd) {
logrus.Fatalf("%s doesn't have a %s function", app.Type, execCmd)
}
restoreConfigs[service.Name] = rsConfig
}
}
backupFile := c.Args().Get(2)
if backupFile != "" {
execCmd = fmt.Sprintf("%s %s", execCmd, backupFile)
}
rsConfig, ok := restoreConfigs[serviceName]
if !ok {
rsConfig = restoreConfig{}
}
if err := runRestore(app, backupPath, serviceName, rsConfig); err != nil {
sourceAndExec := fmt.Sprintf("%s; %s", sourceCmd, execCmd)
cmd := exec.Command("bash", "-c", sourceAndExec)
if err := internal.RunCmd(cmd); 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
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/pkg/autocomplete"
@ -15,23 +14,19 @@ import (
"coopcloud.tech/abra/pkg/client"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appRollbackCommand = cli.Command{
var appRollbackCommand = &cli.Command{
Name: "rollback",
Aliases: []string{"rl"},
Usage: "Roll an app back to a previous version",
ArgsUsage: "<domain>",
Aliases: []string{"rl"},
ArgsUsage: "<app>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command rolls an app back to a previous version if one exists.
@ -39,7 +34,7 @@ You may pass "--force/-f" to downgrade to the same version again. This can be
useful if the container runtime has gotten into a weird state.
This action could be destructive, please ensure you have a copy of your app
data beforehand.
data beforehand - see "abra app backup <app>" for more.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new
@ -51,12 +46,12 @@ recipes.
stackName := app.StackName()
if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
if err := recipe.EnsureUpToDate(app.Type); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe)
r, err := recipe.Get(app.Type)
if err != nil {
logrus.Fatal(err)
}
@ -72,7 +67,7 @@ recipes.
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
@ -86,13 +81,13 @@ recipes.
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
versions, err := recipe.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 && !internal.Chaos {
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe)
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Type)
}
var availableDowngrades []string
@ -126,7 +121,7 @@ recipes.
var chosenDowngrade string
if !internal.Chaos {
if internal.Force || internal.NoInput {
if internal.Force {
chosenDowngrade = availableDowngrades[0]
logrus.Debugf("choosing %s as version to downgrade to (--force)", chosenDowngrade)
} else {
@ -141,7 +136,7 @@ recipes.
}
if !internal.Chaos {
if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil {
if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil {
logrus.Fatal(err)
}
}
@ -149,13 +144,13 @@ recipes.
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenDowngrade, err = recipe.ChaosVersion(app.Recipe)
chosenDowngrade, err = recipe.ChaosVersion(app.Type)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Type, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
@ -164,7 +159,7 @@ recipes.
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
if err != nil {
logrus.Fatal(err)
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"errors"
"fmt"
@ -14,42 +13,41 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var user string
var userFlag = &cli.StringFlag{
Name: "user, u",
Name: "user",
Value: "",
Destination: &user,
}
var noTTY bool
var noTTYFlag = &cli.BoolFlag{
Name: "no-tty, t",
Name: "no-tty",
Value: false,
Destination: &noTTY,
}
var appRunCommand = cli.Command{
var appRunCommand = &cli.Command{
Name: "run",
Aliases: []string{"r"},
Flags: []cli.Flag{
internal.DebugFlag,
noTTYFlag,
userFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <service> <args>...",
Aliases: []string{"r"},
ArgsUsage: "<service> <args>...",
Usage: "Run a command in a service container",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if len(c.Args()) < 2 {
if c.Args().Len() < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
}
if len(c.Args()) < 3 {
if c.Args().Len() < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
}
@ -59,16 +57,16 @@ var appRunCommand = cli.Command{
}
serviceName := c.Args().Get(1)
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
stackAndServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs()
filters.Add("name", stackAndServiceName)
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
targetContainer, err := containerPkg.GetContainer(c.Context, cl, filters, true)
if err != nil {
logrus.Fatal(err)
}
cmd := c.Args()[2:]
cmd := c.Args().Slice()[2:]
execCreateOpts := types.ExecConfig{
AttachStderr: true,
AttachStdin: true,

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"errors"
"fmt"
"os"
@ -10,45 +9,34 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var allSecrets bool
var allSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Name: "all",
Aliases: []string{"a"},
Value: false,
Destination: &allSecrets,
Usage: "Generate all secrets",
}
var rmAllSecrets bool
var rmAllSecretsFlag = &cli.BoolFlag{
Name: "all, a",
Destination: &rmAllSecrets,
Usage: "Remove all secrets",
}
var appSecretGenerateCommand = cli.Command{
var appSecretGenerateCommand = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate secrets",
ArgsUsage: "<domain> <secret> <version>",
Flags: []cli.Flag{
internal.DebugFlag,
allSecretsFlag,
internal.PassFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<secret> <version>",
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if len(c.Args()) == 1 && !allSecrets {
if c.Args().Len() == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err)
}
@ -70,10 +58,8 @@ var appSecretGenerateCommand = cli.Command{
parsed := secret.ParseSecretEnvVarName(sec)
if secretName == parsed {
secretsToCreate[sec] = secretVersion
matches = true
}
}
if !matches {
logrus.Fatalf("%s doesn't exist in the env config?", secretName)
}
@ -86,7 +72,7 @@ var appSecretGenerateCommand = cli.Command{
if internal.Pass {
for name, data := range secretVals {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
if err := secret.PassInsertSecret(data, name, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
@ -109,16 +95,12 @@ var appSecretGenerateCommand = cli.Command{
},
}
var appSecretInsertCommand = cli.Command{
var appSecretInsertCommand = &cli.Command{
Name: "insert",
Aliases: []string{"i"},
Usage: "Insert secret",
Flags: []cli.Flag{
internal.DebugFlag,
internal.PassFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>",
Flags: []cli.Flag{internal.PassFlag},
ArgsUsage: "<app> <secret-name> <version> <data>",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command inserts a secret into an app environment.
@ -135,7 +117,7 @@ Example:
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if len(c.Args()) != 4 {
if c.Args().Len() != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
}
@ -148,10 +130,8 @@ Example:
logrus.Fatal(err)
}
logrus.Infof("%s successfully stored on server", secretName)
if internal.Pass {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
if err := secret.PassInsertSecret(data, name, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
}
@ -160,54 +140,29 @@ Example:
},
}
// secretRm removes a secret.
func secretRm(cl *dockerClient.Client, app config.App, secretName, parsed string) error {
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
return err
}
logrus.Infof("deleted %s successfully from server", secretName)
if internal.PassRemove {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err
}
logrus.Infof("deleted %s successfully from local pass store", secretName)
}
return nil
}
var appSecretRmCommand = cli.Command{
var appSecretRmCommand = &cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove a secret",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
rmAllSecretsFlag,
internal.PassRemoveFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<secret-name>]",
Aliases: []string{"rm"},
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
ArgsUsage: "<app> <secret-name>",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command removes app secrets.
This command removes a secret from an app environment.
Example:
abra app secret remove myapp db_pass
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
secrets := secret.ReadSecretEnvVars(app.Env)
if c.Args().Get(1) != "" && rmAllSecrets {
if c.Args().Get(1) != "" && allSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
}
if c.Args().Get(1) == "" && !rmAllSecrets {
if c.Args().Get(1) == "" && !allSecrets {
internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
}
@ -216,71 +171,57 @@ Example:
logrus.Fatal(err)
}
filters, err := app.Filters(false, false)
filters := filters.NewArgs()
filters.Add("name", app.StackName())
secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
remoteSecretNames := make(map[string]bool)
for _, cont := range secretList {
remoteSecretNames[cont.Spec.Annotations.Name] = true
}
match := false
secretToRm := c.Args().Get(1)
for sec := range secrets {
secretName := secret.ParseSecretEnvVarName(sec)
for _, cont := range secretList {
secretName := cont.Spec.Annotations.Name
parsed := secret.ParseGeneratedSecretName(secretName, app)
if allSecrets {
if err := cl.SecretRemove(c.Context, secretName); err != nil {
logrus.Fatal(err)
}
logrus.Infof("deleted %s successfully from server", secretName)
secVal, err := secret.ParseSecretEnvVarValue(secrets[sec])
if err != nil {
if internal.Pass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, secVal.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok {
if secretToRm != "" {
if secretName == secretToRm {
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
logrus.Fatal(err)
}
return nil
logrus.Infof("deleted %s successfully from local pass store", secretName)
}
} else {
match = true
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
if parsed == secretToRm {
if err := cl.SecretRemove(c.Context, secretName); err != nil {
logrus.Fatal(err)
}
}
}
logrus.Infof("deleted %s successfully from server", secretName)
if internal.Pass {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
logrus.Fatal(err)
}
if !match && secretToRm != "" {
logrus.Fatalf("%s doesn't exist on server?", secretToRm)
logrus.Infof("deleted %s successfully from local pass store", secretName)
}
}
}
if !match {
logrus.Fatal("no secrets to remove?")
}
return nil
},
}
var appSecretLsCommand = cli.Command{
var appSecretLsCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
Usage: "List all secrets",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
secrets := secret.ReadSecretEnvVars(app.Env)
@ -293,12 +234,9 @@ var appSecretLsCommand = cli.Command{
logrus.Fatal(err)
}
filters, err := app.Filters(false, false)
if err != nil {
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
filters := filters.NewArgs()
filters.Add("name", app.StackName())
secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: filters})
if err != nil {
logrus.Fatal(err)
}
@ -334,12 +272,12 @@ var appSecretLsCommand = cli.Command{
BashComplete: autocomplete.AppNameComplete,
}
var appSecretCommand = cli.Command{
var appSecretCommand = &cli.Command{
Name: "secret",
Aliases: []string{"s"},
Usage: "Manage app secrets",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
ArgsUsage: "<command>",
Subcommands: []*cli.Command{
appSecretGenerateCommand,
appSecretInsertCommand,
appSecretRmCommand,

View File

@ -1,25 +1,17 @@
package app
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appUndeployCommand = cli.Command{
var appUndeployCommand = &cli.Command{
Name: "undeploy",
Aliases: []string{"un"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
Description: `
This does not destroy any of the application data. However, you should remain
@ -37,7 +29,7 @@ volumes as eligiblef or pruning once undeployed.
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
@ -51,7 +43,7 @@ volumes as eligiblef or pruning once undeployed.
}
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
if err := stack.RunRemove(c.Context, cl, rmOpts); err != nil {
logrus.Fatal(err)
}

View File

@ -1,7 +1,6 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
@ -14,36 +13,32 @@ import (
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appUpgradeCommand = cli.Command{
var appUpgradeCommand = &cli.Command{
Name: "upgrade",
Aliases: []string{"up"},
Usage: "Upgrade an app",
ArgsUsage: "<domain>",
ArgsUsage: "<app>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
},
Before: internal.SubCommandBefore,
Description: `
Upgrade an app. You can use it to choose and roll out a new upgrade to an
existing app.
This command supports upgrading an app. You can use it to choose and roll out a
new upgrade to an existing app.
This command specifically supports incrementing the version of running apps, as
opposed to "abra app deploy <domain>" which will not change the version of a
opposed to "abra app deploy <app>" which will not change the version of a
deployed app.
You may pass "--force/-f" to upgrade to the same version again. This can be
useful if the container runtime has gotten into a weird state.
This action could be destructive, please ensure you have a copy of your app
data beforehand.
data beforehand - see "abra app backup <app>" for more.
Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new
@ -54,12 +49,12 @@ recipes.
stackName := app.StackName()
if !internal.Chaos {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
if err := recipe.EnsureUpToDate(app.Type); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe)
r, err := recipe.Get(app.Type)
if err != nil {
logrus.Fatal(err)
}
@ -75,7 +70,7 @@ recipes.
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
@ -89,17 +84,17 @@ recipes.
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
versions, err := recipe.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 && !internal.Chaos {
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Recipe)
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Type)
}
var availableUpgrades []string
if deployedVersion == "unknown" {
if deployedVersion == "uknown" {
availableUpgrades = versions
logrus.Warnf("failed to determine version of deployed %s", app.Name)
}
@ -129,7 +124,7 @@ recipes.
var chosenUpgrade string
if len(availableUpgrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput {
if internal.Force {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
} else {
@ -146,13 +141,13 @@ recipes.
// if release notes written after git tag published, read them before we
// check out the tag and then they'll appear to be missing. this covers
// when we obviously will forget to write release notes before publishing
releaseNotes, err := internal.GetReleaseNotes(app.Recipe, chosenUpgrade)
releaseNotes, err := internal.GetReleaseNotes(app.Type, chosenUpgrade)
if err != nil {
return err
}
if !internal.Chaos {
if err := recipe.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil {
logrus.Fatal(err)
}
}
@ -160,13 +155,13 @@ recipes.
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenUpgrade, err = recipe.ChaosVersion(app.Recipe)
chosenUpgrade, err = recipe.ChaosVersion(app.Type)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Type, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
@ -175,7 +170,7 @@ recipes.
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
if err != nil {
logrus.Fatal(err)
}

View File

@ -1,8 +1,6 @@
package app
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
@ -11,7 +9,7 @@ import (
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// getImagePath returns the image name
@ -23,27 +21,21 @@ func getImagePath(image string) (string, error) {
path := reference.Path(img)
path = formatter.StripTagMeta(path)
path = recipe.StripTagMeta(path)
logrus.Debugf("parsed %s from %s", path, image)
return path, nil
}
var appVersionCommand = cli.Command{
var appVersionCommand = &cli.Command{
Name: "version",
Aliases: []string{"v"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Show app versions",
Description: `
Show all information about versioning related to a deployed app. This includes
the individual image names, tags and digests. But also the Co-op Cloud recipe
version.
This command shows all information about versioning related to a deployed app.
This includes the individual image names, tags and digests. But also the Co-op
Cloud recipe version.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
@ -56,7 +48,7 @@ version.
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil {
logrus.Fatal(err)
}
@ -69,7 +61,7 @@ version.
logrus.Fatalf("%s is not deployed?", app.Name)
}
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe)
recipeMeta, err := recipe.GetRecipeMeta(app.Type)
if err != nil {
logrus.Fatal(err)
}

View File

@ -1,45 +1,35 @@
package app
import (
"context"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var appVolumeListCommand = cli.Command{
var appVolumeListCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "List volumes associated with an app",
Aliases: []string{"ls"},
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
filters, err := app.Filters(false, true)
volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
if err != nil {
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(context.Background(), app.Server, filters)
if err != nil {
logrus.Fatal(err)
}
table := formatter.CreateTable([]string{"name", "created", "mounted"})
table := formatter.CreateTable([]string{"driver", "volume name"})
var volTable [][]string
for _, volume := range volumeList {
volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
volRow := []string{
volume.Driver,
volume.Name,
}
volTable = append(volTable, volRow)
}
@ -55,44 +45,36 @@ var appVolumeListCommand = cli.Command{
},
}
var appVolumeRemoveCommand = cli.Command{
var appVolumeRemoveCommand = &cli.Command{
Name: "remove",
Usage: "Remove volume(s) associated with an app",
Description: `
This command supports removing volumes associated with an app. The app in
question must be undeployed before you try to remove volumes. See "abra app
undeploy <domain>" for more.
undeploy <app>" for more.
The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this
interface.
Passing "--force/-f" will select all volumes for removal. Be careful.
Passing "--force" will select all volumes for removal. Be careful.
`,
ArgsUsage: "<domain>",
ArgsUsage: "<app>",
Aliases: []string{"rm"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
filters, err := app.Filters(false, true)
if err != nil {
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(context.Background(), app.Server, filters)
volumeList, err := client.GetVolumes(c.Context, app.Server, app.Name)
if err != nil {
logrus.Fatal(err)
}
volumeNames := client.GetVolumeNames(volumeList)
var volumesToRemove []string
if !internal.Force && !internal.NoInput {
if !internal.Force {
volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
@ -103,13 +85,11 @@ Passing "--force/-f" will select all volumes for removal. Be careful.
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
logrus.Fatal(err)
}
}
if internal.Force || internal.NoInput {
} else {
volumesToRemove = volumeNames
}
err = client.RemoveVolumes(context.Background(), app.Server, volumesToRemove, internal.Force)
err = client.RemoveVolumes(c.Context, app.Server, volumesToRemove, internal.Force)
if err != nil {
logrus.Fatal(err)
}
@ -121,12 +101,12 @@ Passing "--force/-f" will select all volumes for removal. Be careful.
BashComplete: autocomplete.AppNameComplete,
}
var appVolumeCommand = cli.Command{
var appVolumeCommand = &cli.Command{
Name: "volume",
Aliases: []string{"vl"},
Usage: "Manage app volumes",
ArgsUsage: "<domain>",
Subcommands: []cli.Command{
ArgsUsage: "<command>",
Subcommands: []*cli.Command{
appVolumeListCommand,
appVolumeRemoveCommand,
},

View File

@ -15,7 +15,7 @@ import (
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// CatalogueSkipList is all the repos that are not recipes.
@ -48,9 +48,7 @@ var CatalogueSkipList = map[string]bool{
"outline-with-patch": true,
"pyabra": true,
"radicle-seed-node": true,
"recipes-catalogue-json": true,
"recipes-wishlist": true,
"recipes.coopcloud.tech": true,
"recipes": true,
"stack-ssh-deploy": true,
"swarm-cronjob": true,
"tagcmp": true,
@ -58,27 +56,25 @@ var CatalogueSkipList = map[string]bool{
"tyop": true,
}
var catalogueGenerateCommand = cli.Command{
var catalogueGenerateCommand = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate the recipe catalogue",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PublishFlag,
internal.DryFlag,
internal.SkipUpdatesFlag,
internal.RegistryUsernameFlag,
internal.RegistryPasswordFlag,
},
Before: internal.SubCommandBefore,
Description: `
Generate a new copy of the recipe catalogue which can be found on:
This command generates a new copy of the recipe catalogue which can be found on:
https://recipes.coopcloud.tech (website that humans read)
https://recipes.coopcloud.tech/recipes.json (JSON that Abra reads)
https://recipes.coopcloud.tech
It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
listing, parses README.md and git tags to produce recipe metadata which is
loaded into the catalogue JSON file.
listing, parses README.md and git tags of those repositories to produce recipe
metadata and produces a recipes JSON file.
It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten.
@ -95,7 +91,7 @@ keys configured on your account.
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName != "" {
internal.ValidateRecipe(c, true)
internal.ValidateRecipe(c)
}
repos, err := recipe.ReadReposMetadata()
@ -133,9 +129,13 @@ keys configured on your account.
continue
}
versions, err := recipe.GetRecipeVersions(recipeMeta.Name)
versions, err := recipe.GetRecipeVersions(
recipeMeta.Name,
internal.RegistryUsername,
internal.RegistryPassword,
)
if err != nil {
logrus.Warn(err)
logrus.Fatal(err)
}
features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name)
@ -212,7 +212,7 @@ keys configured on your account.
logrus.Fatal(err)
}
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, "recipes")
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
logrus.Fatal(err)
}
@ -233,7 +233,7 @@ keys configured on your account.
}
if !internal.Dry && internal.Publish {
url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash())
url := fmt.Sprintf("%s/recipes/commit/%s", config.REPOS_BASE_URL, head.Hash())
logrus.Infof("new changes published: %s", url)
}
@ -247,13 +247,13 @@ keys configured on your account.
}
// CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = cli.Command{
var CatalogueCommand = &cli.Command{
Name: "catalogue",
Usage: "Manage the recipe catalogue",
Aliases: []string{"c"},
ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue",
Subcommands: []cli.Command{
Subcommands: []*cli.Command{
catalogueGenerateCommand,
},
}

View File

@ -14,31 +14,35 @@ import (
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/record"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/web"
logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// AutoCompleteCommand helps people set up auto-complete in their shells
var AutoCompleteCommand = cli.Command{
var AutoCompleteCommand = &cli.Command{
Name: "autocomplete",
Aliases: []string{"ac"},
Usage: "Configure shell autocompletion (recommended)",
Aliases: []string{"ac"},
Description: `
Set up auto-completion in your shell by downloading the relevant files and
laying out what additional information must be loaded. Supported shells are as
follows: bash, fish, fizsh & zsh.
This command helps set up autocompletion in your shell by downloading the
relevant autocompletion files and laying out what additional information must
be loaded.
Example:
abra autocomplete bash
Supported shells are as follows:
fizsh
zsh
bash
`,
ArgsUsage: "<shell>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Action: func(c *cli.Context) error {
shellType := c.Args().First()
@ -50,7 +54,6 @@ Example:
"bash": true,
"zsh": true,
"fizsh": true,
"fish": true,
}
if _, ok := supportedShells[shellType]; !ok {
@ -81,27 +84,19 @@ Example:
switch shellType {
case "bash":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
# Run the following commands to install autocompletion
sudo mkdir /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app domains listed!
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app names listed!
`, autocompletionFile))
case "zsh":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
# Run the following commands to install autocompletion
sudo mkdir /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app domains listed!
`, autocompletionFile))
case "fish":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
sudo mkdir -p /etc/fish/completions
sudo cp %s /etc/fish/completions/abra
echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app domains listed!
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app names listed!
`, autocompletionFile))
}
@ -110,16 +105,18 @@ echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
}
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = cli.Command{
var UpgradeCommand = &cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade Abra itself",
Aliases: []string{"u"},
Description: `
Upgrade Abra in-place with the latest stable or release candidate.
This command allows you to upgrade Abra in-place with the latest stable or
release candidate.
Pass "-r/--rc" to install the latest release candidate. Please bear in mind
that it may contain catastrophic bugs. Thank you very much for the testing
efforts!
If you would like to install the latest release candidate, please pass the
"--rc" option. Please bear in mind that the latest release candidate may have
some catastrophic bugs contained in it. In any case, thank you very much for
the testing efforts!
`,
Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error {
@ -153,7 +150,7 @@ func newAbraApp(version, commit string) *cli.App {
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{
Commands: []*cli.Command{
app.AppCommand,
server.ServerCommand,
recipe.RecipeCommand,
@ -162,18 +159,37 @@ func newAbraApp(version, commit string) *cli.App {
UpgradeCommand,
AutoCompleteCommand,
},
BashComplete: autocomplete.SubcommandComplete,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Authors: []*cli.Author{
// If you're looking at this and you hack on Abra and you're not listed
// here, please do add yourself! This is a community project, let's show
// some love
{Name: "3wordchant"},
{Name: "decentral1se"},
{Name: "kawaiipunk"},
{Name: "knoflook"},
{Name: "roxxers"},
},
}
app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error {
if internal.Debug {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
}
paths := []string{
config.ABRA_DIR,
path.Join(config.SERVERS_DIR),
path.Join(config.RECIPES_DIR),
path.Join(config.VENDOR_DIR),
path.Join(config.BACKUP_DIR),
}
for _, path := range paths {

View File

@ -1,35 +0,0 @@
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
}

View File

@ -1,7 +1,6 @@
package internal
import (
"context"
"fmt"
"io/ioutil"
"os"
@ -18,7 +17,7 @@ import (
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// DeployAction is the main command-line action for this package
@ -26,12 +25,12 @@ func DeployAction(c *cli.Context) error {
app := ValidateApp(c)
if !Chaos {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
if err := recipe.EnsureUpToDate(app.Type); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe)
r, err := recipe.Get(app.Type)
if err != nil {
logrus.Fatal(err)
}
@ -47,7 +46,7 @@ func DeployAction(c *cli.Context) error {
logrus.Debugf("checking whether %s is already deployed", app.StackName())
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
@ -66,24 +65,24 @@ func DeployAction(c *cli.Context) error {
if err != nil {
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
versions, err := recipe.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) > 0 {
version = versions[len(versions)-1]
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err)
}
} else {
head, err := git.GetRecipeHead(app.Recipe)
head, err := git.GetRecipeHead(app.Type)
if err != nil {
logrus.Fatal(err)
}
version = formatter.SmallSHA(head.String())
logrus.Warn("no versions detected, using latest commit")
if err := recipe.EnsureLatest(app.Recipe); err != nil {
if err := recipe.EnsureLatest(app.Type); err != nil {
logrus.Fatal(err)
}
}
@ -91,13 +90,13 @@ func DeployAction(c *cli.Context) error {
if version == "unknown" && !Chaos {
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err)
}
}
if version != "unknown" && !Chaos {
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err)
}
}
@ -105,13 +104,13 @@ func DeployAction(c *cli.Context) error {
if Chaos {
logrus.Warnf("chaos mode engaged")
var err error
version, err = recipe.ChaosVersion(app.Recipe)
version, err = recipe.ChaosVersion(app.Type)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Type, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
@ -120,7 +119,7 @@ func DeployAction(c *cli.Context) error {
app.Env[k] = v
}
composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env)
composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env)
if err != nil {
logrus.Fatal(err)
}
@ -141,6 +140,11 @@ func DeployAction(c *cli.Context) error {
if !NoDomainChecks {
domainName := app.Env["DOMAIN"]
ipv4, err := dns.EnsureIPv4(domainName)
if err != nil || ipv4 == "" {
logrus.Fatalf("could not find an IP address assigned to %s?", domainName)
}
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
logrus.Fatal(err)
}
@ -157,7 +161,7 @@ func DeployAction(c *cli.Context) error {
// DeployOverview shows a deployment overview
func DeployOverview(app config.App, version, message string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version"}
tableCol := []string{"server", "compose", "domain", "app name", "version"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
@ -170,7 +174,7 @@ func DeployOverview(app config.App, version, message string) error {
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
table.Append([]string{server, deployConfig, app.Domain, app.Name, version})
table.Render()
if NoInput {
@ -195,7 +199,7 @@ func DeployOverview(app config.App, version, message string) error {
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
tableCol := []string{"server", "compose", "domain", "app name", "current version", "to be deployed"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
@ -208,12 +212,12 @@ func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
table.Append([]string{server, deployConfig, app.Domain, app.Name, currentVersion, newVersion})
table.Render()
if releaseNotes == "" {
var err error
releaseNotes, err = GetReleaseNotes(app.Recipe, newVersion)
releaseNotes, err = GetReleaseNotes(app.Type, newVersion)
if err != nil {
return err
}

View File

@ -4,7 +4,7 @@ import (
"os"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// ShowSubcommandHelpAndError exits the program on error, logs the error to the

View File

@ -1,11 +1,7 @@
package internal
import (
"os"
logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// Secrets stores the variable from SecretsFlag
@ -13,7 +9,9 @@ var Secrets bool
// SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{
Name: "secrets, S",
Name: "secrets",
Aliases: []string{"ss"},
Value: false,
Usage: "Automatically generate secrets",
Destination: &Secrets,
}
@ -23,19 +21,22 @@ var Pass bool
// PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{
Name: "pass, p",
Name: "pass",
Aliases: []string{"p"},
Value: false,
Usage: "Store the generated secrets in a local pass store",
Destination: &Pass,
}
// PassRemove stores the variable for PassRemoveFlag
var PassRemove bool
// Context is temp
var Context string
// PassRemoveFlag turns on/off removing generated secrets from pass
var PassRemoveFlag = &cli.BoolFlag{
Name: "pass, p",
Usage: "Remove generated secrets from a local pass store",
Destination: &PassRemove,
// ContextFlag is temp
var ContextFlag = &cli.StringFlag{
Name: "context",
Value: "",
Aliases: []string{"c"},
Destination: &Context,
}
// Force force functionality without asking.
@ -43,7 +44,9 @@ var Force bool
// ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{
Name: "force, f",
Name: "force",
Value: false,
Aliases: []string{"f"},
Usage: "Perform action without further prompt. Use with care!",
Destination: &Force,
}
@ -53,7 +56,9 @@ var Chaos bool
// ChaosFlag turns on/off chaos functionality.
var ChaosFlag = &cli.BoolFlag{
Name: "chaos, C",
Name: "chaos",
Value: false,
Aliases: []string{"ch"},
Usage: "Deploy uncommitted recipes changes. Use with care!",
Destination: &Chaos,
}
@ -63,15 +68,18 @@ var DNSProvider string
// DNSProviderFlag selects a DNS provider.
var DNSProviderFlag = &cli.StringFlag{
Name: "provider, p",
Name: "provider",
Value: "",
Aliases: []string{"p"},
Usage: "DNS provider",
Destination: &DNSProvider,
}
var NoInput bool
var NoInputFlag = &cli.BoolFlag{
Name: "no-input, n",
Name: "no-input",
Value: false,
Aliases: []string{"n"},
Usage: "Toggle non-interactive mode",
Destination: &NoInput,
}
@ -79,8 +87,9 @@ var NoInputFlag = &cli.BoolFlag{
var DNSType string
var DNSTypeFlag = &cli.StringFlag{
Name: "record-type, rt",
Name: "type",
Value: "",
Aliases: []string{"t"},
Usage: "Domain name record type (e.g. A)",
Destination: &DNSType,
}
@ -88,8 +97,9 @@ var DNSTypeFlag = &cli.StringFlag{
var DNSName string
var DNSNameFlag = &cli.StringFlag{
Name: "record-name, rn",
Name: "name",
Value: "",
Aliases: []string{"n"},
Usage: "Domain name record name (e.g. mysubdomain)",
Destination: &DNSName,
}
@ -97,16 +107,18 @@ var DNSNameFlag = &cli.StringFlag{
var DNSValue string
var DNSValueFlag = &cli.StringFlag{
Name: "record-value, rv",
Name: "value",
Value: "",
Aliases: []string{"v"},
Usage: "Domain name record value (e.g. 192.168.1.1)",
Destination: &DNSValue,
}
var DNSTTL string
var DNSTTLFlag = &cli.StringFlag{
Name: "record-ttl, rl",
Name: "ttl",
Value: "600s",
Aliases: []string{"T"},
Usage: "Domain name TTL value (seconds)",
Destination: &DNSTTL,
}
@ -114,8 +126,9 @@ var DNSTTLFlag = &cli.StringFlag{
var DNSPriority int
var DNSPriorityFlag = &cli.IntFlag{
Name: "record-priority, rp",
Name: "priority",
Value: 10,
Aliases: []string{"P"},
Usage: "Domain name priority value",
Destination: &DNSPriority,
}
@ -123,7 +136,8 @@ var DNSPriorityFlag = &cli.IntFlag{
var ServerProvider string
var ServerProviderFlag = &cli.StringFlag{
Name: "provider, p",
Name: "provider",
Aliases: []string{"p"},
Usage: "3rd party server provider",
Destination: &ServerProvider,
}
@ -131,8 +145,9 @@ var ServerProviderFlag = &cli.StringFlag{
var CapsulInstanceURL string
var CapsulInstanceURLFlag = &cli.StringFlag{
Name: "capsul-url, cu",
Name: "capsul-url",
Value: "yolo.servers.coop",
Aliases: []string{"cu"},
Usage: "capsul instance URL",
Destination: &CapsulInstanceURL,
}
@ -140,8 +155,9 @@ var CapsulInstanceURLFlag = &cli.StringFlag{
var CapsulName string
var CapsulNameFlag = &cli.StringFlag{
Name: "capsul-name, cn",
Name: "capsul-name",
Value: "",
Aliases: []string{"cn"},
Usage: "capsul name",
Destination: &CapsulName,
}
@ -149,8 +165,9 @@ var CapsulNameFlag = &cli.StringFlag{
var CapsulType string
var CapsulTypeFlag = &cli.StringFlag{
Name: "capsul-type, ct",
Name: "capsul-type",
Value: "f1-xs",
Aliases: []string{"ct"},
Usage: "capsul type",
Destination: &CapsulType,
}
@ -158,33 +175,38 @@ var CapsulTypeFlag = &cli.StringFlag{
var CapsulImage string
var CapsulImageFlag = &cli.StringFlag{
Name: "capsul-image, ci",
Name: "capsul-image",
Value: "debian10",
Aliases: []string{"ci"},
Usage: "capsul image",
Destination: &CapsulImage,
}
var CapsulSSHKeys cli.StringSlice
var CapsulSSHKeysFlag = &cli.StringSliceFlag{
Name: "capsul-ssh-keys, cs",
Name: "capsul-ssh-keys",
Aliases: []string{"cs"},
Usage: "capsul SSH key",
Value: &CapsulSSHKeys,
Destination: &CapsulSSHKeys,
}
var CapsulAPIToken string
var CapsulAPITokenFlag = &cli.StringFlag{
Name: "capsul-token, ca",
Name: "capsul-token",
Aliases: []string{"ca"},
Usage: "capsul API token",
EnvVar: "CAPSUL_TOKEN",
EnvVars: []string{"CAPSUL_TOKEN"},
Destination: &CapsulAPIToken,
}
var HetznerCloudName string
var HetznerCloudNameFlag = &cli.StringFlag{
Name: "hetzner-name, hn",
Name: "hetzner-name",
Value: "",
Aliases: []string{"hn"},
Usage: "hetzner cloud name",
Destination: &HetznerCloudName,
}
@ -192,7 +214,8 @@ var HetznerCloudNameFlag = &cli.StringFlag{
var HetznerCloudType string
var HetznerCloudTypeFlag = &cli.StringFlag{
Name: "hetzner-type, ht",
Name: "hetzner-type",
Aliases: []string{"ht"},
Usage: "hetzner cloud type",
Destination: &HetznerCloudType,
Value: "cx11",
@ -201,7 +224,8 @@ var HetznerCloudTypeFlag = &cli.StringFlag{
var HetznerCloudImage string
var HetznerCloudImageFlag = &cli.StringFlag{
Name: "hetzner-image, hi",
Name: "hetzner-image",
Aliases: []string{"hi"},
Usage: "hetzner cloud image",
Value: "debian-10",
Destination: &HetznerCloudImage,
@ -210,15 +234,17 @@ var HetznerCloudImageFlag = &cli.StringFlag{
var HetznerCloudSSHKeys cli.StringSlice
var HetznerCloudSSHKeysFlag = &cli.StringSliceFlag{
Name: "hetzner-ssh-keys, hs",
Name: "hetzner-ssh-keys",
Aliases: []string{"hs"},
Usage: "hetzner cloud SSH keys (e.g. me@foo.com)",
Value: &HetznerCloudSSHKeys,
Destination: &HetznerCloudSSHKeys,
}
var HetznerCloudLocation string
var HetznerCloudLocationFlag = &cli.StringFlag{
Name: "hetzner-location, hl",
Name: "hetzner-location",
Aliases: []string{"hl"},
Usage: "hetzner cloud server location",
Value: "hel1",
Destination: &HetznerCloudLocation,
@ -227,9 +253,10 @@ var HetznerCloudLocationFlag = &cli.StringFlag{
var HetznerCloudAPIToken string
var HetznerCloudAPITokenFlag = &cli.StringFlag{
Name: "hetzner-token, ha",
Name: "hetzner-token",
Aliases: []string{"ha"},
Usage: "hetzner cloud API token",
EnvVar: "HCLOUD_TOKEN",
EnvVars: []string{"HCLOUD_TOKEN"},
Destination: &HetznerCloudAPIToken,
}
@ -238,7 +265,9 @@ var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug, d",
Name: "debug",
Aliases: []string{"d"},
Value: false,
Destination: &Debug,
Usage: "Show DEBUG messages",
}
@ -248,49 +277,61 @@ var RC bool
// RCFlag chooses the latest release candidate for install
var RCFlag = &cli.BoolFlag{
Name: "rc, r",
Name: "rc",
Value: false,
Destination: &RC,
Usage: "Insatll the latest release candidate",
}
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major, x",
Name: "major",
Usage: "Increase the major part of the version",
Value: false,
Aliases: []string{"ma", "x"},
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor, y",
Name: "minor",
Usage: "Increase the minor part of the version",
Value: false,
Aliases: []string{"mi", "y"},
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch, z",
Name: "patch",
Usage: "Increase the patch part of the version",
Value: false,
Aliases: []string{"pa", "z"},
Destination: &Patch,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run, r",
Name: "dry-run",
Usage: "Only reports changes that would be made",
Value: false,
Aliases: []string{"d"},
Destination: &Dry,
}
var Publish bool
var PublishFlag = &cli.BoolFlag{
Name: "publish, p",
Name: "publish",
Usage: "Publish changes to git.coopcloud.tech",
Value: false,
Aliases: []string{"p"},
Destination: &Publish,
}
var Domain string
var DomainFlag = &cli.StringFlag{
Name: "domain, D",
Name: "domain",
Aliases: []string{"d"},
Value: "",
Usage: "Choose a domain name",
Destination: &Domain,
@ -298,76 +339,114 @@ var DomainFlag = &cli.StringFlag{
var NewAppServer string
var NewAppServerFlag = &cli.StringFlag{
Name: "server, s",
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &NewAppServer,
}
var NewAppName string
var NewAppNameFlag = &cli.StringFlag{
Name: "app-name",
Aliases: []string{"a"},
Value: "",
Usage: "Choose an app name",
Destination: &NewAppName,
}
var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks, D",
Name: "no-domain-checks",
Aliases: []string{"nd"},
Value: false,
Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks,
}
var StdErrOnly bool
var StdErrOnlyFlag = &cli.BoolFlag{
Name: "stderr, s",
Name: "stderr",
Aliases: []string{"s"},
Value: false,
Usage: "Only tail stderr",
Destination: &StdErrOnly,
}
var AutoDNSRecord bool
var AutoDNSRecordFlag = &cli.BoolFlag{
Name: "auto",
Aliases: []string{"a"},
Value: false,
Usage: "Automatically configure DNS records",
Destination: &AutoDNSRecord,
}
var DontWaitConverge bool
var DontWaitConvergeFlag = &cli.BoolFlag{
Name: "no-converge-checks, c",
Name: "no-converge-checks",
Aliases: []string{"nc"},
Value: false,
Usage: "Don't wait for converge logic checks",
Destination: &DontWaitConverge,
}
var Watch bool
var WatchFlag = &cli.BoolFlag{
Name: "watch, w",
Name: "watch",
Aliases: []string{"w"},
Value: false,
Usage: "Watch status by polling repeatedly",
Destination: &Watch,
}
var OnlyErrors bool
var OnlyErrorFlag = &cli.BoolFlag{
Name: "errors, e",
Name: "errors",
Aliases: []string{"e"},
Value: false,
Usage: "Only show errors",
Destination: &OnlyErrors,
}
var SkipUpdates bool
var SkipUpdatesFlag = &cli.BoolFlag{
Name: "skip-updates, s",
Name: "skip-updates",
Aliases: []string{"s"},
Value: false,
Usage: "Skip updating recipe repositories",
Destination: &SkipUpdates,
}
var RegistryUsername string
var RegistryUsernameFlag = &cli.StringFlag{
Name: "username",
Aliases: []string{"user"},
Value: "",
Usage: "Registry username",
EnvVars: []string{"REGISTRY_USERNAME"},
Destination: &RegistryUsername,
}
var RegistryPassword string
var RegistryPasswordFlag = &cli.StringFlag{
Name: "password",
Aliases: []string{"pass"},
Value: "",
Usage: "Registry password",
EnvVars: []string{"REGISTRY_PASSWORD"},
Destination: &RegistryUsername,
}
var AllTags bool
var AllTagsFlag = &cli.BoolFlag{
Name: "all-tags, a",
Name: "all-tags",
Aliases: []string{"a"},
Value: false,
Usage: "List all tags, not just upgrades",
Destination: &AllTags,
}
var LocalCmd bool
var LocalCmdFlag = &cli.BoolFlag{
Name: "local, l",
Usage: "Run command locally",
Destination: &LocalCmd,
}
var RemoteUser string
var RemoteUserFlag = &cli.StringFlag{
Name: "user, u",
Value: "",
Usage: "User to run command within a service context",
Destination: &RemoteUser,
}
// SSHFailMsg is a hopefully helpful SSH failure message
var SSHFailMsg = `
Woops, Abra is unable to connect to connect to %s.
@ -416,33 +495,3 @@ Host foo.coopcloud.tech
Good luck!
`
var ServerAddFailMsg = `
Failed to add server %s.
This could be caused by two things.
Abra isn't picking up your SSH configuration or you need to specify it on the
command-line (e.g you use a non-standard port or username to connect). Run
"server add" with "-d/--debug" to learn more about what Abra is doing under the
hood.
Docker is not installed on your server. You can pass "-p/--provision" to
install Docker and initialise Docker Swarm mode. See help output for "server
add"
See "abra server add -h" for more.
`
// SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
func SubCommandBefore(c *cli.Context) error {
if Debug {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
}
return nil
}

View File

@ -4,7 +4,6 @@ import (
"fmt"
"path"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
@ -12,9 +11,8 @@ import (
"coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/abra/pkg/ssh"
"github.com/AlecAivazis/survey/v2"
"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// AppSecrets represents all app secrest
@ -25,7 +23,7 @@ var RecipeName string
// createSecrets creates all secrets for a new app.
func createSecrets(sanitisedAppName string) (AppSecrets, error) {
appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", Domain))
appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", NewAppName))
appEnv, err := config.ReadEnv(appEnvPath)
if err != nil {
return nil, err
@ -40,7 +38,7 @@ func createSecrets(sanitisedAppName string) (AppSecrets, error) {
if Pass {
for secretName := range secrets {
secretValue := secrets[secretName]
if err := secret.PassInsertSecret(secretValue, secretName, Domain, NewAppServer); err != nil {
if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, NewAppServer); err != nil {
return nil, err
}
}
@ -67,31 +65,6 @@ func ensureDomainFlag(recipe recipe.Recipe, server string) error {
return nil
}
// promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(appName string) error {
app, err := app.Get(appName)
if err != nil {
return err
}
secretEnvVars := secret.ReadSecretEnvVars(app.Env)
if len(secretEnvVars) == 0 {
logrus.Debugf("%s has no secrets to generate, skipping...", app.Recipe)
return nil
}
if !Secrets && !NoInput {
prompt := &survey.Confirm{
Message: "Generate app secrets?",
}
if err := survey.AskOne(prompt, &Secrets); err != nil {
return err
}
}
return nil
}
// ensureServerFlag checks if the server flag was used. if not, asks the user for it.
func ensureServerFlag() error {
servers, err := config.GetServers()
@ -116,9 +89,28 @@ func ensureServerFlag() error {
return nil
}
// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it.
func ensureAppNameFlag() error {
if NewAppName == "" && !NoInput {
prompt := &survey.Input{
Message: "Specify app name:",
Default: Domain,
}
if err := survey.AskOne(prompt, &NewAppName); err != nil {
return err
}
}
if NewAppName == "" {
return fmt.Errorf("no app name provided")
}
return nil
}
// NewAction is the new app creation logic
func NewAction(c *cli.Context) error {
recipe := ValidateRecipeWithPrompt(c, false)
recipe := ValidateRecipeWithPrompt(c)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
@ -132,45 +124,48 @@ func NewAction(c *cli.Context) error {
logrus.Fatal(err)
}
sanitisedAppName := config.SanitiseAppName(Domain)
logrus.Debugf("%s sanitised as %s for new app", Domain, sanitisedAppName)
if err := config.TemplateAppEnvSample(recipe.Name, Domain, NewAppServer, Domain); err != nil {
if err := ensureAppNameFlag(); err != nil {
logrus.Fatal(err)
}
if err := promptForSecrets(Domain); err != nil {
sanitisedAppName := config.SanitiseAppName(NewAppName)
if len(sanitisedAppName) > 45 {
logrus.Fatalf("%s cannot be longer than 45 characters", sanitisedAppName)
}
logrus.Debugf("%s sanitised as %s for new app", NewAppName, sanitisedAppName)
if err := config.TemplateAppEnvSample(recipe.Name, NewAppName, NewAppServer, Domain); err != nil {
logrus.Fatal(err)
}
var secrets AppSecrets
var secretTable *tablewriter.Table
if Secrets {
if err := ssh.EnsureHostKey(NewAppServer); err != nil {
logrus.Fatal(err)
}
var err error
secrets, err = createSecrets(sanitisedAppName)
secrets, err := createSecrets(sanitisedAppName)
if err != nil {
logrus.Fatal(err)
}
secretCols := []string{"Name", "Value"}
secretTable = formatter.CreateTable(secretCols)
secretTable := formatter.CreateTable(secretCols)
for secret := range secrets {
secretTable.Append([]string{secret, secrets[secret]})
}
if len(secrets) > 0 {
defer secretTable.Render()
}
}
if NewAppServer == "default" {
NewAppServer = "local"
}
tableCol := []string{"server", "recipe", "domain"}
tableCol := []string{"server", "type", "domain", "app name"}
table := formatter.CreateTable(tableCol)
table.Append([]string{NewAppServer, recipe.Name, Domain})
table.Append([]string{NewAppServer, recipe.Name, Domain, NewAppName})
fmt.Println("")
fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
@ -178,19 +173,11 @@ func NewAction(c *cli.Context) error {
table.Render()
fmt.Println("")
fmt.Println("You can configure this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app config %s", Domain))
fmt.Println(fmt.Sprintf("\n abra app config %s", NewAppName))
fmt.Println("")
fmt.Println("You can deploy this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", Domain))
fmt.Println(fmt.Sprintf("\n abra app deploy %s", NewAppName))
fmt.Println("")
if len(secrets) > 0 {
fmt.Println("Here are your generated secrets:")
fmt.Println("")
secretTable.Render()
fmt.Println("")
logrus.Warn("generated secrets are not shown again, please take note of them *now*")
}
return nil
}

View File

@ -3,15 +3,15 @@ package internal
import (
"fmt"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
)
// PromptBumpType prompts for version bump type
func PromptBumpType(tagString, latestRelease string) error {
func PromptBumpType(tagString string) error {
if (!Major && !Minor && !Patch) && tagString == "" {
fmt.Printf(`
You need to make a decision about what kind of an update this new recipe
@ -20,8 +20,6 @@ migration work or take care of some breaking changes? This can be signaled in
the version you specify on the recipe deploy label and is called a semantic
version.
The latest published version is %s.
Here is a semver cheat sheet (more on https://semver.org):
major: new features/bug fixes, backwards incompatible (e.g 1.0.0 -> 2.0.0).
@ -36,7 +34,7 @@ Here is a semver cheat sheet (more on https://semver.org):
should also Just Work and is mostly to do with minor bug fixes
and/or security patches. "nothing to worry about".
`, latestRelease)
`)
var chosenBumpType string
prompt := &survey.Select{
@ -96,7 +94,7 @@ func GetMainAppImage(recipe recipe.Recipe) (string, error) {
}
path = reference.Path(img)
path = formatter.StripTagMeta(path)
path = recipePkg.StripTagMeta(path)
return path, nil
}

View File

@ -12,14 +12,14 @@ import (
"coopcloud.tech/abra/pkg/ssh"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// AppName is used for configuring app name programmatically
var AppName string
// ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(c *cli.Context, ensureLatest bool) recipe.Recipe {
func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipeName := c.Args().First()
if recipeName == "" {
@ -38,12 +38,6 @@ func ValidateRecipe(c *cli.Context, ensureLatest bool) recipe.Recipe {
}
}
if ensureLatest {
if err := recipe.EnsureLatest(recipeName); err != nil {
logrus.Fatal(err)
}
}
logrus.Debugf("validated %s as recipe argument", recipeName)
return chosenRecipe
@ -51,7 +45,7 @@ func ValidateRecipe(c *cli.Context, ensureLatest bool) recipe.Recipe {
// ValidateRecipeWithPrompt ensures a recipe argument is present before
// validating, asking for input if required.
func ValidateRecipeWithPrompt(c *cli.Context, ensureLatest bool) recipe.Recipe {
func ValidateRecipeWithPrompt(c *cli.Context) recipe.Recipe {
recipeName := c.Args().First()
if recipeName == "" && !NoInput {
@ -105,12 +99,6 @@ func ValidateRecipeWithPrompt(c *cli.Context, ensureLatest bool) recipe.Recipe {
logrus.Fatal(err)
}
if ensureLatest {
if err := recipe.EnsureLatest(recipeName); err != nil {
logrus.Fatal(err)
}
}
logrus.Debugf("validated %s as recipe argument", recipeName)
return chosenRecipe
@ -134,7 +122,7 @@ func ValidateApp(c *cli.Context) config.App {
logrus.Fatal(err)
}
if err := recipe.EnsureExists(app.Recipe); err != nil {
if err := recipe.EnsureExists(app.Type); err != nil {
logrus.Fatal(err)
}
@ -148,7 +136,7 @@ func ValidateApp(c *cli.Context) config.App {
}
// ValidateDomain ensures the domain name arg is valid.
func ValidateDomain(c *cli.Context) string {
func ValidateDomain(c *cli.Context) (string, error) {
domainName := c.Args().First()
if domainName == "" && !NoInput {
@ -157,7 +145,7 @@ func ValidateDomain(c *cli.Context) string {
Default: "example.com",
}
if err := survey.AskOne(prompt, &domainName); err != nil {
logrus.Fatal(err)
return domainName, err
}
}
@ -167,14 +155,14 @@ func ValidateDomain(c *cli.Context) string {
logrus.Debugf("validated %s as domain argument", domainName)
return domainName
return domainName, nil
}
// ValidateSubCmdFlags ensures flag order conforms to correct order
func ValidateSubCmdFlags(c *cli.Context) bool {
for argIdx, arg := range c.Args() {
for argIdx, arg := range c.Args().Slice() {
if !strings.HasPrefix(arg, "--") {
for _, flag := range c.Args()[argIdx:] {
for _, flag := range c.Args().Slice()[argIdx:] {
if strings.HasPrefix(flag, "--") {
return false
}
@ -185,12 +173,12 @@ func ValidateSubCmdFlags(c *cli.Context) bool {
}
// ValidateServer ensures the server name arg is valid.
func ValidateServer(c *cli.Context) string {
func ValidateServer(c *cli.Context) (string, error) {
serverName := c.Args().First()
serverNames, err := config.ReadServerNames()
if err != nil {
logrus.Fatal(err)
return serverName, err
}
if serverName == "" && !NoInput {
@ -199,28 +187,17 @@ func ValidateServer(c *cli.Context) string {
Options: serverNames,
}
if err := survey.AskOne(prompt, &serverName); err != nil {
logrus.Fatal(err)
return serverName, err
}
}
matched := false
for _, name := range serverNames {
if name == serverName {
matched = true
}
}
if !matched {
ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
}
if serverName == "" {
ShowSubcommandHelpAndError(c, errors.New("no server provided"))
}
logrus.Debugf("validated %s as server argument", serverName)
return serverName
return serverName, nil
}
// EnsureDNSProvider ensures a DNS provider is chosen.
@ -392,7 +369,7 @@ func EnsureNewCapsulVPSFlags(c *cli.Context) error {
if err := survey.AskOne(prompt, &CapsulSSHKeys); err != nil {
return err
}
CapsulSSHKeys = cli.StringSlice(strings.Split(sshKeys, ","))
CapsulSSHKeys = *cli.NewStringSlice(strings.Split(sshKeys, ",")...)
}
if CapsulAPIToken == "" && !NoInput {
@ -471,7 +448,7 @@ func EnsureNewHetznerCloudVPSFlags(c *cli.Context) error {
if err := survey.AskOne(prompt, &sshKeys); err != nil {
return err
}
HetznerCloudSSHKeys = cli.StringSlice(strings.Split(sshKeys, ","))
HetznerCloudSSHKeys = *cli.NewStringSlice(strings.Split(sshKeys, ",")...)
}
if !NoInput {

View File

@ -9,22 +9,18 @@ import (
"coopcloud.tech/abra/pkg/lint"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var recipeLintCommand = cli.Command{
var recipeLintCommand = &cli.Command{
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OnlyErrorFlag,
},
Before: internal.SubCommandBefore,
Flags: []cli.Flag{internal.OnlyErrorFlag},
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c, true)
recipe := internal.ValidateRecipe(c)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)

View File

@ -6,30 +6,28 @@ import (
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var pattern string
var patternFlag = &cli.StringFlag{
Name: "pattern, p",
Name: "pattern",
Value: "",
Aliases: []string{"p"},
Usage: "Simple string to filter recipes",
Destination: &pattern,
}
var recipeListCommand = cli.Command{
var recipeListCommand = &cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
patternFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
catl, err := recipe.ReadRecipeCatalogue()
if err != nil {

View File

@ -13,7 +13,7 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// recipeMetadata is the recipe metadata for the README.md
@ -30,20 +30,15 @@ type recipeMetadata struct {
SSO string
}
var recipeNewCommand = cli.Command{
var recipeNewCommand = &cli.Command{
Name: "new",
Aliases: []string{"n"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
},
Before: internal.SubCommandBefore,
Usage: "Create a new recipe",
Aliases: []string{"n"},
ArgsUsage: "<recipe>",
Description: `
Create a new recipe.
This command creates a new recipe.
Abra uses the built-in example repository which is available here:
Abra uses our built-in example repository which is available here:
https://git.coopcloud.tech/coop-cloud/example
@ -111,7 +106,7 @@ In order to share your recipe, you can upload it the git repository to:
If you're not sure how to do that, come chat with us:
https://docs.coopcloud.tech/intro/contact
https://docs.coopcloud.tech/contact
See "abra recipe -h" for additional recipe maintainer commands.

View File

@ -1,27 +1,26 @@
package recipe
import (
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = cli.Command{
var RecipeCommand = &cli.Command{
Name: "recipe",
Aliases: []string{"r"},
Usage: "Manage recipes",
ArgsUsage: "<recipe>",
Aliases: []string{"r"},
Description: `
A recipe is a blueprint for an app. It is a bunch of config files which
describe how to deploy and maintain an app. Recipes are maintained by the Co-op
Cloud community and you can use Abra to read them, deploy them and create apps
for you.
Cloud community and you can use Abra to read them and create apps for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely
manner. Abra supports convenient automation for recipe maintainenace, see the
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
`,
Subcommands: []cli.Command{
Subcommands: []*cli.Command{
recipeListCommand,
recipeVersionCommand,
recipeReleaseCommand,

View File

@ -18,25 +18,26 @@ import (
"github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var recipeReleaseCommand = cli.Command{
var recipeReleaseCommand = &cli.Command{
Name: "release",
Aliases: []string{"rl"},
Usage: "Release a new recipe version",
Aliases: []string{"rl"},
ArgsUsage: "<recipe> [<version>]",
Description: `
Create a new version of a recipe. These versions are then published on the
Co-op Cloud recipe catalogue. These versions take the following form:
This command is used to specify a new version of a recipe. These versions are
then published on the Co-op Cloud recipe catalogue. These versions take the
following form:
a.b.c+x.y.z
Where the "a.b.c" part is a semantic version determined by the maintainer. The
"x.y.z" part is the image tag of the recipe "app" service (the main container
which contains the software to be used, by naming convention).
Where the "a.b.c" part is a semantic version determined by the maintainer. And
the "x.y.z" part is the image tag of the recipe "app" service (the main
container which contains the software to be used).
We maintain a semantic versioning scheme ("a.b.c") alongside the recipe
We maintain a semantic versioning scheme ("a.b.c") alongside the libre app
versioning scheme ("x.y.z") in order to maximise the chances that the nature of
recipe updates are properly communicated. I.e. developers of an app might
publish a minor version but that might lead to changes in the recipe which are
@ -47,18 +48,15 @@ requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
internal.PublishFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c, false)
recipe := internal.ValidateRecipeWithPrompt(c)
imagesTmp, err := getImageVersions(recipe)
if err != nil {
@ -142,7 +140,7 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
path := reference.Path(img)
path = formatter.StripTagMeta(path)
path = recipePkg.StripTagMeta(path)
var tag string
switch img.(type) {
@ -321,6 +319,12 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
}
var lastGitTag tagcmp.Tag
if tagString == "" {
if err := internal.PromptBumpType(tagString); err != nil {
return err
}
}
for _, tag := range tags {
parsed, err := tagcmp.Parse(tag)
if err != nil {
@ -361,12 +365,6 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
newTag.Major = strconv.Itoa(now + 1)
}
if tagString == "" {
if err := internal.PromptBumpType(tagString, lastGitTag.String()); err != nil {
return err
}
}
if internal.Major || internal.Minor || internal.Patch {
newTag.Metadata = mainAppVersion
tagString = newTag.String()
@ -392,15 +390,15 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
}
if err := commitRelease(recipe, tagString); err != nil {
logrus.Fatalf("failed to commit changes: %s", err.Error())
logrus.Fatal(err)
}
if err := tagRelease(tagString, repo); err != nil {
logrus.Fatalf("failed to tag release: %s", err.Error())
logrus.Fatal(err)
}
if err := pushRelease(recipe, tagString); err != nil {
logrus.Fatalf("failed to publish new release: %s", err.Error())
logrus.Fatal(err)
}
return nil

View File

@ -13,26 +13,23 @@ import (
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var recipeSyncCommand = cli.Command{
var recipeSyncCommand = &cli.Command{
Name: "sync",
Aliases: []string{"s"},
Usage: "Sync recipe version label",
Aliases: []string{"s"},
ArgsUsage: "<recipe> [<version>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
},
Before: internal.SubCommandBefore,
Description: `
Generate labels for the main recipe service (i.e. by convention, the service
named "app") which corresponds to the following format:
This command will generate labels for the main recipe service (i.e. by
convention, the service named 'app') which corresponds to the following format:
coop-cloud.${STACK_NAME}.version=<version>
@ -41,7 +38,7 @@ auto-generate it for you. The <recipe> configuration will be updated on the
local file system.
`,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c, false)
recipe := internal.ValidateRecipeWithPrompt(c)
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
@ -95,8 +92,7 @@ likely to change.
}
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
latestRelease := tags[len(tags)-1]
if err := internal.PromptBumpType("", latestRelease); err != nil {
if err := internal.PromptBumpType(""); err != nil {
logrus.Fatal(err)
}
}

View File

@ -12,13 +12,12 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
type imgPin struct {
@ -26,14 +25,14 @@ type imgPin struct {
version tagcmp.Tag
}
var recipeUpgradeCommand = cli.Command{
var recipeUpgradeCommand = &cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade recipe image tags",
Aliases: []string{"u"},
Description: `
Parse all image tags within the given <recipe> configuration and prompt with
more recent tags to upgrade to. It will update the relevant compose file tags
on the local file system.
This command reads and attempts to parse all image tags within the given
<recipe> configuration and prompt with more recent tags to upgrade to. It will
update the relevant compose file tags on the local file system.
Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it
@ -46,24 +45,18 @@ interface.
You may invoke this command in "wizard" mode and be prompted for input:
abra recipe upgrade
`,
BashComplete: autocomplete.RecipeNameComplete,
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PatchFlag,
internal.MinorFlag,
internal.MajorFlag,
internal.AllTagsFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c, true)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
recipe := internal.ValidateRecipeWithPrompt(c)
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
@ -116,14 +109,14 @@ You may invoke this command in "wizard" mode and be prompted for input:
logrus.Fatal(err)
}
regVersions, err := client.GetRegistryTags(img)
image := reference.Path(img)
regVersions, err := client.GetRegistryTags(image)
if err != nil {
logrus.Fatal(err)
}
image := reference.Path(img)
logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
image = formatter.StripTagMeta(image)
image = recipePkg.StripTagMeta(image)
switch img.(type) {
case reference.NamedTagged:
@ -145,7 +138,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion)
other, err := tagcmp.Parse(regVersion.Name)
if err != nil {
continue // skip tags that cannot be parsed
}
@ -160,7 +153,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && !internal.AllTags {
logrus.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
logrus.Info(fmt.Sprintf("no new versions available for %s, %s is the latest", image, tag))
continue // skip on to the next tag and don't update any compose files
}
@ -235,7 +228,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{"skip"}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion)
compatibleStrings = append(compatibleStrings, regVersion.Name)
}
}

View File

@ -6,21 +6,17 @@ import (
"coopcloud.tech/abra/pkg/formatter"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var recipeVersionCommand = cli.Command{
var recipeVersionCommand = &cli.Command{
Name: "versions",
Aliases: []string{"v"},
Usage: "List recipe versions",
Aliases: []string{"v"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c, false)
recipe := internal.ValidateRecipe(c)
catalogue, err := recipePkg.ReadRecipeCatalogue()
if err != nil {

View File

@ -1,7 +1,6 @@
package record
import (
"context"
"fmt"
"strconv"
@ -10,23 +9,21 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"github.com/libdns/gandi"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// RecordListCommand lists domains.
var RecordListCommand = cli.Command{
var RecordListCommand = &cli.Command{
Name: "list",
Usage: "List domain name records",
Aliases: []string{"ls"},
ArgsUsage: "<zone>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.DNSProviderFlag,
},
Before: internal.SubCommandBefore,
Description: `
List all domain name records managed by a 3rd party provider for a specific
zone.
This command lists all domain name records managed by a 3rd party provider for
a specific zone.
You must specify a zone (e.g. example.com) under which your domain name records
are listed. This zone must already be created on your provider account.
@ -52,7 +49,7 @@ are listed. This zone must already be created on your provider account.
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
}
records, err := provider.GetRecords(context.Background(), zone)
records, err := provider.GetRecords(c.Context, zone)
if err != nil {
logrus.Fatal(err)
}

View File

@ -1,7 +1,6 @@
package record
import (
"context"
"fmt"
"strconv"
@ -12,28 +11,26 @@ import (
"github.com/libdns/gandi"
"github.com/libdns/libdns"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// RecordNewCommand creates a new domain name record.
var RecordNewCommand = cli.Command{
var RecordNewCommand = &cli.Command{
Name: "new",
Usage: "Create a new domain record",
Aliases: []string{"n"},
ArgsUsage: "<zone>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DNSProviderFlag,
internal.DNSTypeFlag,
internal.DNSNameFlag,
internal.DNSValueFlag,
internal.DNSTTLFlag,
internal.DNSPriorityFlag,
internal.AutoDNSRecordFlag,
},
Before: internal.SubCommandBefore,
Description: `
Create a new domain name record for a specific zone.
This command creates a new domain name record for a specific zone.
You must specify a zone (e.g. example.com) under which your domain name records
are listed. This zone must already be created on your provider account.
@ -42,9 +39,16 @@ Example:
abra record new foo.com -p gandi -t A -n myapp -v 192.168.178.44
You may also invoke this command in "wizard" mode and be prompted for input:
Typically, you need two records, an A record which points at the zone (@.) and
a wildcard record for your apps (*.). Pass "--auto" to have Abra automatically
set this up.
abra record new --auto foo.com -p gandi -v 192.168.178.44
You may also invoke this command in "wizard" mode and be prompted for input
abra record new
`,
Action: func(c *cli.Context) error {
zone, err := internal.EnsureZoneArgument(c)
@ -67,6 +71,25 @@ You may also invoke this command in "wizard" mode and be prompted for input:
logrus.Fatalf("%s is not a supported DNS provider", internal.DNSProvider)
}
if internal.AutoDNSRecord {
ipv4, err := dns.EnsureIPv4(zone)
if err != nil {
logrus.Debugf("no ipv4 associated with %s, prompting for input", zone)
if err := internal.EnsureDNSValueFlag(c); err != nil {
logrus.Fatal(err)
}
ipv4 = internal.DNSValue
}
logrus.Infof("automatically configuring @./*. A records for %s for %s (--auto)", zone, ipv4)
if err := autoConfigure(c, &provider, zone, ipv4); err != nil {
logrus.Fatal(err)
}
return nil
}
if err := internal.EnsureDNSTypeFlag(c); err != nil {
logrus.Fatal(err)
}
@ -95,7 +118,7 @@ You may also invoke this command in "wizard" mode and be prompted for input:
record.Priority = internal.DNSPriority
}
records, err := provider.GetRecords(context.Background(), zone)
records, err := provider.GetRecords(c.Context, zone)
if err != nil {
logrus.Fatal(err)
}
@ -109,7 +132,7 @@ You may also invoke this command in "wizard" mode and be prompted for input:
}
createdRecords, err := provider.SetRecords(
context.Background(),
c.Context,
zone,
[]libdns.Record{record},
)
@ -146,3 +169,84 @@ You may also invoke this command in "wizard" mode and be prompted for input:
return nil
},
}
func autoConfigure(c *cli.Context, provider *gandi.Provider, zone, ipv4 string) error {
ttl, err := dns.GetTTL(internal.DNSTTL)
if err != nil {
return err
}
atRecord := libdns.Record{
Type: "A",
Name: "@",
Value: ipv4,
TTL: ttl,
}
wildcardRecord := libdns.Record{
Type: "A",
Name: "*",
Value: ipv4,
TTL: ttl,
}
records := []libdns.Record{atRecord, wildcardRecord}
tableCol := []string{"type", "name", "value", "TTL", "priority"}
table := formatter.CreateTable(tableCol)
for _, record := range records {
existingRecords, err := provider.GetRecords(c.Context, zone)
if err != nil {
return err
}
discovered := false
for _, existingRecord := range existingRecords {
if existingRecord.Type == record.Type &&
existingRecord.Name == record.Name &&
existingRecord.Value == record.Value {
logrus.Warnf("%s record: %s %s for %s already exists?", record.Type, record.Name, record.Value, zone)
discovered = true
}
}
if discovered {
continue
}
createdRecords, err := provider.SetRecords(
c.Context,
zone,
[]libdns.Record{record},
)
if err != nil {
return err
}
if len(createdRecords) == 0 {
return fmt.Errorf("provider library reports that no record was created?")
}
createdRecord := createdRecords[0]
value := createdRecord.Value
if len(createdRecord.Value) > 30 {
value = fmt.Sprintf("%s...", createdRecord.Value[:30])
}
table.Append([]string{
createdRecord.Type,
createdRecord.Name,
value,
createdRecord.TTL.String(),
strconv.Itoa(createdRecord.Priority),
})
}
if table.NumLines() > 0 {
table.Render()
}
return nil
}

View File

@ -1,19 +1,19 @@
package record
import (
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// RecordCommand supports managing DNS entries.
var RecordCommand = cli.Command{
var RecordCommand = &cli.Command{
Name: "record",
Usage: "Manage domain name records",
Aliases: []string{"rc"},
ArgsUsage: "<record>",
Description: `
Manage domain name records via 3rd party providers such as Gandi DNS. It
supports listing, creating and removing all types of records that you might
need for managing Co-op Cloud apps.
This command supports managing domain name records via 3rd party providers such
as Gandi DNS. It supports listing, creating and removing all types of records
that you might need for managing Co-op Cloud apps.
The following providers are supported:
@ -28,8 +28,9 @@ library documentation for more. It supports many existing providers and allows
to implement new provider support easily.
https://pkg.go.dev/github.com/libdns/libdns
`,
Subcommands: []cli.Command{
Subcommands: []*cli.Command{
RecordListCommand,
RecordNewCommand,
RecordRemoveCommand,

View File

@ -1,7 +1,6 @@
package record
import (
"context"
"fmt"
"strconv"
@ -12,25 +11,22 @@ import (
"github.com/libdns/gandi"
"github.com/libdns/libdns"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// RecordRemoveCommand lists domains.
var RecordRemoveCommand = cli.Command{
var RecordRemoveCommand = &cli.Command{
Name: "remove",
Usage: "Remove a domain name record",
Aliases: []string{"rm"},
ArgsUsage: "<zone>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.DNSProviderFlag,
internal.DNSTypeFlag,
internal.DNSNameFlag,
},
Before: internal.SubCommandBefore,
Description: `
Remove a domain name record for a specific zone.
This command removes a domain name record for a specific zone.
It uses the type of record and name to match existing records and choose one
for deletion. You must specify a zone (e.g. example.com) under which your
@ -41,7 +37,7 @@ Example:
abra record remove foo.com -p gandi -t A -n myapp
You may also invoke this command in "wizard" mode and be prompted for input:
You may also invoke this command in "wizard" mode and be prompted for input
abra record rm
`,
@ -74,7 +70,7 @@ You may also invoke this command in "wizard" mode and be prompted for input:
logrus.Fatal(err)
}
records, err := provider.GetRecords(context.Background(), zone)
records, err := provider.GetRecords(c.Context, zone)
if err != nil {
logrus.Fatal(err)
}
@ -124,7 +120,7 @@ You may also invoke this command in "wizard" mode and be prompted for input:
}
}
_, err = provider.DeleteRecords(context.Background(), zone, []libdns.Record{toDelete})
_, err = provider.DeleteRecords(c.Context, zone, []libdns.Record{toDelete})
if err != nil {
logrus.Fatal(err)
}

View File

@ -1,12 +1,12 @@
package server
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"strings"
@ -22,14 +22,14 @@ import (
"github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var (
dockerInstallMsg = `
A docker installation cannot be found on %s. This is a required system
dependency for running Co-op Cloud apps on your server. If you would like, Abra
can attempt to install Docker for you using the upstream non-interactive
dependency for running Co-op Cloud on your server. If you would like, Abra can
attempt to install Docker for you using the upstream non-interactive
installation script.
See the following documentation for more:
@ -41,26 +41,32 @@ such purposes. Docker stable is now installed by default by this script. The
source for this script can be seen here:
https://github.com/docker/docker-install
`
)
var local bool
var localFlag = &cli.BoolFlag{
Name: "local, l",
Name: "local",
Aliases: []string{"l"},
Value: false,
Usage: "Use local server",
Destination: &local,
}
var provision bool
var provisionFlag = &cli.BoolFlag{
Name: "provision, p",
Name: "provision",
Aliases: []string{"p"},
Value: false,
Usage: "Provision server so it can deploy apps",
Destination: &provision,
}
var sshAuth string
var sshAuthFlag = &cli.StringFlag{
Name: "ssh-auth, s",
Name: "ssh-auth",
Aliases: []string{"sh"},
Value: "identity-file",
Usage: "Select SSH authentication method (identity-file, password)",
Destination: &sshAuth,
@ -68,11 +74,22 @@ var sshAuthFlag = &cli.StringFlag{
var askSudoPass bool
var askSudoPassFlag = &cli.BoolFlag{
Name: "ask-sudo-pass, a",
Name: "ask-sudo-pass",
Aliases: []string{"as"},
Value: false,
Usage: "Ask for sudo password",
Destination: &askSudoPass,
}
var traefik bool
var traefikFlag = &cli.BoolFlag{
Name: "traefik",
Aliases: []string{"t"},
Value: false,
Usage: "Deploy traefik",
Destination: &traefik,
}
func cleanUp(domainName string) {
logrus.Warnf("cleaning up context for %s", domainName)
if err := client.DeleteContext(domainName); err != nil {
@ -146,6 +163,12 @@ func newLocalServer(c *cli.Context, domainName string) error {
}
}
if traefik {
if err := deployTraefik(c, cl, domainName); err != nil {
return err
}
}
logrus.Info("local server has been added")
return nil
@ -246,7 +269,7 @@ Abra was unable to bootstrap Docker, see below for logs:
%s
If nothing works, you can try running the Docker install script manually on your server:
If nothing works, you try running the Docker install script manually on your server:
wget -O- https://get.docker.com | bash
@ -276,7 +299,7 @@ Abra was unable to bootstrap Docker, see below for logs:
%s
This could be due to several reasons. One of the most common is that your
This could be due to a number of things but one of the most common is that your
server user account does not have sudo access, and if it does, you need to pass
"--ask-sudo-pass" in order to supply Abra with your password.
@ -297,7 +320,7 @@ If nothing works, you try running the Docker install script manually on your ser
func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string) error {
initReq := swarm.InitRequest{ListenAddr: "0.0.0.0:2377"}
if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
if _, err := cl.SwarmInit(c.Context, initReq); err != nil {
if strings.Contains(err.Error(), "is already part of a swarm") ||
strings.Contains(err.Error(), "must specify a listening address") {
logrus.Infof("swarm mode already initialised on %s", domainName)
@ -309,7 +332,7 @@ func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string)
}
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
if _, err := cl.NetworkCreate(c.Context, "proxy", netOpts); err != nil {
if !strings.Contains(err.Error(), "proxy already exists") {
return err
}
@ -331,7 +354,7 @@ func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error
ListenAddr: "0.0.0.0:2377",
AdvertiseAddr: ipv4,
}
if _, err := cl.SwarmInit(context.Background(), initReq); err != nil {
if _, err := cl.SwarmInit(c.Context, initReq); err != nil {
if strings.Contains(err.Error(), "is already part of a swarm") ||
strings.Contains(err.Error(), "must specify a listening address") {
logrus.Infof("swarm mode already initialised on %s", domainName)
@ -343,7 +366,7 @@ func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error
}
netOpts := types.NetworkCreate{Driver: "overlay", Scope: "swarm"}
if _, err := cl.NetworkCreate(context.Background(), "proxy", netOpts); err != nil {
if _, err := cl.NetworkCreate(c.Context, "proxy", netOpts); err != nil {
if !strings.Contains(err.Error(), "proxy already exists") {
return err
}
@ -365,33 +388,70 @@ func createServerDir(domainName string) error {
return nil
}
var serverAddCommand = cli.Command{
func deployTraefik(c *cli.Context, cl *dockerClient.Client, domainName string) error {
internal.NoInput = true
internal.RecipeName = "traefik"
internal.NewAppServer = domainName
internal.Domain = fmt.Sprintf("%s.%s", "traefik", domainName)
internal.NewAppName = fmt.Sprintf("%s_%s", "traefik", config.SanitiseAppName(domainName))
appEnvPath := path.Join(config.ABRA_DIR, "servers", internal.Domain, fmt.Sprintf("%s.env", internal.NewAppName))
if _, err := os.Stat(appEnvPath); os.IsNotExist(err) {
logrus.Info(fmt.Sprintf("-t/--traefik specified, automatically deploying traefik to %s", internal.NewAppServer))
if err := internal.NewAction(c); err != nil {
logrus.Fatal(err)
}
} else {
logrus.Infof("%s already exists, not creating again", appEnvPath)
}
internal.AppName = internal.NewAppName
if err := internal.DeployAction(c); err != nil {
logrus.Fatal(err)
}
return nil
}
var serverAddCommand = &cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Add a server to your configuration",
Description: `
Add a new server to your configuration so that it can be managed by Abra. This
command can also provision your server ("--provision/-p") with a Docker
installation so that it is capable of hosting Co-op Cloud apps.
This command adds a new server to your configuration so that it can be managed
by Abra. This can be useful when you already have a server provisioned and want
to start running Abra commands against it.
Abra will default to expecting that you have a running ssh-agent and are using
SSH keys to connect to your new server. Abra will also read your SSH config
(matching "Host" as <domain>). SSH connection details precedence follows as
such: command-line > SSH config > guessed defaults.
This command can also provision your server ("--provision/-p") so that it is
capable of hosting Co-op Cloud apps. Abra will default to expecting that you
have a running ssh-agent and are using SSH keys to connect to your new server.
Abra will also read your SSH config (matching "Host" as <domain>). SSH
connection details precedence follows as such: command-line > SSH config >
guessed defaults.
If you have no SSH key configured for this host and are instead using password
authentication, you may pass "--ssh-auth password" to have Abra ask you for the
password. "--ask-sudo-pass" may be passed if you run your provisioning commands
via sudo privilege escalation.
The <domain> argument must be a publicy accessible domain name which points to
your server. You should have working SSH access to this server already, Abra
will assume port 22 and will use your current system username to make an
initial connection. You can use the <user> and <port> arguments to adjust this.
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local
developer machine.
Example:
abra server add varia.zone glodemodem 12345 -p
abra server add --local
Otherwise, you may specify a remote server. The <domain> argument must be a
publicy accessible domain name which points to your server. You should have SSH
access to this server, Abra will assume port 22 and will use your current
system username to make an initial connection. You can use the <user> and
<port> arguments to adjust this.
Example:
abra server add --provision --traefik varia.zone glodemodem 12345
Abra will construct the following SSH connection and Docker context:
@ -399,23 +459,25 @@ Abra will construct the following SSH connection and Docker context:
All communication between Abra and the server will use this SSH connection.
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local
developer machine.
In this example, Abra will run the following operations:
1. Install Docker
2. Initialise Swarm mode
3. Deploy Traefik (core web proxy)
You may omit flags to avoid performing this provisioning logic.
`,
Aliases: []string{"a"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
localFlag,
provisionFlag,
sshAuthFlag,
askSudoPassFlag,
traefikFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> [<user>] [<port>]",
Action: func(c *cli.Context) error {
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
if c.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(c) {
err := errors.New("cannot use <domain> and --local together")
internal.ShowSubcommandHelpAndError(c, err)
}
@ -425,8 +487,6 @@ developer machine.
internal.ShowSubcommandHelpAndError(c, err)
}
domainName := internal.ValidateDomain(c)
if local {
if err := newLocalServer(c, "default"); err != nil {
logrus.Fatal(err)
@ -434,6 +494,11 @@ developer machine.
return nil
}
domainName, err := internal.ValidateDomain(c)
if err != nil {
logrus.Fatal(err)
}
username := c.Args().Get(1)
if username == "" {
systemUser, err := user.Current()
@ -458,17 +523,14 @@ developer machine.
cl, err := newClient(c, domainName)
if err != nil {
cleanUp(domainName)
logrus.Debugf("failed to construct client for %s, saw %s", domainName, err.Error())
logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
logrus.Fatal(err)
}
if provision {
logrus.Debugf("attempting to construct SSH client for %s", domainName)
sshCl, err := ssh.New(domainName, sshAuth, username, port)
if err != nil {
cleanUp(domainName)
logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
logrus.Fatal(err)
}
defer sshCl.Close()
logrus.Debugf("successfully created SSH client for %s", domainName)
@ -481,9 +543,15 @@ developer machine.
}
}
if _, err := cl.Info(context.Background()); err != nil {
if _, err := cl.Info(c.Context); err != nil {
cleanUp(domainName)
logrus.Fatalf(fmt.Sprintf(internal.ServerAddFailMsg, domainName))
logrus.Fatalf("couldn't make a remote docker connection to %s? use --provision/-p to attempt to install", domainName)
}
if traefik {
if err := deployTraefik(c, cl, domainName); err != nil {
logrus.Fatal(err)
}
}
return nil

View File

@ -3,23 +3,20 @@ package server
import (
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter"
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var serverListCommand = cli.Command{
var serverListCommand = &cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
Flags: []cli.Flag{
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: " ",
HideHelp: true,
Action: func(c *cli.Context) error {
dockerContextStore := context.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()

View File

@ -1,7 +1,6 @@
package server
import (
"context"
"fmt"
"strings"
@ -11,7 +10,7 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
func newHetznerCloudVPS(c *cli.Context) error {
@ -28,7 +27,7 @@ func newHetznerCloudVPS(c *cli.Context) error {
continue
}
sshKey, _, err := client.SSHKey.GetByName(context.Background(), sshKey)
sshKey, _, err := client.SSHKey.GetByName(c.Context, sshKey)
if err != nil {
return err
}
@ -73,7 +72,7 @@ func newHetznerCloudVPS(c *cli.Context) error {
logrus.Fatal("exiting as requested")
}
res, _, err := client.Server.Create(context.Background(), serverOpts)
res, _, err := client.Server.Create(c.Context, serverOpts)
if err != nil {
return err
}
@ -99,10 +98,9 @@ You can access this new VPS via SSH using the following command:
ssh root@%s
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record (manually
or by using "abra record new") and add the server to your Abra configuration
("abra server add") to have a working server that you can deploy Co-op Cloud
apps to.
not list this server)! You will need to assign a domain name record ("abra
record new") and add the server to your Abra configuration ("abra server add")
to have a working server that you can deploy Co-op Cloud apps to.
When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g.
@ -111,6 +109,10 @@ bar.example.com).
@ 1800 IN A %s
* 1800 IN A %s
"abra record new --auto" can help you do this quickly if you use a supported
DNS provider.
`,
internal.HetznerCloudName, ip, rootPassword,
ip, ip, ip,
@ -181,10 +183,9 @@ address. You can learn all about how to get SSH access to your new Capsul on:
%s/about-ssh
Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record (manually
or by using "abra record new") and add the server to your Abra configuration
("abra server add") to have a working server that you can deploy Co-op Cloud
apps to.
not list this server)! You will need to assign a domain name record ("abra
record new") and add the server to your Abra configuration ("abra server add")
to have a working server that you can deploy Co-op Cloud apps to.
When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g.
@ -193,17 +194,18 @@ bar.example.com).
@ 1800 IN A <your-capsul-ip>
* 1800 IN A <your-capsul-ip>
`, internal.CapsulName, resp.ID, internal.CapsulInstanceURL))
return nil
}
var serverNewCommand = cli.Command{
var serverNewCommand = &cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new server using a 3rd party provider",
Description: `
Create a new server via a 3rd party provider.
This command creates a new server via a 3rd party provider.
The following providers are supported:
@ -217,10 +219,10 @@ You may invoke this command in "wizard" mode and be prompted for input:
API tokens are read from the environment if specified, e.g.
export HCLOUD_TOKEN=...
Where "$provider_TOKEN" is the expected env var format.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.ServerProviderFlag,
// Capsul
@ -238,7 +240,6 @@ API tokens are read from the environment if specified, e.g.
internal.HetznerCloudLocationFlag,
internal.HetznerCloudAPITokenFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
if err := internal.EnsureServerProvider(); err != nil {
logrus.Fatal(err)

View File

@ -1,7 +1,6 @@
package server
import (
"context"
"fmt"
"os"
"path/filepath"
@ -13,12 +12,14 @@ import (
"github.com/AlecAivazis/survey/v2"
"github.com/hetznercloud/hcloud-go/hcloud"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
var rmServer bool
var rmServerFlag = &cli.BoolFlag{
Name: "server, s",
Name: "server",
Aliases: []string{"s"},
Value: false,
Usage: "remove the actual server also",
Destination: &rmServer,
}
@ -49,7 +50,7 @@ func rmHetznerCloudVPS(c *cli.Context) error {
client := hcloud.NewClient(hcloud.WithToken(internal.HetznerCloudAPIToken))
server, _, err := client.Server.Get(context.Background(), internal.HetznerCloudName)
server, _, err := client.Server.Get(c.Context, internal.HetznerCloudName)
if err != nil {
return err
}
@ -88,7 +89,7 @@ destroyed.
logrus.Fatal("exiting as requested")
}
_, err = client.Server.Delete(context.Background(), server)
_, err = client.Server.Delete(c.Context, server)
if err != nil {
return err
}
@ -98,13 +99,13 @@ destroyed.
return nil
}
var serverRemoveCommand = cli.Command{
var serverRemoveCommand = &cli.Command{
Name: "remove",
Aliases: []string{"rm"},
ArgsUsage: "[<server>]",
Usage: "Remove a managed server",
Description: `
Remova a server from Abra management.
This command removes a server from Abra management.
Depending on whether you used a 3rd party provider to create this server ("abra
server new"), you can also destroy the virtual server as well. Pass
@ -115,8 +116,6 @@ underlying client connection context. This server will then be lost in time,
like tears in rain.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
rmServerFlag,
internal.ServerProviderFlag,
@ -124,26 +123,22 @@ like tears in rain.
internal.HetznerCloudNameFlag,
internal.HetznerCloudAPITokenFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
warnMsg := `Did not pass -s/--server for actual server deletion, prompting!
Abra doesn't currently know if it helped you create this server with one of the
3rd party integrations (e.g. Capsul). You have a choice here to actually,
really and finally destroy this server using those integrations. If you want to
do this, choose Yes.
If you just want to remove the server config files & context, choose No.
`
serverName := c.Args().Get(1)
if serverName != "" {
var err error
serverName, err = internal.ValidateServer(c)
if err != nil {
logrus.Fatal(err)
}
}
if !rmServer {
logrus.Warn(fmt.Sprintf(warnMsg))
logrus.Warn("did not pass -s/--server for actual server deletion, prompting")
response := false
prompt := &survey.Confirm{
Message: "delete actual live server?",
Message: "prompt to actual server deletion?",
}
if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err)
@ -167,8 +162,10 @@ If you just want to remove the server config files & context, choose No.
logrus.Fatal(err)
}
}
}
if serverName != "" {
if err := client.DeleteContext(serverName); err != nil {
logrus.Fatal(err)
}
@ -178,6 +175,7 @@ If you just want to remove the server config files & context, choose No.
}
logrus.Infof("server at %s has been lost in time, like tears in rain", serverName)
}
return nil
},

View File

@ -1,23 +1,24 @@
package server
import (
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = cli.Command{
var ServerCommand = &cli.Command{
Name: "server",
Aliases: []string{"s"},
Usage: "Manage servers",
Description: `
Create, manage and remove servers using 3rd party integrations.
These commands support creating, managing and removing servers using 3rd party
integrations.
Servers can be created from scratch using the "abra server new" command. If you
already have a server, you can add it to your configuration using "abra server
add". Abra can provision servers so that they are ready to deploy Co-op Cloud
recipes, see available flags on "abra server add" for more.
apps, see available flags on "server add" for more.
`,
Subcommands: []cli.Command{
Subcommands: []*cli.Command{
serverNewCommand,
serverAddCommand,
serverListCommand,

51
go.mod
View File

@ -4,51 +4,46 @@ go 1.16
require (
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/AlecAivazis/survey/v2 v2.3.2
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v20.10.21+incompatible
github.com/docker/distribution v2.8.1+incompatible
github.com/docker/docker v20.10.21+incompatible
github.com/docker/go-units v0.5.0
github.com/docker/cli v20.10.12+incompatible
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.12+incompatible
github.com/docker/go-units v0.4.0
github.com/go-git/go-git/v5 v5.4.2
github.com/hetznercloud/hcloud-go v1.37.0
github.com/moby/sys/signal v0.7.0
github.com/hetznercloud/hcloud-go v1.33.1
github.com/moby/sys/signal v0.6.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.12.1
github.com/sirupsen/logrus v1.9.0
gotest.tools/v3 v3.4.0
github.com/schollz/progressbar/v3 v3.8.5
github.com/schultz-is/passgen v1.0.1
github.com/sirupsen/logrus v1.8.1
github.com/urfave/cli/v2 v2.3.0
gotest.tools/v3 v3.1.0
)
require (
coopcloud.tech/libcapsul v0.0.0-20211022074848-c35e78fe3f3e
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
github.com/buger/goterm v1.0.4
github.com/containerd/containerd v1.5.9 // indirect
github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1
github.com/Microsoft/hcsshim v0.8.21 // indirect
github.com/buger/goterm v1.0.3
github.com/containerd/containerd v1.5.5 // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fvbommel/sortorder v1.0.2 // indirect
github.com/gliderlabs/ssh v0.3.5
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.1
github.com/kevinburke/ssh_config v1.2.0
github.com/klauspost/pgzip v1.2.5
github.com/hashicorp/go-retryablehttp v0.7.0
github.com/kevinburke/ssh_config v1.1.0
github.com/libdns/gandi v1.0.2
github.com/libdns/libdns v0.2.1
github.com/moby/sys/mount v0.2.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/cobra v1.3.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/runc v1.0.2 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli v1.22.9
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
golang.org/x/crypto v0.3.0
golang.org/x/sys v0.2.0
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
)

627
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/urfave/cli/v2"
)
// AppNameComplete copletes app names
@ -40,24 +40,3 @@ func RecipeNameComplete(c *cli.Context) {
fmt.Println(name)
}
}
// SubcommandComplete completes subcommands.
func SubcommandComplete(c *cli.Context) {
if c.NArg() > 0 {
return
}
subcmds := []string{
"app",
"autocomplete",
"catalogue",
"recipe",
"record",
"server",
"upgrade",
}
for _, cmd := range subcmds {
fmt.Println(cmd)
}
}

View File

@ -27,11 +27,7 @@ func New(contextName string) (*client.Client, error) {
return nil, err
}
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint)
if err != nil {
return nil, err
}
helper := commandconnPkg.NewConnectionHelper(ctxEndpoint)
httpClient := &http.Client{
// No tls, no proxy
Transport: &http.Transport{

View File

@ -1,57 +1,193 @@
package client
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/containers/image/docker"
"github.com/containers/image/types"
"coopcloud.tech/abra/pkg/web"
"github.com/docker/distribution/reference"
"github.com/docker/docker/client"
"github.com/hashicorp/go-retryablehttp"
"github.com/sirupsen/logrus"
)
// GetRegistryTags retrieves all tags of an image from a container registry.
func GetRegistryTags(img reference.Named) ([]string, error) {
var tags []string
ref, err := docker.ParseReference(fmt.Sprintf("//%s", img))
if err != nil {
return tags, fmt.Errorf("failed to parse image %s, saw: %s", img, err.Error())
type RawTag struct {
Layer string
Name string
}
ctx := context.Background()
tags, err = docker.GetRepositoryTags(ctx, &types.SystemContext{}, ref)
if err != nil {
type RawTags []RawTag
var registryURL = "https://registry.hub.docker.com/v1/repositories/%s/tags"
func GetRegistryTags(image string) (RawTags, error) {
var tags RawTags
tagsUrl := fmt.Sprintf(registryURL, image)
if err := web.ReadJSON(tagsUrl, &tags); err != nil {
return tags, err
}
return tags, nil
}
// GetTagDigest retrieves an image digest from a container registry.
func GetTagDigest(cl *client.Client, image reference.Named) (string, error) {
target := fmt.Sprintf("//%s", reference.Path(image))
ref, err := docker.ParseReference(target)
if err != nil {
return "", fmt.Errorf("failed to parse image %s, saw: %s", image, err.Error())
func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}
ctx := context.Background()
img, err := ref.NewImage(ctx, nil)
if err != nil {
logrus.Debugf("failed to query remote registry for %s, saw: %s", image, err.Error())
return "", fmt.Errorf("unable to read digest for %s", image)
}
defer img.Close()
// getRegv2Token retrieves a registry v2 authentication token.
func getRegv2Token(cl *client.Client, image reference.Named, registryUsername, registryPassword string) (string, error) {
img := reference.Path(image)
tokenURL := "https://auth.docker.io/token"
values := fmt.Sprintf("service=registry.docker.io&scope=repository:%s:pull", img)
digest := img.ConfigInfo().Digest.String()
fullURL := fmt.Sprintf("%s?%s", tokenURL, values)
req, err := retryablehttp.NewRequest("GET", fullURL, nil)
if err != nil {
return "", err
}
if registryUsername != "" && registryPassword != "" {
logrus.Debugf("using registry log in credentials for token request")
auth := basicAuth(registryUsername, registryPassword)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", auth))
}
client := web.NewHTTPRetryClient()
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", nil
}
tokenRes := struct {
AccessToken string `json:"access_token"`
Expiry int `json:"expires_in"`
Issued string `json:"issued_at"`
Token string `json:"token"`
}{}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return "", err
}
return tokenRes.Token, nil
}
// GetTagDigest retrieves an image digest from a v2 registry
func GetTagDigest(cl *client.Client, image reference.Named, registryUsername, registryPassword string) (string, error) {
img := reference.Path(image)
tag := image.(reference.NamedTagged).Tag()
manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag)
req, err := retryablehttp.NewRequest("GET", manifestURL, nil)
if err != nil {
return "", err
}
token, err := getRegv2Token(cl, image, registryUsername, registryPassword)
if err != nil {
return "", err
}
if token == "" {
return "", fmt.Errorf("unable to retrieve registry token?")
}
req.Header = http.Header{
"Accept": []string{
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.docker.distribution.manifest.list.v2+json",
},
"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
}
client := web.NewHTTPRetryClient()
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
registryResT1 := struct {
SchemaVersion int
MediaType string
Manifests []struct {
MediaType string
Size int
Digest string
Platform struct {
Architecture string
Os string
}
}
}{}
registryResT2 := struct {
SchemaVersion int
MediaType string
Config struct {
MediaType string
Size int
Digest string
}
Layers []struct {
MediaType string
Size int
Digest string
}
}{}
if err := json.Unmarshal(body, &registryResT1); err != nil {
return "", err
}
var digest string
for _, manifest := range registryResT1.Manifests {
if string(manifest.Platform.Architecture) == "amd64" {
digest = strings.Split(manifest.Digest, ":")[1][:7]
}
}
if digest == "" {
return digest, fmt.Errorf("unable to read digest for %s", image)
if err := json.Unmarshal(body, &registryResT2); err != nil {
return "", err
}
digest = strings.Split(registryResT2.Config.Digest, ":")[1][:7]
}
return strings.Split(digest, ":")[1][:7], nil
if digest == "" {
return "", fmt.Errorf("Unable to retrieve amd64 digest for %s", image)
}
return digest, nil
}

View File

@ -5,18 +5,23 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
)
func GetVolumes(ctx context.Context, server string, fs filters.Args) ([]*types.Volume, error) {
func GetVolumes(ctx context.Context, server string, appName string) ([]*types.Volume, error) {
cl, err := New(server)
if err != nil {
return nil, err
}
fs := filters.NewArgs()
fs.Add("name", appName)
volumeListOKBody, err := cl.VolumeList(ctx, fs)
volumeList := volumeListOKBody.Volumes
if err != nil {
return volumeList, err
logrus.Fatal(err)
}
return volumeList, nil
@ -24,11 +29,9 @@ func GetVolumes(ctx context.Context, server string, fs filters.Args) ([]*types.V
func GetVolumeNames(volumes []*types.Volume) []string {
var volumeNames []string
for _, vol := range volumes {
volumeNames = append(volumeNames, vol.Name)
}
return volumeNames
}
@ -37,13 +40,12 @@ func RemoveVolumes(ctx context.Context, server string, volumeNames []string, for
if err != nil {
return err
}
for _, volName := range volumeNames {
err := cl.VolumeRemove(ctx, volName, force)
if err != nil {
return err
}
}
return nil
}

View File

@ -8,7 +8,6 @@ import (
"strings"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
@ -54,11 +53,11 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
case reference.NamedTagged:
composeTag = img.(reference.NamedTagged).Tag()
default:
logrus.Debugf("unable to parse %s, skipping", img)
continue
// unable to parse, typically image missing tag
return false, nil
}
composeImage := formatter.StripTagMeta(reference.Path(img))
composeImage := reference.Path(img)
logrus.Debugf("parsed %s from %s", composeTag, service.Image)
@ -75,7 +74,7 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
logrus.Debugf("updating %s to %s in %s", old, new, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
return false, err
return true, err
}
}
}

View File

@ -2,6 +2,7 @@ package config
import (
"fmt"
"html/template"
"io/ioutil"
"os"
"path"
@ -12,7 +13,6 @@ import (
loader "coopcloud.tech/abra/pkg/upstream/stack"
stack "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
)
@ -36,7 +36,7 @@ type AppFiles map[AppName]AppFile
// App reprents an app with its env file read into memory
type App struct {
Name AppName
Recipe string
Type string
Domain string
Env AppEnv
Server string
@ -52,59 +52,12 @@ func (a App) StackName() string {
}
stackName := SanitiseAppName(a.Name)
if len(stackName) > 45 {
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
stackName = stackName[:45]
}
a.Env["STACK_NAME"] = stackName
return stackName
}
// 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)
if err != nil {
return filters, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(a.Recipe, opts, a.Env)
if err != nil {
return filters, err
}
for _, service := range compose.Services {
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)
}
return filters, nil
}
// SORTING TYPES
// ByServer sort a slice of Apps
type ByServer []App
@ -115,25 +68,25 @@ func (a ByServer) Less(i, j int) bool {
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByServerAndRecipe sort a slice of Apps
type ByServerAndRecipe []App
// ByServerAndType sort a slice of Apps
type ByServerAndType []App
func (a ByServerAndRecipe) Len() int { return len(a) }
func (a ByServerAndRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndRecipe) Less(i, j int) bool {
func (a ByServerAndType) Len() int { return len(a) }
func (a ByServerAndType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndType) Less(i, j int) bool {
if a[i].Server == a[j].Server {
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
}
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByRecipe sort a slice of Apps
type ByRecipe []App
// ByType sort a slice of Apps
type ByType []App
func (a ByRecipe) Len() int { return len(a) }
func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByRecipe) Less(i, j int) bool {
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
func (a ByType) Len() int { return len(a) }
func (a ByType) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByType) Less(i, j int) bool {
return strings.ToLower(a[i].Type) < strings.ToLower(a[j].Type)
}
// ByName sort a slice of Apps
@ -165,18 +118,15 @@ func readAppEnvFile(appFile AppFile, name AppName) (App, error) {
func newApp(env AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"]
recipe, exists := env["RECIPE"]
appType, exists := env["TYPE"]
if !exists {
recipe, exists = env["TYPE"]
if !exists {
return App{}, fmt.Errorf("%s is missing the RECIPE env var", name)
}
return App{}, fmt.Errorf("%s is missing the TYPE env var", name)
}
return App{
Name: name,
Domain: domain,
Recipe: recipe,
Type: appType,
Env: env,
Server: appFile.Server,
Path: appFile.Path,
@ -263,13 +213,13 @@ func GetAppServiceNames(appName string) ([]string, error) {
return serviceNames, err
}
composeFiles, err := GetAppComposeFiles(app.Recipe, app.Env)
composeFiles, err := GetAppComposeFiles(app.Type, app.Env)
if err != nil {
return serviceNames, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Recipe, opts, app.Env)
compose, err := GetAppComposeConfig(app.Type, opts, app.Env)
if err != nil {
return serviceNames, err
}
@ -320,15 +270,18 @@ func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
return err
}
read, err := ioutil.ReadFile(appEnvPath)
file, err := os.OpenFile(appEnvPath, os.O_RDWR, 0664)
if err != nil {
return err
}
defer file.Close()
tpl, err := template.ParseFiles(appEnvPath)
if err != nil {
return err
}
newContents := strings.Replace(string(read), recipeName+".example.com", domain, -1)
err = ioutil.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
if err := tpl.Execute(file, struct{ Name string }{recipeName}); err != nil {
return err
}

View File

@ -16,12 +16,10 @@ import (
var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
var SERVERS_DIR = path.Join(ABRA_DIR, "servers")
var RECIPES_DIR = path.Join(ABRA_DIR, "recipes")
var RECIPES_DIR = path.Join(ABRA_DIR, "apps")
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"
var SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
// GetServers retrieves all servers.

View File

@ -20,12 +20,12 @@ var serverName = "evil.corp"
var expectedAppEnv = AppEnv{
"DOMAIN": "ecloud.evil.corp",
"RECIPE": "ecloud",
"TYPE": "ecloud",
}
var expectedApp = App{
Name: appName,
Recipe: expectedAppEnv["RECIPE"],
Type: expectedAppEnv["TYPE"],
Domain: expectedAppEnv["DOMAIN"],
Env: expectedAppEnv,
Path: expectedAppFile.Path,
@ -74,11 +74,11 @@ func TestReadEnv(t *testing.T) {
}
if !reflect.DeepEqual(env, expectedAppEnv) {
t.Fatalf(
"did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s",
"did not get expected application settings. Expected: DOMAIN=%s TYPE=%s; Got: DOMAIN=%s TYPE=%s",
expectedAppEnv["DOMAIN"],
expectedAppEnv["RECIPE"],
expectedAppEnv["TYPE"],
env["DOMAIN"],
env["RECIPE"],
env["TYPE"],
)
}
}

View File

@ -13,10 +13,10 @@ import (
"github.com/sirupsen/logrus"
)
// GetContainer retrieves a container. If noInput is false and the retrievd
// count of containers does not match 1, then a prompt is presented to let the
// user choose. A count of 0 is handled gracefully.
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, noInput bool) (types.Container, error) {
// GetContainer retrieves a container. If prompt is true and the retrievd count
// of containers does not match 1, then a prompt is presented to let the user
// choose. A count of 0 is handled gracefully.
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, prompt bool) (types.Container, error) {
containerOpts := types.ContainerListOptions{Filters: filters}
containers, err := cl.ContainerList(c, containerOpts)
if err != nil {
@ -37,7 +37,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
containersRaw = append(containersRaw, fmt.Sprintf("%s (created %v)", trimmed, created))
}
if noInput {
if !prompt {
err := fmt.Errorf("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " "))
return types.Container{}, err
}

View File

@ -47,6 +47,8 @@ func EnsureIPv4(domainName string) (string, error) {
},
}
logrus.Debugf("created DNS resolver via %s", freifunkDNS)
ctx := context.Background()
ips, err := resolver.LookupIPAddr(ctx, domainName)
if err != nil {
@ -58,17 +60,13 @@ func EnsureIPv4(domainName string) (string, error) {
}
ipv4 = ips[0].IP.To4().String()
logrus.Debugf("%s points to %s (resolver: %s)", domainName, ipv4, freifunkDNS)
logrus.Debugf("discovered the following ipv4 addr: %s", ipv4)
return ipv4, nil
}
// EnsureDomainsResolveSameIPv4 ensures that domains resolve to the same ipv4 address
func EnsureDomainsResolveSameIPv4(domainName, server string) (string, error) {
if server == "default" || server == "local" {
return "", nil
}
var ipv4 string
domainIPv4, err := EnsureIPv4(domainName)

View File

@ -8,7 +8,6 @@ import (
"github.com/docker/go-units"
"github.com/olekukonko/tablewriter"
"github.com/schollz/progressbar/v3"
"github.com/sirupsen/logrus"
)
func ShortenID(str string) string {
@ -50,22 +49,3 @@ func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
progressbar.OptionSetDescription(title),
)
}
// StripTagMeta strips front-matter image tag data that we don't need for parsing.
func StripTagMeta(image string) string {
originalImage := image
if strings.Contains(image, "docker.io") {
image = strings.Split(image, "/")[1]
}
if strings.Contains(image, "library") {
image = strings.Split(image, "/")[1]
}
if originalImage != image {
logrus.Debugf("stripped %s to %s for parsing", originalImage, image)
}
return image
}

View File

@ -5,25 +5,6 @@ import (
"github.com/go-git/go-git/v5/plumbing"
)
// Check if a branch exists in a repo.
// Use this and not repository.Branch(), because the latter does not
// actually check for existing branches.
// See https://github.com/go-git/go-git/issues/518
func HasBranch(repository *git.Repository, name string) bool {
var exist bool
if iter, err := repository.Branches(); err == nil {
iterFunc := func(reference *plumbing.Reference) error {
if name == reference.Name().Short() {
exist = true
return nil
}
return nil
}
_ = iter.ForEach(iterFunc)
}
return exist
}
// GetCurrentBranch retrieves the current branch of a repository
func GetCurrentBranch(repository *git.Repository) (string, error) {
branchRefs, err := repository.Branches()

View File

@ -78,13 +78,6 @@ var LintRules = map[string][]LintRule{
HowToResolve: "fill out all the metadata",
Function: LintMetadataFilledIn,
},
{
Ref: "R013",
Level: "warn",
Description: "git.coopcloud.tech repo exists",
HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...",
Function: LintHasRecipeRepo,
},
},
"error": {
{
@ -122,6 +115,13 @@ var LintRules = map[string][]LintRule{
HowToResolve: "vendor config versions in an abra.sh",
Function: LintAbraShVendors,
},
{
Ref: "R013",
Level: "error",
Description: "git.coopcloud.tech repo exists",
HowToResolve: "upload your recipe to git.coopcloud.tech/coop-cloud/...",
Function: LintHasRecipeRepo,
},
},
}

View File

@ -26,7 +26,7 @@ import (
)
// RecipeCatalogueURL is the only current recipe catalogue available.
const RecipeCatalogueURL = "https://recipes.coopcloud.tech/recipes.json"
const RecipeCatalogueURL = "https://apps.coopcloud.tech"
// ReposMetadataURL is the recipe repository metadata
const ReposMetadataURL = "https://git.coopcloud.tech/api/v1/orgs/coop-cloud/repos"
@ -166,7 +166,7 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
func (r Recipe) UpdateTag(image, tag string) (bool, error) {
pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, r.Name)
image = formatter.StripTagMeta(image)
image = StripTagMeta(image)
ok, err := compose.UpdateTag(pattern, image, tag, r.Name)
if err != nil {
@ -232,12 +232,8 @@ func Get(recipeName string) (Recipe, error) {
meta, err := GetRecipeMeta(recipeName)
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
meta = RecipeMeta{}
} else {
return Recipe{}, err
}
}
return Recipe{
Name: recipeName,
@ -359,7 +355,7 @@ func EnsureLatest(recipeName string) error {
return err
}
branch, err := GetDefaultBranch(repo, recipeName)
branch, err := gitPkg.GetCurrentBranch(repo)
if err != nil {
return err
}
@ -572,7 +568,7 @@ func EnsureUpToDate(recipeName string) error {
}
if !isClean {
return fmt.Errorf("%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding", recipeName, recipeDir)
return fmt.Errorf("%s has locally unstaged changes", recipeName)
}
repo, err := git.PlainOpen(recipeDir)
@ -619,15 +615,11 @@ func EnsureUpToDate(recipeName string) error {
func GetDefaultBranch(repo *git.Repository, recipeName string) (plumbing.ReferenceName, error) {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
meta, _ := GetRecipeMeta(recipeName)
if meta.DefaultBranch != "" {
return plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", meta.DefaultBranch)), nil
}
branch := "master"
if !gitPkg.HasBranch(repo, "master") {
if !gitPkg.HasBranch(repo, "main") {
return "", fmt.Errorf("failed to select default branch in %s", recipeDir)
if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil {
logrus.Debugf("failed to select branch in %s", recipeDir)
return "", err
}
branch = "main"
}
@ -697,7 +689,7 @@ func recipeCatalogueFSIsLatest() (bool, error) {
return false, nil
}
logrus.Debug("file system cached recipe catalogue is up-to-date")
logrus.Debug("file system cached recipe catalogue is now up-to-date")
return true, nil
}
@ -716,12 +708,14 @@ func ReadRecipeCatalogue() (RecipeCatalogue, error) {
}
if !recipeFSIsLatest {
logrus.Debugf("reading recipe catalogue from web to get latest")
if err := readRecipeCatalogueWeb(&recipes); err != nil {
return nil, err
}
return recipes, nil
}
logrus.Debugf("reading recipe catalogue from file system cache to get latest")
if err := readRecipeCatalogueFS(&recipes); err != nil {
return nil, err
}
@ -803,7 +797,8 @@ func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
recipeMeta, ok := catl[recipeName]
if !ok {
return RecipeMeta{}, fmt.Errorf("recipe %s does not exist?", recipeName)
err := fmt.Errorf("recipe %s does not exist?", recipeName)
return RecipeMeta{}, err
}
if err := EnsureExists(recipeName); err != nil {
@ -928,7 +923,7 @@ func ReadReposMetadata() (RepoCatalogue, error) {
}
// GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (RecipeVersions, error) {
versions := RecipeVersions{}
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
@ -942,7 +937,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
worktree, err := repo.Worktree()
if err != nil {
return versions, err
logrus.Fatal(err)
}
gitTags, err := repo.Tags()
@ -972,9 +967,9 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
return err
}
cl, err := client.New("default") // only required for container registry calls
cl, err := client.New("default") // only required for docker.io registry calls
if err != nil {
return err
logrus.Fatal(err)
}
queryCache := make(map[reference.Named]string)
@ -988,7 +983,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
path := reference.Path(img)
path = formatter.StripTagMeta(path)
path = StripTagMeta(path)
var tag string
switch img.(type) {
@ -1002,19 +997,18 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
var exists bool
var digest string
if digest, exists = queryCache[img]; !exists {
logrus.Debugf("cache miss: querying for image: %s, tag: %s", path, tag)
logrus.Debugf("looking up image: %s from %s", img, path)
var err error
digest, err = client.GetTagDigest(cl, img)
digest, err = client.GetTagDigest(cl, img, registryUsername, registryPassword)
if err != nil {
logrus.Warn(err)
digest = "unknown"
continue
}
logrus.Debugf("queried for image: %s, tag: %s, digest: %s", path, tag, digest)
queryCache[img] = digest
logrus.Debugf("cached insert: %s, tag: %s, digest: %s", path, tag, digest)
logrus.Debugf("cached image: %s, tag: %s, digest: %s", path, tag, digest)
} else {
logrus.Debugf("cache hit: image: %s, tag: %s, digest: %s", path, tag, digest)
logrus.Debugf("reading image: %s, tag: %s, digest: %s from cache", path, tag, digest)
}
versionMeta[service.Name] = ServiceMeta{
@ -1056,11 +1050,30 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
return versions, nil
}
// StripTagMeta strips front-matter image tag data that we don't need for parsing.
func StripTagMeta(image string) string {
originalImage := image
if strings.Contains(image, "docker.io") {
image = strings.Split(image, "/")[1]
}
if strings.Contains(image, "library") {
image = strings.Split(image, "/")[1]
}
if originalImage != image {
logrus.Debugf("stripped %s to %s for parsing", originalImage, image)
}
return image
}
// EnsureCatalogue ensures that the catalogue is cloned locally & present.
func EnsureCatalogue() error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes")
if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err
}

View File

@ -8,11 +8,10 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/decentral1se/passgen"
"github.com/schultz-is/passgen"
"github.com/sirupsen/logrus"
)
@ -120,32 +119,23 @@ func ParseSecretEnvVarValue(secret string) (secretValue, error) {
func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (map[string]string, error) {
secrets := make(map[string]string)
var mutex sync.Mutex
var wg sync.WaitGroup
ch := make(chan error, len(secretEnvVars))
for secretEnvVar := range secretEnvVars {
wg.Add(1)
go func(s string) {
defer wg.Done()
secretName := ParseSecretEnvVarName(s)
secretValue, err := ParseSecretEnvVarValue(secretEnvVars[s])
if err != nil {
ch <- err
return
}
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secretValue.Version)
logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server)
if secretValue.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secretValue.Length))
if err != nil {
ch <- err
return
}
if err := client.StoreSecret(secretRemoteName, passwords[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
@ -155,9 +145,6 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
}
return
}
mutex.Lock()
defer mutex.Unlock()
secrets[secretName] = passwords[0]
} else {
passphrases, err := GeneratePassphrases(1)
@ -165,7 +152,6 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
ch <- err
return
}
if err := client.StoreSecret(secretRemoteName, passphrases[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
@ -175,17 +161,12 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
}
return
}
mutex.Lock()
defer mutex.Unlock()
secrets[secretName] = passphrases[0]
}
ch <- nil
}(secretEnvVar)
}
wg.Wait()
for range secretEnvVars {
err := <-ch
if err != nil {

View File

@ -325,7 +325,7 @@ func connect(username, host, port string, authMethod ssh.AuthMethod, timeout tim
conn, err = net.DialTimeout("tcp", hostnameAndPort, timeout)
if err != nil {
logrus.Debugf("tcp dialing %s failed, trying via ~/.ssh/config", hostnameAndPort)
hostConfig, err := GetHostConfig(host, username, port, true)
hostConfig, err := GetHostConfig(host, username, port)
if err != nil {
return nil, err
}
@ -452,7 +452,7 @@ func GetContextConnDetails(serverName string) (*dockerSSHPkg.Spec, error) {
}
}
hostConfig, err := GetHostConfig(serverName, "", "", false)
hostConfig, err := GetHostConfig(serverName, "", "")
if err != nil {
return &dockerSSHPkg.Spec{}, err
}
@ -472,36 +472,30 @@ func GetContextConnDetails(serverName string) (*dockerSSHPkg.Spec, error) {
}
// GetHostConfig retrieves a ~/.ssh/config config for a host.
func GetHostConfig(hostname, username, port string, override bool) (HostConfig, error) {
func GetHostConfig(hostname, username, port string) (HostConfig, error) {
var hostConfig HostConfig
if hostname == "" || override {
if sshHost := ssh_config.Get(hostname, "Hostname"); sshHost != "" {
hostname = sshHost
if hostname == "" {
if hostname = ssh_config.Get(hostname, "Hostname"); hostname == "" {
logrus.Debugf("no hostname found in SSH config, assuming %s", hostname)
}
}
if username == "" || override {
if sshUser := ssh_config.Get(hostname, "User"); sshUser != "" {
username = sshUser
} else {
if username == "" {
if username = ssh_config.Get(hostname, "User"); username == "" {
systemUser, err := user.Current()
if err != nil {
return hostConfig, err
}
logrus.Debugf("no username found in SSH config or passed on command-line, assuming %s", username)
username = systemUser.Username
}
}
if port == "" || override {
if sshPort := ssh_config.Get(hostname, "Port"); sshPort != "" {
// skip override probably correct port with dummy default value from
// ssh_config which is 22. only when the original port number is empty
// should we try this default. this might not cover all cases
// unfortunately.
if port != "" && sshPort != "22" {
port = sshPort
}
if port == "" {
if port = ssh_config.Get(hostname, "Port"); port == "" {
logrus.Debugf("no port found in SSH config or passed on command-line, assuming 22")
port = "22"
}
}
@ -513,6 +507,7 @@ func GetHostConfig(hostname, username, port string, override bool) (HostConfig,
}
hostConfig.IdentityFile = idf
} else {
logrus.Debugf("no identity file found in SSH config for %s", hostname)
hostConfig.IdentityFile = ""
}

View File

@ -1,38 +0,0 @@
package test
import (
"log"
"os"
"github.com/sirupsen/logrus"
)
// RmServerAppRecipe deletes the test server / app / recipe.
func RmServerAppRecipe() {
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
if err := os.Remove(testAppLink); err != nil {
logrus.Fatal(err)
}
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
if err := os.Remove(testRecipeLink); err != nil {
logrus.Fatal(err)
}
}
// MkServerAppRecipe symlinks the test server / app / recipe.
func MkServerAppRecipe() {
RmServerAppRecipe()
testAppDir := os.ExpandEnv("$PWD/../../tests/resources/testapp")
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
if err := os.Symlink(testAppDir, testAppLink); err != nil {
log.Fatal(err)
}
testRecipeDir := os.ExpandEnv("$PWD/../../tests/resources/testrecipe")
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
if err := os.Symlink(testRecipeDir, testRecipeLink); err != nil {
log.Fatal(err)
}
}

View File

@ -44,7 +44,6 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
ctxConnDetails.Host,
ctxConnDetails.User,
ctxConnDetails.Port,
false,
)
if err != nil {
return nil, err
@ -67,13 +66,13 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
return nil, err
}
func NewConnectionHelper(daemonURL string) (*connhelper.ConnectionHelper, error) {
func NewConnectionHelper(daemonURL string) *connhelper.ConnectionHelper {
helper, err := GetConnectionHelper(daemonURL)
if err != nil {
return nil, err
logrus.Fatal(err)
}
return helper, nil
return helper
}
func getDockerEndpoint(host string) (docker.Endpoint, error) {

View File

@ -420,12 +420,6 @@ func convertServiceSecrets(
return nil, err
}
// NOTE(d1): strip # length=... modifiers
if strings.Contains(obj.Name, "#") {
vals := strings.Split(obj.Name, "#")
obj.Name = strings.TrimSpace(vals[0])
}
file := swarm.SecretReferenceFileTarget(obj.File)
refs = append(refs, &swarm.SecretReference{
File: &file,

View File

@ -35,21 +35,16 @@ func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Confi
return nil, err
}
recipeName, exists := appEnv["RECIPE"]
if !exists {
recipeName, _ = appEnv["TYPE"]
}
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
if len(unsupportedProperties) > 0 {
logrus.Warnf("%s: ignoring unsupported options: %s",
recipeName, strings.Join(unsupportedProperties, ", "))
appEnv["TYPE"], strings.Join(unsupportedProperties, ", "))
}
deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
if len(deprecatedProperties) > 0 {
logrus.Warnf("%s: ignoring deprecated options: %s",
recipeName, propertyWarnings(deprecatedProperties))
appEnv["TYPE"], propertyWarnings(deprecatedProperties))
}
return config, nil
}

View File

@ -1,6 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"ignoreDeps": [
"github.com/urfave/cli"
]
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@ -1,7 +0,0 @@
function complete_abra_args
set -l cmd (commandline -poc) --generate-bash-completion
$cmd
end
complete -c abra -f -n "not __fish_seen_subcommand_from -h --help -v --version complete_abra_args" -a "(complete_abra_args)"
complete -c abra -f -s h -l help -d 'show help'
complete -c abra -f -s v -l version -d 'print the version'

View File

@ -1,8 +1,8 @@
#!/usr/bin/env bash
ABRA_VERSION="0.5.1-beta"
ABRA_VERSION="0.3.0-alpha"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.5.1-beta"
RC_VERSION="0.4.0-alpha-rc5"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do
@ -44,17 +44,8 @@ function install_abra_release {
exit 1
fi
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
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m)
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'

View File

@ -1,5 +0,0 @@
#!/bin/bash
set -ex
upx ./dist/abra_*/abra

View File

@ -1,4 +0,0 @@
GANDI_TOKEN=...
HCLOUD_TOKEN=...
REGISTRY_PASSWORD=...
REGISTRY_USERNAME=...

View File

@ -1 +0,0 @@
logs

View File

@ -1,28 +0,0 @@
# integration tests
> You need to be a member of Autonomic Co-op to run these tests, sorry!
`testfunctions.sh` contains the functions necessary to save and manipulate
logs. Run `test_all.sh logdir` to run tests specified in that file and save the
logs to `logdir`.
When creating new tests, make sure the test command is a one-liner (you can use
`;` to separate commands). Include `testfunctions.sh` and then write your tests
like this:
```
run_test '$ABRA other stuff here'
```
By default, the testing script will ask after every command if the execution
succeeded. If you reply `n`, it will log the test in the `logdir`. If you want
all tests to run without questions, run `export logall=yes` before executing
the test script.
To run tests, you'll need to prepare your environment:
```
cp .envrc.sample .envrc # fill out values...
direnv allow
./test_all.sh logs
```

View File

@ -1,14 +0,0 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA app ls'
run_test '$ABRA app ls --status'
run_test '$ABRA app ls --type wordpress'
run_test '$ABRA app ls --type wordpress --server swarm.autonomic.zone'
run_test '$ABRA app ls --type wordpress --server swarm.autonomic.zone --status'

View File

@ -1,12 +0,0 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA autocomplete bash'
run_test '$ABRA autocomplete fizsh'
run_test '$ABRA autocomplete zsh'
run_test '$ABRA autocomplete fish'

View File

@ -1,8 +0,0 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA catalogue generate'
run_test '$ABRA catalogue generate gitea'

View File

@ -1,14 +0,0 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
create_server_app_recipe
run_test '$ABRA app cmd foo.com test --local'
run_test '$ABRA app cmd foo.com test --local -- foo'
run_test '$ABRA app cmd foo.com test --local -- foo bar baz'
clean_server_app_recipe

View File

@ -1,25 +0,0 @@
#!/bin/bash
set -e
create_server_app_recipe() {
ln -srf ../resources/testapp ~/.abra/servers/foo.com
ln -srf ../resources/testrecipe ~/.abra/recipes
}
clean_server_app_recipe() {
unlink ~/.abra/servers/foo.com
unlink ~/.abra/recipes/testrecipe
}
function init() {
ABRA="$(pwd)/../../abra"
INSTALLER_URL="https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
export PATH=$PATH:$HOME/.local/bin
echo "choosing $ABRA as abra command"
echo "choosing $INSTALLER_URL as abra installer url"
}
init "$@"

101
tests/integration/core.sh Executable file
View File

@ -0,0 +1,101 @@
#!/bin/bash
# the goal of this script is to ensure basic core functionality is working
# before we make new releases. we try to make a balance between manual testing
# and automated testing, i.e. we don't invest too much time in a fragile
# automation that costs us more time to maintain and instead just do the test
# manually (see `../manual/manual.md` for more). it is a balance which we
# figure out together.
set -ex
ABRA="$HOME/.local/bin/abra -d"
INSTALLER_URL="https://install.abra.coopcloud.tech"
for arg in "$@"; do
if [ "$arg" == "--dev" ]; then
ABRA="/src/abra -d"
INSTALLER_URL="https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
fi
done
export PATH=$PATH:$HOME/.local/bin
# ========================================================================
# choosing abra executable for test run
# ========================================================================
echo "choosing $ABRA as abra executable"
echo "choosing $INSTALLER_URL as abra installer url"
# ========================================================================
# latest stable release
# ========================================================================
wget -O- https://install.abra.autonomic.zone | bash
~/.local/bin/abra -v
# ========================================================================
# latest rc release
# ========================================================================
wget -O- https://install.abra.autonomic.zone | bash -s -- --rc
~/.local/bin/abra -v
# ========================================================================
# upgrade to stable in-place
# ========================================================================
$ABRA upgrade
~/.local/bin/abra -v
# ========================================================================
# upgrade to rc in-place
# ========================================================================
$ABRA upgrade --rc
~/.local/bin/abra -v
# ========================================================================
# autocomplete
# ========================================================================
$ABRA autocomplete bash
$ABRA autocomplete fizsh
$ABRA autocomplete zsh
# ========================================================================
# record command
# ========================================================================
$ABRA record new -p gandi -t A -n int-core -v 192.157.2.21 coopcloud.tech
$ABRA record list -p gandi coopcloud.tech | grep -q int-core
$ABRA -n record rm -p gandi -t A -n int-core coopcloud.tech
# ========================================================================
# catalogue command
# ========================================================================
$ABRA catalogue generate
$ABRA catalogue generate -s gitea
# ========================================================================
# recipe command
# ========================================================================
$ABRA recipe new testrecipe
$ABRA recipe list
$ABRA recipe list -p cloud
$ABRA recipe versions peertube
$ABRA recipe lint gitea
# ========================================================================
# server command
# ========================================================================
$ABRA -n server new -p hetzner-cloud --hn int-core
$ABRA server ls | grep -q int-core
$ABRA -n server rm -s -p hetzner-cloud --hn int-core
# ========================================================================
# app command
# ========================================================================
$ABRA app ls
$ABRA app ls -S

View File

@ -1,12 +0,0 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test 'wget -O- https://install.abra.autonomic.zone | bash; ~/.local/bin/abra -v'
run_test 'wget -O- https://install.abra.autonomic.zone | bash -s -- --rc; ~/.local/bin/abra -v'
run_test '$ABRA upgrade; ~/.local/bin/abra -v'
run_test '$ABRA upgrade --rc; ~/.local/bin/abra -v'

View File

@ -1,14 +0,0 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA recipe new testrecipe'
run_test '$ABRA recipe list'
run_test '$ABRA recipe list --pattern cloud'
run_test '$ABRA recipe versions peertube'
run_test '$ABRA recipe lint gitea'

View File

@ -1,21 +0,0 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test "$ABRA record new \
--provider gandi \
--record-type A \
--record-name integration-tests \
--record-value 192.157.2.21 \
--no-input coopcloud.tech \
"
run_test '$ABRA record list --provider gandi coopcloud.tech'
run_test "$ABRA record rm \
--provider gandi \
--record-type A \
--record-name integration-tests \
--no-input coopcloud.tech
"

View File

@ -1,10 +0,0 @@
#!/bin/bash
source ./testfunctions.sh
source ./common.sh
run_test '$ABRA server new --provider hetzner-cloud --hetzner-name integration-tests --no-input'
run_test '$ABRA server ls'
run_test '$ABRA server rm --provider hetzner-cloud --hetzner-name int-core --server --no-input'

View File

@ -1,24 +0,0 @@
#!/bin/bash
if [ -z $1 ]; then
echo "usage: ./test_all.sh logdir"
exit
fi
res_dir=$1/
if [[ ! -d "$res_dir" ]]; then
mkdir "$res_dir"
fi
# Usage: run_test [number] [name] [command]
run_test () {
logfile="$res_dir/$1-$2.log"
echo $logfile
}
testScripts=("app.sh" "autocomplete.sh" "catalogue.sh" "install.sh" "recipe.sh" "records.sh" "server.sh", "cmd.sh")
for i in "${testScripts[@]}"; do
cmd="./$i $res_dir${i/sh/log}"
eval $cmd
done

View File

@ -1,35 +0,0 @@
#!/bin/bash
if [ -z $1 ]; then
logfile=/dev/null
else
logfile=$1
fi
if [ -z $logall ]; then
logall=no
fi
run_test () {
if [ -z "$@" ]; then
echo "run_test needs a command to run"
else
tempLogfile=$(mktemp)
cmd=$(eval echo "$@")
echo -e "\\n------------ INPUT -------------------" | tee -a $tempLogfile
echo "$" "$cmd" | tee -a $tempLogfile
echo "------------ OUTPUT ------------------" | tee -a $tempLogfile
eval $cmd 2>&1 | tee -a $tempLogfile
if [ $logall = "yes" ]; then
cat $tempLogfile >> $logfile
echo -e "\\n\\n" >> $logfile
else
read -N 1 -p "Did the test pass? [y/n]: " pass
if [ $pass = 'n' ]; then
cat $tempLogfile >> $logfile
echo -e "\\n\\n" >> $logfile
fi
fi
rm $tempLogfile
fi
}

View File

@ -0,0 +1 @@
TYPE=works

View File

@ -0,0 +1,84 @@
---
# The goal of this compose file is to have a testing ground for understanding
# what cases we need to handle to get stable deployments. For that, we need to
# work with healthchecks and deploy configurations quite closely. If you run
# the `make symlink` target then this will be loaded into a "fake" app on your
# local machine which you can deploy with `abra`.
version: "3.8"
services:
r1_should_work:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 1
healthcheck:
test: redis-cli ping
interval: 2s
retries: 3
start_period: 1s
timeout: 3s
r2_broken_health_check:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 3
healthcheck:
test: foobar
interval: 2s
retries: 3
start_period: 1s
timeout: 3s
r3_no_health_check:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 3
r4_disabled_health_check:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 3
healthcheck:
disable: true
r5_should_also_work:
image: redis:alpine
deploy:
update_config:
failure_action: rollback
order: start-first
rollback_config:
order: start-first
restart_policy:
max_attempts: 1
healthcheck:
test: redis-cli ping
interval: 2s
retries: 3
start_period: 1s
timeout: 3s

View File

@ -0,0 +1 @@
TYPE=works

Some files were not shown because too many files have changed in this diff Show More