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 - name: make test
image: golang:1.21 image: golang:1.21
environment: environment:
CATL_URL: https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git ABRA_DIR: "/root/.abra"
commands: commands:
- mkdir -p $HOME/.abra - make build-abra
- git clone $CATL_URL $HOME/.abra/catalogue - ./abra help # show version, initialise $ABRA_DIR
- make test - make test
depends_on: depends_on:
- make check - make check
@ -54,36 +54,11 @@ steps:
tags: dev tags: dev
registry: git.coopcloud.tech registry: git.coopcloud.tech
when: when:
branch: event:
- main exclude:
- pull_request
depends_on: depends_on:
- make check - 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: volumes:
- name: deps - name: deps

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,12 @@ package app
import ( import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "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" "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 { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) 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"} tableCol := []string{"recipe env sample", "app env"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
envVars, err := appPkg.CheckEnv(app) envVars, err := config.CheckEnv(app)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, envVar := range envVars { for _, envVar := range envVars {

View File

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

View File

@ -6,10 +6,10 @@ import (
"os/exec" "os/exec"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -30,24 +30,24 @@ var appConfigCommand = cli.Command{
internal.ShowSubcommandHelpAndError(c, errors.New("no app provided")) internal.ShowSubcommandHelpAndError(c, errors.New("no app provided"))
} }
files, err := appPkg.LoadAppFiles("") files, err := config.LoadAppFiles("")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
appFile, exists := files[appName] appFile, exists := files[appName]
if !exists { 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") ed, ok := os.LookupEnv("EDITOR")
if !ok { if !ok {
edPrompt := &survey.Select{ 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"}, Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"},
} }
if err := survey.AskOne(edPrompt, &ed); err != nil { 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.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil return nil

View File

@ -15,13 +15,13 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "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 { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
src := c.Args().Get(1) src := c.Args().Get(1)
dst := c.Args().Get(2) dst := c.Args().Get(2)
if src == "" { if src == "" {
log.Fatal("missing <src> argument") logrus.Fatal("missing <src> argument")
} }
if dst == "" { if dst == "" {
log.Fatal("missing <dest> argument") logrus.Fatal("missing <dest> argument")
} }
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst) srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service) container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
if err != nil { 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 { if toContainer {
err = CopyToContainer(cl, container.ID, srcPath, dstPath) 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) err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
} }
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil return nil
@ -171,7 +167,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
return err 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} copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil { if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
return err return err

View File

@ -2,19 +2,21 @@ package app
import ( import (
"context" "context"
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -33,147 +35,156 @@ var appDeployCommand = cli.Command{
internal.OfflineFlag, internal.OfflineFlag,
}, },
Before: internal.SubCommandBefore, 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 You may pass "--force" to re-deploy the same version again. This can be useful
checkout as-is. Recipe commit hashes are also supported values for if the container runtime has gotten into a weird state.
"[<version>]". Please note, "upgrade"/"rollback" do not support chaos
operations.
EXAMPLE: 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
abra app deploy foo.example.com recipes.
abra app deploy foo.example.com 1.2.3+3.2.1 `,
abra app deploy foo.example.com 1e83340e`,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
specificVersion := c.Args().Get(1) specificVersion := c.Args().Get(1)
if specificVersion == "" {
specificVersion = app.Recipe.Version
}
if specificVersion != "" && internal.Chaos { 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 { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := lint.LintForErrors(app.Recipe); err != nil { if !internal.Chaos {
log.Fatal(err) 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) cl, err := client.New(app.Server)
if err != nil { 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 { if err != nil {
log.Fatal(err) logrus.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
}
} }
secStats, err := secret.PollSecretsStatus(cl, app) secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, secStat := range secStats { for _, secStat := range secStats {
if !secStat.CreatedOnRemote { 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 { 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 { } 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 == "" { if !internal.Chaos && specificVersion == "" {
versions, err := app.Recipe.Tags() catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { 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 { if len(versions) > 0 && !internal.Chaos {
version = versions[len(versions)-1] version = versions[len(versions)-1]
log.Debugf("choosing %s as version to deploy", version) logrus.Debugf("choosing %s as version to deploy", version)
if _, err := app.Recipe.EnsureVersion(version); err != nil { if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
head, err := app.Recipe.Head() head, err := git.GetRecipeHead(app.Recipe)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
version = formatter.SmallSHA(head.String()) 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 { if internal.Chaos {
log.Warnf("chaos mode engaged") logrus.Warnf("chaos mode engaged")
if isChaosCommit {
chaosVersion = specificVersion
versionLabelLocal, err := app.Recipe.GetVersionLabelLocal()
if err != nil {
log.Fatal(err)
}
version = versionLabelLocal
} else {
var err error var err error
chaosVersion, err = app.Recipe.ChaosVersion() version, err = recipe.ChaosVersion(app.Recipe)
if err != nil { 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 { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for k, v := range abraShEnv { for k, v := range abraShEnv {
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
@ -181,69 +192,61 @@ EXAMPLE:
Namespace: stackName, Namespace: stackName,
Prune: false, Prune: false,
ResolveImage: stack.ResolveImageAlways, 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 { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
appPkg.ExposeAllEnv(stackName, compose, app.Env) config.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) config.SetRecipeLabel(compose, stackName, app.Recipe)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos) config.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, chaosVersion) config.SetChaosVersionLabel(compose, stackName, version)
appPkg.SetUpdateLabel(compose, stackName, app.Env) config.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app) envVars, err := config.CheckEnv(app)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, envVar := range envVars { for _, envVar := range envVars {
if !envVar.Present { 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 { if err := internal.DeployOverview(app, version, "continue with deployment?"); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !internal.NoDomainChecks { if !internal.NoDomainChecks {
domainName, ok := app.Env["DOMAIN"] domainName, ok := app.Env["DOMAIN"]
if ok { if ok {
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil { if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
log.Warn("skipping domain checks as no DOMAIN=... configured for app") logrus.Warn("skipping domain checks as no DOMAIN=... configured for app")
} }
} else { } 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 { 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 { 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"] postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
if ok && !internal.DontWaitConverge { 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 { if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err) logrus.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)
} }
} }
return nil 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" "strings"
"coopcloud.tech/abra/cli/internal" "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/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
var ( var status bool
status bool var statusFlag = &cli.BoolFlag{
statusFlag = &cli.BoolFlag{
Name: "status, S", Name: "status, S",
Usage: "Show app deployment status", Usage: "Show app deployment status",
Destination: &status, Destination: &status,
} }
)
var ( var recipeFilter string
recipeFilter string var recipeFlag = &cli.StringFlag{
recipeFlag = &cli.StringFlag{
Name: "recipe, r", Name: "recipe, r",
Value: "", Value: "",
Usage: "Show apps of a specific recipe", Usage: "Show apps of a specific recipe",
Destination: &recipeFilter, Destination: &recipeFilter,
} }
)
var ( var listAppServer string
listAppServer string var listAppServerFlag = &cli.StringFlag{
listAppServerFlag = &cli.StringFlag{
Name: "server, s", Name: "server, s",
Value: "", Value: "",
Usage: "Show apps of a specific server", Usage: "Show apps of a specific server",
Destination: &listAppServer, Destination: &listAppServer,
} }
)
type appStatus struct { type appStatus struct {
Server string `json:"server"` 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 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 actual live deployment status. Depending on how many servers you manage, this
can take some time.`, can take some time.
`,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.MachineReadableFlag, internal.MachineReadableFlag,
@ -87,19 +83,20 @@ can take some time.`,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
appFiles, err := appPkg.LoadAppFiles(listAppServer) appFiles, err := config.LoadAppFiles(listAppServer)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
apps, err := appPkg.GetApps(appFiles, recipeFilter) apps, err := config.GetApps(appFiles, recipeFilter)
if err != nil { 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) statuses := make(map[string]map[string]string)
var catl recipe.RecipeCatalogue
if status { if status {
alreadySeen := make(map[string]bool) alreadySeen := make(map[string]bool)
for _, app := range apps { 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 { 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 != "" { if recipeFilter != "" {
// only count server if matches filter // only count server if matches filter
totalServersCount++ totalServersCount++
@ -175,20 +177,20 @@ can take some time.`,
var newUpdates []string var newUpdates []string
if version != "unknown" { if version != "unknown" {
updates, err := app.Recipe.Tags() updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, update := range updates { for _, update := range updates {
parsedUpdate, err := tagcmp.Parse(update) parsedUpdate, err := tagcmp.Parse(update)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) { if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
@ -212,7 +214,7 @@ can take some time.`,
} }
appStats.Server = app.Server appStats.Server = app.Server
appStats.Recipe = app.Recipe.Name appStats.Recipe = app.Recipe
appStats.AppName = app.Name appStats.AppName = app.Name
appStats.Domain = app.Domain appStats.Domain = app.Domain
@ -224,7 +226,7 @@ can take some time.`,
if internal.MachineReadable { if internal.MachineReadable {
jsonstring, err := json.Marshal(allStats) jsonstring, err := json.Marshal(allStats)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} else { } else {
fmt.Println(string(jsonstring)) fmt.Println(string(jsonstring))
} }
@ -253,7 +255,7 @@ can take some time.`,
if chaosStatus != "unknown" { if chaosStatus != "unknown" {
chaosEnabled, err := strconv.ParseBool(chaosStatus) chaosEnabled, err := strconv.ParseBool(chaosStatus)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if chaosEnabled && appStat.ChaosVersion != "unknown" { if chaosEnabled && appStat.ChaosVersion != "unknown" {
chaosStatus = appStat.ChaosVersion chaosStatus = appStat.ChaosVersion

View File

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

View File

@ -2,25 +2,26 @@ package app
import ( import (
"fmt" "fmt"
"path"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/jsontable" "coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
var appNewDescription = ` var appNewDescription = `
Creates a new app from a default recipe. This new app configuration is stored Take a recipe and uses it to create a new app. This new app configuration is
in your $ABRA_DIR directory under the appropriate server. 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 This command does not deploy your app for you. You will need to run "abra app
deploy <domain>" to do so. 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 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 pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.` on your $PATH.
`
var appNewCommand = cli.Command{ var appNewCommand = cli.Command{
Name: "new", Name: "new",
@ -67,63 +69,43 @@ var appNewCommand = cli.Command{
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c)
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(); err != nil { if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(); err != nil { if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
if c.Args().Get(1) == "" { if c.Args().Get(1) == "" {
var version string if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
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)
} }
} else { } else {
if err := recipe.EnsureLatest(); err != nil { if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil {
log.Fatal(err) logrus.Fatal(err)
}
}
} else {
if _, err := recipe.EnsureVersion(c.Args().Get(1)); err != nil {
log.Fatal(err)
} }
} }
} }
if err := ensureServerFlag(); err != nil { if err := ensureServerFlag(); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil { if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
sanitisedAppName := appPkg.SanitiseAppName(internal.Domain) sanitisedAppName := config.SanitiseAppName(internal.Domain)
log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName) logrus.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
if err := appPkg.TemplateAppEnvSample( if err := config.TemplateAppEnvSample(
recipe, recipe.Name,
internal.Domain, internal.Domain,
internal.NewAppServer, internal.NewAppServer,
internal.Domain, internal.Domain,
); err != nil { ); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
var secrets AppSecrets var secrets AppSecrets
@ -131,31 +113,32 @@ var appNewCommand = cli.Command{
if internal.Secrets { if internal.Secrets {
sampleEnv, err := recipe.SampleEnv() sampleEnv, err := recipe.SampleEnv()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
composeFiles, err := recipe.GetComposeFiles(sampleEnv) composeFiles, err := config.GetComposeFiles(recipe.Name, sampleEnv)
if err != nil { 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 { if err != nil {
return err return err
} }
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil { if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
cl, err := client.New(internal.NewAppServer) cl, err := client.New(internal.NewAppServer)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName) secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secretCols := []string{"Name", "Value"} secretCols := []string{"Name", "Value"}
@ -173,25 +156,22 @@ var appNewCommand = cli.Command{
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain}) 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("") fmt.Println("")
table.Render() table.Render()
fmt.Println("") fmt.Println("")
fmt.Println("You can configure this app by running the following:")
fmt.Println("Configure this app:")
fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain)) fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain))
fmt.Println("") 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)) fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
if len(secrets) > 0 { if len(secrets) > 0 {
fmt.Println("") fmt.Println("")
fmt.Println("Generated secrets:") fmt.Println("Here are your generated secrets:")
fmt.Println("") fmt.Println("")
secretTable.Render() 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 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) { func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
// NOTE(d1): trim to match app.StackName() implementation // NOTE(d1): trim to match app.StackName() implementation
if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH { 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] 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/ // 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 { if internal.Domain == "" && !internal.NoInput {
prompt := &survey.Input{ prompt := &survey.Input{
Message: "Specify app domain", 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. // promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error { func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
if len(secretsConfig) == 0 { 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 return nil
} }

View File

@ -2,21 +2,21 @@ package app
import ( import (
"context" "context"
"encoding/json" "strings"
"fmt" "time"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/service"
abraService "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/buger/goterm"
dockerFormatter "github.com/docker/cli/cli/command/formatter" dockerFormatter "github.com/docker/cli/cli/command/formatter"
containerTypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -25,145 +25,77 @@ var appPsCommand = cli.Command{
Aliases: []string{"p"}, Aliases: []string{"p"},
Usage: "Check app status", Usage: "Check app status",
ArgsUsage: "<domain>", ArgsUsage: "<domain>",
Description: "Show status of a deployed app.", Description: "Show a more detailed status output of a specific deployed app",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.MachineReadableFlag, internal.WatchFlag,
internal.DebugFlag, internal.DebugFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(false, false); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { 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 { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !deployMeta.IsDeployed { if !isDeployed {
log.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
chaosVersion := "false" if !internal.Watch {
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true) showPSOutput(c, app, cl)
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)
return nil return nil
}
goterm.Clear()
for {
goterm.MoveCursor(1, 1)
showPSOutput(c, app, cl)
goterm.Flush()
time.Sleep(2 * time.Second)
}
}, },
} }
// showPSOutput renders ps output. // showPSOutput renders ps output.
func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chaosVersion string) { func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) filters, err := app.Filters(true, true)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
return
} }
deployOpts := stack.Deploy{ containers, err := cl.ContainerList(context.Background(), types.ContainerListOptions{Filters: filters})
Composefiles: composeFiles,
Namespace: app.StackName(),
Prune: false,
ResolveImage: stack.ResolveImageAlways,
}
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
return
} }
var tablerows [][]string tableCol := []string{"service name", "image", "created", "status", "state", "ports"}
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"}
table := formatter.CreateTable(tableCol) 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() table.Render()
} }

View File

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

View File

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

View File

@ -6,7 +6,8 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -31,32 +32,49 @@ var appRestoreCommand = cli.Command{
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) 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) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
targetContainer, err := internal.RetrieveBackupBotContainer(cl) targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)} execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" { 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)) execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
} }
if targetPath != "" { 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)) execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
} }
if err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil { if err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil return nil

View File

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

View File

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

View File

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

View File

@ -9,10 +9,10 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" 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" "github.com/urfave/cli"
) )
@ -28,32 +28,29 @@ var appServicesCommand = cli.Command{
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { 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 { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !deployMeta.IsDeployed { if !isDeployed {
log.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
filters, err := app.Filters(true, true) filters, err := app.Filters(true, true)
if err != nil { 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 { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
tableCol := []string{"service name", "image"} tableCol := []string{"service name", "image"}

View File

@ -3,16 +3,17 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -27,10 +28,27 @@ var pruneFlag = &cli.BoolFlag{
// pruneApp runs the equivalent of a "docker system prune" but only filtering // 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 // against resources connected with the app deployment. It is not a system wide
// prune. Volumes are not pruned to avoid unwated data loss. // 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() stackName := app.StackName()
ctx := context.Background() 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() pruneFilters := filters.NewArgs()
stackSearch := fmt.Sprintf("%s*", stackName) stackSearch := fmt.Sprintf("%s*", stackName)
pruneFilters.Add("label", stackSearch) pruneFilters.Add("label", stackSearch)
@ -40,14 +58,14 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
} }
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) 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) nr, err := cl.NetworksPrune(ctx, pruneFilters)
if err != nil { if err != nil {
return err return err
} }
log.Infof("networks pruned: %d", len(nr.NetworksDeleted)) logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
ir, err := cl.ImagesPrune(ctx, pruneFilters) ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil { if err != nil {
@ -55,7 +73,7 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
} }
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed) 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 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 However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed. 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 { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
stackName := app.StackName() stackName := app.StackName()
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { 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 { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !deployMeta.IsDeployed { if !isDeployed {
log.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
chaosVersion := "false" if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
if deployMeta.IsChaos { logrus.Fatal(err)
chaosVersion = deployMeta.ChaosVersion
} }
if err := internal.DeployOverview(app, deployMeta.Version, chaosVersion, "continue with undeploy?"); err != nil { rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
log.Fatal(err)
}
rmOpts := stack.Remove{
Namespaces: []string{app.StackName()},
Detach: false,
}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil { if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if prune { if prune {
if err := pruneApp(cl, app); err != nil { if err := pruneApp(c, cl, app); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }

View File

@ -5,15 +5,16 @@ import (
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint" "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" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -26,101 +27,135 @@ var appUpgradeCommand = cli.Command{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
internal.ForceFlag, internal.ForceFlag,
internal.ChaosFlag,
internal.NoDomainChecksFlag, internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag, internal.DontWaitConvergeFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.ReleaseNotesFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` 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 This command specifically supports incrementing the version of running apps, as
are supported values for "[<version>]". 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 You may pass "--force/-f" to upgrade to the same version again. This can be
beforehand. 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 Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
abra app upgrade foo.example.com 1.2.3+3.2.1`, including unstaged changes and can be useful for live hacking and testing new
recipes.
`,
BashComplete: autocomplete.AppNameComplete, BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { specificVersion := c.Args().Get(1)
log.Fatal(err) if specificVersion != "" && internal.Chaos {
logrus.Fatal("cannot use <version> and --chaos together")
} }
if err := lint.LintForErrors(app.Recipe); err != nil { if !internal.Chaos {
log.Fatal(err) 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) cl, err := client.New(app.Server)
if err != nil { 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 { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !deployMeta.IsDeployed { if !isDeployed {
log.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
versions, err := app.Recipe.Tags() catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil { 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 var availableUpgrades []string
if deployMeta.Version == "unknown" { if deployedVersion == "unknown" {
availableUpgrades = versions 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 != "" { if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil { 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) parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil { 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) {
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) { logrus.Fatalf("%s is not an upgrade for %s?", deployedVersion, specificVersion)
log.Fatalf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
} }
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
log.Fatalf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
availableUpgrades = append(availableUpgrades, specificVersion) availableUpgrades = append(availableUpgrades, specificVersion)
} }
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
log.Warn("attempting to upgrade a chaos deployment")
} }
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
for _, version := range versions { for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) { if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableUpgrades = append(availableUpgrades, version) availableUpgrades = append(availableUpgrades, version)
@ -128,27 +163,21 @@ EXAMPLE:
} }
if len(availableUpgrades) == 0 && !internal.Force { if len(availableUpgrades) == 0 && !internal.Force {
log.Info("no available upgrades") logrus.Infof("no available upgrades, you're on latest (%s) ✌️", deployedVersion)
return nil return nil
} }
} }
var chosenUpgrade string var chosenUpgrade string
if len(availableUpgrades) > 0 { if len(availableUpgrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput || specificVersion != "" { if internal.Force || internal.NoInput || specificVersion != "" {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] 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 { } 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{ prompt := &survey.Select{
Message: msg, Message: fmt.Sprintf("Please select an upgrade (current version: %s):", deployedVersion),
Options: internal.ReverseStringList(availableUpgrades), Options: internal.ReverseStringList(availableUpgrades),
} }
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return err return err
} }
@ -156,26 +185,26 @@ EXAMPLE:
} }
if internal.Force && chosenUpgrade == "" { if internal.Force && chosenUpgrade == "" {
log.Warnf("%s is already upgraded to latest but continuing (--force)", app.Name) logrus.Warnf("%s is already upgraded to latest but continuing (--force/--chaos)", app.Name)
chosenUpgrade = deployMeta.Version chosenUpgrade = deployedVersion
} }
// if release notes written after git tag published, read them before we // 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 // 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 // when we obviously will forget to write release notes before publishing
var releaseNotes string var releaseNotes string
if chosenUpgrade != "" {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
log.Fatal(err)
}
for _, version := range versions { for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) { parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
note, err := app.Recipe.GetReleaseNotes(version) if err != nil {
logrus.Fatal(err)
}
if !(parsedVersion.Equals(parsedDeployedVersion)) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := internal.GetReleaseNotes(app.Recipe, version)
if err != nil { if err != nil {
return err 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 internal.Chaos {
if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil { logrus.Warn("chaos mode engaged")
log.Fatal(err) var err error
} chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe)
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
if err != nil { 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 { for k, v := range abraShEnv {
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Composefiles: composeFiles, Composefiles: composeFiles,
Namespace: stackName, Namespace: stackName,
Prune: false, Prune: false,
ResolveImage: stack.ResolveImageAlways, ResolveImage: stack.ResolveImageAlways,
Detach: false,
} }
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil { 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) envVars, err := config.CheckEnv(app)
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)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, envVar := range envVars { for _, envVar := range envVars {
if !envVar.Present { 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 { if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
fmt.Println() logrus.Fatal(err)
fmt.Print(releaseNotes)
return nil
} }
chaosVersion := "false" stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
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)
if err != nil { 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 { 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"] postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
if ok && !internal.DontWaitConverge { 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 { if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err) logrus.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)
} }
} }

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

View File

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

View File

@ -14,10 +14,10 @@ import (
"coopcloud.tech/abra/cli/recipe" "coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server" "coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
cataloguePkg "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/web" "coopcloud.tech/abra/pkg/web"
charmLog "github.com/charmbracelet/log" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -25,15 +25,16 @@ import (
var AutoCompleteCommand = cli.Command{ var AutoCompleteCommand = cli.Command{
Name: "autocomplete", Name: "autocomplete",
Aliases: []string{"ac"}, Aliases: []string{"ac"},
Usage: "Configure shell autocompletion", Usage: "Configure shell autocompletion (recommended)",
Description: ` 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>", ArgsUsage: "<shell>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
@ -53,7 +54,7 @@ EXAMPLE:
} }
if _, ok := supportedShells[shellType]; !ok { 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" { if shellType == "fizsh" {
@ -63,24 +64,24 @@ EXAMPLE:
autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion") autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion")
if err := os.Mkdir(autocompletionDir, 0764); err != nil { if err := os.Mkdir(autocompletionDir, 0764); err != nil {
if !os.IsExist(err) { 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) autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType)
if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) { 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) 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 { if err := web.GetFile(autocompletionFile, url); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
switch shellType { switch shellType {
case "bash": case "bash":
fmt.Println(fmt.Sprintf(` 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 mkdir /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
@ -88,19 +89,19 @@ echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
`, autocompletionFile)) `, autocompletionFile))
case "zsh": case "zsh":
fmt.Println(fmt.Sprintf(` 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 mkdir /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc 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)) `, autocompletionFile))
case "fish": case "fish":
fmt.Println(fmt.Sprintf(` 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 mkdir -p /etc/fish/completions
sudo cp %s /etc/fish/completions/abra sudo cp %s /etc/fish/completions/abra
echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish 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)) `, autocompletionFile))
} }
@ -112,18 +113,14 @@ echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
var UpgradeCommand = cli.Command{ var UpgradeCommand = cli.Command{
Name: "upgrade", Name: "upgrade",
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "Upgrade abra", Usage: "Upgrade Abra itself",
Description: ` 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 Pass "-r/--rc" to install the latest release candidate. Please bear in mind
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much that it may contain catastrophic bugs. Thank you very much for the testing
for the testing efforts 💗 efforts!
`,
EXAMPLE:
abra upgrade
abra upgrade --rc`,
Flags: []cli.Flag{internal.RCFlag}, Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
mainURL := "https://install.abra.coopcloud.tech" 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)) 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 { if err := internal.RunCmd(cmd); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil return nil
@ -147,7 +144,7 @@ EXAMPLE:
func newAbraApp(version, commit string) *cli.App { func newAbraApp(version, commit string) *cli.App {
app := &cli.App{ app := &cli.App{
Name: "abra", 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 { for _, path := range paths {
if err := os.Mkdir(path, 0764); err != nil { if err := os.Mkdir(path, 0764); err != nil {
if !os.IsExist(err) { if !os.IsExist(err) {
log.Fatal(err) logrus.Fatal(err)
} }
continue continue
} }
} }
charmLog.SetDefault(log.Logger) if err := cataloguePkg.EnsureCatalogue(); err != nil {
log.Debugf("abra version %s, commit %s", version, commit) logrus.Fatal(err)
}
logrus.Debugf("abra version %s, commit %s", version, commit)
return nil return nil
} }
@ -201,6 +201,6 @@ func RunApp(version, commit string) {
app := newAbraApp(version, commit) app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil { 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" "coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/service"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
) )
// RetrieveBackupBotContainer gets the deployed backupbot container. // RetrieveBackupBotContainer gets the deployed backupbot container.
@ -22,7 +22,7 @@ func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error
return types.Container{}, err 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 := filters.NewArgs()
filters.Add("name", chosenService.Spec.Name) filters.Add("name", chosenService.Spec.Name)
@ -51,7 +51,7 @@ func RunBackupCmdRemote(cl *dockerClient.Client, backupCmd string, containerID s
Tty: true, 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 // FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli() dcli, err := command.NewDockerCli()

View File

@ -3,7 +3,8 @@ package internal
import ( import (
"os" "os"
"coopcloud.tech/abra/pkg/log" logrusStack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -37,20 +38,6 @@ var PassRemoveFlag = &cli.BoolFlag{
Destination: &PassRemove, 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. // Force force functionality without asking.
var Force bool var Force bool
@ -108,16 +95,6 @@ var OfflineFlag = &cli.BoolFlag{
Usage: "Prefer offline & filesystem access when possible", 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 // MachineReadable stores the variable from MachineReadableFlag
var MachineReadable bool var MachineReadable bool
@ -192,7 +169,7 @@ var NewAppServerFlag = &cli.StringFlag{
var NoDomainChecks bool var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{ var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks, D", Name: "no-domain-checks, D",
Usage: "Disable public DNS checks", Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks, Destination: &NoDomainChecks,
} }
@ -261,35 +238,13 @@ var RemoteUserFlag = &cli.StringFlag{
Destination: &RemoteUser, 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). // SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
func SubCommandBefore(c *cli.Context) error { func SubCommandBefore(c *cli.Context) error {
if Debug { if Debug {
log.SetLevel(log.DebugLevel) logrus.SetLevel(logrus.DebugLevel)
log.SetOutput(os.Stderr) logrus.SetFormatter(&logrus.TextFormatter{})
log.SetReportCaller(true) logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
} }
return nil return nil

View File

@ -8,20 +8,20 @@ import (
"os/exec" "os/exec"
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
) )
// RunCmdRemote executes an abra.sh command in the target service // 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 := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName)) 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 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} toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(abraSh, toTarOpts) 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 { 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" 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)} 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 != "" { if RemoteUser != "" {
log.Debugf("running command with user %s", RemoteUser) logrus.Debugf("running command with user %s", RemoteUser)
execCreateOpts.User = RemoteUser execCreateOpts.User = RemoteUser
} }

View File

@ -2,19 +2,21 @@ package internal
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path"
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
) )
// NewVersionOverview shows an upgrade or downgrade overview // NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion, releaseNotes string) error { func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos", "to deploy"} tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml" deployConfig := "compose.yml"
@ -27,22 +29,14 @@ func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion
server = "local" server = "local"
} }
table.Append([]string{ table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
server,
app.Recipe.Name,
deployConfig,
app.Domain,
currentVersion,
chaosVersion,
newVersion,
})
table.Render() table.Render()
if releaseNotes != "" && newVersion != "" { if releaseNotes != "" && newVersion != "" {
fmt.Println() fmt.Println()
fmt.Print(releaseNotes) fmt.Print(releaseNotes)
} else { } else {
log.Warnf("no release notes available for %s", newVersion) logrus.Warnf("no release notes available for %s", newVersion)
} }
if NoInput { if NoInput {
@ -59,19 +53,40 @@ func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion
} }
if !response { if !response {
log.Fatal("exiting as requested") logrus.Fatal("exiting as requested")
} }
return nil 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 // PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format: // the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... " // "<service> <command> <arguments>|<service> <command> <arguments>|... "
func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
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) { 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 return err
} }
@ -87,13 +102,13 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
if len(commandParts) > 2 { if len(commandParts) > 2 {
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(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 return err
} }
serviceNames, err := appPkg.GetAppServiceNames(app.Name) serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil { if err != nil {
return err 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)) 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 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 return err
} }
} }
@ -120,8 +135,8 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
} }
// DeployOverview shows a deployment overview // DeployOverview shows a deployment overview
func DeployOverview(app appPkg.App, version, chaosVersion, message string) error { func DeployOverview(app config.App, version, message string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos"} tableCol := []string{"server", "recipe", "config", "domain", "version"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml" deployConfig := "compose.yml"
@ -134,14 +149,7 @@ func DeployOverview(app appPkg.App, version, chaosVersion, message string) error
server = "local" server = "local"
} }
table.Append([]string{ table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
server,
app.Recipe.Name,
deployConfig,
app.Domain,
version,
chaosVersion,
})
table.Render() table.Render()
if NoInput { if NoInput {
@ -158,7 +166,7 @@ func DeployOverview(app appPkg.App, version, chaosVersion, message string) error
} }
if !response { if !response {
log.Fatal("exiting as requested") logrus.Fatal("exiting as requested")
} }
return nil return nil

View File

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

View File

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

View File

@ -6,9 +6,9 @@ import (
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -21,7 +21,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
catl, err := recipe.ReadRecipeCatalogue(Offline) catl, err := recipe.ReadRecipeCatalogue(Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
knownRecipes := make(map[string]bool) knownRecipes := make(map[string]bool)
@ -31,7 +31,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
localRecipes, err := recipe.GetRecipesLocal() localRecipes, err := recipe.GetRecipesLocal()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, recipeLocal := range localRecipes { for _, recipeLocal := range localRecipes {
@ -49,7 +49,7 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
Options: recipes, Options: recipes,
} }
if err := survey.AskOne(prompt, &recipeName); err != nil { 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")) ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
} }
chosenRecipe := recipe.Get(recipeName) chosenRecipe, err := recipe.Get(recipeName, Offline)
err := chosenRecipe.EnsureExists()
if err != nil {
log.Fatal(err)
}
_, err = chosenRecipe.GetComposeConfig(nil)
if err != nil { if err != nil {
if c.Command.Name == "generate" { if c.Command.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") { if strings.Contains(err.Error(), "missing a compose") {
log.Fatal(err) logrus.Fatal(err)
} }
log.Warn(err) logrus.Warn(err)
} else { } else {
if strings.Contains(err.Error(), "template_driver is not allowed") { 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 return chosenRecipe
} }
// ValidateApp ensures the app name arg is valid. // 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() appName := c.Args().First()
if appName == "" { if appName == "" {
@ -92,10 +87,10 @@ func ValidateApp(c *cli.Context) app.App {
app, err := app.Get(appName) app, err := app.Get(appName)
if err != nil { 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 return app
} }
@ -110,7 +105,7 @@ func ValidateDomain(c *cli.Context) string {
Default: "example.com", Default: "example.com",
} }
if err := survey.AskOne(prompt, &domainName); err != nil { 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")) 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 return domainName
} }
@ -143,7 +138,7 @@ func ValidateServer(c *cli.Context) string {
serverNames, err := config.ReadServerNames() serverNames, err := config.ReadServerNames()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if serverName == "" && !NoInput { if serverName == "" && !NoInput {
@ -152,7 +147,7 @@ func ValidateServer(c *cli.Context) string {
Options: serverNames, Options: serverNames,
} }
if err := survey.AskOne(prompt, &serverName); err != nil { 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?")) 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 return serverName
} }

View File

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

View File

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

View File

@ -7,7 +7,8 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -28,8 +29,24 @@ var recipeLintCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c)
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := recipePkg.EnsureExists(recipe.Name); err != nil {
log.Fatal(err) 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"} tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
@ -40,7 +57,7 @@ var recipeLintCommand = cli.Command{
for level := range lint.LintRules { for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] { for _, rule := range lint.LintRules[level] {
if internal.OnlyErrors && rule.Level != "error" { 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 continue
} }
@ -58,7 +75,7 @@ var recipeLintCommand = cli.Command{
if !skipped { if !skipped {
ok, err := rule.Function(recipe) ok, err := rule.Function(recipe)
if err != nil { if err != nil {
log.Warn(err) logrus.Warn(err)
} }
if !ok && rule.Level == "error" { if !ok && rule.Level == "error" {
@ -97,7 +114,7 @@ var recipeLintCommand = cli.Command{
} }
if hasError { 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 return nil

View File

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

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path" "path"
"text/template" "text/template"
@ -11,8 +12,7 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log" "github.com/sirupsen/logrus"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -37,8 +37,6 @@ var recipeNewCommand = cli.Command{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
internal.OfflineFlag, internal.OfflineFlag,
internal.GitNameFlag,
internal.GitEmailFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Usage: "Create a new recipe", Usage: "Create a new recipe",
@ -48,55 +46,79 @@ Create a new recipe.
Abra uses the built-in example repository which is available here: 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 { Action: func(c *cli.Context) error {
recipeName := c.Args().First() recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName == "" { if recipeName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
} }
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) { directory := path.Join(config.RECIPES_DIR, recipeName)
log.Fatalf("%s recipe directory already exists?", r.Dir) 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) url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(r.Dir, url); err != nil { if err := git.Clone(directory, url); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
gitRepo := path.Join(r.Dir, ".git") gitRepo := path.Join(config.RECIPES_DIR, recipeName, ".git")
if err := os.RemoveAll(gitRepo); err != nil { 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) 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) tpl, err := template.ParseFiles(path)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
var templated bytes.Buffer var templated bytes.Buffer
if err := tpl.Execute(&templated, meta); err != nil { if err := tpl.Execute(&templated, meta); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil { if err := ioutil.WriteFile(path, templated.Bytes(), 0644); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
if err := git.Init(r.Dir, true, internal.GitName, internal.GitEmail); err != nil { newGitRepo := path.Join(config.RECIPES_DIR, recipeName)
log.Fatal(err) if err := git.Init(newGitRepo, true); err != nil {
logrus.Fatal(err)
} }
log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)) fmt.Print(fmt.Sprintf(`
log.Info("happy hacking 🎉") 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 return nil
}, },

View File

@ -18,7 +18,9 @@ for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make 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 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{ Subcommands: []cli.Command{
recipeFetchCommand, recipeFetchCommand,
recipeLintCommand, recipeLintCommand,

View File

@ -10,14 +10,16 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference" "github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "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 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 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{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
@ -62,74 +65,74 @@ your SSH keys configured on your account.`,
imagesTmp, err := getImageVersions(recipe) imagesTmp, err := getImageVersions(recipe)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
mainApp, err := internal.GetMainAppImage(recipe) mainApp, err := internal.GetMainAppImage(recipe)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
mainAppVersion := imagesTmp[mainApp] mainAppVersion := imagesTmp[mainApp]
if mainAppVersion == "" { 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) tagString := c.Args().Get(1)
if tagString != "" { if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil { 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 != "" { 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 tagString != "" {
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil { if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
tags, err := recipe.Tags() tags, err := recipe.Tags()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) { if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
var err error var err error
tagString, err = getLabelVersion(recipe, false) tagString, err = getLabelVersion(recipe, false)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
isClean, err := gitPkg.IsClean(recipe.Dir) isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
if len(tags) > 0 { 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 { if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} else { } 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 err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanUpErr := cleanUpTag(recipe, tagString); err != nil { if cleanUpErr := cleanUpTag(tagString, recipe.Name); err != nil {
log.Fatal(cleanUpErr) 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) { func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
services := make(map[string]string) services := make(map[string]string)
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return nil, err
}
missingTag := false missingTag := false
for _, service := range config.Services { for _, service := range recipe.Config.Services {
if service.Image == "" { if service.Image == "" {
continue continue
} }
@ -185,7 +184,8 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error { func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err 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 { if err != nil {
return err return err
} }
@ -210,19 +210,19 @@ func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string
} }
if err := addReleaseNotes(recipe, tagString); err != nil { if err := addReleaseNotes(recipe, tagString); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := commitRelease(recipe, tagString); err != nil { if err := commitRelease(recipe, tagString); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := tagRelease(tagString, repo); err != nil { if err := tagRelease(tagString, repo); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := pushRelease(recipe, tagString); err != nil { if err := pushRelease(recipe, tagString); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil 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 // addReleaseNotes checks if the release/next release note exists and moves the
// file to release/<tag>. // file to release/<tag>.
func addReleaseNotes(recipe recipe.Recipe, tag string) error { 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 { if _, err := os.Stat(tagReleaseNotePath); err == nil {
// Release note for current tag already exist exists. // Release note for current tag already exist exists.
return nil return nil
@ -254,11 +255,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return err return err
} }
nextReleaseNotePath := path.Join(recipe.Dir, "release", "next") nextReleaseNotePath := path.Join(repoPath, "release", "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil { if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag> // release/next note exists. Move it to release/<tag>
if internal.Dry { 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 return nil
} }
if !internal.NoInput { if !internal.NoInput {
@ -277,11 +278,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -310,7 +311,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -320,23 +321,24 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
func commitRelease(recipe recipe.Recipe, tag string) error { func commitRelease(recipe recipe.Recipe, tag string) error {
if internal.Dry { if internal.Dry {
log.Debugf("dry run: no changes committed") logrus.Debugf("dry run: no changes committed")
return nil return nil
} }
isClean, err := gitPkg.IsClean(recipe.Dir) isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil { if err != nil {
return err return err
} }
if isClean { if isClean {
if !internal.Dry { 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) 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 return err
} }
@ -345,7 +347,7 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
func tagRelease(tagString string, repo *git.Repository) error { func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry { 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 return nil
} }
@ -365,14 +367,14 @@ func tagRelease(tagString string, repo *git.Repository) error {
} }
hash := formatter.SmallSHA(head.Hash().String()) 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 return nil
} }
func pushRelease(recipe recipe.Recipe, tagString string) error { func pushRelease(recipe recipe.Recipe, tagString string) error {
if internal.Dry { if internal.Dry {
log.Info("dry run: no changes published") logrus.Info("dry run: no changes published")
return nil return nil
} }
@ -390,17 +392,18 @@ func pushRelease(recipe recipe.Recipe, tagString string) error {
if err := recipe.Push(internal.Dry); err != nil { if err := recipe.Push(internal.Dry); err != nil {
return err return err
} }
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString) url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString)
log.Infof("new release published: %s", url) logrus.Infof("new release published: %s", url)
} else { } else {
log.Info("no -p/--publish passed, not publishing") logrus.Info("no -p/--publish passed, not publishing")
} }
return nil return nil
} }
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error { 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 { if err != nil {
return err return err
} }
@ -465,7 +468,7 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
} }
if lastGitTag.String() == tagString { 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 { if !internal.NoInput {
@ -475,36 +478,37 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
var ok bool var ok bool
if err := survey.AskOne(prompt, &ok); err != nil { if err := survey.AskOne(prompt, &ok); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !ok { if !ok {
log.Fatal("exiting as requested") logrus.Fatal("exiting as requested")
} }
} }
if err := addReleaseNotes(recipe, tagString); err != nil { if err := addReleaseNotes(recipe, tagString); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := commitRelease(recipe, tagString); err != nil { 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 { 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 { 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 return nil
} }
// cleanUpTag removes a freshly created tag // cleanUpTag removes a freshly created tag
func cleanUpTag(recipe recipe.Recipe, tag string) error { func cleanUpTag(tag, recipeName string) error {
repo, err := git.PlainOpen(recipe.Dir) directory := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
return err 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 return nil
} }
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) { func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
initTag, err := recipe.GetVersionLabelLocal() initTag, err := recipePkg.GetVersionLabelLocal(recipe)
if err != nil { if err != nil {
return "", err return "", err
} }
if initTag == "" { 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 { if prompt && !internal.NoInput {
var response bool var response bool

View File

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

View File

@ -2,16 +2,18 @@ package recipe
import ( import (
"fmt" "fmt"
"path"
"strconv" "strconv"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "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 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 auto-generate it for you. The <recipe> configuration will be updated on the
local file system.`, local file system.
`,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c)
mainApp, err := internal.GetMainAppImage(recipe) mainApp, err := internal.GetMainAppImage(recipe)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
imagesTmp, err := getImageVersions(recipe) imagesTmp, err := getImageVersions(recipe)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
mainAppVersion := imagesTmp[mainApp] mainAppVersion := imagesTmp[mainApp]
tags, err := recipe.Tags() tags, err := recipe.Tags()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
nextTag := c.Args().Get(1) nextTag := c.Args().Get(1)
if len(tags) == 0 && nextTag == "" { 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 { 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(` fmt.Println(fmt.Sprintf(`
The following options are two types of initial semantic version that you can 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 { if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion) nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
@ -99,26 +102,27 @@ likely to change.
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) { if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
latestRelease := tags[len(tags)-1] latestRelease := tags[len(tags)-1]
if err := internal.PromptBumpType("", latestRelease); err != nil { if err := internal.PromptBumpType("", latestRelease); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
if nextTag == "" { if nextTag == "" {
repo, err := git.PlainOpen(recipe.Dir) recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(recipeDir)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
var lastGitTag tagcmp.Tag var lastGitTag tagcmp.Tag
iter, err := repo.Tags() iter, err := repo.Tags()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := iter.ForEach(func(ref *plumbing.Reference) error { if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash()) obj, err := repo.TagObject(ref.Hash())
if err != nil { 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 return err
} }
@ -135,7 +139,7 @@ likely to change.
return nil return nil
}); err != nil { }); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
// bumpType is used to decide what part of the tag should be incremented // bumpType is used to decide what part of the tag should be incremented
@ -143,7 +147,7 @@ likely to change.
if bumpType != 0 { if bumpType != 0 {
// a bitwise check if the number is a power of 2 // a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 { 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 { if internal.Patch {
now, err := strconv.Atoi(newTag.Patch) now, err := strconv.Atoi(newTag.Patch)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = strconv.Itoa(now + 1) newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor { } else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor) now, err := strconv.Atoi(newTag.Minor)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = "0" newTag.Patch = "0"
@ -167,7 +171,7 @@ likely to change.
} else if internal.Major { } else if internal.Major {
now, err := strconv.Atoi(newTag.Major) now, err := strconv.Atoi(newTag.Major)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = "0" newTag.Patch = "0"
@ -177,32 +181,32 @@ likely to change.
} }
newTag.Metadata = mainAppVersion 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() nextTag = newTag.String()
} }
if _, err := tagcmp.Parse(nextTag); err != nil { if _, err := tagcmp.Parse(nextTag); err != nil {
log.Fatalf("invalid version %s specified", nextTag) logrus.Fatalf("invalid version %s specified", nextTag)
} }
mainService := "app" mainService := "app"
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag) label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if !internal.Dry { if !internal.Dry {
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil { if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} else { } 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 { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }

View File

@ -12,13 +12,14 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference" "github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "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 make a seclection. Use the "?" key to see more help on navigating this
interface. 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>", ArgsUsage: "<recipe>",
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
@ -73,15 +73,27 @@ EXAMPLE:
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c)
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
log.Fatal(err) 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) bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 { if bumpType != 0 {
// a bitwise check if the number is a power of 2 // a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 { 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 // check for versions file and load pinned versions
versionsPresent := false versionsPresent := false
versionsPath := path.Join(recipe.Dir, "versions") recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
servicePins := make(map[string]imgPin) versionsPath := path.Join(recipeDir, "versions")
var servicePins = make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil { 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) file, err := os.Open(versionsPath)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
splitLine := strings.Split(line, " ") splitLine := strings.Split(line, " ")
if splitLine[0] != "pin" || len(splitLine) != 3 { 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], ":") pinSlice := strings.Split(splitLine[2], ":")
pinTag, err := tagcmp.Parse(pinSlice[1]) pinTag, err := tagcmp.Parse(pinSlice[1])
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
pin := imgPin{ pin := imgPin{
image: pinSlice[0], image: pinSlice[0],
@ -121,50 +134,45 @@ EXAMPLE:
servicePins[splitLine[1]] = pin servicePins[splitLine[1]] = pin
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
log.Error(err) logrus.Error(err)
} }
versionsPresent = true versionsPresent = true
} else { } 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) for _, service := range recipe.Config.Services {
if err != nil {
log.Fatal(err)
}
for _, service := range config.Services {
img, err := reference.ParseNormalizedNamed(service.Image) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
regVersions, err := client.GetRegistryTags(img) regVersions, err := client.GetRegistryTags(img)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
image := reference.Path(img) 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) image = formatter.StripTagMeta(image)
switch img.(type) { switch img.(type) {
case reference.NamedTagged: case reference.NamedTagged:
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { 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: 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 continue
} }
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag()) tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil { 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 continue
} }
log.Debugf("parsed %s for %s", tag, service.Name) logrus.Debugf("parsed %s for %s", tag, service.Name)
var compatible []tagcmp.Tag var compatible []tagcmp.Tag
for _, regVersion := range regVersions { 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)) sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && !internal.AllTags { 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 continue // skip on to the next tag and don't update any compose files
} }
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline) catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
compatibleStrings := []string{"skip"} 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 var upgradeTag string
_, ok := servicePins[service.Name] _, ok := servicePins[service.Name]
@ -222,13 +230,13 @@ EXAMPLE:
} }
} }
if contains { 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 { } 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 continue
} }
} else { } 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 continue
} }
} else { } else {
@ -245,7 +253,7 @@ EXAMPLE:
} }
} }
if upgradeTag == "" { 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 continue
} }
} else { } else {
@ -253,7 +261,7 @@ EXAMPLE:
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags { if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags {
tag := img.(reference.NamedTagged).Tag() tag := img.(reference.NamedTagged).Tag()
if !internal.AllTags { 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) msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{"skip"} compatibleStrings = []string{"skip"}
@ -291,7 +299,7 @@ EXAMPLE:
Options: compatibleStrings, Options: compatibleStrings,
} }
if err := survey.AskOne(prompt, &upgradeTag); err != nil { if err := survey.AskOne(prompt, &upgradeTag); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
} }
@ -299,14 +307,14 @@ EXAMPLE:
if upgradeTag != "skip" { if upgradeTag != "skip" {
ok, err := recipe.UpdateTag(image, upgradeTag) ok, err := recipe.UpdateTag(image, upgradeTag)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if ok { 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 { } else {
if !internal.NoInput { 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 { if internal.MachineReadable {
jsonstring, err := json.Marshal(upgradeList) jsonstring, err := json.Marshal(upgradeList)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
fmt.Println(string(jsonstring)) fmt.Println(string(jsonstring))
@ -324,21 +332,21 @@ EXAMPLE:
} }
for _, upgrade := range upgradeList { 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 { 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 { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }

View File

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

View File

@ -10,9 +10,9 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context" contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/server" "coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh" sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -23,29 +23,29 @@ var localFlag = &cli.BoolFlag{
Destination: &local, Destination: &local,
} }
// cleanUp cleans up the partially created context/client details for a failed func cleanUp(domainName string) {
// "server add" attempt. if domainName != "default" {
func cleanUp(name string) { logrus.Infof("cleaning up context for %s", domainName)
if name != "default" { if err := client.DeleteContext(domainName); err != nil {
log.Debugf("serverAdd: cleanUp: cleaning up context for %s", name) logrus.Fatal(err)
if err := client.DeleteContext(name); err != nil {
log.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) files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil { 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 { 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 return
} }
if err := os.RemoveAll(serverDir); err != nil { 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 manages SSH connection details. These are stored to disk in
// ~/.docker. Abra can manage this completely for the user, so it's an // ~/.docker. Abra can manage this completely for the user, so it's an
// implementation detail. // implementation detail.
func newContext(name string) (bool, error) { func newContext(c *cli.Context, domainName, username, port string) error {
store := contextPkg.NewDefaultDockerContextStore() store := contextPkg.NewDefaultDockerContextStore()
contexts, err := store.Store.List() contexts, err := store.Store.List()
if err != nil { if err != nil {
return false, err return err
} }
for _, context := range contexts { for _, context := range contexts {
if context.Name == name { if context.Name == domainName {
log.Debugf("context for %s already exists", name) logrus.Debugf("context for %s already exists", domainName)
return false, nil 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 { if err := client.CreateContext(domainName, username, port); err != nil {
return false, nil return err
} }
return true, nil return nil
} }
// createServerDir creates the ~/.abra/servers/... directory for a new server. // createServerDir creates the ~/.abra/servers/... directory for a new server.
func createServerDir(name string) (bool, error) { func createServerDir(domainName string) error {
if err := server.CreateServerDir(name); err != nil { if err := server.CreateServerDir(domainName); err != nil {
if !os.IsExist(err) { 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 nil
return false, nil
}
return true, nil
} }
var serverAddCommand = cli.Command{ var serverAddCommand = cli.Command{
Name: "add", Name: "add",
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "Add a new server to your configuration", Usage: "Add a server to your configuration",
Description: ` Description: `
Add a new server to your configuration so that it can be managed by Abra. 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 Abra uses the SSH command-line to discover connection details for your server.
connection details. You must configure an entry per-host in your ~/.ssh/config It is advised to configure an entry per-host in your ~/.ssh/config for each
for each server. For example: server. For example:
Host example.com example Host example.com
Hostname example.com Hostname example.com
User exampleUser User exampleUser
Port 12345 Port 12345
IdentityFile ~/.ssh/example@somewhere 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 abra server add example.com
If "--local" is passed, then Abra assumes that the current local server is 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 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 Co-op Cloud config located on the server itself, and not on your local
developer machine. The domain is then set to "default". developer machine.
`,
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`,
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
internal.NoDomainChecksFlag,
localFlag, localFlag,
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
ArgsUsage: "<name>", ArgsUsage: "<domain>",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) { 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) internal.ShowSubcommandHelpAndError(c, err)
} }
var name string var domainName string
if local { if local {
name = "default" domainName = "default"
} else { } 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 { if local {
created, err := createServerDir(name) if err := createServerDir(domainName); err != nil {
if err != nil { logrus.Fatal(err)
log.Fatal(err)
} }
log.Debugf("attempting to create client for %s", name) logrus.Infof("attempting to create client for %s", domainName)
if _, err := client.New(domainName); err != nil {
if _, err := client.New(name, timeout); err != nil { cleanUp(domainName)
cleanUp(name) logrus.Fatal(err)
log.Fatal(err)
} }
if created { logrus.Info("local server added")
log.Info("local server successfully added")
} else {
log.Warn("local server already exists")
}
return nil return nil
} }
if !internal.NoDomainChecks { if _, err := dns.EnsureIPv4(domainName); err != nil {
if _, err := dns.EnsureIPv4(name); err != nil { logrus.Fatal(err)
log.Fatal(err)
}
} }
_, err := createServerDir(name) if err := createServerDir(domainName); err != nil {
logrus.Fatal(err)
}
hostConfig, err := sshPkg.GetHostConfig(domainName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
created, err := newContext(name) if err := newContext(c, domainName, hostConfig.User, hostConfig.Port); err != nil {
if err != nil { logrus.Fatal(err)
cleanUp(name)
log.Fatal(err)
} }
log.Debugf("attempting to create client for %s", name) logrus.Infof("attempting to create client for %s", domainName)
if _, err := client.New(name, timeout); err != nil { if _, err := client.New(domainName); err != nil {
cleanUp(name) cleanUp(domainName)
log.Fatal(sshPkg.Fatal(name, err)) logrus.Debugf("failed to construct client for %s, saw %s", domainName, err.Error())
logrus.Fatal(sshPkg.Fatal(domainName, err))
} }
if created { logrus.Infof("%s added", domainName)
log.Infof("%s successfully added", name)
} else {
log.Warnf("%s already exists", name)
}
return nil return nil
}, },

View File

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

View File

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

View File

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

View File

@ -8,21 +8,19 @@ import (
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
charmLog "github.com/charmbracelet/log"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
"coopcloud.tech/abra/pkg/log" "github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -54,34 +52,32 @@ var Notify = cli.Command{
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
Read the deployed app versions and look for new versions in the recipe It reads the deployed app versions and looks for new versions in the recipe
catalogue. catalogue. If a new patch/minor version is available, a notification is
printed. To include major versions use the --major flag.
If a new patch/minor version is available, a notification is printed. `,
Use "--major" to include new major versions.`,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
cl, err := client.New("default") cl, err := client.New("default")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
stacks, err := stack.GetStacks(cl) stacks, err := stack.GetStacks(cl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, stackInfo := range stacks { for _, stackInfo := range stacks {
stackName := stackInfo.Name stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe") recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if recipeName != "" { if recipeName != "" {
_, err = getLatestUpgrade(cl, stackName, recipeName) _, err = getLatestUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
} }
@ -105,21 +101,18 @@ var UpgradeApp = cli.Command{
}, },
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Description: ` Description: `
Upgrade an app by specifying stack name and recipe. 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
Use "--all" to upgrade every deployed app. 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
For each app with auto updates enabled, the deployed version is compared with include major versions use the "--major" flag. Don't do that, it will probably
the current recipe catalogue version. If a new patch/minor version is break things. Only apps that are not deployed with "--chaos" are upgraded, to
available, the app is upgraded. update chaos deployments use the "--chaos" flag. Use it with care.
`,
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.`,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
cl, err := client.New("default") cl, err := client.New("default")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !updateAll { if !updateAll {
@ -127,7 +120,7 @@ upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`
recipeName := c.Args().Get(1) recipeName := c.Args().Get(1)
err = tryUpgrade(cl, stackName, recipeName) err = tryUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil return nil
@ -135,19 +128,19 @@ upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`
stacks, err := stack.GetStacks(cl) stacks, err := stack.GetStacks(cl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, stackInfo := range stacks { for _, stackInfo := range stacks {
stackName := stackInfo.Name stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe") recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
err = tryUpgrade(cl, stackName, recipeName) err = tryUpgrade(cl, stackName, recipeName)
if err != nil { 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 return "", nil
} }
@ -193,13 +186,13 @@ func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool
return value, nil 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 return false, nil
} }
// getEnv reads env variables from docker services. // 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) envMap := make(map[string]string)
filter := filters.NewArgs() filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName)) 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 { for _, envString := range envList {
splitString := strings.SplitN(envString, "=", 2) splitString := strings.SplitN(envString, "=", 2)
if len(splitString) != 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 continue
} }
k := splitString[0] k := splitString[0]
v := splitString[1] 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 envMap[k] = v
} }
} }
@ -241,14 +234,14 @@ func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName stri
} }
if len(availableUpgrades) == 0 { if len(availableUpgrades) == 0 {
log.Debugf("no available upgrades for %s", stackName) logrus.Debugf("no available upgrades for %s", stackName)
return "", nil return "", nil
} }
var chosenUpgrade string var chosenUpgrade string
if len(availableUpgrades) > 0 { if len(availableUpgrades) > 0 {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] 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 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. // getDeployedVersion returns the currently deployed version of an app.
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) { 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 { if err != nil {
return "", err return "", err
} }
if !deployMeta.IsDeployed { if !isDeployed {
return "", fmt.Errorf("%s is not deployed?", stackName) 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 "", 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 // 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 { 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 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 return availableUpgrades, nil
} }
// processRecipeRepoVersion clones, pulls, checks out the version and lints the // processRecipeRepoVersion clones, pulls, checks out the version and lints the
// recipe repository. // recipe repository.
func processRecipeRepoVersion(r recipe.Recipe, version string) error { func processRecipeRepoVersion(recipeName, version string) error {
if err := r.EnsureExists(); err != nil { if err := recipe.EnsureExists(recipeName); err != nil {
return err return err
} }
if err := r.EnsureUpToDate(); err != nil { if err := recipe.EnsureUpToDate(recipeName); err != nil {
return err return err
} }
if _, err := r.EnsureVersion(version); err != nil { if err := recipe.EnsureVersion(recipeName, version); err != nil {
return err 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 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. // mergeAbraShEnv merges abra.sh env vars into the app env vars.
func mergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error { func mergeAbraShEnv(recipeName string, env config.AppEnv) error {
abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath) abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
return err return err
} }
for k, v := range abraShEnv { 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 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. // 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 env["STACK_NAME"] = stackName
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Namespace: stackName, Namespace: stackName,
Prune: false, Prune: false,
ResolveImage: stack.ResolveImageAlways, ResolveImage: stack.ResolveImageAlways,
Detach: false,
} }
composeFiles, err := r.GetComposeFiles(env) composeFiles, err := config.GetComposeFiles(recipeName, env)
if err != nil { if err != nil {
return nil, deployOpts, err return nil, deployOpts, err
} }
deployOpts.Composefiles = composeFiles deployOpts.Composefiles = composeFiles
compose, err := appPkg.GetAppComposeConfig(stackName, deployOpts, env) compose, err := config.GetAppComposeConfig(stackName, deployOpts, env)
if err != nil { if err != nil {
return nil, deployOpts, err 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 // after the upgrade the deployment won't be in chaos state anymore
appPkg.SetChaosLabel(compose, stackName, false) config.SetChaosLabel(compose, stackName, false)
appPkg.SetRecipeLabel(compose, stackName, r.Name) config.SetRecipeLabel(compose, stackName, recipeName)
appPkg.SetUpdateLabel(compose, stackName, env) config.SetUpdateLabel(compose, stackName, env)
return compose, deployOpts, nil 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. // tryUpgrade performs the upgrade if all the requirements are fulfilled.
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error { func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
if recipeName == "" { 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 return nil
} }
@ -403,7 +398,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
if chaos && !internal.Chaos { 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 return nil
} }
@ -413,7 +408,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
if !updatesEnabled { 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 return nil
} }
@ -423,7 +418,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
if upgradeVersion == "" { 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 return nil
} }
@ -433,35 +428,34 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
// upgrade performs all necessary steps to upgrade an app. // 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) env, err := getEnv(cl, stackName)
if err != nil { if err != nil {
return err return err
} }
app := appPkg.App{ app := config.App{
Name: stackName, Name: stackName,
Recipe: recipe.Get(recipeName), Recipe: recipeName,
Server: SERVER, Server: SERVER,
Env: env, Env: env,
} }
r := recipe.Get(recipeName) if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil {
if err = processRecipeRepoVersion(r, upgradeVersion); err != nil {
return err return err
} }
if err = mergeAbraShEnv(app.Recipe, app.Env); err != nil { if err = mergeAbraShEnv(recipeName, app.Env); err != nil {
return err return err
} }
compose, deployOpts, err := createDeployConfig(r, stackName, app.Env) compose, deployOpts, err := createDeployConfig(recipeName, stackName, app.Env)
if err != nil { if err != nil {
return err 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) 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 { app.Before = func(c *cli.Context) error {
charmLog.SetDefault(log.Logger) logrus.Debugf("kadabra version %s, commit %s", version, commit)
log.Debugf("kadabra version %s, commit %s", version, commit)
return nil return nil
} }
@ -500,6 +493,6 @@ func RunApp(version, commit string) {
app := newAbraApp(version, commit) app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil { 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 go 1.21
require ( 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 git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/log v0.4.0 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/distribution/reference v0.6.0 github.com/docker/cli v24.0.7+incompatible
github.com/docker/cli v27.0.3+incompatible github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v27.0.3+incompatible github.com/docker/docker v24.0.7+incompatible
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.12.0 github.com/go-git/go-git/v5 v5.10.0
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.5.9
github.com/moby/sys/signal v0.7.0 github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.5.0 github.com/moby/term v0.5.0
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.14.4 github.com/schollz/progressbar/v3 v3.14.1
gopkg.in/yaml.v3 v3.0.1 github.com/sirupsen/logrus v1.9.3
gotest.tools/v3 v3.5.1 gotest.tools/v3 v3.5.1
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/Microsoft/hcsshim v0.9.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect
github.com/charmbracelet/lipgloss v0.11.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.1.2 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // 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/davecgh/go-spew v1.1.1 // 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 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/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emirpasic/gods v1.18.1 // 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/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // 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-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/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // 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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/compress v1.14.2 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/pkcs11 v1.1.1 // 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/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/morikuni/aec v1.0.0 // 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/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/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.10.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.0 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect golang.org/x/crypto v0.14.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect golang.org/x/mod v0.12.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.28.0 // indirect golang.org/x/net v0.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect golang.org/x/sync v0.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 // indirect golang.org/x/term v0.14.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect golang.org/x/text v0.13.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
go.opentelemetry.io/otel/sdk/metric v1.28.0 // indirect golang.org/x/tools v0.13.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect google.golang.org/protobuf v1.30.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
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require ( 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/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1 github.com/decentral1se/passgen v1.0.1
github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // 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/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/go-retryablehttp v0.7.5
github.com/moby/patternmatcher v0.6.0 // indirect 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/moby/sys/sequential v0.5.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202193544-a5463b7f9c84 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_golang v1.16.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.2.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/cobra v1.3.0 // indirect
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.8.4
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli v1.22.15 github.com/urfave/cli v1.22.9
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
golang.org/x/sys v0.22.0 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 package app
import ( import (
"bufio"
"fmt"
"os"
"path"
"regexp"
"sort"
"strings" "strings"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "github.com/sirupsen/logrus"
"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"
) )
// Get retrieves an app // Get retrieves an app
func Get(appName string) (App, error) { func Get(appName string) (config.App, error) {
files, err := LoadAppFiles("") files, err := config.LoadAppFiles("")
if err != nil { if err != nil {
return App{}, err return config.App{}, err
} }
app, err := GetApp(files, appName) app, err := config.GetApp(files, appName)
if err != nil { 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 return app, nil
} }
// GetApp loads an apps settings, reading it from file, in preparation to use // deployedServiceSpec represents a deployed service of an app.
// it. It should only be used when ready to use the env file to keep IO type deployedServiceSpec struct {
// operations down. Name string
func GetApp(apps AppFiles, name AppName) (App, error) { Version string
appFile, exists := apps[name]
if !exists {
return App{}, fmt.Errorf("cannot find app with name %s", name)
} }
app, err := ReadAppEnvFile(appFile, name) // VersionSpec represents a deployed app and associated metadata.
if err != nil { type VersionSpec map[string]deployedServiceSpec
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.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 ( import (
"fmt" "fmt"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
// AppNameComplete copletes app names. // AppNameComplete copletes app names.
func AppNameComplete(c *cli.Context) { func AppNameComplete(c *cli.Context) {
appNames, err := app.GetAppNames() appNames, err := config.GetAppNames()
if err != nil { if err != nil {
log.Warn(err) logrus.Warn(err)
} }
if c.NArg() > 0 { if c.NArg() > 0 {
@ -26,7 +26,7 @@ func AppNameComplete(c *cli.Context) {
} }
func ServiceNameComplete(appName string) { func ServiceNameComplete(appName string) {
serviceNames, err := app.GetAppServiceNames(appName) serviceNames, err := config.GetAppServiceNames(appName)
if err != nil { if err != nil {
return return
} }
@ -39,7 +39,7 @@ func ServiceNameComplete(appName string) {
func RecipeNameComplete(c *cli.Context) { func RecipeNameComplete(c *cli.Context) {
catl, err := recipe.ReadRecipeCatalogue(false) catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
log.Warn(err) logrus.Warn(err)
} }
if c.NArg() > 0 { if c.NArg() > 0 {
@ -55,7 +55,7 @@ func RecipeNameComplete(c *cli.Context) {
func RecipeVersionComplete(recipeName string) { func RecipeVersionComplete(recipeName string) {
catl, err := recipe.ReadRecipeCatalogue(false) catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
log.Warn(err) logrus.Warn(err)
} }
for _, v := range catl[recipeName].Versions { for _, v := range catl[recipeName].Versions {
@ -67,9 +67,9 @@ func RecipeVersionComplete(recipeName string) {
// ServerNameComplete completes server names. // ServerNameComplete completes server names.
func ServerNameComplete(c *cli.Context) { func ServerNameComplete(c *cli.Context) {
files, err := app.LoadAppFiles("") files, err := config.LoadAppFiles("")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if c.NArg() > 0 { if c.NArg() > 0 {

View File

@ -8,21 +8,21 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
) )
// EnsureCatalogue ensures that the catalogue is cloned locally & present. // EnsureCatalogue ensures that the catalogue is cloned locally & present.
func EnsureCatalogue() error { func EnsureCatalogue() error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue") catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) { 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) url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.Clone(catalogueDir, url); err != nil { if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err return err
} }
log.Debugf("cloned catalogue repository to %s", catalogueDir) logrus.Debugf("cloned catalogue repository to %s", catalogueDir)
} }
return nil return nil
@ -57,7 +57,7 @@ func EnsureUpToDate() error {
if len(remotes) == 0 { if len(remotes) == 0 {
msg := "cannot ensure %s is up-to-date, no git remotes configured" 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 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 return nil
} }

View File

@ -10,32 +10,17 @@ import (
"time" "time"
contextPkg "coopcloud.tech/abra/pkg/context" contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/log"
sshPkg "coopcloud.tech/abra/pkg/ssh" sshPkg "coopcloud.tech/abra/pkg/ssh"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn" commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
"github.com/docker/docker/client" "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 // 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 // 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. // account that you may only want the local client and not communicate via SSH.
// For this use-case, please pass "default" as the contextName. // 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 var clientOpts []client.Opt
if serverName != "default" { if serverName != "default" {
@ -49,12 +34,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
return nil, err return nil, err
} }
conf := &Conf{} helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint)
for _, opt := range opts {
opt(conf)
}
helper, err := commandconnPkg.NewConnectionHelper(ctxEndpoint, conf.Timeout)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -85,7 +65,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
return nil, err return nil, err
} }
log.Debugf("created client for %s", serverName) logrus.Debugf("created client for %s", serverName)
info, err := cl.Info(context.Background()) info, err := cl.Info(context.Background())
if err != nil { if err != nil {
@ -95,10 +75,10 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
if info.Swarm.LocalNodeState == "inactive" { if info.Swarm.LocalNodeState == "inactive" {
if serverName != "default" { if serverName != "default" {
return cl, fmt.Errorf("swarm mode not enabled on %s?", serverName) 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, errors.New("swarm mode not enabled on local server?")
} }
}
return cl, nil return cl, nil
} }

View File

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

View File

@ -6,7 +6,7 @@ import (
"github.com/containers/image/docker" "github.com/containers/image/docker"
"github.com/containers/image/types" "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. // GetRegistryTags retrieves all tags of an image from a container registry.

View File

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

View File

@ -1,22 +1,51 @@
package config package config
import ( import (
"bufio"
"fmt" "fmt"
"io/fs" "io/fs"
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"sort"
"strings" "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_SANITISED_APP_NAME_LENGTH = 45
const MAX_DOCKER_SECRET_LENGTH = 64 const MAX_DOCKER_SECRET_LENGTH = 64
var BackupbotLabel = "coop-cloud.backupbot.enabled" 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. // GetServers retrieves all servers.
func GetServers() ([]string, error) { func GetServers() ([]string, error) {
var servers []string var servers []string
@ -26,11 +55,39 @@ func GetServers() ([]string, error) {
return servers, err return servers, err
} }
log.Debugf("retrieved %v servers: %s", len(servers), servers) logrus.Debugf("retrieved %v servers: %s", len(servers), servers)
return servers, nil 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. // ReadServerNames retrieves all server names.
func ReadServerNames() ([]string, error) { func ReadServerNames() ([]string, error) {
serverNames, err := GetAllFoldersInDirectory(SERVERS_DIR) serverNames, err := GetAllFoldersInDirectory(SERVERS_DIR)
@ -39,7 +96,7 @@ func ReadServerNames() ([]string, error) {
return nil, err 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 return serverNames, nil
} }
@ -63,7 +120,7 @@ func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
realPath, err := filepath.EvalSymlinks(filePath) realPath, err := filepath.EvalSymlinks(filePath)
if err != nil { 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 { } else {
realFile, err := os.Stat(realPath) realFile, err := os.Stat(realPath)
if err != nil { if err != nil {
@ -96,7 +153,7 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
filePath := path.Join(directory, file.Name()) filePath := path.Join(directory, file.Name())
realDir, err := filepath.EvalSymlinks(filePath) realDir, err := filepath.EvalSymlinks(filePath)
if err != nil { 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() { } else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
// path is a directory // path is a directory
folders = append(folders, file.Name()) folders = append(folders, file.Name())
@ -106,3 +163,119 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
return folders, nil 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 ( import (
"fmt"
"os"
"path"
"reflect" "reflect"
"slices" "slices"
"strings" "strings"
"testing" "testing"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/recipe" "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) { func TestGetAllFoldersInDirectory(t *testing.T) {
folders, err := config.GetAllFoldersInDirectory(testPkg.TestFolder) folders, err := config.GetAllFoldersInDirectory(TestFolder)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(folders, testPkg.TFolders) { if !reflect.DeepEqual(folders, TFolders) {
t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(testPkg.TFolders, ","), strings.Join(folders, ",")) t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(TFolders, ","), strings.Join(folders, ","))
} }
} }
func TestGetAllFilesInDirectory(t *testing.T) { func TestGetAllFilesInDirectory(t *testing.T) {
files, err := config.GetAllFilesInDirectory(testPkg.TestFolder) files, err := config.GetAllFilesInDirectory(TestFolder)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -32,21 +71,21 @@ func TestGetAllFilesInDirectory(t *testing.T) {
for _, file := range files { for _, file := range files {
fileNames = append(fileNames, file.Name()) fileNames = append(fileNames, file.Name())
} }
if !reflect.DeepEqual(fileNames, testPkg.TFiles) { if !reflect.DeepEqual(fileNames, TFiles) {
t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(testPkg.TFiles, ","), strings.Join(fileNames, ",")) t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(TFiles, ","), strings.Join(fileNames, ","))
} }
} }
func TestReadEnv(t *testing.T) { func TestReadEnv(t *testing.T) {
env, err := envfile.ReadEnv(testPkg.ExpectedAppFile.Path) env, err := config.ReadEnv(ExpectedAppFile.Path)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(env, testPkg.ExpectedAppEnv) { if !reflect.DeepEqual(env, ExpectedAppEnv) {
t.Fatalf( t.Fatalf(
"did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s", "did not get expected application settings. Expected: DOMAIN=%s RECIPE=%s; Got: DOMAIN=%s RECIPE=%s",
testPkg.ExpectedAppEnv["DOMAIN"], ExpectedAppEnv["DOMAIN"],
testPkg.ExpectedAppEnv["RECIPE"], ExpectedAppEnv["RECIPE"],
env["DOMAIN"], env["DOMAIN"],
env["RECIPE"], env["RECIPE"],
) )
@ -54,13 +93,14 @@ func TestReadEnv(t *testing.T) {
} }
func TestReadAbraShEnvVars(t *testing.T) { func TestReadAbraShEnvVars(t *testing.T) {
r := recipe.Get("abra-test-recipe") offline := true
err := r.EnsureExists() r, err := recipe.Get("abra-test-recipe", offline)
if err != nil { if err != nil {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -83,13 +123,14 @@ func TestReadAbraShEnvVars(t *testing.T) {
} }
func TestReadAbraShCmdNames(t *testing.T) { func TestReadAbraShCmdNames(t *testing.T) {
r := recipe.Get("abra-test-recipe") offline := true
err := r.EnsureExists() r, err := recipe.Get("abra-test-recipe", offline)
if err != nil { if err != nil {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -101,33 +142,34 @@ func TestReadAbraShCmdNames(t *testing.T) {
expectedCmdNames := []string{"test_cmd", "test_cmd_args"} expectedCmdNames := []string{"test_cmd", "test_cmd_args"}
for _, cmdName := range expectedCmdNames { for _, cmdName := range expectedCmdNames {
if !slices.Contains(cmdNames, cmdName) { 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) { func TestCheckEnv(t *testing.T) {
r := recipe.Get("abra-test-recipe") offline := true
err := r.EnsureExists() r, err := recipe.Get("abra-test-recipe", offline)
if err != nil { if err != nil {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
app := appPkg.App{ app := config.App{
Name: "test-app", Name: "test-app",
Recipe: recipe.Get(r.Name), Recipe: r.Name,
Domain: "example.com", Domain: "example.com",
Env: envSample, Env: envSample,
Path: "example.com.env", Path: "example.com.env",
Server: "example.com", Server: "example.com",
} }
envVars, err := appPkg.CheckEnv(app) envVars, err := config.CheckEnv(app)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -140,29 +182,30 @@ func TestCheckEnv(t *testing.T) {
} }
func TestCheckEnvError(t *testing.T) { func TestCheckEnvError(t *testing.T) {
r := recipe.Get("abra-test-recipe") offline := true
err := r.EnsureExists() r, err := recipe.Get("abra-test-recipe", offline)
if err != nil { if err != nil {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
delete(envSample, "DOMAIN") delete(envSample, "DOMAIN")
app := appPkg.App{ app := config.App{
Name: "test-app", Name: "test-app",
Recipe: recipe.Get(r.Name), Recipe: r.Name,
Domain: "example.com", Domain: "example.com",
Env: envSample, Env: envSample,
Path: "example.com.env", Path: "example.com.env",
Server: "example.com", Server: "example.com",
} }
envVars, err := appPkg.CheckEnv(app) envVars, err := config.CheckEnv(app)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -175,13 +218,14 @@ func TestCheckEnvError(t *testing.T) {
} }
func TestEnvVarCommentsRemoved(t *testing.T) { func TestEnvVarCommentsRemoved(t *testing.T) {
r := recipe.Get("abra-test-recipe") offline := true
err := r.EnsureExists() r, err := recipe.Get("abra-test-recipe", offline)
if err != nil { if err != nil {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -206,13 +250,14 @@ func TestEnvVarCommentsRemoved(t *testing.T) {
} }
func TestEnvVarModifiersIncluded(t *testing.T) { func TestEnvVarModifiersIncluded(t *testing.T) {
r := recipe.Get("abra-test-recipe") offline := true
err := r.EnsureExists() r, err := recipe.Get("abra-test-recipe", offline)
if err != nil { if err != nil {
t.Fatal(err) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

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

View File

@ -6,19 +6,18 @@ import (
"strings" "strings"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types" "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/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
) )
// GetContainer retrieves a container. If noInput is false and the retrievd // 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 // count of containers does not match 1, then a prompt is presented to let the
// user choose. A count of 0 is handled gracefully. // 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) { 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) containers, err := cl.ContainerList(c, containerOpts)
if err != nil { if err != nil {
return types.Container{}, err return types.Container{}, err
@ -43,7 +42,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
return types.Container{}, err 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 var response string
prompt := &survey.Select{ 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 return containers[0], nil

View File

@ -9,7 +9,7 @@ import (
func EnsureIPv4(domainName string) (string, error) { func EnsureIPv4(domainName string) (string, error) {
ipv4, err := net.ResolveIPAddr("ip4", domainName) ipv4, err := net.ResolveIPAddr("ip4", domainName)
if err != nil { 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 // 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/docker/go-units"
// "github.com/olekukonko/tablewriter" // "github.com/olekukonko/tablewriter"
"coopcloud.tech/abra/pkg/jsontable" "coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log"
"github.com/schollz/progressbar/v3" "github.com/schollz/progressbar/v3"
"github.com/sirupsen/logrus"
) )
func ShortenID(str string) string { func ShortenID(str string) string {
@ -66,7 +66,7 @@ func StripTagMeta(image string) string {
} }
if originalImage != image { 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 return image

View File

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

View File

@ -3,9 +3,9 @@ package git
import ( import (
"fmt" "fmt"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "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(), // 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 { 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 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 return branch, nil
} }

View File

@ -6,15 +6,15 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
) )
// Clone runs a git clone which accounts for different default branches. // Clone runs a git clone which accounts for different default branches.
func Clone(dir, url string) error { func Clone(dir, url string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) { 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{ _, err := git.PlainClone(dir, false, &git.CloneOptions{
URL: url, URL: url,
@ -23,7 +23,7 @@ func Clone(dir, url string) error {
SingleBranch: true, SingleBranch: true,
}) })
if err != nil { 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{ _, err := git.PlainClone(dir, false, &git.CloneOptions{
URL: url, 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 { } else {
log.Debugf("%s already exists", dir) logrus.Debugf("%s already exists", dir)
} }
return nil return nil

View File

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

View File

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"os/exec" "os/exec"
"coopcloud.tech/abra/pkg/log" "github.com/sirupsen/logrus"
) )
// getGitDiffArgs builds the `git diff` invocation args. It removes the usage // 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. // skips if it cannot find the command on the system.
func DiffUnstaged(path string) error { func DiffUnstaged(path string) error {
if _, err := exec.LookPath("git"); err != nil { 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 return nil
} }

View File

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

View File

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

View File

@ -4,15 +4,35 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/user" "os/user"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
gitConfigPkg "github.com/go-git/go-git/v5/config" 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/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 // IsClean checks if a repo has unstaged changes
func IsClean(repoPath string) (bool, error) { func IsClean(repoPath string) (bool, error) {
repo, err := git.PlainOpen(repoPath) repo, err := git.PlainOpen(repoPath)
@ -40,9 +60,9 @@ func IsClean(repoPath string) (bool, error) {
} }
if status.String() != "" { 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 { } else {
log.Debugf("discovered clean git status in %s", repoPath) logrus.Debugf("discovered clean git status in %s", repoPath)
} }
return status.IsClean(), nil return status.IsClean(), nil
@ -78,7 +98,7 @@ func parseGitConfig() (*gitConfigPkg.Config, error) {
globalGitConfig := filepath.Join(usr.HomeDir, ".gitconfig") globalGitConfig := filepath.Join(usr.HomeDir, ".gitconfig")
if _, err := os.Stat(globalGitConfig); err != nil { if _, err := os.Stat(globalGitConfig); err != nil {
if os.IsNotExist(err) { 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, nil
} }
return cfg, err return cfg, err
@ -120,7 +140,7 @@ func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
if _, err := os.Stat(excludesfile); err != nil { if _, err := os.Stat(excludesfile); err != nil {
if os.IsNotExist(err) { 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, nil
} }
return ps, err 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 return ps, nil
} }

View File

@ -3,15 +3,15 @@ package git
import ( import (
"strings" "strings"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/config"
"github.com/sirupsen/logrus"
) )
// CreateRemote creates a new git remote in a repository // CreateRemote creates a new git remote in a repository
func CreateRemote(repo *git.Repository, name, url string, dryRun bool) error { func CreateRemote(repo *git.Repository, name, url string, dryRun bool) error {
if dryRun { 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 return nil
} }

View File

@ -6,13 +6,14 @@ import (
"os" "os"
"path" "path"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "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"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus"
) )
var Warn = "warn" var Warn = "warn"
@ -45,10 +46,10 @@ func (l LintRule) Skip(recipe recipe.Recipe) bool {
if l.SkipCondition != nil { if l.SkipCondition != nil {
ok, err := l.SkipCondition(recipe) ok, err := l.SkipCondition(recipe)
if err != nil { if err != nil {
log.Debugf("%s: skip condition: %s", l.Ref, err) logrus.Debugf("%s: skip condition: %s", l.Ref, err)
} }
if ok { if ok {
log.Debugf("skipping %s based on skip condition", l.Ref) logrus.Debugf("skipping %s based on skip condition", l.Ref)
return true 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 // used in code paths such as "app deploy" to avoid nasty surprises but not for
// the typical linting commands, which do handle other levels. // the typical linting commands, which do handle other levels.
func LintForErrors(recipe recipe.Recipe) error { 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 { for level := range LintRules {
if level != "error" { 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 return nil
} }
func LintComposeVersion(recipe recipe.Recipe) (bool, error) { func LintComposeVersion(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) if recipe.Config.Version == "3.8" {
if err != nil {
return false, err
}
if config.Version == "3.8" {
return true, nil return true, nil
} }
return true, nil return true, nil
} }
func LintEnvConfigPresent(r recipe.Recipe) (bool, error) { func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) {
if _, err := os.Stat(r.SampleEnvPath); !os.IsNotExist(err) { envSample := fmt.Sprintf("%s/%s/.env.sample", config.RECIPES_DIR, recipe.Name)
if _, err := os.Stat(envSample); !os.IsNotExist(err) {
return true, nil return true, nil
} }
@ -221,11 +219,7 @@ func LintEnvConfigPresent(r recipe.Recipe) (bool, error) {
} }
func LintAppService(recipe recipe.Recipe) (bool, error) { func LintAppService(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) for _, service := range recipe.Config.Services {
if err != nil {
return false, err
}
for _, service := range config.Services {
if service.Name == "app" { if service.Name == "app" {
return true, nil 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 // 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 // the recipe. This typically means that no domain is required to deploy and
// therefore no matching traefik deploy label will be present. // therefore no matching traefik deploy label will be present.
func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) { func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) {
sampleEnv, err := r.SampleEnv() envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil { 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 { if _, ok := sampleEnv["DOMAIN"]; !ok {
@ -252,11 +247,7 @@ func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) {
} }
func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) { func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) for _, service := range recipe.Config.Services {
if err != nil {
return false, err
}
for _, service := range config.Services {
for label := range service.Deploy.Labels { for label := range service.Deploy.Labels {
if label == "traefik.enable" { if label == "traefik.enable" {
if service.Deploy.Labels[label] == "true" { if service.Deploy.Labels[label] == "true" {
@ -270,11 +261,7 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
} }
func LintHealthchecks(recipe recipe.Recipe) (bool, error) { func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) for _, service := range recipe.Config.Services {
if err != nil {
return false, err
}
for _, service := range config.Services {
if service.HealthCheck == nil { if service.HealthCheck == nil {
return false, nil return false, nil
} }
@ -284,11 +271,7 @@ func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
} }
func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) { func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) for _, service := range recipe.Config.Services {
if err != nil {
return false, err
}
for _, service := range config.Services {
img, err := reference.ParseNormalizedNamed(service.Image) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
return false, err return false, err
@ -302,11 +285,7 @@ func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
} }
func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) { func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) for _, service := range recipe.Config.Services {
if err != nil {
return false, err
}
for _, service := range config.Services {
img, err := reference.ParseNormalizedNamed(service.Image) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
return false, err return false, err
@ -329,11 +308,7 @@ func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
} }
func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) { func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) for _, service := range recipe.Config.Services {
if err != nil {
return false, err
}
for _, service := range config.Services {
img, err := reference.ParseNormalizedNamed(service.Image) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
return false, err return false, err
@ -356,11 +331,7 @@ func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
} }
func LintImagePresent(recipe recipe.Recipe) (bool, error) { func LintImagePresent(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) for _, service := range recipe.Config.Services {
if err != nil {
return false, err
}
for _, service := range config.Services {
if service.Image == "" { if service.Image == "" {
return false, nil return false, nil
} }
@ -371,12 +342,12 @@ func LintImagePresent(recipe recipe.Recipe) (bool, error) {
func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) { func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
catl, err := recipePkg.ReadRecipeCatalogue(false) catl, err := recipePkg.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl) versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if len(versions) == 0 { if len(versions) == 0 {
@ -387,7 +358,7 @@ func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
} }
func LintMetadataFilledIn(r 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 { if err != nil {
return false, err return false, err
} }
@ -408,13 +379,9 @@ func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
} }
func LintAbraShVendors(recipe recipe.Recipe) (bool, error) { func LintAbraShVendors(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) for _, service := range recipe.Config.Services {
if err != nil {
return false, err
}
for _, service := range config.Services {
if len(service.Configs) > 0 { 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 _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return false, err return false, err
@ -427,7 +394,9 @@ func LintAbraShVendors(recipe recipe.Recipe) (bool, error) {
} }
func LintHasRecipeRepo(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 { if err != nil {
return false, err return false, err
} }
@ -440,11 +409,7 @@ func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) {
} }
func LintSecretLengths(recipe recipe.Recipe) (bool, error) { func LintSecretLengths(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) for name := range recipe.Config.Secrets {
if err != nil {
return false, err
}
for name := range config.Secrets {
if len(name) > 12 { if len(name) > 12 {
return false, fmt.Errorf("secret %s is longer than 12 characters", name) 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) { 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 { 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() iter, err := repo.Tags()
if err != nil { 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 { 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" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"path" "path"
"path/filepath"
"slices" "slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/compose"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/limit" "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/abra/pkg/web"
"coopcloud.tech/tagcmp" "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. // RecipeCatalogueURL is the only current recipe catalogue available.
@ -73,7 +80,7 @@ func (r RecipeMeta) LatestVersion() string {
version = tag 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 return version
} }
@ -123,62 +130,307 @@ type Features struct {
SSO string `json:"sso"` SSO string `json:"sso"`
} }
func Get(name string) Recipe { // Recipe represents a 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"),
}
}
type Recipe struct { type Recipe struct {
Name string Name string
Version string Config *composetypes.Config
Dir string Meta RecipeMeta
GitURL string
SSHURL string
ComposePath string
ReadmePath string
SampleEnvPath string
AbraShPath string
} }
func escapeRecipeName(recipeName string) string { // Push pushes the latest changes to a SSH URL remote. You need to have your
recipeName = strings.ReplaceAll(recipeName, "/", "_") // local SSH configuration for git.coopcloud.tech working for this to work
recipeName = strings.ReplaceAll(recipeName, ".", "_") func (r Recipe) Push(dryRun bool) error {
return recipeName 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 // GetRecipesLocal retrieves all local recipe directories
@ -193,20 +445,41 @@ func GetRecipesLocal() ([]string, error) {
return recipes, nil 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{} feat := Features{}
var category string 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 { if err != nil {
return feat, category, err return feat, category, err
} }
readmeMetadata, err := GetStringInBetween( // Find text between delimiters readmeMetadata, err := GetStringInBetween( // Find text between delimiters
r.Name, recipeName,
string(readmeFS), string(readmeFS),
"<!-- metadata -->", "<!-- endmetadata -->", "<!-- metadata -->", "<!-- endmetadata -->",
) )
@ -257,7 +530,7 @@ func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, error) {
if strings.Contains(val, "**Image**") { if strings.Contains(val, "**Image**") {
imageMetadata, err := GetImageMetadata(strings.TrimSpace( imageMetadata, err := GetImageMetadata(strings.TrimSpace(
strings.TrimPrefix(val, "* **Image**:"), strings.TrimPrefix(val, "* **Image**:"),
), r.Name) ), recipeName)
if err != nil { if err != nil {
continue continue
} }
@ -279,9 +552,9 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, error) {
if len(imgFields) < 3 { if len(imgFields) < 3 {
if imageRowString != "" { 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 { } else {
log.Warnf("%s image meta is empty?", recipeName) logrus.Warnf("%s image meta is empty?", recipeName)
} }
return img, nil return img, nil
} }
@ -293,13 +566,13 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, error) {
imageName, err := GetStringInBetween(recipeName, imgString, "[", "]") imageName, err := GetStringInBetween(recipeName, imgString, "[", "]")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
img.Image = strings.ReplaceAll(imageName, "`", "") img.Image = strings.ReplaceAll(imageName, "`", "")
imageURL, err := GetStringInBetween(recipeName, imgString, "(", ")") imageURL, err := GetStringInBetween(recipeName, imgString, "(", ")")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
img.URL = imageURL img.URL = imageURL
@ -323,6 +596,59 @@ func GetStringInBetween(recipeName, str, start, end string) (result string, err
return str[s : s+e], nil 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. // ReadRecipeCatalogue reads the recipe catalogue.
func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) { func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) {
recipes := make(RecipeCatalogue) recipes := make(RecipeCatalogue)
@ -355,7 +681,7 @@ func readRecipeCatalogueFS(target interface{}) error {
return err 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 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 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 return recipeMeta, nil
} }
@ -504,7 +830,7 @@ func ReadReposMetadata() (RepoCatalogue, error) {
pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx) 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 { if err := web.ReadJSON(pagedURL, &reposList); err != nil {
return reposMeta, err return reposMeta, err
@ -537,6 +863,95 @@ func ReadReposMetadata() (RepoCatalogue, error) {
return reposMeta, nil 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 // sortRecipeVersions sorts the recipe semver versions
func sortRecipeVersions(versions RecipeVersions) { func sortRecipeVersions(versions RecipeVersions) {
sort.Slice(versions, func(i, j int) bool { sort.Slice(versions, func(i, j int) bool {
@ -617,8 +1032,9 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
return return
} }
if err := gitPkg.Clone(Get(rm.Name).Dir, rm.CloneURL); err != nil { recipeDir := path.Join(config.RECIPES_DIR, rm.Name)
log.Fatal(err) if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil {
logrus.Fatal(err)
} }
ch <- rm.Name ch <- rm.Name
@ -637,11 +1053,3 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
func getReposTopicUrl(repoName string) string { func getReposTopicUrl(repoName string) string {
return fmt.Sprintf("https://git.coopcloud.tech/api/v1/repos/coop-cloud/%s/topics", repoName) 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 package recipe
import ( import (
"path"
"testing" "testing"
"coopcloud.tech/abra/pkg/config"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert" "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) { func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
r := Get("traefik") offline := true
if err := r.EnsureExists(); err != nil { recipe, err := Get("traefik", offline)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for i := 1; i < 50; i++ { for i := 1; i < 1000; i++ {
label, err := r.GetVersionLabelLocal() label, err := GetVersionLabelLocal(recipe)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

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

View File

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

View File

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

View File

@ -6,12 +6,12 @@ import (
"strings" "strings"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
) )
// GetService retrieves a service container based on a label. If prompt is true // 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 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 var response string
prompt := &survey.Select{ 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 return matchingServices[0], nil
@ -106,7 +106,7 @@ func GetService(c context.Context, cl *client.Client, filters filters.Args, prom
return swarm.Service{}, err 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 var response string
prompt := &survey.Select{ 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 return services[0], nil

View File

@ -2,14 +2,73 @@ package ssh
import ( import (
"fmt" "fmt"
"os/exec"
"strings" "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 // Fatal is a error output wrapper which aims to make SSH failures easier to
// parse through re-wording. // parse through re-wording.
func Fatal(hostname string, err error) error { func Fatal(hostname string, err error) error {
out := err.Error() out := err.Error()
if strings.Contains(out, "Host key verification failed.") { if strings.Contains(out, "Host key verification failed.") {
return fmt.Errorf("SSH host key verification failed for %s", hostname) return fmt.Errorf("SSH host key verification failed for %s", hostname)
} else if strings.Contains(out, "Could not resolve 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") { } else if strings.Contains(out, "Permission denied") {
return fmt.Errorf("ssh auth: permission denied for %s", hostname) return fmt.Errorf("ssh auth: permission denied for %s", hostname)
} else if strings.Contains(out, "Network is unreachable") { } 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 return err
} }
}

View File

@ -1,64 +1,22 @@
package test package test
import ( import (
"log"
"os" "os"
"path"
appPkg "coopcloud.tech/abra/pkg/app" "github.com/sirupsen/logrus"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
) )
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. // RmServerAppRecipe deletes the test server / app / recipe.
func RmServerAppRecipe() { func RmServerAppRecipe() {
testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com") testAppLink := os.ExpandEnv("$HOME/.abra/servers/foo.com")
if err := os.Remove(testAppLink); err != nil { if err := os.Remove(testAppLink); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test") testRecipeLink := os.ExpandEnv("$HOME/.abra/recipes/test")
if err := os.Remove(testRecipeLink); err != nil { 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