Compare commits

..

2 Commits

Author SHA1 Message Date
5a4dac7e76 Merge remote-tracking branch 'upstream/main' into deploy-relax-force
Some checks failed
continuous-integration/drone/pr Build is failing
2024-12-30 16:44:29 +01:00
628a9a4b3f feat: Relaxes --force enforcement on deploy an instead error in some cases
Some checks failed
continuous-integration/drone/pr Build is failing
2024-12-30 16:39:20 +01:00
337 changed files with 7795 additions and 187763 deletions

View File

@ -10,7 +10,7 @@ steps:
- name: make test
image: golang:1.22
environment:
CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git
CATL_URL: https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git
commands:
- mkdir -p $HOME/.abra
- git clone $CATL_URL $HOME/.abra/catalogue
@ -29,7 +29,7 @@ steps:
event: tag
- name: release
image: goreleaser/goreleaser:v2.5.1
image: goreleaser/goreleaser:v1.24.0
environment:
GITEA_TOKEN:
from_secret: goreleaser_gitea_token
@ -47,10 +47,10 @@ steps:
image: plugins/docker
settings:
auto_tag: true
username: abra-bot
username: 3wordchant
password:
from_secret: git_coopcloud_tech_token_abra_bot
repo: git.coopcloud.tech/toolshed/abra
from_secret: git_coopcloud_tech_token_3wc
repo: git.coopcloud.tech/coop-cloud/abra
tags: dev
registry: git.coopcloud.tech
when:
@ -74,7 +74,7 @@ steps:
request_pty: true
script:
- |
wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
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:

8
.gitea/ISSUE_TEMPLATE.md Normal file
View File

@ -0,0 +1,8 @@
---
name: "Do not use this issue tracker"
about: "Do not use this issue tracker"
title: "Do not use this issue tracker"
labels: []
---
Please report your issue on [`coop-cloud/organising`](https://git.coopcloud.tech/coop-cloud/organising)

View File

@ -4,7 +4,6 @@
> please do add yourself! This is a community project, let's show some 💞
- 3wordchant
- ammaratef45
- cassowary
- codegod100
- decentral1se
@ -18,5 +17,3 @@
- roxxers
- vera
- yksflip
- basebuilder
- mayel

View File

@ -1,7 +1,7 @@
# Build image
FROM golang:1.22-alpine AS build
ENV GOPRIVATE=coopcloud.tech
ENV GOPRIVATE coopcloud.tech
RUN apk add --no-cache \
gcc \

View File

@ -1,7 +1,7 @@
# `abra`
[![Build Status](https://build.coopcloud.tech/api/badges/toolshed/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/toolshed/abra)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/toolshed/abra)](https://goreportcard.com/report/git.coopcloud.tech/toolshed/abra)
[![Build Status](https://build.coopcloud.tech/api/badges/coop-cloud/abra/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/coop-cloud/abra)
[![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/coop-cloud/abra)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
[![Go Reference](https://pkg.go.dev/badge/coopcloud.tech/abra.svg)](https://pkg.go.dev/coopcloud.tech/abra)
The Co-op Cloud utility belt 🎩🐇

View File

@ -11,7 +11,7 @@ import (
)
var AppBackupListCommand = &cobra.Command{
Use: "list <domain> [flags]",
Use: "list <app> [flags]",
Aliases: []string{"ls"},
Short: "List the contents of a snapshot",
Args: cobra.ExactArgs(1),
@ -61,7 +61,7 @@ var AppBackupListCommand = &cobra.Command{
}
var AppBackupDownloadCommand = &cobra.Command{
Use: "download <domain> [flags]",
Use: "download <app> [flags]",
Aliases: []string{"d"},
Short: "Download a snapshot",
Long: `Downloads a backup.tar.gz to the current working directory.
@ -78,7 +78,7 @@ var AppBackupDownloadCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -130,7 +130,7 @@ var AppBackupDownloadCommand = &cobra.Command{
}
var AppBackupCreateCommand = &cobra.Command{
Use: "create <domain> [flags]",
Use: "create <app> [flags]",
Aliases: []string{"c"},
Short: "Create a new snapshot",
Args: cobra.ExactArgs(1),
@ -143,7 +143,7 @@ var AppBackupCreateCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -174,7 +174,7 @@ var AppBackupCreateCommand = &cobra.Command{
}
var AppBackupSnapshotsCommand = &cobra.Command{
Use: "snapshots <domain> [flags]",
Use: "snapshots <app> [flags]",
Aliases: []string{"s"},
Short: "List all snapshots",
Args: cobra.ExactArgs(1),

View File

@ -13,7 +13,7 @@ import (
)
var AppCheckCommand = &cobra.Command{
Use: "check <domain> [flags]",
Use: "check <app> [flags]",
Aliases: []string{"chk"},
Short: "Ensure an app is well configured",
Long: `Compare env vars in both the app ".env" and recipe ".env.sample" file.
@ -36,7 +36,7 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}

View File

@ -18,7 +18,7 @@ import (
)
var AppCmdCommand = &cobra.Command{
Use: "command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
Use: "command <app> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
Aliases: []string{"cmd"},
Short: "Run app commands",
Long: `Run an app specific command.
@ -92,7 +92,7 @@ does not).`,
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -192,14 +192,14 @@ does not).`,
}
var AppCmdListCommand = &cobra.Command{
Use: "list <domain> [flags]",
Use: "list <app> [flags]",
Aliases: []string{"ls"},
Short: "List all available commands",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}

View File

@ -13,7 +13,7 @@ func TestParseCmdArgs(t *testing.T) {
}{
// `--` is not parsed when passed in from the command-line e.g. -- foo bar baz
// so we need to eumlate that as missing when testing if bash args are passed in
// see https://git.coopcloud.tech/toolshed/organising/issues/336 for more
// see https://git.coopcloud.tech/coop-cloud/organising/issues/336 for more
{[]string{"foo.com", "app", "test"}, false, ""},
{[]string{"foo.com", "app", "test", "foo"}, true, "foo "},
{[]string{"foo.com", "app", "test", "foo", "bar", "baz"}, true, "foo bar baz "},

View File

@ -12,7 +12,7 @@ import (
)
var AppConfigCommand = &cobra.Command{
Use: "config <domain> [flags]",
Use: "config <app> [flags]",
Aliases: []string{"cfg"},
Short: "Edit app config",
Example: " abra config 1312.net",

View File

@ -26,7 +26,7 @@ import (
)
var AppCpCommand = &cobra.Command{
Use: "cp <domain> <src> <dst> [flags]",
Use: "cp <app> <src> <dst> [flags]",
Aliases: []string{"c"},
Short: "Copy files to/from a deployed app service",
Example: ` # copy myfile.txt to the root of the app service
@ -49,7 +49,7 @@ var AppCpCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}

View File

@ -2,14 +2,15 @@ package app
import (
"context"
"errors"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/secret"
"coopcloud.tech/tagcmp"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client"
@ -18,19 +19,18 @@ import (
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
var AppDeployCommand = &cobra.Command{
Use: "deploy <domain> [version] [flags]",
Use: "deploy <app> [version] [flags]",
Aliases: []string{"d"},
Short: "Deploy an app",
Long: `Deploy an app.
This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported as values for
"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`,
This command supports chaos operations. Use "--chaos/-c" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported values for "[version]".
Please note, "upgrade"/"rollback" do not support chaos operations.`,
Example: ` # standard deployment
abra app deploy 1312.net
@ -46,7 +46,8 @@ checkout as-is. Recipe commit hashes are also supported as values for
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
@ -62,20 +63,29 @@ checkout as-is. Recipe commit hashes are also supported as values for
}
},
Run: func(cmd *cobra.Command, args []string) {
var (
deployWarnMessages []string
toDeployVersion string
isChaosCommit bool
toDeployChaosVersion = config.CHAOS_DEFAULT
)
var warnMessages []string
app := internal.ValidateApp(args)
stackName := app.StackName()
if err := validateArgsAndFlags(args); err != nil {
log.Fatal(err)
ok, err := validateChaosXORVersion(args)
if !ok {
log.Fatalf(err.Error())
}
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
specificVersion := getSpecifiedVersion(args)
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
}
if specificVersion == "" && app.Recipe.Version != "" && !internal.Chaos {
log.Debugf("retrieved %s as version from env file", app.Recipe.Version)
specificVersion = app.Recipe.Version
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -83,66 +93,117 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", app.StackName())
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
}
if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
log.Fatalf("%s is already deployed", app.Name)
}
// NOTE(d1): handles "<version> as git hash" use case
var isChaosCommit bool
if len(args) == 2 && args[1] != "" {
toDeployVersion = args[1]
}
// 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
toDeployVersion := deployMeta.Version
if specificVersion != "" {
toDeployVersion = specificVersion
log.Debugf("choosing %s as version to deploy", toDeployVersion)
if !deployMeta.IsDeployed &&
toDeployVersion == "" &&
app.Recipe.EnvVersion != "" && !internal.IgnoreEnvVersion {
log.Debugf("new deployment, choosing .env version: %s", app.Recipe.EnvVersion)
toDeployVersion = app.Recipe.EnvVersion
}
if !internal.Chaos && toDeployVersion == "" {
if err := getLatestVersionOrCommit(app, &toDeployVersion); err != nil {
log.Fatal(err)
}
}
if internal.Chaos {
if err := getChaosVersion(app, &toDeployVersion, &toDeployChaosVersion); err != nil {
log.Fatal(err)
}
}
if !internal.Chaos {
var err error
isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil {
log.Fatal(err)
}
if isChaosCommit {
log.Debugf("assuming chaos commit: %s", toDeployVersion)
log.Debugf("assuming '%s' is a chaos commit", toDeployVersion)
internal.Chaos = true
toDeployChaosVersion = toDeployVersion
}
}
toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
log.Fatal(err)
}
for _, secStat := range secStats {
if !secStat.CreatedOnRemote {
log.Fatalf("unable to deploy, secrets not generated (%s)?", secStat.LocalName)
}
}
if !internal.Chaos && specificVersion == "" {
versions, err := app.Recipe.Tags()
if err != nil {
log.Fatal(err)
}
if len(versions) > 0 && !internal.Chaos {
toDeployVersion = versions[len(versions)-1]
log.Debugf("choosing %s as version to deploy", toDeployVersion)
if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil {
log.Fatal(err)
}
} else {
head, err := app.Recipe.Head()
if err != nil {
log.Fatal(err)
}
toDeployVersion = formatter.SmallSHA(head.String())
}
}
toDeployChaosVersion := config.CHAOS_DEFAULT
if internal.Chaos {
if isChaosCommit {
toDeployChaosVersion = specificVersion
versionLabelLocal, err := app.Recipe.GetVersionLabelLocal()
if err != nil {
log.Fatal(err)
}
toDeployVersion = versionLabelLocal
} else {
var err error
toDeployChaosVersion, err = app.Recipe.ChaosVersion()
if err != nil {
log.Fatal(err)
}
}
}
if err := validateSecrets(cl, app); err != nil {
log.Fatal(err)
if deployMeta.IsDeployed {
if !internal.Force {
appStatus, err := app.Status()
if err != nil {
log.Fatal(err)
}
if appStatus.Chaos && !internal.Chaos {
log.Fatalf("%s is deployed from a chaos version. Are you sure the local changes are in the current version (%s)?", app.Name, toDeployVersion)
}
if toDeployVersion != "" && appStatus.Version != "" {
localVersion, err := tagcmp.Parse(toDeployVersion)
if err != nil {
log.Fatal(err)
}
remoteVersion, err := tagcmp.Parse(appStatus.Version)
if err != nil {
log.Fatal(err)
}
if localVersion.IsLessThan(remoteVersion) {
log.Fatalf("%s is deployed at %s. Are you sure you want to downgrade to %s?", app.Name, appStatus.Version, toDeployVersion)
}
}
}
}
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
@ -158,7 +219,6 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err)
}
stackName := app.StackName()
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
@ -171,17 +231,10 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err)
}
toDeployChaosVersionLabel := toDeployChaosVersion
if app.Recipe.Dirty {
toDeployChaosVersionLabel = formatter.AddDirtyMarker(toDeployChaosVersionLabel)
}
appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersionLabel)
}
appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion)
appPkg.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app)
@ -191,22 +244,23 @@ checkout as-is. Recipe commit hashes are also supported as values for
for _, envVar := range envVars {
if !envVar.Present {
deployWarnMessages = append(deployWarnMessages,
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
warnMessages = append(warnMessages,
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain),
)
}
}
if !internal.NoDomainChecks {
if domainName, ok := app.Env["DOMAIN"]; ok {
domainName, ok := app.Env["DOMAIN"]
if ok {
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
log.Fatal(err)
}
} else {
log.Debug("skipping domain checks, no DOMAIN=... configured")
log.Debug("skipping domain checks as no DOMAIN=... configured for app")
}
} else {
log.Debug("skipping domain checks")
log.Debug("skipping domain checks as requested")
}
deployedVersion := config.NO_VERSION_DEFAULT
@ -214,20 +268,13 @@ checkout as-is. Recipe commit hashes are also supported as values for
deployedVersion = deployMeta.Version
}
toWriteVersion := toDeployVersion
if internal.Chaos || isChaosCommit {
toWriteVersion = toDeployChaosVersion
}
if err := internal.DeployOverview(
app,
deployWarnMessages,
warnMessages,
deployedVersion,
deployMeta.ChaosVersion,
toDeployVersion,
toDeployChaosVersion,
toWriteVersion,
); err != nil {
toDeployChaosVersion); err != nil {
log.Fatal(err)
}
@ -235,8 +282,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
if err != nil {
log.Fatal(err)
}
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil {
log.Fatal(err)
@ -250,77 +296,31 @@ checkout as-is. Recipe commit hashes are also supported as values for
}
}
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
app.Recipe.Version = toDeployVersion
if toDeployChaosVersion != config.CHAOS_DEFAULT {
app.Recipe.Version = toDeployChaosVersion
}
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
},
}
func getChaosVersion(app app.App, toDeployVersion, toDeployChaosVersion *string) error {
var err error
*toDeployChaosVersion, err = app.Recipe.ChaosVersion()
if err != nil {
return err
// validateChaosXORVersion xor checks version/chaos mode
func validateChaosXORVersion(args []string) (bool, error) {
if getSpecifiedVersion(args) != "" && internal.Chaos {
return false, errors.New("cannot use <version> and --chaos together")
}
*toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
if err != nil {
return err
}
return nil
return true, nil
}
func getLatestVersionOrCommit(app app.App, toDeployVersion *string) error {
versions, err := app.Recipe.Tags()
if err != nil {
return err
// getSpecifiedVersion retrieves the specific version if available
func getSpecifiedVersion(args []string) string {
if len(args) >= 2 {
return args[1]
}
if len(versions) > 0 && !internal.Chaos {
*toDeployVersion = versions[len(versions)-1]
log.Debugf("choosing %s as version to deploy", *toDeployVersion)
if _, err := app.Recipe.EnsureVersion(*toDeployVersion); err != nil {
return err
}
return nil
}
head, err := app.Recipe.Head()
if err != nil {
return err
}
*toDeployVersion = formatter.SmallSHA(head.String())
return nil
}
// validateArgsAndFlags ensures compatible args/flags.
func validateArgsAndFlags(args []string) error {
if len(args) == 2 && args[1] != "" && internal.Chaos {
return fmt.Errorf("cannot use [version] and --chaos together")
}
return nil
}
func validateSecrets(cl *dockerClient.Client, app app.App) error {
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
return err
}
for _, secStat := range secStats {
if !secStat.CreatedOnRemote {
return fmt.Errorf("secret not generated: %s", secStat.LocalName)
}
}
return nil
return ""
}
func init() {
@ -353,6 +353,6 @@ func init() {
"no-converge-checks",
"c",
false,
"disable converge logic checks",
"do not wait for converge logic checks",
)
}

57
cli/app/deploy_test.go Normal file
View File

@ -0,0 +1,57 @@
package app
import (
"testing"
"coopcloud.tech/abra/cli/internal"
)
func TestGetSpecificVersion(t *testing.T) {
tests := []struct {
input []string
expectedOutput string
}{
// No specified version when command has one or less args
{[]string{}, ""},
{[]string{"arg0"}, ""},
// Second in arg (index-1) is the specified result when command has more than 1 args
{[]string{"arg0", "arg1"}, "arg1"},
{[]string{"arg0", "arg1", "arg2"}, "arg1"},
}
for _, test := range tests {
if test.expectedOutput != getSpecifiedVersion(test.input) {
t.Fatalf("result for %s should be %s", test.input, test.expectedOutput)
}
}
}
func TestValidateChaosXORVersion(t *testing.T) {
tests := []struct {
input []string
isChaos bool
expectedResult bool
}{
// Chaos = true, Specified Version absent
{[]string{}, true, true},
// Chaos = false, Specified Version absent
{[]string{}, false, true},
// Chaos = true, Specified Version present
{[]string{"arg0", "arg1"}, true, false},
// Chaos = false, Specified Version present
{[]string{"arg0", "arg1", "arg2"}, false, true},
}
for _, test := range tests {
internal.Chaos = test.isChaos
res, _ := validateChaosXORVersion(test.input)
if res != test.expectedResult {
t.Fatalf(
"When args are %s and Chaos mode is %t result needs to be %t",
test.input,
test.isChaos,
test.expectedResult,
)
}
}
}

View File

@ -1,43 +0,0 @@
package app
import (
"fmt"
"sort"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"github.com/spf13/cobra"
)
var AppEnvCommand = &cobra.Command{
Use: "env <domain> [flags]",
Aliases: []string{"e"},
Short: "Show app .env values",
Example: " abra app env 1312.net",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
var envKeys []string
for k := range app.Env {
envKeys = append(envKeys, k)
}
sort.Strings(envKeys)
var rows [][]string
for _, k := range envKeys {
rows = append(rows, []string{k, app.Env[k]})
}
overview := formatter.CreateOverview("ENV OVERVIEW", rows)
fmt.Println(overview)
},
}

View File

@ -1,139 +0,0 @@
package app
import (
"context"
"fmt"
"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/log"
"coopcloud.tech/abra/pkg/upstream/convert"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
var AppLabelsCommand = &cobra.Command{
Use: "labels <domain> [flags]",
Aliases: []string{"lb"},
Short: "Show deployment labels",
Long: "Both local recipe and live deployment labels are shown.",
Example: " abra app labels 1312.net",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
remoteLabels, err := getLabels(cl, app.StackName())
if err != nil {
log.Fatal(err)
}
rows := [][]string{
{"DEPLOYED LABELS", "---"},
}
remoteLabelKeys := make([]string, 0, len(remoteLabels))
for k := range remoteLabels {
remoteLabelKeys = append(remoteLabelKeys, k)
}
sort.Strings(remoteLabelKeys)
for _, k := range remoteLabelKeys {
rows = append(rows, []string{
k,
remoteLabels[k],
})
}
if len(remoteLabelKeys) == 0 {
rows = append(rows, []string{"unknown"})
}
rows = append(rows, []string{"RECIPE LABELS", "---"})
config, err := app.Recipe.GetComposeConfig(app.Env)
if err != nil {
log.Fatal(err)
}
var localLabelKeys []string
var appServiceConfig composetypes.ServiceConfig
for _, service := range config.Services {
if service.Name == "app" {
appServiceConfig = service
for k := range service.Deploy.Labels {
localLabelKeys = append(localLabelKeys, k)
}
}
}
sort.Strings(localLabelKeys)
for _, k := range localLabelKeys {
rows = append(rows, []string{
k,
appServiceConfig.Deploy.Labels[k],
})
}
overview := formatter.CreateOverview("LABELS OVERVIEW", rows)
fmt.Println(overview)
},
}
// getLabels reads docker labels from running services in the format of "coop-cloud.${STACK_NAME}.${LABEL}".
func getLabels(cl *dockerClient.Client, stackName string) (map[string]string, error) {
labels := make(map[string]string)
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
if err != nil {
return labels, err
}
for _, service := range services {
if service.Spec.Name != fmt.Sprintf("%s_app", stackName) {
continue
}
for k, v := range service.Spec.Labels {
labels[k] = v
}
}
return labels, nil
}
func init() {
AppLabelsCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -24,7 +24,7 @@ import (
)
var AppLogsCommand = &cobra.Command{
Use: "logs <domain> [service] [flags]",
Use: "logs <app> [service] [flags]",
Aliases: []string{"l"},
Short: "Tail app logs",
Args: cobra.RangeArgs(1, 2),

View File

@ -25,7 +25,7 @@ This new app configuration is stored in your $ABRA_DIR directory under the
appropriate server.
This command does not deploy your app for you. You will need to run "abra app
deploy <domain>" to do so.
deploy <app>" to do so.
You can see what recipes are available (i.e. values for the [recipe] argument)
by running "abra recipe ls".
@ -75,34 +75,9 @@ var AppNewCommand = &cobra.Command{
chaosVersion := config.CHAOS_DEFAULT
if internal.Chaos {
var err error
chaosVersion, err = recipe.ChaosVersion()
if err != nil {
log.Fatal(err)
}
recipeVersion = chaosVersion
// NOTE(d1): rely on tags as there is no recipe.EnvVersion yet because
// the app has not been fully created. we rely on the local git state of
// the repository
tags, err := recipe.Tags()
if err != nil {
log.Fatal(err)
}
internal.SortVersionsDesc(tags)
if len(tags) == 0 {
// NOTE(d1): this is a new recipe with no released versions
recipeVersion = config.UNKNOWN_DEFAULT
} else {
recipeVersion = tags[len(tags)-1]
}
if err := recipe.IsDirty(); err != nil {
log.Fatal(err)
}
if !internal.Offline && !recipe.Dirty {
if !internal.Offline {
if err := recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
@ -113,29 +88,29 @@ var AppNewCommand = &cobra.Command{
if err := recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
}
var recipeVersions recipePkg.RecipeVersions
if recipeVersion == "" {
var err error
recipeVersions, _, err = recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
var recipeVersions recipePkg.RecipeVersions
if recipeVersion == "" {
var err error
recipeVersions, err = recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
}
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
recipeVersion = tag
}
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
recipeVersion = tag
}
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
} else {
if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
} else {
if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
@ -226,8 +201,8 @@ var AppNewCommand = &cobra.Command{
log.Warnf(
"secrets are %s shown again, please save them %s",
formatter.BoldUnderlineStyle.Render("NOT"),
formatter.BoldUnderlineStyle.Render("NOW"),
formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"),
)
}
@ -236,17 +211,9 @@ var AppNewCommand = &cobra.Command{
log.Fatal(err)
}
if err := app.Recipe.IsDirty(); err != nil {
log.Fatal(err)
}
toWriteVersion := recipeVersion
if internal.Chaos || app.Recipe.Dirty {
toWriteVersion = chaosVersion
}
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
log.Debugf("choosing %s as version to save to env file", recipeVersion)
if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
},
}

View File

@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
@ -23,9 +22,9 @@ import (
)
var AppPsCommand = &cobra.Command{
Use: "ps <domain> [flags]",
Use: "ps <app> [flags]",
Aliases: []string{"p"},
Short: "Check app deployment status",
Short: "Check app status",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -36,7 +35,7 @@ var AppPsCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -58,11 +57,9 @@ var AppPsCommand = &cobra.Command{
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok {
if isChaos, exists := statusMeta["chaos"]; exists && isChaos == "true" {
if cVersion, exists := statusMeta["chaosVersion"]; exists {
chaosVersion = cVersion
if strings.HasSuffix(chaosVersion, config.DIRTY_DEFAULT) {
chaosVersion = formatter.BoldDirtyDefault(chaosVersion)
}
chaosVersion, err = app.Recipe.ChaosVersion()
if err != nil {
log.Fatal(err)
}
}
}

View File

@ -16,7 +16,7 @@ import (
)
var AppRemoveCommand = &cobra.Command{
Use: "remove <domain> [flags]",
Use: "remove <app> [flags]",
Aliases: []string{"rm"},
Short: "Remove all app data, locally and remotely",
Long: `Remove everything related to an app which is already undeployed.

View File

@ -15,12 +15,12 @@ import (
)
var AppRestartCommand = &cobra.Command{
Use: "restart <domain> [[service] | --all-services] [flags]",
Use: "restart <app> [[service] | --all-services] [flags]",
Aliases: []string{"re"},
Short: "Restart an app",
Long: `This command restarts services within a deployed app.
Run "abra app ps <domain>" to see a list of service names.
Run "abra app ps <app>" to see a list of service names.
Pass "--all-services/-a" to restart all services.`,
Example: ` # restart a single app service
@ -48,7 +48,7 @@ Pass "--all-services/-a" to restart all services.`,
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(false, false); err != nil {
log.Fatal(err)
}

View File

@ -12,7 +12,7 @@ import (
)
var AppRestoreCommand = &cobra.Command{
Use: "restore <domain> [flags]",
Use: "restore <app> [flags]",
Aliases: []string{"rs"},
Short: "Restore a snapshot",
Long: `Snapshots are restored while apps are deployed.
@ -28,7 +28,7 @@ Some restore scenarios may require service / app restarts.`,
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}

View File

@ -1,14 +1,13 @@
package app
import (
"context"
"fmt"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
@ -21,23 +20,16 @@ import (
)
var AppRollbackCommand = &cobra.Command{
Use: "rollback <domain> [version] [flags]",
Use: "rollback <app> [version] [flags]",
Aliases: []string{"rl"},
Short: "Roll an app back to a previous version",
Long: `This command rolls an app back to a previous version.
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]".
Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[<version>]".
It is possible to "--force/-f" an downgrade if you want to re-deploy a specific
version.
Only the deployed version is consulted when trying to determine what downgrades
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.
A downgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`,
A rollback can be destructive, please ensure you have a copy of your app data
beforehand.`,
Example: ` # standard rollback
abra app rollback 1312.net
@ -63,15 +55,26 @@ beforehand. See "abra app backup" for more.`,
}
},
Run: func(cmd *cobra.Command, args []string) {
var (
downgradeWarnMessages []string
chosenDowngrade string
availableDowngrades []string
)
var warnMessages []string
app := internal.ValidateApp(args)
stackName := app.StackName()
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
var specificVersion string
if len(args) == 2 {
specificVersion = args[1]
}
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err)
}
@ -80,13 +83,15 @@ beforehand. See "abra app backup" for more.`,
log.Fatal(err)
}
deployMeta, err := ensureDeployed(cl, app)
log.Debugf("checking whether %s is already deployed", stackName)
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
}
if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err)
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
}
versions, err := app.Recipe.Tags()
@ -94,56 +99,84 @@ beforehand. See "abra app backup" for more.`,
log.Fatal(err)
}
// NOTE(d1): we've no idea what the live deployment version is, so every
// possible downgrade can be shown. it's up to the user to make the choice
if deployMeta.Version == config.UNKNOWN_DEFAULT {
var availableDowngrades []string
if deployMeta.Version == "unknown" {
availableDowngrades = versions
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
}
if len(args) == 2 && args[1] != "" {
chosenDowngrade = args[1]
if err := validateDowngradeVersionArg(chosenDowngrade, app, deployMeta); err != nil {
log.Fatal(err)
}
availableDowngrades = append(availableDowngrades, chosenDowngrade)
}
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenDowngrade == "" {
downgradeAvailable, err := ensureDowngradesAvailable(versions, &availableDowngrades, deployMeta)
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
log.Fatal(err)
log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
}
if !downgradeAvailable {
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
}
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) {
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)
}
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
warnMessages = append(warnMessages, fmt.Sprintf("attempting to rollback a chaos deployment"))
}
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
log.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
}
if parsedVersion.IsLessThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableDowngrades = append(availableDowngrades, version)
}
}
if len(availableDowngrades) == 0 && !internal.Force {
log.Info("no available downgrades")
return
}
}
if internal.Force || internal.NoInput || chosenDowngrade != "" {
if len(availableDowngrades) > 0 {
var chosenDowngrade string
if len(availableDowngrades) > 0 {
if internal.Force || internal.NoInput || specificVersion != "" {
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
}
} else {
if err := chooseDowngrade(availableDowngrades, deployMeta, &chosenDowngrade); err != nil {
log.Fatal(err)
}
}
log.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade)
} else {
msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
msg = fmt.Sprintf("please select a downgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion)
}
if internal.Force &&
chosenDowngrade == "" &&
deployMeta.Version != config.UNKNOWN_DEFAULT {
chosenDowngrade = deployMeta.Version
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableDowngrades),
}
if chosenDowngrade == "" {
log.Fatal("unknown deployed version, unable to downgrade")
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return
}
}
}
log.Debugf("choosing %s as version to rollback", chosenDowngrade)
if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
log.Fatal(err)
}
@ -161,7 +194,6 @@ beforehand. See "abra app backup" for more.`,
log.Fatal(err)
}
stackName := app.StackName()
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
@ -178,9 +210,7 @@ beforehand. See "abra app backup" for more.`,
appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
}
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
appPkg.SetUpdateLabel(compose, stackName, app.Env)
chaosVersion := config.CHAOS_DEFAULT
@ -191,13 +221,12 @@ beforehand. See "abra app backup" for more.`,
// NOTE(d1): no release notes implemeneted for rolling back
if err := internal.NewVersionOverview(
app,
downgradeWarnMessages,
warnMessages,
"rollback",
deployMeta.Version,
chaosVersion,
chosenDowngrade,
"",
); err != nil {
""); err != nil {
log.Fatal(err)
}
@ -205,100 +234,14 @@ beforehand. See "abra app backup" for more.`,
log.Fatal(err)
}
if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
app.Recipe.Version = chosenDowngrade
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
},
}
// chooseDowngrade prompts the user to choose an downgrade interactively.
func chooseDowngrade(
availableDowngrades []string,
deployMeta stack.DeployMeta,
chosenDowngrade *string,
) error {
msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = fmt.Sprintf(
"please select a downgrade (version: %s, chaos: %s):",
deployMeta.Version,
chaosVersion,
)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableDowngrades),
}
if err := survey.AskOne(prompt, chosenDowngrade); err != nil {
return err
}
return nil
}
// validateDownpgradeVersionArg validates the specific version.
func validateDowngradeVersionArg(
specificVersion string,
app app.App,
deployMeta stack.DeployMeta,
) error {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
}
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
}
return nil
}
// ensureDowngradesAvailable ensures that there are available downgrades.
func ensureDowngradesAvailable(
versions []string,
availableDowngrades *[]string,
deployMeta stack.DeployMeta,
) (bool, error) {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return false, err
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return false, err
}
if parsedVersion.IsLessThan(parsedDeployedVersion) &&
!(parsedVersion.Equals(parsedDeployedVersion)) {
*availableDowngrades = append(*availableDowngrades, version)
}
}
if len(*availableDowngrades) == 0 && !internal.Force {
return false, nil
}
return true, nil
}
func init() {
AppRollbackCommand.Flags().BoolVarP(
&internal.Force,
@ -320,6 +263,6 @@ func init() {
&internal.DontWaitConverge, "no-converge-checks",
"c",
false,
"disable converge logic checks",
"do not wait for converge logic checks",
)
}

View File

@ -17,7 +17,7 @@ import (
)
var AppRunCommand = &cobra.Command{
Use: "run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]",
Use: "run <app> <service> <cmd> [[args] [flags] | [flags] -- [args]]",
Aliases: []string{"r"},
Short: "Run a command inside a service container",
Example: ` # run <cmd> with args/flags

View File

@ -20,7 +20,7 @@ import (
)
var AppSecretGenerateCommand = &cobra.Command{
Use: "generate <domain> [[secret] [version] | --all] [flags]",
Use: "generate <app> [[secret] [version] | --all] [flags]",
Aliases: []string{"g"},
Short: "Generate secrets",
Args: cobra.RangeArgs(1, 3),
@ -45,7 +45,7 @@ var AppSecretGenerateCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -140,7 +140,7 @@ var AppSecretGenerateCommand = &cobra.Command{
}
var AppSecretInsertCommand = &cobra.Command{
Use: "insert <domain> <secret> <version> <data> [flags]",
Use: "insert <app> <secret> <version> <data> [flags]",
Aliases: []string{"i"},
Short: "Insert secret",
Long: `This command inserts a secret into an app environment.
@ -170,7 +170,7 @@ environment. Typically, you can let Abra generate them for you on app creation
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -230,7 +230,7 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string
}
var AppSecretRmCommand = &cobra.Command{
Use: "remove <domain> [[secret] | --all] [flags]",
Use: "remove <app> [[secret] | --all] [flags]",
Aliases: []string{"rm"},
Short: "Remove a secret",
Args: cobra.RangeArgs(1, 2),
@ -258,7 +258,7 @@ var AppSecretRmCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -338,7 +338,7 @@ var AppSecretRmCommand = &cobra.Command{
}
var AppSecretLsCommand = &cobra.Command{
Use: "list <domain>",
Use: "list <app>",
Aliases: []string{"ls"},
Short: "List all secrets",
Args: cobra.MinimumNArgs(1),
@ -351,7 +351,7 @@ var AppSecretLsCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}

View File

@ -17,7 +17,7 @@ import (
)
var AppServicesCommand = &cobra.Command{
Use: "services <domain> [flags]",
Use: "services <app> [flags]",
Aliases: []string{"sr"},
Short: "Display all services of an app",
Args: cobra.ExactArgs(1),
@ -30,7 +30,7 @@ var AppServicesCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}

View File

@ -18,7 +18,7 @@ import (
)
var AppUndeployCommand = &cobra.Command{
Use: "undeploy <domain> [flags]",
Use: "undeploy <app> [flags]",
Aliases: []string{"un"},
Short: "Undeploy an app",
Long: `This does not destroy any application data.
@ -59,22 +59,15 @@ Passing "--prune/-p" does not remove those volumes.`,
chaosVersion = deployMeta.ChaosVersion
}
toWriteVersion := deployMeta.Version
if deployMeta.IsChaos {
toWriteVersion = chaosVersion
}
if err := internal.UndeployOverview(
app,
deployMeta.Version,
chaosVersion,
toWriteVersion,
); err != nil {
chaosVersion); err != nil {
log.Fatal(err)
}
rmOpts := stack.Remove{
Namespaces: []string{stackName},
Namespaces: []string{app.StackName()},
Detach: false,
}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
@ -87,8 +80,9 @@ Passing "--prune/-p" does not remove those volumes.`,
}
}
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
log.Debugf("choosing %s as version to save to env file", deployMeta.Version)
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
log.Fatalf("writing undeployed recipe version in env file: %s", err)
}
},
}

View File

@ -5,40 +5,30 @@ import (
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
)
var AppUpgradeCommand = &cobra.Command{
Use: "upgrade <domain> [version] [flags]",
Use: "upgrade <app> [version] [flags]",
Aliases: []string{"up"},
Short: "Upgrade an app",
Long: `Upgrade an app.
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]".
It is possible to "--force/-f" an upgrade if you want to re-deploy a specific
version.
Only the deployed version is consulted when trying to determine what upgrades
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.
Unlike "deploy", chaos operations are not supported here. Only recipe versions
are supported values for "[version]".
An upgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`,
beforehand.`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -59,26 +49,22 @@ beforehand. See "abra app backup" for more.`,
}
},
Run: func(cmd *cobra.Command, args []string) {
var (
upgradeWarnMessages []string
chosenUpgrade string
availableUpgrades []string
upgradeReleaseNotes string
)
var warnMessages []string
app := internal.ValidateApp(args)
stackName := app.StackName()
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
var specificVersion string
if len(args) == 2 {
specificVersion = args[1]
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
}
deployMeta, err := ensureDeployed(cl, app)
if err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -86,69 +72,134 @@ beforehand. See "abra app backup" for more.`,
log.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatalf("%s is not deployed?", app.Name)
}
versions, err := app.Recipe.Tags()
if err != nil {
log.Fatal(err)
}
// NOTE(d1): we've no idea what the live deployment version is, so every
// possible upgrade can be shown. it's up to the user to make the choice
if deployMeta.Version == config.UNKNOWN_DEFAULT {
var availableUpgrades []string
if deployMeta.Version == "unknown" {
availableUpgrades = versions
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
}
if len(args) == 2 && args[1] != "" {
chosenUpgrade = args[1]
if err := validateUpgradeVersionArg(chosenUpgrade, app, deployMeta); err != nil {
log.Fatal(err)
}
availableUpgrades = append(availableUpgrades, chosenUpgrade)
}
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenUpgrade == "" {
upgradeAvailable, err := ensureUpgradesAvailable(versions, &availableUpgrades, deployMeta)
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
log.Fatal(err)
log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
}
if !upgradeAvailable {
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) {
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)
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
log.Fatal(err)
}
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
warnMessages = append(warnMessages, fmt.Sprintf("attempting to upgrade a chaos deployment"))
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableUpgrades = append(availableUpgrades, version)
}
}
if len(availableUpgrades) == 0 && !internal.Force {
log.Info("no available upgrades")
return
}
}
if internal.Force || internal.NoInput || chosenUpgrade != "" {
if len(availableUpgrades) > 0 {
var chosenUpgrade string
if len(availableUpgrades) > 0 {
if internal.Force || internal.NoInput || specificVersion != "" {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
}
} else {
if err := chooseUpgrade(availableUpgrades, deployMeta, &chosenUpgrade); err != nil {
log.Fatal(err)
log.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
} else {
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
msg = fmt.Sprintf("please select an upgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableUpgrades),
}
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return
}
}
}
if internal.Force &&
chosenUpgrade == "" &&
deployMeta.Version != config.UNKNOWN_DEFAULT {
if internal.Force && chosenUpgrade == "" {
warnMessages = append(warnMessages, fmt.Sprintf("%s is already upgraded to latest", app.Name))
chosenUpgrade = deployMeta.Version
}
if chosenUpgrade == "" {
log.Fatal("unknown deployed version, unable to upgrade")
// if release notes written after git tag published, read them before we
// check out the tag and then they'll appear to be missing. this covers
// when we obviously will forget to write release notes before publishing
var releaseNotes string
if chosenUpgrade != "" {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
log.Fatal(err)
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
log.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version)
if err != nil {
log.Fatal(err)
}
if note != "" {
releaseNotes += fmt.Sprintf("%s\n", note)
}
}
}
}
log.Debugf("choosing %s as version to upgrade", chosenUpgrade)
// NOTE(d1): if release notes written after git tag published, read them
// before we check out the tag and then they'll appear to be missing. this
// covers when we obviously will forget to write release notes before
// publishing
if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil {
log.Fatal(err)
}
if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
log.Fatal(err)
}
@ -166,7 +217,6 @@ beforehand. See "abra app backup" for more.`,
log.Fatal(err)
}
stackName := app.StackName()
deployOpts := stack.Deploy{
Composefiles: composeFiles,
Namespace: stackName,
@ -183,9 +233,7 @@ beforehand. See "abra app backup" for more.`,
appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
}
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
appPkg.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app)
@ -195,35 +243,30 @@ beforehand. See "abra app backup" for more.`,
for _, envVar := range envVars {
if !envVar.Present {
upgradeWarnMessages = append(upgradeWarnMessages,
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
warnMessages = append(warnMessages,
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain),
)
}
}
if showReleaseNotes {
fmt.Print(upgradeReleaseNotes)
fmt.Print(releaseNotes)
return
}
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
if deployMeta.ChaosVersion == "" {
chaosVersion = config.UNKNOWN_DEFAULT
}
}
if err := internal.NewVersionOverview(
app,
upgradeWarnMessages,
warnMessages,
"upgrade",
deployMeta.Version,
chaosVersion,
chosenUpgrade,
upgradeReleaseNotes,
); err != nil {
releaseNotes); err != nil {
log.Fatal(err)
}
@ -231,8 +274,7 @@ beforehand. See "abra app backup" for more.`,
if err != nil {
log.Fatal(err)
}
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
log.Fatal(err)
@ -241,162 +283,19 @@ beforehand. See "abra app backup" for more.`,
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
if ok && !internal.DontWaitConverge {
log.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err)
}
}
if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
app.Recipe.Version = chosenUpgrade
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
},
}
// chooseUpgrade prompts the user to choose an upgrade interactively.
func chooseUpgrade(
availableUpgrades []string,
deployMeta stack.DeployMeta,
chosenUpgrade *string,
) error {
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = fmt.Sprintf(
"please select an upgrade (version: %s, chaos: %s):",
deployMeta.Version,
chaosVersion,
)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableUpgrades),
}
if err := survey.AskOne(prompt, chosenUpgrade); err != nil {
return err
}
return nil
}
func getReleaseNotes(
app app.App,
versions []string,
chosenUpgrade string,
deployMeta stack.DeployMeta,
upgradeReleaseNotes *string,
) error {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
return err
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return err
}
for _, version := range internal.SortVersionsDesc(versions) {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return err
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version)
if err != nil {
return err
}
if note != "" {
*upgradeReleaseNotes += fmt.Sprintf("%s\n", note)
}
}
}
return nil
}
// ensureUpgradesAvailable ensures that there are available upgrades.
func ensureUpgradesAvailable(
versions []string,
availableUpgrades *[]string,
deployMeta stack.DeployMeta,
) (bool, error) {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return false, err
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return false, err
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
!(parsedVersion.Equals(parsedDeployedVersion)) {
*availableUpgrades = append(*availableUpgrades, version)
}
}
if len(*availableUpgrades) == 0 && !internal.Force {
return false, nil
}
return true, nil
}
// validateUpgradeVersionArg validates the specific version.
func validateUpgradeVersionArg(
specificVersion string,
app app.App,
deployMeta stack.DeployMeta,
) error {
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return err
}
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
return nil
}
// ensureDeployed ensures the app is deployed and if so, returns deployment
// meta info.
func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, error) {
log.Debugf("checking whether %s is already deployed", app.StackName())
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
return stack.DeployMeta{}, err
}
if !deployMeta.IsDeployed {
return stack.DeployMeta{}, fmt.Errorf("%s is not deployed?", app.Name)
}
return deployMeta, nil
}
var (
showReleaseNotes bool
)
@ -422,7 +321,7 @@ func init() {
&internal.DontWaitConverge, "no-converge-checks",
"c",
false,
"disable converge logic checks",
"do not wait for converge logic checks",
)
AppUpgradeCommand.Flags().BoolVarP(

View File

@ -14,7 +14,7 @@ import (
)
var AppVolumeListCommand = &cobra.Command{
Use: "list <domain> [flags]",
Use: "list <app> [flags]",
Aliases: []string{"ls"},
Short: "List volumes associated with an app",
Args: cobra.ExactArgs(1),
@ -71,12 +71,12 @@ var AppVolumeListCommand = &cobra.Command{
}
var AppVolumeRemoveCommand = &cobra.Command{
Use: "remove <domain> [flags]",
Use: "remove <app> [flags]",
Short: "Remove volume(s) associated with an app",
Long: `Remove volumes associated with an app.
The app in question must be undeployed before you try to remove volumes. See
"abra app undeploy <domain>" for more.
"abra app undeploy <app>" for more.
The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this

View File

@ -5,7 +5,6 @@ import (
"fmt"
"io/ioutil"
"path"
"slices"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -25,17 +24,12 @@ var CatalogueGenerateCommand = &cobra.Command{
Short: "Generate the recipe catalogue",
Long: `Generate a new copy of the recipe catalogue.
N.B. this command **will** wipe local unstaged changes from your local recipes
if present. "--chaos/-C" on this command refers to the catalogue repository
("$ABRA_DIR/catalogue") and not the recipes. Please take care not to lose your
changes.
It is possible to generate new metadata for a single recipe by passing
[recipe]. The existing local catalogue will be updated, not overwritten.
It is quite easy to get rate limited by Docker Hub when running this command.
If you have a Hub account you can "docker login" and Abra will automatically
use those details.
If you have a Hub account you can have Abra log you in to avoid this. Pass
"--user" and "--pass".
Push your new release to git.coopcloud.tech with "--publish/-p". This requires
that you have permission to git push to these repositories and have your SSH
@ -53,62 +47,56 @@ keys configured on your account.`,
recipeName = args[0]
}
r := recipe.Get(recipeName)
if recipeName != "" {
internal.ValidateRecipe(args, cmd.Name())
}
if err := catalogue.EnsureCatalogue(); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := catalogue.EnsureIsClean(); err != nil {
log.Fatal(err)
}
}
repos, err := recipe.ReadReposMetadata(internal.Debug)
repos, err := recipe.ReadReposMetadata()
if err != nil {
log.Fatal(err)
}
barLength := len(repos)
var barLength int
var logMsg string
if recipeName != "" {
barLength = 1
logMsg = fmt.Sprintf("ensuring %v recipe is cloned & up-to-date", barLength)
} else {
barLength = len(repos)
logMsg = fmt.Sprintf("ensuring %v recipes are cloned & up-to-date, this could take some time...", barLength)
}
if !skipUpdates {
if err := recipe.UpdateRepositories(repos, recipeName, internal.Debug); err != nil {
log.Warn(logMsg)
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
log.Fatal(err)
}
}
var warnings []string
catl := make(recipe.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(barLength, "collecting catalogue metadata")
catlBar := formatter.CreateProgressbar(barLength, "generating catalogue metadata...")
for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name {
if !internal.Debug {
catlBar.Add(1)
}
catlBar.Add(1)
continue
}
r := recipe.Get(recipeMeta.Name)
versions, warnMsgs, err := r.GetRecipeVersions()
versions, err := r.GetRecipeVersions()
if err != nil {
warnings = append(warnings, err.Error())
}
if len(warnMsgs) > 0 {
warnings = append(warnings, warnMsgs...)
log.Warn(err)
}
features, category, warnMsgs, err := recipe.GetRecipeFeaturesAndCategory(r)
features, category, err := recipe.GetRecipeFeaturesAndCategory(r)
if err != nil {
warnings = append(warnings, err.Error())
}
if len(warnMsgs) > 0 {
warnings = append(warnings, warnMsgs...)
log.Warn(err)
}
catl[recipeMeta.Name] = recipe.RecipeMeta{
@ -124,24 +112,7 @@ keys configured on your account.`,
Features: features,
}
if !internal.Debug {
catlBar.Add(1)
}
}
if err := catlBar.Close(); err != nil {
log.Fatal(err)
}
var uniqueWarnings []string
for _, w := range warnings {
if !slices.Contains(uniqueWarnings, w) {
uniqueWarnings = append(uniqueWarnings, w)
}
}
for _, warnMsg := range uniqueWarnings {
log.Warn(warnMsg)
catlBar.Add(1)
}
recipesJSON, err := json.MarshalIndent(catl, "", " ")
@ -171,7 +142,7 @@ keys configured on your account.`,
}
}
log.Infof("generated recipe catalogue: %s", config.RECIPES_JSON)
log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if publishChanges {
@ -197,7 +168,7 @@ keys configured on your account.`,
log.Fatal(err)
}
sshURL := fmt.Sprintf(config.TOOLSHED_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 {
log.Fatal(err)
}

View File

@ -12,16 +12,17 @@ var AutocompleteCommand = &cobra.Command{
Long: `To load completions:
Bash:
# Load autocompletion for the current Bash session
$ source <(abra autocomplete bash)
# To load autocompletion for each session, execute once:
# Linux:
$ abra autocomplete bash | sudo tee /etc/bash_completion.d/abra
$ abra autocomplete bash > /etc/bash_completion.d/abra
# macOS:
$ abra autocomplete bash | sudo tee $(brew --prefix)/etc/bash_completion.d/abra
$ abra autocomplete bash > $(brew --prefix)/etc/bash_completion.d/abra
Zsh:
# If shell autocompletion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
@ -33,12 +34,14 @@ Zsh:
# You will need to start a new shell for this setup to take effect.
fish:
$ abra autocomplete fish | source
# To load autocompletions for each session, execute once:
$ abra autocomplete fish > ~/.config/fish/completions/abra.fish
PowerShell:
PS> abra autocomplete powershell | Out-String | Invoke-Expression
# To load autocompletions for every new session, run:

View File

@ -2,10 +2,9 @@ package internal
var (
// NOTE(d1): global
Debug bool
NoInput bool
Offline bool
IgnoreEnvVersion bool
Debug bool
NoInput bool
Offline bool
// NOTE(d1): sub-command specific
Chaos bool

View File

@ -10,7 +10,6 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss"
@ -62,37 +61,18 @@ func NewVersionOverview(
domain = config.NO_DOMAIN_DEFAULT
}
upperKind := strings.ToUpper(kind)
envVersion, err := recipe.GetEnvVersionRaw(app.Recipe.Name)
if err != nil {
return err
}
if envVersion == "" {
envVersion = config.NO_VERSION_DEFAULT
}
rows := [][]string{
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(deployedVersion)},
{"CHAOS ", formatter.BoldDirtyDefault(deployedChaosVersion)},
{upperKind, "---"},
{"VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Domain)), "---"},
{"CURRENT VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
[]string{"APP", domain},
[]string{"RECIPE", app.Recipe.Name},
[]string{"SERVER", server},
[]string{"DEPLOYED", deployedVersion},
[]string{"CURRENT CHAOS ", deployedChaosVersion},
[]string{fmt.Sprintf("TO %s", strings.ToUpper(kind)), toDeployVersion},
[]string{"CONFIG", deployConfig},
}
overview := formatter.CreateOverview(
fmt.Sprintf("%s OVERVIEW", upperKind),
fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind)),
rows,
)
@ -135,9 +115,7 @@ func DeployOverview(
deployedVersion string,
deployedChaosVersion string,
toDeployVersion,
toDeployChaosVersion string,
toWriteVersion string,
) error {
toDeployChaosVersion string) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles
@ -153,42 +131,15 @@ func DeployOverview(
domain = config.NO_DOMAIN_DEFAULT
}
if app.Recipe.Dirty {
toWriteVersion = formatter.AddDirtyMarker(toWriteVersion)
toDeployChaosVersion = formatter.AddDirtyMarker(toDeployChaosVersion)
}
recipeName, exists := app.Env["RECIPE"]
if !exists {
recipeName = app.Env["TYPE"]
}
envVersion, err := recipe.GetEnvVersionRaw(recipeName)
if err != nil {
return err
}
if envVersion == "" {
envVersion = config.NO_VERSION_DEFAULT
}
rows := [][]string{
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(deployedVersion)},
{"CHAOS", formatter.BoldDirtyDefault(deployedChaosVersion)},
{"NEW DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
{"CHAOS", formatter.BoldDirtyDefault(toDeployChaosVersion)},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"},
{"CURRENT VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toWriteVersion)},
[]string{"APP", domain},
[]string{"RECIPE", app.Recipe.Name},
[]string{"SERVER", server},
[]string{"DEPLOYED", deployedVersion},
[]string{"CURRENT CHAOS ", deployedChaosVersion},
[]string{"TO DEPLOY", toDeployVersion},
[]string{"NEW CHAOS", toDeployChaosVersion},
[]string{"CONFIG", deployConfig},
}
overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows)
@ -219,10 +170,8 @@ func DeployOverview(
// UndeployOverview shows an undeployment overview
func UndeployOverview(
app appPkg.App,
deployedVersion,
deployedChaosVersion,
toWriteVersion string,
) error {
version,
chaosVersion string) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles
@ -238,33 +187,13 @@ func UndeployOverview(
domain = config.NO_DOMAIN_DEFAULT
}
recipeName, exists := app.Env["RECIPE"]
if !exists {
recipeName = app.Env["TYPE"]
}
envVersion, err := recipe.GetEnvVersionRaw(recipeName)
if err != nil {
return err
}
if envVersion == "" {
envVersion = config.NO_VERSION_DEFAULT
}
rows := [][]string{
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(deployedVersion)},
{"CHAOS", formatter.BoldDirtyDefault(deployedChaosVersion)},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"},
{"CURRENT VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toWriteVersion)},
[]string{"APP", domain},
[]string{"RECIPE", app.Recipe.Name},
[]string{"SERVER", server},
[]string{"DEPLOYED", version},
[]string{"CHAOS", chaosVersion},
[]string{"CONFIG", deployConfig},
}
overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows)

View File

@ -1,17 +0,0 @@
package internal
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSortVersionsDesc(t *testing.T) {
versions := SortVersionsDesc([]string{
"0.2.3+1.2.2",
"1.0.0+2.2.2",
})
assert.Equal(t, "1.0.0+2.2.2", versions[0])
assert.Equal(t, "0.2.3+1.2.2", versions[1])
}

View File

@ -1,11 +0,0 @@
package internal
import "coopcloud.tech/abra/pkg/recipe"
func GetEnsureContext() recipe.EnsureContext {
return recipe.EnsureContext{
Chaos,
Offline,
IgnoreEnvVersion,
}
}

18
cli/internal/errors.go Normal file
View File

@ -0,0 +1,18 @@
package internal
import (
"os"
"coopcloud.tech/abra/pkg/log"
"github.com/urfave/cli/v3"
)
// ShowSubcommandHelpAndError exits the program on error, logs the error to the
// terminal, and shows the help command.
func ShowSubcommandHelpAndError(cmd *cli.Command, err interface{}) {
if err2 := cli.ShowSubcommandHelp(cmd); err2 != nil {
log.Error(err2)
}
log.Error(err)
os.Exit(1)
}

View File

@ -34,10 +34,9 @@ var RecipeFetchCommand = &cobra.Command{
log.Fatal("cannot use [recipe] and --all/-a together")
}
ensureCtx := internal.GetEnsureContext()
if recipeName != "" {
r := internal.ValidateRecipe(args, cmd.Name())
if err := r.Ensure(ensureCtx); err != nil {
if err := r.Ensure(false, false); err != nil {
log.Fatal(err)
}
return
@ -51,7 +50,7 @@ var RecipeFetchCommand = &cobra.Command{
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
for recipeName := range catalogue {
r := recipe.Get(recipeName)
if err := r.Ensure(ensureCtx); err != nil {
if err := r.Ensure(false, false); err != nil {
log.Error(err)
}
catlBar.Add(1)

View File

@ -23,7 +23,7 @@ var RecipeLintCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}

View File

@ -58,7 +58,7 @@ var RecipeNewCommand = &cobra.Command{
if err := os.RemoveAll(gitRepo); err != nil {
log.Fatal(err)
}
log.Debugf("removed .git repo in %s", gitRepo)
log.Debugf("removed example git repo in %s", gitRepo)
meta := newRecipeMeta(recipeName)
@ -76,6 +76,7 @@ var RecipeNewCommand = &cobra.Command{
if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil {
log.Fatal(err)
}
}
if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil {

View File

@ -267,8 +267,6 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return err
}
var addNextAsReleaseNotes bool
nextReleaseNotePath := path.Join(releaseDir, "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag>
@ -278,37 +276,38 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
}
if !internal.NoInput {
prompt := &survey.Confirm{
prompt := &survey.Input{
Message: "Use release note in release/next?",
}
if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil {
var addReleaseNote bool
if err := survey.AskOne(prompt, &addReleaseNote); err != nil {
return err
}
if !addNextAsReleaseNotes {
if !addReleaseNote {
return nil
}
}
if err := os.Rename(nextReleaseNotePath, tagReleaseNotePath); err != nil {
err := os.Rename(nextReleaseNotePath, tagReleaseNotePath)
if err != nil {
return err
}
if err := gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry); err != nil {
err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry)
if err != nil {
return err
}
if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil {
err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry)
if err != nil {
return err
}
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
// NOTE(d1): No release note exists for the current release. Or, we've
// already used release/next as the release note
if internal.NoInput || addNextAsReleaseNotes {
// No release note exists for the current release.
if internal.NoInput {
return nil
}

View File

@ -63,7 +63,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}

View File

@ -37,13 +37,10 @@ var RecipeVersionCommand = &cobra.Command{
if !ok {
warnMessages = append(warnMessages, "retrieved versions from local recipe repository")
recipeVersions, warnMsg, err := recipe.GetRecipeVersions()
recipeVersions, err := recipe.GetRecipeVersions()
if err != nil {
warnMessages = append(warnMessages, err.Error())
}
if len(warnMsg) > 0 {
warnMessages = append(warnMessages, warnMsg...)
}
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
}

View File

@ -48,13 +48,9 @@ func Run(version, commit string) {
}
}
log.Logger.SetStyles(charmLog.DefaultStyles())
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger)
if internal.MachineReadable {
log.SetOutput(os.Stderr)
}
if internal.Debug {
log.SetLevel(log.DebugLevel)
log.SetOutput(os.Stderr)
@ -99,37 +95,20 @@ func Run(version, commit string) {
}
rootCmd.PersistentFlags().BoolVarP(
&internal.Debug,
"debug",
"d",
false,
&internal.Debug, "debug", "d", false,
"show debug messages",
)
rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput,
"no-input",
"n",
false,
&internal.NoInput, "no-input", "n", false,
"toggle non-interactive mode",
)
rootCmd.PersistentFlags().BoolVarP(
&internal.Offline,
"offline",
"o",
false,
&internal.Offline, "offline", "o", false,
"prefer offline & filesystem access",
)
rootCmd.PersistentFlags().BoolVarP(
&internal.IgnoreEnvVersion,
"ignore-env-version",
"i",
false,
"ignore .env version checkout",
)
catalogue.CatalogueCommand.AddCommand(
catalogue.CatalogueGenerateCommand,
)
@ -208,11 +187,9 @@ func Run(version, commit string) {
app.AppUndeployCommand,
app.AppUpgradeCommand,
app.AppVolumeCommand,
app.AppLabelsCommand,
app.AppEnvCommand,
)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
log.Fatal(err)
}
}

View File

@ -452,7 +452,7 @@ func newKadabraApp(version, commit string) *cobra.Command {
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Short: "The Co-op Cloud auto-updater 🤖 🚀",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.Logger.SetStyles(charmLog.DefaultStyles())
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger)
if internal.Debug {

38
go.mod
View File

@ -4,9 +4,11 @@ go 1.22.7
toolchain go1.23.1
replace github.com/urfave/cli/v3 => github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44
require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/lipgloss v1.0.0
github.com/charmbracelet/log v0.4.0
@ -14,13 +16,14 @@ require (
github.com/docker/cli v27.4.1+incompatible
github.com/docker/docker v27.4.1+incompatible
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.13.1
github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-cmp v0.6.0
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2
github.com/moby/term v0.5.0
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.17.1
golang.org/x/term v0.28.0
github.com/urfave/cli/v3 v3.0.0-alpha9
golang.org/x/term v0.27.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1
)
@ -28,7 +31,7 @@ require (
require (
dario.cat/mergo v1.0.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect
@ -52,7 +55,7 @@ require (
github.com/fsnotify/fsnotify v1.6.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/go-billy/v5 v5.6.1 // indirect
github.com/go-git/go-billy/v5 v5.6.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
@ -75,7 +78,6 @@ require (
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mmcloughlin/avo v0.6.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect
github.com/moby/sys/user v0.3.0 // indirect
@ -87,7 +89,7 @@ require (
github.com/opencontainers/runc v1.1.13 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pjbgf/sha1cd v0.3.1 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.61.0 // indirect
@ -111,19 +113,17 @@ require (
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
golang.org/x/time v0.8.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.69.2 // indirect
google.golang.org/protobuf v1.36.2 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
@ -146,5 +146,5 @@ require (
github.com/stretchr/testify v1.10.0
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.29.0
golang.org/x/sys v0.28.0
)

127
go.sum
View File

@ -27,16 +27,16 @@ coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c h1:oeKnUB79PKYD8D0/unYuu7MRcWryQQWOns8+JL+acrs=
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c/go.mod h1:fQuhwrpg6qb9NlFXKYi/LysWu1wxjraS8sxyW12CUF0=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
@ -140,6 +140,8 @@ github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E=
github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
@ -276,6 +278,7 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@ -286,6 +289,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
@ -306,6 +311,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
@ -314,6 +321,8 @@ github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
@ -341,8 +350,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@ -373,16 +382,16 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYis
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -522,6 +531,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
@ -651,8 +662,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY=
github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
@ -675,8 +684,6 @@ github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcY
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@ -754,8 +761,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI=
github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -786,6 +793,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@ -896,6 +905,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44 h1:BeSTAZEDkDVNv9EOrycIGCkEg+6EhRRgSsbdc93Q3OM=
github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
@ -936,31 +947,49 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -985,10 +1014,10 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -999,10 +1028,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1025,8 +1054,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1068,10 +1095,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1089,6 +1116,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1170,17 +1199,17 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1190,6 +1219,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1200,8 +1231,6 @@ golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1247,10 +1276,6 @@ golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4X
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1299,14 +1324,14 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA=
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0=
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -1326,6 +1351,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@ -1341,10 +1368,10 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=

View File

@ -36,7 +36,7 @@ func Get(appName string) (App, error) {
return App{}, err
}
log.Debugf("loaded app %s: %s", appName, app)
log.Debugf("retrieved %s for %s", app, appName)
return app, nil
}
@ -89,17 +89,8 @@ type App struct {
Env envfile.AppEnv
Server string
Path string
}
// String outputs a human-friendly string representation.
func (a App) String() string {
out := fmt.Sprintf("{name: %s, ", a.Name)
out += fmt.Sprintf("recipe: %s, ", a.Recipe)
out += fmt.Sprintf("domain: %s, ", a.Domain)
out += fmt.Sprintf("env %s, ", a.Env)
out += fmt.Sprintf("server %s, ", a.Server)
out += fmt.Sprintf("path %s}", a.Path)
return out
status *Status
}
// Type aliases to make code hints easier to understand
@ -143,6 +134,19 @@ func StackName(appName string) string {
return stackName
}
func (a App) Status() (Status, error) {
if a.status != nil {
return *a.status, nil
}
appStatuses, err := GetAppStatuses([]App{a}, true)
if err != nil {
return Status{}, err
}
appStatus := appStatuses[a.StackName()]
a.status = &appStatus
return appStatus, nil
}
// 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.
@ -246,6 +250,8 @@ func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
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())
@ -406,9 +412,17 @@ func SanitiseAppName(name string) string {
return strings.ReplaceAll(name, ".", "_")
}
type Status struct {
Status string
Version string
Chaos bool
ChaosVersion string
AutoUpdate bool
}
// 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)
func GetAppStatuses(apps []App, MachineReadable bool) (map[string]Status, error) {
statuses := make(map[string]Status)
servers := make(map[string]struct{})
for _, app := range apps {
@ -444,36 +458,32 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
}
for _, service := range status.Services {
result := make(map[string]string)
result := Status{}
name := service.Spec.Labels[convert.LabelNamespace]
if _, ok := statuses[name]; !ok {
result["status"] = "deployed"
result.Status = "deployed"
}
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name)
chaos, ok := service.Spec.Labels[labelKey]
if ok {
result["chaos"] = chaos
result.Chaos = chaos == "true"
}
labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name)
if chaosVersion, ok := service.Spec.Labels[labelKey]; ok {
result["chaosVersion"] = chaosVersion
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"
result.AutoUpdate = autoUpdate == "true"
}
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
if version, ok := service.Spec.Labels[labelKey]; ok {
result["version"] = version
} else {
continue
result.Version = version
}
statuses[name] = result
@ -503,13 +513,13 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debugf("adding env vars to %s service config", stackName)
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("%s: %s: %s", stackName, k, value)
log.Debugf("add env var: %s value: %s to %s", k, value, stackName)
}
}
}
@ -578,49 +588,6 @@ func ReadAbraShCmdNames(abraSh string) ([]string, error) {
return cmdNames, nil
}
// Wipe removes the version from the app .env file.
func (a App) WipeRecipeVersion() error {
file, err := os.Open(a.Path)
if err != nil {
return err
}
defer file.Close()
var (
lines []string
scanner = bufio.NewScanner(file)
)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
lines = append(lines, line)
continue
}
if strings.HasPrefix(line, "#") {
lines = append(lines, line)
continue
}
splitted := strings.Split(line, ":")
lines = append(lines, splitted[0])
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
log.Fatal(err)
}
log.Debugf("version wiped from %s.env", a.Domain)
return nil
}
// WriteRecipeVersion writes the recipe version to the app .env file.
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
file, err := os.Open(a.Path)
if err != nil {
@ -628,13 +595,9 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
}
defer file.Close()
var (
dirtyVersion string
skipped bool
lines []string
scanner = bufio.NewScanner(file)
)
skipped := false
scanner := bufio.NewScanner(file)
lines := []string{}
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
@ -647,27 +610,13 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
continue
}
if strings.Contains(line, version) && !a.Recipe.Dirty && !strings.HasSuffix(line, config.DIRTY_DEFAULT) {
if strings.Contains(line, version) {
skipped = true
lines = append(lines, line)
continue
}
splitted := strings.Split(line, ":")
if a.Recipe.Dirty {
dirtyVersion = fmt.Sprintf("%s%s", version, config.DIRTY_DEFAULT)
if strings.Contains(line, dirtyVersion) {
skipped = true
lines = append(lines, line)
continue
}
line = fmt.Sprintf("%s:%s", splitted[0], dirtyVersion)
lines = append(lines, line)
continue
}
line = fmt.Sprintf("%s:%s", splitted[0], version)
lines = append(lines, line)
}
@ -676,10 +625,6 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
log.Fatal(err)
}
if a.Recipe.Dirty && dirtyVersion != "" {
version = dirtyVersion
}
if !dryRun {
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
log.Fatal(err)

View File

@ -198,41 +198,3 @@ func compareFilter(t *testing.T, f1 filters.Args, f2 map[string]map[string]bool)
t.Errorf("filters mismatch (-want +got):\n%s", diff)
}
}
func TestWriteRecipeVersionOverwrite(t *testing.T) {
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
defer t.Cleanup(func() {
if err := app.WipeRecipeVersion(); err != nil {
t.Fatal(err)
}
})
assert.Equal(t, "", app.Recipe.EnvVersion)
if err := app.WriteRecipeVersion("foo", false); err != nil {
t.Fatal(err)
}
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "foo", app.Recipe.EnvVersion)
app.Recipe.Dirty = true
if err := app.WriteRecipeVersion("foo+U", false); err != nil {
t.Fatal(err)
}
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "foo+U", app.Recipe.EnvVersion)
}

View File

@ -54,7 +54,7 @@ func RecipeNameComplete() ([]string, cobra.ShellCompDirective) {
// RecipeVersionComplete completes versions for the recipe.
func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(true)
catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError

View File

@ -16,12 +16,13 @@ import (
func EnsureCatalogue() error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
log.Debugf("catalogue is missing, retrieving now")
log.Warnf("local recipe catalogue is missing, retrieving now")
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err
}
log.Debugf("cloned catalogue repository to %s", catalogueDir)
}
return nil

View File

@ -97,17 +97,16 @@ func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catal
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"
TOOLSHED_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/toolshed/%s.git"
RECIPES_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
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"
// NOTE(d1): please note, this was done purely out of laziness on our part
// AFAICR. it's easy to punt the value into the label because that is what is
@ -115,10 +114,6 @@ var (
// complained yet!
CHAOS_DEFAULT = "false"
DIRTY_DEFAULT = "+U"
NO_DOMAIN_DEFAULT = "N/A"
NO_VERSION_DEFAULT = "N/A"
UNKNOWN_DEFAULT = "unknown"
)

View File

@ -26,16 +26,9 @@ func GetServers() ([]string, error) {
return servers, err
}
var filtered []string
for _, s := range servers {
if !strings.HasPrefix(s, ".") {
filtered = append(filtered, s)
}
}
log.Debugf("retrieved %v servers: %s", len(servers), servers)
log.Debugf("retrieved %v servers: %s", len(filtered), filtered)
return filtered, nil
return servers, nil
}
// ReadServerNames retrieves all server names.

View File

@ -15,7 +15,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
}{
// NOTE(d1): DNS records get checked, so use something that is maintained
// within the federation. if you're here because of a failing test, try
// `dig +short <domain>` to ensure stuff matches first! If flakyness
// `dig +short <app>` to ensure stuff matches first! If flakyness
// becomes an issue we can look into mocking
{"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true},
{"docs.coopcloud.tech", "coopcloud.tech", true},
@ -43,7 +43,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
func TestEnsureIpv4(t *testing.T) {
// NOTE(d1): DNS records get checked, so use something that is maintained
// within the federation. if you're here because of a failing test, try `dig
// +short <domain>` to ensure stuff matches first! If flakyness becomes an
// +short <app>` to ensure stuff matches first! If flakyness becomes an
// issue we can look into mocking
domainName := "collabora.ostrom.collective.tools"
serverName := "ostrom.collective.tools"

View File

@ -8,7 +8,7 @@ import (
"strings"
"coopcloud.tech/abra/pkg/log"
"git.coopcloud.tech/toolshed/godotenv"
"git.coopcloud.tech/coop-cloud/godotenv"
)
// envVarModifiers is a list of env var modifier strings. These are added to
@ -31,6 +31,8 @@ func ReadEnv(filePath string) (AppEnv, error) {
return nil, err
}
log.Debugf("read %s from %s", envVars, filePath)
return envVars, nil
}

View File

@ -192,7 +192,7 @@ func TestEnvVarCommentsRemoved(t *testing.T) {
envVar, exists = envSample["SECRET_TEST_PASS_TWO_VERSION"]
if !exists {
t.Fatal("SECRET_TEST_PASS_TWO_VERSION env var should be present in .env.sample")
t.Fatal("WITH_COMMENT env var should be present in .env.sample")
}
if strings.Contains(envVar, "length") {

View File

@ -13,15 +13,11 @@ import (
"github.com/docker/go-units"
"golang.org/x/term"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"github.com/schollz/progressbar/v3"
)
var BoldStyle = lipgloss.NewStyle().
Bold(true)
var BoldUnderlineStyle = lipgloss.NewStyle().
Bold(true).
Underline(true)
@ -106,6 +102,7 @@ func CreateOverview(header string, rows [][]string) string {
var borderStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Padding(0, 1, 0, 1).
MaxWidth(79).
BorderForeground(lipgloss.Color("63"))
var headerStyle = lipgloss.NewStyle().
@ -113,7 +110,9 @@ func CreateOverview(header string, rows [][]string) string {
Bold(true).
PaddingBottom(1)
var leftStyle = lipgloss.NewStyle()
var leftStyle = lipgloss.NewStyle().
Bold(true)
var rightStyle = lipgloss.NewStyle()
var longest int
@ -125,10 +124,6 @@ func CreateOverview(header string, rows [][]string) string {
var renderedRows []string
for _, row := range rows {
if len(row) < 2 {
continue
}
if len(row) > 2 {
panic("CreateOverview: only accepts rows of len == 2")
}
@ -143,21 +138,10 @@ func CreateOverview(header string, rows [][]string) string {
offset = offset + " "
}
rendered := horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1]))
if row[1] == "---" {
rendered = horizontal(
leftStyle.
Bold(true).
Underline(true).
PaddingTop(1).
Render(row[0]),
offset,
rightStyle.Render(""),
)
}
renderedRows = append(renderedRows, rendered)
renderedRows = append(
renderedRows,
horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1])),
)
}
body := strings.Builder{}
@ -217,6 +201,7 @@ func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
progressbar.OptionClearOnFinish(),
progressbar.OptionSetPredictTime(false),
progressbar.OptionShowCount(),
progressbar.OptionFullWidth(),
progressbar.OptionSetDescription(title),
)
}
@ -257,18 +242,3 @@ func ByteCountSI(b uint64) string {
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}
// BoldDirtyDefault ensures a dirty modifier is rendered in bold.
func BoldDirtyDefault(v string) string {
if strings.HasSuffix(v, config.DIRTY_DEFAULT) {
vBold := BoldStyle.Render(config.DIRTY_DEFAULT)
v = strings.Replace(v, config.DIRTY_DEFAULT, vBold, 1)
}
return v
}
// AddDirtyMarker adds the dirty marker to a version string.
func AddDirtyMarker(v string) string {
return fmt.Sprintf("%s%s", v, config.DIRTY_DEFAULT)
}

View File

@ -1,11 +0,0 @@
package formatter
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestBoldDirtyDefault(t *testing.T) {
assert.Equal(t, "foo", BoldDirtyDefault("foo"))
}

View File

@ -1,7 +1,9 @@
package git
import (
"fmt"
"os"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/log"
@ -9,59 +11,39 @@ import (
"github.com/go-git/go-git/v5/plumbing"
)
// gitCloneIgnoreErr checks whether we can ignore a git clone error or not.
func gitCloneIgnoreErr(err error) bool {
if strings.Contains(err.Error(), "authentication required") {
return true
}
if strings.Contains(err.Error(), "remote repository is empty") {
return true
}
return false
}
// Clone runs a git clone which accounts for different default branches.
func Clone(dir, url string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Debugf("git clone: %s", dir, url)
log.Debugf("%s does not exist, attempting to git clone from %s", dir, url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
SingleBranch: true,
})
if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir)
return nil
}
if err != nil {
log.Debug("git clone: main branch failed, attempting master branch")
log.Debugf("cloning %s default branch failed, attempting from main branch", url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
SingleBranch: true,
})
if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir)
return nil
}
if err != nil {
if strings.Contains(err.Error(), "authentication required") {
name := filepath.Base(dir)
return fmt.Errorf("unable to clone %s, does %s exist?", name, url)
}
return err
}
}
log.Debugf("git clone: %s cloned successfully", dir)
log.Debugf("%s has been git cloned successfully", dir)
} else {
log.Debugf("git clone: %s already exists", dir)
log.Debugf("%s already exists", dir)
}
return nil

View File

@ -15,11 +15,9 @@ func Init(repoPath string, commit bool, gitName, gitEmail string) error {
if err != nil {
return fmt.Errorf("git init: %s", err)
}
if err = SwitchToMain(repo); err != nil {
return fmt.Errorf("git branch rename: %s", err)
}
log.Debugf("initialised new git repo in %s", repoPath)
if commit {
@ -41,11 +39,9 @@ func Init(repoPath string, commit bool, gitName, gitEmail string) error {
if gitName != "" && gitEmail != "" {
author = &object.Signature{Name: gitName, Email: gitEmail}
}
if _, err = commitWorktree.Commit("init", &git.CommitOptions{Author: author}); err != nil {
return fmt.Errorf("git commit: %s", err)
}
log.Debugf("init committed all files for new git repo in %s", repoPath)
}
@ -58,18 +54,14 @@ func SwitchToMain(repo *git.Repository) error {
if err := repo.Storer.SetReference(ref); err != nil {
return fmt.Errorf("set reference: %s", err)
}
cfg, err := repo.Config()
if err != nil {
return fmt.Errorf("repo config: %s", err)
}
cfg.Init.DefaultBranch = "main"
if err := repo.SetConfig(cfg); err != nil {
return fmt.Errorf("repo set config: %s", err)
}
log.Debug("set 'main' as the default branch")
log.Debug("set 'main' as the default branch.")
return nil
}

View File

@ -1,8 +1,6 @@
package git
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/user"
@ -19,16 +17,12 @@ import (
func IsClean(repoPath string) (bool, error) {
repo, err := git.PlainOpen(repoPath)
if err != nil {
if errors.Is(err, git.ErrRepositoryNotExists) {
return false, git.ErrRepositoryNotExists
}
return false, fmt.Errorf("unable to open %s: %s", repoPath, err)
return false, err
}
worktree, err := repo.Worktree()
if err != nil {
return false, fmt.Errorf("unable to open worktree of %s: %s", repoPath, err)
return false, err
}
patterns, err := GetExcludesFiles()
@ -42,14 +36,13 @@ func IsClean(repoPath string) (bool, error) {
status, err := worktree.Status()
if err != nil {
return false, fmt.Errorf("unable to query status of %s: %s", repoPath, err)
return false, err
}
if status.String() != "" {
noNewline := strings.TrimSuffix(status.String(), "\n")
log.Debugf("git status: %s: %s", repoPath, noNewline)
log.Debugf("discovered git status in %s: %s", repoPath, status.String())
} else {
log.Debugf("git status: %s: clean", repoPath)
log.Debugf("discovered clean git status in %s", repoPath)
}
return status.IsClean(), nil

View File

@ -1,15 +0,0 @@
package git
import (
"errors"
"testing"
"github.com/go-git/go-git/v5"
"github.com/stretchr/testify/assert"
)
func TestIsClean(t *testing.T) {
isClean, err := IsClean("/tmp")
assert.Equal(t, isClean, false)
assert.True(t, errors.Is(err, git.ErrRepositoryNotExists))
}

View File

@ -409,7 +409,7 @@ func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
}
func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
features, category, _, err := recipe.GetRecipeFeaturesAndCategory(r)
features, category, err := recipe.GetRecipeFeaturesAndCategory(r)
if err != nil {
return false, err
}

View File

@ -3,7 +3,9 @@ package log
import (
"os"
"strings"
"github.com/charmbracelet/lipgloss"
charmLog "github.com/charmbracelet/log"
)
@ -32,3 +34,42 @@ var SetLevel = Logger.SetLevel
var DebugLevel = charmLog.DebugLevel
var SetOutput = charmLog.SetOutput
var SetReportCaller = charmLog.SetReportCaller
func Styles() *charmLog.Styles {
styles := charmLog.DefaultStyles()
styles.Levels = map[charmLog.Level]lipgloss.Style{
charmLog.DebugLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(DebugLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("63")).
Foreground(lipgloss.Color("15")),
charmLog.InfoLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.InfoLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("86")).
Foreground(lipgloss.Color("16")),
charmLog.WarnLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.WarnLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("192")).
Foreground(lipgloss.Color("16")),
charmLog.ErrorLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.ErrorLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("204")).
Foreground(lipgloss.Color("15")),
charmLog.FatalLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.FatalLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("134")).
Foreground(lipgloss.Color("15")),
}
return styles
}

View File

@ -3,7 +3,6 @@ package recipe
import (
"fmt"
"os"
"slices"
"strings"
"coopcloud.tech/abra/pkg/formatter"
@ -14,20 +13,13 @@ import (
"github.com/go-git/go-git/v5/plumbing"
)
type EnsureContext struct {
Chaos bool
Offline bool
IgnoreEnvVersion bool
}
// Ensure makes sure the recipe exists, is up to date and has the specific
// version checked out.
func (r Recipe) Ensure(ctx EnsureContext) error {
// 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 ctx.Chaos {
if chaos {
return nil
}
@ -35,16 +27,15 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
return err
}
if !ctx.Offline {
if !offline {
if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if r.EnvVersion != "" && !ctx.IgnoreEnvVersion {
log.Debugf("ensuring env version %s", r.EnvVersion)
if _, err := r.EnsureVersion(r.EnvVersion); err != nil {
if r.Version != "" {
log.Debugf("ensuring version %s", r.Version)
if _, err := r.EnsureVersion(r.Version); err != nil {
return err
}
@ -61,6 +52,7 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
// 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
}
@ -73,41 +65,6 @@ func (r Recipe) EnsureExists() error {
return nil
}
// IsChaosCommit determines if a version sttring is a chaos commit or not.
func (r Recipe) IsChaosCommit(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 tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
if ref.Name().Short() == version {
tagRef = ref.Name()
}
return nil
}); err != nil {
return isChaosCommit, err
}
if tagRef.String() == "" {
isChaosCommit = true
}
return isChaosCommit, nil
}
// EnsureVersion checks whether a specific version exists for a recipe.
func (r Recipe) EnsureVersion(version string) (bool, error) {
isChaosCommit := false
@ -180,7 +137,8 @@ func (r Recipe) EnsureIsClean() error {
}
if !isClean {
return fmt.Errorf("%s (%s) has locally unstaged changes?", r.Name, r.Dir)
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
return fmt.Errorf(msg, r.Name, r.Dir)
}
return nil
@ -272,23 +230,8 @@ func (r Recipe) EnsureUpToDate() error {
return nil
}
// IsDirty checks whether a recipe is dirty or not. N.B., if you call IsDirty
// from another Recipe method, you should propagate the pointer reference (*).
func (r *Recipe) IsDirty() error {
isClean, err := gitPkg.IsClean(r.Dir)
if err != nil {
return err
}
if !isClean {
r.Dirty = true
}
return nil
}
// ChaosVersion constructs a chaos mode recipe version.
func (r *Recipe) ChaosVersion() (string, error) {
func (r Recipe) ChaosVersion() (string, error) {
var version string
head, err := r.Head()
@ -298,10 +241,15 @@ func (r *Recipe) ChaosVersion() (string, error) {
version = formatter.SmallSHA(head.String())
if err := r.IsDirty(); err != nil {
isClean, err := gitPkg.IsClean(r.Dir)
if err != nil {
return version, err
}
if !isClean {
version = fmt.Sprintf("%s+U", version)
}
return version, nil
}
@ -351,26 +299,23 @@ func (r Recipe) Tags() ([]string, error) {
}
// GetRecipeVersions retrieves all recipe versions.
func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
var warnMsg []string
func (r Recipe) GetRecipeVersions() (RecipeVersions, error) {
versions := RecipeVersions{}
log.Debugf("git: opening repository in %s", r.Dir)
log.Debugf("attempting to open git repository in %s", r.Dir)
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return versions, warnMsg, nil
return versions, err
}
worktree, err := repo.Worktree()
if err != nil {
return versions, warnMsg, nil
return versions, err
}
gitTags, err := repo.Tags()
if err != nil {
return versions, warnMsg, nil
return versions, err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
@ -388,7 +333,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
return err
}
log.Debugf("git checkout: %s in %s", ref.Name(), r.Dir)
log.Debugf("successfully checked out %s in %s", ref.Name(), r.Dir)
config, err := r.GetComposeConfig(nil)
if err != nil {
@ -412,7 +357,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
warnMsg = append(warnMsg, fmt.Sprintf("%s service is missing image tag?", path))
log.Warnf("%s service is missing image tag?", path)
continue
}
@ -426,26 +371,19 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
return nil
}); err != nil {
return versions, warnMsg, nil
return versions, err
}
_, err = gitPkg.CheckoutDefaultBranch(repo, r.Dir)
if err != nil {
return versions, warnMsg, nil
return versions, err
}
sortRecipeVersions(versions)
log.Debugf("collected %s for %s", versions, r.Dir)
var uniqueWarnings []string
for _, w := range warnMsg {
if !slices.Contains(uniqueWarnings, w) {
uniqueWarnings = append(uniqueWarnings, w)
}
}
return versions, uniqueWarnings, nil
return versions, nil
}
// Head retrieves latest HEAD metadata.

View File

@ -1,39 +0,0 @@
package recipe
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsDirty(t *testing.T) {
r := Get("abra-test-recipe")
if err := r.EnsureExists(); err != nil {
t.Fatal(err)
}
if err := r.IsDirty(); err != nil {
t.Fatal(err)
}
assert.False(t, r.Dirty)
fpath := filepath.Join(r.Dir, "foo.txt")
f, err := os.Create(fpath)
if err != nil {
t.Fatal(err)
}
defer f.Close()
defer t.Cleanup(func() {
os.Remove(fpath)
})
if err := r.IsDirty(); err != nil {
t.Fatal(err)
}
assert.True(t, r.Dirty)
}

View File

@ -2,12 +2,12 @@ package recipe
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"slices"
"sort"
"strconv"
"strings"
@ -20,7 +20,6 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/web"
"coopcloud.tech/tagcmp"
"github.com/go-git/go-git/v5"
)
// RecipeCatalogueURL is the only current recipe catalogue available.
@ -58,6 +57,11 @@ type RecipeMeta struct {
Website string `json:"website"`
}
// TopicMeta represents a list of topics for a repository.
type TopicMeta struct {
Topics []string `json:"topics"`
}
// LatestVersion returns the latest version of a recipe.
func (r RecipeMeta) LatestVersion() string {
var version string
@ -119,20 +123,6 @@ type Features struct {
SSO string `json:"sso"`
}
func GetEnvVersionRaw(name string) (string, error) {
var version string
if strings.Contains(name, ":") {
split := strings.Split(name, ":")
if len(split) > 2 {
return version, fmt.Errorf("version seems invalid: %s", name)
}
version = split[1]
}
return version, nil
}
func Get(name string) Recipe {
version := ""
if strings.Contains(name, ":") {
@ -141,16 +131,11 @@ func Get(name string) Recipe {
log.Fatalf("version seems invalid: %s", name)
}
name = split[0]
version = split[1]
if strings.HasSuffix(version, config.DIRTY_DEFAULT) {
version = strings.Replace(split[1], config.DIRTY_DEFAULT, "", 1)
log.Debugf("removed dirty suffix from .env version: %s -> %s", split[1], version)
}
}
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, name)
sshURL := fmt.Sprintf(config.RECIPES_SSH_URL_TEMPLATE, name)
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, name)
if strings.Contains(name, "/") {
u, err := url.Parse(name)
if err != nil {
@ -166,33 +151,26 @@ func Get(name string) Recipe {
dir := path.Join(config.RECIPES_DIR, escapeRecipeName(name))
r := Recipe{
Name: name,
EnvVersion: version,
Dir: dir,
GitURL: gitURL,
SSHURL: sshURL,
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"),
}
if err := r.IsDirty(); err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
log.Fatalf("failed to check git status of %s: %s", r.Name, err)
}
return r
}
type Recipe struct {
Name string
EnvVersion string
Dirty bool // NOTE(d1): git terminology for unstaged changes
Dir string
GitURL string
SSHURL string
Name string
Version string
Dir string
GitURL string
SSHURL string
ComposePath string
ReadmePath string
@ -200,21 +178,6 @@ type Recipe struct {
AbraShPath string
}
// String outputs a human-friendly string representation.
func (r Recipe) String() string {
out := fmt.Sprintf("{name: %s, ", r.Name)
out += fmt.Sprintf("version : %s, ", r.EnvVersion)
out += fmt.Sprintf("dirty: %v, ", r.Dirty)
out += fmt.Sprintf("dir: %s, ", r.Dir)
out += fmt.Sprintf("git url: %s, ", r.GitURL)
out += fmt.Sprintf("ssh url: %s, ", r.SSHURL)
out += fmt.Sprintf("compose: %s, ", r.ComposePath)
out += fmt.Sprintf("readme: %s, ", r.ReadmePath)
out += fmt.Sprintf("sample env: %s, ", r.SampleEnvPath)
out += fmt.Sprintf("abra.sh: %s}", r.AbraShPath)
return out
}
func escapeRecipeName(recipeName string) string {
recipeName = strings.ReplaceAll(recipeName, "/", "_")
recipeName = strings.ReplaceAll(recipeName, ".", "_")
@ -233,18 +196,16 @@ func GetRecipesLocal() ([]string, error) {
return recipes, nil
}
func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, []string, error) {
var (
category string
warnMsgs []string
feat = Features{}
)
func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, error) {
feat := Features{}
log.Debugf("%s: attempt recipe metadata parse", r.ReadmePath)
var category string
log.Debugf("attempting to open %s for recipe metadata parsing", r.ReadmePath)
readmeFS, err := ioutil.ReadFile(r.ReadmePath)
if err != nil {
return feat, category, warnMsgs, err
return feat, category, err
}
readmeMetadata, err := GetStringInBetween( // Find text between delimiters
@ -253,7 +214,7 @@ func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, []string, error)
"<!-- metadata -->", "<!-- endmetadata -->",
)
if err != nil {
return feat, category, warnMsgs, err
return feat, category, err
}
readmeLines := strings.Split( // Array item from lines
@ -297,25 +258,20 @@ func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, []string, error)
)
}
if strings.Contains(val, "**Image**") {
imageMetadata, warnings, err := GetImageMetadata(strings.TrimSpace(
imageMetadata, err := GetImageMetadata(strings.TrimSpace(
strings.TrimPrefix(val, "* **Image**:"),
), r.Name)
if err != nil {
continue
}
if len(warnings) > 0 {
warnMsgs = append(warnMsgs, warnings...)
}
feat.Image = imageMetadata
}
}
return feat, category, warnMsgs, nil
return feat, category, nil
}
func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error) {
var warnMsgs []string
func GetImageMetadata(imageRowString, recipeName string) (Image, error) {
img := Image{}
imgFields := strings.Split(imageRowString, ",")
@ -326,18 +282,11 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error
if len(imgFields) < 3 {
if imageRowString != "" {
warnMsgs = append(
warnMsgs,
fmt.Sprintf("%s: image meta has incorrect format: %s", recipeName, imageRowString),
)
log.Warnf("%s image meta has incorrect format: %s", recipeName, imageRowString)
} else {
warnMsgs = append(
warnMsgs,
fmt.Sprintf("%s: image meta is empty?", recipeName),
)
log.Warnf("%s image meta is empty?", recipeName)
}
return img, warnMsgs, nil
return img, nil
}
img.Rating = imgFields[1]
@ -347,17 +296,17 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error
imageName, err := GetStringInBetween(recipeName, imgString, "[", "]")
if err != nil {
return img, warnMsgs, err
log.Fatal(err)
}
img.Image = strings.ReplaceAll(imageName, "`", "")
imageURL, err := GetStringInBetween(recipeName, imgString, "(", ")")
if err != nil {
return img, warnMsgs, err
log.Fatal(err)
}
img.URL = imageURL
return img, warnMsgs, nil
return img, nil
}
// GetStringInBetween returns empty string if no start or end string found
@ -548,11 +497,11 @@ type InternalTracker struct {
type RepoCatalogue map[string]RepoMeta
// ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea.
func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
func ReadReposMetadata() (RepoCatalogue, error) {
reposMeta := make(RepoCatalogue)
pageIdx := 1
bar := formatter.CreateProgressbar(-1, "collecting recipe listing")
bar := formatter.CreateProgressbar(-1, "retrieving recipe repos list from git.coopcloud.tech...")
for {
var reposList []RepoMeta
@ -565,32 +514,28 @@ func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
}
if len(reposList) == 0 {
if !debug {
bar.Add(1)
}
bar.Add(1)
break
}
for idx, repo := range reposList {
// NOTE(d1): the "example" recipe is a temporary special case
// https://git.coopcloud.tech/toolshed/organising/issues/666
if repo.Name == "example" {
continue
var topicMeta TopicMeta
topicsURL := getReposTopicUrl(repo.Name)
if err := web.ReadJSON(topicsURL, &topicMeta); err != nil {
return reposMeta, err
}
reposMeta[repo.Name] = reposList[idx]
if slices.Contains(topicMeta.Topics, "recipe") && repo.Name != "example" {
reposMeta[repo.Name] = reposList[idx]
}
}
pageIdx++
if !debug {
bar.Add(1)
}
bar.Add(1)
}
if err := bar.Close(); err != nil {
return reposMeta, err
}
fmt.Println() // newline for spinner
return reposMeta, nil
}
@ -652,7 +597,7 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
}
// UpdateRepositories clones and updates all recipe repositories locally.
func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) error {
func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
var barLength int
if recipeName != "" {
barLength = 1
@ -660,9 +605,9 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
barLength = len(repos)
}
cloneLimiter := limit.New(3)
cloneLimiter := limit.New(10)
retrieveBar := formatter.CreateProgressbar(barLength, "retrieving recipes")
retrieveBar := formatter.CreateProgressbar(barLength, "ensuring recipes are cloned & up-to-date...")
ch := make(chan string, barLength)
for _, repoMeta := range repos {
go func(rm RepoMeta) {
@ -671,9 +616,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
if recipeName != "" && recipeName != rm.Name {
ch <- rm.Name
if !debug {
retrieveBar.Add(1)
}
retrieveBar.Add(1)
return
}
@ -682,9 +625,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
}
ch <- rm.Name
if !debug {
retrieveBar.Add(1)
}
retrieveBar.Add(1)
}(repoMeta)
}
@ -692,13 +633,14 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
<-ch // wait for everything
}
if err := retrieveBar.Close(); err != nil {
return err
}
return nil
}
// getReposTopicUrl retrieves the repository specific topic listing.
func getReposTopicUrl(repoName string) string {
return fmt.Sprintf("https://git.coopcloud.tech/api/v1/repos/coop-cloud/%s/topics", repoName)
}
// ensurePathExists ensures that a path exists.
func ensurePathExists(path string) error {
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {

View File

@ -33,7 +33,7 @@ func TestGet(t *testing.T) {
name: "foo:1.2.3",
recipe: Recipe{
Name: "foo",
EnvVersion: "1.2.3",
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",
@ -60,7 +60,7 @@ func TestGet(t *testing.T) {
name: "mygit.org/myorg/cool-recipe:1.2.4",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
EnvVersion: "1.2.4",
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",
@ -105,16 +105,3 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
assert.NotEqual(t, label, defaultTimeoutLabel)
}
}
func TestDirtyMarkerRemoved(t *testing.T) {
r := Get("abra-test-recipe:1e83340e+U")
assert.Equal(t, "1e83340e", r.EnvVersion)
}
func TestGetEnvVersionRaw(t *testing.T) {
v, err := GetEnvVersionRaw("abra-test-recipe:1e83340e+U")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "1e83340e+U", v)
}

View File

@ -13,7 +13,6 @@ import (
stdlibErr "errors"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/convert"
"github.com/docker/cli/cli/command/service/progress"
@ -107,22 +106,13 @@ type DeployMeta struct {
ChaosVersion string // the --chaos deployment version
}
func (d DeployMeta) String() string {
var out string
out += fmt.Sprintf("{isDeployed: %v, ", d.IsDeployed)
out += fmt.Sprintf("version: %s, ", d.Version)
out += fmt.Sprintf("isChaos: %v, ", d.IsChaos)
out += fmt.Sprintf("chaosVersion: %s}", d.ChaosVersion)
return out
}
// IsDeployed gathers metadata about an app deployment.
func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) (DeployMeta, error) {
deployMeta := DeployMeta{
IsDeployed: false,
Version: "unknown",
IsChaos: false,
ChaosVersion: config.CHAOS_DEFAULT,
ChaosVersion: "false", // NOTE(d1): match string type used on label
}
filter := filters.NewArgs()

View File

@ -1,9 +1,9 @@
#!/usr/bin/env bash
ABRA_VERSION="0.9.0-beta"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.10.0-rc1-beta"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.8.0-rc1-beta"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do
if [ "$arg" == "--rc" ]; then
@ -40,7 +40,7 @@ function install_abra_release {
if ! type "wget" > /dev/null 2>&1; then
echo "'wget' is not installed, cannot proceed..."
echo "perhaps try installing manually via the releases URL?"
echo "https://git.coopcloud.tech/toolshed/abra/releases"
echo "https://git.coopcloud.tech/coop-cloud/abra/releases"
exit 1
fi

View File

@ -42,7 +42,7 @@ echo "========================================================================"
echo "CLONING ABRA"
echo "========================================================================"
rm -rf abra
git clone ssh://git@git.coopcloud.tech:2222/toolshed/abra.git
git clone ssh://git@git.coopcloud.tech:2222/coop-cloud/abra.git
cd abra
git checkout main
echo "========================================================================"

View File

@ -50,9 +50,6 @@ teardown(){
assert_failure
assert_output --partial 'locally unstaged changes'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@ -65,9 +62,6 @@ teardown(){
run $ABRA app check "$TEST_APP_DOMAIN" --chaos
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}

View File

@ -53,9 +53,6 @@ teardown(){
assert_failure
assert_output --partial 'locally unstaged changes'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@ -69,9 +66,6 @@ teardown(){
assert_success
assert_output --partial 'baz'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}

View File

@ -24,9 +24,6 @@ teardown(){
_rm_remote "/etc/*.txt"
_rm "$BATS_TMPDIR/mydir"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "validate app argument" {
@ -37,42 +34,6 @@ teardown(){
assert_failure
}
@test "bail if unstaged changes and no --chaos" {
_mkdir "$BATS_TMPDIR/mydir"
_mkfile "$BATS_TMPDIR/mydir/myfile.txt" "foo"
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/mydir" app:/etc
assert_failure
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "do not bail if unstaged changes and --chaos" {
_mkdir "$BATS_TMPDIR/mydir"
_mkfile "$BATS_TMPDIR/mydir/myfile.txt" "foo"
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/mydir" app:/etc --chaos
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "error if missing src/dest arguments" {
run $ABRA app cp "$TEST_APP_DOMAIN"
assert_failure

View File

@ -21,8 +21,8 @@ setup(){
teardown(){
_reset_recipe
_undeploy_app
_reset_app
_undeploy_app
_reset_tags
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
@ -46,9 +46,6 @@ teardown(){
assert_success
assert_output --partial 'foo'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_failure
assert_output --partial 'locally unstaged changes'
@ -65,12 +62,10 @@ teardown(){
assert_success
assert_output --partial 'foo'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--chaos --no-input --no-converge-checks
assert_success
assert_output --partial 'NEW CHAOS'
}
# bats test_tags=slow
@ -133,6 +128,7 @@ teardown(){
--no-input --no-converge-checks --chaos
assert_success
assert_output --partial "${wantHash:0:8}"
assert_output --partial 'NEW CHAOS'
}
# bats test_tags=slow
@ -167,32 +163,6 @@ teardown(){
assert_output --partial 'already deployed'
}
@test "no re-deploy after chaos deploy without --force/--chaos" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_failure
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force
assert_success
}
@test "no re-deploy without --force" {
_deploy_app
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_failure
}
# bats test_tags=slow
@test "re-deploy deployed app if --force/--chaos" {
_deploy_app
@ -334,6 +304,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure
assert_output --partial 'unable to deploy, secrets not generated'
}
# bats test_tags=slow
@ -369,72 +340,10 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \
--no-input --no-converge-checks
assert_success
refute_output --partial 'no such file or directory'
_undeploy_app
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all
assert_success
}
# bats test_tags=slow
@test "chaos version label includes dirty marker" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app labels "$TEST_APP_DOMAIN" --chaos
assert_success
assert_output --regexp 'chaos-version.*+U'
}
# bats test_tags=slow
@test "ignore env version checkout after deploy" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
assert_equal $(_get_current_hash) "$tagHash"
run $ABRA app check --ignore-env-version "$TEST_APP_DOMAIN"
assert_success
assert_equal $(_get_current_hash) "$(_get_head_hash)"
run $ABRA app check "$TEST_APP_DOMAIN"
assert_success
assert_equal $(_get_current_hash) "$tagHash"
}
# bats test_tags=slow
@test "ignore env version on new deploy" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
_undeploy_app
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --ignore-env-version
assert_success
assert_output --partial "$latestRelease"
}
# bats test_tags=slow
@test "no chaos version label if no chaos" {
_deploy_app
run $ABRA app labels "$TEST_APP_DOMAIN" --no-input
assert_success
refute_output --regexp "coop-cloud.abra-test-recipe.$TEST_SERVER.chaos-version"
}

View File

@ -91,27 +91,9 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --force --debug
assert_success
assert_output --partial "overriding env file version"
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "deploy overwrites chaos deploy" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "1e83340e" \
--no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:1e83340e" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--force --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:1e83340e" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_failure
}

View File

@ -1,320 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_reset_recipe
_undeploy_app
_reset_app
_reset_tags
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "first deploy with empty env version" {
latestRelease=$(_latest_release)
_wipe_env_version
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
# current deployment
assert_output --regexp 'VERSION.*N/A'
assert_output --regexp 'CHAOS.*false'
# new deployment
assert_output --regexp 'VERSION.* ' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*N/A'
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "first deploy to latest version" {
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
# current deployment
assert_output --regexp 'VERSION.*N/A'
assert_output --regexp 'CHAOS.*false'
# new deployment
assert_output --regexp 'VERSION.* ' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "deploy, re-deploy, choose env version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \
--no-input --no-converge-checks
assert_success
_undeploy_app
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
# current deployment
assert_output --regexp 'VERSION.*N/A'
assert_output --regexp 'CHAOS.*false'
# new deployment
assert_output --regexp 'VERSION.*' + "0.1.1+1.20.2"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.1.1+1.20.2"
assert_output --regexp 'NEW VERSION.*' + "0.1.1+1.20.2"
run grep -q "TYPE=$TEST_RECIPE:0.1.1+1.20.2" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "deploy, re-deploy, ignore env version" {
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \
--no-input --no-converge-checks
assert_success
_undeploy_app
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --ignore-env-version
assert_success
# current deployment
assert_output --regexp 'VERSION.*N/A'
assert_output --regexp 'CHAOS.*false'
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.1.1+1.20.2"
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "deploy then chaos deploy" {
headHash=$(_get_head_hash)
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}+U"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}+U"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "chaos deploy then force deploy" {
headHash=$(_get_head_hash)
latestRelease=$(_latest_release)
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}+U"
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${headHash:0:8}+U"
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "deploy then force chaos commit deploy" {
headHash=$(_get_head_hash)
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" "${headHash:0:8}" \
--no-input --no-converge-checks --force
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}"
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "chaos deploy then chaos deploy with unstaged" {
headHash=$(_get_head_hash)
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
# current deployment
assert_output --regexp 'VERSION.*N/A'
assert_output --regexp 'CHAOS.*false'
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}"
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}"
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}+U"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}+U"
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "hash deploy then force deploy" {
headHash=$(_get_head_hash)
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" "${headHash:0:8}" \
--no-input --no-converge-checks
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}"
# new deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${headHash:0:8}"
assert_output --regexp 'NEW VERSION.*' + "${latestRelease}"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -27,18 +27,18 @@ teardown(){
# bats test_tags=slow
@test "deploy remote recipe" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/toolshed\/abra-test-recipe/g' \
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "git.coopcloud.tech/toolshed/abra-test-recipe"
assert_output --partial "git.coopcloud.tech/coop-cloud/abra-test-recipe"
}
# bats test_tags=slow
@test "deploy remote recipe with version" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/toolshed\/abra-test-recipe:0.2.0+1.21.0/g' \
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
@ -49,7 +49,7 @@ teardown(){
# bats test_tags=slow
@test "deploy remote recipe with chaos commit" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/toolshed\/abra-test-recipe:1e83340e/g' \
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:1e83340e/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
@ -60,14 +60,14 @@ teardown(){
# bats test_tags=slow
@test "remote recipe version written to env" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/toolshed\/abra-test-recipe/g' \
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=git.coopcloud.tech\/toolshed\/abra-test-recipe:$(_latest_release)" \
run grep -q "TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:$(_latest_release)" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -1,59 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_reset_recipe
_undeploy_app
_reset_app
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "validate app argument" {
run $ABRA app env
assert_failure
run $ABRA app env DOESNTEXIST
assert_failure
}
@test "show env version" {
latestRelease=$(_latest_release)
run $ABRA app env "$TEST_APP_DOMAIN"
assert_success
assert_output --partial "$latestRelease"
}
@test "show env version despite --chaos" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app env "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}

View File

@ -1,109 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_reset_recipe
_undeploy_app
_reset_app
_reset_tags
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "validate app argument" {
run $ABRA app labels
assert_failure
run $ABRA app labels DOESNTEXIST
assert_failure
}
@test "bail if unstaged changes and no --chaos" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run $ABRA app labels "$TEST_APP_DOMAIN" --no-input
assert_failure
}
@test "do not bail if unstaged changes and --chaos" {
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
assert_success
assert_output --partial 'foo'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run $ABRA app labels "$TEST_APP_DOMAIN" --chaos
assert_success
}
@test "ensure recipe up to date if no --offline" {
wantHash=$(_get_n_hash 3)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
assert_success
assert_equal $(_get_current_hash) "$wantHash"
run $ABRA app labels "$TEST_APP_DOMAIN"
assert_success
assert_equal $(_get_head_hash) $(_get_current_hash)
}
@test "ensure recipe not up to date if --offline" {
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
assert_success
run $ABRA app labels "$TEST_APP_DOMAIN" --offline
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
}
# bats test_tags=slow
@test "show deploy labels when deployed" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
run $ABRA app labels "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'com.docker.stack.image'
}

View File

@ -20,10 +20,6 @@ setup(){
teardown(){
_rm_app
_reset_recipe
_reset_tags
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@test "create new app" {
@ -51,22 +47,25 @@ teardown(){
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_equal $(_get_tag_hash 0.3.0+1.21.0) $(_get_current_hash)
run grep -q "TYPE=$TEST_RECIPE:0.3.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "create new app with chaos commit" {
tagHash=$(_get_tag_hash "0.3.0+1.21.0")
run $ABRA app new "$TEST_RECIPE" "$tagHash" \
run $ABRA app new "$TEST_RECIPE" 1e83340e \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run grep -q "TYPE=$TEST_RECIPE:${tagHash}" \
currentHash=$(_get_current_hash)
assert_equal 1e83340e ${currentHash:0:8}
run grep -q "TYPE=$TEST_RECIPE:1e83340e" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@ -102,9 +101,6 @@ teardown(){
assert_failure
assert_output --partial 'locally unstaged changes'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@ -126,9 +122,6 @@ teardown(){
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@ -174,8 +167,6 @@ teardown(){
# bats test_tags=slow
@test "generate secrets" {
latestRelease=$(_latest_release)
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
@ -187,64 +178,4 @@ teardown(){
run $ABRA app secret ls "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'test_pass_one'
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "app new from chaos recipe" {
currentHash=$(_get_current_hash)
latestRelease=$(_latest_release)
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \
--secrets \
--chaos
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_output --partial "version: $latestRelease"
assert_output --partial "chaos: ${currentHash:0:8}"
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run grep -q "TYPE=$TEST_RECIPE:${currentHash:0:8}+U" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "app new, no releases, from chaos recipe" {
currentHash=$(_get_current_hash)
_remove_tags
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN" \
--secrets \
--chaos
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_output --partial "version: unknown"
assert_output --partial "chaos: ${currentHash:0:8}"
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run grep -q "TYPE=$TEST_RECIPE:${currentHash:0:8}+U" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -55,9 +55,6 @@ teardown(){
assert_failure
assert_output --partial 'locally unstaged changes'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@ -73,9 +70,6 @@ teardown(){
run $ABRA app ps --chaos "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@ -123,8 +117,6 @@ teardown(){
@test "show ps report" {
_deploy_app
_ensure_env_version "$(_latest_release)"
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'app'
@ -145,16 +137,3 @@ teardown(){
assert_output --partial "$latestRelease"
assert_output --partial "${headHash:0:8}" # is a chaos deploy
}
# bats test_tags=slow
@test "ensure live chaos commit is shown" {
headHash=$(_get_head_hash)
run $ABRA app deploy "$TEST_APP_DOMAIN" "0f5a0570" --no-input
assert_success
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_success
assert_output --partial "0f5a0570" # is not latest HEAD
refute_output --partial "${headHash:0:8}"
}

View File

@ -19,7 +19,6 @@ setup(){
}
teardown(){
_reset_app
_undeploy_app
_reset_recipe
}
@ -130,17 +129,6 @@ teardown(){
assert_output --partial "0.1.0+1.20.0"
}
@test "force rollback with no available downgrades" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
run $ABRA app rollback "$TEST_APP_DOMAIN" \
--force --no-input --no-converge-checks
assert_success
assert_output --partial "0.1.0+1.20.0"
}
# bats test_tags=slow
@test "rollback to a version 2 tags behind" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks
@ -186,16 +174,3 @@ teardown(){
assert_failure
assert_output --partial "not a known version"
}
# bats test_tags=slow
@test "no chaos version label if no chaos" {
_deploy_app
run $ABRA app rollback "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
run $ABRA app labels "$TEST_APP_DOMAIN" --no-input
assert_success
refute_output --regexp "coop-cloud.abra-test-recipe.$TEST_SERVER.chaos-version"
}

View File

@ -24,9 +24,9 @@ teardown(){
}
@test "rollback writes version to env file" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks
assert_success
assert_output --partial "0.2.0+1.21.0"
run grep -q "TYPE=abra-test-recipe:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
@ -35,6 +35,8 @@ teardown(){
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks --debug
assert_success
assert_output --partial "0.1.0+1.20.0"
assert_output --partial "overriding env file version"
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"

View File

@ -1,102 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_undeploy_app
_reset_app
_reset_recipe
}
@test "deploy then rollback" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks
assert_success
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# rollback
assert_output --regexp 'VERSION.*' + "0.1.0+1.20.0"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'NEW VERSION.*' + "0.1.0+1.20.0"
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "force rollback" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks
assert_success
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --force
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# rollback
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "app rollback no .env version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks
assert_success
_wipe_env_version
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# rollback
assert_output --regexp 'VERSION.*' + "0.1.0+1.20.0"
# env version
assert_output --regexp 'CURRENT VERSION.*N/A'
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -131,9 +131,6 @@ teardown(){
assert_failure
assert_output --partial 'locally unstaged changes'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@ -274,9 +271,6 @@ teardown(){
assert_failure
assert_output --partial 'locally unstaged changes'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}
@ -325,9 +319,6 @@ teardown(){
assert_failure
assert_output --partial 'locally unstaged changes'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}

View File

@ -1,102 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_reset_recipe
_undeploy_app
_reset_app
}
@test "deploy and undeploy" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "0.1.0+1.20.0"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.1.0+1.20.0"
assert_output --regexp 'NEW VERSION.*' + "0.1.0+1.20.0"
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "chaos deploy and undeploy" {
headHash=$(_get_head_hash)
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}"
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "chaos deploy with unstaged commits and undeploy" {
headHash=$(_get_head_hash)
latestRelease=$(_latest_release)
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "${latestRelease}"
assert_output --regexp 'CHAOS.*' + "${headHash:0:8}+U"
# env version
assert_output --regexp 'CURRENT VERSION.*' + "${latestRelease}"
assert_output --regexp 'NEW VERSION.*' + "${headHash:0:8}+U"
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}

View File

@ -33,16 +33,6 @@ teardown(){
assert_failure
}
@test "retrieve recipe if missing" {
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
}
# bats test_tags=slow
@test "error if specific version is not an upgrade" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
@ -128,19 +118,6 @@ teardown(){
assert_output --partial '0.2.0+1.21.0'
}
@test "force upgrade with no available upgrades" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
latestRelease=$(_latest_release)
run $ABRA app upgrade "$TEST_APP_DOMAIN" \
--force --no-input --no-converge-checks
assert_success
assert_output --partial "$latestRelease"
}
# bats test_tags=slow
@test "upgrade to latest" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
@ -239,16 +216,3 @@ teardown(){
assert_failure
assert_output --partial "not a known version"
}
# bats test_tags=slow
@test "no chaos version label if no chaos" {
_deploy_app
run $ABRA app upgrade "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_success
run $ABRA app labels "$TEST_APP_DOMAIN" --no-input
assert_success
refute_output --regexp "coop-cloud.abra-test-recipe.$TEST_SERVER.chaos-version"
}

View File

@ -22,19 +22,20 @@ teardown(){
_reset_recipe
}
# bats test_tags=slow
@test "upgrade writes version to env file" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
assert_output --partial '0.1.0+1.20.0'
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks
--no-input --no-converge-checks --debug
assert_success
assert_output --partial "0.2.0+1.21.0"
assert_output --partial "overriding env file version"
run grep -q "TYPE=abra-test-recipe:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"

View File

@ -1,105 +0,0 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_undeploy_app
_reset_recipe
}
@test "deploy then upgrade" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "0.1.0+1.20.0"
assert_output --regexp 'CHAOS.*false'
# upgrade
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.1.0+1.20.0"
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "force upgrade" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks
assert_success
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --force
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# upgrade
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "app upgrade no .env version" {
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks
assert_success
_wipe_env_version
run $ABRA app upgrade "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force
assert_success
# current deployment
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# upgrade
assert_output --regexp 'VERSION.*' + "0.2.0+1.21.0"
assert_output --regexp 'CHAOS.*false'
# env version
assert_output --regexp 'CURRENT VERSION.*N/A'
assert_output --regexp 'NEW VERSION.*' + "0.2.0+1.21.0"
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -6,13 +6,9 @@ setup(){
}
# bats test_tags=slow
@test "generate catalogue" {
@test "generate entire catalogue" {
run $ABRA catalogue generate
assert_success
for d in $(ls $ABRA_DIR/recipes); do
assert_exists "$ABRA_DIR/recipes/$d/.git"
done
}
@test "error if unstaged changes" {
@ -45,5 +41,4 @@ setup(){
@test "generate only specific recipe" {
run $ABRA catalogue generate gitea
assert_success
assert_exists "$ABRA_DIR/recipes/gitea/.git"
}

View File

@ -60,7 +60,3 @@ _get_current_hash() {
_get_n_hash() {
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show -s --format="%H" "HEAD~$1")
}
_git_status() {
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status --porcelain)
}

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
_latest_release(){
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l --sort=v:refname | tail -n 1)
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
}
_fetch_recipe() {
@ -9,7 +9,7 @@ _fetch_recipe() {
run mkdir -p "$ABRA_DIR/recipes"
assert_success
run git clone "https://git.coopcloud.tech/toolshed/$TEST_RECIPE" "$ABRA_DIR/recipes/$TEST_RECIPE"
run git clone "https://git.coopcloud.tech/coop-cloud/$TEST_RECIPE" "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
fi
}
@ -22,9 +22,18 @@ _reset_recipe(){
_fetch_recipe
}
_ensure_latest_version(){
latestRelease=$(_latest_release)
if [ ! $latestRelease = "$1" ]; then
echo "expected latest recipe version of '$1', saw: $latestRelease"
return 1
fi
}
_ensure_catalogue(){
if [[ ! -d "$ABRA_DIR/catalogue" ]]; then
run git clone https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git $ABRA_DIR/catalogue
run git clone https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git $ABRA_DIR/catalogue
assert_success
fi
}

View File

@ -28,6 +28,8 @@ teardown(){
# bats test_tags=slow
@test "install release candidate from script" {
skip "current RC is brokenly specified in the installer script"
run bash -c 'curl https://install.abra.coopcloud.tech | bash -s -- --rc'
assert_success

View File

@ -41,9 +41,6 @@ teardown(){
assert_success
assert_output --partial 'foo'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run $ABRA recipe lint "$TEST_RECIPE"
assert_failure
assert_output --partial 'locally unstaged changes'
@ -61,9 +58,6 @@ teardown(){
assert_success
assert_output --partial 'foo'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run $ABRA recipe lint "$TEST_RECIPE" --chaos
assert_success

View File

@ -35,10 +35,6 @@ teardown(){
assert_success
assert_output --partial "new recipe 'foobar' created"
assert_exists "$ABRA_DIR/recipes/foobar"
run git -C "$ABRA_DIR/recipes/foobar" rev-parse --abbrev-ref HEAD
assert_success
assert_output "main"
}
# bats test_tags=slow

View File

@ -20,7 +20,6 @@ setup(){
teardown() {
_reset_recipe
_reset_tags
}
@test "validate recipe argument" {
@ -32,6 +31,8 @@ teardown() {
}
@test "release patch bump" {
_ensure_latest_version "0.3.0+1.21.0"
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
assert_success
@ -39,12 +40,6 @@ teardown() {
assert_success
assert_output --partial 'image: nginx:1.21.6'
# NOTE(d1): ensure the latest tag is the one we expect
_remove_tags
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
assert_success
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
assert_success
assert_output --partial 'synced label'
@ -63,6 +58,8 @@ teardown() {
}
@test "release minor bump" {
_ensure_latest_version "0.3.0+1.21.0"
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
assert_success
@ -70,12 +67,6 @@ teardown() {
assert_success
assert_output --regexp 'image: nginx:1.2.*'
# NOTE(d1): ensure the latest tag is the one we expect
_remove_tags
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
assert_success
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
assert_success
assert_output --partial 'synced label'
@ -111,6 +102,8 @@ teardown() {
}
@test "release with next release note" {
_ensure_latest_version "0.3.0+1.21.0"
_mkfile "$ABRA_DIR/recipes/$TEST_RECIPE/release/next" "those are some release notes for the next release"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" add release/next

View File

@ -40,9 +40,6 @@ teardown(){
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "M compose.yml ?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
@ -61,6 +58,8 @@ teardown(){
}
@test "sync patch label bump" {
_ensure_latest_version "0.3.0+1.21.0"
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
assert_success
@ -68,12 +67,6 @@ teardown(){
assert_success
assert_output --partial 'image: nginx:1.21.6'
# NOTE(d1): ensure the latest tag is the one we expect
_remove_tags
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
assert_success
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
assert_success
@ -83,6 +76,8 @@ teardown(){
}
@test "sync minor label bump" {
_ensure_latest_version "0.3.0+1.21.0"
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
assert_success
@ -90,12 +85,6 @@ teardown(){
assert_success
assert_output --regexp 'image: nginx:1.2.*'
# NOTE(d1): ensure the latest tag is the one we expect
_remove_tags
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
assert_success
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
assert_success

View File

@ -54,9 +54,6 @@ teardown(){
assert_failure
assert_output --partial 'locally unstaged changes'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
}

View File

@ -29,6 +29,8 @@ teardown(){
# bats test_tags=slow
@test "abra upgrade release candidate" {
skip "TODO: RC publishing broke somehow, needs investigation"
run $ABRA upgrade --rc
assert_success
assert_output --partial 'Public interest infrastructure'

View File

@ -1,3 +1,3 @@
RECIPE=ecloud
DOMAIN=ecloud.evil.corp
SMTP_AUTHTYPE=login
SMTP_AUTHTYPE=login

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