Compare commits

...

39 Commits

Author SHA1 Message Date
8af87aa382 chore: upgrade goreleaser
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-01-05 12:47:46 +01:00
ac0b9cd052 chore: new RC
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2025-01-05 12:42:42 +01:00
4923984e84 fix: not flaky catalogue generate
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See #464
2025-01-05 12:08:10 +01:00
2bc77de751 test: ensure main branch on new recipe 2025-01-05 10:38:34 +01:00
b3a2402cec chore: remove redundant logging 2025-01-05 10:38:24 +01:00
a773fd4256 chore: spacing 2025-01-05 10:38:13 +01:00
b1a0d54bd3 fix: default to main then master 2025-01-05 10:37:30 +01:00
3869d6bce9 Revert "test: try uppercase naming (following UI)"
All checks were successful
continuous-integration/drone/push Build is passing
This reverts commit 0ff07ab224.

Wrong UI, trying again via Drone.
2025-01-04 11:55:13 +01:00
0ff07ab224 test: try uppercase naming (following UI)
Some checks failed
continuous-integration/drone/push Build is failing
2025-01-04 11:47:33 +01:00
936c1b0626 fix: use new syntax
Some checks failed
continuous-integration/drone/push Build is failing
2025-01-04 11:20:17 +01:00
b576cba227 fix: use abra-bot
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-01-04 11:09:14 +01:00
d087f3debf chore: go mod tidy
Some checks failed
continuous-integration/drone Build is failing
2025-01-03 21:25:11 +01:00
e57a6d87a3 test: use recipes url 2025-01-03 20:35:19 +01:00
74b64099de fix: skip example && fix generate 2025-01-03 20:24:49 +01:00
354712ca46 fix: remove old docstring 2025-01-03 20:23:48 +01:00
81cdc843ec fix: coop-cloud -> toolshed 2025-01-03 20:23:27 +01:00
d2931e3af0 fix: drop warning, can use this now 2025-01-03 20:21:20 +01:00
b9f2d1f568 chore: go mod vendor / tidy 2025-01-03 20:21:06 +01:00
a379b31a19 refactor: dont use topics
See coop-cloud/organising#377
See coop-cloud/organising#569
2025-01-03 17:01:37 +00:00
17e15dba77 chore: spacing / wording on log message [ci skip] 2025-01-03 17:53:22 +01:00
1194f3b228 refactor!: maintain "domain"
All checks were successful
continuous-integration/drone/push Build is passing
See toolshed/organising#636
2025-01-03 08:24:03 +01:00
2dc8034c16 fix: no dot dirs for server selection
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-03 08:16:30 +01:00
c5ddeb2d8a fix: dont update catalogue on autocomplete 2025-01-03 08:10:57 +01:00
0a63f9ce27 fix: undeploy handles chaos/unstaged in overview
Follows 3a71dc47f8
2025-01-02 21:50:23 +01:00
3a71dc47f8 fix: more env version write tests
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#661
2025-01-02 21:20:40 +01:00
f07c64f7b8 fix: sort abra app env output
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-02 16:40:23 +01:00
dd03c40e10 feat: abra app env 2025-01-02 16:32:32 +01:00
48198d55bd chore: rename [ci skip] 2025-01-02 11:31:15 +01:00
c0931b96d8 fix: use same wording 2025-01-02 11:31:04 +01:00
64ea0f9684 test: drop, version is written on app new [ci skip] 2025-01-02 11:26:27 +01:00
b0cd8ccbb9 refactor/fix: deploy/upgrade/rollback
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See coop-cloud/abra#461
2025-01-02 11:12:38 +01:00
5975be6870 fix: unstaged changes handling
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
See toolshed/organising#651
2024-12-31 16:37:02 +01:00
bfed51a69c fix: no newline on status in logs
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-31 08:26:35 +01:00
5d0faf5e13 fix: only log once for the loaded app 2024-12-31 08:26:19 +01:00
cd6af9708c docs: <> -> [] 2024-12-31 08:26:01 +01:00
ef95bce1e4 fix: use default styles
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-30 18:10:01 +01:00
a159583874 chore: make format
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-30 18:07:58 +01:00
e3b0500875 fix: dont output error twice
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-30 18:05:26 +01:00
994310a4ff refactor!: use charm defaults 2024-12-30 18:05:04 +01:00
309 changed files with 186792 additions and 7633 deletions

View File

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

View File

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

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

View File

@ -1,7 +1,7 @@
# `abra` # `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) [![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/coop-cloud/abra)](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra) [![Go Report Card](https://goreportcard.com/badge/git.coopcloud.tech/toolshed/abra)](https://goreportcard.com/report/git.coopcloud.tech/toolshed/abra)
[![Go Reference](https://pkg.go.dev/badge/coopcloud.tech/abra.svg)](https://pkg.go.dev/coopcloud.tech/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 🎩🐇 The Co-op Cloud utility belt 🎩🐇

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import (
) )
var AppCmdCommand = &cobra.Command{ var AppCmdCommand = &cobra.Command{
Use: "command <app> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]", Use: "command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
Aliases: []string{"cmd"}, Aliases: []string{"cmd"},
Short: "Run app commands", Short: "Run app commands",
Long: `Run an app specific command. Long: `Run an app specific command.
@ -92,7 +92,7 @@ does not).`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -192,14 +192,14 @@ does not).`,
} }
var AppCmdListCommand = &cobra.Command{ var AppCmdListCommand = &cobra.Command{
Use: "list <app> [flags]", Use: "list <domain> [flags]",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Short: "List all available commands", Short: "List all available commands",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) 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 // `--` 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 // so we need to eumlate that as missing when testing if bash args are passed in
// see https://git.coopcloud.tech/coop-cloud/organising/issues/336 for more // see https://git.coopcloud.tech/toolshed/organising/issues/336 for more
{[]string{"foo.com", "app", "test"}, false, ""}, {[]string{"foo.com", "app", "test"}, false, ""},
{[]string{"foo.com", "app", "test", "foo"}, true, "foo "}, {[]string{"foo.com", "app", "test", "foo"}, true, "foo "},
{[]string{"foo.com", "app", "test", "foo", "bar", "baz"}, true, "foo bar baz "}, {[]string{"foo.com", "app", "test", "foo", "bar", "baz"}, true, "foo bar baz "},

View File

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

View File

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

View File

@ -2,10 +2,10 @@ package app
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
@ -18,18 +18,19 @@ import (
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppDeployCommand = &cobra.Command{ var AppDeployCommand = &cobra.Command{
Use: "deploy <app> [version] [flags]", Use: "deploy <domain> [version] [flags]",
Aliases: []string{"d"}, Aliases: []string{"d"},
Short: "Deploy an app", Short: "Deploy an app",
Long: `Deploy an app. Long: `Deploy an app.
This command supports chaos operations. Use "--chaos/-c" to deploy your recipe This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported values for "[version]". checkout as-is. Recipe commit hashes are also supported as values for
Please note, "upgrade"/"rollback" do not support chaos operations.`, "[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`,
Example: ` # standard deployment Example: ` # standard deployment
abra app deploy 1312.net abra app deploy 1312.net
@ -61,29 +62,20 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
} }
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string var (
deployWarnMessages []string
toDeployVersion string
isChaosCommit bool
toDeployChaosVersion = config.CHAOS_DEFAULT
)
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
stackName := app.StackName()
ok, err := validateChaosXORVersion(args) if err := validateArgsAndFlags(args); err != nil {
if !ok { log.Fatal(err)
log.Fatalf(err.Error())
} }
specificVersion := getSpecifiedVersion(args) if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
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) log.Fatal(err)
} }
@ -91,94 +83,66 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName) log.Debugf("checking whether %s is already deployed", app.StackName())
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// NOTE(d1): handles "<version> as git hash" use case
var isChaosCommit bool
// NOTE(d1): check out specific version before dealing with secrets. This
// is because we need to deal with GetComposeFiles under the hood and these
// files change from version to version which therefore affects which
// secrets might be generated
toDeployVersion := deployMeta.Version
if specificVersion != "" {
toDeployVersion = specificVersion
log.Debugf("choosing %s as version to deploy", toDeployVersion)
var err error
isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil {
log.Fatal(err)
}
if isChaosCommit {
log.Debugf("assuming '%s' is a chaos commit", toDeployVersion)
internal.Chaos = true
}
}
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 deployMeta.IsDeployed && !(internal.Force || internal.Chaos) { if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
log.Fatalf("%s is already deployed", app.Name) log.Fatalf("%s is already deployed", app.Name)
} }
if !internal.Chaos && specificVersion == "" { if len(args) == 2 && args[1] != "" {
versions, err := app.Recipe.Tags() toDeployVersion = args[1]
}
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 {
isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if len(versions) > 0 && !internal.Chaos { if isChaosCommit {
toDeployVersion = versions[len(versions)-1] log.Debugf("assuming chaos commit: %s", toDeployVersion)
log.Debugf("choosing %s as version to deploy", toDeployVersion)
if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil { internal.Chaos = true
log.Fatal(err) toDeployChaosVersion = toDeployVersion
}
} else { toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
head, err := app.Recipe.Head()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
toDeployVersion = formatter.SmallSHA(head.String())
} }
} }
toDeployChaosVersion := config.CHAOS_DEFAULT if err := validateSecrets(cl, app); err != nil {
if internal.Chaos { log.Fatal(err)
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)
}
}
} }
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
@ -194,6 +158,7 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
log.Fatal(err) log.Fatal(err)
} }
stackName := app.StackName()
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Composefiles: composeFiles, Composefiles: composeFiles,
Namespace: stackName, Namespace: stackName,
@ -206,10 +171,15 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
log.Fatal(err) log.Fatal(err)
} }
toDeployChaosVersionLabel := toDeployChaosVersion
if app.Recipe.Dirty {
toDeployChaosVersionLabel = formatter.AddDirtyMarker(toDeployChaosVersionLabel)
}
appPkg.ExposeAllEnv(stackName, compose, app.Env) appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion) appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersionLabel)
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app) envVars, err := appPkg.CheckEnv(app)
@ -219,23 +189,22 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
for _, envVar := range envVars { for _, envVar := range envVars {
if !envVar.Present { if !envVar.Present {
warnMessages = append(warnMessages, deployWarnMessages = append(deployWarnMessages,
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain), fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
) )
} }
} }
if !internal.NoDomainChecks { if !internal.NoDomainChecks {
domainName, ok := app.Env["DOMAIN"] if domainName, ok := app.Env["DOMAIN"]; ok {
if ok {
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil { if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
log.Debug("skipping domain checks as no DOMAIN=... configured for app") log.Debug("skipping domain checks, no DOMAIN=... configured")
} }
} else { } else {
log.Debug("skipping domain checks as requested") log.Debug("skipping domain checks")
} }
deployedVersion := config.NO_VERSION_DEFAULT deployedVersion := config.NO_VERSION_DEFAULT
@ -243,13 +212,20 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
deployedVersion = deployMeta.Version deployedVersion = deployMeta.Version
} }
toWriteVersion := toDeployVersion
if internal.Chaos || isChaosCommit {
toWriteVersion = toDeployChaosVersion
}
if err := internal.DeployOverview( if err := internal.DeployOverview(
app, app,
warnMessages, deployWarnMessages,
deployedVersion, deployedVersion,
deployMeta.ChaosVersion, deployMeta.ChaosVersion,
toDeployVersion, toDeployVersion,
toDeployChaosVersion); err != nil { toDeployChaosVersion,
toWriteVersion,
); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -257,7 +233,8 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil { if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil {
log.Fatal(err) log.Fatal(err)
@ -271,31 +248,77 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
} }
} }
app.Recipe.Version = toDeployVersion if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
if toDeployChaosVersion != config.CHAOS_DEFAULT { log.Fatalf("writing recipe version failed: %s", err)
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)
} }
}, },
} }
// validateChaosXORVersion xor checks version/chaos mode func getChaosVersion(app app.App, toDeployVersion, toDeployChaosVersion *string) error {
func validateChaosXORVersion(args []string) (bool, error) { var err error
if getSpecifiedVersion(args) != "" && internal.Chaos { *toDeployChaosVersion, err = app.Recipe.ChaosVersion()
return false, errors.New("cannot use <version> and --chaos together") if err != nil {
return err
} }
return true, nil
*toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
if err != nil {
return err
}
return nil
} }
// getSpecifiedVersion retrieves the specific version if available func getLatestVersionOrCommit(app app.App, toDeployVersion *string) error {
func getSpecifiedVersion(args []string) string { versions, err := app.Recipe.Tags()
if len(args) >= 2 { if err != nil {
return args[1] return err
} }
return ""
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
} }
func init() { func init() {
@ -328,6 +351,6 @@ func init() {
"no-converge-checks", "no-converge-checks",
"c", "c",
false, false,
"do not wait for converge logic checks", "disable converge logic checks",
) )
} }

View File

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

43
cli/app/env.go Normal file
View File

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

139
cli/app/labels.go Normal file
View File

@ -0,0 +1,139 @@
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{ var AppLogsCommand = &cobra.Command{
Use: "logs <app> [service] [flags]", Use: "logs <domain> [service] [flags]",
Aliases: []string{"l"}, Aliases: []string{"l"},
Short: "Tail app logs", Short: "Tail app logs",
Args: cobra.RangeArgs(1, 2), 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. appropriate server.
This command does not deploy your app for you. You will need to run "abra app This command does not deploy your app for you. You will need to run "abra app
deploy <app>" to do so. deploy <domain>" to do so.
You can see what recipes are available (i.e. values for the [recipe] argument) You can see what recipes are available (i.e. values for the [recipe] argument)
by running "abra recipe ls". by running "abra recipe ls".
@ -93,7 +93,7 @@ var AppNewCommand = &cobra.Command{
var recipeVersions recipePkg.RecipeVersions var recipeVersions recipePkg.RecipeVersions
if recipeVersion == "" { if recipeVersion == "" {
var err error var err error
recipeVersions, err = recipe.GetRecipeVersions() recipeVersions, _, err = recipe.GetRecipeVersions()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -201,8 +201,8 @@ var AppNewCommand = &cobra.Command{
log.Warnf( log.Warnf(
"secrets are %s shown again, please save them %s", "secrets are %s shown again, please save them %s",
formatter.BoldStyle.Render("NOT"), formatter.BoldUnderlineStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"), formatter.BoldUnderlineStyle.Render("NOW"),
) )
} }
@ -211,9 +211,8 @@ var AppNewCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("choosing %s as version to save to env file", recipeVersion)
if err := app.WriteRecipeVersion(recipeVersion, false); err != nil { if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err) log.Fatalf("writing recipe version failed: %s", err)
} }
}, },
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,30 +5,40 @@ import (
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppUpgradeCommand = &cobra.Command{ var AppUpgradeCommand = &cobra.Command{
Use: "upgrade <app> [version] [flags]", Use: "upgrade <domain> [version] [flags]",
Aliases: []string{"up"}, Aliases: []string{"up"},
Short: "Upgrade an app", Short: "Upgrade an app",
Long: `Upgrade an app. Long: `Upgrade an app.
Unlike "deploy", chaos operations are not supported here. Only recipe versions Unlike "abra app deploy", chaos operations are not supported here. Only recipe
are supported values for "[version]". 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.
An upgrade can be destructive, please ensure you have a copy of your app data An upgrade can be destructive, please ensure you have a copy of your app data
beforehand.`, beforehand. See "abra app backup" for more.`,
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -49,22 +59,26 @@ beforehand.`,
} }
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string var (
upgradeWarnMessages []string
chosenUpgrade string
availableUpgrades []string
upgradeReleaseNotes string
)
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
stackName := app.StackName()
var specificVersion string if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
if len(args) == 2 { log.Fatal(err)
specificVersion = args[1]
} }
if specificVersion != "" { cl, err := client.New(app.Server)
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion) if err != nil {
app.Recipe.Version = specificVersion log.Fatal(err)
} }
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { deployMeta, err := ensureDeployed(cl, app)
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -72,134 +86,69 @@ beforehand.`,
log.Fatal(err) 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() versions, err := app.Recipe.Tags()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var availableUpgrades []string // NOTE(d1): we've no idea what the live deployment version is, so every
if deployMeta.Version == "unknown" { // possible upgrade can be shown. it's up to the user to make the choice
if deployMeta.Version == config.UNKNOWN_DEFAULT {
availableUpgrades = versions availableUpgrades = versions
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
} }
if specificVersion != "" { if len(args) == 2 && args[1] != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) 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 err != nil { if err != nil {
log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name) log.Fatal(err)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
} }
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) { if !upgradeAvailable {
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") log.Info("no available upgrades")
return return
} }
} }
var chosenUpgrade string if internal.Force || internal.NoInput || chosenUpgrade != "" {
if len(availableUpgrades) > 0 { if len(availableUpgrades) > 0 {
if internal.Force || internal.NoInput || specificVersion != "" {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
log.Debugf("choosing %s as version to upgrade to", chosenUpgrade) }
} else { } else {
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version) if err := chooseUpgrade(availableUpgrades, deployMeta, &chosenUpgrade); err != nil {
if deployMeta.IsChaos { log.Fatal(err)
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 == "" { if internal.Force &&
warnMessages = append(warnMessages, fmt.Sprintf("%s is already upgraded to latest", app.Name)) chosenUpgrade == "" &&
deployMeta.Version != config.UNKNOWN_DEFAULT {
chosenUpgrade = deployMeta.Version chosenUpgrade = deployMeta.Version
} }
// if release notes written after git tag published, read them before we if chosenUpgrade == "" {
// check out the tag and then they'll appear to be missing. this covers log.Fatal("unknown deployed version, unable to upgrade")
// 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) 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 { if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -217,6 +166,7 @@ beforehand.`,
log.Fatal(err) log.Fatal(err)
} }
stackName := app.StackName()
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Composefiles: composeFiles, Composefiles: composeFiles,
Namespace: stackName, Namespace: stackName,
@ -243,30 +193,35 @@ beforehand.`,
for _, envVar := range envVars { for _, envVar := range envVars {
if !envVar.Present { if !envVar.Present {
warnMessages = append(warnMessages, upgradeWarnMessages = append(upgradeWarnMessages,
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain), fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
) )
} }
} }
if showReleaseNotes { if showReleaseNotes {
fmt.Print(releaseNotes) fmt.Print(upgradeReleaseNotes)
return return
} }
chaosVersion := config.CHAOS_DEFAULT chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos { if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion chaosVersion = deployMeta.ChaosVersion
if deployMeta.ChaosVersion == "" {
chaosVersion = config.UNKNOWN_DEFAULT
}
} }
if err := internal.NewVersionOverview( if err := internal.NewVersionOverview(
app, app,
warnMessages, upgradeWarnMessages,
"upgrade", "upgrade",
deployMeta.Version, deployMeta.Version,
chaosVersion, chaosVersion,
chosenUpgrade, chosenUpgrade,
releaseNotes); err != nil { upgradeReleaseNotes,
); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -274,7 +229,8 @@ beforehand.`,
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil { if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
log.Fatal(err) log.Fatal(err)
@ -283,19 +239,162 @@ beforehand.`,
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"] postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
if ok && !internal.DontWaitConverge { if ok && !internal.DontWaitConverge {
log.Debugf("run the following post-deploy commands: %s", postDeployCmds) log.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil { if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err) log.Fatalf("attempting to run post deploy commands, saw: %s", err)
} }
} }
app.Recipe.Version = chosenUpgrade if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil {
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version) log.Fatalf("writing recipe version failed: %s", err)
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 ( var (
showReleaseNotes bool showReleaseNotes bool
) )
@ -321,7 +420,7 @@ func init() {
&internal.DontWaitConverge, "no-converge-checks", &internal.DontWaitConverge, "no-converge-checks",
"c", "c",
false, false,
"do not wait for converge logic checks", "disable converge logic checks",
) )
AppUpgradeCommand.Flags().BoolVarP( AppUpgradeCommand.Flags().BoolVarP(

View File

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

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"path" "path"
"slices"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
@ -28,8 +29,8 @@ It is possible to generate new metadata for a single recipe by passing
[recipe]. The existing local catalogue will be updated, not overwritten. [recipe]. The existing local catalogue will be updated, not overwritten.
It is quite easy to get rate limited by Docker Hub when running this command. It is quite easy to get rate limited by Docker Hub when running this command.
If you have a Hub account you can have Abra log you in to avoid this. Pass If you have a Hub account you can "docker login" and Abra will automatically
"--user" and "--pass". use those details.
Push your new release to git.coopcloud.tech with "--publish/-p". This requires 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 that you have permission to git push to these repositories and have your SSH
@ -47,56 +48,62 @@ keys configured on your account.`,
recipeName = args[0] recipeName = args[0]
} }
r := recipe.Get(recipeName)
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(args, cmd.Name()) internal.ValidateRecipe(args, cmd.Name())
} }
if err := catalogue.EnsureCatalogue(); err != nil {
log.Fatal(err)
}
if !internal.Chaos { if !internal.Chaos {
if err := catalogue.EnsureIsClean(); err != nil { if err := catalogue.EnsureIsClean(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
repos, err := recipe.ReadReposMetadata() repos, err := recipe.ReadReposMetadata(internal.Debug)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
var barLength int barLength := len(repos)
var logMsg string
if recipeName != "" { if recipeName != "" {
barLength = 1 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 !skipUpdates {
log.Warn(logMsg) if err := recipe.UpdateRepositories(repos, recipeName, internal.Debug); err != nil {
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
var warnings []string
catl := make(recipe.RecipeCatalogue) catl := make(recipe.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(barLength, "generating catalogue metadata...") catlBar := formatter.CreateProgressbar(barLength, "collecting catalogue metadata")
for _, recipeMeta := range repos { for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name { if recipeName != "" && recipeName != recipeMeta.Name {
catlBar.Add(1) if !internal.Debug {
catlBar.Add(1)
}
continue continue
} }
versions, err := r.GetRecipeVersions() r := recipe.Get(recipeMeta.Name)
versions, warnMsgs, err := r.GetRecipeVersions()
if err != nil { if err != nil {
log.Warn(err) warnings = append(warnings, err.Error())
}
if len(warnMsgs) > 0 {
warnings = append(warnings, warnMsgs...)
} }
features, category, err := recipe.GetRecipeFeaturesAndCategory(r) features, category, warnMsgs, err := recipe.GetRecipeFeaturesAndCategory(r)
if err != nil { if err != nil {
log.Warn(err) warnings = append(warnings, err.Error())
}
if len(warnMsgs) > 0 {
warnings = append(warnings, warnMsgs...)
} }
catl[recipeMeta.Name] = recipe.RecipeMeta{ catl[recipeMeta.Name] = recipe.RecipeMeta{
@ -112,7 +119,24 @@ keys configured on your account.`,
Features: features, Features: features,
} }
catlBar.Add(1) 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)
} }
recipesJSON, err := json.MarshalIndent(catl, "", " ") recipesJSON, err := json.MarshalIndent(catl, "", " ")
@ -142,7 +166,7 @@ keys configured on your account.`,
} }
} }
log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON) log.Infof("generated recipe catalogue: %s", config.RECIPES_JSON)
cataloguePath := path.Join(config.ABRA_DIR, "catalogue") cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if publishChanges { if publishChanges {
@ -168,7 +192,7 @@ keys configured on your account.`,
log.Fatal(err) log.Fatal(err)
} }
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME) sshURL := fmt.Sprintf(config.TOOLSHED_SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil { if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

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

View File

@ -10,6 +10,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
@ -61,18 +62,28 @@ func NewVersionOverview(
domain = config.NO_DOMAIN_DEFAULT domain = config.NO_DOMAIN_DEFAULT
} }
upperKind := strings.ToUpper(kind)
rows := [][]string{ rows := [][]string{
[]string{"APP", domain}, {"DOMAIN", domain},
[]string{"RECIPE", app.Recipe.Name}, {"RECIPE", app.Recipe.Name},
[]string{"SERVER", server}, {"SERVER", server},
[]string{"DEPLOYED", deployedVersion}, {"CONFIG", deployConfig},
[]string{"CURRENT CHAOS ", deployedChaosVersion},
[]string{fmt.Sprintf("TO %s", strings.ToUpper(kind)), toDeployVersion}, {"CURRENT DEPLOYMENT", "---"},
[]string{"CONFIG", deployConfig}, {"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(app.Recipe.EnvVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
} }
overview := formatter.CreateOverview( overview := formatter.CreateOverview(
fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind)), fmt.Sprintf("%s OVERVIEW", upperKind),
rows, rows,
) )
@ -115,7 +126,9 @@ func DeployOverview(
deployedVersion string, deployedVersion string,
deployedChaosVersion string, deployedChaosVersion string,
toDeployVersion, toDeployVersion,
toDeployChaosVersion string) error { toDeployChaosVersion string,
toWriteVersion string,
) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles deployConfig = composeFiles
@ -131,15 +144,42 @@ func DeployOverview(
domain = config.NO_DOMAIN_DEFAULT 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{ rows := [][]string{
[]string{"APP", domain}, {"DOMAIN", domain},
[]string{"RECIPE", app.Recipe.Name}, {"RECIPE", app.Recipe.Name},
[]string{"SERVER", server}, {"SERVER", server},
[]string{"DEPLOYED", deployedVersion}, {"CONFIG", deployConfig},
[]string{"CURRENT CHAOS ", deployedChaosVersion},
[]string{"TO DEPLOY", toDeployVersion}, {"CURRENT DEPLOYMENT", "---"},
[]string{"NEW CHAOS", toDeployChaosVersion}, {"VERSION", formatter.BoldDirtyDefault(deployedVersion)},
[]string{"CONFIG", deployConfig}, {"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)},
} }
overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows) overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows)
@ -170,8 +210,10 @@ func DeployOverview(
// UndeployOverview shows an undeployment overview // UndeployOverview shows an undeployment overview
func UndeployOverview( func UndeployOverview(
app appPkg.App, app appPkg.App,
version, deployedVersion,
chaosVersion string) error { deployedChaosVersion,
toWriteVersion string,
) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles deployConfig = composeFiles
@ -187,13 +229,33 @@ func UndeployOverview(
domain = config.NO_DOMAIN_DEFAULT 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{ rows := [][]string{
[]string{"APP", domain}, {"DOMAIN", domain},
[]string{"RECIPE", app.Recipe.Name}, {"RECIPE", app.Recipe.Name},
[]string{"SERVER", server}, {"SERVER", server},
[]string{"DEPLOYED", version}, {"CONFIG", deployConfig},
[]string{"CHAOS", chaosVersion},
[]string{"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)},
} }
overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows) overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows)

View File

@ -0,0 +1,17 @@
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])
}

11
cli/internal/ensure.go Normal file
View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ var RecipeNewCommand = &cobra.Command{
if err := os.RemoveAll(gitRepo); err != nil { if err := os.RemoveAll(gitRepo); err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("removed example git repo in %s", gitRepo) log.Debugf("removed .git repo in %s", gitRepo)
meta := newRecipeMeta(recipeName) meta := newRecipeMeta(recipeName)
@ -76,7 +76,6 @@ var RecipeNewCommand = &cobra.Command{
if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil { if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil { if err := git.Init(r.Dir, true, gitName, gitEmail); err != 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) { Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name()) recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

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

View File

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

View File

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

22
go.mod
View File

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

107
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 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 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= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE= git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c h1:oeKnUB79PKYD8D0/unYuu7MRcWryQQWOns8+JL+acrs=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q= git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c/go.mod h1:fQuhwrpg6qb9NlFXKYi/LysWu1wxjraS8sxyW12CUF0=
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 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 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 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= 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/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-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 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 v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
@ -131,7 +131,6 @@ github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@ -140,8 +139,6 @@ 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/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 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= 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 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= 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= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
@ -278,7 +275,6 @@ 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/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-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.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.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 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@ -289,8 +285,6 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 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 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 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= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
@ -311,8 +305,6 @@ 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/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/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 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 h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 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= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
@ -321,8 +313,6 @@ 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 h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 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 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 h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 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= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
@ -350,8 +340,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/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/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 v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 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/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@ -382,16 +372,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 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 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
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 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-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 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-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -529,10 +519,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 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 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 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= 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= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
@ -662,6 +649,8 @@ 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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/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 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 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= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
@ -761,8 +750,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 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 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/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -793,8 +782,6 @@ 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.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.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.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 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= 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= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@ -819,7 +806,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -905,8 +891,6 @@ 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.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/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 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/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 v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
@ -947,47 +931,27 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/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 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 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 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/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 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= 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 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/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 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 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 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/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 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= 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 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= 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 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= 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 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= 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 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= 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 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 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@ -1014,8 +978,6 @@ 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-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-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.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 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1028,10 +990,8 @@ 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-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-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-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
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-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/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= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1054,6 +1014,8 @@ 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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1095,8 +1057,6 @@ 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-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-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.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 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -1116,8 +1076,6 @@ 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-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-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.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 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1199,15 +1157,11 @@ 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-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.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.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 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.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-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-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.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 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1219,8 +1173,6 @@ 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.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.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.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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1276,6 +1228,8 @@ 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.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.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1324,14 +1278,10 @@ 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-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-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 v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
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-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
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-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
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 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.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -1351,8 +1301,6 @@ 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.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 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.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 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
@ -1368,8 +1316,6 @@ 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-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.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/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 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.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
@ -1411,7 +1357,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=

View File

@ -36,7 +36,7 @@ func Get(appName string) (App, error) {
return App{}, err return App{}, err
} }
log.Debugf("retrieved %s for %s", app, appName) log.Debugf("loaded app %s: %s", appName, app)
return app, nil return app, nil
} }
@ -91,6 +91,17 @@ type App struct {
Path 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
}
// Type aliases to make code hints easier to understand // Type aliases to make code hints easier to understand
// AppName is AppName // AppName is AppName
@ -235,8 +246,6 @@ func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.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) app, err := NewApp(env, name, appFile)
if err != nil { if err != nil {
return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error()) return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error())
@ -494,13 +503,13 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) { func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) {
for _, service := range compose.Services { for _, service := range compose.Services {
if service.Name == "app" { if service.Name == "app" {
log.Debugf("add the following environment to the app service config of %s:", stackName) log.Debugf("adding env vars to %s service config", stackName)
for k, v := range appEnv { for k, v := range appEnv {
_, exists := service.Environment[k] _, exists := service.Environment[k]
if !exists { if !exists {
value := v value := v
service.Environment[k] = &value service.Environment[k] = &value
log.Debugf("add env var: %s value: %s to %s", k, value, stackName) log.Debugf("%s: %s: %s", stackName, k, value)
} }
} }
} }
@ -569,16 +578,19 @@ func ReadAbraShCmdNames(abraSh string) ([]string, error) {
return cmdNames, nil return cmdNames, nil
} }
func (a App) WriteRecipeVersion(version string, dryRun bool) error { // Wipe removes the version from the app .env file.
func (a App) WipeRecipeVersion() error {
file, err := os.Open(a.Path) file, err := os.Open(a.Path)
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
skipped := false var (
scanner := bufio.NewScanner(file) lines []string
lines := []string{} scanner = bufio.NewScanner(file)
)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") { if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
@ -591,13 +603,71 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
continue continue
} }
if strings.Contains(line, version) { 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 {
return err
}
defer file.Close()
var (
dirtyVersion string
skipped bool
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
}
if strings.Contains(line, version) && !a.Recipe.Dirty && !strings.HasSuffix(line, config.DIRTY_DEFAULT) {
skipped = true skipped = true
lines = append(lines, line) lines = append(lines, line)
continue continue
} }
splitted := strings.Split(line, ":") 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) line = fmt.Sprintf("%s:%s", splitted[0], version)
lines = append(lines, line) lines = append(lines, line)
} }
@ -606,6 +676,10 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
log.Fatal(err) log.Fatal(err)
} }
if a.Recipe.Dirty && dirtyVersion != "" {
version = dirtyVersion
}
if !dryRun { if !dryRun {
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil { if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -198,3 +198,41 @@ func compareFilter(t *testing.T, f1 filters.Args, f2 map[string]map[string]bool)
t.Errorf("filters mismatch (-want +got):\n%s", diff) 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. // RecipeVersionComplete completes versions for the recipe.
func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) { func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(false) catl, err := recipe.ReadRecipeCatalogue(true)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) err := fmt.Sprintf("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError

View File

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

View File

@ -97,16 +97,17 @@ func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catal
var config = LoadAbraConfig() var config = LoadAbraConfig()
var ( var (
ABRA_DIR = config.GetAbraDir() ABRA_DIR = config.GetAbraDir()
SERVERS_DIR = config.GetServersDir() SERVERS_DIR = config.GetServersDir()
RECIPES_DIR = config.GetRecipesDir() RECIPES_DIR = config.GetRecipesDir()
VENDOR_DIR = config.GetVendorDir() VENDOR_DIR = config.GetVendorDir()
BACKUP_DIR = config.GetBackupDir() BACKUP_DIR = config.GetBackupDir()
CATALOGUE_DIR = config.GetCatalogueDir() CATALOGUE_DIR = config.GetCatalogueDir()
RECIPES_JSON = path.Join(config.GetCatalogueDir(), "recipes.json") RECIPES_JSON = path.Join(config.GetCatalogueDir(), "recipes.json")
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json" CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git" 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"
// NOTE(d1): please note, this was done purely out of laziness on our part // 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 // AFAICR. it's easy to punt the value into the label because that is what is
@ -114,6 +115,10 @@ var (
// complained yet! // complained yet!
CHAOS_DEFAULT = "false" CHAOS_DEFAULT = "false"
DIRTY_DEFAULT = "+U"
NO_DOMAIN_DEFAULT = "N/A" NO_DOMAIN_DEFAULT = "N/A"
NO_VERSION_DEFAULT = "N/A" NO_VERSION_DEFAULT = "N/A"
UNKNOWN_DEFAULT = "unknown"
) )

View File

@ -26,9 +26,16 @@ func GetServers() ([]string, error) {
return servers, err return servers, err
} }
log.Debugf("retrieved %v servers: %s", len(servers), servers) var filtered []string
for _, s := range servers {
if !strings.HasPrefix(s, ".") {
filtered = append(filtered, s)
}
}
return servers, nil log.Debugf("retrieved %v servers: %s", len(filtered), filtered)
return filtered, nil
} }
// ReadServerNames retrieves all server names. // 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 // 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 // within the federation. if you're here because of a failing test, try
// `dig +short <app>` to ensure stuff matches first! If flakyness // `dig +short <domain>` to ensure stuff matches first! If flakyness
// becomes an issue we can look into mocking // becomes an issue we can look into mocking
{"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true}, {"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true},
{"docs.coopcloud.tech", "coopcloud.tech", true}, {"docs.coopcloud.tech", "coopcloud.tech", true},
@ -43,7 +43,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
func TestEnsureIpv4(t *testing.T) { func TestEnsureIpv4(t *testing.T) {
// NOTE(d1): DNS records get checked, so use something that is maintained // 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 // within the federation. if you're here because of a failing test, try `dig
// +short <app>` to ensure stuff matches first! If flakyness becomes an // +short <domain>` to ensure stuff matches first! If flakyness becomes an
// issue we can look into mocking // issue we can look into mocking
domainName := "collabora.ostrom.collective.tools" domainName := "collabora.ostrom.collective.tools"
serverName := "ostrom.collective.tools" serverName := "ostrom.collective.tools"

View File

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

View File

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

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

View File

@ -1,9 +1,7 @@
package git package git
import ( import (
"fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
@ -11,39 +9,59 @@ import (
"github.com/go-git/go-git/v5/plumbing" "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. // Clone runs a git clone which accounts for different default branches.
func Clone(dir, url string) error { func Clone(dir, url string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) { if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Debugf("%s does not exist, attempting to git clone from %s", dir, url) log.Debugf("git clone: %s", dir, url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{ _, err := git.PlainClone(dir, false, &git.CloneOptions{
URL: url, URL: url,
Tags: git.AllTags, Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/master"), ReferenceName: plumbing.ReferenceName("refs/heads/main"),
SingleBranch: true, SingleBranch: true,
}) })
if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir)
return nil
}
if err != nil { if err != nil {
log.Debugf("cloning %s default branch failed, attempting from main branch", url) log.Debug("git clone: main branch failed, attempting master branch")
_, err := git.PlainClone(dir, false, &git.CloneOptions{ _, err := git.PlainClone(dir, false, &git.CloneOptions{
URL: url, URL: url,
Tags: git.AllTags, Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/main"), ReferenceName: plumbing.ReferenceName("refs/heads/master"),
SingleBranch: true, SingleBranch: true,
}) })
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)
}
if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir)
return nil
}
if err != nil {
return err return err
} }
} }
log.Debugf("%s has been git cloned successfully", dir) log.Debugf("git clone: %s cloned successfully", dir)
} else { } else {
log.Debugf("%s already exists", dir) log.Debugf("git clone: %s already exists", dir)
} }
return nil return nil

View File

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

View File

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

15
pkg/git/read_test.go Normal file
View File

@ -0,0 +1,15 @@
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) { func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
features, category, err := recipe.GetRecipeFeaturesAndCategory(r) features, category, _, err := recipe.GetRecipeFeaturesAndCategory(r)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

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

39
pkg/recipe/git_test.go Normal file
View File

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

View File

@ -33,7 +33,7 @@ func TestGet(t *testing.T) {
name: "foo:1.2.3", name: "foo:1.2.3",
recipe: Recipe{ recipe: Recipe{
Name: "foo", Name: "foo",
Version: "1.2.3", EnvVersion: "1.2.3",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"), Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"),
GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git", GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git",
SSHURL: "ssh://git@git.coopcloud.tech:2222/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", name: "mygit.org/myorg/cool-recipe:1.2.4",
recipe: Recipe{ recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe", Name: "mygit.org/myorg/cool-recipe",
Version: "1.2.4", EnvVersion: "1.2.4",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"), Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git", GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git", SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
@ -105,3 +105,16 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
assert.NotEqual(t, label, defaultTimeoutLabel) 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,6 +13,7 @@ import (
stdlibErr "errors" stdlibErr "errors"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
"github.com/docker/cli/cli/command/service/progress" "github.com/docker/cli/cli/command/service/progress"
@ -106,13 +107,22 @@ type DeployMeta struct {
ChaosVersion string // the --chaos deployment version 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. // IsDeployed gathers metadata about an app deployment.
func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) (DeployMeta, error) { func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) (DeployMeta, error) {
deployMeta := DeployMeta{ deployMeta := DeployMeta{
IsDeployed: false, IsDeployed: false,
Version: "unknown", Version: "unknown",
IsChaos: false, IsChaos: false,
ChaosVersion: "false", // NOTE(d1): match string type used on label ChaosVersion: config.CHAOS_DEFAULT,
} }
filter := filters.NewArgs() filter := filters.NewArgs()

View File

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

View File

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

View File

@ -65,7 +65,6 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--chaos --no-input --no-converge-checks --chaos --no-input --no-converge-checks
assert_success assert_success
assert_output --partial 'NEW CHAOS'
} }
# bats test_tags=slow # bats test_tags=slow
@ -128,7 +127,6 @@ teardown(){
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
assert_output --partial "${wantHash:0:8}" assert_output --partial "${wantHash:0:8}"
assert_output --partial 'NEW CHAOS'
} }
# bats test_tags=slow # bats test_tags=slow
@ -163,6 +161,32 @@ teardown(){
assert_output --partial 'already deployed' 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 # bats test_tags=slow
@test "re-deploy deployed app if --force/--chaos" { @test "re-deploy deployed app if --force/--chaos" {
_deploy_app _deploy_app
@ -304,7 +328,6 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure assert_failure
assert_output --partial 'unable to deploy, secrets not generated'
} }
# bats test_tags=slow # bats test_tags=slow
@ -340,10 +363,63 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \ run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
refute_output --partial 'no such file or directory'
_undeploy_app _undeploy_app
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all run $ABRA app secret rm "$TEST_APP_DOMAIN" --all
assert_success 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"
}

View File

@ -91,9 +91,27 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \ run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --force --debug --no-input --no-converge-checks --force --debug
assert_success assert_success
assert_output --partial "overriding env file version"
run grep -q "TYPE=$TEST_RECIPE: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" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success 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

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

View File

@ -0,0 +1,46 @@
#!/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
_reset_app
_undeploy_app
}
@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 $ABRA app env "$TEST_APP_DOMAIN"
assert_success
}

View File

@ -0,0 +1,103 @@
#!/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
_reset_app
_undeploy_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'
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'
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

@ -137,3 +137,16 @@ teardown(){
assert_output --partial "$latestRelease" assert_output --partial "$latestRelease"
assert_output --partial "${headHash:0:8}" # is a chaos deploy 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

@ -129,6 +129,17 @@ teardown(){
assert_output --partial "0.1.0+1.20.0" 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 # bats test_tags=slow
@test "rollback to a version 2 tags behind" { @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 run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks

View File

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

View File

@ -0,0 +1,76 @@
#!/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_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'
# new deployment
assert_output --regexp 'VERSION.*' + "0.1.0+1.20.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.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'
# new deployment
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
}

View File

@ -0,0 +1,102 @@
#!/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,6 +33,16 @@ teardown(){
assert_failure 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 # bats test_tags=slow
@test "error if specific version is not an upgrade" { @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 run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
@ -118,6 +128,19 @@ teardown(){
assert_output --partial '0.2.0+1.21.0' 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 # bats test_tags=slow
@test "upgrade to latest" { @test "upgrade to latest" {
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

View File

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

View File

@ -0,0 +1,75 @@
#!/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'
# new deployment
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'
# new deployment
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
}

View File

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

View File

@ -9,7 +9,7 @@ _fetch_recipe() {
run mkdir -p "$ABRA_DIR/recipes" run mkdir -p "$ABRA_DIR/recipes"
assert_success assert_success
run git clone "https://git.coopcloud.tech/coop-cloud/$TEST_RECIPE" "$ABRA_DIR/recipes/$TEST_RECIPE" run git clone "https://git.coopcloud.tech/toolshed/$TEST_RECIPE" "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success assert_success
fi fi
} }
@ -33,7 +33,7 @@ _ensure_latest_version(){
_ensure_catalogue(){ _ensure_catalogue(){
if [[ ! -d "$ABRA_DIR/catalogue" ]]; then if [[ ! -d "$ABRA_DIR/catalogue" ]]; then
run git clone https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git $ABRA_DIR/catalogue run git clone https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git $ABRA_DIR/catalogue
assert_success assert_success
fi fi
} }

View File

@ -35,6 +35,10 @@ teardown(){
assert_success assert_success
assert_output --partial "new recipe 'foobar' created" assert_output --partial "new recipe 'foobar' created"
assert_exists "$ABRA_DIR/recipes/foobar" 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 # bats test_tags=slow

View File

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

View File

@ -11,21 +11,13 @@ func (oscState oscStringState) Handle(b byte) (s state, e error) {
return nextState, err return nextState, err
} }
switch { // There are several control characters and sequences which can
case isOscStringTerminator(b): // terminate an OSC string. Most of them are handled by the baseState
// handler. The ANSI_BEL character is a special case which behaves as a
// terminator only for an OSC string.
if b == ANSI_BEL {
return oscState.parser.ground, nil return oscState.parser.ground, nil
} }
return oscState, nil return oscState, nil
} }
// See below for OSC string terminators for linux
// http://man7.org/linux/man-pages/man4/console_codes.4.html
func isOscStringTerminator(b byte) bool {
if b == ANSI_BEL || b == 0x5C {
return true
}
return false
}

View File

@ -11,7 +11,7 @@ compatibility status with go-git.
| `init` | `--bare` | ✅ | | | | `init` | `--bare` | ✅ | | |
| `init` | `--template` <br/> `--separate-git-dir` <br/> `--shared` | ❌ | | | | `init` | `--template` <br/> `--separate-git-dir` <br/> `--shared` | ❌ | | |
| `clone` | | ✅ | | - [PlainClone](_examples/clone/main.go) | | `clone` | | ✅ | | - [PlainClone](_examples/clone/main.go) |
| `clone` | Authentication: <br/> - none <br/> - access token <br/> - username + password <br/> - ssh | ✅ | | - [clone ssh](_examples/clone/auth/ssh/main.go) <br/> - [clone access token](_examples/clone/auth/basic/access_token/main.go) <br/> - [clone user + password](_examples/clone/auth/basic/username_password/main.go) | | `clone` | Authentication: <br/> - none <br/> - access token <br/> - username + password <br/> - ssh | ✅ | | - [clone ssh (private_key)](_examples/clone/auth/ssh/private_key/main.go) <br/> - [clone ssh (ssh_agent)](_examples/clone/auth/ssh/ssh_agent/main.go) <br/> - [clone access token](_examples/clone/auth/basic/access_token/main.go) <br/> - [clone user + password](_examples/clone/auth/basic/username_password/main.go) |
| `clone` | `--progress` <br/> `--single-branch` <br/> `--depth` <br/> `--origin` <br/> `--recurse-submodules` <br/>`--shared` | ✅ | | - [recurse submodules](_examples/clone/main.go) <br/> - [progress](_examples/progress/main.go) | | `clone` | `--progress` <br/> `--single-branch` <br/> `--depth` <br/> `--origin` <br/> `--recurse-submodules` <br/>`--shared` | ✅ | | - [recurse submodules](_examples/clone/main.go) <br/> - [progress](_examples/progress/main.go) |
## Basic snapshotting ## Basic snapshotting
@ -34,6 +34,7 @@ compatibility status with go-git.
| `merge` | | ⚠️ (partial) | Fast-forward only | | | `merge` | | ⚠️ (partial) | Fast-forward only | |
| `mergetool` | | ❌ | | | | `mergetool` | | ❌ | | |
| `stash` | | ❌ | | | | `stash` | | ❌ | | |
| `sparse-checkout` | | ✅ | | - [sparse-checkout](_examples/sparse-checkout/main.go) |
| `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) | | `tag` | | ✅ | | - [tag](_examples/tag/main.go) <br/> - [tag create and push](_examples/tag-create-push/main.go) |
## Sharing and updating projects ## Sharing and updating projects

View File

@ -31,6 +31,13 @@ In order for a PR to be accepted it needs to pass a list of requirements:
- If the PR is a new feature, it has to come with a suite of unit tests, that tests the new functionality. - If the PR is a new feature, it has to come with a suite of unit tests, that tests the new functionality.
- In any case, all the PRs have to pass the personal evaluation of at least one of the maintainers of go-git. - In any case, all the PRs have to pass the personal evaluation of at least one of the maintainers of go-git.
### Branches
The `master` branch is currently used for maintaining the `v5` major release only. The accepted changes would
be dependency bumps, bug fixes and small changes that aren't needed for `v6`. New development should target the
`v6-exp` branch, and if agreed with at least one go-git maintainer, it can be back ported to `v5` by creating
a new PR that targets `master`.
### Format of the commit message ### Format of the commit message
Every commit message should describe what was changed, under which context and, if applicable, the GitHub issue it relates to: Every commit message should describe what was changed, under which context and, if applicable, the GitHub issue it relates to:

View File

@ -97,13 +97,10 @@ func Blame(c *object.Commit, path string) (*BlameResult, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if finished == true { if finished {
break break
} }
} }
if err != nil {
return nil, err
}
b.lineToCommit = make([]*object.Commit, finalLength) b.lineToCommit = make([]*object.Commit, finalLength)
for i := range needsMap { for i := range needsMap {
@ -309,8 +306,8 @@ func (b *blame) addBlames(curItems []*queueItem) (bool, error) {
for h := range hunks { for h := range hunks {
hLines := countLines(hunks[h].Text) hLines := countLines(hunks[h].Text)
for hl := 0; hl < hLines; hl++ { for hl := 0; hl < hLines; hl++ {
switch { switch hunks[h].Type {
case hunks[h].Type == diffmatchpatch.DiffEqual: case diffmatchpatch.DiffEqual:
prevl++ prevl++
curl++ curl++
if curl == curItem.NeedsMap[need].Cur { if curl == curItem.NeedsMap[need].Cur {
@ -322,7 +319,7 @@ func (b *blame) addBlames(curItems []*queueItem) (bool, error) {
break out break out
} }
} }
case hunks[h].Type == diffmatchpatch.DiffInsert: case diffmatchpatch.DiffInsert:
curl++ curl++
if curl == curItem.NeedsMap[need].Cur { if curl == curItem.NeedsMap[need].Cur {
// the line we want is added, it may have been added here (or by another parent), skip it for now // the line we want is added, it may have been added here (or by another parent), skip it for now
@ -331,7 +328,7 @@ func (b *blame) addBlames(curItems []*queueItem) (bool, error) {
break out break out
} }
} }
case hunks[h].Type == diffmatchpatch.DiffDelete: case diffmatchpatch.DiffDelete:
prevl += hLines prevl += hLines
continue out continue out
default: default:

View File

@ -252,6 +252,7 @@ const (
extensionsSection = "extensions" extensionsSection = "extensions"
fetchKey = "fetch" fetchKey = "fetch"
urlKey = "url" urlKey = "url"
pushurlKey = "pushurl"
bareKey = "bare" bareKey = "bare"
worktreeKey = "worktree" worktreeKey = "worktree"
commentCharKey = "commentChar" commentCharKey = "commentChar"
@ -633,6 +634,7 @@ func (c *RemoteConfig) unmarshal(s *format.Subsection) error {
c.Name = c.raw.Name c.Name = c.raw.Name
c.URLs = append([]string(nil), c.raw.Options.GetAll(urlKey)...) c.URLs = append([]string(nil), c.raw.Options.GetAll(urlKey)...)
c.URLs = append(c.URLs, c.raw.Options.GetAll(pushurlKey)...)
c.Fetch = fetch c.Fetch = fetch
c.Mirror = c.raw.Options.Get(mirrorKey) == "true" c.Mirror = c.raw.Options.Get(mirrorKey) == "true"

View File

@ -43,6 +43,11 @@ func tokenizeExpression(ch rune, tokenType token, check runeCategoryValidator, r
return tokenType, string(data), nil return tokenType, string(data), nil
} }
// maxRevisionLength holds the maximum length that will be parsed for a
// revision. Git itself doesn't enforce a max length, but rather leans on
// the OS to enforce it via its ARG_MAX.
const maxRevisionLength = 128 * 1024 // 128kb
var zeroRune = rune(0) var zeroRune = rune(0)
// scanner represents a lexical scanner. // scanner represents a lexical scanner.
@ -52,7 +57,7 @@ type scanner struct {
// newScanner returns a new instance of scanner. // newScanner returns a new instance of scanner.
func newScanner(r io.Reader) *scanner { func newScanner(r io.Reader) *scanner {
return &scanner{r: bufio.NewReader(r)} return &scanner{r: bufio.NewReader(io.LimitReader(r, maxRevisionLength))}
} }
// Scan extracts tokens and their strings counterpart // Scan extracts tokens and their strings counterpart

View File

@ -416,6 +416,9 @@ type ResetOptions struct {
// the index (resetting it to the tree of Commit) and the working tree // the index (resetting it to the tree of Commit) and the working tree
// depending on Mode. If empty MixedReset is used. // depending on Mode. If empty MixedReset is used.
Mode ResetMode Mode ResetMode
// Files, if not empty will constrain the reseting the index to only files
// specified in this list.
Files []string
} }
// Validate validates the fields and sets the default values. // Validate validates the fields and sets the default values.
@ -790,3 +793,26 @@ type PlainInitOptions struct {
// Validate validates the fields and sets the default values. // Validate validates the fields and sets the default values.
func (o *PlainInitOptions) Validate() error { return nil } func (o *PlainInitOptions) Validate() error { return nil }
var (
ErrNoRestorePaths = errors.New("you must specify path(s) to restore")
)
// RestoreOptions describes how a restore should be performed.
type RestoreOptions struct {
// Marks to restore the content in the index
Staged bool
// Marks to restore the content of the working tree
Worktree bool
// List of file paths that will be restored
Files []string
}
// Validate validates the fields and sets the default values.
func (o *RestoreOptions) Validate() error {
if len(o.Files) == 0 {
return ErrNoRestorePaths
}
return nil
}

View File

@ -64,6 +64,10 @@ func ReadPatterns(fs billy.Filesystem, path []string) (ps []Pattern, err error)
for _, fi := range fis { for _, fi := range fis {
if fi.IsDir() && fi.Name() != gitDir { if fi.IsDir() && fi.Name() != gitDir {
if NewMatcher(ps).Match(append(path, fi.Name()), true) {
continue
}
var subps []Pattern var subps []Pattern
subps, err = ReadPatterns(fs, append(path, fi.Name())) subps, err = ReadPatterns(fs, append(path, fi.Name()))
if err != nil { if err != nil {

View File

@ -24,8 +24,8 @@ var (
// ErrInvalidChecksum is returned by Decode if the SHA1 hash mismatch with // ErrInvalidChecksum is returned by Decode if the SHA1 hash mismatch with
// the read content // the read content
ErrInvalidChecksum = errors.New("invalid checksum") ErrInvalidChecksum = errors.New("invalid checksum")
// ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory
errUnknownExtension = errors.New("unknown extension") ErrUnknownExtension = errors.New("unknown extension")
) )
const ( const (
@ -39,6 +39,7 @@ const (
// A Decoder reads and decodes index files from an input stream. // A Decoder reads and decodes index files from an input stream.
type Decoder struct { type Decoder struct {
buf *bufio.Reader
r io.Reader r io.Reader
hash hash.Hash hash hash.Hash
lastEntry *Entry lastEntry *Entry
@ -49,8 +50,10 @@ type Decoder struct {
// NewDecoder returns a new decoder that reads from r. // NewDecoder returns a new decoder that reads from r.
func NewDecoder(r io.Reader) *Decoder { func NewDecoder(r io.Reader) *Decoder {
h := hash.New(hash.CryptoType) h := hash.New(hash.CryptoType)
buf := bufio.NewReader(r)
return &Decoder{ return &Decoder{
r: io.TeeReader(r, h), buf: buf,
r: io.TeeReader(buf, h),
hash: h, hash: h,
extReader: bufio.NewReader(nil), extReader: bufio.NewReader(nil),
} }
@ -210,71 +213,75 @@ func (d *Decoder) readExtensions(idx *Index) error {
// count that they are not supported by jgit or libgit // count that they are not supported by jgit or libgit
var expected []byte var expected []byte
var peeked []byte
var err error var err error
var header [4]byte // we should always be able to peek for 4 bytes (header) + 4 bytes (extlen) + final hash
// if this fails, we know that we're at the end of the index
peekLen := 4 + 4 + d.hash.Size()
for { for {
expected = d.hash.Sum(nil) expected = d.hash.Sum(nil)
peeked, err = d.buf.Peek(peekLen)
var n int if len(peeked) < peekLen {
if n, err = io.ReadFull(d.r, header[:]); err != nil { // there can't be an extension at this point, so let's bail out
if n == 0 {
err = io.EOF
}
break break
} }
err = d.readExtension(idx, header[:])
if err != nil {
break
}
}
if err != errUnknownExtension {
return err
}
return d.readChecksum(expected, header)
}
func (d *Decoder) readExtension(idx *Index, header []byte) error {
switch {
case bytes.Equal(header, treeExtSignature):
r, err := d.getExtensionReader()
if err != nil { if err != nil {
return err return err
} }
err = d.readExtension(idx)
if err != nil {
return err
}
}
return d.readChecksum(expected)
}
func (d *Decoder) readExtension(idx *Index) error {
var header [4]byte
if _, err := io.ReadFull(d.r, header[:]); err != nil {
return err
}
r, err := d.getExtensionReader()
if err != nil {
return err
}
switch {
case bytes.Equal(header[:], treeExtSignature):
idx.Cache = &Tree{} idx.Cache = &Tree{}
d := &treeExtensionDecoder{r} d := &treeExtensionDecoder{r}
if err := d.Decode(idx.Cache); err != nil { if err := d.Decode(idx.Cache); err != nil {
return err return err
} }
case bytes.Equal(header, resolveUndoExtSignature): case bytes.Equal(header[:], resolveUndoExtSignature):
r, err := d.getExtensionReader()
if err != nil {
return err
}
idx.ResolveUndo = &ResolveUndo{} idx.ResolveUndo = &ResolveUndo{}
d := &resolveUndoDecoder{r} d := &resolveUndoDecoder{r}
if err := d.Decode(idx.ResolveUndo); err != nil { if err := d.Decode(idx.ResolveUndo); err != nil {
return err return err
} }
case bytes.Equal(header, endOfIndexEntryExtSignature): case bytes.Equal(header[:], endOfIndexEntryExtSignature):
r, err := d.getExtensionReader()
if err != nil {
return err
}
idx.EndOfIndexEntry = &EndOfIndexEntry{} idx.EndOfIndexEntry = &EndOfIndexEntry{}
d := &endOfIndexEntryDecoder{r} d := &endOfIndexEntryDecoder{r}
if err := d.Decode(idx.EndOfIndexEntry); err != nil { if err := d.Decode(idx.EndOfIndexEntry); err != nil {
return err return err
} }
default: default:
return errUnknownExtension // See https://git-scm.com/docs/index-format, which says:
// If the first byte is 'A'..'Z' the extension is optional and can be ignored.
if header[0] < 'A' || header[0] > 'Z' {
return ErrUnknownExtension
}
d := &unknownExtensionDecoder{r}
if err := d.Decode(); err != nil {
return err
}
} }
return nil return nil
@ -290,11 +297,10 @@ func (d *Decoder) getExtensionReader() (*bufio.Reader, error) {
return d.extReader, nil return d.extReader, nil
} }
func (d *Decoder) readChecksum(expected []byte, alreadyRead [4]byte) error { func (d *Decoder) readChecksum(expected []byte) error {
var h plumbing.Hash var h plumbing.Hash
copy(h[:4], alreadyRead[:])
if _, err := io.ReadFull(d.r, h[4:]); err != nil { if _, err := io.ReadFull(d.r, h[:]); err != nil {
return err return err
} }
@ -476,3 +482,22 @@ func (d *endOfIndexEntryDecoder) Decode(e *EndOfIndexEntry) error {
_, err = io.ReadFull(d.r, e.Hash[:]) _, err = io.ReadFull(d.r, e.Hash[:])
return err return err
} }
type unknownExtensionDecoder struct {
r *bufio.Reader
}
func (d *unknownExtensionDecoder) Decode() error {
var buf [1024]byte
for {
_, err := d.r.Read(buf[:])
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
}

View File

@ -3,8 +3,11 @@ package index
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"io" "io"
"path"
"sort" "sort"
"strings"
"time" "time"
"github.com/go-git/go-git/v5/plumbing/hash" "github.com/go-git/go-git/v5/plumbing/hash"
@ -13,7 +16,7 @@ import (
var ( var (
// EncodeVersionSupported is the range of supported index versions // EncodeVersionSupported is the range of supported index versions
EncodeVersionSupported uint32 = 3 EncodeVersionSupported uint32 = 4
// ErrInvalidTimestamp is returned by Encode if a Index with a Entry with // ErrInvalidTimestamp is returned by Encode if a Index with a Entry with
// negative timestamp values // negative timestamp values
@ -22,20 +25,25 @@ var (
// An Encoder writes an Index to an output stream. // An Encoder writes an Index to an output stream.
type Encoder struct { type Encoder struct {
w io.Writer w io.Writer
hash hash.Hash hash hash.Hash
lastEntry *Entry
} }
// NewEncoder returns a new encoder that writes to w. // NewEncoder returns a new encoder that writes to w.
func NewEncoder(w io.Writer) *Encoder { func NewEncoder(w io.Writer) *Encoder {
h := hash.New(hash.CryptoType) h := hash.New(hash.CryptoType)
mw := io.MultiWriter(w, h) mw := io.MultiWriter(w, h)
return &Encoder{mw, h} return &Encoder{mw, h, nil}
} }
// Encode writes the Index to the stream of the encoder. // Encode writes the Index to the stream of the encoder.
func (e *Encoder) Encode(idx *Index) error { func (e *Encoder) Encode(idx *Index) error {
// TODO: support v4 return e.encode(idx, true)
}
func (e *Encoder) encode(idx *Index, footer bool) error {
// TODO: support extensions // TODO: support extensions
if idx.Version > EncodeVersionSupported { if idx.Version > EncodeVersionSupported {
return ErrUnsupportedVersion return ErrUnsupportedVersion
@ -49,7 +57,10 @@ func (e *Encoder) Encode(idx *Index) error {
return err return err
} }
return e.encodeFooter() if footer {
return e.encodeFooter()
}
return nil
} }
func (e *Encoder) encodeHeader(idx *Index) error { func (e *Encoder) encodeHeader(idx *Index) error {
@ -64,7 +75,7 @@ func (e *Encoder) encodeEntries(idx *Index) error {
sort.Sort(byName(idx.Entries)) sort.Sort(byName(idx.Entries))
for _, entry := range idx.Entries { for _, entry := range idx.Entries {
if err := e.encodeEntry(entry); err != nil { if err := e.encodeEntry(idx, entry); err != nil {
return err return err
} }
entryLength := entryHeaderLength entryLength := entryHeaderLength
@ -73,7 +84,7 @@ func (e *Encoder) encodeEntries(idx *Index) error {
} }
wrote := entryLength + len(entry.Name) wrote := entryLength + len(entry.Name)
if err := e.padEntry(wrote); err != nil { if err := e.padEntry(idx, wrote); err != nil {
return err return err
} }
} }
@ -81,7 +92,7 @@ func (e *Encoder) encodeEntries(idx *Index) error {
return nil return nil
} }
func (e *Encoder) encodeEntry(entry *Entry) error { func (e *Encoder) encodeEntry(idx *Index, entry *Entry) error {
sec, nsec, err := e.timeToUint32(&entry.CreatedAt) sec, nsec, err := e.timeToUint32(&entry.CreatedAt)
if err != nil { if err != nil {
return err return err
@ -132,9 +143,68 @@ func (e *Encoder) encodeEntry(entry *Entry) error {
return err return err
} }
switch idx.Version {
case 2, 3:
err = e.encodeEntryName(entry)
case 4:
err = e.encodeEntryNameV4(entry)
default:
err = ErrUnsupportedVersion
}
return err
}
func (e *Encoder) encodeEntryName(entry *Entry) error {
return binary.Write(e.w, []byte(entry.Name)) return binary.Write(e.w, []byte(entry.Name))
} }
func (e *Encoder) encodeEntryNameV4(entry *Entry) error {
name := entry.Name
l := 0
if e.lastEntry != nil {
dir := path.Dir(e.lastEntry.Name) + "/"
if strings.HasPrefix(entry.Name, dir) {
l = len(e.lastEntry.Name) - len(dir)
name = strings.TrimPrefix(entry.Name, dir)
} else {
l = len(e.lastEntry.Name)
}
}
e.lastEntry = entry
err := binary.WriteVariableWidthInt(e.w, int64(l))
if err != nil {
return err
}
return binary.Write(e.w, []byte(name+string('\x00')))
}
func (e *Encoder) encodeRawExtension(signature string, data []byte) error {
if len(signature) != 4 {
return fmt.Errorf("invalid signature length")
}
_, err := e.w.Write([]byte(signature))
if err != nil {
return err
}
err = binary.WriteUint32(e.w, uint32(len(data)))
if err != nil {
return err
}
_, err = e.w.Write(data)
if err != nil {
return err
}
return nil
}
func (e *Encoder) timeToUint32(t *time.Time) (uint32, uint32, error) { func (e *Encoder) timeToUint32(t *time.Time) (uint32, uint32, error) {
if t.IsZero() { if t.IsZero() {
return 0, 0, nil return 0, 0, nil
@ -147,7 +217,11 @@ func (e *Encoder) timeToUint32(t *time.Time) (uint32, uint32, error) {
return uint32(t.Unix()), uint32(t.Nanosecond()), nil return uint32(t.Unix()), uint32(t.Nanosecond()), nil
} }
func (e *Encoder) padEntry(wrote int) error { func (e *Encoder) padEntry(idx *Index, wrote int) error {
if idx.Version == 4 {
return nil
}
padLen := 8 - wrote%8 padLen := 8 - wrote%8
_, err := e.w.Write(bytes.Repeat([]byte{'\x00'}, padLen)) _, err := e.w.Write(bytes.Repeat([]byte{'\x00'}, padLen))

View File

@ -32,19 +32,17 @@ func (idx *deltaIndex) findMatch(src, tgt []byte, tgtOffset int) (srcOffset, l i
return 0, -1 return 0, -1
} }
if len(tgt) >= tgtOffset+s && len(src) >= blksz { h := hashBlock(tgt, tgtOffset)
h := hashBlock(tgt, tgtOffset) tIdx := h & idx.mask
tIdx := h & idx.mask eIdx := idx.table[tIdx]
eIdx := idx.table[tIdx] if eIdx == 0 {
if eIdx != 0 { return
srcOffset = idx.entries[eIdx]
} else {
return
}
l = matchLength(src, tgt, tgtOffset, srcOffset)
} }
srcOffset = idx.entries[eIdx]
l = matchLength(src, tgt, tgtOffset, srcOffset)
return return
} }

View File

@ -26,6 +26,13 @@ var (
const ( const (
payload = 0x7f // 0111 1111 payload = 0x7f // 0111 1111
continuation = 0x80 // 1000 0000 continuation = 0x80 // 1000 0000
// maxPatchPreemptionSize defines what is the max size of bytes to be
// premptively made available for a patch operation.
maxPatchPreemptionSize uint = 65536
// minDeltaSize defines the smallest size for a delta.
minDeltaSize = 4
) )
type offset struct { type offset struct {
@ -86,9 +93,13 @@ func ApplyDelta(target, base plumbing.EncodedObject, delta []byte) (err error) {
} }
// PatchDelta returns the result of applying the modification deltas in delta to src. // PatchDelta returns the result of applying the modification deltas in delta to src.
// An error will be returned if delta is corrupted (ErrDeltaLen) or an action command // An error will be returned if delta is corrupted (ErrInvalidDelta) or an action command
// is not copy from source or copy from delta (ErrDeltaCmd). // is not copy from source or copy from delta (ErrDeltaCmd).
func PatchDelta(src, delta []byte) ([]byte, error) { func PatchDelta(src, delta []byte) ([]byte, error) {
if len(src) == 0 || len(delta) < minDeltaSize {
return nil, ErrInvalidDelta
}
b := &bytes.Buffer{} b := &bytes.Buffer{}
if err := patchDelta(b, src, delta); err != nil { if err := patchDelta(b, src, delta); err != nil {
return nil, err return nil, err
@ -239,7 +250,9 @@ func patchDelta(dst *bytes.Buffer, src, delta []byte) error {
remainingTargetSz := targetSz remainingTargetSz := targetSz
var cmd byte var cmd byte
dst.Grow(int(targetSz))
growSz := min(targetSz, maxPatchPreemptionSize)
dst.Grow(int(growSz))
for { for {
if len(delta) == 0 { if len(delta) == 0 {
return ErrInvalidDelta return ErrInvalidDelta
@ -403,6 +416,10 @@ func patchDeltaWriter(dst io.Writer, base io.ReaderAt, delta io.Reader,
// This must be called twice on the delta data buffer, first to get the // This must be called twice on the delta data buffer, first to get the
// expected source buffer size, and again to get the target buffer size. // expected source buffer size, and again to get the target buffer size.
func decodeLEB128(input []byte) (uint, []byte) { func decodeLEB128(input []byte) (uint, []byte) {
if len(input) == 0 {
return 0, input
}
var num, sz uint var num, sz uint
var b byte var b byte
for { for {

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