Compare commits

..

No commits in common. "main" and "strip-debug-symbols" have entirely different histories.

153 changed files with 4975 additions and 4919 deletions

View File

@ -10,10 +10,10 @@ steps:
- name: make test
image: golang:1.21
environment:
CATL_URL: https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git
ABRA_DIR: "/root/.abra"
commands:
- mkdir -p $HOME/.abra
- git clone $CATL_URL $HOME/.abra/catalogue
- make build-abra
- ./abra help # show version, initialise $ABRA_DIR
- make test
depends_on:
- make check
@ -54,36 +54,11 @@ steps:
tags: dev
registry: git.coopcloud.tech
when:
branch:
- main
event:
exclude:
- pull_request
depends_on:
- make check
- make test
- name: integration test
image: appleboy/drone-ssh
settings:
host:
- int.coopcloud.tech
username: abra
key:
from_secret: abra_int_private_key
port: 22
command_timeout: 60m
script_stop: true
envs: [ DRONE_SOURCE_BRANCH ]
request_pty: true
script:
- |
wget https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
chmod +x run-ci-int
sh run-ci-int
when:
event:
- cron:
cron:
# @daily https://docs.drone.io/cron/
- integration
volumes:
- name: deps

View File

@ -7,7 +7,6 @@
- cassowary
- codegod100
- decentral1se
- fauno
- frando
- kawaiipunk
- knoflook

View File

@ -53,6 +53,3 @@ test:
loc:
@find . -name "*.go" | xargs wc -l
deps:
@go get -t -u ./...

View File

@ -9,6 +9,7 @@ var AppCommand = cli.Command{
Aliases: []string{"a"},
Usage: "Manage apps",
ArgsUsage: "<domain>",
Description: "Functionality for managing the life cycle of your apps",
Subcommands: []cli.Command{
appBackupCommand,
appCheckCommand,
@ -16,6 +17,7 @@ var AppCommand = cli.Command{
appConfigCommand,
appCpCommand,
appDeployCommand,
appErrorsCommand,
appListCommand,
appLogsCommand,
appNewCommand,
@ -29,6 +31,7 @@ var AppCommand = cli.Command{
appServicesCommand,
appUndeployCommand,
appUpgradeCommand,
appVersionCommand,
appVolumeCommand,
},
}

View File

@ -6,7 +6,8 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -46,32 +47,48 @@ var appBackupListCommand = cli.Command{
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if includePath != "" {
log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
logrus.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
}
if err := internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil
@ -93,54 +110,54 @@ var appBackupDownloadCommand = cli.Command{
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if includePath != "" {
log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
logrus.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
}
if err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
remoteBackupDir := "/tmp/backup.tar.gz"
currentWorkingDir := "."
if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
fmt.Println("backup successfully downloaded to current working directory")
@ -163,44 +180,44 @@ var appBackupCreateCommand = cli.Command{
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if resticRepo != "" {
log.Debugf("including RESTIC_REPO=%s in backupbot exec invocation", resticRepo)
logrus.Debugf("including RESTIC_REPO=%s in backupbot exec invocation", resticRepo)
execEnv = append(execEnv, fmt.Sprintf("RESTIC_REPO=%s", resticRepo))
}
if err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil
@ -221,44 +238,44 @@ var appBackupSnapshotsCommand = cli.Command{
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if err := internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil

View File

@ -2,10 +2,12 @@ package app
import (
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -36,16 +38,32 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
tableCol := []string{"recipe env sample", "app env"}
table := formatter.CreateTable(tableCol)
envVars, err := appPkg.CheckEnv(app)
envVars, err := config.CheckEnv(app)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, envVar := range envVars {

View File

@ -5,15 +5,18 @@ import (
"fmt"
"os"
"os/exec"
"path"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -28,11 +31,10 @@ 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.
**WARNING**: options must be passed directly after the sub-command "cmd".
Example:
EXAMPLE:
abra app cmd --local example.com app create_user -- me@example.com`,
abra app cmd example.com app create_user -- me@example.com
`,
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
Flags: []cli.Flag{
internal.DebugFlag,
@ -58,8 +60,24 @@ EXAMPLE:
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if internal.LocalCmd && internal.RemoteUser != "" {
@ -68,11 +86,12 @@ EXAMPLE:
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
log.Fatalf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name)
logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
}
log.Fatal(err)
logrus.Fatal(err)
}
if internal.LocalCmd {
@ -81,11 +100,11 @@ EXAMPLE:
}
cmdName := c.Args().Get(1)
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
log.Debugf("--local detected, running %s on local work station", cmdName)
logrus.Debugf("--local detected, running %s on local work station", cmdName)
var exportEnv string
for k, v := range app.Env {
@ -94,22 +113,22 @@ EXAMPLE:
var sourceAndExec string
if hasCmdArgs {
log.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, app.Recipe.AbraShPath, cmdName, parsedCmdArgs)
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 {
log.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, app.Recipe.AbraShPath, cmdName)
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) {
log.Debugf("%s does not exist locally, use /bin/sh as fallback", shell)
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 {
log.Fatal(err)
logrus.Fatal(err)
}
} else {
if !(len(c.Args()) >= 3) {
@ -119,13 +138,13 @@ EXAMPLE:
targetServiceName := c.Args().Get(1)
cmdName := c.Args().Get(2)
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
matchingServiceName := false
@ -136,24 +155,24 @@ EXAMPLE:
}
if !matchingServiceName {
log.Fatalf("no service %s for %s?", targetServiceName, app.Name)
logrus.Fatalf("no service %s for %s?", targetServiceName, app.Name)
}
log.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
logrus.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
if hasCmdArgs {
log.Debugf("parsed following command arguments: %s", parsedCmdArgs)
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
} else {
log.Debug("did not detect any command arguments")
logrus.Debug("did not detect any command arguments")
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := internal.RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil {
log.Fatal(err)
if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
logrus.Fatal(err)
}
}
@ -209,29 +228,29 @@ var appCmdListCommand = cli.Command{
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err)
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cmdNames, err := getShCmdNames(app)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, cmdName := range cmdNames {
@ -242,8 +261,9 @@ var appCmdListCommand = cli.Command{
},
}
func getShCmdNames(app appPkg.App) ([]string, error) {
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
func getShCmdNames(app config.App) ([]string, error) {
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
cmdNames, err := config.ReadAbraShCmdNames(abraShPath)
if err != nil {
return nil, err
}

View File

@ -6,10 +6,10 @@ import (
"os/exec"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -30,24 +30,24 @@ var appConfigCommand = cli.Command{
internal.ShowSubcommandHelpAndError(c, errors.New("no app provided"))
}
files, err := appPkg.LoadAppFiles("")
files, err := config.LoadAppFiles("")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
appFile, exists := files[appName]
if !exists {
log.Fatalf("cannot find app with name %s", appName)
logrus.Fatalf("cannot find app with name %s", appName)
}
ed, ok := os.LookupEnv("EDITOR")
if !ok {
edPrompt := &survey.Select{
Message: "which editor do you wish to use?",
Message: "Which editor do you wish to use?",
Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"},
}
if err := survey.AskOne(edPrompt, &ed); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
@ -56,7 +56,7 @@ var appConfigCommand = cli.Command{
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil

View File

@ -15,13 +15,13 @@ import (
"coopcloud.tech/abra/pkg/client"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -50,34 +50,30 @@ And if you want to copy that file back to your current working directory locally
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
src := c.Args().Get(1)
dst := c.Args().Get(2)
if src == "" {
log.Fatal("missing <src> argument")
logrus.Fatal("missing <src> argument")
}
if dst == "" {
log.Fatal("missing <dest> argument")
logrus.Fatal("missing <dest> argument")
}
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
if toContainer {
err = CopyToContainer(cl, container.ID, srcPath, dstPath)
@ -85,7 +81,7 @@ And if you want to copy that file back to your current working directory locally
err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
}
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil
@ -171,7 +167,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
return err
}
log.Debugf("copy %s from local to %s on container", srcPath, dstPath)
logrus.Debugf("copy %s from local to %s on container", srcPath, dstPath)
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
return err

View File

@ -2,19 +2,21 @@ package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/secret"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -33,147 +35,156 @@ var appDeployCommand = cli.Command{
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `Deploy an app.
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 supports chaos operations. Use "--chaos" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported values for
"[<version>]". Please note, "upgrade"/"rollback" do not support chaos
operations.
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.
EXAMPLE:
abra app deploy foo.example.com
abra app deploy foo.example.com 1.2.3+3.2.1
abra app deploy foo.example.com 1e83340e`,
Chaos 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
recipes.
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
specificVersion := c.Args().Get(1)
if specificVersion == "" {
specificVersion = app.Recipe.Version
}
if specificVersion != "" && internal.Chaos {
log.Fatal("cannot use <version> and --chaos together")
logrus.Fatal("cannot use <version> and --chaos together")
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err)
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
}
// NOTE(d1): handles "<version> as git hash" use case
var isChaosCommit bool
// NOTE(d1): check out specific version before dealing with secrets. This
// is because we need to deal with GetComposeFiles under the hood and these
// files change from version to version which therefore affects which
// secrets might be generated
version := deployMeta.Version
if specificVersion != "" {
version = specificVersion
log.Debugf("choosing %s as version to deploy", version)
var err error
isChaosCommit, err = app.Recipe.EnsureVersion(version)
if err != nil {
log.Fatal(err)
}
if isChaosCommit {
log.Debugf("assuming '%s' is a chaos commit", version)
internal.Chaos = true
}
logrus.Fatal(err)
}
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, secStat := range secStats {
if !secStat.CreatedOnRemote {
log.Fatalf("unable to deploy, secrets not generated (%s)?", secStat.LocalName)
logrus.Fatalf("unable to deploy, secrets not generated (%s)?", secStat.LocalName)
}
}
if deployMeta.IsDeployed {
if isDeployed {
if internal.Force || internal.Chaos {
log.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
} else {
log.Fatalf("%s is already deployed", app.Name)
logrus.Fatalf("%s is already deployed", app.Name)
}
}
version := deployedVersion
if specificVersion != "" {
version = specificVersion
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
}
if !internal.Chaos && specificVersion == "" {
versions, err := app.Recipe.Tags()
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 && !internal.Chaos {
logrus.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline)
if err != nil {
logrus.Warn(err)
}
for _, recipeVersion := range recipeVersions {
for version := range recipeVersion {
versions = append(versions, version)
}
}
}
if len(versions) > 0 && !internal.Chaos {
version = versions[len(versions)-1]
log.Debugf("choosing %s as version to deploy", version)
if _, err := app.Recipe.EnsureVersion(version); err != nil {
log.Fatal(err)
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
} else {
head, err := app.Recipe.Head()
head, err := git.GetRecipeHead(app.Recipe)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
version = formatter.SmallSHA(head.String())
log.Warn("no versions detected, using latest commit")
logrus.Warn("no versions detected, using latest commit")
}
}
chaosVersion := "false"
if internal.Chaos {
log.Warnf("chaos mode engaged")
if isChaosCommit {
chaosVersion = specificVersion
versionLabelLocal, err := app.Recipe.GetVersionLabelLocal()
if err != nil {
log.Fatal(err)
}
version = versionLabelLocal
} else {
logrus.Warnf("chaos mode engaged")
var err error
chaosVersion, err = app.Recipe.ChaosVersion()
version, err = recipe.ChaosVersion(app.Recipe)
if err != nil {
log.Fatal(err)
}
logrus.Fatal(err)
}
}
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployOpts := stack.Deploy{
@ -181,69 +192,61 @@ EXAMPLE:
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Detach: false,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, chaosVersion)
appPkg.SetUpdateLabel(compose, stackName, app.Env)
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetChaosVersionLabel(compose, stackName, version)
config.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app)
envVars, err := config.CheckEnv(app)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, envVar := range envVars {
if !envVar.Present {
log.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
logrus.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
}
}
if err := internal.DeployOverview(app, version, chaosVersion, "continue with deployment?"); err != nil {
log.Fatal(err)
if err := internal.DeployOverview(app, version, "continue with deployment?"); err != nil {
logrus.Fatal(err)
}
if !internal.NoDomainChecks {
domainName, ok := app.Env["DOMAIN"]
if ok {
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
} else {
log.Warn("skipping domain checks as no DOMAIN=... configured for app")
logrus.Warn("skipping domain checks as no DOMAIN=... configured for app")
}
} else {
log.Warn("skipping domain checks as requested")
logrus.Warn("skipping domain checks as requested")
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
if ok && !internal.DontWaitConverge {
log.Debugf("run the following post-deploy commands: %s", postDeployCmds)
logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err)
}
}
if app.Recipe.Version != "" && specificVersion != "" && specificVersion != app.Recipe.Version {
err := app.WriteRecipeVersion(specificVersion)
if err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
}
}
return nil

142
cli/app/errors.go Normal file
View File

@ -0,0 +1,142 @@
package app
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"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"
)
var appErrorsCommand = cli.Command{
Name: "errors",
Usage: "List errors for a deployed app",
ArgsUsage: "<domain>",
Description: `
List 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
is failing to deploy or having issues, it could be a lot of things.
This command currently takes into account:
Is the service deployed?
Is the service killed by an OOM error?
Is the service reporting an error (like in "ps --no-trunc" output)
Is the service healthcheck failing? what are the healthcheck logs?
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
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,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
if !internal.Watch {
if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err)
}
return nil
}
for {
if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err)
}
time.Sleep(2 * time.Second)
}
},
}
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
recipe, err := recipe.Get(app.Recipe, internal.Offline)
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})
if err != nil {
return err
}
if len(containers) == 0 {
logrus.Warnf("%s is not up, something seems wrong", service.Name)
continue
}
container := containers[0]
containerState, err := cl.ContainerInspect(context.Background(), container.ID)
if err != nil {
logrus.Fatal(err)
}
if containerState.State.OOMKilled {
logrus.Warnf("%s has been killed due to an out of memory error", service.Name)
}
if containerState.State.Error != "" {
logrus.Warnf("%s reports this error: %s", service.Name, containerState.State.Error)
}
if containerState.State.Health != nil {
if containerState.State.Health.Status != "healthy" {
logrus.Warnf("%s healthcheck status is %s", service.Name, containerState.State.Health.Status)
logrus.Warnf("%s healthcheck has failed %s times", service.Name, strconv.Itoa(containerState.State.Health.FailingStreak))
for _, log := range containerState.State.Health.Log {
logrus.Warnf("%s healthcheck logs: %s", service.Name, strings.TrimSpace(log.Output))
}
}
}
}
return nil
}
func getServiceName(names []string) string {
containerName := strings.Join(names, " ")
trimmed := strings.TrimPrefix(containerName, "/")
return strings.Split(trimmed, ".")[0]
}

View File

@ -8,41 +8,36 @@ import (
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var (
status bool
statusFlag = &cli.BoolFlag{
var status bool
var statusFlag = &cli.BoolFlag{
Name: "status, S",
Usage: "Show app deployment status",
Destination: &status,
}
)
}
var (
recipeFilter string
recipeFlag = &cli.StringFlag{
var recipeFilter string
var recipeFlag = &cli.StringFlag{
Name: "recipe, r",
Value: "",
Usage: "Show apps of a specific recipe",
Destination: &recipeFilter,
}
)
}
var (
listAppServer string
listAppServerFlag = &cli.StringFlag{
var listAppServer string
var listAppServerFlag = &cli.StringFlag{
Name: "server, s",
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
}
)
}
type appStatus struct {
Server string `json:"server"`
@ -76,7 +71,8 @@ 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.`,
can take some time.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.MachineReadableFlag,
@ -87,19 +83,20 @@ can take some time.`,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
appFiles, err := appPkg.LoadAppFiles(listAppServer)
appFiles, err := config.LoadAppFiles(listAppServer)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
apps, err := appPkg.GetApps(appFiles, recipeFilter)
apps, err := config.GetApps(appFiles, recipeFilter)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
sort.Sort(appPkg.ByServerAndRecipe(apps))
sort.Sort(config.ByServerAndRecipe(apps))
statuses := make(map[string]map[string]string)
var catl recipe.RecipeCatalogue
if status {
alreadySeen := make(map[string]bool)
for _, app := range apps {
@ -108,9 +105,14 @@ can take some time.`,
}
}
statuses, err = appPkg.GetAppStatuses(apps, internal.MachineReadable)
statuses, err = config.GetAppStatuses(apps, internal.MachineReadable)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
catl, err = recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
}
}
@ -128,7 +130,7 @@ can take some time.`,
}
}
if app.Recipe.Name == recipeFilter || recipeFilter == "" {
if app.Recipe == recipeFilter || recipeFilter == "" {
if recipeFilter != "" {
// only count server if matches filter
totalServersCount++
@ -175,20 +177,20 @@ can take some time.`,
var newUpdates []string
if version != "unknown" {
updates, err := app.Recipe.Tags()
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, update := range updates {
parsedUpdate, err := tagcmp.Parse(update)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
@ -212,7 +214,7 @@ can take some time.`,
}
appStats.Server = app.Server
appStats.Recipe = app.Recipe.Name
appStats.Recipe = app.Recipe
appStats.AppName = app.Name
appStats.Domain = app.Domain
@ -224,7 +226,7 @@ can take some time.`,
if internal.MachineReadable {
jsonstring, err := json.Marshal(allStats)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
} else {
fmt.Println(string(jsonstring))
}
@ -253,7 +255,7 @@ can take some time.`,
if chaosStatus != "unknown" {
chaosEnabled, err := strconv.ParseBool(chaosStatus)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if chaosEnabled && appStat.ChaosVersion != "unknown" {
chaosStatus = appStat.ChaosVersion

View File

@ -9,16 +9,16 @@ import (
"time"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -38,22 +38,22 @@ var appLogsCommand = cli.Command{
app := internal.ValidateApp(c)
stackName := app.StackName()
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
serviceName := c.Args().Get(1)
@ -63,7 +63,7 @@ var appLogsCommand = cli.Command{
}
err = tailLogs(cl, app, serviceNames)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil
@ -73,7 +73,7 @@ var appLogsCommand = cli.Command{
// tailLogs prints logs for the given app with optional service names to be
// filtered on. It also checks if the latest task is not runnning and then
// prints the past tasks.
func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) error {
func tailLogs(cl *dockerClient.Client, app config.App, serviceNames []string) error {
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
return err
@ -101,7 +101,7 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
lastTask := tasks[0].Status
if lastTask.State != swarm.TaskStateRunning {
for _, task := range tasks {
log.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err)
logrus.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err)
}
}
}
@ -110,7 +110,7 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
// collected in parallel.
wg.Add(1)
go func(serviceID string) {
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
logs, err := cl.ServiceLogs(context.Background(), serviceID, types.ContainerLogsOptions{
ShowStderr: true,
ShowStdout: !internal.StdErrOnly,
Since: internal.SinceLogs,
@ -121,13 +121,13 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
Details: false,
})
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
log.Fatal(err)
logrus.Fatal(err)
}
}(service.ID)
}

View File

@ -2,25 +2,26 @@ package app
import (
"fmt"
"path"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appNewDescription = `
Creates a new app from a default recipe. This new app configuration is stored
in your $ABRA_DIR directory under the appropriate server.
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 does not deploy your app for you. You will need to run "abra app
deploy <domain>" to do so.
@ -35,7 +36,8 @@ store them somewhere safe.
You can use the "--pass/-P" to store these generated passwords locally in a
pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.`
on your $PATH.
`
var appNewCommand = cli.Command{
Name: "new",
@ -67,63 +69,43 @@ var appNewCommand = cli.Command{
recipe := internal.ValidateRecipe(c)
if !internal.Chaos {
if err := recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
if c.Args().Get(1) == "" {
var version string
recipeVersions, err := recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
// NOTE(d1): determine whether recipe versions exist or not and check
// out the latest version or current HEAD
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
version = tag
}
if _, err := recipe.EnsureVersion(version); err != nil {
log.Fatal(err)
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
} else {
if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
} else {
if _, err := recipe.EnsureVersion(c.Args().Get(1)); err != nil {
log.Fatal(err)
if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil {
logrus.Fatal(err)
}
}
}
if err := ensureServerFlag(); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
sanitisedAppName := appPkg.SanitiseAppName(internal.Domain)
log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
sanitisedAppName := config.SanitiseAppName(internal.Domain)
logrus.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
if err := appPkg.TemplateAppEnvSample(
recipe,
if err := config.TemplateAppEnvSample(
recipe.Name,
internal.Domain,
internal.NewAppServer,
internal.Domain,
); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
var secrets AppSecrets
@ -131,31 +113,32 @@ var appNewCommand = cli.Command{
if internal.Secrets {
sampleEnv, err := recipe.SampleEnv()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
composeFiles, err := recipe.GetComposeFiles(sampleEnv)
composeFiles, err := config.GetComposeFiles(recipe.Name, sampleEnv)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
secretsConfig, err := secret.ReadSecretsConfig(recipe.SampleEnvPath, composeFiles, appPkg.StackName(internal.Domain))
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, config.StackName(internal.Domain))
if err != nil {
return err
}
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
cl, err := client.New(internal.NewAppServer)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
secretCols := []string{"Name", "Value"}
@ -173,25 +156,22 @@ var appNewCommand = cli.Command{
table := formatter.CreateTable(tableCol)
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
log.Infof("new app '%s' created 🌞", recipe.Name)
fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
fmt.Println("")
table.Render()
fmt.Println("")
fmt.Println("Configure this app:")
fmt.Println("You can configure this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain))
fmt.Println("")
fmt.Println("Deploy this app:")
fmt.Println("You can deploy this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
if len(secrets) > 0 {
fmt.Println("")
fmt.Println("Generated secrets:")
fmt.Println("Here are your generated secrets:")
fmt.Println("")
secretTable.Render()
log.Warn("generated secrets are not shown again, please take note of them NOW")
logrus.Warn("generated secrets are not shown again, please take note of them NOW")
}
return nil
@ -205,7 +185,7 @@ type AppSecrets map[string]string
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
// NOTE(d1): trim to match app.StackName() implementation
if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH {
log.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH])
logrus.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH])
sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]
}
@ -232,7 +212,7 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr
}
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
func ensureDomainFlag(recipe recipe.Recipe, server string) error {
if internal.Domain == "" && !internal.NoInput {
prompt := &survey.Input{
Message: "Specify app domain",
@ -253,7 +233,7 @@ func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
// promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
if len(secretsConfig) == 0 {
log.Debugf("%s has no secrets to generate, skipping...", recipeName)
logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
return nil
}

View File

@ -2,21 +2,21 @@ package app
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
abraService "coopcloud.tech/abra/pkg/service"
"coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/buger/goterm"
dockerFormatter "github.com/docker/cli/cli/command/formatter"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -25,145 +25,77 @@ var appPsCommand = cli.Command{
Aliases: []string{"p"},
Usage: "Check app status",
ArgsUsage: "<domain>",
Description: "Show status of a deployed app.",
Description: "Show a more detailed status output of a specific deployed app",
Flags: []cli.Flag{
internal.MachineReadableFlag,
internal.WatchFlag,
internal.DebugFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(false, false); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
chaosVersion := "false"
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok {
isChaos, exists := statusMeta["chaos"]
if exists && isChaos == "false" {
if _, err := app.Recipe.EnsureVersion(deployMeta.Version); err != nil {
log.Fatal(err)
}
} else {
chaosVersion, err = app.Recipe.ChaosVersion()
if err != nil {
log.Fatal(err)
}
}
}
showPSOutput(app, cl, deployMeta.Version, chaosVersion)
if !internal.Watch {
showPSOutput(c, app, cl)
return nil
}
goterm.Clear()
for {
goterm.MoveCursor(1, 1)
showPSOutput(c, app, cl)
goterm.Flush()
time.Sleep(2 * time.Second)
}
},
}
// showPSOutput renders ps output.
func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chaosVersion string) {
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
filters, err := app.Filters(true, true)
if err != nil {
log.Fatal(err)
return
logrus.Fatal(err)
}
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: app.StackName(),
Prune: false,
ResolveImage: stack.ResolveImageAlways,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil {
log.Fatal(err)
return
logrus.Fatal(err)
}
var tablerows [][]string
allContainerStats := make(map[string]map[string]string)
for _, service := range compose.Services {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
if err != nil {
log.Fatal(err)
return
}
var containerStats map[string]string
if len(containers) == 0 {
containerStats = map[string]string{
"version": deployedVersion,
"chaos": chaosVersion,
"service": service.Name,
"image": "unknown",
"created": "unknown",
"status": "unknown",
"state": "unknown",
"ports": "unknown",
}
} else {
container := containers[0]
containerStats = map[string]string{
"version": deployedVersion,
"chaos": chaosVersion,
"service": abraService.ContainerToServiceName(container.Names, app.StackName()),
"image": formatter.RemoveSha(container.Image),
"created": formatter.HumanDuration(container.Created),
"status": container.Status,
"state": container.State,
"ports": dockerFormatter.DisplayablePorts(container.Ports),
}
}
allContainerStats[containerStats["service"]] = containerStats
tablerow := []string{
deployedVersion,
chaosVersion,
containerStats["service"],
containerStats["image"],
containerStats["created"],
containerStats["status"],
containerStats["state"],
containerStats["ports"],
}
tablerows = append(tablerows, tablerow)
}
if internal.MachineReadable {
jsonstring, err := json.Marshal(allContainerStats)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(jsonstring))
return
}
tableCol := []string{"version", "chaos", "service", "image", "created", "status", "state", "ports"}
tableCol := []string{"service name", "image", "created", "status", "state", "ports"}
table := formatter.CreateTable(tableCol)
for _, row := range tablerows {
table.Append(row)
for _, container := range containers {
var containerNames []string
for _, containerName := range container.Names {
trimmed := strings.TrimPrefix(containerName, "/")
containerNames = append(containerNames, trimmed)
}
table.SetAutoMergeCellsByColumnIndex([]int{0, 1})
tableRow := []string{
service.ContainerToServiceName(container.Names, app.StackName()),
formatter.RemoveSha(container.Image),
formatter.HumanDuration(container.Created),
container.Status,
container.State,
dockerFormatter.DisplayablePorts(container.Ports),
}
table.Append(tableRow)
}
table.Render()
}

View File

@ -3,15 +3,16 @@ package app
import (
"context"
"fmt"
"log"
"os"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -36,7 +37,8 @@ Please note, if you delete the local app env file without removing volumes and
secrets first, Abra will *not* be able to help you remove them afterwards.
To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
flag.`,
flag.
`,
Flags: []cli.Flag{
internal.ForceFlag,
internal.DebugFlag,
@ -53,34 +55,34 @@ flag.`,
msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?"
prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
if err := survey.AskOne(prompt, &response); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !response {
log.Fatal("aborting as requested")
logrus.Fatal("aborting as requested")
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if deployMeta.IsDeployed {
log.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
if isDeployed {
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
}
fs, err := app.Filters(false, false)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
secrets := make(map[string]string)
@ -95,22 +97,22 @@ flag.`,
for _, name := range secretNames {
err := cl.SecretRemove(context.Background(), secrets[name])
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Info(fmt.Sprintf("secret: %s removed", name))
logrus.Info(fmt.Sprintf("secret: %s removed", name))
}
} else {
log.Info("no secrets to remove")
logrus.Info("no secrets to remove")
}
fs, err = app.Filters(false, true)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, fs)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
volumeNames := client.GetVolumeNames(volumeList)
@ -120,16 +122,16 @@ flag.`,
log.Fatalf("removing volumes failed: %s", err)
}
log.Infof("%d volumes removed successfully", len(volumeNames))
logrus.Infof("%d volumes removed successfully", len(volumeNames))
} else {
log.Info("no volumes to remove")
logrus.Info("no volumes to remove")
}
if err = os.Remove(app.Path); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Info(fmt.Sprintf("file: %s removed", app.Path))
logrus.Info(fmt.Sprintf("file: %s removed", app.Path))
return nil
},

View File

@ -6,12 +6,11 @@ import (
"fmt"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -19,92 +18,62 @@ var appRestartCommand = cli.Command{
Name: "restart",
Aliases: []string{"re"},
Usage: "Restart an app",
ArgsUsage: "<domain> [<service>]",
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.AllServicesFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command restarts services within a deployed app.
Run "abra app ps <domain>" to see a list of service names.
Pass "--all-services/-a" to restart all services.
EXAMPLE:
abra app restart example.com app`,
Description: `This command restarts a service within a deployed app.`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(false, false); err != nil {
log.Fatal(err)
}
serviceName := c.Args().Get(1)
if serviceName == "" && !internal.AllServices {
err := errors.New("missing <service>")
serviceNameShort := c.Args().Get(1)
if serviceNameShort == "" {
err := errors.New("missing service?")
internal.ShowSubcommandHelpAndError(c, err)
}
if serviceName != "" && internal.AllServices {
log.Fatal("cannot use <service> and --all-services together")
}
var serviceNames []string
if internal.AllServices {
var err error
serviceNames, err = appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
} else {
serviceNames = append(serviceNames, serviceName)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
for _, serviceName := range serviceNames {
stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
serviceName := fmt.Sprintf("%s_%s", app.StackName(), serviceNameShort)
log.Debugf("attempting to scale %s to 0", stackServiceName)
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil {
log.Fatal(err)
logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 0); err != nil {
logrus.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil {
log.Fatal(err)
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}
log.Debugf("%s has been scaled to 0", stackServiceName)
log.Debugf("attempting to scale %s to 1", stackServiceName)
logrus.Debugf("%s has been scaled to 0 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 1); err != nil {
log.Fatal(err)
logrus.Debugf("attempting to scale %s to 1 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 1); err != nil {
logrus.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil {
log.Fatal(err)
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}
log.Debugf("%s has been scaled to 1", stackServiceName)
log.Infof("%s service successfully restarted", serviceName)
}
logrus.Debugf("%s has been scaled to 1 (restart logic)", serviceName)
logrus.Infof("%s service successfully restarted", serviceNameShort)
return nil
},

View File

@ -6,7 +6,8 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -31,32 +32,49 @@ var appRestoreCommand = cli.Command{
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if targetPath != "" {
log.Debugf("including TARGET=%s in backupbot exec invocation", targetPath)
logrus.Debugf("including TARGET=%s in backupbot exec invocation", targetPath)
execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
}
if err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil

View File

@ -4,17 +4,17 @@ import (
"context"
"fmt"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -27,195 +27,212 @@ var appRollbackCommand = cli.Command{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
This command rolls an app back to a previous version.
This command rolls an app back to a previous version if one exists.
Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[<version>]".
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.
A rollback can be destructive, please ensure you have a copy of your app data
beforehand.
This action could be destructive, please ensure you have a copy of your app
data beforehand.
EXAMPLE:
abra app rollback foo.example.com
abra app rollback foo.example.com 1.2.3+3.2.1`,
Chaos 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
recipes.
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
specificVersion := c.Args().Get(1)
if specificVersion != "" && internal.Chaos {
logrus.Fatal("cannot use <version> and --chaos together")
}
if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
logrus.Debugf("checking whether %s is already deployed", stackName)
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
versions, err := app.Recipe.Tags()
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 && !internal.Chaos {
logrus.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline)
if err != nil {
logrus.Warn(err)
}
for _, recipeVersion := range recipeVersions {
for version := range recipeVersion {
versions = append(versions, version)
}
}
}
var availableDowngrades []string
if deployMeta.Version == "unknown" {
if deployedVersion == "unknown" {
availableDowngrades = versions
log.Warnf("failed to determine deployed version of %s", app.Name)
logrus.Warnf("failed to determine deployed version of %s", app.Name)
}
specificVersion := c.Args().Get(1)
if specificVersion == "" {
specificVersion = app.Recipe.Version
}
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
logrus.Fatal(err)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
logrus.Fatal(err)
}
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) {
log.Fatalf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) {
logrus.Fatalf("%s is not a downgrade for %s?", deployedVersion, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
log.Fatalf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
}
availableDowngrades = append(availableDowngrades, specificVersion)
}
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
log.Warn("attempting to rollback a chaos deployment")
}
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if parsedVersion.IsLessThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableDowngrades = append(availableDowngrades, version)
}
}
if len(availableDowngrades) == 0 && !internal.Force {
log.Info("no available downgrades")
logrus.Info("no available downgrades, you're on oldest ✌️")
return nil
}
}
var chosenDowngrade string
if len(availableDowngrades) > 0 {
if len(availableDowngrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput || specificVersion != "" {
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
log.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade)
logrus.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade)
} else {
msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
msg = fmt.Sprintf("please select a downgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion)
}
prompt := &survey.Select{
Message: msg,
Message: fmt.Sprintf("Please select a downgrade (current version: %s):", deployedVersion),
Options: internal.ReverseStringList(availableDowngrades),
}
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return err
}
}
}
log.Debugf("choosing %s as version to rollback", chosenDowngrade)
if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
log.Fatal(err)
if !internal.Chaos {
if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil {
logrus.Fatal(err)
}
}
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenDowngrade, err = recipe.ChaosVersion(app.Recipe)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Detach: false,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
log.Fatal(err)
}
appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
appPkg.SetUpdateLabel(compose, stackName, app.Env)
chaosVersion := "false"
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
logrus.Fatal(err)
}
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
config.SetUpdateLabel(compose, stackName, app.Env)
// NOTE(d1): no release notes implemeneted for rolling back
if err := internal.NewVersionOverview(app, deployMeta.Version, chaosVersion, chosenDowngrade, ""); err != nil {
log.Fatal(err)
if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {
logrus.Fatal(err)
}
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
log.Fatal(err)
}
if app.Recipe.Version != "" {
err := app.WriteRecipeVersion(chosenDowngrade)
if err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
logrus.Fatal(err)
}
return nil

View File

@ -9,11 +9,11 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/log"
"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/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -55,7 +55,7 @@ var appRunCommand = cli.Command{
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
serviceName := c.Args().Get(1)
@ -65,7 +65,7 @@ var appRunCommand = cli.Command{
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
cmd := c.Args()[2:]
@ -88,11 +88,11 @@ var appRunCommand = cli.Command{
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil

View File

@ -6,17 +6,17 @@ import (
"fmt"
"os"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -55,8 +55,25 @@ var appSecretGenerateCommand = cli.Command{
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if len(c.Args()) == 1 && !allSecrets {
@ -69,14 +86,14 @@ var appSecretGenerateCommand = cli.Command{
internal.ShowSubcommandHelpAndError(c, err)
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !allSecrets {
@ -84,7 +101,7 @@ var appSecretGenerateCommand = cli.Command{
secretVersion := c.Args().Get(2)
s, ok := secrets[secretName]
if !ok {
log.Fatalf("%s doesn't exist in the env config?", secretName)
logrus.Fatalf("%s doesn't exist in the env config?", secretName)
}
s.Version = secretVersion
secrets = map[string]secret.Secret{
@ -94,24 +111,24 @@ var appSecretGenerateCommand = cli.Command{
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if internal.Pass {
for name, data := range secretVals {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
}
if len(secretVals) == 0 {
log.Warn("no secrets generated")
logrus.Warn("no secrets generated")
os.Exit(1)
}
@ -126,7 +143,7 @@ var appSecretGenerateCommand = cli.Command{
} else {
table.Render()
}
log.Warn("generated secrets are not shown again, please take note of them NOW")
logrus.Warn("generated secrets are not shown again, please take note of them NOW")
return nil
},
@ -139,8 +156,6 @@ var appSecretInsertCommand = cli.Command{
Flags: []cli.Flag{
internal.DebugFlag,
internal.PassFlag,
internal.FileFlag,
internal.TrimFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>",
@ -166,35 +181,23 @@ Example:
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
name := c.Args().Get(1)
version := c.Args().Get(2)
data := c.Args().Get(3)
if internal.File {
raw, err := os.ReadFile(data)
if err != nil {
log.Fatalf("reading secret from file: %s", err)
}
data = string(raw)
}
if internal.Trim {
data = strings.TrimSpace(data)
}
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Infof("%s successfully stored on server", secretName)
logrus.Infof("%s successfully stored on server", secretName)
if internal.Pass {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
@ -203,19 +206,19 @@ Example:
}
// secretRm removes a secret.
func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string) error {
func secretRm(cl *dockerClient.Client, app config.App, secretName, parsed string) error {
if err := cl.SecretRemove(context.Background(), secretName); err != nil {
return err
}
log.Infof("deleted %s successfully from server", secretName)
logrus.Infof("deleted %s successfully from server", secretName)
if internal.PassRemove {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err
}
log.Infof("deleted %s successfully from local pass store", secretName)
logrus.Infof("deleted %s successfully from local pass store", secretName)
}
return nil
@ -245,18 +248,35 @@ Example:
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if c.Args().Get(1) != "" && rmAllSecrets {
@ -269,17 +289,17 @@ Example:
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
filters, err := app.Filters(false, false)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
remoteSecretNames := make(map[string]bool)
@ -295,7 +315,7 @@ Example:
if secretToRm != "" {
if secretName == secretToRm {
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil
@ -304,18 +324,18 @@ Example:
match = true
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
}
}
if !match && secretToRm != "" {
log.Fatalf("%s doesn't exist on server?", secretToRm)
logrus.Fatalf("%s doesn't exist on server?", secretToRm)
}
if !match {
log.Fatal("no secrets to remove?")
logrus.Fatal("no secrets to remove?")
}
return nil
@ -336,13 +356,30 @@ var appSecretLsCommand = cli.Command{
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
@ -350,7 +387,7 @@ var appSecretLsCommand = cli.Command{
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, secStat := range secStats {
@ -370,7 +407,7 @@ var appSecretLsCommand = cli.Command{
table.Render()
}
} else {
log.Warnf("no secrets stored for %s", app.Name)
logrus.Warnf("no secrets stored for %s", app.Name)
}
return nil

View File

@ -9,10 +9,10 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -28,32 +28,29 @@ var appServicesCommand = cli.Command{
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
filters, err := app.Filters(true, true)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
tableCol := []string{"service name", "image"}

View File

@ -3,16 +3,17 @@ package app
import (
"context"
"fmt"
"time"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -27,10 +28,27 @@ var pruneFlag = &cli.BoolFlag{
// pruneApp runs the equivalent of a "docker system prune" but only filtering
// against resources connected with the app deployment. It is not a system wide
// prune. Volumes are not pruned to avoid unwated data loss.
func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
func pruneApp(c *cli.Context, cl *dockerClient.Client, app config.App) error {
stackName := app.StackName()
ctx := context.Background()
for {
logrus.Debugf("polling for %s stack, waiting to be undeployed...", stackName)
services, err := stack.GetStackServices(ctx, cl, stackName)
if err != nil {
return err
}
if len(services) == 0 {
logrus.Debugf("%s undeployed, moving on with pruning logic", stackName)
time.Sleep(time.Second) // give runtime more time to tear down related state
break
}
time.Sleep(time.Second)
}
pruneFilters := filters.NewArgs()
stackSearch := fmt.Sprintf("%s*", stackName)
pruneFilters.Add("label", stackSearch)
@ -40,14 +58,14 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, pruneFilters)
if err != nil {
return err
}
log.Infof("networks pruned: %d", len(nr.NetworksDeleted))
logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil {
@ -55,7 +73,7 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
return nil
}
@ -78,50 +96,40 @@ This does not destroy any of the application data.
However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed.
Passing "-p/--prune" does not remove those volumes.`,
Passing "-p/--prune" does not remove those volumes.
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
stackName := app.StackName()
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
logrus.Debugf("checking whether %s is already deployed", stackName)
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
chaosVersion := "false"
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
logrus.Fatal(err)
}
if err := internal.DeployOverview(app, deployMeta.Version, chaosVersion, "continue with undeploy?"); err != nil {
log.Fatal(err)
}
rmOpts := stack.Remove{
Namespaces: []string{app.StackName()},
Detach: false,
}
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if prune {
if err := pruneApp(cl, app); err != nil {
log.Fatal(err)
if err := pruneApp(c, cl, app); err != nil {
logrus.Fatal(err)
}
}

View File

@ -5,15 +5,16 @@ import (
"fmt"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -26,101 +27,135 @@ var appUpgradeCommand = cli.Command{
internal.DebugFlag,
internal.NoInputFlag,
internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
internal.OfflineFlag,
internal.ReleaseNotesFlag,
},
Before: internal.SubCommandBefore,
Description: `
Upgrade an app.
Upgrade an app. You can use it to choose and roll out a new upgrade to an
existing app.
Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[<version>]".
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
deployed app.
An upgrade can be destructive, please ensure you have a copy of your app data
beforehand.
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.
EXAMPLE:
This action could be destructive, please ensure you have a copy of your app
data beforehand.
abra app upgrade foo.example.com
abra app upgrade foo.example.com 1.2.3+3.2.1`,
Chaos 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
recipes.
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
specificVersion := c.Args().Get(1)
if specificVersion != "" && internal.Chaos {
logrus.Fatal("cannot use <version> and --chaos together")
}
if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err)
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
recipe, err := recipePkg.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(recipe); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
versions, err := app.Recipe.Tags()
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
versions, err := recipePkg.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 && !internal.Chaos {
logrus.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipePkg.GetRecipeVersions(app.Recipe, internal.Offline)
if err != nil {
logrus.Warn(err)
}
for _, recipeVersion := range recipeVersions {
for version := range recipeVersion {
versions = append(versions, version)
}
}
}
var availableUpgrades []string
if deployMeta.Version == "unknown" {
if deployedVersion == "unknown" {
availableUpgrades = versions
log.Warnf("failed to determine deployed version of %s", app.Name)
logrus.Warnf("failed to determine deployed version of %s", app.Name)
}
specificVersion := c.Args().Get(1)
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
logrus.Fatal(err)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
logrus.Fatal(err)
}
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) {
log.Fatalf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) {
logrus.Fatalf("%s is not an upgrade for %s?", deployedVersion, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
log.Fatalf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
availableUpgrades = append(availableUpgrades, specificVersion)
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
log.Fatal(err)
}
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
log.Warn("attempting to upgrade a chaos deployment")
logrus.Fatal(err)
}
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableUpgrades = append(availableUpgrades, version)
@ -128,27 +163,21 @@ EXAMPLE:
}
if len(availableUpgrades) == 0 && !internal.Force {
log.Info("no available upgrades")
logrus.Infof("no available upgrades, you're on latest (%s) ✌️", deployedVersion)
return nil
}
}
var chosenUpgrade string
if len(availableUpgrades) > 0 {
if len(availableUpgrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput || specificVersion != "" {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
log.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
} else {
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
msg = fmt.Sprintf("please select an upgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion)
}
prompt := &survey.Select{
Message: msg,
Message: fmt.Sprintf("Please select an upgrade (current version: %s):", deployedVersion),
Options: internal.ReverseStringList(availableUpgrades),
}
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return err
}
@ -156,26 +185,26 @@ EXAMPLE:
}
if internal.Force && chosenUpgrade == "" {
log.Warnf("%s is already upgraded to latest but continuing (--force)", app.Name)
chosenUpgrade = deployMeta.Version
logrus.Warnf("%s is already upgraded to latest but continuing (--force/--chaos)", app.Name)
chosenUpgrade = deployedVersion
}
// 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
var releaseNotes string
if chosenUpgrade != "" {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
log.Fatal(err)
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version)
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
logrus.Fatal(err)
}
if !(parsedVersion.Equals(parsedDeployedVersion)) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := internal.GetReleaseNotes(app.Recipe, version)
if err != nil {
return err
}
@ -184,93 +213,81 @@ EXAMPLE:
}
}
}
if !internal.Chaos {
if err := recipePkg.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
logrus.Fatal(err)
}
}
log.Debugf("choosing %s as version to upgrade", chosenUpgrade)
if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
log.Fatal(err)
}
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
}
for k, v := range abraShEnv {
app.Env[k] = v
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Detach: false,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
config.SetUpdateLabel(compose, stackName, app.Env)
appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
appPkg.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app)
envVars, err := config.CheckEnv(app)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, envVar := range envVars {
if !envVar.Present {
log.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
logrus.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
}
}
if internal.ReleaseNotes {
fmt.Println()
fmt.Print(releaseNotes)
return nil
if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
logrus.Fatal(err)
}
chaosVersion := "false"
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
if err := internal.NewVersionOverview(app, deployMeta.Version, chaosVersion, chosenUpgrade, releaseNotes); err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
if ok && !internal.DontWaitConverge {
log.Debugf("run the following post-deploy commands: %s", postDeployCmds)
logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err)
}
}
if app.Recipe.Version != "" {
err := app.WriteRecipeVersion(chosenUpgrade)
if err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
}
}

117
cli/app/version.go Normal file
View File

@ -0,0 +1,117 @@
package app
import (
"context"
"sort"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/distribution/reference"
"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
func sortServiceByName(versions [][]string) func(i, j int) bool {
return func(i, j int) bool {
// NOTE(d1): corresponds to the `tableCol` definition below
if versions[i][1] == "app" {
return true
}
return versions[i][1] < versions[j][1]
}
}
// getImagePath returns the image name
func getImagePath(image string) (string, error) {
img, err := reference.ParseNormalizedNamed(image)
if err != nil {
return "", err
}
path := reference.Path(img)
path = formatter.StripTagMeta(path)
logrus.Debugf("parsed %s from %s", path, image)
return path, nil
}
var appVersionCommand = cli.Command{
Name: "version",
Aliases: []string{"v"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Usage: "Show version info of a deployed app",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
if deployedVersion == "unknown" {
logrus.Fatalf("failed to determine version of deployed %s", app.Name)
}
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
versionsMeta := make(map[string]recipe.ServiceMeta)
for _, recipeVersion := range recipeMeta.Versions {
if currentVersion, exists := recipeVersion[deployedVersion]; exists {
versionsMeta = currentVersion
}
}
if len(versionsMeta) == 0 {
logrus.Fatalf("could not retrieve deployed version (%s) from recipe catalogue?", deployedVersion)
}
tableCol := []string{"version", "service", "image", "tag"}
table := formatter.CreateTable(tableCol)
var versions [][]string
for serviceName, versionMeta := range versionsMeta {
versions = append(versions, []string{deployedVersion, serviceName, versionMeta.Image, versionMeta.Tag})
}
sort.Slice(versions, sortServiceByName(versions))
for _, version := range versions {
table.Append(version)
}
table.SetAutoMergeCellsByColumnIndex([]int{0})
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.Render()
return nil
},
}

View File

@ -2,14 +2,15 @@ package app
import (
"context"
"log"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -29,17 +30,17 @@ var appVolumeListCommand = cli.Command{
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
filters, err := app.Filters(false, true)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
table := formatter.CreateTable([]string{"name", "created", "mounted"})
@ -54,7 +55,7 @@ var appVolumeListCommand = cli.Command{
if table.NumLines() > 0 {
table.Render()
} else {
log.Warnf("no volumes created for %s", app.Name)
logrus.Warnf("no volumes created for %s", app.Name)
}
return nil
@ -73,7 +74,8 @@ 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/-f" will select all volumes for removal. Be careful.
`,
ArgsUsage: "<domain>",
Aliases: []string{"rm"},
Flags: []cli.Flag{
@ -88,26 +90,26 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if deployMeta.IsDeployed {
log.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
if isDeployed {
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
}
filters, err := app.Filters(false, true)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
volumeNames := client.GetVolumeNames(volumeList)
@ -121,7 +123,7 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
Default: volumeNames,
}
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
@ -135,9 +137,9 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
log.Fatalf("removing volumes failed: %s", err)
}
log.Infof("%d volumes removed successfully", len(volumesToRemove))
logrus.Infof("%d volumes removed successfully", len(volumesToRemove))
} else {
log.Info("no volumes removed")
logrus.Info("no volumes removed")
}
return nil

View File

@ -12,9 +12,9 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -33,7 +33,14 @@ var catalogueGenerateCommand = cli.Command{
},
Before: internal.SubCommandBefore,
Description: `
Generate a new copy of the recipe catalogue.
Generate 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)
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.
It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten.
@ -44,12 +51,12 @@ If you have a Hub account you can have Abra log you in to avoid this. Pass
Push your new release to git.coopcloud.tech with "-p/--publish". This requires
that you have permission to git push to these repositories and have your SSH
keys configured on your account.`,
keys configured on your account.
`,
ArgsUsage: "[<recipe>]",
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" {
internal.ValidateRecipe(c)
@ -57,13 +64,13 @@ keys configured on your account.`,
if !internal.Chaos {
if err := catalogue.EnsureIsClean(); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
repos, err := recipe.ReadReposMetadata()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
var barLength int
@ -77,9 +84,9 @@ keys configured on your account.`,
}
if !internal.SkipUpdates {
log.Warn(logMsg)
logrus.Warn(logMsg)
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
@ -91,14 +98,14 @@ keys configured on your account.`,
continue
}
versions, err := r.GetRecipeVersions()
versions, err := recipe.GetRecipeVersions(recipeMeta.Name, internal.Offline)
if err != nil {
log.Warn(err)
logrus.Warn(err)
}
features, category, err := recipe.GetRecipeFeaturesAndCategory(r)
features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name)
if err != nil {
log.Warn(err)
logrus.Warn(err)
}
catl[recipeMeta.Name] = recipe.RecipeMeta{
@ -119,84 +126,84 @@ keys configured on your account.`,
recipesJSON, err := json.MarshalIndent(catl, "", " ")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if recipeName == "" {
if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
} else {
catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
catlFS[recipeName] = catl[recipeName]
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
logrus.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if internal.Publish {
isClean, err := gitPkg.IsClean(cataloguePath)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if isClean {
if !internal.Dry {
log.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath)
logrus.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath)
}
}
msg := "chore: publish new catalogue release changes"
if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
repo, err := git.PlainOpen(cataloguePath)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
repo, err := git.PlainOpen(cataloguePath)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
head, err := repo.Head()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !internal.Dry && internal.Publish {
url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash())
log.Infof("new changes published: %s", url)
logrus.Infof("new changes published: %s", url)
}
if internal.Dry {
log.Info("dry run: no changes published")
logrus.Info("dry run: no changes published")
}
return nil
@ -209,6 +216,7 @@ var CatalogueCommand = cli.Command{
Usage: "Manage the recipe catalogue",
Aliases: []string{"c"},
ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue",
Subcommands: []cli.Command{
catalogueGenerateCommand,
},

View File

@ -14,10 +14,10 @@ import (
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/autocomplete"
cataloguePkg "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/web"
charmLog "github.com/charmbracelet/log"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -25,15 +25,16 @@ import (
var AutoCompleteCommand = cli.Command{
Name: "autocomplete",
Aliases: []string{"ac"},
Usage: "Configure shell autocompletion",
Usage: "Configure shell autocompletion (recommended)",
Description: `
Set up shell auto-completion.
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.
Supported shells are: bash, fish, fizsh & zsh.
Example:
EXAMPLE:
abra autocomplete bash`,
abra autocomplete bash
`,
ArgsUsage: "<shell>",
Flags: []cli.Flag{
internal.DebugFlag,
@ -53,7 +54,7 @@ EXAMPLE:
}
if _, ok := supportedShells[shellType]; !ok {
log.Fatalf("%s is not a supported shell right now, sorry", shellType)
logrus.Fatalf("%s is not a supported shell right now, sorry", shellType)
}
if shellType == "fizsh" {
@ -63,24 +64,24 @@ EXAMPLE:
autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion")
if err := os.Mkdir(autocompletionDir, 0764); err != nil {
if !os.IsExist(err) {
log.Fatal(err)
logrus.Fatal(err)
}
log.Debugf("%s already created", autocompletionDir)
logrus.Debugf("%s already created", autocompletionDir)
}
autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType)
if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) {
url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType)
log.Infof("fetching %s", url)
logrus.Infof("fetching %s", url)
if err := web.GetFile(autocompletionFile, url); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
switch shellType {
case "bash":
fmt.Println(fmt.Sprintf(`
# run the following commands to install auto-completion
# Run the following commands to install auto-completion
sudo mkdir /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
@ -88,19 +89,19 @@ echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
`, autocompletionFile))
case "zsh":
fmt.Println(fmt.Sprintf(`
# run the following commands to install auto-completion
# Run the following commands to install auto-completion
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
# to test, run the following: "abra app <hit tab key>" - you should see command completion!
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "fish":
fmt.Println(fmt.Sprintf(`
# run the following commands to install auto-completion
# 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
# to test, run the following: "abra app <hit tab key>" - you should see command completion!
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
}
@ -112,18 +113,14 @@ echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
var UpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade abra",
Usage: "Upgrade Abra itself",
Description: `
Upgrade abra in-place with the latest stable or release candidate.
Upgrade Abra in-place with the latest stable or release candidate.
Use "-r/--rc" to install the latest release candidate. Please bear in mind that
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
for the testing efforts 💗
EXAMPLE:
abra upgrade
abra upgrade --rc`,
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!
`,
Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error {
mainURL := "https://install.abra.coopcloud.tech"
@ -134,10 +131,10 @@ EXAMPLE:
cmd = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
log.Debugf("attempting to run %s", cmd)
logrus.Debugf("attempting to run %s", cmd)
if err := internal.RunCmd(cmd); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil
@ -147,7 +144,7 @@ EXAMPLE:
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "abra",
Usage: `the Co-op Cloud command-line utility belt 🎩🐇
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
@ -181,14 +178,17 @@ func newAbraApp(version, commit string) *cli.App {
for _, path := range paths {
if err := os.Mkdir(path, 0764); err != nil {
if !os.IsExist(err) {
log.Fatal(err)
logrus.Fatal(err)
}
continue
}
}
charmLog.SetDefault(log.Logger)
log.Debugf("abra version %s, commit %s", version, commit)
if err := cataloguePkg.EnsureCatalogue(); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("abra version %s, commit %s", version, commit)
return nil
}
@ -201,6 +201,6 @@ func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}

View File

@ -5,13 +5,13 @@ import (
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service"
"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"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// RetrieveBackupBotContainer gets the deployed backupbot container.
@ -22,7 +22,7 @@ func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error
return types.Container{}, err
}
log.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name)
logrus.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name)
filters := filters.NewArgs()
filters.Add("name", chosenService.Spec.Name)
@ -51,7 +51,7 @@ func RunBackupCmdRemote(cl *dockerClient.Client, backupCmd string, containerID s
Tty: true,
}
log.Debugf("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts)
logrus.Debugf("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts)
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli()

View File

@ -3,7 +3,8 @@ package internal
import (
"os"
"coopcloud.tech/abra/pkg/log"
logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -37,20 +38,6 @@ var PassRemoveFlag = &cli.BoolFlag{
Destination: &PassRemove,
}
var File bool
var FileFlag = &cli.BoolFlag{
Name: "file, f",
Usage: "Treat input as a file",
Destination: &File,
}
var Trim bool
var TrimFlag = &cli.BoolFlag{
Name: "trim, t",
Usage: "Trim input",
Destination: &Trim,
}
// Force force functionality without asking.
var Force bool
@ -108,16 +95,6 @@ var OfflineFlag = &cli.BoolFlag{
Usage: "Prefer offline & filesystem access when possible",
}
// ReleaseNotes stores the variable from ReleaseNotesFlag.
var ReleaseNotes bool
// ReleaseNotesFlag turns on/off printing only release notes when upgrading.
var ReleaseNotesFlag = &cli.BoolFlag{
Name: "releasenotes, r",
Destination: &ReleaseNotes,
Usage: "Only show release notes",
}
// MachineReadable stores the variable from MachineReadableFlag
var MachineReadable bool
@ -192,7 +169,7 @@ var NewAppServerFlag = &cli.StringFlag{
var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks, D",
Usage: "Disable public DNS checks",
Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks,
}
@ -261,35 +238,13 @@ var RemoteUserFlag = &cli.StringFlag{
Destination: &RemoteUser,
}
var GitName string
var GitNameFlag = &cli.StringFlag{
Name: "git-name, gn",
Value: "",
Usage: "Git (user) name to do commits with",
Destination: &GitName,
}
var GitEmail string
var GitEmailFlag = &cli.StringFlag{
Name: "git-email, ge",
Value: "",
Usage: "Git email name to do commits with",
Destination: &GitEmail,
}
var AllServices bool
var AllServicesFlag = &cli.BoolFlag{
Name: "all-services, a",
Usage: "Restart all services",
Destination: &AllServices,
}
// SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
func SubCommandBefore(c *cli.Context) error {
if Debug {
log.SetLevel(log.DebugLevel)
log.SetOutput(os.Stderr)
log.SetReportCaller(true)
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
}
return nil

View File

@ -8,20 +8,20 @@ import (
"os/exec"
"strings"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"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"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
)
// RunCmdRemote executes an abra.sh command in the target service
func RunCmdRemote(cl *dockerClient.Client, app appPkg.App, abraSh, serviceName, cmdName, cmdArgs string) error {
func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, cmdName, cmdArgs string) error {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
@ -30,7 +30,7 @@ func RunCmdRemote(cl *dockerClient.Client, app appPkg.App, abraSh, serviceName,
return err
}
log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
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)
@ -61,7 +61,7 @@ func RunCmdRemote(cl *dockerClient.Client, app appPkg.App, abraSh, serviceName,
}
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
shell = "/bin/sh"
}
@ -72,10 +72,10 @@ func RunCmdRemote(cl *dockerClient.Client, app appPkg.App, abraSh, serviceName,
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
}
log.Debugf("running command: %s", strings.Join(cmd, " "))
logrus.Debugf("running command: %s", strings.Join(cmd, " "))
if RemoteUser != "" {
log.Debugf("running command with user %s", RemoteUser)
logrus.Debugf("running command with user %s", RemoteUser)
execCreateOpts.User = RemoteUser
}

View File

@ -2,19 +2,21 @@ package internal
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos", "to deploy"}
func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
@ -27,22 +29,14 @@ func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion
server = "local"
}
table.Append([]string{
server,
app.Recipe.Name,
deployConfig,
app.Domain,
currentVersion,
chaosVersion,
newVersion,
})
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
table.Render()
if releaseNotes != "" && newVersion != "" {
fmt.Println()
fmt.Print(releaseNotes)
} else {
log.Warnf("no release notes available for %s", newVersion)
logrus.Warnf("no release notes available for %s", newVersion)
}
if NoInput {
@ -59,19 +53,40 @@ func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion
}
if !response {
log.Fatal("exiting as requested")
logrus.Fatal("exiting as requested")
}
return nil
}
// GetReleaseNotes prints release notes for a recipe version
func GetReleaseNotes(recipeName, version string) (string, error) {
if version == "" {
return "", nil
}
fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version)
if _, err := os.Stat(fpath); !os.IsNotExist(err) {
releaseNotes, err := ioutil.ReadFile(fpath)
if err != nil {
return "", err
}
withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes))
return withTitle, nil
}
return "", nil
}
// PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name))
return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name))
}
return err
}
@ -87,13 +102,13 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
if len(commandParts) > 2 {
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
}
log.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
if err := EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
return err
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil {
return err
}
@ -109,10 +124,10 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name))
}
log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
Tty = true
if err := RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil {
if err := RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
return err
}
}
@ -120,8 +135,8 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
}
// DeployOverview shows a deployment overview
func DeployOverview(app appPkg.App, version, chaosVersion, message string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos"}
func DeployOverview(app config.App, version, message string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
@ -134,14 +149,7 @@ func DeployOverview(app appPkg.App, version, chaosVersion, message string) error
server = "local"
}
table.Append([]string{
server,
app.Recipe.Name,
deployConfig,
app.Domain,
version,
chaosVersion,
})
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
table.Render()
if NoInput {
@ -158,7 +166,7 @@ func DeployOverview(app appPkg.App, version, chaosVersion, message string) error
}
if !response {
log.Fatal("exiting as requested")
logrus.Fatal("exiting as requested")
}
return nil

View File

@ -3,7 +3,7 @@ package internal
import (
"os"
"coopcloud.tech/abra/pkg/log"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -11,8 +11,8 @@ import (
// terminal, and shows the help command.
func ShowSubcommandHelpAndError(c *cli.Context, err interface{}) {
if err2 := cli.ShowSubcommandHelp(c); err2 != nil {
log.Error(err2)
logrus.Error(err2)
}
log.Error(err)
logrus.Error(err)
os.Exit(1)
}

View File

@ -4,10 +4,10 @@ import (
"fmt"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
)
// PromptBumpType prompts for version bump type
@ -65,7 +65,7 @@ func GetBumpType() string {
} else if Patch {
bumpType = "patch"
} else {
log.Fatal("no version bump type specififed?")
logrus.Fatal("no version bump type specififed?")
}
return bumpType
@ -80,7 +80,7 @@ func SetBumpType(bumpType string) {
} else if bumpType == "patch" {
Patch = true
} else {
log.Fatal("no version bump type specififed?")
logrus.Fatal("no version bump type specififed?")
}
}
@ -88,11 +88,7 @@ func SetBumpType(bumpType string) {
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
var path string
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return "", err
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
if service.Name == "app" {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {

View File

@ -6,9 +6,9 @@ import (
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -21,7 +21,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
catl, err := recipe.ReadRecipeCatalogue(Offline)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
knownRecipes := make(map[string]bool)
@ -31,7 +31,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
localRecipes, err := recipe.GetRecipesLocal()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, recipeLocal := range localRecipes {
@ -49,7 +49,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
Options: recipes,
}
if err := survey.AskOne(prompt, &recipeName); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
@ -57,33 +57,28 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
chosenRecipe := recipe.Get(recipeName)
err := chosenRecipe.EnsureExists()
if err != nil {
log.Fatal(err)
}
_, err = chosenRecipe.GetComposeConfig(nil)
chosenRecipe, err := recipe.Get(recipeName, Offline)
if err != nil {
if c.Command.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") {
log.Fatal(err)
logrus.Fatal(err)
}
log.Warn(err)
logrus.Warn(err)
} else {
if strings.Contains(err.Error(), "template_driver is not allowed") {
log.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName)
logrus.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName)
}
log.Fatalf("unable to validate recipe: %s", err)
logrus.Fatalf("unable to validate recipe: %s", err)
}
}
log.Debugf("validated %s as recipe argument", recipeName)
logrus.Debugf("validated %s as recipe argument", recipeName)
return chosenRecipe
}
// ValidateApp ensures the app name arg is valid.
func ValidateApp(c *cli.Context) app.App {
func ValidateApp(c *cli.Context) config.App {
appName := c.Args().First()
if appName == "" {
@ -92,10 +87,10 @@ func ValidateApp(c *cli.Context) app.App {
app, err := app.Get(appName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Debugf("validated %s as app argument", appName)
logrus.Debugf("validated %s as app argument", appName)
return app
}
@ -110,7 +105,7 @@ func ValidateDomain(c *cli.Context) string {
Default: "example.com",
}
if err := survey.AskOne(prompt, &domainName); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
@ -118,7 +113,7 @@ func ValidateDomain(c *cli.Context) string {
ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
}
log.Debugf("validated %s as domain argument", domainName)
logrus.Debugf("validated %s as domain argument", domainName)
return domainName
}
@ -143,7 +138,7 @@ func ValidateServer(c *cli.Context) string {
serverNames, err := config.ReadServerNames()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if serverName == "" && !NoInput {
@ -152,7 +147,7 @@ func ValidateServer(c *cli.Context) string {
Options: serverNames,
}
if err := survey.AskOne(prompt, &serverName); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
@ -171,7 +166,7 @@ func ValidateServer(c *cli.Context) string {
ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
}
log.Debugf("validated %s as server argument", serverName)
logrus.Debugf("validated %s as server argument", serverName)
return serverName
}

View File

@ -1,17 +1,20 @@
package recipe
import (
"path"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var recipeDiffCommand = cli.Command{
Name: "diff",
Usage: "Show unstaged changes in recipe config",
Description: "This command requires /usr/bin/git.",
Description: "Due to limitations in our underlying Git dependency, this command requires /usr/bin/git.",
Aliases: []string{"d"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
@ -21,10 +24,15 @@ var recipeDiffCommand = cli.Command{
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
r := internal.ValidateRecipe(c)
recipeName := c.Args().First()
if err := gitPkg.DiffUnstaged(r.Dir); err != nil {
log.Fatal(err)
if recipeName != "" {
internal.ValidateRecipe(c)
}
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
logrus.Fatal(err)
}
return nil

View File

@ -4,8 +4,8 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -24,25 +24,23 @@ var recipeFetchCommand = cli.Command{
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" {
internal.ValidateRecipe(c)
if err := r.Ensure(false, false); err != nil {
log.Fatal(err)
if err := recipe.Ensure(recipeName); err != nil {
logrus.Fatal(err)
}
return nil
}
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
for recipeName := range catalogue {
r := recipe.Get(recipeName)
if err := r.Ensure(false, false); err != nil {
log.Error(err)
if err := recipe.Ensure(recipeName); err != nil {
logrus.Error(err)
}
catlBar.Add(1)
}

View File

@ -7,7 +7,8 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -28,8 +29,24 @@ var recipeLintCommand = cli.Command{
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
@ -40,7 +57,7 @@ var recipeLintCommand = cli.Command{
for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] {
if internal.OnlyErrors && rule.Level != "error" {
log.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
logrus.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
continue
}
@ -58,7 +75,7 @@ var recipeLintCommand = cli.Command{
if !skipped {
ok, err := rule.Function(recipe)
if err != nil {
log.Warn(err)
logrus.Warn(err)
}
if !ok && rule.Level == "error" {
@ -97,7 +114,7 @@ var recipeLintCommand = cli.Command{
}
if hasError {
log.Warn("watch out, some critical errors are present in your recipe config")
logrus.Warn("watch out, some critical errors are present in your recipe config")
}
return nil

View File

@ -8,8 +8,8 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -35,7 +35,7 @@ var recipeListCommand = cli.Command{
Action: func(c *cli.Context) error {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err.Error())
logrus.Fatal(err.Error())
}
recipes := catl.Flatten()

View File

@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"text/template"
@ -11,8 +12,7 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -37,8 +37,6 @@ var recipeNewCommand = cli.Command{
internal.DebugFlag,
internal.NoInputFlag,
internal.OfflineFlag,
internal.GitNameFlag,
internal.GitEmailFlag,
},
Before: internal.SubCommandBefore,
Usage: "Create a new recipe",
@ -48,55 +46,79 @@ Create a new recipe.
Abra uses the built-in example repository which is available here:
https://git.coopcloud.tech/coop-cloud/example`,
https://git.coopcloud.tech/coop-cloud/example
Files within the example repository make use of the Golang templating system
which Abra uses to inject values into the generated recipe folder (e.g. name of
recipe and domain in the sample environment config).
`,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
log.Fatalf("%s recipe directory already exists?", r.Dir)
directory := path.Join(config.RECIPES_DIR, recipeName)
if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("%s recipe directory already exists?", directory)
}
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(r.Dir, url); err != nil {
log.Fatal(err)
if err := git.Clone(directory, url); err != nil {
logrus.Fatal(err)
}
gitRepo := path.Join(r.Dir, ".git")
gitRepo := path.Join(config.RECIPES_DIR, recipeName, ".git")
if err := os.RemoveAll(gitRepo); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Debugf("removed example git repo in %s", gitRepo)
logrus.Debugf("removed example git repo in %s", gitRepo)
meta := newRecipeMeta(recipeName)
for _, path := range []string{r.ReadmePath, r.SampleEnvPath} {
toParse := []string{
path.Join(config.RECIPES_DIR, recipeName, "README.md"),
path.Join(config.RECIPES_DIR, recipeName, ".env.sample"),
}
for _, path := range toParse {
tpl, err := template.ParseFiles(path)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
var templated bytes.Buffer
if err := tpl.Execute(&templated, meta); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil {
log.Fatal(err)
if err := ioutil.WriteFile(path, templated.Bytes(), 0644); err != nil {
logrus.Fatal(err)
}
}
if err := git.Init(r.Dir, true, internal.GitName, internal.GitEmail); err != nil {
log.Fatal(err)
newGitRepo := path.Join(config.RECIPES_DIR, recipeName)
if err := git.Init(newGitRepo, true); err != nil {
logrus.Fatal(err)
}
log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir))
log.Info("happy hacking 🎉")
fmt.Print(fmt.Sprintf(`
Your new %s recipe has been created in %s.
In order to share your recipe, you can upload it the git repository to:
https://git.coopcloud.tech/coop-cloud/%s
If you're not sure how to do that, come chat with us:
https://docs.coopcloud.tech/intro/contact
See "abra recipe -h" for additional recipe maintainer commands.
Happy Hacking!
`, recipeName, path.Join(config.RECIPES_DIR, recipeName), recipeName))
return nil
},

View File

@ -18,7 +18,9 @@ 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.`,
manner. Abra supports convenient automation for recipe maintainenace, see the
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
`,
Subcommands: []cli.Command{
recipeFetchCommand,
recipeLintCommand,

View File

@ -10,14 +10,16 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
"github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -44,7 +46,8 @@ major and therefore require intervention while doing the upgrade work.
Publish your new release to git.coopcloud.tech with "-p/--publish". This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.`,
your SSH keys configured on your account.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
@ -62,74 +65,74 @@ your SSH keys configured on your account.`,
imagesTmp, err := getImageVersions(recipe)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
mainAppVersion := imagesTmp[mainApp]
if mainAppVersion == "" {
log.Fatalf("main app service version for %s is empty?", recipe.Name)
logrus.Fatalf("main app service version for %s is empty?", recipe.Name)
}
tagString := c.Args().Get(1)
if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil {
log.Fatalf("cannot parse %s, invalid tag specified?", tagString)
logrus.Fatalf("cannot parse %s, invalid tag specified?", tagString)
}
}
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
log.Fatal("cannot specify tag and bump type at the same time")
logrus.Fatal("cannot specify tag and bump type at the same time")
}
if tagString != "" {
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
tags, err := recipe.Tags()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
var err error
tagString, err = getLabelVersion(recipe, false)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
isClean, err := gitPkg.IsClean(recipe.Dir)
isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err)
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
logrus.Fatal(err)
}
}
if len(tags) > 0 {
log.Warnf("previous git tags detected, assuming this is a new semver release")
logrus.Warnf("previous git tags detected, assuming this is a new semver release")
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
} else {
log.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
logrus.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanUpErr := cleanUpTag(recipe, tagString); err != nil {
log.Fatal(cleanUpErr)
if cleanUpErr := cleanUpTag(tagString, recipe.Name); err != nil {
logrus.Fatal(cleanUpErr)
}
log.Fatal(err)
logrus.Fatal(err)
}
}
@ -141,12 +144,8 @@ your SSH keys configured on your account.`,
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
services := make(map[string]string)
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return nil, err
}
missingTag := false
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
if service.Image == "" {
continue
}
@ -185,7 +184,8 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err error
repo, err := git.PlainOpen(recipe.Dir)
directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
@ -210,19 +210,19 @@ func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string
}
if err := addReleaseNotes(recipe, tagString); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := commitRelease(recipe, tagString); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := tagRelease(tagString, repo); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := pushRelease(recipe, tagString); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil
@ -246,7 +246,8 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
// addReleaseNotes checks if the release/next release note exists and moves the
// file to release/<tag>.
func addReleaseNotes(recipe recipe.Recipe, tag string) error {
tagReleaseNotePath := path.Join(recipe.Dir, "release", tag)
repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
tagReleaseNotePath := path.Join(repoPath, "release", tag)
if _, err := os.Stat(tagReleaseNotePath); err == nil {
// Release note for current tag already exist exists.
return nil
@ -254,11 +255,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return err
}
nextReleaseNotePath := path.Join(recipe.Dir, "release", "next")
nextReleaseNotePath := path.Join(repoPath, "release", "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag>
if internal.Dry {
log.Debugf("dry run: move release note from 'next' to %s", tag)
logrus.Debugf("dry run: move release note from 'next' to %s", tag)
return nil
}
if !internal.NoInput {
@ -277,11 +278,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
if err != nil {
return err
}
err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry)
err = gitPkg.Add(repoPath, path.Join("release", "next"), internal.Dry)
if err != nil {
return err
}
err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry)
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry)
if err != nil {
return err
}
@ -310,7 +311,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
if err != nil {
return err
}
err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry)
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry)
if err != nil {
return err
}
@ -320,23 +321,24 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
func commitRelease(recipe recipe.Recipe, tag string) error {
if internal.Dry {
log.Debugf("dry run: no changes committed")
logrus.Debugf("dry run: no changes committed")
return nil
}
isClean, err := gitPkg.IsClean(recipe.Dir)
isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil {
return err
}
if isClean {
if !internal.Dry {
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir)
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir())
}
}
msg := fmt.Sprintf("chore: publish %s release", tag)
if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil {
repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
if err := gitPkg.Commit(repoPath, msg, internal.Dry); err != nil {
return err
}
@ -345,7 +347,7 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry {
log.Debugf("dry run: no git tag created (%s)", tagString)
logrus.Debugf("dry run: no git tag created (%s)", tagString)
return nil
}
@ -365,14 +367,14 @@ func tagRelease(tagString string, repo *git.Repository) error {
}
hash := formatter.SmallSHA(head.Hash().String())
log.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash))
logrus.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash))
return nil
}
func pushRelease(recipe recipe.Recipe, tagString string) error {
if internal.Dry {
log.Info("dry run: no changes published")
logrus.Info("dry run: no changes published")
return nil
}
@ -390,17 +392,18 @@ func pushRelease(recipe recipe.Recipe, tagString string) error {
if err := recipe.Push(internal.Dry); err != nil {
return err
}
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
log.Infof("new release published: %s", url)
url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString)
logrus.Infof("new release published: %s", url)
} else {
log.Info("no -p/--publish passed, not publishing")
logrus.Info("no -p/--publish passed, not publishing")
}
return nil
}
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
repo, err := git.PlainOpen(recipe.Dir)
directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
@ -465,7 +468,7 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
}
if lastGitTag.String() == tagString {
log.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString)
logrus.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString)
}
if !internal.NoInput {
@ -475,36 +478,37 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
var ok bool
if err := survey.AskOne(prompt, &ok); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !ok {
log.Fatal("exiting as requested")
logrus.Fatal("exiting as requested")
}
}
if err := addReleaseNotes(recipe, tagString); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := commitRelease(recipe, tagString); err != nil {
log.Fatalf("failed to commit changes: %s", err.Error())
logrus.Fatalf("failed to commit changes: %s", err.Error())
}
if err := tagRelease(tagString, repo); err != nil {
log.Fatalf("failed to tag release: %s", err.Error())
logrus.Fatalf("failed to tag release: %s", err.Error())
}
if err := pushRelease(recipe, tagString); err != nil {
log.Fatalf("failed to publish new release: %s", err.Error())
logrus.Fatalf("failed to publish new release: %s", err.Error())
}
return nil
}
// cleanUpTag removes a freshly created tag
func cleanUpTag(recipe recipe.Recipe, tag string) error {
repo, err := git.PlainOpen(recipe.Dir)
func cleanUpTag(tag, recipeName string) error {
directory := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
@ -515,22 +519,22 @@ func cleanUpTag(recipe recipe.Recipe, tag string) error {
}
}
log.Debugf("removed freshly created tag %s", tag)
logrus.Debugf("removed freshly created tag %s", tag)
return nil
}
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
initTag, err := recipe.GetVersionLabelLocal()
initTag, err := recipePkg.GetVersionLabelLocal(recipe)
if err != nil {
return "", err
}
if initTag == "" {
log.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name)
logrus.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name)
}
log.Warnf("discovered %s as currently synced recipe label", initTag)
logrus.Warnf("discovered %s as currently synced recipe label", initTag)
if prompt && !internal.NoInput {
var response bool

View File

@ -1,18 +1,20 @@
package recipe
import (
"path"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var recipeResetCommand = cli.Command{
Name: "reset",
Usage: "Remove all unstaged changes from recipe config",
Description: "WARNING: this will delete your changes. Be Careful.",
Description: "WARNING, this will delete your changes. Be Careful.",
Aliases: []string{"rs"},
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
@ -23,30 +25,30 @@ var recipeResetCommand = cli.Command{
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" {
internal.ValidateRecipe(c)
}
repo, err := git.PlainOpen(r.Dir)
repoPath := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(repoPath)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
ref, err := repo.Head()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
worktree, err := repo.Worktree()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
opts := &git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset}
if err := worktree.Reset(opts); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil

View File

@ -2,16 +2,18 @@ package recipe
import (
"fmt"
"path"
"strconv"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -37,33 +39,34 @@ named "app") which corresponds to the following format:
Where <version> can be specifed on the command-line or Abra can attempt to
auto-generate it for you. The <recipe> configuration will be updated on the
local file system.`,
local file system.
`,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
mainApp, err := internal.GetMainAppImage(recipe)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
imagesTmp, err := getImageVersions(recipe)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
mainAppVersion := imagesTmp[mainApp]
tags, err := recipe.Tags()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
nextTag := c.Args().Get(1)
if len(tags) == 0 && nextTag == "" {
log.Warnf("no git tags found for %s", recipe.Name)
logrus.Warnf("no git tags found for %s", recipe.Name)
if internal.NoInput {
log.Fatalf("unable to continue, input required for initial version")
logrus.Fatalf("unable to continue, input required for initial version")
}
fmt.Println(fmt.Sprintf(`
The following options are two types of initial semantic version that you can
@ -90,7 +93,7 @@ likely to change.
}
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
@ -99,26 +102,27 @@ likely to change.
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
latestRelease := tags[len(tags)-1]
if err := internal.PromptBumpType("", latestRelease); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
if nextTag == "" {
repo, err := git.PlainOpen(recipe.Dir)
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
var lastGitTag tagcmp.Tag
iter, err := repo.Tags()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash())
if err != nil {
log.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.")
logrus.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.")
return err
}
@ -135,7 +139,7 @@ likely to change.
return nil
}); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
// bumpType is used to decide what part of the tag should be incremented
@ -143,7 +147,7 @@ likely to change.
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
log.Fatal("you can only use one version flag: --major, --minor or --patch")
logrus.Fatal("you can only use one version flag: --major, --minor or --patch")
}
}
@ -152,14 +156,14 @@ likely to change.
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
newTag.Patch = "0"
@ -167,7 +171,7 @@ likely to change.
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
newTag.Patch = "0"
@ -177,32 +181,32 @@ likely to change.
}
newTag.Metadata = mainAppVersion
log.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
logrus.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
nextTag = newTag.String()
}
if _, err := tagcmp.Parse(nextTag); err != nil {
log.Fatalf("invalid version %s specified", nextTag)
logrus.Fatalf("invalid version %s specified", nextTag)
}
mainService := "app"
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if !internal.Dry {
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
} else {
log.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name)
logrus.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name)
}
isClean, err := gitPkg.IsClean(recipe.Dir)
isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err)
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
logrus.Fatal(err)
}
}

View File

@ -12,13 +12,14 @@ 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"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -53,11 +54,10 @@ The command is interactive and will show a select input which allows you to
make a seclection. Use the "?" key to see more help on navigating this
interface.
You may invoke this command in "wizard" mode and be prompted for input.
You may invoke this command in "wizard" mode and be prompted for input:
EXAMPLE:
abra recipe upgrade`,
abra recipe upgrade
`,
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
@ -73,15 +73,27 @@ EXAMPLE:
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
log.Fatal("you can only use one of: --major, --minor, --patch.")
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
}
}
@ -94,25 +106,26 @@ EXAMPLE:
// check for versions file and load pinned versions
versionsPresent := false
versionsPath := path.Join(recipe.Dir, "versions")
servicePins := make(map[string]imgPin)
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
versionsPath := path.Join(recipeDir, "versions")
var servicePins = make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil {
log.Debugf("found versions file for %s", recipe.Name)
logrus.Debugf("found versions file for %s", recipe.Name)
file, err := os.Open(versionsPath)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
splitLine := strings.Split(line, " ")
if splitLine[0] != "pin" || len(splitLine) != 3 {
log.Fatalf("malformed version pin specification: %s", line)
logrus.Fatalf("malformed version pin specification: %s", line)
}
pinSlice := strings.Split(splitLine[2], ":")
pinTag, err := tagcmp.Parse(pinSlice[1])
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
pin := imgPin{
image: pinSlice[0],
@ -121,50 +134,45 @@ EXAMPLE:
servicePins[splitLine[1]] = pin
}
if err := scanner.Err(); err != nil {
log.Error(err)
logrus.Error(err)
}
versionsPresent = true
} else {
log.Debugf("did not find versions file for %s", recipe.Name)
logrus.Debugf("did not find versions file for %s", recipe.Name)
}
config, err := recipe.GetComposeConfig(nil)
if err != nil {
log.Fatal(err)
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
regVersions, err := client.GetRegistryTags(img)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
image := reference.Path(img)
log.Debugf("retrieved %s from remote registry for %s", regVersions, image)
logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
image = formatter.StripTagMeta(image)
switch img.(type) {
case reference.NamedTagged:
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
log.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
}
default:
log.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name)
logrus.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name)
continue
}
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil {
log.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)
logrus.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)
continue
}
log.Debugf("parsed %s for %s", tag, service.Name)
logrus.Debugf("parsed %s for %s", tag, service.Name)
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
@ -178,18 +186,18 @@ EXAMPLE:
}
}
log.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
logrus.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && !internal.AllTags {
log.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, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
continue // skip on to the next tag and don't update any compose files
}
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
compatibleStrings := []string{"skip"}
@ -205,7 +213,7 @@ EXAMPLE:
}
}
log.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name)
logrus.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name)
var upgradeTag string
_, ok := servicePins[service.Name]
@ -222,13 +230,13 @@ EXAMPLE:
}
}
if contains {
log.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
logrus.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
} else {
log.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
continue
}
} else {
log.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
logrus.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
continue
}
} else {
@ -245,7 +253,7 @@ EXAMPLE:
}
}
if upgradeTag == "" {
log.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image)
logrus.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image)
continue
}
} else {
@ -253,7 +261,7 @@ EXAMPLE:
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags {
tag := img.(reference.NamedTagged).Tag()
if !internal.AllTags {
log.Warn(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag))
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag))
}
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{"skip"}
@ -291,7 +299,7 @@ EXAMPLE:
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
}
@ -299,14 +307,14 @@ EXAMPLE:
if upgradeTag != "skip" {
ok, err := recipe.UpdateTag(image, upgradeTag)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if ok {
log.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
}
} else {
if !internal.NoInput {
log.Warnf("not upgrading %s, skipping as requested", image)
logrus.Warnf("not upgrading %s, skipping as requested", image)
}
}
}
@ -315,7 +323,7 @@ EXAMPLE:
if internal.MachineReadable {
jsonstring, err := json.Marshal(upgradeList)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
fmt.Println(string(jsonstring))
@ -324,21 +332,21 @@ EXAMPLE:
}
for _, upgrade := range upgradeList {
log.Infof("can upgrade service: %s, image: %s, tag: %s ::", upgrade.Service, upgrade.Image, upgrade.Tag)
logrus.Infof("can upgrade service: %s, image: %s, tag: %s ::\n", upgrade.Service, upgrade.Image, upgrade.Tag)
for _, utag := range upgrade.UpgradeTags {
log.Infof(" %s", utag)
logrus.Infof(" %s\n", utag)
}
}
}
isClean, err := gitPkg.IsClean(recipe.Dir)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err)
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
logrus.Fatal(err)
}
}

View File

@ -7,9 +7,9 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -28,6 +28,7 @@ var recipeVersionCommand = cli.Command{
Aliases: []string{"v"},
Usage: "List recipe versions",
ArgsUsage: "<recipe>",
Description: "Versions are read from the recipe catalogue.",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
@ -41,28 +42,20 @@ var recipeVersionCommand = cli.Command{
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
recipeMeta, ok := catl[recipe.Name]
if !ok {
log.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipe.GetRecipeVersions()
if err != nil {
log.Warn(err)
}
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
logrus.Fatalf("%s is not published on the catalogue?", recipe.Name)
}
if len(recipeMeta.Versions) == 0 {
log.Fatalf("%s has no catalogue published versions?", recipe.Name)
logrus.Fatalf("%s has no catalogue published versions?", recipe.Name)
}
tableCols := []string{"version", "service", "image", "tag"}
aggregated_table := formatter.CreateTable(tableCols)
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
tableCols := []string{"version", "service", "image", "tag"}
table := formatter.CreateTable(tableCols)
for version, meta := range recipeMeta.Versions[i] {
var versions [][]string
@ -74,10 +67,11 @@ var recipeVersionCommand = cli.Command{
for _, version := range versions {
table.Append(version)
aggregated_table.Append(version)
}
if !internal.MachineReadable {
if internal.MachineReadable {
table.JSONRender()
} else {
table.SetAutoMergeCellsByColumnIndex([]int{0})
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.Render()
@ -85,9 +79,6 @@ var recipeVersionCommand = cli.Command{
}
}
}
if internal.MachineReadable {
aggregated_table.JSONRender()
}
return nil
},

View File

@ -10,9 +10,9 @@ import (
"coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -23,29 +23,29 @@ var localFlag = &cli.BoolFlag{
Destination: &local,
}
// cleanUp cleans up the partially created context/client details for a failed
// "server add" attempt.
func cleanUp(name string) {
if name != "default" {
log.Debugf("serverAdd: cleanUp: cleaning up context for %s", name)
if err := client.DeleteContext(name); err != nil {
log.Fatal(err)
func cleanUp(domainName string) {
if domainName != "default" {
logrus.Infof("cleaning up context for %s", domainName)
if err := client.DeleteContext(domainName); err != nil {
logrus.Fatal(err)
}
}
serverDir := filepath.Join(config.SERVERS_DIR, name)
logrus.Infof("attempting to clean up server directory for %s", domainName)
serverDir := filepath.Join(config.SERVERS_DIR, domainName)
files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil {
log.Fatalf("serverAdd: cleanUp: unable to list files in %s: %s", serverDir, err)
logrus.Fatalf("unable to list files in %s: %s", serverDir, err)
}
if len(files) > 0 {
log.Debugf("serverAdd: cleanUp: %s is not empty, aborting cleanup", serverDir)
logrus.Warnf("aborting clean up of %s because it is not empty", serverDir)
return
}
if err := os.RemoveAll(serverDir); err != nil {
log.Fatalf("serverAdd: cleanUp: failed to remove %s: %s", serverDir, err)
logrus.Fatal(err)
}
}
@ -53,151 +53,128 @@ func cleanUp(name string) {
// Docker manages SSH connection details. These are stored to disk in
// ~/.docker. Abra can manage this completely for the user, so it's an
// implementation detail.
func newContext(name string) (bool, error) {
func newContext(c *cli.Context, domainName, username, port string) error {
store := contextPkg.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
if err != nil {
return false, err
return err
}
for _, context := range contexts {
if context.Name == name {
log.Debugf("context for %s already exists", name)
return false, nil
if context.Name == domainName {
logrus.Debugf("context for %s already exists", domainName)
return nil
}
}
log.Debugf("creating context with domain %s", name)
logrus.Debugf("creating context with domain %s, username %s and port %s", domainName, username, port)
if err := client.CreateContext(name); err != nil {
return false, nil
if err := client.CreateContext(domainName, username, port); err != nil {
return err
}
return true, nil
return nil
}
// createServerDir creates the ~/.abra/servers/... directory for a new server.
func createServerDir(name string) (bool, error) {
if err := server.CreateServerDir(name); err != nil {
func createServerDir(domainName string) error {
if err := server.CreateServerDir(domainName); err != nil {
if !os.IsExist(err) {
return false, err
return err
}
logrus.Debugf("server dir for %s already created", domainName)
}
log.Debugf("server dir for %s already created", name)
return false, nil
}
return true, nil
return nil
}
var serverAddCommand = cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Add a new server to your configuration",
Usage: "Add a server to your configuration",
Description: `
Add a new server to your configuration so that it can be managed by Abra.
Abra relies on the standard SSH command-line and ~/.ssh/config for client
connection details. You must configure an entry per-host in your ~/.ssh/config
for each server. For example:
Abra uses the SSH command-line to discover connection details for your server.
It is advised to configure an entry per-host in your ~/.ssh/config for each
server. For example:
Host example.com example
Host example.com
Hostname example.com
User exampleUser
Port 12345
IdentityFile ~/.ssh/example@somewhere
You can then add a server like so:
Abra can then load SSH connection details from this configuratiion with:
abra server add example.com
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. The domain is then set to "default".
You can also pass "--no-domain-checks/-D" flag to use any arbitrary name
instead of a real domain. The host will be resolved with the "Hostname" entry
of your ~/.ssh/config. Checks for a valid online domain will be skipped:
abra server add -D example`,
developer machine.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.NoDomainChecksFlag,
localFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<name>",
ArgsUsage: "<domain>",
Action: func(c *cli.Context) error {
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
err := errors.New("cannot use <name> and --local together")
err := errors.New("cannot use <domain> and --local together")
internal.ShowSubcommandHelpAndError(c, err)
}
var name string
var domainName string
if local {
name = "default"
domainName = "default"
} else {
name = internal.ValidateDomain(c)
domainName = internal.ValidateDomain(c)
}
// NOTE(d1): reasonable 5 second timeout for connections which can't
// succeed. The connection is attempted twice, so this results in 10
// seconds.
timeout := client.WithTimeout(5)
if local {
created, err := createServerDir(name)
if err != nil {
log.Fatal(err)
if err := createServerDir(domainName); err != nil {
logrus.Fatal(err)
}
log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Fatal(err)
logrus.Infof("attempting to create client for %s", domainName)
if _, err := client.New(domainName); err != nil {
cleanUp(domainName)
logrus.Fatal(err)
}
if created {
log.Info("local server successfully added")
} else {
log.Warn("local server already exists")
}
logrus.Info("local server added")
return nil
}
if !internal.NoDomainChecks {
if _, err := dns.EnsureIPv4(name); err != nil {
log.Fatal(err)
}
if _, err := dns.EnsureIPv4(domainName); err != nil {
logrus.Fatal(err)
}
_, err := createServerDir(name)
if err := createServerDir(domainName); err != nil {
logrus.Fatal(err)
}
hostConfig, err := sshPkg.GetHostConfig(domainName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
created, err := newContext(name)
if err != nil {
cleanUp(name)
log.Fatal(err)
if err := newContext(c, domainName, hostConfig.User, hostConfig.Port); err != nil {
logrus.Fatal(err)
}
log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Fatal(sshPkg.Fatal(name, err))
logrus.Infof("attempting to create client for %s", domainName)
if _, err := client.New(domainName); err != nil {
cleanUp(domainName)
logrus.Debugf("failed to construct client for %s, saw %s", domainName, err.Error())
logrus.Fatal(sshPkg.Fatal(domainName, err))
}
if created {
log.Infof("%s successfully added", name)
} else {
log.Warnf("%s already exists", name)
}
logrus.Infof("%s added", domainName)
return nil
},

View File

@ -7,16 +7,25 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var problemsFilter bool
var problemsFilterFlag = &cli.BoolFlag{
Name: "problems, p",
Usage: "Show only servers with potential connection problems",
Destination: &problemsFilter,
}
var serverListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List managed servers",
Flags: []cli.Flag{
problemsFilterFlag,
internal.DebugFlag,
internal.MachineReadableFlag,
internal.OfflineFlag,
@ -26,15 +35,15 @@ var serverListCommand = cli.Command{
dockerContextStore := context.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
tableColumns := []string{"name", "host"}
tableColumns := []string{"name", "host", "user", "port"}
table := formatter.CreateTable(tableColumns)
serverNames, err := config.ReadServerNames()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, serverName := range serverNames {
@ -49,34 +58,52 @@ var serverListCommand = cli.Command{
if ctx.Name == serverName {
sp, err := ssh.ParseURL(endpoint)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if sp.Host == "" {
sp.Host = "unknown"
}
if sp.User == "" {
sp.User = "unknown"
}
if sp.Port == "" {
sp.Port = "unknown"
}
row = []string{serverName, sp.Host}
row = []string{serverName, sp.Host, sp.User, sp.Port}
}
}
if len(row) == 0 {
if serverName == "default" {
row = []string{serverName, "local"}
row = []string{serverName, "local", "n/a", "n/a"}
} else {
row = []string{serverName, "unknown"}
row = []string{serverName, "unknown", "unknown", "unknown"}
}
}
if problemsFilter {
for _, val := range row {
if val == "unknown" {
table.Append(row)
break
}
}
} else {
table.Append(row)
}
}
if internal.MachineReadable {
table.JSONRender()
return nil
}
} else {
if problemsFilter && table.NumLines() == 0 {
logrus.Info("all servers wired up correctly 👏")
} else {
table.Render()
}
}
return nil
},

View File

@ -7,8 +7,8 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -31,12 +31,13 @@ var volumesFilterFlag = &cli.BoolFlag{
var serverPruneCommand = cli.Command{
Name: "prune",
Aliases: []string{"p"},
Usage: "Prune resources on a server",
Usage: "Prune a managed server; Runs a docker system prune",
Description: `
Prunes unused containers, networks, and dangling images.
Use "-v/--volumes" to remove volumes that are not associated with a deployed
app. This can result in unwanted data loss if not used carefully.`,
If passing "-v/--volumes" then volumes not connected to a deployed app will
also be removed. This can result in unwanted data loss if not used carefully.
`,
ArgsUsage: "[<server>]",
Flags: []cli.Flag{
allFilterFlag,
@ -52,7 +53,7 @@ app. This can result in unwanted data loss if not used carefully.`,
cl, err := client.New(serverName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
var args filters.Args
@ -60,41 +61,41 @@ app. This can result in unwanted data loss if not used carefully.`,
ctx := context.Background()
cr, err := cl.ContainersPrune(ctx, args)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, args)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Infof("networks pruned: %d", len(nr.NetworksDeleted))
logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
pruneFilters := filters.NewArgs()
if allFilter {
log.Debugf("removing all images, not only dangling ones")
logrus.Debugf("removing all images, not only dangling ones")
pruneFilters.Add("dangling", "false")
}
ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
if volumesFilter {
vr, err := cl.VolumesPrune(ctx, args)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed)
log.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed)
logrus.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed)
}
return nil

View File

@ -8,7 +8,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -17,12 +17,12 @@ var serverRemoveCommand = cli.Command{
Aliases: []string{"rm"},
ArgsUsage: "<server>",
Usage: "Remove a managed server",
Description: `
Remove a managed server.
Description: `Remove a managed server.
Abra will remove the internal bookkeeping (~/.abra/servers/...) and underlying
client connection context. This server will then be lost in time, like tears in
rain.`,
rain.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
@ -34,14 +34,14 @@ rain.`,
serverName := internal.ValidateServer(c)
if err := client.DeleteContext(serverName); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, serverName)); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
log.Infof("%s is now lost in time, like tears in rain", serverName)
logrus.Infof("server at %s has been lost in time, like tears in rain", serverName)
return nil
},

View File

@ -8,21 +8,19 @@ import (
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
charmLog "github.com/charmbracelet/log"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerclient "github.com/docker/docker/client"
"coopcloud.tech/abra/pkg/log"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -54,34 +52,32 @@ var Notify = cli.Command{
},
Before: internal.SubCommandBefore,
Description: `
Read the deployed app versions and look for new versions in the recipe
catalogue.
If a new patch/minor version is available, a notification is printed.
Use "--major" to include new major versions.`,
It reads the deployed app versions and looks for new versions in the recipe
catalogue. If a new patch/minor version is available, a notification is
printed. To include major versions use the --major flag.
`,
Action: func(c *cli.Context) error {
cl, err := client.New("default")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
stacks, err := stack.GetStacks(cl)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, stackInfo := range stacks {
stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if recipeName != "" {
_, err = getLatestUpgrade(cl, stackName, recipeName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
}
@ -105,21 +101,18 @@ var UpgradeApp = cli.Command{
},
Before: internal.SubCommandBefore,
Description: `
Upgrade an app by specifying stack name and recipe.
Use "--all" to upgrade every deployed app.
For each app with auto updates enabled, the deployed version is compared with
the current recipe catalogue version. If a new patch/minor version is
available, the app is upgraded.
To include major versions use the "--major" flag. You probably don't want that
as it will break things. Only apps that are not deployed with "--chaos" are
upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`,
Upgrade an app by specifying its stack name and recipe. By passing "--all"
instead, every deployed app is upgraded. For each apps with enabled auto
updates the deployed version is compared with the current recipe catalogue
version. If a new patch/minor version is available, the app is upgraded. To
include major versions use the "--major" flag. Don't do that, it will probably
break things. Only apps that are not deployed with "--chaos" are upgraded, to
update chaos deployments use the "--chaos" flag. Use it with care.
`,
Action: func(c *cli.Context) error {
cl, err := client.New("default")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if !updateAll {
@ -127,7 +120,7 @@ upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`
recipeName := c.Args().Get(1)
err = tryUpgrade(cl, stackName, recipeName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
return nil
@ -135,19 +128,19 @@ upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`
stacks, err := stack.GetStacks(cl)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
for _, stackInfo := range stacks {
stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
err = tryUpgrade(cl, stackName, recipeName)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}
@ -172,7 +165,7 @@ func getLabel(cl *dockerclient.Client, stackName string, label string) (string,
}
}
log.Debugf("no %s label found for %s", label, stackName)
logrus.Debugf("no %s label found for %s", label, stackName)
return "", nil
}
@ -193,13 +186,13 @@ func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool
return value, nil
}
log.Debugf("boolean label %s could not be found for %s, set default to false.", label, stackName)
logrus.Debugf("Boolean label %s could not be found for %s, set default to false.", label, stackName)
return false, nil
}
// getEnv reads env variables from docker services.
func getEnv(cl *dockerclient.Client, stackName string) (envfile.AppEnv, error) {
func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) {
envMap := make(map[string]string)
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
@ -214,12 +207,12 @@ func getEnv(cl *dockerclient.Client, stackName string) (envfile.AppEnv, error) {
for _, envString := range envList {
splitString := strings.SplitN(envString, "=", 2)
if len(splitString) != 2 {
log.Debugf("can't separate key from value: %s (this variable is probably unset)", envString)
logrus.Debugf("can't separate key from value: %s (this variable is probably unset)", envString)
continue
}
k := splitString[0]
v := splitString[1]
log.Debugf("for %s read env %s with value: %s from docker service", stackName, k, v)
logrus.Debugf("For %s read env %s with value: %s from docker service", stackName, k, v)
envMap[k] = v
}
}
@ -241,14 +234,14 @@ func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName stri
}
if len(availableUpgrades) == 0 {
log.Debugf("no available upgrades for %s", stackName)
logrus.Debugf("no available upgrades for %s", stackName)
return "", nil
}
var chosenUpgrade string
if len(availableUpgrades) > 0 {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
log.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade)
logrus.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade)
}
return chosenUpgrade, nil
@ -256,22 +249,22 @@ func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName stri
// getDeployedVersion returns the currently deployed version of an app.
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
log.Debugf("retrieve deployed version whether %s is already deployed", stackName)
logrus.Debugf("Retrieve deployed version whether %s is already deployed", stackName)
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
return "", err
}
if !deployMeta.IsDeployed {
if !isDeployed {
return "", fmt.Errorf("%s is not deployed?", stackName)
}
if deployMeta.Version == "unknown" {
if deployedVersion == "unknown" {
return "", fmt.Errorf("failed to determine deployed version of %s", stackName)
}
return deployMeta.Version, nil
return deployedVersion, nil
}
// getAvailableUpgrades returns all available versions of an app that are newer
@ -290,7 +283,7 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
}
if len(versions) == 0 {
log.Warnf("no published releases for %s in the recipe catalogue?", recipeName)
logrus.Warnf("no published releases for %s in the recipe catalogue?", recipeName)
return nil, nil
}
@ -316,27 +309,29 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
}
}
log.Debugf("available updates for %s: %s", stackName, availableUpgrades)
logrus.Debugf("Available updates for %s: %s", stackName, availableUpgrades)
return availableUpgrades, nil
}
// processRecipeRepoVersion clones, pulls, checks out the version and lints the
// recipe repository.
func processRecipeRepoVersion(r recipe.Recipe, version string) error {
if err := r.EnsureExists(); err != nil {
func processRecipeRepoVersion(recipeName, version string) error {
if err := recipe.EnsureExists(recipeName); err != nil {
return err
}
if err := r.EnsureUpToDate(); err != nil {
if err := recipe.EnsureUpToDate(recipeName); err != nil {
return err
}
if _, err := r.EnsureVersion(version); err != nil {
if err := recipe.EnsureVersion(recipeName, version); err != nil {
return err
}
if err := lint.LintForErrors(r); err != nil {
if r, err := recipe.Get(recipeName, internal.Offline); err != nil {
return err
} else if err := lint.LintForErrors(r); err != nil {
return err
}
@ -344,14 +339,15 @@ func processRecipeRepoVersion(r recipe.Recipe, version string) error {
}
// mergeAbraShEnv merges abra.sh env vars into the app env vars.
func mergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath)
func mergeAbraShEnv(recipeName string, env config.AppEnv) error {
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
return err
}
for k, v := range abraShEnv {
log.Debugf("read v:%s k: %s", v, k)
logrus.Debugf("read v:%s k: %s", v, k)
env[k] = v
}
@ -359,33 +355,32 @@ func mergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
}
// createDeployConfig merges and enriches the compose config for the deployment.
func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) {
func createDeployConfig(recipeName string, stackName string, env config.AppEnv) (*composetypes.Config, stack.Deploy, error) {
env["STACK_NAME"] = stackName
deployOpts := stack.Deploy{
Namespace: stackName,
Prune: false,
ResolveImage: stack.ResolveImageAlways,
Detach: false,
}
composeFiles, err := r.GetComposeFiles(env)
composeFiles, err := config.GetComposeFiles(recipeName, env)
if err != nil {
return nil, deployOpts, err
}
deployOpts.Composefiles = composeFiles
compose, err := appPkg.GetAppComposeConfig(stackName, deployOpts, env)
compose, err := config.GetAppComposeConfig(stackName, deployOpts, env)
if err != nil {
return nil, deployOpts, err
}
appPkg.ExposeAllEnv(stackName, compose, env)
config.ExposeAllEnv(stackName, compose, env)
// after the upgrade the deployment won't be in chaos state anymore
appPkg.SetChaosLabel(compose, stackName, false)
appPkg.SetRecipeLabel(compose, stackName, r.Name)
appPkg.SetUpdateLabel(compose, stackName, env)
config.SetChaosLabel(compose, stackName, false)
config.SetRecipeLabel(compose, stackName, recipeName)
config.SetUpdateLabel(compose, stackName, env)
return compose, deployOpts, nil
}
@ -393,7 +388,7 @@ func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (
// tryUpgrade performs the upgrade if all the requirements are fulfilled.
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
if recipeName == "" {
log.Debugf("don't update %s due to missing recipe name", stackName)
logrus.Debugf("don't update %s due to missing recipe name", stackName)
return nil
}
@ -403,7 +398,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
}
if chaos && !internal.Chaos {
log.Debugf("don't update %s due to chaos deployment", stackName)
logrus.Debugf("don't update %s due to chaos deployment", stackName)
return nil
}
@ -413,7 +408,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
}
if !updatesEnabled {
log.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName)
logrus.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName)
return nil
}
@ -423,7 +418,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
}
if upgradeVersion == "" {
log.Debugf("don't update %s due to no new version", stackName)
logrus.Debugf("don't update %s due to no new version", stackName)
return nil
}
@ -433,35 +428,34 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
}
// upgrade performs all necessary steps to upgrade an app.
func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string) error {
func upgrade(cl *dockerclient.Client, stackName, recipeName,
upgradeVersion string) error {
env, err := getEnv(cl, stackName)
if err != nil {
return err
}
app := appPkg.App{
app := config.App{
Name: stackName,
Recipe: recipe.Get(recipeName),
Recipe: recipeName,
Server: SERVER,
Env: env,
}
r := recipe.Get(recipeName)
if err = processRecipeRepoVersion(r, upgradeVersion); err != nil {
if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil {
return err
}
if err = mergeAbraShEnv(app.Recipe, app.Env); err != nil {
if err = mergeAbraShEnv(recipeName, app.Env); err != nil {
return err
}
compose, deployOpts, err := createDeployConfig(r, stackName, app.Env)
compose, deployOpts, err := createDeployConfig(recipeName, stackName, app.Env)
if err != nil {
return err
}
log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
logrus.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
err = stack.RunDeploy(cl, deployOpts, compose, stackName, true)
@ -487,8 +481,7 @@ func newAbraApp(version, commit string) *cli.App {
}
app.Before = func(c *cli.Context) error {
charmLog.SetDefault(log.Logger)
log.Debugf("kadabra version %s, commit %s", version, commit)
logrus.Debugf("kadabra version %s, commit %s", version, commit)
return nil
}
@ -500,6 +493,6 @@ func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}

150
go.mod
View File

@ -3,138 +3,118 @@ module coopcloud.tech/abra
go 1.21
require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/log v0.4.0
github.com/distribution/reference v0.6.0
github.com/docker/cli v27.0.3+incompatible
github.com/docker/docker v27.0.3+incompatible
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v24.0.7+incompatible
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v24.0.7+incompatible
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-cmp v0.6.0
github.com/go-git/go-git/v5 v5.10.0
github.com/google/go-cmp v0.5.9
github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.14.4
gopkg.in/yaml.v3 v3.0.1
github.com/schollz/progressbar/v3 v3.14.1
github.com/sirupsen/logrus v1.9.3
gotest.tools/v3 v3.5.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/BurntSushi/toml v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.9.2 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/lipgloss v0.11.0 // indirect
github.com/charmbracelet/x/ansi v0.1.2 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/klauspost/compress v1.14.2 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/miekg/pkcs11 v1.0.3 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runc v1.1.13 // indirect
github.com/opencontainers/runc v1.1.0 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/term v0.14.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/containerd/containerd v1.7.19 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // 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/docker/docker-credential-helpers v0.8.2 // indirect
github.com/fvbommel/sortorder v1.1.0 // 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/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.5
github.com/klauspost/pgzip v1.2.6
github.com/moby/patternmatcher v0.5.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/stretchr/testify v1.9.0
github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/cobra v1.3.0 // indirect
github.com/stretchr/testify v1.8.4
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli v1.22.15
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.22.0
github.com/urfave/cli v1.22.9
github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
golang.org/x/sys v0.14.0
)

651
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -1,597 +1,42 @@
package app
import (
"bufio"
"fmt"
"os"
"path"
"regexp"
"sort"
"strings"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/abra/pkg/log"
loader "coopcloud.tech/abra/pkg/upstream/stack"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types/filters"
"github.com/schollz/progressbar/v3"
"github.com/sirupsen/logrus"
)
// Get retrieves an app
func Get(appName string) (App, error) {
files, err := LoadAppFiles("")
func Get(appName string) (config.App, error) {
files, err := config.LoadAppFiles("")
if err != nil {
return App{}, err
return config.App{}, err
}
app, err := GetApp(files, appName)
app, err := config.GetApp(files, appName)
if err != nil {
return App{}, err
return config.App{}, err
}
log.Debugf("retrieved %s for %s", app, appName)
logrus.Debugf("retrieved %s for %s", app, appName)
return app, nil
}
// GetApp loads an apps settings, reading it from file, in preparation to use
// it. It should only be used when ready to use the env file to keep IO
// operations down.
func GetApp(apps AppFiles, name AppName) (App, error) {
appFile, exists := apps[name]
if !exists {
return App{}, fmt.Errorf("cannot find app with name %s", name)
}
app, err := ReadAppEnvFile(appFile, name)
if err != nil {
return App{}, err
}
return app, nil
// deployedServiceSpec represents a deployed service of an app.
type deployedServiceSpec struct {
Name string
Version string
}
// GetApps returns a slice of Apps with their env files read from a given
// slice of AppFiles.
func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
var apps []App
// VersionSpec represents a deployed app and associated metadata.
type VersionSpec map[string]deployedServiceSpec
for name := range appFiles {
app, err := GetApp(appFiles, name)
if err != nil {
return nil, err
}
if recipeFilter != "" {
if app.Recipe.Name == recipeFilter {
apps = append(apps, app)
}
} else {
apps = append(apps, app)
}
}
return apps, nil
}
// App reprents an app with its env file read into memory
type App struct {
Name AppName
Recipe recipe.Recipe
Domain string
Env envfile.AppEnv
Server string
Path string
}
// Type aliases to make code hints easier to understand
// AppName is AppName
type AppName = string
// AppFile represents app env files on disk without reading the contents
type AppFile struct {
Path string
Server string
}
// AppFiles is a slice of appfiles
type AppFiles map[AppName]AppFile
// See documentation of config.StackName
func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists {
return a.Env["STACK_NAME"]
}
stackName := StackName(a.Name)
a.Env["STACK_NAME"] = stackName
return stackName
}
// StackName gets whatever the docker safe (uses the right delimiting
// character, e.g. "_") stack name is for the app. In general, you don't want
// to use this to show anything to end-users, you want use a.Name instead.
func StackName(appName string) string {
stackName := SanitiseAppName(appName)
if len(stackName) > config.MAX_SANITISED_APP_NAME_LENGTH {
log.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:config.MAX_SANITISED_APP_NAME_LENGTH])
stackName = stackName[:config.MAX_SANITISED_APP_NAME_LENGTH]
}
return stackName
}
// Filters retrieves app filters for querying the container runtime. By default
// it filters on all services in the app. It is also possible to pass an
// otional list of service names, which get filtered instead.
//
// 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, services ...string) (filters.Args, error) {
filters := filters.NewArgs()
if len(services) > 0 {
for _, serviceName := range services {
filters.Add("name", ServiceFilter(a.StackName(), serviceName, exactMatch))
}
return filters, nil
}
// When not appending the service name, just add one filter for the whole
// stack.
if !appendServiceNames {
f := fmt.Sprintf("%s", a.StackName())
if exactMatch {
f = fmt.Sprintf("^%s", f)
}
filters.Add("name", f)
return filters, nil
}
composeFiles, err := a.Recipe.GetComposeFiles(a.Env)
if err != nil {
return filters, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(a.Recipe.Name, opts, a.Env)
if err != nil {
return filters, err
}
for _, service := range compose.Services {
f := ServiceFilter(a.StackName(), service.Name, exactMatch)
filters.Add("name", f)
}
return filters, nil
}
// ServiceFilter creates a filter string for filtering a service in the docker
// container runtime. When exact match is true, it uses regex to match the
// string exactly.
func ServiceFilter(stack, service string, exact bool) string {
if exact {
return fmt.Sprintf("^%s_%s", stack, service)
}
return fmt.Sprintf("%s_%s", stack, service)
}
// ByServer sort a slice of Apps
type ByServer []App
func (a ByServer) Len() int { return len(a) }
func (a ByServer) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
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
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 {
if a[i].Server == a[j].Server {
return strings.ToLower(a[i].Recipe.Name) < strings.ToLower(a[j].Recipe.Name)
}
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByRecipe sort a slice of Apps
type ByRecipe []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.Name) < strings.ToLower(a[j].Recipe.Name)
}
// ByName sort a slice of Apps
type ByName []App
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name)
}
func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
env, err := envfile.ReadEnv(appFile.Path)
if err != nil {
return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.Error())
}
log.Debugf("read env %s from %s", env, appFile.Path)
app, err := NewApp(env, name, appFile)
if err != nil {
return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error())
}
return app, nil
}
// NewApp creates new App object
func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"]
recipeName, exists := env["RECIPE"]
if !exists {
recipeName, exists = env["TYPE"]
if !exists {
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name)
}
}
return App{
Name: name,
Domain: domain,
Recipe: recipe.Get(recipeName),
Env: env,
Server: appFile.Server,
Path: appFile.Path,
}, nil
}
// LoadAppFiles gets all app files for a given set of servers or all servers.
func LoadAppFiles(servers ...string) (AppFiles, error) {
appFiles := make(AppFiles)
if len(servers) == 1 {
if servers[0] == "" {
// Empty servers flag, one string will always be passed
var err error
servers, err = config.GetAllFoldersInDirectory(config.SERVERS_DIR)
if err != nil {
return appFiles, err
}
}
}
log.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", "))
for _, server := range servers {
serverDir := path.Join(config.SERVERS_DIR, server)
files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil {
return appFiles, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server)
}
for _, file := range files {
appName := strings.TrimSuffix(file.Name(), ".env")
appFilePath := path.Join(config.SERVERS_DIR, server, file.Name())
appFiles[appName] = AppFile{
Path: appFilePath,
Server: server,
}
}
}
return appFiles, nil
}
// GetAppServiceNames retrieves a list of app service names.
func GetAppServiceNames(appName string) ([]string, error) {
var serviceNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return serviceNames, err
}
app, err := GetApp(appFiles, appName)
if err != nil {
return serviceNames, err
}
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
return serviceNames, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Recipe.Name, opts, app.Env)
if err != nil {
return serviceNames, err
}
for _, service := range compose.Services {
serviceNames = append(serviceNames, service.Name)
}
return serviceNames, nil
}
// GetAppNames retrieves a list of app names.
func GetAppNames() ([]string, error) {
var appNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return appNames, err
}
apps, err := GetApps(appFiles, "")
if err != nil {
return appNames, err
}
for _, app := range apps {
appNames = append(appNames, app.Name)
}
return appNames, nil
}
// TemplateAppEnvSample copies the example env file for the app into the users
// env files.
func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error {
envSample, err := os.ReadFile(r.SampleEnvPath)
if err != nil {
return err
}
appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
return fmt.Errorf("%s already exists?", appEnvPath)
}
err = os.WriteFile(appEnvPath, envSample, 0o664)
if err != nil {
return err
}
read, err := os.ReadFile(appEnvPath)
if err != nil {
return err
}
newContents := strings.Replace(string(read), r.Name+".example.com", domain, -1)
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
return err
}
log.Debugf("copied & templated %s to %s", r.SampleEnvPath, appEnvPath)
return nil
}
// SanitiseAppName makes a app name usable with Docker by replacing illegal
// characters.
func SanitiseAppName(name string) string {
return strings.ReplaceAll(name, ".", "_")
}
// GetAppStatuses queries servers to check the deployment status of given apps.
func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]string, error) {
statuses := make(map[string]map[string]string)
servers := make(map[string]struct{})
for _, app := range apps {
if _, ok := servers[app.Server]; !ok {
servers[app.Server] = struct{}{}
}
}
var bar *progressbar.ProgressBar
if !MachineReadable {
bar = formatter.CreateProgressbar(len(servers), "querying remote servers...")
}
ch := make(chan stack.StackStatus, len(servers))
for server := range servers {
cl, err := client.New(server)
if err != nil {
return statuses, err
}
go func(s string) {
ch <- stack.GetAllDeployedServices(cl, s)
if !MachineReadable {
bar.Add(1)
}
}(server)
}
for range servers {
status := <-ch
if status.Err != nil {
return statuses, status.Err
}
for _, service := range status.Services {
result := make(map[string]string)
name := service.Spec.Labels[convert.LabelNamespace]
if _, ok := statuses[name]; !ok {
result["status"] = "deployed"
}
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
chaos, ok := service.Spec.Labels[labelKey]
if ok {
result["chaos"] = chaos
}
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
result["chaosVersion"] = chaosVersion
}
labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
result["autoUpdate"] = autoUpdate
} else {
result["autoUpdate"] = "false"
}
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
if version, ok := service.Spec.Labels[labelKey]; ok {
result["version"] = version
} else {
continue
}
statuses[name] = result
}
}
log.Debugf("retrieved app statuses: %s", statuses)
return statuses, nil
}
// GetAppComposeConfig retrieves a compose specification for a recipe. This
// specification is the result of a merge of all the compose.**.yml files in
// the recipe repository.
func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv) (*composetypes.Config, error) {
compose, err := loader.LoadComposefile(opts, appEnv)
if err != nil {
return &composetypes.Config{}, err
}
log.Debugf("retrieved %s for %s", compose.Filename, recipe)
return compose, nil
}
// ExposeAllEnv exposes all env variables to the app container
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debugf("add the following environment to the app service config of %s:", stackName)
for k, v := range appEnv {
_, exists := service.Environment[k]
if !exists {
value := v
service.Environment[k] = &value
log.Debugf("add env var: %s value: %s to %s", k, value, stackName)
}
}
}
}
}
func CheckEnv(app App) ([]envfile.EnvVar, error) {
var envVars []envfile.EnvVar
envSample, err := app.Recipe.SampleEnv()
if err != nil {
return envVars, err
}
var keys []string
for key := range envSample {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if _, ok := app.Env[key]; ok {
envVars = append(envVars, envfile.EnvVar{Name: key, Present: true})
} else {
envVars = append(envVars, envfile.EnvVar{Name: key, Present: false})
}
}
return envVars, nil
}
// ReadAbraShCmdNames reads the names of commands.
func ReadAbraShCmdNames(abraSh string) ([]string, error) {
var cmdNames []string
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return cmdNames, nil
}
return cmdNames, err
}
defer file.Close()
cmdNameRegex, err := regexp.Compile(`(\w+)(\(\).*\{)`)
if err != nil {
return cmdNames, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
matches := cmdNameRegex.FindStringSubmatch(line)
if len(matches) > 0 {
cmdNames = append(cmdNames, matches[1])
}
}
if len(cmdNames) > 0 {
log.Debugf("read %s from %s", strings.Join(cmdNames, " "), abraSh)
} else {
log.Debugf("read 0 command names from %s", abraSh)
}
return cmdNames, nil
}
func (a App) WriteRecipeVersion(version string) error {
file, err := os.Open(a.Path)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
lines := []string{}
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, "RECIPE=") && !strings.Contains(line, "TYPE") {
lines = append(lines, line)
continue
}
splitted := strings.Split(line, ":")
line = fmt.Sprintf("%s:%s", splitted[0], version)
lines = append(lines, line)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
return os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm)
// ParseServiceName parses a $STACK_NAME_$SERVICE_NAME service label.
func ParseServiceName(label string) string {
idx := strings.LastIndex(label, "_")
serviceName := label[idx+1:]
logrus.Debugf("parsed %s as service name from %s", serviceName, label)
return serviceName
}

View File

@ -1,88 +0,0 @@
package app
import (
"fmt"
"strconv"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/log"
composetypes "github.com/docker/cli/cli/compose/types"
)
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
// to signal which recipe is connected to the deployed app
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debugf("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
service.Deploy.Labels[labelKey] = recipe
}
}
}
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
// to signal if the app is deployed in chaos mode
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debugf("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
}
}
}
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debugf("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName)
service.Deploy.Labels[labelKey] = chaosVersion
}
}
}
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
// auto update process for this app. The default if this variable is not set is to disable
// the auto update process.
func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv envfile.AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
if !exists {
enable_auto_update = "false"
}
log.Debugf("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
service.Deploy.Labels[labelKey] = enable_auto_update
}
}
}
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
for _, service := range compose.Services {
if service.Name == "app" {
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
log.Debugf("get label '%s'", labelKey)
if labelValue, ok := service.Deploy.Labels[labelKey]; ok {
return labelValue
}
}
}
log.Debugf("no %s label found for %s", label, stackName)
return ""
}
// GetTimeoutFromLabel reads the timeout value from docker label "coop-cloud.${STACK_NAME}.TIMEOUT" and returns 50 as default value
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
timeout := 50 // Default Timeout
var err error = nil
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
log.Debugf("timeout label: %s", timeoutLabel)
timeout, err = strconv.Atoi(timeoutLabel)
}
return timeout, err
}

View File

@ -3,17 +3,17 @@ package autocomplete
import (
"fmt"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// AppNameComplete copletes app names.
func AppNameComplete(c *cli.Context) {
appNames, err := app.GetAppNames()
appNames, err := config.GetAppNames()
if err != nil {
log.Warn(err)
logrus.Warn(err)
}
if c.NArg() > 0 {
@ -26,7 +26,7 @@ func AppNameComplete(c *cli.Context) {
}
func ServiceNameComplete(appName string) {
serviceNames, err := app.GetAppServiceNames(appName)
serviceNames, err := config.GetAppServiceNames(appName)
if err != nil {
return
}
@ -39,7 +39,7 @@ func ServiceNameComplete(appName string) {
func RecipeNameComplete(c *cli.Context) {
catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil {
log.Warn(err)
logrus.Warn(err)
}
if c.NArg() > 0 {
@ -55,7 +55,7 @@ func RecipeNameComplete(c *cli.Context) {
func RecipeVersionComplete(recipeName string) {
catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil {
log.Warn(err)
logrus.Warn(err)
}
for _, v := range catl[recipeName].Versions {
@ -67,9 +67,9 @@ func RecipeVersionComplete(recipeName string) {
// ServerNameComplete completes server names.
func ServerNameComplete(c *cli.Context) {
files, err := app.LoadAppFiles("")
files, err := config.LoadAppFiles("")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if c.NArg() > 0 {

View File

@ -8,21 +8,21 @@ import (
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
// 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) {
log.Warnf("local recipe catalogue is missing, retrieving now")
logrus.Warnf("local recipe catalogue is missing, retrieving now")
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err
}
log.Debugf("cloned catalogue repository to %s", catalogueDir)
logrus.Debugf("cloned catalogue repository to %s", catalogueDir)
}
return nil
@ -57,7 +57,7 @@ func EnsureUpToDate() error {
if len(remotes) == 0 {
msg := "cannot ensure %s is up-to-date, no git remotes configured"
log.Debugf(msg, config.CATALOGUE_DIR)
logrus.Debugf(msg, config.CATALOGUE_DIR)
return nil
}
@ -82,7 +82,7 @@ func EnsureUpToDate() error {
}
}
log.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR)
logrus.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR)
return nil
}

View File

@ -10,32 +10,17 @@ import (
"time"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/log"
sshPkg "coopcloud.tech/abra/pkg/ssh"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// Conf is a Docker client configuration.
type Conf struct {
Timeout int
}
// Opt is a Docker client option.
type Opt func(c *Conf)
// WithTimeout specifies a timeout for a Docker client.
func WithTimeout(timeout int) Opt {
return func(c *Conf) {
c.Timeout = timeout
}
}
// New initiates a new Docker client. New client connections are validated so
// that we ensure connections via SSH to the daemon can succeed. It takes into
// account that you may only want the local client and not communicate via SSH.
// For this use-case, please pass "default" as the contextName.
func New(serverName string, opts ...Opt) (*client.Client, error) {
func New(serverName string) (*client.Client, error) {
var clientOpts []client.Opt
if serverName != "default" {
@ -49,12 +34,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
return nil, err
}
conf := &Conf{}
for _, opt := range opts {
opt(conf)
}
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint, conf.Timeout)
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint)
if err != nil {
return nil, err
}
@ -85,7 +65,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
return nil, err
}
log.Debugf("created client for %s", serverName)
logrus.Debugf("created client for %s", serverName)
info, err := cl.Info(context.Background())
if err != nil {
@ -95,10 +75,10 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
if info.Swarm.LocalNodeState == "inactive" {
if serverName != "default" {
return cl, fmt.Errorf("swarm mode not enabled on %s?", serverName)
}
} else {
return cl, errors.New("swarm mode not enabled on local server?")
}
}
return cl, nil
}

View File

@ -5,25 +5,28 @@ import (
"fmt"
"coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/log"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
dConfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/context/docker"
contextStore "github.com/docker/cli/cli/context/store"
"github.com/sirupsen/logrus"
)
type Context = contextStore.Metadata
// CreateContext creates a new Docker context.
func CreateContext(contextName string) error {
host := fmt.Sprintf("ssh://%s", contextName)
func CreateContext(contextName string, user string, port string) error {
host := contextName
if user != "" {
host = fmt.Sprintf("%s@%s", user, host)
}
if port != "" {
host = fmt.Sprintf("%s:%s", host, port)
}
host = fmt.Sprintf("ssh://%s", host)
if err := createContext(contextName, host); err != nil {
return err
}
log.Debugf("created the %s context", contextName)
logrus.Debugf("created the %s context", contextName)
return nil
}

View File

@ -6,7 +6,7 @@ import (
"github.com/containers/image/docker"
"github.com/containers/image/types"
"github.com/distribution/reference"
"github.com/docker/distribution/reference"
)
// GetRegistryTags retrieves all tags of an image from a container registry.

View File

@ -5,10 +5,10 @@ import (
"fmt"
"time"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
func GetVolumes(cl *client.Client, ctx context.Context, server string, fs filters.Args) ([]*volume.Volume, error) {
@ -54,7 +54,7 @@ func retryFunc(retries int, fn func() error) error {
}
if i+1 < retries {
sleep := time.Duration(i+1) * time.Duration(i+1)
log.Infof("%s: waiting %d seconds before next retry", err, sleep)
logrus.Infof("%s: waiting %d seconds before next retry", err, sleep)
time.Sleep(sleep * time.Second)
}
}

158
pkg/compose/compose.go Normal file
View File

@ -0,0 +1,158 @@
package compose
import (
"fmt"
"io/ioutil"
"path"
"path/filepath"
"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"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
)
// UpdateTag updates an image tag in-place on file system local compose files.
func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return false, err
}
logrus.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return false, err
}
compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return false, err
}
for _, service := range compose.Services {
if service.Image == "" {
continue // may be a compose.$optional.yml file
}
img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return false, err
}
var composeTag string
switch img.(type) {
case reference.NamedTagged:
composeTag = img.(reference.NamedTagged).Tag()
default:
logrus.Debugf("unable to parse %s, skipping", img)
continue
}
composeImage := formatter.StripTagMeta(reference.Path(img))
logrus.Debugf("parsed %s from %s", composeTag, service.Image)
if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return false, err
}
old := fmt.Sprintf("%s:%s", composeImage, composeTag)
new := fmt.Sprintf("%s:%s", composeImage, tag)
replacedBytes := strings.Replace(string(bytes), old, new, -1)
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 false, nil
}
// UpdateLabel updates a label in-place on file system local compose files.
func UpdateLabel(pattern, serviceName, label, recipeName string) error {
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return err
}
logrus.Debugf("considering %s config(s) for label update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return err
}
compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return err
}
serviceExists := false
var service composetypes.ServiceConfig
for _, s := range compose.Services {
if s.Name == serviceName {
service = s
serviceExists = true
}
}
if !serviceExists {
continue
}
discovered := false
for oldLabel, value := range service.Deploy.Labels {
if strings.HasPrefix(oldLabel, "coop-cloud") && strings.Contains(oldLabel, "version") {
discovered = true
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return err
}
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
replacedBytes := strings.Replace(string(bytes), old, label, -1)
if old == label {
logrus.Warnf("%s is already set, nothing to do?", label)
return nil
}
logrus.Debugf("updating %s to %s in %s", old, label, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
return err
}
logrus.Infof("synced label %s to service %s", label, serviceName)
}
}
if !discovered {
logrus.Warn("no existing label found, automagic insertion not supported yet")
logrus.Fatalf("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile)
}
}
return nil
}

View File

@ -1,110 +0,0 @@
package config
import (
"os"
"path"
"path/filepath"
"coopcloud.tech/abra/pkg/log"
"gopkg.in/yaml.v3"
)
// LoadAbraConfig returns the abra configuration. It tries to find a abra
// configuration file (see findAbraConfig for lookup logic). When no
// configuration was found it returns the default config.
func LoadAbraConfig() Abra {
wd, _ := os.Getwd()
configFile := findAbraConfig(wd)
if configFile == "" {
log.Debugf("no config file found")
return Abra{}
}
data, err := os.ReadFile(configFile)
if err != nil {
// Do nothing, when an error occurs
log.Debugf("error reading config file: %s", err)
return Abra{}
}
config := Abra{}
err = yaml.Unmarshal(data, &config)
if err != nil {
// Do nothing, when an error occurs
log.Debugf("error loading config file: %s", err)
return Abra{}
}
log.Debugf("config file loaded from: %s", configFile)
config.configPath = filepath.Dir(configFile)
return config
}
// findAbraConfig recursively looks for a abra.y(a)ml file in the given directory.
// When the file was not found it calls the function again with the parent
// directory until the home directory is hit. When no abra config was found it
// returns an empty string.
func findAbraConfig(dir string) string {
dir, err := filepath.Abs(dir)
if err != nil {
return ""
}
if dir == os.ExpandEnv("$HOME") || dir == "/" {
return ""
}
p := path.Join(dir, "abra.yaml")
if _, err := os.Stat(p); err == nil {
return p
}
p = path.Join(dir, "abra.yml")
if _, err := os.Stat(p); err == nil {
return p
}
return findAbraConfig(filepath.Dir(dir))
}
// Abra defines the configuration file for abra.
type Abra struct {
configPath string
AbraDir string `yaml:"abraDir"`
}
// GetAbraDir returns the abra dir. It has the following logic:
// 1. check if $ABRA_DIR is set
// 2. check if abraDir was set in a config file
// 3. use $HOME/.abra when above two options failed
func (a Abra) GetAbraDir() string {
if dir, exists := os.LookupEnv("ABRA_DIR"); exists && dir != "" {
log.Debug("read abra dir from $ABRA_DIR")
return dir
}
if a.AbraDir != "" {
log.Debug("read abra dir from config file")
if path.IsAbs(a.AbraDir) {
return a.AbraDir
}
// Make the path absolute
return path.Join(a.configPath, a.AbraDir)
}
log.Debug("using default abra dir")
return os.ExpandEnv("$HOME/.abra")
}
func (a Abra) GetServersDir() string { return path.Join(a.GetAbraDir(), "servers") }
func (a Abra) GetRecipesDir() string { return path.Join(a.GetAbraDir(), "recipes") }
func (a Abra) GetVendorDir() string { return path.Join(a.GetAbraDir(), "vendor") }
func (a Abra) GetBackupDir() string { return path.Join(a.GetAbraDir(), "backups") }
func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catalogue") }
var config = LoadAbraConfig()
var (
ABRA_DIR = config.GetAbraDir()
SERVERS_DIR = config.GetServersDir()
RECIPES_DIR = config.GetRecipesDir()
VENDOR_DIR = config.GetVendorDir()
BACKUP_DIR = config.GetBackupDir()
CATALOGUE_DIR = config.GetCatalogueDir()
RECIPES_JSON = path.Join(config.GetCatalogueDir(), "recipes.json")
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
)

View File

@ -1,133 +0,0 @@
package config
import (
"log"
"os"
"path/filepath"
"testing"
)
func TestFindAbraConfig(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
tests := []struct {
Dir string
Config string
}{
{
Dir: "testdata/abraconfig1",
Config: filepath.Join(wd, "testdata/abraconfig1/abra.yaml"),
},
{
Dir: "testdata/abraconfig1/subdir",
Config: filepath.Join(wd, "testdata/abraconfig1/abra.yaml"),
},
{
Dir: "testdata/abraconfig2",
Config: filepath.Join(wd, "testdata/abraconfig2/abra.yml"),
},
{
Dir: "testdata/abraconfig2/subdir",
Config: filepath.Join(wd, "testdata/abraconfig2/abra.yml"),
},
{
Dir: "testdata",
Config: "",
},
}
for _, tc := range tests {
t.Run(tc.Dir, func(t *testing.T) {
config := findAbraConfig(tc.Dir)
if config != tc.Config {
t.Errorf("\nwant: %s\ngot: %s", tc.Config, config)
}
})
}
}
func TestLoadAbraConfigGetAbraDir(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
t.Setenv("ABRA_DIR", "")
t.Run("default", func(t *testing.T) {
cfg := LoadAbraConfig()
wantAbraDir := os.ExpandEnv("$HOME/.abra")
if cfg.GetAbraDir() != wantAbraDir {
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
}
})
t.Run("from config file", func(t *testing.T) {
t.Cleanup(func() { os.Chdir(wd) })
err = os.Chdir(filepath.Join(wd, "testdata/abraconfig1"))
if err != nil {
log.Fatal(err)
}
cfg := LoadAbraConfig()
wantAbraDir := filepath.Join(wd, "testdata/abraconfig1/foobar")
if cfg.GetAbraDir() != wantAbraDir {
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
}
})
t.Run("default when config file is empty", func(t *testing.T) {
t.Cleanup(func() { os.Chdir(wd) })
err := os.Chdir(filepath.Join(wd, "testdata/abraconfig2"))
if err != nil {
log.Fatal(err)
}
cfg := LoadAbraConfig()
wantAbraDir := os.ExpandEnv("$HOME/.abra")
if cfg.GetAbraDir() != wantAbraDir {
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
}
})
t.Run("from env variable", func(t *testing.T) {
t.Setenv("ABRA_DIR", "foo")
cfg := LoadAbraConfig()
wantAbraDir := "foo"
if cfg.GetAbraDir() != wantAbraDir {
t.Errorf("\nwant: %s\ngot: %s", wantAbraDir, cfg.GetAbraDir())
}
})
}
func TestLoadAbraConfigServersDir(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
t.Setenv("ABRA_DIR", "")
t.Run("default", func(t *testing.T) {
cfg := LoadAbraConfig()
wantServersDir := os.ExpandEnv("$HOME/.abra/servers")
if cfg.GetServersDir() != wantServersDir {
t.Errorf("\nwant: %s\ngot: %s", wantServersDir, cfg.GetServersDir())
}
})
t.Run("from config file", func(t *testing.T) {
t.Cleanup(func() { os.Chdir(wd) })
err = os.Chdir(filepath.Join(wd, "testdata/abraconfig1"))
if err != nil {
log.Fatal(err)
}
cfg := LoadAbraConfig()
log.Println(cfg)
wantServersDir := filepath.Join(wd, "testdata/abraconfig1/foobar/servers")
if cfg.GetServersDir() != wantServersDir {
t.Errorf("\nwant: %s\ngot: %s", wantServersDir, cfg.GetServersDir())
}
})
}

627
pkg/config/app.go Normal file
View File

@ -0,0 +1,627 @@
package config
import (
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"github.com/schollz/progressbar/v3"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/upstream/convert"
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"
)
// Type aliases to make code hints easier to understand
// AppEnv is a map of the values in an apps env config
type AppEnv = map[string]string
// AppModifiers is a map of modifiers in an apps env config
type AppModifiers = map[string]map[string]string
// AppName is AppName
type AppName = string
// AppFile represents app env files on disk without reading the contents
type AppFile struct {
Path string
Server string
}
// AppFiles is a slice of appfiles
type AppFiles map[AppName]AppFile
// App reprents an app with its env file read into memory
type App struct {
Name AppName
Recipe string
Domain string
Env AppEnv
Server string
Path string
}
// See documentation of config.StackName
func (a App) StackName() string {
if _, exists := a.Env["STACK_NAME"]; exists {
return a.Env["STACK_NAME"]
}
stackName := StackName(a.Name)
a.Env["STACK_NAME"] = stackName
return stackName
}
// StackName gets whatever the docker safe (uses the right delimiting
// character, e.g. "_") stack name is for the app. In general, you don't want
// to use this to show anything to end-users, you want use a.Name instead.
func StackName(appName string) string {
stackName := SanitiseAppName(appName)
if len(stackName) > MAX_SANITISED_APP_NAME_LENGTH {
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:MAX_SANITISED_APP_NAME_LENGTH])
stackName = stackName[:MAX_SANITISED_APP_NAME_LENGTH]
}
return stackName
}
// Filters retrieves app filters for querying the container runtime. By default
// it filters on all services in the app. It is also possible to pass an
// otional list of service names, which get filtered instead.
//
// 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, services ...string) (filters.Args, error) {
filters := filters.NewArgs()
if len(services) > 0 {
for _, serviceName := range services {
filters.Add("name", ServiceFilter(a.StackName(), serviceName, exactMatch))
}
return filters, nil
}
// When not appending the service name, just add one filter for the whole
// stack.
if !appendServiceNames {
f := fmt.Sprintf("%s", a.StackName())
if exactMatch {
f = fmt.Sprintf("^%s", f)
}
filters.Add("name", f)
return filters, nil
}
composeFiles, err := GetComposeFiles(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 {
f := ServiceFilter(a.StackName(), service.Name, exactMatch)
filters.Add("name", f)
}
return filters, nil
}
// ServiceFilter creates a filter string for filtering a service in the docker
// container runtime. When exact match is true, it uses regex to match the
// string exactly.
func ServiceFilter(stack, service string, exact bool) string {
if exact {
return fmt.Sprintf("^%s_%s", stack, service)
}
return fmt.Sprintf("%s_%s", stack, service)
}
// ByServer sort a slice of Apps
type ByServer []App
func (a ByServer) Len() int { return len(a) }
func (a ByServer) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
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
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 {
if a[i].Server == a[j].Server {
return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe)
}
return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server)
}
// ByRecipe sort a slice of Apps
type ByRecipe []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)
}
// ByName sort a slice of Apps
type ByName []App
func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
return strings.ToLower(a[i].Name) < strings.ToLower(a[j].Name)
}
func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
env, err := ReadEnv(appFile.Path)
if err != nil {
return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.Error())
}
logrus.Debugf("read env %s from %s", env, appFile.Path)
app, err := NewApp(env, name, appFile)
if err != nil {
return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error())
}
return app, nil
}
// NewApp creates new App object
func NewApp(env AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"]
recipe, exists := env["RECIPE"]
if !exists {
recipe, exists = env["TYPE"]
if !exists {
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name)
}
}
return App{
Name: name,
Domain: domain,
Recipe: recipe,
Env: env,
Server: appFile.Server,
Path: appFile.Path,
}, nil
}
// LoadAppFiles gets all app files for a given set of servers or all servers.
func LoadAppFiles(servers ...string) (AppFiles, error) {
appFiles := make(AppFiles)
if len(servers) == 1 {
if servers[0] == "" {
// Empty servers flag, one string will always be passed
var err error
servers, err = GetAllFoldersInDirectory(SERVERS_DIR)
if err != nil {
return appFiles, err
}
}
}
logrus.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", "))
for _, server := range servers {
serverDir := path.Join(SERVERS_DIR, server)
files, err := GetAllFilesInDirectory(serverDir)
if err != nil {
return appFiles, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server)
}
for _, file := range files {
appName := strings.TrimSuffix(file.Name(), ".env")
appFilePath := path.Join(SERVERS_DIR, server, file.Name())
appFiles[appName] = AppFile{
Path: appFilePath,
Server: server,
}
}
}
return appFiles, nil
}
// GetApp loads an apps settings, reading it from file, in preparation to use
// it. It should only be used when ready to use the env file to keep IO
// operations down.
func GetApp(apps AppFiles, name AppName) (App, error) {
appFile, exists := apps[name]
if !exists {
return App{}, fmt.Errorf("cannot find app with name %s", name)
}
app, err := ReadAppEnvFile(appFile, name)
if err != nil {
return App{}, err
}
return app, nil
}
// GetApps returns a slice of Apps with their env files read from a given
// slice of AppFiles.
func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
var apps []App
for name := range appFiles {
app, err := GetApp(appFiles, name)
if err != nil {
return nil, err
}
if recipeFilter != "" {
if app.Recipe == recipeFilter {
apps = append(apps, app)
}
} else {
apps = append(apps, app)
}
}
return apps, nil
}
// GetAppServiceNames retrieves a list of app service names.
func GetAppServiceNames(appName string) ([]string, error) {
var serviceNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return serviceNames, err
}
app, err := GetApp(appFiles, appName)
if err != nil {
return serviceNames, err
}
composeFiles, err := GetComposeFiles(app.Recipe, app.Env)
if err != nil {
return serviceNames, err
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := GetAppComposeConfig(app.Recipe, opts, app.Env)
if err != nil {
return serviceNames, err
}
for _, service := range compose.Services {
serviceNames = append(serviceNames, service.Name)
}
return serviceNames, nil
}
// GetAppNames retrieves a list of app names.
func GetAppNames() ([]string, error) {
var appNames []string
appFiles, err := LoadAppFiles("")
if err != nil {
return appNames, err
}
apps, err := GetApps(appFiles, "")
if err != nil {
return appNames, err
}
for _, app := range apps {
appNames = append(appNames, app.Name)
}
return appNames, nil
}
// TemplateAppEnvSample copies the example env file for the app into the users
// env files.
func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
envSamplePath := path.Join(RECIPES_DIR, recipeName, ".env.sample")
envSample, err := ioutil.ReadFile(envSamplePath)
if err != nil {
return err
}
appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
return fmt.Errorf("%s already exists?", appEnvPath)
}
err = ioutil.WriteFile(appEnvPath, envSample, 0o664)
if err != nil {
return err
}
read, err := ioutil.ReadFile(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 {
return err
}
logrus.Debugf("copied & templated %s to %s", envSamplePath, appEnvPath)
return nil
}
// SanitiseAppName makes a app name usable with Docker by replacing illegal
// characters.
func SanitiseAppName(name string) string {
return strings.ReplaceAll(name, ".", "_")
}
// GetAppStatuses queries servers to check the deployment status of given apps.
func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]string, error) {
statuses := make(map[string]map[string]string)
servers := make(map[string]struct{})
for _, app := range apps {
if _, ok := servers[app.Server]; !ok {
servers[app.Server] = struct{}{}
}
}
var bar *progressbar.ProgressBar
if !MachineReadable {
bar = formatter.CreateProgressbar(len(servers), "querying remote servers...")
}
ch := make(chan stack.StackStatus, len(servers))
for server := range servers {
cl, err := client.New(server)
if err != nil {
return statuses, err
}
go func(s string) {
ch <- stack.GetAllDeployedServices(cl, s)
if !MachineReadable {
bar.Add(1)
}
}(server)
}
for range servers {
status := <-ch
if status.Err != nil {
return statuses, status.Err
}
for _, service := range status.Services {
result := make(map[string]string)
name := service.Spec.Labels[convert.LabelNamespace]
if _, ok := statuses[name]; !ok {
result["status"] = "deployed"
}
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
chaos, ok := service.Spec.Labels[labelKey]
if ok {
result["chaos"] = chaos
}
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
result["chaosVersion"] = chaosVersion
}
labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
result["autoUpdate"] = autoUpdate
} else {
result["autoUpdate"] = "false"
}
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
if version, ok := service.Spec.Labels[labelKey]; ok {
result["version"] = version
} else {
continue
}
statuses[name] = result
}
}
logrus.Debugf("retrieved app statuses: %s", statuses)
return statuses, nil
}
// ensurePathExists ensures that a path exists.
func ensurePathExists(path string) error {
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
return err
}
return nil
}
// GetComposeFiles gets the list of compose files for an app (or recipe if you
// don't already have an app) which should be merged into a composetypes.Config
// while respecting the COMPOSE_FILE env var.
func GetComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
var composeFiles []string
composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
if !ok {
path := fmt.Sprintf("%s/%s/compose.yml", RECIPES_DIR, recipe)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
logrus.Debugf("no COMPOSE_FILE detected, loading default: %s", path)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
if !strings.Contains(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, composeFileEnvVar)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
logrus.Debugf("COMPOSE_FILE detected, loading %s", path)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
if len(envVars) != numComposeFiles {
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
}
for _, file := range envVars {
path := fmt.Sprintf("%s/%s/%s", RECIPES_DIR, recipe, file)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
composeFiles = append(composeFiles, path)
}
logrus.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
logrus.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe)
return composeFiles, nil
}
// GetAppComposeConfig retrieves a compose specification for a recipe. This
// specification is the result of a merge of all the compose.**.yml files in
// the recipe repository.
func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*composetypes.Config, error) {
compose, err := loader.LoadComposefile(opts, appEnv)
if err != nil {
return &composetypes.Config{}, err
}
logrus.Debugf("retrieved %s for %s", compose.Filename, recipe)
return compose, nil
}
// ExposeAllEnv exposes all env variables to the app container
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("Add the following environment to the app service config of %s:", stackName)
for k, v := range appEnv {
_, exists := service.Environment[k]
if !exists {
value := v
service.Environment[k] = &value
logrus.Debugf("Add Key: %s Value: %s to %s", k, value, stackName)
}
}
}
}
}
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
// to signal which recipe is connected to the deployed app
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
service.Deploy.Labels[labelKey] = recipe
}
}
}
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
// to signal if the app is deployed in chaos mode
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos' to %v for %s", stackName, chaos, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
}
}
}
// SetChaosVersionLabel adds the label 'coop-cloud.${STACK_NAME}.chaos-version=$(GIT_COMMIT)' to the app container
func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosVersion string) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos-version' to %v for %s", stackName, chaosVersion, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos-version", stackName)
service.Deploy.Labels[labelKey] = chaosVersion
}
}
}
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
// auto update process for this app. The default if this variable is not set is to disable
// the auto update process.
func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
if !exists {
enable_auto_update = "false"
}
logrus.Debugf("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
service.Deploy.Labels[labelKey] = enable_auto_update
}
}
}
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
for _, service := range compose.Services {
if service.Name == "app" {
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
logrus.Debugf("get label '%s'", labelKey)
if labelValue, ok := service.Deploy.Labels[labelKey]; ok {
return labelValue
}
}
}
logrus.Debugf("no %s label found for %s", label, stackName)
return ""
}
// GetTimeoutFromLabel reads the timeout value from docker label "coop-cloud.${STACK_NAME}.TIMEOUT" and returns 50 as default value
func GetTimeoutFromLabel(compose *composetypes.Config, stackName string) (int, error) {
timeout := 50 // Default Timeout
var err error = nil
if timeoutLabel := GetLabel(compose, stackName, "timeout"); timeoutLabel != "" {
logrus.Debugf("timeout label: %s", timeoutLabel)
timeout, err = strconv.Atoi(timeoutLabel)
}
return timeout, err
}

View File

@ -1,4 +1,4 @@
package app_test
package config_test
import (
"encoding/json"
@ -6,49 +6,46 @@ import (
"reflect"
"testing"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/recipe"
testPkg "coopcloud.tech/abra/pkg/test"
"github.com/docker/docker/api/types/filters"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
func TestNewApp(t *testing.T) {
app, err := appPkg.NewApp(testPkg.ExpectedAppEnv, testPkg.AppName, testPkg.ExpectedAppFile)
app, err := config.NewApp(ExpectedAppEnv, AppName, ExpectedAppFile)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
if !reflect.DeepEqual(app, ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
}
}
func TestReadAppEnvFile(t *testing.T) {
app, err := appPkg.ReadAppEnvFile(testPkg.ExpectedAppFile, testPkg.AppName)
app, err := config.ReadAppEnvFile(ExpectedAppFile, AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
if !reflect.DeepEqual(app, ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
}
}
func TestGetApp(t *testing.T) {
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
app, err := config.GetApp(ExpectedAppFiles, AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
if !reflect.DeepEqual(app, ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, ExpectedApp)
}
}
func TestGetComposeFiles(t *testing.T) {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
@ -60,32 +57,32 @@ func TestGetComposeFiles(t *testing.T) {
{
map[string]string{},
[]string{
fmt.Sprintf("%s/compose.yml", r.Dir),
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name),
},
},
{
map[string]string{"COMPOSE_FILE": "compose.yml"},
[]string{
fmt.Sprintf("%s/compose.yml", r.Dir),
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name),
},
},
{
map[string]string{"COMPOSE_FILE": "compose.extra_secret.yml"},
[]string{
fmt.Sprintf("%s/compose.extra_secret.yml", r.Dir),
fmt.Sprintf("%s/%s/compose.extra_secret.yml", config.RECIPES_DIR, r.Name),
},
},
{
map[string]string{"COMPOSE_FILE": "compose.yml:compose.extra_secret.yml"},
[]string{
fmt.Sprintf("%s/compose.yml", r.Dir),
fmt.Sprintf("%s/compose.extra_secret.yml", r.Dir),
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name),
fmt.Sprintf("%s/%s/compose.extra_secret.yml", config.RECIPES_DIR, r.Name),
},
},
}
for _, test := range tests {
composeFiles, err := r.GetComposeFiles(test.appEnv)
composeFiles, err := config.GetComposeFiles(r.Name, test.appEnv)
if err != nil {
t.Fatal(err)
}
@ -94,8 +91,8 @@ func TestGetComposeFiles(t *testing.T) {
}
func TestGetComposeFilesError(t *testing.T) {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
@ -106,7 +103,7 @@ func TestGetComposeFilesError(t *testing.T) {
}
for _, test := range tests {
_, err := r.GetComposeFiles(test.appEnv)
_, err := config.GetComposeFiles(r.Name, test.appEnv)
if err == nil {
t.Fatalf("should have failed: %v", test.appEnv)
}
@ -115,16 +112,16 @@ func TestGetComposeFilesError(t *testing.T) {
func TestFilters(t *testing.T) {
oldDir := config.RECIPES_DIR
config.RECIPES_DIR = "./testdata"
config.RECIPES_DIR = "./testdir"
defer func() {
config.RECIPES_DIR = oldDir
}()
app, err := appPkg.NewApp(envfile.AppEnv{
app, err := config.NewApp(config.AppEnv{
"DOMAIN": "test.example.com",
"RECIPE": "test-recipe",
}, "test_example_com", appPkg.AppFile{
Path: "./testdata/filtertest.end",
}, "test_example_com", config.AppFile{
Path: "./testdir/filtertest.end",
Server: "local",
})
if err != nil {

View File

@ -1,22 +1,51 @@
package config
import (
"bufio"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"coopcloud.tech/abra/pkg/log"
"git.coopcloud.tech/coop-cloud/godotenv"
"github.com/sirupsen/logrus"
)
// getBaseDir retrieves the Abra base directory.
func getBaseDir() string {
home := os.ExpandEnv("$HOME/.abra")
if customAbraDir, exists := os.LookupEnv("ABRA_DIR"); exists && customAbraDir != "" {
home = customAbraDir
}
return home
}
var ABRA_DIR = getBaseDir()
var SERVERS_DIR = path.Join(ABRA_DIR, "servers")
var RECIPES_DIR = path.Join(ABRA_DIR, "recipes")
var VENDOR_DIR = path.Join(ABRA_DIR, "vendor")
var BACKUP_DIR = path.Join(ABRA_DIR, "backups")
var CATALOGUE_DIR = path.Join(ABRA_DIR, "catalogue")
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"
const MAX_SANITISED_APP_NAME_LENGTH = 45
const MAX_DOCKER_SECRET_LENGTH = 64
var BackupbotLabel = "coop-cloud.backupbot.enabled"
// envVarModifiers is a list of env var modifier strings. These are added to
// env vars as comments and modify their processing by Abra, e.g. determining
// how long secrets should be.
var envVarModifiers = []string{"length"}
// GetServers retrieves all servers.
func GetServers() ([]string, error) {
var servers []string
@ -26,11 +55,39 @@ func GetServers() ([]string, error) {
return servers, err
}
log.Debugf("retrieved %v servers: %s", len(servers), servers)
logrus.Debugf("retrieved %v servers: %s", len(servers), servers)
return servers, nil
}
// ReadEnv loads an app envivornment into a map.
func ReadEnv(filePath string) (AppEnv, error) {
var envVars AppEnv
envVars, _, err := godotenv.Read(filePath)
if err != nil {
return nil, err
}
logrus.Debugf("read %s from %s", envVars, filePath)
return envVars, nil
}
// ReadEnv loads an app envivornment and their modifiers in two different maps.
func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
var envVars AppEnv
envVars, mods, err := godotenv.Read(filePath)
if err != nil {
return nil, mods, err
}
logrus.Debugf("read %s from %s", envVars, filePath)
return envVars, mods, nil
}
// ReadServerNames retrieves all server names.
func ReadServerNames() ([]string, error) {
serverNames, err := GetAllFoldersInDirectory(SERVERS_DIR)
@ -39,7 +96,7 @@ func ReadServerNames() ([]string, error) {
return nil, err
}
log.Debugf("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR)
logrus.Debugf("read %s from %s", strings.Join(serverNames, ","), SERVERS_DIR)
return serverNames, nil
}
@ -63,7 +120,7 @@ func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
realPath, err := filepath.EvalSymlinks(filePath)
if err != nil {
log.Warnf("broken symlink in your abra config folders: %s", filePath)
logrus.Warningf("broken symlink in your abra config folders: %s", filePath)
} else {
realFile, err := os.Stat(realPath)
if err != nil {
@ -96,7 +153,7 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
filePath := path.Join(directory, file.Name())
realDir, err := filepath.EvalSymlinks(filePath)
if err != nil {
log.Warnf("broken symlink in your abra config folders: %s", filePath)
logrus.Warningf("broken symlink in your abra config folders: %s", filePath)
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
// path is a directory
folders = append(folders, file.Name())
@ -106,3 +163,119 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
return folders, nil
}
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
envVars := make(map[string]string)
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return envVars, nil
}
return envVars, err
}
defer file.Close()
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
if err != nil {
return envVars, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
txt := scanner.Text()
if exportRegex.MatchString(txt) {
splitVals := strings.Split(txt, "export ")
envVarDef := splitVals[len(splitVals)-1]
keyVal := strings.Split(envVarDef, "=")
if len(keyVal) != 2 {
return envVars, fmt.Errorf("couldn't parse %s", txt)
}
envVars[keyVal[0]] = keyVal[1]
}
}
if len(envVars) > 0 {
logrus.Debugf("read %s from %s", envVars, abraSh)
} else {
logrus.Debugf("read 0 env var exports from %s", abraSh)
}
return envVars, nil
}
type EnvVar struct {
Name string
Present bool
}
func CheckEnv(app App) ([]EnvVar, error) {
var envVars []EnvVar
envSamplePath := path.Join(RECIPES_DIR, app.Recipe, ".env.sample")
if _, err := os.Stat(envSamplePath); err != nil {
if os.IsNotExist(err) {
return envVars, fmt.Errorf("%s does not exist?", envSamplePath)
}
return envVars, err
}
envSample, err := ReadEnv(envSamplePath)
if err != nil {
return envVars, err
}
var keys []string
for key := range envSample {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if _, ok := app.Env[key]; ok {
envVars = append(envVars, EnvVar{Name: key, Present: true})
} else {
envVars = append(envVars, EnvVar{Name: key, Present: false})
}
}
return envVars, nil
}
// ReadAbraShCmdNames reads the names of commands.
func ReadAbraShCmdNames(abraSh string) ([]string, error) {
var cmdNames []string
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return cmdNames, nil
}
return cmdNames, err
}
defer file.Close()
cmdNameRegex, err := regexp.Compile(`(\w+)(\(\).*\{)`)
if err != nil {
return cmdNames, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
matches := cmdNameRegex.FindStringSubmatch(line)
if len(matches) > 0 {
cmdNames = append(cmdNames, matches[1])
}
}
if len(cmdNames) > 0 {
logrus.Debugf("read %s from %s", strings.Join(cmdNames, " "), abraSh)
} else {
logrus.Debugf("read 0 command names from %s", abraSh)
}
return cmdNames, nil
}

View File

@ -1,30 +1,69 @@
package envfile_test
package config_test
import (
"fmt"
"os"
"path"
"reflect"
"slices"
"strings"
"testing"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/recipe"
testPkg "coopcloud.tech/abra/pkg/test"
)
var (
TestFolder = os.ExpandEnv("$PWD/../../tests/resources/test_folder")
ValidAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config")
)
// make sure these are in alphabetical order
var (
TFolders = []string{"folder1", "folder2"}
TFiles = []string{"bar.env", "foo.env"}
)
var (
AppName = "ecloud"
ServerName = "evil.corp"
)
var ExpectedAppEnv = config.AppEnv{
"DOMAIN": "ecloud.evil.corp",
"RECIPE": "ecloud",
}
var ExpectedApp = config.App{
Name: AppName,
Recipe: ExpectedAppEnv["RECIPE"],
Domain: ExpectedAppEnv["DOMAIN"],
Env: ExpectedAppEnv,
Path: ExpectedAppFile.Path,
Server: ExpectedAppFile.Server,
}
var ExpectedAppFile = config.AppFile{
Path: path.Join(ValidAbraConf, "servers", ServerName, AppName+".env"),
Server: ServerName,
}
var ExpectedAppFiles = map[string]config.AppFile{
AppName: ExpectedAppFile,
}
func TestGetAllFoldersInDirectory(t *testing.T) {
folders, err := config.GetAllFoldersInDirectory(testPkg.TestFolder)
folders, err := config.GetAllFoldersInDirectory(TestFolder)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(folders, testPkg.TFolders) {
t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(testPkg.TFolders, ","), strings.Join(folders, ","))
if !reflect.DeepEqual(folders, TFolders) {
t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(TFolders, ","), strings.Join(folders, ","))
}
}
func TestGetAllFilesInDirectory(t *testing.T) {
files, err := config.GetAllFilesInDirectory(testPkg.TestFolder)
files, err := config.GetAllFilesInDirectory(TestFolder)
if err != nil {
t.Fatal(err)
}
@ -32,21 +71,21 @@ func TestGetAllFilesInDirectory(t *testing.T) {
for _, file := range files {
fileNames = append(fileNames, file.Name())
}
if !reflect.DeepEqual(fileNames, testPkg.TFiles) {
t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(testPkg.TFiles, ","), strings.Join(fileNames, ","))
if !reflect.DeepEqual(fileNames, TFiles) {
t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(TFiles, ","), strings.Join(fileNames, ","))
}
}
func TestReadEnv(t *testing.T) {
env, err := envfile.ReadEnv(testPkg.ExpectedAppFile.Path)
env, err := config.ReadEnv(ExpectedAppFile.Path)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(env, testPkg.ExpectedAppEnv) {
if !reflect.DeepEqual(env, ExpectedAppEnv) {
t.Fatalf(
"did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s",
testPkg.ExpectedAppEnv["DOMAIN"],
testPkg.ExpectedAppEnv["RECIPE"],
ExpectedAppEnv["DOMAIN"],
ExpectedAppEnv["RECIPE"],
env["DOMAIN"],
env["RECIPE"],
)
@ -54,13 +93,14 @@ func TestReadEnv(t *testing.T) {
}
func TestReadAbraShEnvVars(t *testing.T) {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
abraShEnv, err := envfile.ReadAbraShEnvVars(r.AbraShPath)
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
t.Fatal(err)
}
@ -83,13 +123,14 @@ func TestReadAbraShEnvVars(t *testing.T) {
}
func TestReadAbraShCmdNames(t *testing.T) {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
cmdNames, err := appPkg.ReadAbraShCmdNames(r.AbraShPath)
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh")
cmdNames, err := config.ReadAbraShCmdNames(abraShPath)
if err != nil {
t.Fatal(err)
}
@ -101,33 +142,34 @@ func TestReadAbraShCmdNames(t *testing.T) {
expectedCmdNames := []string{"test_cmd", "test_cmd_args"}
for _, cmdName := range expectedCmdNames {
if !slices.Contains(cmdNames, cmdName) {
t.Fatalf("%s should have been found in %s", cmdName, r.AbraShPath)
t.Fatalf("%s should have been found in %s", cmdName, abraShPath)
}
}
}
func TestCheckEnv(t *testing.T) {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
envSample, err := r.SampleEnv()
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath)
if err != nil {
t.Fatal(err)
}
app := appPkg.App{
app := config.App{
Name: "test-app",
Recipe: recipe.Get(r.Name),
Recipe: r.Name,
Domain: "example.com",
Env: envSample,
Path: "example.com.env",
Server: "example.com",
}
envVars, err := appPkg.CheckEnv(app)
envVars, err := config.CheckEnv(app)
if err != nil {
t.Fatal(err)
}
@ -140,29 +182,30 @@ func TestCheckEnv(t *testing.T) {
}
func TestCheckEnvError(t *testing.T) {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
envSample, err := r.SampleEnv()
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath)
if err != nil {
t.Fatal(err)
}
delete(envSample, "DOMAIN")
app := appPkg.App{
app := config.App{
Name: "test-app",
Recipe: recipe.Get(r.Name),
Recipe: r.Name,
Domain: "example.com",
Env: envSample,
Path: "example.com.env",
Server: "example.com",
}
envVars, err := appPkg.CheckEnv(app)
envVars, err := config.CheckEnv(app)
if err != nil {
t.Fatal(err)
}
@ -175,13 +218,14 @@ func TestCheckEnvError(t *testing.T) {
}
func TestEnvVarCommentsRemoved(t *testing.T) {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
envSample, err := r.SampleEnv()
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, err := config.ReadEnv(envSamplePath)
if err != nil {
t.Fatal(err)
}
@ -206,13 +250,14 @@ func TestEnvVarCommentsRemoved(t *testing.T) {
}
func TestEnvVarModifiersIncluded(t *testing.T) {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
offline := true
r, err := recipe.Get("abra-test-recipe", offline)
if err != nil {
t.Fatal(err)
}
envSample, modifiers, err := envfile.ReadEnvWithModifiers(r.SampleEnvPath)
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
envSample, modifiers, err := config.ReadEnvWithModifiers(envSamplePath)
if err != nil {
t.Fatal(err)
}

View File

@ -1 +0,0 @@
abraDir: foobar

View File

@ -6,19 +6,18 @@ import (
"strings"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"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) {
containerOpts := containerTypes.ListOptions{Filters: filters}
containerOpts := types.ContainerListOptions{Filters: filters}
containers, err := cl.ContainerList(c, containerOpts)
if err != nil {
return types.Container{}, err
@ -43,7 +42,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
return types.Container{}, err
}
log.Warnf("ambiguous container list received, prompting for input")
logrus.Warnf("ambiguous container list received, prompting for input")
var response string
prompt := &survey.Select{
@ -64,7 +63,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
}
}
log.Fatal("failed to match chosen container")
logrus.Panic("failed to match chosen container")
}
return containers[0], nil

View File

@ -9,7 +9,7 @@ import (
func EnsureIPv4(domainName string) (string, error) {
ipv4, err := net.ResolveIPAddr("ip4", domainName)
if err != nil {
return "", fmt.Errorf("unable to resolve ipv4 address for %s, %s", domainName, err)
return "", err
}
// NOTE(d1): e.g. when there is only an ipv6 record available

View File

@ -1,97 +0,0 @@
package envfile
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
"coopcloud.tech/abra/pkg/log"
"git.coopcloud.tech/coop-cloud/godotenv"
)
// envVarModifiers is a list of env var modifier strings. These are added to
// env vars as comments and modify their processing by Abra, e.g. determining
// how long secrets should be.
var envVarModifiers = []string{"length"}
// AppEnv is a map of the values in an apps env config
type AppEnv = map[string]string
// AppModifiers is a map of modifiers in an apps env config
type AppModifiers = map[string]map[string]string
// ReadEnv loads an app envivornment into a map.
func ReadEnv(filePath string) (AppEnv, error) {
var envVars AppEnv
envVars, _, err := godotenv.Read(filePath)
if err != nil {
return nil, err
}
log.Debugf("read %s from %s", envVars, filePath)
return envVars, nil
}
// ReadEnv loads an app envivornment and their modifiers in two different maps.
func ReadEnvWithModifiers(filePath string) (AppEnv, AppModifiers, error) {
var envVars AppEnv
envVars, mods, err := godotenv.Read(filePath)
if err != nil {
return nil, mods, err
}
log.Debugf("read %s from %s", envVars, filePath)
return envVars, mods, nil
}
// ReadAbraShEnvVars reads env vars from an abra.sh recipe file.
func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
envVars := make(map[string]string)
file, err := os.Open(abraSh)
if err != nil {
if os.IsNotExist(err) {
return envVars, nil
}
return envVars, err
}
defer file.Close()
exportRegex, err := regexp.Compile(`^export\s+(\w+=\w+)`)
if err != nil {
return envVars, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
txt := scanner.Text()
if exportRegex.MatchString(txt) {
splitVals := strings.Split(txt, "export ")
envVarDef := splitVals[len(splitVals)-1]
keyVal := strings.Split(envVarDef, "=")
if len(keyVal) != 2 {
return envVars, fmt.Errorf("couldn't parse %s", txt)
}
envVars[keyVal[0]] = keyVal[1]
}
}
if len(envVars) > 0 {
log.Debugf("read %s from %s", envVars, abraSh)
} else {
log.Debugf("read 0 env var exports from %s", abraSh)
}
return envVars, nil
}
type EnvVar struct {
Name string
Present bool
}

View File

@ -9,8 +9,8 @@ import (
"github.com/docker/go-units"
// "github.com/olekukonko/tablewriter"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log"
"github.com/schollz/progressbar/v3"
"github.com/sirupsen/logrus"
)
func ShortenID(str string) string {
@ -66,7 +66,7 @@ func StripTagMeta(image string) string {
}
if originalImage != image {
log.Debugf("stripped %s to %s for parsing", originalImage, image)
logrus.Debugf("stripped %s to %s for parsing", originalImage, image)
}
return image

View File

@ -1,8 +1,8 @@
package git
import (
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
// Add adds a file to the git index.
@ -18,7 +18,7 @@ func Add(repoPath, path string, dryRun bool) error {
}
if dryRun {
log.Debugf("dry run: adding %s", path)
logrus.Debugf("dry run: adding %s", path)
} else {
worktree.Add(path)
}

View File

@ -3,9 +3,9 @@ package git
import (
"fmt"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
// Check if a branch exists in a repo. Use this and not repository.Branch(),
@ -90,11 +90,11 @@ func CheckoutDefaultBranch(repo *git.Repository, repoPath string) (plumbing.Refe
}
if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", branch, repoPath)
logrus.Debugf("failed to check out %s in %s", branch, repoPath)
return branch, err
}
log.Debugf("successfully checked out %v in %s", branch, repoPath)
logrus.Debugf("successfully checked out %v in %s", branch, repoPath)
return branch, nil
}

View File

@ -6,15 +6,15 @@ import (
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
// Clone runs a git clone which accounts for different default branches.
func Clone(dir, url string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Debugf("%s does not exist, attempting to git clone from %s", dir, url)
logrus.Debugf("%s does not exist, attempting to git clone from %s", dir, url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{
URL: url,
@ -23,7 +23,7 @@ func Clone(dir, url string) error {
SingleBranch: true,
})
if err != nil {
log.Debugf("cloning %s default branch failed, attempting from main branch", url)
logrus.Debugf("cloning %s default branch failed, attempting from main branch", url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{
URL: url,
@ -41,9 +41,9 @@ func Clone(dir, url string) error {
}
}
log.Debugf("%s has been git cloned successfully", dir)
logrus.Debugf("%s has been git cloned successfully", dir)
} else {
log.Debugf("%s already exists", dir)
logrus.Debugf("%s already exists", dir)
}
return nil

View File

@ -3,8 +3,8 @@ package git
import (
"fmt"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
// Commit runs a git commit
@ -38,9 +38,9 @@ func Commit(repoPath, commitMessage string, dryRun bool) error {
if err != nil {
return err
}
log.Debug("git changes commited")
logrus.Debug("git changes commited")
} else {
log.Debug("dry run: no changes commited")
logrus.Debug("dry run: no changes commited")
}
return nil

View File

@ -4,7 +4,7 @@ import (
"fmt"
"os/exec"
"coopcloud.tech/abra/pkg/log"
"github.com/sirupsen/logrus"
)
// getGitDiffArgs builds the `git diff` invocation args. It removes the usage
@ -26,7 +26,7 @@ func getGitDiffArgs(repoPath string) []string {
// skips if it cannot find the command on the system.
func DiffUnstaged(path string) error {
if _, err := exec.LookPath("git"); err != nil {
log.Warnf("unable to locate git command, cannot output diff")
logrus.Warnf("unable to locate git command, cannot output diff")
return nil
}

View File

@ -1,43 +1,37 @@
package git
import (
"fmt"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
gitPkg "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
// Init inits a new repo and commits all the stuff if you want
func Init(repoPath string, commit bool, gitName, gitEmail string) error {
if _, err := git.PlainInit(repoPath, false); err != nil {
return fmt.Errorf("git init: %s", err)
func Init(repoPath string, commit bool) error {
if _, err := gitPkg.PlainInit(repoPath, false); err != nil {
logrus.Fatal(err)
}
log.Debugf("initialised new git repo in %s", repoPath)
logrus.Debugf("initialised new git repo in %s", repoPath)
if commit {
commitRepo, err := git.PlainOpen(repoPath)
if err != nil {
return fmt.Errorf("git open: %s", err)
logrus.Fatal(err)
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
return fmt.Errorf("git worktree: %s", err)
logrus.Fatal(err)
}
if err := commitWorktree.AddWithOptions(&git.AddOptions{All: true}); err != nil {
return fmt.Errorf("git add: %s", err)
return err
}
var author *object.Signature
if gitName != "" && gitEmail != "" {
author = &object.Signature{Name: gitName, Email: gitEmail}
if _, err = commitWorktree.Commit("init", &git.CommitOptions{}); err != nil {
return err
}
if _, err = commitWorktree.Commit("init", &git.CommitOptions{Author: author}); err != nil {
return fmt.Errorf("git commit: %s", err)
}
log.Debugf("init committed all files for new git repo in %s", repoPath)
logrus.Debugf("init committed all files for new git repo in %s", repoPath)
}
return nil

View File

@ -1,15 +1,15 @@
package git
import (
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/sirupsen/logrus"
)
// Push pushes the latest changes & optionally tags to the default remote
func Push(repoDir string, remote string, tags bool, dryRun bool) error {
if dryRun {
log.Debugf("dry run: no git changes pushed in %s", repoDir)
logrus.Debugf("dry run: no git changes pushed in %s", repoDir)
return nil
}
@ -27,7 +27,7 @@ func Push(repoDir string, remote string, tags bool, dryRun bool) error {
return err
}
log.Debugf("git changes pushed")
logrus.Debugf("git changes pushed")
if tags {
opts.RefSpecs = append(opts.RefSpecs, config.RefSpec("+refs/tags/*:refs/tags/*"))
@ -36,7 +36,7 @@ func Push(repoDir string, remote string, tags bool, dryRun bool) error {
return err
}
log.Debugf("git tags pushed")
logrus.Debugf("git tags pushed")
}
return nil

View File

@ -4,15 +4,35 @@ import (
"io/ioutil"
"os"
"os/user"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5"
gitConfigPkg "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/sirupsen/logrus"
)
// GetRecipeHead retrieves latest HEAD metadata.
func GetRecipeHead(recipeName string) (*plumbing.Reference, error) {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return nil, err
}
head, err := repo.Head()
if err != nil {
return nil, err
}
return head, nil
}
// IsClean checks if a repo has unstaged changes
func IsClean(repoPath string) (bool, error) {
repo, err := git.PlainOpen(repoPath)
@ -40,9 +60,9 @@ func IsClean(repoPath string) (bool, error) {
}
if status.String() != "" {
log.Debugf("discovered git status in %s: %s", repoPath, status.String())
logrus.Debugf("discovered git status in %s: %s", repoPath, status.String())
} else {
log.Debugf("discovered clean git status in %s", repoPath)
logrus.Debugf("discovered clean git status in %s", repoPath)
}
return status.IsClean(), nil
@ -78,7 +98,7 @@ func parseGitConfig() (*gitConfigPkg.Config, error) {
globalGitConfig := filepath.Join(usr.HomeDir, ".gitconfig")
if _, err := os.Stat(globalGitConfig); err != nil {
if os.IsNotExist(err) {
log.Debugf("no %s exists, not reading any global gitignore config", globalGitConfig)
logrus.Debugf("no %s exists, not reading any global gitignore config", globalGitConfig)
return cfg, nil
}
return cfg, err
@ -120,7 +140,7 @@ func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
if _, err := os.Stat(excludesfile); err != nil {
if os.IsNotExist(err) {
log.Debugf("no %s exists, skipping reading gitignore paths", excludesfile)
logrus.Debugf("no %s exists, skipping reading gitignore paths", excludesfile)
return ps, nil
}
return ps, err
@ -139,7 +159,7 @@ func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
}
}
log.Debugf("read global ignore paths: %s", strings.Join(pathsRaw, " "))
logrus.Debugf("read global ignore paths: %s", strings.Join(pathsRaw, " "))
return ps, nil
}

View File

@ -3,15 +3,15 @@ package git
import (
"strings"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/sirupsen/logrus"
)
// CreateRemote creates a new git remote in a repository
func CreateRemote(repo *git.Repository, name, url string, dryRun bool) error {
if dryRun {
log.Debugf("dry run: remote %s (%s) not created", name, url)
logrus.Debugf("dry run: remote %s (%s) not created", name, url)
return nil
}

View File

@ -6,13 +6,14 @@ import (
"os"
"path"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/distribution/reference"
"github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
var Warn = "warn"
@ -45,10 +46,10 @@ func (l LintRule) Skip(recipe recipe.Recipe) bool {
if l.SkipCondition != nil {
ok, err := l.SkipCondition(recipe)
if err != nil {
log.Debugf("%s: skip condition: %s", l.Ref, err)
logrus.Debugf("%s: skip condition: %s", l.Ref, err)
}
if ok {
log.Debugf("skipping %s based on skip condition", l.Ref)
logrus.Debugf("skipping %s based on skip condition", l.Ref)
return true
}
}
@ -173,7 +174,7 @@ var LintRules = map[string][]LintRule{
// used in code paths such as "app deploy" to avoid nasty surprises but not for
// the typical linting commands, which do handle other levels.
func LintForErrors(recipe recipe.Recipe) error {
log.Debugf("linting for critical errors in %s configs", recipe.Name)
logrus.Debugf("linting for critical errors in %s configs", recipe.Name)
for level := range LintRules {
if level != "error" {
@ -195,25 +196,22 @@ func LintForErrors(recipe recipe.Recipe) error {
}
}
log.Debugf("linting successful, %s is well configured", recipe.Name)
logrus.Debugf("linting successful, %s is well configured", recipe.Name)
return nil
}
func LintComposeVersion(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
if config.Version == "3.8" {
if recipe.Config.Version == "3.8" {
return true, nil
}
return true, nil
}
func LintEnvConfigPresent(r recipe.Recipe) (bool, error) {
if _, err := os.Stat(r.SampleEnvPath); !os.IsNotExist(err) {
func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) {
envSample := fmt.Sprintf("%s/%s/.env.sample", config.RECIPES_DIR, recipe.Name)
if _, err := os.Stat(envSample); !os.IsNotExist(err) {
return true, nil
}
@ -221,11 +219,7 @@ func LintEnvConfigPresent(r recipe.Recipe) (bool, error) {
}
func LintAppService(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
if service.Name == "app" {
return true, nil
}
@ -238,10 +232,11 @@ func LintAppService(recipe recipe.Recipe) (bool, error) {
// confirms that there is no "DOMAIN=..." in the .env.sample configuration of
// the recipe. This typically means that no domain is required to deploy and
// therefore no matching traefik deploy label will be present.
func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) {
sampleEnv, err := r.SampleEnv()
func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) {
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return false, fmt.Errorf("Unable to discover .env.sample for %s", r.Name)
return false, fmt.Errorf("Unable to discover .env.sample for %s", recipe.Name)
}
if _, ok := sampleEnv["DOMAIN"]; !ok {
@ -252,11 +247,7 @@ func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) {
}
func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
for label := range service.Deploy.Labels {
if label == "traefik.enable" {
if service.Deploy.Labels[label] == "true" {
@ -270,11 +261,7 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
}
func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
if service.HealthCheck == nil {
return false, nil
}
@ -284,11 +271,7 @@ func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
}
func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return false, err
@ -302,11 +285,7 @@ func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
}
func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return false, err
@ -329,11 +308,7 @@ func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
}
func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return false, err
@ -356,11 +331,7 @@ func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
}
func LintImagePresent(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
if service.Image == "" {
return false, nil
}
@ -371,12 +342,12 @@ func LintImagePresent(recipe recipe.Recipe) (bool, error) {
func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
catl, err := recipePkg.ReadRecipeCatalogue(false)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
if len(versions) == 0 {
@ -387,7 +358,7 @@ func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
}
func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
features, category, err := recipe.GetRecipeFeaturesAndCategory(r)
features, category, err := recipe.GetRecipeFeaturesAndCategory(r.Name)
if err != nil {
return false, err
}
@ -408,13 +379,9 @@ func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
}
func LintAbraShVendors(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
for _, service := range recipe.Config.Services {
if len(service.Configs) > 0 {
abraSh := path.Join(recipe.Dir, "abra.sh")
abraSh := path.Join(config.RECIPES_DIR, recipe.Name, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) {
return false, err
@ -427,7 +394,9 @@ func LintAbraShVendors(recipe recipe.Recipe) (bool, error) {
}
func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) {
res, err := http.Get(recipe.GitURL)
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipe.Name)
res, err := http.Get(url)
if err != nil {
return false, err
}
@ -440,11 +409,7 @@ func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) {
}
func LintSecretLengths(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for name := range config.Secrets {
for name := range recipe.Config.Secrets {
if len(name) > 12 {
return false, fmt.Errorf("secret %s is longer than 12 characters", name)
}
@ -454,14 +419,16 @@ func LintSecretLengths(recipe recipe.Recipe) (bool, error) {
}
func LintValidTags(recipe recipe.Recipe) (bool, error) {
repo, err := git.PlainOpen(recipe.Dir)
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return false, fmt.Errorf("unable to open %s: %s", recipe.Dir, err)
return false, fmt.Errorf("unable to open %s: %s", recipeDir, err)
}
iter, err := repo.Tags()
if err != nil {
log.Fatalf("unable to list local tags for %s", recipe.Name)
logrus.Fatalf("unable to list local tags for %s", recipe.Name)
}
if err := iter.ForEach(func(ref *plumbing.Reference) error {

View File

@ -1,34 +0,0 @@
// Package log defines the core logging functionality for Abra.
package log
import (
"os"
charmLog "github.com/charmbracelet/log"
)
// Logger is the central logging interface.
var Logger = charmLog.NewWithOptions(os.Stdout, charmLog.Options{
ReportCaller: false,
ReportTimestamp: false,
})
var Fatal = Logger.Fatal
var Fatalf = Logger.Fatalf
var Debug = Logger.Debug
var Debugf = Logger.Debugf
var Info = Logger.Info
var Infof = Logger.Infof
var Warn = Logger.Warn
var Warnf = Logger.Warnf
var Error = Logger.Error
var Errorf = Logger.Errorf
var SetLevel = Logger.SetLevel
var DebugLevel = charmLog.DebugLevel
var SetOutput = charmLog.SetOutput
var SetReportCaller = charmLog.SetReportCaller

View File

@ -1,252 +0,0 @@
package recipe
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/distribution/reference"
composetypes "github.com/docker/cli/cli/compose/types"
)
// GetComposeFiles gets the list of compose files for an app (or recipe if you
// don't already have an app) which should be merged into a composetypes.Config
// while respecting the COMPOSE_FILE env var.
func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
if !ok {
if err := ensurePathExists(r.ComposePath); err != nil {
return []string{}, err
}
log.Debugf("no COMPOSE_FILE detected, loading default: %s", r.ComposePath)
return []string{r.ComposePath}, nil
}
if !strings.Contains(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s", r.Dir, composeFileEnvVar)
if err := ensurePathExists(path); err != nil {
return []string{}, err
}
log.Debugf("COMPOSE_FILE detected, loading %s", path)
return []string{path}, nil
}
var composeFiles []string
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
if len(envVars) != numComposeFiles {
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
}
for _, file := range envVars {
path := fmt.Sprintf("%s/%s", r.Dir, file)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
composeFiles = append(composeFiles, path)
}
log.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
log.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name)
return composeFiles, nil
}
func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, error) {
pattern := fmt.Sprintf("%s/compose**yml", r.Dir)
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
if len(composeFiles) == 0 {
return nil, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", r.Name)
}
if env == nil {
env, err = r.SampleEnv()
if err != nil {
return nil, err
}
}
opts := stack.Deploy{Composefiles: composeFiles}
config, err := loader.LoadComposefile(opts, env)
if err != nil {
return nil, err
}
return config, nil
}
// GetVersionLabelLocal retrieves the version label on the local recipe config
func (r Recipe) GetVersionLabelLocal() (string, error) {
var label string
config, err := r.GetComposeConfig(nil)
if err != nil {
return "", err
}
for _, service := range config.Services {
for label, value := range service.Deploy.Labels {
if strings.HasPrefix(label, "coop-cloud") && strings.Contains(label, "version") {
return value, nil
}
}
}
if label == "" {
return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", r.Name, r.Name)
}
return label, nil
}
// UpdateTag updates an image tag in-place on file system local compose files.
func (r Recipe) UpdateTag(image, tag string) (bool, error) {
fullPattern := fmt.Sprintf("%s/compose**yml", r.Dir)
image = formatter.StripTagMeta(image)
composeFiles, err := filepath.Glob(fullPattern)
if err != nil {
return false, err
}
log.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
sampleEnv, err := r.SampleEnv()
if err != nil {
return false, err
}
compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return false, err
}
for _, service := range compose.Services {
if service.Image == "" {
continue // may be a compose.$optional.yml file
}
img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return false, err
}
var composeTag string
switch img.(type) {
case reference.NamedTagged:
composeTag = img.(reference.NamedTagged).Tag()
default:
log.Debugf("unable to parse %s, skipping", img)
continue
}
composeImage := formatter.StripTagMeta(reference.Path(img))
log.Debugf("parsed %s from %s", composeTag, service.Image)
if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return false, err
}
old := fmt.Sprintf("%s:%s", composeImage, composeTag)
new := fmt.Sprintf("%s:%s", composeImage, tag)
replacedBytes := strings.Replace(string(bytes), old, new, -1)
log.Debugf("updating %s to %s in %s", old, new, compose.Filename)
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return false, err
}
}
}
}
return false, nil
}
// UpdateLabel updates a label in-place on file system local compose files.
func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
fullPattern := fmt.Sprintf("%s/%s", r.Dir, pattern)
composeFiles, err := filepath.Glob(fullPattern)
if err != nil {
return err
}
log.Debugf("considering %s config(s) for label update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
sampleEnv, err := r.SampleEnv()
if err != nil {
return err
}
compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return err
}
serviceExists := false
var service composetypes.ServiceConfig
for _, s := range compose.Services {
if s.Name == serviceName {
service = s
serviceExists = true
}
}
if !serviceExists {
continue
}
discovered := false
for oldLabel, value := range service.Deploy.Labels {
if strings.HasPrefix(oldLabel, "coop-cloud") && strings.Contains(oldLabel, "version") {
discovered = true
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return err
}
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
replacedBytes := strings.Replace(string(bytes), old, label, -1)
if old == label {
log.Warnf("%s is already set, nothing to do?", label)
return nil
}
log.Debugf("updating %s to %s in %s", old, label, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return err
}
log.Infof("synced label %s to service %s", label, serviceName)
}
}
if !discovered {
log.Warn("no existing label found, automagic insertion not supported yet")
log.Fatalf("add '- \"%s\"' manually to the 'app' service in %s", label, composeFile)
}
}
return nil
}

View File

@ -1,37 +0,0 @@
package recipe
import (
"fmt"
"os"
"path"
"coopcloud.tech/abra/pkg/envfile"
)
func (r Recipe) SampleEnv() (map[string]string, error) {
sampleEnv, err := envfile.ReadEnv(r.SampleEnvPath)
if err != nil {
return sampleEnv, fmt.Errorf("unable to discover .env.sample for %s", r.Name)
}
return sampleEnv, nil
}
// GetReleaseNotes prints release notes for the recipe version
func (r Recipe) GetReleaseNotes(version string) (string, error) {
if version == "" {
return "", nil
}
fpath := path.Join(r.Dir, "release", version)
if _, err := os.Stat(fpath); !os.IsNotExist(err) {
releaseNotes, err := os.ReadFile(fpath)
if err != nil {
return "", err
}
withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes))
return withTitle, nil
}
return "", nil
}

View File

@ -1,396 +0,0 @@
package recipe
import (
"fmt"
"os"
"strings"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
// Ensure makes sure the recipe exists, is up to date and has the latest version checked out.
func (r Recipe) Ensure(chaos bool, offline bool) error {
if err := r.EnsureExists(); err != nil {
return err
}
if chaos {
return nil
}
if err := r.EnsureIsClean(); err != nil {
return err
}
if !offline {
if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if r.Version != "" {
if _, err := r.EnsureVersion(r.Version); err != nil {
return err
}
} else {
if err := r.EnsureLatest(); err != nil {
return err
}
}
return nil
}
// EnsureExists ensures that the recipe is locally cloned
func (r Recipe) EnsureExists() error {
if _, err := os.Stat(r.Dir); os.IsNotExist(err) {
log.Debugf("%s does not exist, attemmpting to clone", r.Dir)
if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil {
return err
}
}
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
return err
}
return nil
}
// EnsureVersion checks whether a specific version exists for a recipe.
func (r Recipe) EnsureVersion(version string) (bool, error) {
isChaosCommit := false
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
return isChaosCommit, err
}
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return isChaosCommit, err
}
tags, err := repo.Tags()
if err != nil {
return isChaosCommit, err
}
var parsedTags []string
var tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
parsedTags = append(parsedTags, ref.Name().Short())
if ref.Name().Short() == version {
tagRef = ref.Name()
}
return nil
}); err != nil {
return isChaosCommit, err
}
joinedTags := strings.Join(parsedTags, ", ")
if joinedTags != "" {
log.Debugf("read %s as tags for recipe %s", joinedTags, r.Name)
}
var opts *git.CheckoutOptions
if tagRef.String() == "" {
log.Debugf("attempting to checkout '%s' as chaos commit", version)
hash, err := repo.ResolveRevision(plumbing.Revision(version))
if err != nil {
log.Fatalf("unable to resolve '%s': %s", version, err)
}
opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true}
isChaosCommit = true
} else {
opts = &git.CheckoutOptions{Branch: tagRef, Create: false, Force: true}
}
worktree, err := repo.Worktree()
if err != nil {
return isChaosCommit, nil
}
if err := worktree.Checkout(opts); err != nil {
return isChaosCommit, nil
}
log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir)
return isChaosCommit, nil
}
// EnsureIsClean makes sure that the recipe repository has no unstaged changes.
func (r Recipe) EnsureIsClean() error {
isClean, err := gitPkg.IsClean(r.Dir)
if err != nil {
return fmt.Errorf("unable to check git clean status in %s: %s", r.Dir, err)
}
if !isClean {
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
return fmt.Errorf(msg, r.Name, r.Dir)
}
return nil
}
// EnsureLatest makes sure the latest commit is checked out for the local recipe repository
func (r Recipe) EnsureLatest() error {
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
return err
}
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
branch, err := gitPkg.GetDefaultBranch(repo, r.Dir)
if err != nil {
return err
}
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(branch),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", branch, r.Dir)
return err
}
return nil
}
// EnsureUpToDate ensures that the local repo is synced to the remote
func (r Recipe) EnsureUpToDate() error {
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return fmt.Errorf("unable to open %s: %s", r.Dir, err)
}
remotes, err := repo.Remotes()
if err != nil {
return fmt.Errorf("unable to read remotes in %s: %s", r.Dir, err)
}
if len(remotes) == 0 {
log.Debugf("cannot ensure %s is up-to-date, no git remotes configured", r.Name)
return nil
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("unable to open git work tree in %s: %s", r.Dir, err)
}
branch, err := gitPkg.CheckoutDefaultBranch(repo, r.Dir)
if err != nil {
return fmt.Errorf("unable to check out default branch in %s: %s", r.Dir, err)
}
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
if err := repo.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to fetch tags in %s: %s", r.Dir, err)
}
}
opts := &git.PullOptions{
Force: true,
ReferenceName: branch,
SingleBranch: true,
}
if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to git pull in %s: %s", r.Dir, err)
}
}
log.Debugf("fetched latest git changes for %s", r.Name)
return nil
}
// ChaosVersion constructs a chaos mode recipe version.
func (r Recipe) ChaosVersion() (string, error) {
var version string
head, err := r.Head()
if err != nil {
return version, err
}
version = formatter.SmallSHA(head.String())
isClean, err := gitPkg.IsClean(r.Dir)
if err != nil {
return version, err
}
if !isClean {
version = fmt.Sprintf("%s + unstaged changes", version)
}
return version, nil
}
// Push pushes the latest changes to a SSH URL remote. You need to have your
// local SSH configuration for git.coopcloud.tech working for this to work
func (r Recipe) Push(dryRun bool) error {
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return err
}
if err := gitPkg.CreateRemote(repo, "origin-ssh", r.SSHURL, dryRun); err != nil {
return err
}
if err := gitPkg.Push(r.Dir, "origin-ssh", true, dryRun); err != nil {
return err
}
return nil
}
// Tags list the recipe tags
func (r Recipe) Tags() ([]string, error) {
var tags []string
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return tags, err
}
gitTags, err := repo.Tags()
if err != nil {
return tags, err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/"))
return nil
}); err != nil {
return tags, err
}
log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name)
return tags, nil
}
// GetRecipeVersions retrieves all recipe versions.
func (r Recipe) GetRecipeVersions() (RecipeVersions, error) {
versions := RecipeVersions{}
log.Debugf("attempting to open git repository in %s", r.Dir)
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return versions, err
}
worktree, err := repo.Worktree()
if err != nil {
return versions, err
}
gitTags, err := repo.Tags()
if err != nil {
return versions, err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
log.Debugf("processing %s for %s", tag, r.Name)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(ref.Name()),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", tag, r.Dir)
return err
}
log.Debugf("successfully checked out %s in %s", ref.Name(), r.Dir)
config, err := r.GetComposeConfig(nil)
if err != nil {
return err
}
versionMeta := make(map[string]ServiceMeta)
for _, service := range config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return err
}
path := reference.Path(img)
path = formatter.StripTagMeta(path)
var tag string
switch img.(type) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
log.Warnf("%s service is missing image tag?", path)
continue
}
versionMeta[service.Name] = ServiceMeta{
Image: path,
Tag: tag,
}
}
versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
return nil
}); err != nil {
return versions, err
}
_, err = gitPkg.CheckoutDefaultBranch(repo, r.Dir)
if err != nil {
return versions, err
}
sortRecipeVersions(versions)
log.Debugf("collected %s for %s", versions, r.Dir)
return versions, nil
}
// Head retrieves latest HEAD metadata.
func (r Recipe) Head() (*plumbing.Reference, error) {
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return nil, err
}
head, err := repo.Head()
if err != nil {
return nil, err
}
return head, nil
}

View File

@ -4,22 +4,29 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/compose"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/limit"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/abra/pkg/web"
"coopcloud.tech/tagcmp"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
)
// RecipeCatalogueURL is the only current recipe catalogue available.
@ -73,7 +80,7 @@ func (r RecipeMeta) LatestVersion() string {
version = tag
}
log.Debugf("choosing %s as latest version of %s", version, r.Name)
logrus.Debugf("choosing %s as latest version of %s", version, r.Name)
return version
}
@ -123,62 +130,307 @@ type Features struct {
SSO string `json:"sso"`
}
func Get(name string) Recipe {
version := ""
if strings.Contains(name, ":") {
split := strings.Split(name, ":")
name = split[0]
version = split[1]
}
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, name)
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, name)
if strings.Contains(name, "/") {
u, err := url.Parse(name)
if err != nil {
log.Fatalf("invalid recipe: %s", err)
}
u.Scheme = "https"
gitURL = u.String() + ".git"
u.Scheme = "ssh"
u.User = url.User("git")
sshURL = u.String() + ".git"
}
dir := path.Join(config.RECIPES_DIR, escapeRecipeName(name))
return Recipe{
Name: name,
Version: version,
Dir: dir,
GitURL: gitURL,
SSHURL: sshURL,
ComposePath: path.Join(dir, "compose.yml"),
ReadmePath: path.Join(dir, "README.md"),
SampleEnvPath: path.Join(dir, ".env.sample"),
AbraShPath: path.Join(dir, "abra.sh"),
}
}
// Recipe represents a recipe.
type Recipe struct {
Name string
Version string
Dir string
GitURL string
SSHURL string
ComposePath string
ReadmePath string
SampleEnvPath string
AbraShPath string
Config *composetypes.Config
Meta RecipeMeta
}
func escapeRecipeName(recipeName string) string {
recipeName = strings.ReplaceAll(recipeName, "/", "_")
recipeName = strings.ReplaceAll(recipeName, ".", "_")
return recipeName
// Push pushes the latest changes to a SSH URL remote. You need to have your
// local SSH configuration for git.coopcloud.tech working for this to work
func (r Recipe) Push(dryRun bool) error {
repo, err := git.PlainOpen(r.Dir())
if err != nil {
return err
}
if err := gitPkg.CreateRemote(repo, "origin-ssh", r.Meta.SSHURL, dryRun); err != nil {
return err
}
if err := gitPkg.Push(r.Dir(), "origin-ssh", true, dryRun); err != nil {
return err
}
return nil
}
// Dir retrieves the recipe repository path
func (r Recipe) Dir() string {
return path.Join(config.RECIPES_DIR, r.Name)
}
// UpdateLabel updates a recipe label
func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
fullPattern := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, pattern)
if err := compose.UpdateLabel(fullPattern, serviceName, label, r.Name); err != nil {
return err
}
return nil
}
// UpdateTag updates a recipe tag
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)
ok, err := compose.UpdateTag(pattern, image, tag, r.Name)
if err != nil {
return false, err
}
return ok, nil
}
// Tags list the recipe tags
func (r Recipe) Tags() ([]string, error) {
var tags []string
repo, err := git.PlainOpen(r.Dir())
if err != nil {
return tags, err
}
gitTags, err := repo.Tags()
if err != nil {
return tags, err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/"))
return nil
}); err != nil {
return tags, err
}
logrus.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name)
return tags, nil
}
// Get retrieves a recipe.
func Get(recipeName string, offline bool) (Recipe, error) {
if err := EnsureExists(recipeName); err != nil {
return Recipe{}, err
}
pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, recipeName)
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return Recipe{}, err
}
if len(composeFiles) == 0 {
return Recipe{}, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", recipeName)
}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return Recipe{}, err
}
opts := stack.Deploy{Composefiles: composeFiles}
config, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return Recipe{}, err
}
meta, err := GetRecipeMeta(recipeName, offline)
if err != nil {
switch err.(type) {
case RecipeMissingFromCatalogue:
meta = RecipeMeta{}
default:
return Recipe{}, err
}
}
return Recipe{
Name: recipeName,
Config: config,
Meta: meta,
}, nil
}
func (r Recipe) SampleEnv() (map[string]string, error) {
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
return sampleEnv, fmt.Errorf("unable to discover .env.sample for %s", r.Name)
}
return sampleEnv, nil
}
// Ensure makes sure the recipe exists, is up to date and has the latest version checked out.
func Ensure(recipeName string) error {
if err := EnsureExists(recipeName); err != nil {
return err
}
if err := EnsureUpToDate(recipeName); err != nil {
return err
}
if err := EnsureLatest(recipeName); err != nil {
return err
}
return nil
}
// EnsureExists ensures that a recipe is locally cloned
func EnsureExists(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
logrus.Debugf("%s does not exist, attemmpting to clone", recipeDir)
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipeName)
if err := gitPkg.Clone(recipeDir, url); err != nil {
return err
}
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
return nil
}
// EnsureVersion checks whether a specific version exists for a recipe.
func EnsureVersion(recipeName, version string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
tags, err := repo.Tags()
if err != nil {
return nil
}
var parsedTags []string
var tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
parsedTags = append(parsedTags, ref.Name().Short())
if ref.Name().Short() == version {
tagRef = ref.Name()
}
return nil
}); err != nil {
return err
}
joinedTags := strings.Join(parsedTags, ", ")
if joinedTags != "" {
logrus.Debugf("read %s as tags for recipe %s", joinedTags, recipeName)
}
if tagRef.String() == "" {
return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", recipeName, version)
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
opts := &git.CheckoutOptions{
Branch: tagRef,
Create: false,
Force: true,
}
if err := worktree.Checkout(opts); err != nil {
return err
}
logrus.Debugf("successfully checked %s out to %s in %s", recipeName, tagRef.Short(), recipeDir)
return nil
}
// EnsureIsClean makes sure that the recipe repository has no unstaged changes.
func EnsureIsClean(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err)
}
if !isClean {
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
return fmt.Errorf(msg, recipeName, recipeDir)
}
return nil
}
// EnsureLatest makes sure the latest commit is checked out for a local recipe repository
func EnsureLatest(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
branch, err := gitPkg.GetDefaultBranch(repo, recipeDir)
if err != nil {
return err
}
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(branch),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out %s in %s", branch, recipeDir)
return err
}
return nil
}
// ChaosVersion constructs a chaos mode recipe version.
func ChaosVersion(recipeName string) (string, error) {
var version string
head, err := gitPkg.GetRecipeHead(recipeName)
if err != nil {
return version, err
}
version = formatter.SmallSHA(head.String())
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return version, err
}
if !isClean {
version = fmt.Sprintf("%s + unstaged changes", version)
}
return version, nil
}
// GetRecipesLocal retrieves all local recipe directories
@ -193,20 +445,41 @@ func GetRecipesLocal() ([]string, error) {
return recipes, nil
}
func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, error) {
// GetVersionLabelLocal retrieves the version label on the local recipe config
func GetVersionLabelLocal(recipe Recipe) (string, error) {
var label string
for _, service := range recipe.Config.Services {
for label, value := range service.Deploy.Labels {
if strings.HasPrefix(label, "coop-cloud") && strings.Contains(label, "version") {
return value, nil
}
}
}
if label == "" {
return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", recipe.Name, recipe.Name)
}
return label, nil
}
func GetRecipeFeaturesAndCategory(recipeName string) (Features, string, error) {
feat := Features{}
var category string
log.Debugf("attempting to open %s for recipe metadata parsing", r.ReadmePath)
readmePath := path.Join(config.RECIPES_DIR, recipeName, "README.md")
readmeFS, err := ioutil.ReadFile(r.ReadmePath)
logrus.Debugf("attempting to open %s for recipe metadata parsing", readmePath)
readmeFS, err := ioutil.ReadFile(readmePath)
if err != nil {
return feat, category, err
}
readmeMetadata, err := GetStringInBetween( // Find text between delimiters
r.Name,
recipeName,
string(readmeFS),
"<!-- metadata -->", "<!-- endmetadata -->",
)
@ -257,7 +530,7 @@ func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, error) {
if strings.Contains(val, "**Image**") {
imageMetadata, err := GetImageMetadata(strings.TrimSpace(
strings.TrimPrefix(val, "* **Image**:"),
), r.Name)
), recipeName)
if err != nil {
continue
}
@ -279,9 +552,9 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, error) {
if len(imgFields) < 3 {
if imageRowString != "" {
log.Warnf("%s image meta has incorrect format: %s", recipeName, imageRowString)
logrus.Warnf("%s image meta has incorrect format: %s", recipeName, imageRowString)
} else {
log.Warnf("%s image meta is empty?", recipeName)
logrus.Warnf("%s image meta is empty?", recipeName)
}
return img, nil
}
@ -293,13 +566,13 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, error) {
imageName, err := GetStringInBetween(recipeName, imgString, "[", "]")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
img.Image = strings.ReplaceAll(imageName, "`", "")
imageURL, err := GetStringInBetween(recipeName, imgString, "(", ")")
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
img.URL = imageURL
@ -323,6 +596,59 @@ func GetStringInBetween(recipeName, str, start, end string) (result string, err
return str[s : s+e], nil
}
// EnsureUpToDate ensures that the local repo is synced to the remote
func EnsureUpToDate(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return fmt.Errorf("unable to open %s: %s", recipeDir, err)
}
remotes, err := repo.Remotes()
if err != nil {
return fmt.Errorf("unable to read remotes in %s: %s", recipeDir, err)
}
if len(remotes) == 0 {
logrus.Debugf("cannot ensure %s is up-to-date, no git remotes configured", recipeName)
return nil
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("unable to open git work tree in %s: %s", recipeDir, err)
}
branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir)
if err != nil {
return fmt.Errorf("unable to check out default branch in %s: %s", recipeDir, err)
}
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
if err := repo.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to fetch tags in %s: %s", recipeDir, err)
}
}
opts := &git.PullOptions{
Force: true,
ReferenceName: branch,
SingleBranch: true,
}
if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to git pull in %s: %s", recipeDir, err)
}
}
logrus.Debugf("fetched latest git changes for %s", recipeName)
return nil
}
// ReadRecipeCatalogue reads the recipe catalogue.
func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) {
recipes := make(RecipeCatalogue)
@ -355,7 +681,7 @@ func readRecipeCatalogueFS(target interface{}) error {
return err
}
log.Debugf("read recipe catalogue from file system cache in %s", config.RECIPES_JSON)
logrus.Debugf("read recipe catalogue from file system cache in %s", config.RECIPES_JSON)
return nil
}
@ -384,7 +710,7 @@ func VersionsOfService(recipe, serviceName string, offline bool) ([]string, erro
}
}
log.Debugf("detected versions %s for %s", strings.Join(versions, ", "), recipe)
logrus.Debugf("detected versions %s for %s", strings.Join(versions, ", "), recipe)
return versions, nil
}
@ -411,7 +737,7 @@ func GetRecipeMeta(recipeName string, offline bool) (RecipeMeta, error) {
}
}
log.Debugf("recipe metadata retrieved for %s", recipeName)
logrus.Debugf("recipe metadata retrieved for %s", recipeName)
return recipeMeta, nil
}
@ -504,7 +830,7 @@ func ReadReposMetadata() (RepoCatalogue, error) {
pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx)
log.Debugf("fetching repo metadata from %s", pagedURL)
logrus.Debugf("fetching repo metadata from %s", pagedURL)
if err := web.ReadJSON(pagedURL, &reposList); err != nil {
return reposMeta, err
@ -537,6 +863,95 @@ func ReadReposMetadata() (RepoCatalogue, error) {
return reposMeta, nil
}
// GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName string, offline bool) (RecipeVersions, error) {
versions := RecipeVersions{}
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
logrus.Debugf("attempting to open git repository in %s", recipeDir)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return versions, err
}
worktree, err := repo.Worktree()
if err != nil {
return versions, err
}
gitTags, err := repo.Tags()
if err != nil {
return versions, err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
logrus.Debugf("processing %s for %s", tag, recipeName)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(ref.Name()),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out %s in %s", tag, recipeDir)
return err
}
logrus.Debugf("successfully checked out %s in %s", ref.Name(), recipeDir)
recipe, err := Get(recipeName, offline)
if err != nil {
return err
}
versionMeta := make(map[string]ServiceMeta)
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return err
}
path := reference.Path(img)
path = formatter.StripTagMeta(path)
var tag string
switch img.(type) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
logrus.Warnf("%s service is missing image tag?", path)
continue
}
versionMeta[service.Name] = ServiceMeta{
Image: path,
Tag: tag,
}
}
versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
return nil
}); err != nil {
return versions, err
}
_, err = gitPkg.CheckoutDefaultBranch(repo, recipeDir)
if err != nil {
return versions, err
}
sortRecipeVersions(versions)
logrus.Debugf("collected %s for %s", versions, recipeName)
return versions, nil
}
// sortRecipeVersions sorts the recipe semver versions
func sortRecipeVersions(versions RecipeVersions) {
sort.Slice(versions, func(i, j int) bool {
@ -617,8 +1032,9 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
return
}
if err := gitPkg.Clone(Get(rm.Name).Dir, rm.CloneURL); err != nil {
log.Fatal(err)
recipeDir := path.Join(config.RECIPES_DIR, rm.Name)
if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil {
logrus.Fatal(err)
}
ch <- rm.Name
@ -637,11 +1053,3 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
func getReposTopicUrl(repoName string) string {
return fmt.Sprintf("https://git.coopcloud.tech/api/v1/repos/coop-cloud/%s/topics", repoName)
}
// ensurePathExists ensures that a path exists.
func ensurePathExists(path string) error {
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
return err
}
return nil
}

View File

@ -1,96 +1,20 @@
package recipe
import (
"path"
"testing"
"coopcloud.tech/abra/pkg/config"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
func TestGet(t *testing.T) {
cfg := config.LoadAbraConfig()
testcases := []struct {
name string
recipe Recipe
}{
{
name: "foo",
recipe: Recipe{
Name: "foo",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"),
GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git",
SSHURL: "ssh://git@git.coopcloud.tech:2222/coop-cloud/foo.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/foo/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/foo/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/foo/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/foo/abra.sh"),
},
},
{
name: "foo:1.2.3",
recipe: Recipe{
Name: "foo",
Version: "1.2.3",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"),
GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git",
SSHURL: "ssh://git@git.coopcloud.tech:2222/coop-cloud/foo.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/foo/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/foo/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/foo/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/foo/abra.sh"),
},
},
{
name: "mygit.org/myorg/cool-recipe",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
{
name: "mygit.org/myorg/cool-recipe:1.2.4",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
Version: "1.2.4",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("ABRA_DIR", "<abraDir>")
recipe := Get(tc.name)
if diff := cmp.Diff(tc.recipe, recipe); diff != "" {
t.Errorf("Recipe mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
r := Get("traefik")
if err := r.EnsureExists(); err != nil {
offline := true
recipe, err := Get("traefik", offline)
if err != nil {
t.Fatal(err)
}
for i := 1; i < 50; i++ {
label, err := r.GetVersionLabelLocal()
for i := 1; i < 1000; i++ {
label, err := GetVersionLabelLocal(recipe)
if err != nil {
t.Fatal(err)
}

View File

@ -5,7 +5,7 @@ import (
"fmt"
"os/exec"
"coopcloud.tech/abra/pkg/log"
"github.com/sirupsen/logrus"
)
// PassInsertSecret inserts a secret into a pass store.
@ -19,13 +19,13 @@ func PassInsertSecret(secretValue, secretName, appName, server string) error {
secretValue, server, appName, secretName,
)
log.Debugf("attempting to run %s", cmd)
logrus.Debugf("attempting to run %s", cmd)
if err := exec.Command("bash", "-c", cmd).Run(); err != nil {
return err
}
log.Infof("%s inserted into pass store", secretName)
logrus.Infof("%s inserted into pass store", secretName)
return nil
}
@ -41,13 +41,13 @@ func PassRmSecret(secretName, appName, server string) error {
server, appName, secretName,
)
log.Debugf("attempting to run %s", cmd)
logrus.Debugf("attempting to run %s", cmd)
if err := exec.Command("bash", "-c", cmd).Run(); err != nil {
return err
}
log.Infof("%s removed from pass store", secretName)
logrus.Infof("%s removed from pass store", secretName)
return nil
}

View File

@ -11,16 +11,14 @@ import (
"strings"
"sync"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/decentral1se/passgen"
"github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// Secret represents a secret.
@ -54,7 +52,7 @@ func GeneratePasswords(count, length uint) ([]string, error) {
return nil, err
}
log.Debugf("generated %s", strings.Join(passwords, ", "))
logrus.Debugf("generated %s", strings.Join(passwords, ", "))
return passwords, nil
}
@ -72,7 +70,7 @@ func GeneratePassphrases(count uint) ([]string, error) {
return nil, err
}
log.Debugf("generated %s", strings.Join(passphrases, ", "))
logrus.Debugf("generated %s", strings.Join(passphrases, ", "))
return passphrases, nil
}
@ -83,7 +81,7 @@ func GeneratePassphrases(count uint) ([]string, error) {
// "app new" case where we pass in the .env.sample and the "secret generate"
// case where the app is created.
func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName string) (map[string]Secret, error) {
appEnv, appModifiers, err := envfile.ReadEnvWithModifiers(appEnvPath)
appEnv, appModifiers, err := config.ReadEnvWithModifiers(appEnvPath)
if err != nil {
return nil, err
}
@ -109,7 +107,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
}
if len(enabledSecrets) == 0 {
log.Debugf("not generating app secrets, none enabled in recipe config")
logrus.Debugf("not generating app secrets, none enabled in recipe config")
return nil, nil
}
@ -120,7 +118,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
}
if !(slices.Contains(enabledSecrets, secretId)) {
log.Warnf("%s not enabled in recipe config, skipping", secretId)
logrus.Warnf("%s not enabled in recipe config, skipping", secretId)
continue
}
@ -170,7 +168,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
go func(secretName string, secret Secret) {
defer wg.Done()
log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
logrus.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
if secret.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secret.Length))
@ -181,7 +179,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists, moving on...", secret.RemoteName)
logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil
} else {
ch <- err
@ -201,7 +199,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists, moving on...", secret.RemoteName)
logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
ch <- nil
} else {
ch <- err
@ -226,7 +224,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
}
}
log.Debugf("generated and stored %v on %s", secrets, server)
logrus.Debugf("generated and stored %v on %s", secrets, server)
return secretsGenerated, nil
}
@ -242,10 +240,10 @@ type secretStatuses []secretStatus
// PollSecretsStatus checks status of secrets by comparing the local recipe
// config and deploymend server state.
func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) {
func PollSecretsStatus(cl *dockerClient.Client, app config.App) (secretStatuses, error) {
var secStats secretStatuses
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil {
return secStats, err
}

View File

@ -5,7 +5,7 @@ import (
"path"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"github.com/sirupsen/logrus"
)
// CreateServerDir creates a server directory under ~/.abra.
@ -17,11 +17,11 @@ func CreateServerDir(serverName string) error {
return err
}
log.Debugf("%s already exists", serverPath)
logrus.Infof("%s already exists", serverPath)
return nil
}
log.Debugf("successfully created %s", serverPath)
logrus.Infof("successfully created %s", serverPath)
return nil
}

View File

@ -6,12 +6,12 @@ import (
"strings"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// GetService retrieves a service container based on a label. If prompt is true
@ -52,7 +52,7 @@ func GetServiceByLabel(c context.Context, cl *client.Client, label string, promp
return swarm.Service{}, err
}
log.Warnf("ambiguous service list received, prompting for input")
logrus.Warnf("ambiguous service list received, prompting for input")
var response string
prompt := &survey.Select{
@ -72,7 +72,7 @@ func GetServiceByLabel(c context.Context, cl *client.Client, label string, promp
}
}
log.Fatal("failed to match chosen service")
logrus.Panic("failed to match chosen service")
}
return matchingServices[0], nil
@ -106,7 +106,7 @@ func GetService(c context.Context, cl *client.Client, filters filters.Args, prom
return swarm.Service{}, err
}
log.Warnf("ambiguous service list received, prompting for input")
logrus.Warnf("ambiguous service list received, prompting for input")
var response string
prompt := &survey.Select{
@ -126,7 +126,7 @@ func GetService(c context.Context, cl *client.Client, filters filters.Args, prom
}
}
log.Fatal("failed to match chosen service")
logrus.Panic("failed to match chosen service")
}
return services[0], nil

View File

@ -2,14 +2,73 @@ package ssh
import (
"fmt"
"os/exec"
"strings"
"github.com/sirupsen/logrus"
)
// HostConfig is a SSH host config.
type HostConfig struct {
Host string
IdentityFile string
Port string
User string
}
// String presents a human friendly output for the HostConfig.
func (h HostConfig) String() string {
return fmt.Sprintf(
"{host: %s, username: %s, port: %s, identityfile: %s}",
h.Host,
h.User,
h.Port,
h.IdentityFile,
)
}
// GetHostConfig retrieves a ~/.ssh/config config for a host using /usr/bin/ssh
// directly. We therefore maintain consistent interop with this standard
// tooling. This is useful because SSH confuses a lot of people and having to
// learn how two tools (`ssh` and `abra`) handle SSH connection details instead
// of one (just `ssh`) is Not Cool. Here's to less bug reports on this topic!
func GetHostConfig(hostname string) (HostConfig, error) {
var hostConfig HostConfig
out, err := exec.Command("ssh", "-G", hostname).Output()
if err != nil {
return hostConfig, err
}
for _, line := range strings.Split(string(out), "\n") {
entries := strings.Split(line, " ")
for idx, entry := range entries {
if entry == "hostname" {
hostConfig.Host = entries[idx+1]
}
if entry == "user" {
hostConfig.User = entries[idx+1]
}
if entry == "port" {
hostConfig.Port = entries[idx+1]
}
if entry == "identityfile" {
if hostConfig.IdentityFile == "" {
hostConfig.IdentityFile = entries[idx+1]
}
}
}
}
logrus.Debugf("retrieved ssh config for %s: %s", hostname, hostConfig.String())
return hostConfig, nil
}
// Fatal is a error output wrapper which aims to make SSH failures easier to
// parse through re-wording.
func Fatal(hostname string, err error) error {
out := err.Error()
if strings.Contains(out, "Host key verification failed.") {
return fmt.Errorf("SSH host key verification failed for %s", hostname)
} else if strings.Contains(out, "Could not resolve hostname") {
@ -19,8 +78,8 @@ func Fatal(hostname string, err error) error {
} else if strings.Contains(out, "Permission denied") {
return fmt.Errorf("ssh auth: permission denied for %s", hostname)
} else if strings.Contains(out, "Network is unreachable") {
return fmt.Errorf("unable to connect to %s, please check your SSH config", hostname)
}
return fmt.Errorf("unable to connect to %s, network is unreachable?", hostname)
} else {
return err
}
}

View File

@ -1,64 +1,22 @@
package test
import (
"log"
"os"
"path"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
)
var (
TestFolder = os.ExpandEnv("$PWD/../../tests/resources/test_folder")
ValidAbraConf = os.ExpandEnv("$PWD/../../tests/resources/valid_abra_config")
)
// make sure these are in alphabetical order
var (
TFolders = []string{"folder1", "folder2"}
TFiles = []string{"bar.env", "foo.env"}
)
var (
AppName = "ecloud"
ServerName = "evil.corp"
)
var ExpectedAppEnv = envfile.AppEnv{
"DOMAIN": "ecloud.evil.corp",
"RECIPE": "ecloud",
}
var ExpectedApp = appPkg.App{
Name: AppName,
Recipe: recipe.Get(ExpectedAppEnv["RECIPE"]),
Domain: ExpectedAppEnv["DOMAIN"],
Env: ExpectedAppEnv,
Path: ExpectedAppFile.Path,
Server: ExpectedAppFile.Server,
}
var ExpectedAppFile = appPkg.AppFile{
Path: path.Join(ValidAbraConf, "servers", ServerName, AppName+".env"),
Server: ServerName,
}
var ExpectedAppFiles = map[string]appPkg.AppFile{
AppName: ExpectedAppFile,
}
// RmServerAppRecipe deletes the test server / app / recipe.
func RmServerAppRecipe() {
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
if err := os.Remove(testAppLink); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
if err := os.Remove(testRecipeLink); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
}

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