forked from toolshed/abra
Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
4d7c812fe2 |
47
.drone.yml
47
.drone.yml
@ -3,14 +3,14 @@ kind: pipeline
|
||||
name: coopcloud.tech/abra
|
||||
steps:
|
||||
- name: make check
|
||||
image: golang:1.24
|
||||
image: golang:1.22
|
||||
commands:
|
||||
- make check
|
||||
|
||||
- name: make test
|
||||
image: golang:1.24
|
||||
image: golang:1.22
|
||||
environment:
|
||||
CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git
|
||||
CATL_URL: https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git
|
||||
commands:
|
||||
- mkdir -p $HOME/.abra
|
||||
- git clone $CATL_URL $HOME/.abra/catalogue
|
||||
@ -29,7 +29,7 @@ steps:
|
||||
event: tag
|
||||
|
||||
- name: release
|
||||
image: goreleaser/goreleaser:v2.5.1
|
||||
image: goreleaser/goreleaser:v1.24.0
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: goreleaser_gitea_token
|
||||
@ -47,10 +47,10 @@ steps:
|
||||
image: plugins/docker
|
||||
settings:
|
||||
auto_tag: true
|
||||
username: abra-bot
|
||||
username: 3wordchant
|
||||
password:
|
||||
from_secret: git_coopcloud_tech_token_abra_bot
|
||||
repo: git.coopcloud.tech/toolshed/abra
|
||||
from_secret: git_coopcloud_tech_token_3wc
|
||||
repo: git.coopcloud.tech/coop-cloud/abra
|
||||
tags: dev
|
||||
registry: git.coopcloud.tech
|
||||
when:
|
||||
@ -60,7 +60,7 @@ steps:
|
||||
- make check
|
||||
- make test
|
||||
|
||||
- name: on-demand integration test
|
||||
- name: integration test
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host:
|
||||
@ -74,31 +74,7 @@ steps:
|
||||
request_pty: true
|
||||
script:
|
||||
- |
|
||||
wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
|
||||
chmod +x run-ci-int
|
||||
sh run-ci-int
|
||||
when:
|
||||
ref:
|
||||
- refs/heads/int-*
|
||||
depends_on:
|
||||
- make check
|
||||
- make test
|
||||
|
||||
- name: nightly integration test
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host:
|
||||
- int.coopcloud.tech
|
||||
username: abra
|
||||
key:
|
||||
from_secret: abra_int_private_key
|
||||
port: 22
|
||||
command_timeout: 60m
|
||||
script_stop: true
|
||||
request_pty: true
|
||||
script:
|
||||
- |
|
||||
wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
|
||||
wget https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
|
||||
chmod +x run-ci-int
|
||||
sh run-ci-int
|
||||
when:
|
||||
@ -111,8 +87,3 @@ steps:
|
||||
volumes:
|
||||
- name: deps
|
||||
temp: {}
|
||||
|
||||
trigger:
|
||||
action:
|
||||
exclude:
|
||||
- synchronized
|
||||
|
8
.gitea/ISSUE_TEMPLATE.md
Normal file
8
.gitea/ISSUE_TEMPLATE.md
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
name: "Do not use this issue tracker"
|
||||
about: "Do not use this issue tracker"
|
||||
title: "Do not use this issue tracker"
|
||||
labels: []
|
||||
---
|
||||
|
||||
Please report your issue on [`coop-cloud/organising`](https://git.coopcloud.tech/coop-cloud/organising)
|
@ -4,7 +4,6 @@
|
||||
> please do add yourself! This is a community project, let's show some 💞
|
||||
|
||||
- 3wordchant
|
||||
- ammaratef45
|
||||
- cassowary
|
||||
- codegod100
|
||||
- decentral1se
|
||||
@ -18,5 +17,3 @@
|
||||
- roxxers
|
||||
- vera
|
||||
- yksflip
|
||||
- basebuilder
|
||||
- mayel
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Build image
|
||||
FROM golang:1.24-alpine AS build
|
||||
FROM golang:1.22-alpine AS build
|
||||
|
||||
ENV GOPRIVATE=coopcloud.tech
|
||||
ENV GOPRIVATE coopcloud.tech
|
||||
|
||||
RUN apk add --no-cache \
|
||||
gcc \
|
||||
|
2
Makefile
2
Makefile
@ -2,7 +2,7 @@ ABRA := ./cmd/abra
|
||||
KADABRA := ./cmd/kadabra
|
||||
COMMIT := $(shell git rev-list -1 HEAD)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
GOVERSION := 1.24
|
||||
GOVERSION := 1.22
|
||||
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
|
||||
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
|
||||
GCFLAGS := "all=-l -B"
|
||||
|
@ -1,7 +1,7 @@
|
||||
# `abra`
|
||||
|
||||
[](https://build.coopcloud.tech/toolshed/abra)
|
||||
[](https://goreportcard.com/report/git.coopcloud.tech/toolshed/abra)
|
||||
[](https://build.coopcloud.tech/coop-cloud/abra)
|
||||
[](https://goreportcard.com/report/git.coopcloud.tech/coop-cloud/abra)
|
||||
[](https://pkg.go.dev/coopcloud.tech/abra)
|
||||
|
||||
The Co-op Cloud utility belt 🎩🐇
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
var AppBackupListCommand = &cobra.Command{
|
||||
Use: "list <domain> [flags]",
|
||||
Use: "list <app> [flags]",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List the contents of a snapshot",
|
||||
Args: cobra.ExactArgs(1),
|
||||
@ -61,7 +61,7 @@ var AppBackupListCommand = &cobra.Command{
|
||||
}
|
||||
|
||||
var AppBackupDownloadCommand = &cobra.Command{
|
||||
Use: "download <domain> [flags]",
|
||||
Use: "download <app> [flags]",
|
||||
Aliases: []string{"d"},
|
||||
Short: "Download a snapshot",
|
||||
Long: `Downloads a backup.tar.gz to the current working directory.
|
||||
@ -78,7 +78,7 @@ var AppBackupDownloadCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ var AppBackupDownloadCommand = &cobra.Command{
|
||||
}
|
||||
|
||||
var AppBackupCreateCommand = &cobra.Command{
|
||||
Use: "create <domain> [flags]",
|
||||
Use: "create <app> [flags]",
|
||||
Aliases: []string{"c"},
|
||||
Short: "Create a new snapshot",
|
||||
Args: cobra.ExactArgs(1),
|
||||
@ -143,7 +143,7 @@ var AppBackupCreateCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -174,7 +174,7 @@ var AppBackupCreateCommand = &cobra.Command{
|
||||
}
|
||||
|
||||
var AppBackupSnapshotsCommand = &cobra.Command{
|
||||
Use: "snapshots <domain> [flags]",
|
||||
Use: "snapshots <app> [flags]",
|
||||
Aliases: []string{"s"},
|
||||
Short: "List all snapshots",
|
||||
Args: cobra.ExactArgs(1),
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
var AppCheckCommand = &cobra.Command{
|
||||
Use: "check <domain> [flags]",
|
||||
Use: "check <app> [flags]",
|
||||
Aliases: []string{"chk"},
|
||||
Short: "Ensure an app is well configured",
|
||||
Long: `Compare env vars in both the app ".env" and recipe ".env.sample" file.
|
||||
@ -36,7 +36,7 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
var AppCmdCommand = &cobra.Command{
|
||||
Use: "command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
|
||||
Use: "command <app> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
|
||||
Aliases: []string{"cmd"},
|
||||
Short: "Run app commands",
|
||||
Long: `Run an app specific command.
|
||||
@ -92,7 +92,7 @@ does not).`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -183,7 +183,7 @@ does not).`,
|
||||
if err := internal.RunCmdRemote(
|
||||
cl,
|
||||
app,
|
||||
disableTTY,
|
||||
requestTTY,
|
||||
app.Recipe.AbraShPath,
|
||||
targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
|
||||
log.Fatal(err)
|
||||
@ -192,14 +192,14 @@ does not).`,
|
||||
}
|
||||
|
||||
var AppCmdListCommand = &cobra.Command{
|
||||
Use: "list <domain> [flags]",
|
||||
Use: "list <app> [flags]",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List all available commands",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -238,7 +238,7 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
|
||||
var (
|
||||
local bool
|
||||
remoteUser string
|
||||
disableTTY bool
|
||||
requestTTY bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -259,11 +259,11 @@ func init() {
|
||||
)
|
||||
|
||||
AppCmdCommand.Flags().BoolVarP(
|
||||
&disableTTY,
|
||||
&requestTTY,
|
||||
"tty",
|
||||
"T",
|
||||
"t",
|
||||
false,
|
||||
"disable remote TTY",
|
||||
"request remote TTY",
|
||||
)
|
||||
|
||||
AppCmdCommand.Flags().BoolVarP(
|
||||
|
@ -13,7 +13,7 @@ func TestParseCmdArgs(t *testing.T) {
|
||||
}{
|
||||
// `--` is not parsed when passed in from the command-line e.g. -- foo bar baz
|
||||
// so we need to eumlate that as missing when testing if bash args are passed in
|
||||
// see https://git.coopcloud.tech/toolshed/organising/issues/336 for more
|
||||
// see https://git.coopcloud.tech/coop-cloud/organising/issues/336 for more
|
||||
{[]string{"foo.com", "app", "test"}, false, ""},
|
||||
{[]string{"foo.com", "app", "test", "foo"}, true, "foo "},
|
||||
{[]string{"foo.com", "app", "test", "foo", "bar", "baz"}, true, "foo bar baz "},
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
var AppConfigCommand = &cobra.Command{
|
||||
Use: "config <domain> [flags]",
|
||||
Use: "config <app> [flags]",
|
||||
Aliases: []string{"cfg"},
|
||||
Short: "Edit app config",
|
||||
Example: " abra config 1312.net",
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/upstream/container"
|
||||
"github.com/docker/cli/cli/command"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
@ -26,14 +26,14 @@ import (
|
||||
)
|
||||
|
||||
var AppCpCommand = &cobra.Command{
|
||||
Use: "cp <domain> <src> <dst> [flags]",
|
||||
Use: "cp <app> <src> <dst> [flags]",
|
||||
Aliases: []string{"c"},
|
||||
Short: "Copy files to/from a deployed app service",
|
||||
Example: ` # copy myfile.txt to the root of the app service
|
||||
abra app cp 1312.net myfile.txt app:/
|
||||
|
||||
# copy that file back to your current working directory locally
|
||||
abra app cp 1312.net app:/myfile.txt ./`,
|
||||
abra app cp 1312.net app:/myfile.txt`,
|
||||
Args: cobra.ExactArgs(3),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
@ -49,7 +49,7 @@ var AppCpCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -134,7 +134,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
|
||||
if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
@ -162,7 +162,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
|
||||
}
|
||||
|
||||
log.Debugf("copy %s from local to %s on container", srcPath, dstPath)
|
||||
copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -173,7 +173,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
|
||||
if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
|
@ -2,11 +2,10 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
@ -19,19 +18,18 @@ import (
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppDeployCommand = &cobra.Command{
|
||||
Use: "deploy <domain> [version] [flags]",
|
||||
Use: "deploy <app> [version] [flags]",
|
||||
Aliases: []string{"d"},
|
||||
Short: "Deploy an app",
|
||||
Long: `Deploy an app.
|
||||
|
||||
This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
|
||||
checkout as-is. Recipe commit hashes are also supported as values for
|
||||
"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`,
|
||||
This command supports chaos operations. Use "--chaos/-c" to deploy your recipe
|
||||
checkout as-is. Recipe commit hashes are also supported values for "[version]".
|
||||
Please note, "upgrade"/"rollback" do not support chaos operations.`,
|
||||
Example: ` # standard deployment
|
||||
abra app deploy 1312.net
|
||||
|
||||
@ -47,8 +45,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string,
|
||||
) ([]string, cobra.ShellCompDirective) {
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
switch l := len(args); l {
|
||||
case 0:
|
||||
return autocomplete.AppNameComplete()
|
||||
@ -64,57 +61,126 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
deployWarnMessages []string
|
||||
toDeployVersion string
|
||||
)
|
||||
var warnMessages []string
|
||||
|
||||
app := internal.ValidateApp(args)
|
||||
stackName := app.StackName()
|
||||
|
||||
if err := validateArgsAndFlags(args); err != nil {
|
||||
ok, err := validateChaosXORVersion(args)
|
||||
if !ok {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
specificVersion := getSpecifiedVersion(args)
|
||||
|
||||
if specificVersion != "" {
|
||||
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
|
||||
app.Recipe.Version = specificVersion
|
||||
}
|
||||
|
||||
if specificVersion == "" && app.Recipe.Version != "" && !internal.Chaos {
|
||||
log.Debugf("retrieved %s as version from env file", app.Recipe.Version)
|
||||
specificVersion = app.Recipe.Version
|
||||
}
|
||||
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("checking whether %s is already deployed", app.StackName())
|
||||
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
|
||||
log.Fatalf("%s is already deployed", app.Name)
|
||||
}
|
||||
|
||||
toDeployVersion, err = getDeployVersion(args, deployMeta, app)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Errorf("get deploy version: %s", err))
|
||||
}
|
||||
|
||||
if !internal.Chaos {
|
||||
_, err = app.Recipe.EnsureVersion(toDeployVersion)
|
||||
if err != nil {
|
||||
log.Fatalf("ensure recipe: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := lint.LintForErrors(app.Recipe); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := validateSecrets(cl, app); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
log.Fatalf("%s is already deployed", app.Name)
|
||||
}
|
||||
|
||||
if !internal.Chaos && specificVersion == "" {
|
||||
versions, err := app.Recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if len(versions) > 0 && !internal.Chaos {
|
||||
toDeployVersion = versions[len(versions)-1]
|
||||
log.Debugf("choosing %s as version to deploy", toDeployVersion)
|
||||
if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
head, err := app.Recipe.Head()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
toDeployVersion = formatter.SmallSHA(head.String())
|
||||
}
|
||||
}
|
||||
|
||||
toDeployChaosVersion := config.CHAOS_DEFAULT
|
||||
if internal.Chaos {
|
||||
if isChaosCommit {
|
||||
toDeployChaosVersion = specificVersion
|
||||
versionLabelLocal, err := app.Recipe.GetVersionLabelLocal()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
toDeployVersion = versionLabelLocal
|
||||
} else {
|
||||
var err error
|
||||
toDeployChaosVersion, err = app.Recipe.ChaosVersion()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@ -128,7 +194,6 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stackName := app.StackName()
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
@ -144,11 +209,8 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
appPkg.ExposeAllEnv(stackName, compose, app.Env)
|
||||
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
||||
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||
if internal.Chaos {
|
||||
appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
|
||||
}
|
||||
appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion)
|
||||
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||
appPkg.SetVersionLabel(compose, stackName, toDeployVersion)
|
||||
|
||||
envVars, err := appPkg.CheckEnv(app)
|
||||
if err != nil {
|
||||
@ -157,22 +219,23 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
|
||||
for _, envVar := range envVars {
|
||||
if !envVar.Present {
|
||||
deployWarnMessages = append(deployWarnMessages,
|
||||
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
|
||||
warnMessages = append(warnMessages,
|
||||
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if !internal.NoDomainChecks {
|
||||
if domainName, ok := app.Env["DOMAIN"]; ok {
|
||||
domainName, ok := app.Env["DOMAIN"]
|
||||
if ok {
|
||||
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
log.Debug("skipping domain checks, no DOMAIN=... configured")
|
||||
log.Debug("skipping domain checks as no DOMAIN=... configured for app")
|
||||
}
|
||||
} else {
|
||||
log.Debug("skipping domain checks")
|
||||
log.Debug("skipping domain checks as requested")
|
||||
}
|
||||
|
||||
deployedVersion := config.NO_VERSION_DEFAULT
|
||||
@ -182,11 +245,11 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
|
||||
if err := internal.DeployOverview(
|
||||
app,
|
||||
warnMessages,
|
||||
deployedVersion,
|
||||
deployMeta.ChaosVersion,
|
||||
toDeployVersion,
|
||||
"",
|
||||
deployWarnMessages,
|
||||
); err != nil {
|
||||
toDeployChaosVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -194,28 +257,9 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
|
||||
|
||||
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
|
||||
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(
|
||||
cl,
|
||||
deployOpts,
|
||||
compose,
|
||||
app.Name,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
f,
|
||||
); err != nil {
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -227,92 +271,31 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
}
|
||||
}
|
||||
|
||||
if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil {
|
||||
log.Fatalf("writing recipe version failed: %s", err)
|
||||
app.Recipe.Version = toDeployVersion
|
||||
if toDeployChaosVersion != config.CHAOS_DEFAULT {
|
||||
app.Recipe.Version = toDeployChaosVersion
|
||||
}
|
||||
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
|
||||
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
|
||||
log.Fatalf("writing new recipe version in env file: %s", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func getLatestVersionOrCommit(app app.App) (string, error) {
|
||||
versions, err := app.Recipe.Tags()
|
||||
if err != nil {
|
||||
return "", err
|
||||
// validateChaosXORVersion xor checks version/chaos mode
|
||||
func validateChaosXORVersion(args []string) (bool, error) {
|
||||
if getSpecifiedVersion(args) != "" && internal.Chaos {
|
||||
return false, errors.New("cannot use <version> and --chaos together")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(versions) > 0 && !internal.Chaos {
|
||||
return versions[len(versions)-1], nil
|
||||
// getSpecifiedVersion retrieves the specific version if available
|
||||
func getSpecifiedVersion(args []string) string {
|
||||
if len(args) >= 2 {
|
||||
return args[1]
|
||||
}
|
||||
|
||||
head, err := app.Recipe.Head()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return formatter.SmallSHA(head.String()), 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 getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app app.App) (string, error) {
|
||||
// Chaos mode overrides everything
|
||||
if internal.Chaos {
|
||||
v, err := app.Recipe.ChaosVersion()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Debugf("version: taking chaos version: %s", v)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Check if the deploy version is set with a cli argument
|
||||
if len(cliArgs) == 2 && cliArgs[1] != "" {
|
||||
log.Debugf("version: taking version from cli arg: %s", cliArgs[1])
|
||||
return cliArgs[1], nil
|
||||
}
|
||||
|
||||
// Check if the recipe has a version in the .env file
|
||||
if app.Recipe.EnvVersion != "" && !internal.IgnoreEnvVersion {
|
||||
if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") {
|
||||
return "", fmt.Errorf("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw)
|
||||
}
|
||||
log.Debugf("version: taking version from .env file: %s", app.Recipe.EnvVersion)
|
||||
return app.Recipe.EnvVersion, nil
|
||||
}
|
||||
|
||||
// Take deployed version
|
||||
if deployMeta.IsDeployed {
|
||||
log.Debugf("version: taking deployed version: %s", deployMeta.Version)
|
||||
return deployMeta.Version, nil
|
||||
}
|
||||
|
||||
v, err := getLatestVersionOrCommit(app)
|
||||
log.Debugf("version: taking new recipe version: %s", v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return v, nil
|
||||
return ""
|
||||
}
|
||||
|
||||
func init() {
|
||||
@ -345,6 +328,6 @@ func init() {
|
||||
"no-converge-checks",
|
||||
"c",
|
||||
false,
|
||||
"disable converge logic checks",
|
||||
"do not wait for converge logic checks",
|
||||
)
|
||||
}
|
||||
|
57
cli/app/deploy_test.go
Normal file
57
cli/app/deploy_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
)
|
||||
|
||||
func TestGetSpecificVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input []string
|
||||
expectedOutput string
|
||||
}{
|
||||
// No specified version when command has one or less args
|
||||
{[]string{}, ""},
|
||||
{[]string{"arg0"}, ""},
|
||||
// Second in arg (index-1) is the specified result when command has more than 1 args
|
||||
{[]string{"arg0", "arg1"}, "arg1"},
|
||||
{[]string{"arg0", "arg1", "arg2"}, "arg1"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if test.expectedOutput != getSpecifiedVersion(test.input) {
|
||||
t.Fatalf("result for %s should be %s", test.input, test.expectedOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChaosXORVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input []string
|
||||
isChaos bool
|
||||
expectedResult bool
|
||||
}{
|
||||
// Chaos = true, Specified Version absent
|
||||
{[]string{}, true, true},
|
||||
// Chaos = false, Specified Version absent
|
||||
{[]string{}, false, true},
|
||||
// Chaos = true, Specified Version present
|
||||
{[]string{"arg0", "arg1"}, true, false},
|
||||
// Chaos = false, Specified Version present
|
||||
{[]string{"arg0", "arg1", "arg2"}, false, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
internal.Chaos = test.isChaos
|
||||
res, _ := validateChaosXORVersion(test.input)
|
||||
if res != test.expectedResult {
|
||||
t.Fatalf(
|
||||
"When args are %s and Chaos mode is %t result needs to be %t",
|
||||
test.input,
|
||||
test.isChaos,
|
||||
test.expectedResult,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
51
cli/app/diff.go
Normal file
51
cli/app/diff.go
Normal file
@ -0,0 +1,51 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppDiffCommand = &cobra.Command{
|
||||
Use: "diff <app> [flags]",
|
||||
Aliases: []string{"df"},
|
||||
Short: "Show diff of app env changes",
|
||||
Long: `This command requires /usr/bin/git.`,
|
||||
Example: " abra app diff 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)
|
||||
|
||||
gitDir := gitPkg.FindDir(app.Path)
|
||||
if gitDir == "" {
|
||||
log.Fatal(fmt.Errorf("no git repo found for %s", app.Name))
|
||||
}
|
||||
|
||||
fpath := app.Path
|
||||
realPath, err := filepath.EvalSymlinks(fpath)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to app env: broken symlink: %s", fpath)
|
||||
}
|
||||
fpath = realPath
|
||||
|
||||
diff, err := gitPkg.DiffUnstaged(gitDir, fpath)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to diff %s: %s", app.Name, err)
|
||||
}
|
||||
|
||||
if diff != "" {
|
||||
fmt.Print(diff)
|
||||
}
|
||||
},
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppEnvCommand = &cobra.Command{
|
||||
Use: "env <domain> [flags]",
|
||||
Aliases: []string{"e"},
|
||||
Short: "Show app .env values",
|
||||
Example: " abra app env 1312.net",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return autocomplete.AppNameComplete()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
var envKeys []string
|
||||
for k := range app.Env {
|
||||
envKeys = append(envKeys, k)
|
||||
}
|
||||
|
||||
sort.Strings(envKeys)
|
||||
|
||||
var rows [][]string
|
||||
for _, k := range envKeys {
|
||||
rows = append(rows, []string{k, app.Env[k]})
|
||||
}
|
||||
|
||||
overview := formatter.CreateOverview("ENV OVERVIEW", rows)
|
||||
fmt.Println(overview)
|
||||
},
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppLabelsCommand = &cobra.Command{
|
||||
Use: "labels <domain> [flags]",
|
||||
Aliases: []string{"lb"},
|
||||
Short: "Show deployment labels",
|
||||
Long: "Both local recipe and live deployment labels are shown.",
|
||||
Example: " abra app labels 1312.net",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return autocomplete.AppNameComplete()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
remoteLabels, err := getLabels(cl, app.StackName())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
rows := [][]string{
|
||||
{"DEPLOYED LABELS", "---"},
|
||||
}
|
||||
|
||||
remoteLabelKeys := make([]string, 0, len(remoteLabels))
|
||||
for k := range remoteLabels {
|
||||
remoteLabelKeys = append(remoteLabelKeys, k)
|
||||
}
|
||||
|
||||
sort.Strings(remoteLabelKeys)
|
||||
|
||||
for _, k := range remoteLabelKeys {
|
||||
rows = append(rows, []string{
|
||||
k,
|
||||
remoteLabels[k],
|
||||
})
|
||||
}
|
||||
|
||||
if len(remoteLabelKeys) == 0 {
|
||||
rows = append(rows, []string{"unknown"})
|
||||
}
|
||||
|
||||
rows = append(rows, []string{"RECIPE LABELS", "---"})
|
||||
|
||||
config, err := app.Recipe.GetComposeConfig(app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var localLabelKeys []string
|
||||
var appServiceConfig composetypes.ServiceConfig
|
||||
for _, service := range config.Services {
|
||||
if service.Name == "app" {
|
||||
appServiceConfig = service
|
||||
|
||||
for k := range service.Deploy.Labels {
|
||||
localLabelKeys = append(localLabelKeys, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(localLabelKeys)
|
||||
|
||||
for _, k := range localLabelKeys {
|
||||
rows = append(rows, []string{
|
||||
k,
|
||||
appServiceConfig.Deploy.Labels[k],
|
||||
})
|
||||
}
|
||||
|
||||
overview := formatter.CreateOverview("LABELS OVERVIEW", rows)
|
||||
fmt.Println(overview)
|
||||
},
|
||||
}
|
||||
|
||||
// getLabels reads docker labels from running services in the format of "coop-cloud.${STACK_NAME}.${LABEL}".
|
||||
func getLabels(cl *dockerClient.Client, stackName string) (map[string]string, error) {
|
||||
labels := make(map[string]string)
|
||||
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
||||
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return labels, err
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
if service.Spec.Name != fmt.Sprintf("%s_app", stackName) {
|
||||
continue
|
||||
}
|
||||
|
||||
for k, v := range service.Spec.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
AppLabelsCommand.Flags().BoolVarP(
|
||||
&internal.Chaos,
|
||||
"chaos",
|
||||
"C",
|
||||
false,
|
||||
"ignore uncommitted recipes changes",
|
||||
)
|
||||
}
|
@ -142,14 +142,10 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
|
||||
appStats.AutoUpdate = autoUpdate
|
||||
|
||||
var newUpdates []string
|
||||
if version != "unknown" && chaos == "false" {
|
||||
if err := app.Recipe.EnsureExists(); err != nil {
|
||||
log.Fatalf("unable to clone %s: %s", app.Name, err)
|
||||
}
|
||||
|
||||
if version != "unknown" {
|
||||
updates, err := app.Recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatalf("unable to retrieve tags for %s: %s", app.Name, err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
|
@ -3,19 +3,28 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/logs"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/docker/api/types"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppLogsCommand = &cobra.Command{
|
||||
Use: "logs <domain> [service] [flags]",
|
||||
Use: "logs <app> [service] [flags]",
|
||||
Aliases: []string{"l"},
|
||||
Short: "Tail app logs",
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
@ -64,25 +73,80 @@ var AppLogsCommand = &cobra.Command{
|
||||
serviceNames = []string{args[1]}
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
opts := logs.TailOpts{
|
||||
AppName: app.Name,
|
||||
Services: serviceNames,
|
||||
StdErr: stdErr,
|
||||
Since: sinceLogs,
|
||||
Filters: f,
|
||||
}
|
||||
|
||||
if err := logs.TailLogs(cl, opts); err != nil {
|
||||
if err = tailLogs(cl, app, serviceNames); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// tailLogs prints logs for the given app with optional service names to be
|
||||
// filtered on. It also checks if the latest task is not runnning and then
|
||||
// prints the past tasks.
|
||||
func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) error {
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: f})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, service := range services {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", service.Spec.Name)
|
||||
tasks, err := cl.TaskList(context.Background(), types.TaskListOptions{Filters: f})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tasks) > 0 {
|
||||
// Need to sort the tasks by the CreatedAt field in the inverse order.
|
||||
// Otherwise they are in the reversed order and not sorted properly.
|
||||
slices.SortFunc[[]swarm.Task](tasks, func(t1, t2 swarm.Task) int {
|
||||
return int(t2.Meta.CreatedAt.Unix() - t1.Meta.CreatedAt.Unix())
|
||||
})
|
||||
lastTask := tasks[0].Status
|
||||
if lastTask.State != swarm.TaskStateRunning {
|
||||
for _, task := range tasks {
|
||||
log.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect the logs in a go routine, so the logs from all services are
|
||||
// collected in parallel.
|
||||
wg.Add(1)
|
||||
go func(serviceID string) {
|
||||
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
|
||||
ShowStderr: true,
|
||||
ShowStdout: !stdErr,
|
||||
Since: sinceLogs,
|
||||
Until: "",
|
||||
Timestamps: true,
|
||||
Follow: true,
|
||||
Tail: "20",
|
||||
Details: false,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer logs.Close()
|
||||
|
||||
_, err = io.Copy(os.Stdout, logs)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}(service.ID)
|
||||
}
|
||||
|
||||
// Wait for all log streams to be closed.
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
stdErr bool
|
||||
sinceLogs string
|
||||
|
@ -25,7 +25,7 @@ This new app configuration is stored in your $ABRA_DIR directory under the
|
||||
appropriate server.
|
||||
|
||||
This command does not deploy your app for you. You will need to run "abra app
|
||||
deploy <domain>" to do so.
|
||||
deploy <app>" to do so.
|
||||
|
||||
You can see what recipes are available (i.e. values for the [recipe] argument)
|
||||
by running "abra recipe ls".
|
||||
@ -75,22 +75,25 @@ var AppNewCommand = &cobra.Command{
|
||||
|
||||
chaosVersion := config.CHAOS_DEFAULT
|
||||
if internal.Chaos {
|
||||
var err error
|
||||
chaosVersion, err = recipe.ChaosVersion()
|
||||
if err != nil {
|
||||
recipeVersion = chaosVersion
|
||||
|
||||
if !internal.Offline {
|
||||
if err := recipe.EnsureUpToDate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recipeVersion = chaosVersion
|
||||
} else {
|
||||
if !internal.Chaos {
|
||||
if err := recipe.EnsureIsClean(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var recipeVersions recipePkg.RecipeVersions
|
||||
if recipeVersion == "" {
|
||||
var err error
|
||||
recipeVersions, _, err = recipe.GetRecipeVersions()
|
||||
recipeVersions, err = recipe.GetRecipeVersions()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -109,16 +112,6 @@ var AppNewCommand = &cobra.Command{
|
||||
if err := recipe.EnsureLatest(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if recipeVersion == "" {
|
||||
head, err := recipe.Head()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to retrieve latest commit for %s: %s", recipe.Name, err)
|
||||
}
|
||||
|
||||
recipeVersion = formatter.SmallSHA(head.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := ensureServerFlag(); err != nil {
|
||||
@ -194,7 +187,7 @@ var AppNewCommand = &cobra.Command{
|
||||
newAppServer = "local"
|
||||
}
|
||||
|
||||
log.Infof("%s created (version: %s)", appDomain, recipeVersion)
|
||||
log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion)
|
||||
|
||||
if len(appSecrets) > 0 {
|
||||
rows := [][]string{}
|
||||
@ -208,8 +201,8 @@ var AppNewCommand = &cobra.Command{
|
||||
|
||||
log.Warnf(
|
||||
"secrets are %s shown again, please save them %s",
|
||||
formatter.BoldUnderlineStyle.Render("NOT"),
|
||||
formatter.BoldUnderlineStyle.Render("NOW"),
|
||||
formatter.BoldStyle.Render("NOT"),
|
||||
formatter.BoldStyle.Render("NOW"),
|
||||
)
|
||||
}
|
||||
|
||||
@ -218,8 +211,9 @@ var AppNewCommand = &cobra.Command{
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("choosing %s as version to save to env file", recipeVersion)
|
||||
if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
|
||||
log.Fatalf("writing recipe version failed: %s", err)
|
||||
log.Fatalf("writing new recipe version in env file: %s", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -302,12 +296,6 @@ func ensureServerFlag() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(servers) == 1 {
|
||||
newAppServer = servers[0]
|
||||
log.Infof("single server detected, choosing %s automatically", newAppServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
if newAppServer == "" && !internal.NoInput {
|
||||
prompt := &survey.Select{
|
||||
Message: "Select app server:",
|
||||
|
@ -4,8 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
@ -24,9 +22,9 @@ import (
|
||||
)
|
||||
|
||||
var AppPsCommand = &cobra.Command{
|
||||
Use: "ps <domain> [flags]",
|
||||
Use: "ps <app> [flags]",
|
||||
Aliases: []string{"p"},
|
||||
Short: "Check app deployment status",
|
||||
Short: "Check app status",
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
@ -37,7 +35,7 @@ var AppPsCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -59,11 +57,9 @@ var AppPsCommand = &cobra.Command{
|
||||
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
|
||||
if statusMeta, ok := statuses[app.StackName()]; ok {
|
||||
if isChaos, exists := statusMeta["chaos"]; exists && isChaos == "true" {
|
||||
if cVersion, exists := statusMeta["chaosVersion"]; exists {
|
||||
chaosVersion = cVersion
|
||||
if strings.HasSuffix(chaosVersion, config.DIRTY_DEFAULT) {
|
||||
chaosVersion = formatter.BoldDirtyDefault(chaosVersion)
|
||||
}
|
||||
chaosVersion, err = app.Recipe.ChaosVersion()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -92,14 +88,9 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
|
||||
return
|
||||
}
|
||||
|
||||
services := compose.Services
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return services[i].Name < services[j].Name
|
||||
})
|
||||
|
||||
var rows [][]string
|
||||
allContainerStats := make(map[string]map[string]string)
|
||||
for _, service := range services {
|
||||
for _, service := range compose.Services {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
|
||||
|
||||
@ -149,10 +140,10 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
|
||||
|
||||
row := []string{
|
||||
containerStats["service"],
|
||||
containerStats["status"],
|
||||
containerStats["image"],
|
||||
dVersion,
|
||||
cVersion,
|
||||
containerStats["status"],
|
||||
}
|
||||
|
||||
rows = append(rows, row)
|
||||
@ -176,10 +167,10 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
|
||||
|
||||
headers := []string{
|
||||
"SERVICE",
|
||||
"STATUS",
|
||||
"IMAGE",
|
||||
"VERSION",
|
||||
"CHAOS",
|
||||
"STATUS",
|
||||
}
|
||||
|
||||
table.
|
||||
|
110
cli/app/push.go
Normal file
110
cli/app/push.go
Normal file
@ -0,0 +1,110 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppPushCommand = &cobra.Command{
|
||||
Use: "push <app> [flags]",
|
||||
Aliases: []string{"pu"},
|
||||
Short: "Push app changes to a remote",
|
||||
Long: `Run "abra app pull <app>" beforehand to reduce conflicts.`,
|
||||
Example: "abra app push 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)
|
||||
|
||||
gitDir := gitPkg.FindDir(app.Path)
|
||||
if gitDir == "" {
|
||||
log.Fatal(fmt.Errorf("no git repo found for %s", app.Name))
|
||||
}
|
||||
|
||||
appDir := filepath.Dir(app.Path)
|
||||
log.Infof("%s currently has these unstaged changes 👇", app.Name)
|
||||
diff, err := gitPkg.DiffUnstaged(appDir, app.Path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if diff == "" {
|
||||
log.Infof("no diff for %s, nothing to push", app.Name)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Print(diff)
|
||||
|
||||
var confirmPush bool
|
||||
if !internal.NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
Message: "push these changes?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &confirmPush); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if msg == "" && !internal.NoInput {
|
||||
prompt := &survey.Input{
|
||||
Message: "commit message?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &msg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if msg == "" {
|
||||
log.Fatal("missing --msg/-m")
|
||||
}
|
||||
|
||||
if confirmPush || internal.NoInput {
|
||||
fname := filepath.Base(app.Path)
|
||||
if err := gitPkg.CommitFile(gitDir, fname, msg, internal.Dry); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := gitPkg.Push(gitDir, "origin", false, internal.Dry); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Info("changes pushed successfully 🦋")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
msg string
|
||||
)
|
||||
|
||||
func init() {
|
||||
AppPushCommand.Flags().StringVarP(
|
||||
&msg,
|
||||
"msg",
|
||||
"m",
|
||||
"",
|
||||
"commit message",
|
||||
)
|
||||
|
||||
AppPushCommand.Flags().BoolVarP(
|
||||
&internal.Dry,
|
||||
"dry-run",
|
||||
"r",
|
||||
false,
|
||||
"report changes that would be made",
|
||||
)
|
||||
}
|
@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
var AppRemoveCommand = &cobra.Command{
|
||||
Use: "remove <domain> [flags]",
|
||||
Use: "remove <app> [flags]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Remove all app data, locally and remotely",
|
||||
Long: `Remove everything related to an app which is already undeployed.
|
||||
|
@ -9,20 +9,18 @@ import (
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/ui"
|
||||
upstream "coopcloud.tech/abra/pkg/upstream/service"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppRestartCommand = &cobra.Command{
|
||||
Use: "restart <domain> [[service] | --all-services] [flags]",
|
||||
Use: "restart <app> [[service] | --all-services] [flags]",
|
||||
Aliases: []string{"re"},
|
||||
Short: "Restart an app",
|
||||
Long: `This command restarts services within a deployed app.
|
||||
|
||||
Run "abra app ps <domain>" to see a list of service names.
|
||||
Run "abra app ps <app>" to see a list of service names.
|
||||
|
||||
Pass "--all-services/-a" to restart all services.`,
|
||||
Example: ` # restart a single app service
|
||||
@ -50,7 +48,7 @@ Pass "--all-services/-a" to restart all services.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(false, false); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -95,36 +93,13 @@ Pass "--all-services/-a" to restart all services.`,
|
||||
for _, serviceName := range serviceNames {
|
||||
stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
|
||||
|
||||
service, _, err := cl.ServiceInspectWithRaw(
|
||||
context.Background(),
|
||||
stackServiceName,
|
||||
types.ServiceInspectOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("attempting to scale %s to 0", stackServiceName)
|
||||
|
||||
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
waitOpts := stack.WaitOpts{
|
||||
Services: []ui.ServiceMeta{{Name: stackServiceName, ID: service.ID}},
|
||||
AppName: app.Name,
|
||||
ServerName: app.Server,
|
||||
Filters: f,
|
||||
NoLog: true,
|
||||
Quiet: true,
|
||||
}
|
||||
|
||||
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
|
||||
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -135,7 +110,7 @@ Pass "--all-services/-a" to restart all services.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
|
||||
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -148,13 +123,6 @@ Pass "--all-services/-a" to restart all services.`,
|
||||
var allServices bool
|
||||
|
||||
func init() {
|
||||
AppRestartCommand.Flags().BoolVarP(
|
||||
&internal.Chaos,
|
||||
"chaos",
|
||||
"C",
|
||||
false,
|
||||
"ignore uncommitted recipes changes",
|
||||
)
|
||||
AppRestartCommand.Flags().BoolVarP(
|
||||
&allServices,
|
||||
"all-services",
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
var AppRestoreCommand = &cobra.Command{
|
||||
Use: "restore <domain> [flags]",
|
||||
Use: "restore <app> [flags]",
|
||||
Aliases: []string{"rs"},
|
||||
Short: "Restore a snapshot",
|
||||
Long: `Snapshots are restored while apps are deployed.
|
||||
@ -28,7 +28,7 @@ Some restore scenarios may require service / app restarts.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
@ -21,23 +20,16 @@ import (
|
||||
)
|
||||
|
||||
var AppRollbackCommand = &cobra.Command{
|
||||
Use: "rollback <domain> [version] [flags]",
|
||||
Use: "rollback <app> [version] [flags]",
|
||||
Aliases: []string{"rl"},
|
||||
Short: "Roll an app back to a previous version",
|
||||
Long: `This command rolls an app back to a previous version.
|
||||
|
||||
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
|
||||
versions are supported values for "[version]".
|
||||
Unlike "deploy", chaos operations are not supported here. Only recipe versions
|
||||
are supported values for "[<version>]".
|
||||
|
||||
It is possible to "--force/-f" an downgrade if you want to re-deploy a specific
|
||||
version.
|
||||
|
||||
Only the deployed version is consulted when trying to determine what downgrades
|
||||
are available. The live deployment version is the "source of truth" in this
|
||||
case. The stored .env version is not consulted.
|
||||
|
||||
A downgrade can be destructive, please ensure you have a copy of your app data
|
||||
beforehand. See "abra app backup" for more.`,
|
||||
A rollback can be destructive, please ensure you have a copy of your app data
|
||||
beforehand.`,
|
||||
Example: ` # standard rollback
|
||||
abra app rollback 1312.net
|
||||
|
||||
@ -63,15 +55,26 @@ beforehand. See "abra app backup" for more.`,
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
downgradeWarnMessages []string
|
||||
chosenDowngrade string
|
||||
availableDowngrades []string
|
||||
)
|
||||
var warnMessages []string
|
||||
|
||||
app := internal.ValidateApp(args)
|
||||
stackName := app.StackName()
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
var specificVersion string
|
||||
if len(args) == 2 {
|
||||
specificVersion = args[1]
|
||||
}
|
||||
|
||||
if specificVersion != "" {
|
||||
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
|
||||
app.Recipe.Version = specificVersion
|
||||
}
|
||||
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := lint.LintForErrors(app.Recipe); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -80,13 +83,15 @@ beforehand. See "abra app backup" for more.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
deployMeta, err := ensureDeployed(cl, app)
|
||||
log.Debugf("checking whether %s is already deployed", stackName)
|
||||
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := lint.LintForErrors(app.Recipe); err != nil {
|
||||
log.Fatal(err)
|
||||
if !deployMeta.IsDeployed {
|
||||
log.Fatalf("%s is not deployed?", app.Name)
|
||||
}
|
||||
|
||||
versions, err := app.Recipe.Tags()
|
||||
@ -94,56 +99,84 @@ beforehand. See "abra app backup" for more.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// NOTE(d1): we've no idea what the live deployment version is, so every
|
||||
// possible downgrade can be shown. it's up to the user to make the choice
|
||||
if deployMeta.Version == config.UNKNOWN_DEFAULT {
|
||||
var availableDowngrades []string
|
||||
if deployMeta.Version == "unknown" {
|
||||
availableDowngrades = versions
|
||||
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
|
||||
}
|
||||
|
||||
if len(args) == 2 && args[1] != "" {
|
||||
chosenDowngrade = args[1]
|
||||
|
||||
if err := validateDowngradeVersionArg(chosenDowngrade, app, deployMeta); err != nil {
|
||||
log.Fatal(err)
|
||||
if specificVersion != "" {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||
if err != nil {
|
||||
log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
|
||||
}
|
||||
|
||||
availableDowngrades = append(availableDowngrades, chosenDowngrade)
|
||||
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
|
||||
if err != nil {
|
||||
log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
|
||||
}
|
||||
|
||||
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenDowngrade == "" {
|
||||
downgradeAvailable, err := ensureDowngradesAvailable(versions, &availableDowngrades, deployMeta)
|
||||
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) {
|
||||
log.Fatalf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
|
||||
}
|
||||
|
||||
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
|
||||
log.Fatalf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
|
||||
}
|
||||
|
||||
availableDowngrades = append(availableDowngrades, specificVersion)
|
||||
}
|
||||
|
||||
if deployMeta.Version != "unknown" && specificVersion == "" {
|
||||
if deployMeta.IsChaos {
|
||||
warnMessages = append(warnMessages, fmt.Sprintf("attempting to rollback a chaos deployment"))
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !downgradeAvailable {
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if parsedVersion.IsLessThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
|
||||
availableDowngrades = append(availableDowngrades, version)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availableDowngrades) == 0 && !internal.Force {
|
||||
log.Info("no available downgrades")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if internal.Force || internal.NoInput || chosenDowngrade != "" {
|
||||
var chosenDowngrade string
|
||||
if len(availableDowngrades) > 0 {
|
||||
if internal.Force || internal.NoInput || specificVersion != "" {
|
||||
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
|
||||
}
|
||||
log.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade)
|
||||
} else {
|
||||
if err := chooseDowngrade(availableDowngrades, deployMeta, &chosenDowngrade); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version)
|
||||
if deployMeta.IsChaos {
|
||||
msg = fmt.Sprintf("please select a downgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion)
|
||||
}
|
||||
|
||||
if internal.Force &&
|
||||
chosenDowngrade == "" &&
|
||||
deployMeta.Version != config.UNKNOWN_DEFAULT {
|
||||
chosenDowngrade = deployMeta.Version
|
||||
prompt := &survey.Select{
|
||||
Message: msg,
|
||||
Options: internal.SortVersionsDesc(availableDowngrades),
|
||||
}
|
||||
|
||||
if chosenDowngrade == "" {
|
||||
log.Fatal("unknown deployed version, unable to downgrade")
|
||||
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("choosing %s as version to rollback", chosenDowngrade)
|
||||
|
||||
if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -161,7 +194,6 @@ beforehand. See "abra app backup" for more.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stackName := app.StackName()
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
@ -178,143 +210,36 @@ beforehand. See "abra app backup" for more.`,
|
||||
appPkg.ExposeAllEnv(stackName, compose, app.Env)
|
||||
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
||||
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||
if internal.Chaos {
|
||||
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
|
||||
}
|
||||
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||
|
||||
// NOTE(d1): no release notes implemeneted for rolling back
|
||||
if err := internal.DeployOverview(
|
||||
app,
|
||||
deployMeta.Version,
|
||||
chosenDowngrade,
|
||||
"",
|
||||
downgradeWarnMessages,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
|
||||
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(
|
||||
cl,
|
||||
deployOpts,
|
||||
compose,
|
||||
stackName,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
|
||||
log.Fatalf("writing recipe version failed: %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)
|
||||
|
||||
chaosVersion := config.CHAOS_DEFAULT
|
||||
if deployMeta.IsChaos {
|
||||
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
|
||||
chaosVersion = deployMeta.ChaosVersion
|
||||
}
|
||||
|
||||
msg = fmt.Sprintf(
|
||||
"please select a downgrade (version: %s, chaos: %s):",
|
||||
// NOTE(d1): no release notes implemeneted for rolling back
|
||||
if err := internal.NewVersionOverview(
|
||||
app,
|
||||
warnMessages,
|
||||
"rollback",
|
||||
deployMeta.Version,
|
||||
chaosVersion,
|
||||
)
|
||||
chosenDowngrade,
|
||||
""); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
prompt := &survey.Select{
|
||||
Message: msg,
|
||||
Options: internal.SortVersionsDesc(availableDowngrades),
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, chosenDowngrade); err != nil {
|
||||
return err
|
||||
app.Recipe.Version = chosenDowngrade
|
||||
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
|
||||
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
|
||||
log.Fatalf("writing new recipe version in env file: %s", err)
|
||||
}
|
||||
|
||||
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("current deployment '%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() {
|
||||
@ -338,6 +263,6 @@ func init() {
|
||||
&internal.DontWaitConverge, "no-converge-checks",
|
||||
"c",
|
||||
false,
|
||||
"disable converge logic checks",
|
||||
"do not wait for converge logic checks",
|
||||
)
|
||||
}
|
||||
|
@ -11,13 +11,13 @@ import (
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/upstream/container"
|
||||
"github.com/docker/cli/cli/command"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppRunCommand = &cobra.Command{
|
||||
Use: "run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]",
|
||||
Use: "run <app> <service> <cmd> [[args] [flags] | [flags] -- [args]]",
|
||||
Aliases: []string{"r"},
|
||||
Short: "Run a command inside a service container",
|
||||
Example: ` # run <cmd> with args/flags
|
||||
@ -64,7 +64,7 @@ var AppRunCommand = &cobra.Command{
|
||||
}
|
||||
|
||||
userCmd := args[2:]
|
||||
execCreateOpts := containertypes.ExecOptions{
|
||||
execCreateOpts := types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
|
@ -20,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
var AppSecretGenerateCommand = &cobra.Command{
|
||||
Use: "generate <domain> [[secret] [version] | --all] [flags]",
|
||||
Use: "generate <app> [[secret] [version] | --all] [flags]",
|
||||
Aliases: []string{"g"},
|
||||
Short: "Generate secrets",
|
||||
Args: cobra.RangeArgs(1, 3),
|
||||
@ -45,15 +45,15 @@ var AppSecretGenerateCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if len(args) <= 2 && !generateAllSecrets {
|
||||
if len(args) == 1 && !generateAllSecrets {
|
||||
log.Fatal("missing arguments [secret]/[version] or '--all'")
|
||||
}
|
||||
|
||||
if len(args) > 2 && generateAllSecrets {
|
||||
if len(args) > 1 && generateAllSecrets {
|
||||
log.Fatal("cannot use '[secret] [version]' and '--all' together")
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ var AppSecretGenerateCommand = &cobra.Command{
|
||||
}
|
||||
|
||||
var AppSecretInsertCommand = &cobra.Command{
|
||||
Use: "insert <domain> <secret> <version> <data> [flags]",
|
||||
Use: "insert <app> <secret> <version> <data> [flags]",
|
||||
Aliases: []string{"i"},
|
||||
Short: "Insert secret",
|
||||
Long: `This command inserts a secret into an app environment.
|
||||
@ -170,7 +170,7 @@ environment. Typically, you can let Abra generate them for you on app creation
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -230,7 +230,7 @@ func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string
|
||||
}
|
||||
|
||||
var AppSecretRmCommand = &cobra.Command{
|
||||
Use: "remove <domain> [[secret] | --all] [flags]",
|
||||
Use: "remove <app> [[secret] | --all] [flags]",
|
||||
Aliases: []string{"rm"},
|
||||
Short: "Remove a secret",
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
@ -258,7 +258,7 @@ var AppSecretRmCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -338,7 +338,7 @@ var AppSecretRmCommand = &cobra.Command{
|
||||
}
|
||||
|
||||
var AppSecretLsCommand = &cobra.Command{
|
||||
Use: "list <domain>",
|
||||
Use: "list <app>",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List all secrets",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
@ -351,7 +351,7 @@ var AppSecretLsCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
var AppServicesCommand = &cobra.Command{
|
||||
Use: "services <domain> [flags]",
|
||||
Use: "services <app> [flags]",
|
||||
Aliases: []string{"sr"},
|
||||
Short: "Display all services of an app",
|
||||
Args: cobra.ExactArgs(1),
|
||||
@ -30,7 +30,7 @@ var AppServicesCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
var AppUndeployCommand = &cobra.Command{
|
||||
Use: "undeploy <domain> [flags]",
|
||||
Use: "undeploy <app> [flags]",
|
||||
Aliases: []string{"un"},
|
||||
Short: "Undeploy an app",
|
||||
Long: `This does not destroy any application data.
|
||||
@ -54,36 +54,20 @@ Passing "--prune/-p" does not remove those volumes.`,
|
||||
log.Fatalf("%s is not deployed?", app.Name)
|
||||
}
|
||||
|
||||
if err := internal.DeployOverview(
|
||||
chaosVersion := config.CHAOS_DEFAULT
|
||||
if deployMeta.IsChaos {
|
||||
chaosVersion = deployMeta.ChaosVersion
|
||||
}
|
||||
|
||||
if err := internal.UndeployOverview(
|
||||
app,
|
||||
deployMeta.Version,
|
||||
config.NO_DOMAIN_DEFAULT,
|
||||
"",
|
||||
nil,
|
||||
); err != nil {
|
||||
chaosVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Info("initialising undeploy")
|
||||
|
||||
rmOpts := stack.Remove{
|
||||
Namespaces: []string{stackName},
|
||||
Namespaces: []string{app.StackName()},
|
||||
Detach: false,
|
||||
}
|
||||
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
|
||||
@ -96,10 +80,9 @@ Passing "--prune/-p" does not remove those volumes.`,
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("undeploy succeeded 🟢")
|
||||
|
||||
log.Debugf("choosing %s as version to save to env file", deployMeta.Version)
|
||||
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
|
||||
log.Fatalf("writing recipe version failed: %s", err)
|
||||
log.Fatalf("writing undeployed recipe version in env file: %s", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -3,50 +3,37 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppUpgradeCommand = &cobra.Command{
|
||||
Use: "upgrade <domain> [version] [flags]",
|
||||
Use: "upgrade <app> [version] [flags]",
|
||||
Aliases: []string{"up"},
|
||||
Short: "Upgrade an app",
|
||||
Long: `Upgrade an app.
|
||||
|
||||
Unlike "abra app deploy", chaos operations are not supported here. Only recipe
|
||||
versions are supported values for "[version]".
|
||||
|
||||
It is possible to "--force/-f" an upgrade if you want to re-deploy a specific
|
||||
version.
|
||||
|
||||
Only the deployed version is consulted when trying to determine what upgrades
|
||||
are available. The live deployment version is the "source of truth" in this
|
||||
case. The stored .env version is not consulted.
|
||||
Unlike "deploy", chaos operations are not supported here. Only recipe versions
|
||||
are supported values for "[version]".
|
||||
|
||||
An upgrade can be destructive, please ensure you have a copy of your app data
|
||||
beforehand. See "abra app backup" for more.`,
|
||||
beforehand.`,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string,
|
||||
) ([]string, cobra.ShellCompDirective) {
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
switch l := len(args); l {
|
||||
case 0:
|
||||
return autocomplete.AppNameComplete()
|
||||
@ -62,32 +49,22 @@ beforehand. See "abra app backup" for more.`,
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
upgradeWarnMessages []string
|
||||
chosenUpgrade string
|
||||
availableUpgrades []string
|
||||
upgradeReleaseNotes string
|
||||
)
|
||||
var warnMessages []string
|
||||
|
||||
app := internal.ValidateApp(args)
|
||||
stackName := app.StackName()
|
||||
|
||||
if err := app.Recipe.Ensure(recipe.EnsureContext{
|
||||
Chaos: internal.Chaos,
|
||||
Offline: internal.Offline,
|
||||
// Ignore the env version for now, to make sure we are at the latest commit.
|
||||
// This enables us to get release notes, that were added after a release.
|
||||
IgnoreEnvVersion: true,
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
var specificVersion string
|
||||
if len(args) == 2 {
|
||||
specificVersion = args[1]
|
||||
}
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if specificVersion != "" {
|
||||
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
|
||||
app.Recipe.Version = specificVersion
|
||||
}
|
||||
|
||||
deployMeta, err := ensureDeployed(cl, app)
|
||||
if err != nil {
|
||||
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -95,68 +72,134 @@ beforehand. See "abra app backup" for more.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("checking whether %s is already deployed", stackName)
|
||||
|
||||
cl, err := client.New(app.Server)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !deployMeta.IsDeployed {
|
||||
log.Fatalf("%s is not deployed?", app.Name)
|
||||
}
|
||||
|
||||
versions, err := app.Recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// NOTE(d1): we've no idea what the live deployment version is, so every
|
||||
// possible upgrade can be shown. it's up to the user to make the choice
|
||||
if deployMeta.Version == config.UNKNOWN_DEFAULT {
|
||||
var availableUpgrades []string
|
||||
if deployMeta.Version == "unknown" {
|
||||
availableUpgrades = versions
|
||||
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
|
||||
}
|
||||
|
||||
if len(args) == 2 && args[1] != "" {
|
||||
chosenUpgrade = args[1]
|
||||
|
||||
if err := validateUpgradeVersionArg(chosenUpgrade, app, deployMeta); err != nil {
|
||||
log.Fatal(err)
|
||||
if specificVersion != "" {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
|
||||
if err != nil {
|
||||
log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
|
||||
}
|
||||
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
|
||||
if err != nil {
|
||||
log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
|
||||
}
|
||||
|
||||
availableUpgrades = append(availableUpgrades, chosenUpgrade)
|
||||
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) {
|
||||
log.Fatalf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
|
||||
}
|
||||
|
||||
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenUpgrade == "" {
|
||||
upgradeAvailable, err := ensureUpgradesAvailable(versions, &availableUpgrades, deployMeta)
|
||||
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 !upgradeAvailable {
|
||||
if deployMeta.Version != "unknown" && specificVersion == "" {
|
||||
if deployMeta.IsChaos {
|
||||
warnMessages = append(warnMessages, fmt.Sprintf("attempting to upgrade a chaos deployment"))
|
||||
}
|
||||
|
||||
for _, version := range versions {
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
|
||||
availableUpgrades = append(availableUpgrades, version)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availableUpgrades) == 0 && !internal.Force {
|
||||
log.Info("no available upgrades")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if internal.Force || internal.NoInput || chosenUpgrade != "" {
|
||||
var chosenUpgrade string
|
||||
if len(availableUpgrades) > 0 {
|
||||
if internal.Force || internal.NoInput || specificVersion != "" {
|
||||
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
||||
}
|
||||
log.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
|
||||
} else {
|
||||
if err := chooseUpgrade(availableUpgrades, deployMeta, &chosenUpgrade); err != nil {
|
||||
log.Fatal(err)
|
||||
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version)
|
||||
if deployMeta.IsChaos {
|
||||
msg = fmt.Sprintf("please select an upgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion)
|
||||
}
|
||||
|
||||
prompt := &survey.Select{
|
||||
Message: msg,
|
||||
Options: internal.SortVersionsDesc(availableUpgrades),
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if internal.Force &&
|
||||
chosenUpgrade == "" &&
|
||||
deployMeta.Version != config.UNKNOWN_DEFAULT {
|
||||
if internal.Force && chosenUpgrade == "" {
|
||||
warnMessages = append(warnMessages, fmt.Sprintf("%s is already upgraded to latest", app.Name))
|
||||
chosenUpgrade = deployMeta.Version
|
||||
}
|
||||
|
||||
if chosenUpgrade == "" {
|
||||
log.Fatal("unknown deployed version, unable to upgrade")
|
||||
// if release notes written after git tag published, read them before we
|
||||
// check out the tag and then they'll appear to be missing. this covers
|
||||
// when we obviously will forget to write release notes before publishing
|
||||
var releaseNotes string
|
||||
if chosenUpgrade != "" {
|
||||
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, version := range versions {
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
|
||||
note, err := app.Recipe.GetReleaseNotes(version)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if note != "" {
|
||||
releaseNotes += fmt.Sprintf("%s\n", note)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("choosing %s as version to upgrade", chosenUpgrade)
|
||||
|
||||
// Get the release notes before checking out the new version in the
|
||||
// recipe. This enables us to get release notes, that were added after
|
||||
// a release.
|
||||
if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -174,7 +217,6 @@ beforehand. See "abra app backup" for more.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stackName := app.StackName()
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
@ -191,9 +233,7 @@ beforehand. See "abra app backup" for more.`,
|
||||
appPkg.ExposeAllEnv(stackName, compose, app.Env)
|
||||
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
||||
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||
if internal.Chaos {
|
||||
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
|
||||
}
|
||||
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||
|
||||
envVars, err := appPkg.CheckEnv(app)
|
||||
@ -203,31 +243,30 @@ beforehand. See "abra app backup" for more.`,
|
||||
|
||||
for _, envVar := range envVars {
|
||||
if !envVar.Present {
|
||||
upgradeWarnMessages = append(upgradeWarnMessages,
|
||||
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
|
||||
warnMessages = append(warnMessages,
|
||||
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if showReleaseNotes {
|
||||
fmt.Print(upgradeReleaseNotes)
|
||||
fmt.Print(releaseNotes)
|
||||
return
|
||||
}
|
||||
|
||||
if upgradeReleaseNotes == "" {
|
||||
upgradeWarnMessages = append(
|
||||
upgradeWarnMessages,
|
||||
fmt.Sprintf("no release notes available for %s", chosenUpgrade),
|
||||
)
|
||||
chaosVersion := config.CHAOS_DEFAULT
|
||||
if deployMeta.IsChaos {
|
||||
chaosVersion = deployMeta.ChaosVersion
|
||||
}
|
||||
|
||||
if err := internal.DeployOverview(
|
||||
if err := internal.NewVersionOverview(
|
||||
app,
|
||||
warnMessages,
|
||||
"upgrade",
|
||||
deployMeta.Version,
|
||||
chaosVersion,
|
||||
chosenUpgrade,
|
||||
upgradeReleaseNotes,
|
||||
upgradeWarnMessages,
|
||||
); err != nil {
|
||||
releaseNotes); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -235,196 +274,31 @@ beforehand. See "abra app backup" for more.`,
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
|
||||
|
||||
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
|
||||
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(
|
||||
cl,
|
||||
deployOpts,
|
||||
compose,
|
||||
stackName,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
f,
|
||||
); err != nil {
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
|
||||
if ok && !internal.DontWaitConverge {
|
||||
log.Debugf("run the following post-deploy commands: %s", postDeployCmds)
|
||||
|
||||
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
|
||||
log.Fatalf("attempting to run post deploy commands, saw: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil {
|
||||
log.Fatalf("writing recipe version failed: %s", err)
|
||||
app.Recipe.Version = chosenUpgrade
|
||||
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
|
||||
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
|
||||
log.Fatalf("writing new recipe version in env file: %s", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// chooseUpgrade prompts the user to choose an upgrade interactively.
|
||||
func chooseUpgrade(
|
||||
availableUpgrades []string,
|
||||
deployMeta stack.DeployMeta,
|
||||
chosenUpgrade *string,
|
||||
) error {
|
||||
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version)
|
||||
|
||||
if deployMeta.IsChaos {
|
||||
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
|
||||
|
||||
msg = fmt.Sprintf(
|
||||
"please select an upgrade (version: %s, chaos: %s):",
|
||||
deployMeta.Version,
|
||||
chaosVersion,
|
||||
var (
|
||||
showReleaseNotes bool
|
||||
)
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
// NOTE(d1): trim any final newline on the end of the note itself before
|
||||
// we manually handle newlines (for multiple release notes and
|
||||
// ensuring space between the warning messages)
|
||||
note = strings.TrimSuffix(note, "\n")
|
||||
|
||||
*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 fmt.Errorf("'%s' is not a known version", deployMeta.Version)
|
||||
}
|
||||
|
||||
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) &&
|
||||
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
|
||||
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
|
||||
}
|
||||
|
||||
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
|
||||
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureDeployed ensures the app is deployed and if so, returns deployment
|
||||
// meta info.
|
||||
func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, error) {
|
||||
log.Debugf("checking whether %s is already deployed", app.StackName())
|
||||
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
|
||||
if err != nil {
|
||||
return stack.DeployMeta{}, err
|
||||
}
|
||||
|
||||
if !deployMeta.IsDeployed {
|
||||
return stack.DeployMeta{}, fmt.Errorf("%s is not deployed?", app.Name)
|
||||
}
|
||||
|
||||
return deployMeta, nil
|
||||
}
|
||||
|
||||
var showReleaseNotes bool
|
||||
|
||||
func init() {
|
||||
AppUpgradeCommand.Flags().BoolVarP(
|
||||
@ -447,7 +321,7 @@ func init() {
|
||||
&internal.DontWaitConverge, "no-converge-checks",
|
||||
"c",
|
||||
false,
|
||||
"disable converge logic checks",
|
||||
"do not wait for converge logic checks",
|
||||
)
|
||||
|
||||
AppUpgradeCommand.Flags().BoolVarP(
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
var AppVolumeListCommand = &cobra.Command{
|
||||
Use: "list <domain> [flags]",
|
||||
Use: "list <app> [flags]",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List volumes associated with an app",
|
||||
Args: cobra.ExactArgs(1),
|
||||
@ -71,12 +71,12 @@ var AppVolumeListCommand = &cobra.Command{
|
||||
}
|
||||
|
||||
var AppVolumeRemoveCommand = &cobra.Command{
|
||||
Use: "remove <domain> [flags]",
|
||||
Use: "remove <app> [flags]",
|
||||
Short: "Remove volume(s) associated with an app",
|
||||
Long: `Remove volumes associated with an app.
|
||||
|
||||
The app in question must be undeployed before you try to remove volumes. See
|
||||
"abra app undeploy <domain>" for more.
|
||||
"abra app undeploy <app>" for more.
|
||||
|
||||
The command is interactive and will show a multiple select input which allows
|
||||
you to make a seclection. Use the "?" key to see more help on navigating this
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"slices"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
@ -25,17 +24,12 @@ var CatalogueGenerateCommand = &cobra.Command{
|
||||
Short: "Generate the recipe catalogue",
|
||||
Long: `Generate a new copy of the recipe catalogue.
|
||||
|
||||
N.B. this command **will** wipe local unstaged changes from your local recipes
|
||||
if present. "--chaos/-C" on this command refers to the catalogue repository
|
||||
("$ABRA_DIR/catalogue") and not the recipes. Please take care not to lose your
|
||||
changes.
|
||||
|
||||
It is possible to generate new metadata for a single recipe by passing
|
||||
[recipe]. The existing local catalogue will be updated, not overwritten.
|
||||
|
||||
It is quite easy to get rate limited by Docker Hub when running this command.
|
||||
If you have a Hub account you can "docker login" and Abra will automatically
|
||||
use those details.
|
||||
If you have a Hub account you can have Abra log you in to avoid this. Pass
|
||||
"--user" and "--pass".
|
||||
|
||||
Push your new release to git.coopcloud.tech with "--publish/-p". This requires
|
||||
that you have permission to git push to these repositories and have your SSH
|
||||
@ -53,62 +47,56 @@ keys configured on your account.`,
|
||||
recipeName = args[0]
|
||||
}
|
||||
|
||||
r := recipe.Get(recipeName)
|
||||
|
||||
if recipeName != "" {
|
||||
internal.ValidateRecipe(args, cmd.Name())
|
||||
}
|
||||
|
||||
if err := catalogue.EnsureCatalogue(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !internal.Chaos {
|
||||
if err := catalogue.EnsureIsClean(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
repos, err := recipe.ReadReposMetadata(internal.Debug)
|
||||
repos, err := recipe.ReadReposMetadata()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
barLength := len(repos)
|
||||
var barLength int
|
||||
var logMsg string
|
||||
if recipeName != "" {
|
||||
barLength = 1
|
||||
logMsg = fmt.Sprintf("ensuring %v recipe is cloned & up-to-date", barLength)
|
||||
} else {
|
||||
barLength = len(repos)
|
||||
logMsg = fmt.Sprintf("ensuring %v recipes are cloned & up-to-date, this could take some time...", barLength)
|
||||
}
|
||||
|
||||
if !skipUpdates {
|
||||
if err := recipe.UpdateRepositories(repos, recipeName, internal.Debug); err != nil {
|
||||
log.Warn(logMsg)
|
||||
if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var warnings []string
|
||||
catl := make(recipe.RecipeCatalogue)
|
||||
catlBar := formatter.CreateProgressbar(barLength, "collecting catalogue metadata")
|
||||
catlBar := formatter.CreateProgressbar(barLength, "generating catalogue metadata...")
|
||||
for _, recipeMeta := range repos {
|
||||
if recipeName != "" && recipeName != recipeMeta.Name {
|
||||
if !internal.Debug {
|
||||
catlBar.Add(1)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
r := recipe.Get(recipeMeta.Name)
|
||||
versions, warnMsgs, err := r.GetRecipeVersions()
|
||||
versions, err := r.GetRecipeVersions()
|
||||
if err != nil {
|
||||
warnings = append(warnings, err.Error())
|
||||
}
|
||||
if len(warnMsgs) > 0 {
|
||||
warnings = append(warnings, warnMsgs...)
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
features, category, warnMsgs, err := recipe.GetRecipeFeaturesAndCategory(r)
|
||||
features, category, err := recipe.GetRecipeFeaturesAndCategory(r)
|
||||
if err != nil {
|
||||
warnings = append(warnings, err.Error())
|
||||
}
|
||||
if len(warnMsgs) > 0 {
|
||||
warnings = append(warnings, warnMsgs...)
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
catl[recipeMeta.Name] = recipe.RecipeMeta{
|
||||
@ -124,25 +112,8 @@ keys configured on your account.`,
|
||||
Features: features,
|
||||
}
|
||||
|
||||
if !internal.Debug {
|
||||
catlBar.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
if err := catlBar.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var uniqueWarnings []string
|
||||
for _, w := range warnings {
|
||||
if !slices.Contains(uniqueWarnings, w) {
|
||||
uniqueWarnings = append(uniqueWarnings, w)
|
||||
}
|
||||
}
|
||||
|
||||
for _, warnMsg := range uniqueWarnings {
|
||||
log.Warn(warnMsg)
|
||||
}
|
||||
|
||||
recipesJSON, err := json.MarshalIndent(catl, "", " ")
|
||||
if err != nil {
|
||||
@ -171,7 +142,7 @@ keys configured on your account.`,
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("generated recipe catalogue: %s", config.RECIPES_JSON)
|
||||
log.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
|
||||
|
||||
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
|
||||
if publishChanges {
|
||||
@ -197,7 +168,7 @@ keys configured on your account.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
sshURL := fmt.Sprintf(config.TOOLSHED_SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
|
||||
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, config.CATALOGUE_JSON_REPO_NAME)
|
||||
if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -12,16 +12,17 @@ var AutocompleteCommand = &cobra.Command{
|
||||
Long: `To load completions:
|
||||
|
||||
Bash:
|
||||
# Load autocompletion for the current Bash session
|
||||
|
||||
$ source <(abra autocomplete bash)
|
||||
|
||||
# To load autocompletion for each session, execute once:
|
||||
# Linux:
|
||||
$ abra autocomplete bash | sudo tee /etc/bash_completion.d/abra
|
||||
$ abra autocomplete bash > /etc/bash_completion.d/abra
|
||||
# macOS:
|
||||
$ abra autocomplete bash | sudo tee $(brew --prefix)/etc/bash_completion.d/abra
|
||||
$ abra autocomplete bash > $(brew --prefix)/etc/bash_completion.d/abra
|
||||
|
||||
Zsh:
|
||||
|
||||
# If shell autocompletion is not already enabled in your environment,
|
||||
# you will need to enable it. You can execute the following once:
|
||||
|
||||
@ -33,12 +34,14 @@ Zsh:
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
|
||||
fish:
|
||||
|
||||
$ abra autocomplete fish | source
|
||||
|
||||
# To load autocompletions for each session, execute once:
|
||||
$ abra autocomplete fish > ~/.config/fish/completions/abra.fish
|
||||
|
||||
PowerShell:
|
||||
|
||||
PS> abra autocomplete powershell | Out-String | Invoke-Expression
|
||||
|
||||
# To load autocompletions for every new session, run:
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"coopcloud.tech/abra/pkg/upstream/container"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
)
|
||||
@ -48,7 +47,7 @@ func RunBackupCmdRemote(
|
||||
backupCmd string,
|
||||
containerID string,
|
||||
execEnv []string) (io.Writer, error) {
|
||||
execBackupListOpts := containertypes.ExecOptions{
|
||||
execBackupListOpts := types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
|
@ -5,7 +5,6 @@ var (
|
||||
Debug bool
|
||||
NoInput bool
|
||||
Offline bool
|
||||
IgnoreEnvVersion bool
|
||||
|
||||
// NOTE(d1): sub-command specific
|
||||
Chaos bool
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/upstream/container"
|
||||
"github.com/docker/cli/cli/command"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
@ -24,7 +24,7 @@ import (
|
||||
func RunCmdRemote(
|
||||
cl *dockerClient.Client,
|
||||
app appPkg.App,
|
||||
disableTTY bool,
|
||||
requestTTY bool,
|
||||
abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
|
||||
@ -42,7 +42,7 @@ func RunCmdRemote(
|
||||
return err
|
||||
}
|
||||
|
||||
copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -55,7 +55,7 @@ func RunCmdRemote(
|
||||
|
||||
shell := "/bin/bash"
|
||||
findShell := []string{"test", "-e", shell}
|
||||
execCreateOpts := containertypes.ExecOptions{
|
||||
execCreateOpts := types.ExecConfig{
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
@ -84,10 +84,8 @@ func RunCmdRemote(
|
||||
}
|
||||
|
||||
execCreateOpts.Cmd = cmd
|
||||
|
||||
execCreateOpts.Tty = true
|
||||
if disableTTY {
|
||||
execCreateOpts.Tty = false
|
||||
execCreateOpts.Tty = requestTTY
|
||||
if !requestTTY {
|
||||
log.Debugf("not requesting a remote TTY")
|
||||
}
|
||||
|
||||
|
@ -37,21 +37,18 @@ func horizontal(left, mid, right string) string {
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right)
|
||||
}
|
||||
|
||||
func formatComposeFiles(composeFiles string) string {
|
||||
return strings.ReplaceAll(composeFiles, ":", "\n")
|
||||
}
|
||||
|
||||
// DeployOverview shows a deployment overview
|
||||
func DeployOverview(
|
||||
// NewVersionOverview shows an upgrade or downgrade overview
|
||||
func NewVersionOverview(
|
||||
app appPkg.App,
|
||||
deployedVersion string,
|
||||
toDeployVersion string,
|
||||
releaseNotes string,
|
||||
warnMessages []string,
|
||||
) error {
|
||||
kind,
|
||||
deployedVersion,
|
||||
deployedChaosVersion,
|
||||
toDeployVersion,
|
||||
releaseNotes string) error {
|
||||
deployConfig := "compose.yml"
|
||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||
deployConfig = formatComposeFiles(composeFiles)
|
||||
deployConfig = composeFiles
|
||||
}
|
||||
|
||||
server := app.Server
|
||||
@ -64,29 +61,30 @@ func DeployOverview(
|
||||
domain = config.NO_DOMAIN_DEFAULT
|
||||
}
|
||||
|
||||
envVersion := app.Recipe.EnvVersionRaw
|
||||
if envVersion == "" {
|
||||
envVersion = config.NO_VERSION_DEFAULT
|
||||
}
|
||||
|
||||
rows := [][]string{
|
||||
{"DOMAIN", domain},
|
||||
{"RECIPE", app.Recipe.Name},
|
||||
{"SERVER", server},
|
||||
{"CONFIG", deployConfig},
|
||||
{"", ""},
|
||||
{"CURRENT DEPLOYMENT", formatter.BoldDirtyDefault(deployedVersion)},
|
||||
{"ENV VERSION", formatter.BoldDirtyDefault(envVersion)},
|
||||
{"NEW DEPLOYMENT", formatter.BoldDirtyDefault(toDeployVersion)},
|
||||
[]string{"APP", domain},
|
||||
[]string{"RECIPE", app.Recipe.Name},
|
||||
[]string{"SERVER", server},
|
||||
[]string{"DEPLOYED", deployedVersion},
|
||||
[]string{"CURRENT CHAOS ", deployedChaosVersion},
|
||||
[]string{fmt.Sprintf("TO %s", strings.ToUpper(kind)), toDeployVersion},
|
||||
[]string{"CONFIG", deployConfig},
|
||||
}
|
||||
|
||||
deployType := getDeployType(deployedVersion, toDeployVersion)
|
||||
overview := formatter.CreateOverview(fmt.Sprintf("%s OVERVIEW", deployType), rows)
|
||||
overview := formatter.CreateOverview(
|
||||
fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind)),
|
||||
rows,
|
||||
)
|
||||
|
||||
fmt.Println(overview)
|
||||
|
||||
if releaseNotes != "" {
|
||||
if releaseNotes != "" && toDeployVersion != "" {
|
||||
fmt.Print(releaseNotes)
|
||||
} else {
|
||||
warnMessages = append(
|
||||
warnMessages,
|
||||
fmt.Sprintf("no release notes available for %s", toDeployVersion),
|
||||
)
|
||||
}
|
||||
|
||||
for _, msg := range warnMessages {
|
||||
@ -110,34 +108,113 @@ func DeployOverview(
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDeployType(currentVersion, newVersion string) string {
|
||||
if newVersion == config.NO_DOMAIN_DEFAULT {
|
||||
return "UNDEPLOY"
|
||||
// DeployOverview shows a deployment overview
|
||||
func DeployOverview(
|
||||
app appPkg.App,
|
||||
warnMessages []string,
|
||||
deployedVersion string,
|
||||
deployedChaosVersion string,
|
||||
toDeployVersion,
|
||||
toDeployChaosVersion string) error {
|
||||
deployConfig := "compose.yml"
|
||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||
deployConfig = composeFiles
|
||||
}
|
||||
if strings.Contains(newVersion, "+U") {
|
||||
return "CHAOS DEPLOY"
|
||||
|
||||
server := app.Server
|
||||
if app.Server == "default" {
|
||||
server = "local"
|
||||
}
|
||||
if strings.Contains(currentVersion, "+U") {
|
||||
return "UNCHAOS DEPLOY"
|
||||
|
||||
domain := app.Domain
|
||||
if domain == "" {
|
||||
domain = config.NO_DOMAIN_DEFAULT
|
||||
}
|
||||
if currentVersion == newVersion {
|
||||
return "REDEPLOY"
|
||||
|
||||
rows := [][]string{
|
||||
[]string{"APP", domain},
|
||||
[]string{"RECIPE", app.Recipe.Name},
|
||||
[]string{"SERVER", server},
|
||||
[]string{"DEPLOYED", deployedVersion},
|
||||
[]string{"CURRENT CHAOS ", deployedChaosVersion},
|
||||
[]string{"TO DEPLOY", toDeployVersion},
|
||||
[]string{"NEW CHAOS", toDeployChaosVersion},
|
||||
[]string{"CONFIG", deployConfig},
|
||||
}
|
||||
if currentVersion == config.NO_VERSION_DEFAULT {
|
||||
return "NEW DEPLOY"
|
||||
|
||||
overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows)
|
||||
|
||||
fmt.Println(overview)
|
||||
|
||||
for _, msg := range warnMessages {
|
||||
log.Warn(msg)
|
||||
}
|
||||
currentParsed, err := tagcmp.Parse(currentVersion)
|
||||
if err != nil {
|
||||
return "DEPLOY"
|
||||
|
||||
if NoInput {
|
||||
return nil
|
||||
}
|
||||
newParsed, err := tagcmp.Parse(newVersion)
|
||||
if err != nil {
|
||||
return "DEPLOY"
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{Message: "proceed?"}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
if currentParsed.IsLessThan(newParsed) {
|
||||
return "UPGRADE"
|
||||
|
||||
if !response {
|
||||
log.Fatal("deployment cancelled")
|
||||
}
|
||||
return "DOWNGRADE"
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UndeployOverview shows an undeployment overview
|
||||
func UndeployOverview(
|
||||
app appPkg.App,
|
||||
version,
|
||||
chaosVersion string) error {
|
||||
deployConfig := "compose.yml"
|
||||
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
|
||||
deployConfig = composeFiles
|
||||
}
|
||||
|
||||
server := app.Server
|
||||
if app.Server == "default" {
|
||||
server = "local"
|
||||
}
|
||||
|
||||
domain := app.Domain
|
||||
if domain == "" {
|
||||
domain = config.NO_DOMAIN_DEFAULT
|
||||
}
|
||||
|
||||
rows := [][]string{
|
||||
[]string{"APP", domain},
|
||||
[]string{"RECIPE", app.Recipe.Name},
|
||||
[]string{"SERVER", server},
|
||||
[]string{"DEPLOYED", version},
|
||||
[]string{"CHAOS", chaosVersion},
|
||||
[]string{"CONFIG", deployConfig},
|
||||
}
|
||||
|
||||
overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows)
|
||||
|
||||
fmt.Println(overview)
|
||||
|
||||
if NoInput {
|
||||
return nil
|
||||
}
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{Message: "proceed?"}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !response {
|
||||
log.Fatal("undeploy cancelled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostCmds parses a string of commands and executes them inside of the respective services
|
||||
|
@ -1,17 +0,0 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSortVersionsDesc(t *testing.T) {
|
||||
versions := SortVersionsDesc([]string{
|
||||
"0.2.3+1.2.2",
|
||||
"1.0.0+2.2.2",
|
||||
})
|
||||
|
||||
assert.Equal(t, "1.0.0+2.2.2", versions[0])
|
||||
assert.Equal(t, "0.2.3+1.2.2", versions[1])
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package internal
|
||||
|
||||
import "coopcloud.tech/abra/pkg/recipe"
|
||||
|
||||
func GetEnsureContext() recipe.EnsureContext {
|
||||
return recipe.EnsureContext{
|
||||
Chaos,
|
||||
Offline,
|
||||
IgnoreEnvVersion,
|
||||
}
|
||||
}
|
18
cli/internal/errors.go
Normal file
18
cli/internal/errors.go
Normal file
@ -0,0 +1,18 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// ShowSubcommandHelpAndError exits the program on error, logs the error to the
|
||||
// terminal, and shows the help command.
|
||||
func ShowSubcommandHelpAndError(cmd *cli.Command, err interface{}) {
|
||||
if err2 := cli.ShowSubcommandHelp(cmd); err2 != nil {
|
||||
log.Error(err2)
|
||||
}
|
||||
log.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
@ -22,8 +24,12 @@ var RecipeDiffCommand = &cobra.Command{
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
r := internal.ValidateRecipe(args, cmd.Name())
|
||||
if err := gitPkg.DiffUnstaged(r.Dir); err != nil {
|
||||
diff, err := gitPkg.DiffUnstaged(r.Dir, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if diff != "" {
|
||||
fmt.Print(diff)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -1,15 +1,11 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"github.com/go-git/go-git/v5"
|
||||
gitCfg "github.com/go-git/go-git/v5/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -17,16 +13,7 @@ var RecipeFetchCommand = &cobra.Command{
|
||||
Use: "fetch [recipe | --all] [flags]",
|
||||
Aliases: []string{"f"},
|
||||
Short: "Clone recipe(s) locally",
|
||||
Long: `Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`,
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
Example: ` # fetch from recipe catalogue
|
||||
abra recipe fetch gitea
|
||||
|
||||
# fetch from remote recipe
|
||||
abra recipe fetch git.foo.org/recipes/myrecipe
|
||||
|
||||
# fetch with ssh remote for hacking
|
||||
abra recipe fetch gitea --ssh`,
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
@ -47,41 +34,11 @@ var RecipeFetchCommand = &cobra.Command{
|
||||
log.Fatal("cannot use [recipe] and --all/-a together")
|
||||
}
|
||||
|
||||
ensureCtx := internal.GetEnsureContext()
|
||||
if recipeName != "" {
|
||||
r := recipe.Get(recipeName)
|
||||
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
|
||||
if !force {
|
||||
log.Warnf("%s is already fetched", r.Name)
|
||||
return
|
||||
r := internal.ValidateRecipe(args, cmd.Name())
|
||||
if err := r.Ensure(false, false); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
r = internal.ValidateRecipe(args, cmd.Name())
|
||||
|
||||
if sshRemote {
|
||||
if r.SSHURL == "" {
|
||||
log.Warnf("unable to discover SSH remote for %s", r.Name)
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(r.Dir)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to open %s: %s", r.Dir, err)
|
||||
}
|
||||
|
||||
if err = repo.DeleteRemote("origin"); err != nil {
|
||||
log.Fatalf("unable to remove default remote in %s: %s", r.Dir, err)
|
||||
}
|
||||
|
||||
if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{
|
||||
Name: "origin",
|
||||
URLs: []string{r.SSHURL},
|
||||
}); err != nil {
|
||||
log.Fatalf("unable to set SSH remote in %s: %s", r.Dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -93,7 +50,7 @@ var RecipeFetchCommand = &cobra.Command{
|
||||
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
|
||||
for recipeName := range catalogue {
|
||||
r := recipe.Get(recipeName)
|
||||
if err := r.Ensure(ensureCtx); err != nil {
|
||||
if err := r.Ensure(false, false); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
catlBar.Add(1)
|
||||
@ -103,8 +60,6 @@ var RecipeFetchCommand = &cobra.Command{
|
||||
|
||||
var (
|
||||
fetchAllRecipes bool
|
||||
sshRemote bool
|
||||
force bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -115,20 +70,4 @@ func init() {
|
||||
false,
|
||||
"fetch all recipes",
|
||||
)
|
||||
|
||||
RecipeFetchCommand.Flags().BoolVarP(
|
||||
&sshRemote,
|
||||
"ssh",
|
||||
"s",
|
||||
false,
|
||||
"automatically set ssh remote",
|
||||
)
|
||||
|
||||
RecipeFetchCommand.Flags().BoolVarP(
|
||||
&force,
|
||||
"force",
|
||||
"f",
|
||||
false,
|
||||
"force re-fetch",
|
||||
)
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ var RecipeLintCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||
|
||||
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ var RecipeNewCommand = &cobra.Command{
|
||||
if err := os.RemoveAll(gitRepo); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Debugf("removed .git repo in %s", gitRepo)
|
||||
log.Debugf("removed example git repo in %s", gitRepo)
|
||||
|
||||
meta := newRecipeMeta(recipeName)
|
||||
|
||||
@ -76,6 +76,7 @@ var RecipeNewCommand = &cobra.Command{
|
||||
if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil {
|
||||
|
@ -118,9 +118,13 @@ your SSH keys configured on your account.`,
|
||||
|
||||
if !isClean {
|
||||
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
|
||||
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||
diff, err := gitPkg.DiffUnstaged(recipe.Dir, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if diff != "" {
|
||||
fmt.Print(diff)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
@ -267,8 +271,6 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var addNextAsReleaseNotes bool
|
||||
|
||||
nextReleaseNotePath := path.Join(releaseDir, "next")
|
||||
if _, err := os.Stat(nextReleaseNotePath); err == nil {
|
||||
// release/next note exists. Move it to release/<tag>
|
||||
@ -278,37 +280,38 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
||||
}
|
||||
|
||||
if !internal.NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
prompt := &survey.Input{
|
||||
Message: "Use release note in release/next?",
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil {
|
||||
var addReleaseNote bool
|
||||
if err := survey.AskOne(prompt, &addReleaseNote); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !addNextAsReleaseNotes {
|
||||
if !addReleaseNote {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.Rename(nextReleaseNotePath, tagReleaseNotePath); err != nil {
|
||||
err := os.Rename(nextReleaseNotePath, tagReleaseNotePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry); err != nil {
|
||||
err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil {
|
||||
err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE(d1): No release note exists for the current release. Or, we've
|
||||
// already used release/next as the release note
|
||||
if internal.NoInput || addNextAsReleaseNotes {
|
||||
// No release note exists for the current release.
|
||||
if internal.NoInput {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -208,9 +208,13 @@ likely to change.
|
||||
}
|
||||
if !isClean {
|
||||
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
|
||||
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||
diff, err := gitPkg.DiffUnstaged(recipe.Dir, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if diff != "" {
|
||||
fmt.Print(diff)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||
|
||||
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -327,9 +327,13 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
|
||||
}
|
||||
if !isClean {
|
||||
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
|
||||
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||
diff, err := gitPkg.DiffUnstaged(recipe.Dir, "")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if diff != "" {
|
||||
fmt.Print(diff)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -37,13 +37,10 @@ var RecipeVersionCommand = &cobra.Command{
|
||||
if !ok {
|
||||
warnMessages = append(warnMessages, "retrieved versions from local recipe repository")
|
||||
|
||||
recipeVersions, warnMsg, err := recipe.GetRecipeVersions()
|
||||
recipeVersions, err := recipe.GetRecipeVersions()
|
||||
if err != nil {
|
||||
warnMessages = append(warnMessages, err.Error())
|
||||
}
|
||||
if len(warnMsg) > 0 {
|
||||
warnMessages = append(warnMessages, warnMsg...)
|
||||
}
|
||||
|
||||
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
|
||||
}
|
||||
|
34
cli/run.go
34
cli/run.go
@ -35,7 +35,6 @@ func Run(version, commit string) {
|
||||
config.ABRA_DIR,
|
||||
config.SERVERS_DIR,
|
||||
config.RECIPES_DIR,
|
||||
config.LOGS_DIR,
|
||||
config.VENDOR_DIR, // TODO(d1): remove > 0.9.x
|
||||
config.BACKUP_DIR, // TODO(d1): remove > 0.9.x
|
||||
}
|
||||
@ -49,13 +48,9 @@ func Run(version, commit string) {
|
||||
}
|
||||
}
|
||||
|
||||
log.Logger.SetStyles(charmLog.DefaultStyles())
|
||||
log.Logger.SetStyles(log.Styles())
|
||||
charmLog.SetDefault(log.Logger)
|
||||
|
||||
if internal.MachineReadable {
|
||||
log.SetOutput(os.Stderr)
|
||||
}
|
||||
|
||||
if internal.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetOutput(os.Stderr)
|
||||
@ -100,37 +95,20 @@ func Run(version, commit string) {
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.Debug,
|
||||
"debug",
|
||||
"d",
|
||||
false,
|
||||
&internal.Debug, "debug", "d", false,
|
||||
"show debug messages",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.NoInput,
|
||||
"no-input",
|
||||
"n",
|
||||
false,
|
||||
&internal.NoInput, "no-input", "n", false,
|
||||
"toggle non-interactive mode",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.Offline,
|
||||
"offline",
|
||||
"o",
|
||||
false,
|
||||
&internal.Offline, "offline", "o", false,
|
||||
"prefer offline & filesystem access",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.IgnoreEnvVersion,
|
||||
"ignore-env-version",
|
||||
"i",
|
||||
false,
|
||||
"ignore .env version checkout",
|
||||
)
|
||||
|
||||
catalogue.CatalogueCommand.AddCommand(
|
||||
catalogue.CatalogueGenerateCommand,
|
||||
)
|
||||
@ -209,8 +187,8 @@ func Run(version, commit string) {
|
||||
app.AppUndeployCommand,
|
||||
app.AppUpgradeCommand,
|
||||
app.AppVolumeCommand,
|
||||
app.AppLabelsCommand,
|
||||
app.AppEnvCommand,
|
||||
app.AppDiffCommand,
|
||||
app.AppPushCommand,
|
||||
)
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
|
@ -103,7 +103,8 @@ developer machine. The domain is then set to "default".`,
|
||||
|
||||
if _, err := client.New(name, timeout); err != nil {
|
||||
cleanUp(name)
|
||||
log.Fatalf("ssh %s error: %s", name, sshPkg.Fatal(name, err))
|
||||
log.Debugf("ssh %s error: %s", name, sshPkg.Fatal(name, err))
|
||||
log.Fatalf("can't ssh to %s, make sure \"ssh %s\" works", name, name)
|
||||
}
|
||||
|
||||
if created {
|
||||
|
@ -441,25 +441,7 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
|
||||
|
||||
log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
|
||||
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = stack.RunDeploy(
|
||||
cl,
|
||||
deployOpts,
|
||||
compose,
|
||||
stackName,
|
||||
app.Server,
|
||||
true,
|
||||
f,
|
||||
)
|
||||
err = stack.RunDeploy(cl, deployOpts, compose, stackName, true)
|
||||
|
||||
return err
|
||||
}
|
||||
@ -470,7 +452,7 @@ func newKadabraApp(version, commit string) *cobra.Command {
|
||||
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
|
||||
Short: "The Co-op Cloud auto-updater 🤖 🚀",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
log.Logger.SetStyles(charmLog.DefaultStyles())
|
||||
log.Logger.SetStyles(log.Styles())
|
||||
charmLog.SetDefault(log.Logger)
|
||||
|
||||
if internal.Debug {
|
||||
|
114
go.mod
114
go.mod
@ -1,50 +1,49 @@
|
||||
module coopcloud.tech/abra
|
||||
|
||||
go 1.23.0
|
||||
go 1.22.7
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
replace github.com/urfave/cli/v3 => github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44
|
||||
|
||||
require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
|
||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
|
||||
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/charmbracelet/bubbletea v1.3.4
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v0.4.1
|
||||
github.com/charmbracelet/lipgloss v1.0.0
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v28.0.1+incompatible
|
||||
github.com/docker/docker v28.0.1+incompatible
|
||||
github.com/docker/cli v27.4.1+incompatible
|
||||
github.com/docker/docker v27.4.1+incompatible
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/go-git/go-git/v5 v5.14.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/go-git/go-git/v5 v5.12.0
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/moby/term v0.5.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/schollz/progressbar/v3 v3.18.0
|
||||
golang.org/x/term v0.30.0
|
||||
github.com/schollz/progressbar/v3 v3.17.1
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9
|
||||
golang.org/x/term v0.27.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
gotest.tools/v3 v3.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.3 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.6.0 // indirect
|
||||
github.com/cloudflare/circl v1.5.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
@ -52,12 +51,11 @@ require (
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@ -65,17 +63,16 @@ require (
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
@ -86,50 +83,47 @@ require (
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/runc v1.1.13 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/common v0.61.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/skeema/knownhosts v1.3.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.33.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/grpc v1.69.2 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
@ -138,19 +132,19 @@ require (
|
||||
github.com/containers/image v3.0.2+incompatible
|
||||
github.com/containers/storage v1.38.2 // indirect
|
||||
github.com/decentral1se/passgen v1.0.1
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.8.2 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/sys v0.28.0
|
||||
)
|
||||
|
294
go.sum
294
go.sum
@ -27,16 +27,16 @@ coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c h1:oeKnUB79PKYD8D0/unYuu7MRcWryQQWOns8+JL+acrs=
|
||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c/go.mod h1:fQuhwrpg6qb9NlFXKYi/LysWu1wxjraS8sxyW12CUF0=
|
||||
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE=
|
||||
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
|
||||
@ -79,8 +79,8 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
|
||||
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
@ -131,26 +131,21 @@ 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/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
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/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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
|
||||
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E=
|
||||
github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
|
||||
github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
|
||||
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
|
||||
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
|
||||
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
|
||||
@ -168,8 +163,8 @@ github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2u
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
||||
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
@ -283,6 +278,8 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
@ -292,8 +289,10 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
|
||||
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.3.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8=
|
||||
github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
|
||||
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
|
||||
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
|
||||
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
|
||||
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
|
||||
@ -312,19 +311,23 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs=
|
||||
github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ=
|
||||
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
|
||||
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
|
||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
||||
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
|
||||
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
@ -347,8 +350,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
|
||||
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
@ -359,8 +362,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
@ -381,16 +382,16 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYis
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
|
||||
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
|
||||
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
|
||||
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
|
||||
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
|
||||
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@ -487,8 +488,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
|
||||
github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@ -528,9 +529,12 @@ 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/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.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@ -592,8 +596,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@ -625,14 +629,13 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
|
||||
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
@ -679,8 +682,8 @@ github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
@ -689,12 +692,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
@ -731,8 +730,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
@ -762,8 +761,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -779,8 +778,8 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@ -794,8 +793,10 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc=
|
||||
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
|
||||
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
|
||||
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
@ -816,15 +817,16 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
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/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.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
||||
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
|
||||
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
|
||||
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
|
||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||
@ -841,8 +843,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
|
||||
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
@ -857,8 +859,8 @@ github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
@ -867,9 +869,8 @@ github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
|
||||
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
@ -904,6 +905,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44 h1:BeSTAZEDkDVNv9EOrycIGCkEg+6EhRRgSsbdc93Q3OM=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||
github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
|
||||
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
@ -924,8 +927,6 @@ github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@ -946,29 +947,49 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@ -993,8 +1014,10 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -1005,8 +1028,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
|
||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -1070,8 +1095,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -1089,8 +1116,10 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -1160,7 +1189,6 @@ golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -1168,15 +1196,20 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1186,16 +1219,18 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -1289,10 +1324,14 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@ -1312,8 +1351,10 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@ -1327,8 +1368,10 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
||||
@ -1368,11 +1411,12 @@ 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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/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.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
@ -36,7 +36,7 @@ func Get(appName string) (App, error) {
|
||||
return App{}, err
|
||||
}
|
||||
|
||||
log.Debugf("loaded app %s: %s", appName, app)
|
||||
log.Debugf("retrieved %s for %s", app, appName)
|
||||
|
||||
return app, nil
|
||||
}
|
||||
@ -91,17 +91,6 @@ type App struct {
|
||||
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
|
||||
|
||||
// AppName is AppName
|
||||
@ -246,6 +235,8 @@ func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) {
|
||||
return App{}, fmt.Errorf("env file for %s couldn't be read: %s", name, err.Error())
|
||||
}
|
||||
|
||||
log.Debugf("read env %s from %s", env, appFile.Path)
|
||||
|
||||
app, err := NewApp(env, name, appFile)
|
||||
if err != nil {
|
||||
return App{}, fmt.Errorf("env file for %s has issues: %s", name, err.Error())
|
||||
@ -503,13 +494,13 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
|
||||
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
log.Debugf("adding env vars to %s service config", stackName)
|
||||
log.Debugf("add the following environment to the app service config of %s:", stackName)
|
||||
for k, v := range appEnv {
|
||||
_, exists := service.Environment[k]
|
||||
if !exists {
|
||||
value := v
|
||||
service.Environment[k] = &value
|
||||
log.Debugf("%s: %s: %s", stackName, k, value)
|
||||
log.Debugf("add env var: %s value: %s to %s", k, value, stackName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -578,49 +569,6 @@ func ReadAbraShCmdNames(abraSh string) ([]string, error) {
|
||||
return cmdNames, nil
|
||||
}
|
||||
|
||||
// Wipe removes the version from the app .env file.
|
||||
func (a App) WipeRecipeVersion() error {
|
||||
file, err := os.Open(a.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var (
|
||||
lines []string
|
||||
scanner = bufio.NewScanner(file)
|
||||
)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
|
||||
lines = append(lines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "#") {
|
||||
lines = append(lines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
splitted := strings.Split(line, ":")
|
||||
lines = append(lines, splitted[0])
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("version wiped from %s.env", a.Domain)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteRecipeVersion writes the recipe version to the app .env file.
|
||||
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
file, err := os.Open(a.Path)
|
||||
if err != nil {
|
||||
@ -628,13 +576,9 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var (
|
||||
dirtyVersion string
|
||||
skipped bool
|
||||
lines []string
|
||||
scanner = bufio.NewScanner(file)
|
||||
)
|
||||
|
||||
skipped := false
|
||||
scanner := bufio.NewScanner(file)
|
||||
lines := []string{}
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "RECIPE=") && !strings.HasPrefix(line, "TYPE=") {
|
||||
@ -647,14 +591,13 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(line, version) && !a.Recipe.Dirty && !strings.HasSuffix(line, config.DIRTY_DEFAULT) {
|
||||
if strings.Contains(line, version) {
|
||||
skipped = true
|
||||
lines = append(lines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
splitted := strings.Split(line, ":")
|
||||
|
||||
line = fmt.Sprintf("%s:%s", splitted[0], version)
|
||||
lines = append(lines, line)
|
||||
}
|
||||
@ -663,10 +606,6 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if a.Recipe.Dirty && dirtyVersion != "" {
|
||||
version = dirtyVersion
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
|
||||
log.Fatal(err)
|
||||
|
@ -198,29 +198,3 @@ func compareFilter(t *testing.T, f1 filters.Args, f2 map[string]map[string]bool)
|
||||
t.Errorf("filters mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteRecipeVersionOverwrite(t *testing.T) {
|
||||
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer t.Cleanup(func() {
|
||||
if err := app.WipeRecipeVersion(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
assert.Equal(t, "", app.Recipe.EnvVersion)
|
||||
|
||||
if err := app.WriteRecipeVersion("foo", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "foo", app.Recipe.EnvVersion)
|
||||
}
|
||||
|
@ -44,16 +44,6 @@ func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosV
|
||||
}
|
||||
}
|
||||
|
||||
func SetVersionLabel(compose *composetypes.Config, stackName string, version string) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
log.Debugf("set label 'coop-cloud.%s.version' to %v for %s", stackName, version, stackName)
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
|
||||
service.Deploy.Labels[labelKey] = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
|
||||
// auto update process for this app. The default if this variable is not set is to disable
|
||||
// the auto update process.
|
||||
|
@ -54,7 +54,7 @@ func RecipeNameComplete() ([]string, cobra.ShellCompDirective) {
|
||||
|
||||
// RecipeVersionComplete completes versions for the recipe.
|
||||
func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
|
||||
catl, err := recipe.ReadRecipeCatalogue(true)
|
||||
catl, err := recipe.ReadRecipeCatalogue(false)
|
||||
if err != nil {
|
||||
err := fmt.Sprintf("autocomplete failed: %s", err)
|
||||
return []string{err}, cobra.ShellCompDirectiveError
|
||||
|
@ -16,12 +16,13 @@ import (
|
||||
func EnsureCatalogue() error {
|
||||
catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
|
||||
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
|
||||
log.Debugf("catalogue is missing, retrieving now")
|
||||
|
||||
log.Warnf("local recipe catalogue is missing, retrieving now")
|
||||
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
|
||||
if err := gitPkg.Clone(catalogueDir, url); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("cloned catalogue repository to %s", catalogueDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -90,7 +90,6 @@ func (a Abra) GetAbraDir() string {
|
||||
|
||||
func (a Abra) GetServersDir() string { return path.Join(a.GetAbraDir(), "servers") }
|
||||
func (a Abra) GetRecipesDir() string { return path.Join(a.GetAbraDir(), "recipes") }
|
||||
func (a Abra) GetLogsDir() string { return path.Join(a.GetAbraDir(), "logs") }
|
||||
func (a Abra) GetVendorDir() string { return path.Join(a.GetAbraDir(), "vendor") }
|
||||
func (a Abra) GetBackupDir() string { return path.Join(a.GetAbraDir(), "backups") }
|
||||
func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catalogue") }
|
||||
@ -101,15 +100,13 @@ var (
|
||||
ABRA_DIR = config.GetAbraDir()
|
||||
SERVERS_DIR = config.GetServersDir()
|
||||
RECIPES_DIR = config.GetRecipesDir()
|
||||
LOGS_DIR = config.GetLogsDir()
|
||||
VENDOR_DIR = config.GetVendorDir()
|
||||
BACKUP_DIR = config.GetBackupDir()
|
||||
CATALOGUE_DIR = config.GetCatalogueDir()
|
||||
RECIPES_JSON = path.Join(config.GetCatalogueDir(), "recipes.json")
|
||||
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
|
||||
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
|
||||
TOOLSHED_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/toolshed/%s.git"
|
||||
RECIPES_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
|
||||
SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
|
||||
|
||||
// NOTE(d1): please note, this was done purely out of laziness on our part
|
||||
// AFAICR. it's easy to punt the value into the label because that is what is
|
||||
@ -117,10 +114,6 @@ var (
|
||||
// complained yet!
|
||||
CHAOS_DEFAULT = "false"
|
||||
|
||||
DIRTY_DEFAULT = "+U"
|
||||
|
||||
NO_DOMAIN_DEFAULT = "N/A"
|
||||
NO_VERSION_DEFAULT = "N/A"
|
||||
|
||||
UNKNOWN_DEFAULT = "unknown"
|
||||
)
|
||||
|
@ -26,16 +26,9 @@ func GetServers() ([]string, error) {
|
||||
return servers, err
|
||||
}
|
||||
|
||||
var filtered []string
|
||||
for _, s := range servers {
|
||||
if !strings.HasPrefix(s, ".") {
|
||||
filtered = append(filtered, s)
|
||||
}
|
||||
}
|
||||
log.Debugf("retrieved %v servers: %s", len(servers), servers)
|
||||
|
||||
log.Debugf("retrieved %v servers: %s", len(filtered), filtered)
|
||||
|
||||
return filtered, nil
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// ReadServerNames retrieves all server names.
|
||||
@ -70,7 +63,7 @@ func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
|
||||
|
||||
realPath, err := filepath.EvalSymlinks(filePath)
|
||||
if err != nil {
|
||||
log.Warnf("broken symlink in your abra config folders: %s", filePath)
|
||||
log.Warnf("broken symlink in your $ABRA_DIR: %s", filePath)
|
||||
} else {
|
||||
realFile, err := os.Stat(realPath)
|
||||
if err != nil {
|
||||
|
@ -15,7 +15,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
|
||||
}{
|
||||
// NOTE(d1): DNS records get checked, so use something that is maintained
|
||||
// within the federation. if you're here because of a failing test, try
|
||||
// `dig +short <domain>` to ensure stuff matches first! If flakyness
|
||||
// `dig +short <app>` to ensure stuff matches first! If flakyness
|
||||
// becomes an issue we can look into mocking
|
||||
{"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true},
|
||||
{"docs.coopcloud.tech", "coopcloud.tech", true},
|
||||
@ -43,7 +43,7 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
|
||||
func TestEnsureIpv4(t *testing.T) {
|
||||
// NOTE(d1): DNS records get checked, so use something that is maintained
|
||||
// within the federation. if you're here because of a failing test, try `dig
|
||||
// +short <domain>` to ensure stuff matches first! If flakyness becomes an
|
||||
// +short <app>` to ensure stuff matches first! If flakyness becomes an
|
||||
// issue we can look into mocking
|
||||
domainName := "collabora.ostrom.collective.tools"
|
||||
serverName := "ostrom.collective.tools"
|
||||
|
@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"git.coopcloud.tech/toolshed/godotenv"
|
||||
"git.coopcloud.tech/coop-cloud/godotenv"
|
||||
)
|
||||
|
||||
// envVarModifiers is a list of env var modifier strings. These are added to
|
||||
@ -31,6 +31,8 @@ func ReadEnv(filePath string) (AppEnv, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("read %s from %s", envVars, filePath)
|
||||
|
||||
return envVars, nil
|
||||
}
|
||||
|
||||
|
@ -192,7 +192,7 @@ func TestEnvVarCommentsRemoved(t *testing.T) {
|
||||
|
||||
envVar, exists = envSample["SECRET_TEST_PASS_TWO_VERSION"]
|
||||
if !exists {
|
||||
t.Fatal("SECRET_TEST_PASS_TWO_VERSION env var should be present in .env.sample")
|
||||
t.Fatal("WITH_COMMENT env var should be present in .env.sample")
|
||||
}
|
||||
|
||||
if strings.Contains(envVar, "length") {
|
||||
|
@ -13,15 +13,11 @@ import (
|
||||
"github.com/docker/go-units"
|
||||
"golang.org/x/term"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
var BoldStyle = lipgloss.NewStyle().
|
||||
Bold(true)
|
||||
|
||||
var BoldUnderlineStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Underline(true)
|
||||
|
||||
@ -106,6 +102,7 @@ func CreateOverview(header string, rows [][]string) string {
|
||||
var borderStyle = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
Padding(0, 1, 0, 1).
|
||||
MaxWidth(79).
|
||||
BorderForeground(lipgloss.Color("63"))
|
||||
|
||||
var headerStyle = lipgloss.NewStyle().
|
||||
@ -113,7 +110,9 @@ func CreateOverview(header string, rows [][]string) string {
|
||||
Bold(true).
|
||||
PaddingBottom(1)
|
||||
|
||||
var leftStyle = lipgloss.NewStyle()
|
||||
var leftStyle = lipgloss.NewStyle().
|
||||
Bold(true)
|
||||
|
||||
var rightStyle = lipgloss.NewStyle()
|
||||
|
||||
var longest int
|
||||
@ -125,10 +124,6 @@ func CreateOverview(header string, rows [][]string) string {
|
||||
|
||||
var renderedRows []string
|
||||
for _, row := range rows {
|
||||
if len(row) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(row) > 2 {
|
||||
panic("CreateOverview: only accepts rows of len == 2")
|
||||
}
|
||||
@ -143,23 +138,12 @@ func CreateOverview(header string, rows [][]string) string {
|
||||
offset = offset + " "
|
||||
}
|
||||
|
||||
rendered := horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1]))
|
||||
|
||||
if row[1] == "---" {
|
||||
rendered = horizontal(
|
||||
leftStyle.
|
||||
Bold(true).
|
||||
Underline(true).
|
||||
PaddingTop(1).
|
||||
Render(row[0]),
|
||||
offset,
|
||||
rightStyle.Render(""),
|
||||
renderedRows = append(
|
||||
renderedRows,
|
||||
horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1])),
|
||||
)
|
||||
}
|
||||
|
||||
renderedRows = append(renderedRows, rendered)
|
||||
}
|
||||
|
||||
body := strings.Builder{}
|
||||
body.WriteString(
|
||||
borderStyle.Render(
|
||||
@ -217,6 +201,7 @@ func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
|
||||
progressbar.OptionClearOnFinish(),
|
||||
progressbar.OptionSetPredictTime(false),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionFullWidth(),
|
||||
progressbar.OptionSetDescription(title),
|
||||
)
|
||||
}
|
||||
@ -257,18 +242,3 @@ func ByteCountSI(b uint64) string {
|
||||
|
||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
|
||||
}
|
||||
|
||||
// BoldDirtyDefault ensures a dirty modifier is rendered in bold.
|
||||
func BoldDirtyDefault(v string) string {
|
||||
if strings.HasSuffix(v, config.DIRTY_DEFAULT) {
|
||||
vBold := BoldStyle.Render(config.DIRTY_DEFAULT)
|
||||
v = strings.Replace(v, config.DIRTY_DEFAULT, vBold, 1)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
// AddDirtyMarker adds the dirty marker to a version string.
|
||||
func AddDirtyMarker(v string) string {
|
||||
return fmt.Sprintf("%s%s", v, config.DIRTY_DEFAULT)
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBoldDirtyDefault(t *testing.T) {
|
||||
assert.Equal(t, "foo", BoldDirtyDefault("foo"))
|
||||
}
|
100
pkg/git/clone.go
100
pkg/git/clone.go
@ -1,10 +1,9 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
@ -12,95 +11,40 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
// gitCloneIgnoreErr checks whether we can ignore a git clone error or not.
|
||||
func gitCloneIgnoreErr(err error) bool {
|
||||
if strings.Contains(err.Error(), "authentication required") {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "remote repository is empty") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Clone runs a git clone which accounts for different default branches. This
|
||||
// function respects Ctrl+C (SIGINT) calls from the user, cancelling the
|
||||
// context and deleting the (typically) half-baked clone of the repository.
|
||||
// This avoids broken state for future clone / recipe ops.
|
||||
// Clone runs a git clone which accounts for different default branches.
|
||||
func Clone(dir, url string) error {
|
||||
ctx := context.Background()
|
||||
ctx, cancelCtx := context.WithCancel(ctx)
|
||||
|
||||
sigIntCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigIntCh, os.Interrupt)
|
||||
defer func() {
|
||||
signal.Stop(sigIntCh)
|
||||
cancelCtx()
|
||||
}()
|
||||
|
||||
errCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
log.Debugf("git clone: %s", url)
|
||||
log.Debugf("%s does not exist, attempting to git clone from %s", dir, url)
|
||||
|
||||
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
|
||||
SingleBranch: true,
|
||||
})
|
||||
|
||||
if err != nil && gitCloneIgnoreErr(err) {
|
||||
log.Debugf("git clone: %s cloned successfully", dir)
|
||||
errCh <- nil
|
||||
}
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
errCh <- fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Debug("git clone: main branch failed, attempting master branch")
|
||||
|
||||
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
|
||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
SingleBranch: true,
|
||||
})
|
||||
|
||||
if err != nil && gitCloneIgnoreErr(err) {
|
||||
log.Debugf("git clone: %s cloned successfully", dir)
|
||||
errCh <- nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
}
|
||||
log.Debugf("cloning %s default branch failed, attempting from main branch", url)
|
||||
|
||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
|
||||
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)
|
||||
}
|
||||
|
||||
log.Debugf("git clone: %s cloned successfully", dir)
|
||||
} else {
|
||||
log.Debugf("git clone: %s already exists", dir)
|
||||
}
|
||||
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-sigIntCh:
|
||||
cancelCtx()
|
||||
fmt.Println() // NOTE(d1): newline after ^C
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return fmt.Errorf("unable to clean up git clone of %s: %s", dir, err)
|
||||
}
|
||||
return fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("%s has been git cloned successfully", dir)
|
||||
} else {
|
||||
log.Debugf("%s already exists", dir)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
)
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
dir := path.Join(config.RECIPES_DIR, "gitea")
|
||||
os.RemoveAll(dir)
|
||||
|
||||
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
|
||||
if err := Clone(dir, gitURL); err != nil {
|
||||
t.Fatalf("unable to git clone gitea: %s", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||
t.Fatal("gitea repo was not cloned successfully")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelGitClone(t *testing.T) {
|
||||
dir := path.Join(config.RECIPES_DIR, "gitea")
|
||||
os.RemoveAll(dir)
|
||||
|
||||
go func() {
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find current process: %s", err)
|
||||
}
|
||||
|
||||
p.Signal(syscall.SIGINT)
|
||||
}()
|
||||
|
||||
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
|
||||
if err := Clone(dir, gitURL); err == nil {
|
||||
t.Fatal("cloning should have been interrupted")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dir); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal("recipe repo was not deleted")
|
||||
}
|
||||
}
|
@ -45,3 +45,39 @@ func Commit(repoPath, commitMessage string, dryRun bool) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommitFile commits a specific file.
|
||||
func CommitFile(repoPath, filePath, commitMessage string, dryRun bool) error {
|
||||
if commitMessage == "" {
|
||||
return fmt.Errorf("no commit message specified?")
|
||||
}
|
||||
|
||||
commitRepo, err := git.PlainOpen(repoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
commitWorktree, err := commitRepo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
if _, err := commitWorktree.Add(filePath); err != nil {
|
||||
return fmt.Errorf("unable to add %s: %s", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
opts := &git.CommitOptions{}
|
||||
if !dryRun {
|
||||
_, err = commitWorktree.Commit(commitMessage, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("git changes commited")
|
||||
} else {
|
||||
log.Debug("dry run: no changes commited")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package git
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
)
|
||||
@ -10,8 +11,8 @@ import (
|
||||
// getGitDiffArgs builds the `git diff` invocation args. It removes the usage
|
||||
// of a pager and ensures that colours are specified even when Git might detect
|
||||
// otherwise.
|
||||
func getGitDiffArgs(repoPath string) []string {
|
||||
return []string{
|
||||
func getGitDiffArgs(repoPath, fname string) []string {
|
||||
args := []string{
|
||||
"-C",
|
||||
repoPath,
|
||||
"--no-pager",
|
||||
@ -19,24 +20,29 @@ func getGitDiffArgs(repoPath string) []string {
|
||||
"color.diff=always",
|
||||
"diff",
|
||||
}
|
||||
|
||||
if fname != "" {
|
||||
args = append(args, fname)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// DiffUnstaged shows a `git diff`. Due to limitations in the underlying go-git
|
||||
// library, this implementation requires the /usr/bin/git binary. It gracefully
|
||||
// skips if it cannot find the command on the system.
|
||||
func DiffUnstaged(path string) error {
|
||||
// library, this implementation requires the /usr/bin/git binary.
|
||||
func DiffUnstaged(path, fname string) (string, error) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
log.Warnf("unable to locate git command, cannot output diff")
|
||||
return nil
|
||||
return "", fmt.Errorf("missing /usr/bin/git command? cannot output diff")
|
||||
}
|
||||
|
||||
gitDiffArgs := getGitDiffArgs(path)
|
||||
gitDiffArgs := getGitDiffArgs(path, fname)
|
||||
|
||||
log.Debugf("running: git %s", strings.Join(gitDiffArgs, " "))
|
||||
|
||||
diff, err := exec.Command("git", gitDiffArgs...).Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
return "", err
|
||||
}
|
||||
|
||||
fmt.Print(string(diff))
|
||||
|
||||
return nil
|
||||
return string(diff), nil
|
||||
}
|
||||
|
35
pkg/git/dir.go
Normal file
35
pkg/git/dir.go
Normal file
@ -0,0 +1,35 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
)
|
||||
|
||||
func FindDir(dir string) string {
|
||||
dir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
realPath, err := filepath.EvalSymlinks(dir)
|
||||
if err != nil {
|
||||
log.Warn("unable to find git repo: broken symlink: %s", dir)
|
||||
return ""
|
||||
}
|
||||
|
||||
dir = realPath
|
||||
|
||||
if dir == os.ExpandEnv("$HOME/.abra") || dir == os.ExpandEnv("$ABRA_DIR") || dir == "/" {
|
||||
return ""
|
||||
}
|
||||
|
||||
p := path.Join(dir, ".git")
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return path.Dir(p)
|
||||
}
|
||||
|
||||
return FindDir(filepath.Dir(dir))
|
||||
}
|
@ -15,11 +15,9 @@ func Init(repoPath string, commit bool, gitName, gitEmail string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("git init: %s", err)
|
||||
}
|
||||
|
||||
if err = SwitchToMain(repo); err != nil {
|
||||
return fmt.Errorf("git branch rename: %s", err)
|
||||
}
|
||||
|
||||
log.Debugf("initialised new git repo in %s", repoPath)
|
||||
|
||||
if commit {
|
||||
@ -41,11 +39,9 @@ func Init(repoPath string, commit bool, gitName, gitEmail string) error {
|
||||
if gitName != "" && gitEmail != "" {
|
||||
author = &object.Signature{Name: gitName, Email: gitEmail}
|
||||
}
|
||||
|
||||
if _, err = commitWorktree.Commit("init", &git.CommitOptions{Author: author}); err != nil {
|
||||
return fmt.Errorf("git commit: %s", err)
|
||||
}
|
||||
|
||||
log.Debugf("init committed all files for new git repo in %s", repoPath)
|
||||
}
|
||||
|
||||
@ -58,18 +54,14 @@ func SwitchToMain(repo *git.Repository) error {
|
||||
if err := repo.Storer.SetReference(ref); err != nil {
|
||||
return fmt.Errorf("set reference: %s", err)
|
||||
}
|
||||
|
||||
cfg, err := repo.Config()
|
||||
if err != nil {
|
||||
return fmt.Errorf("repo config: %s", err)
|
||||
}
|
||||
|
||||
cfg.Init.DefaultBranch = "main"
|
||||
if err := repo.SetConfig(cfg); err != nil {
|
||||
return fmt.Errorf("repo set config: %s", err)
|
||||
}
|
||||
|
||||
log.Debug("set 'main' as the default branch")
|
||||
|
||||
log.Debug("set 'main' as the default branch.")
|
||||
return nil
|
||||
}
|
||||
|
41
pkg/git/pull.go
Normal file
41
pkg/git/pull.go
Normal file
@ -0,0 +1,41 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"github.com/go-git/go-git/v5"
|
||||
)
|
||||
|
||||
// Pull pulls the latest changes in.
|
||||
func Pull(repoDir string, dryRun bool) error {
|
||||
if dryRun {
|
||||
log.Debugf("dry run: no git changes pulled in %s", repoDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(repoDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := &git.PullOptions{
|
||||
RemoteName: "origin", // NOTE(d1): what could go wrong 🤡
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := worktree.Pull(opts); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
|
||||
return err
|
||||
} else if err != nil && errors.Is(err, git.NoErrAlreadyUpToDate) {
|
||||
log.Debugf("skipping pulling changes at %s", repoDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debugf("git changes pulled in at %s", repoDir)
|
||||
|
||||
return nil
|
||||
}
|
@ -27,7 +27,7 @@ func Push(repoDir string, remote string, tags bool, dryRun bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("git changes pushed")
|
||||
log.Debug("git changes pushed")
|
||||
|
||||
if tags {
|
||||
opts.RefSpecs = append(opts.RefSpecs, config.RefSpec("+refs/tags/*:refs/tags/*"))
|
||||
@ -36,7 +36,7 @@ func Push(repoDir string, remote string, tags bool, dryRun bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("git tags pushed")
|
||||
log.Debug("git tags pushed")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -1,8 +1,6 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
@ -19,16 +17,12 @@ import (
|
||||
func IsClean(repoPath string) (bool, error) {
|
||||
repo, err := git.PlainOpen(repoPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, git.ErrRepositoryNotExists) {
|
||||
return false, git.ErrRepositoryNotExists
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("unable to open %s: %s", repoPath, err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to open worktree of %s: %s", repoPath, err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
patterns, err := GetExcludesFiles()
|
||||
@ -42,14 +36,13 @@ func IsClean(repoPath string) (bool, error) {
|
||||
|
||||
status, err := worktree.Status()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to query status of %s: %s", repoPath, err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if status.String() != "" {
|
||||
noNewline := strings.TrimSuffix(status.String(), "\n")
|
||||
log.Debugf("git status: %s: %s", repoPath, noNewline)
|
||||
log.Debugf("discovered git status in %s: %s", repoPath, status.String())
|
||||
} else {
|
||||
log.Debugf("git status: %s: clean", repoPath)
|
||||
log.Debugf("discovered clean git status in %s", repoPath)
|
||||
}
|
||||
|
||||
return status.IsClean(), nil
|
||||
|
@ -1,15 +0,0 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsClean(t *testing.T) {
|
||||
isClean, err := IsClean("/tmp")
|
||||
assert.Equal(t, isClean, false)
|
||||
assert.True(t, errors.Is(err, git.ErrRepositoryNotExists))
|
||||
}
|
@ -15,10 +15,8 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
var (
|
||||
Warn = "warn"
|
||||
Critical = "critical"
|
||||
)
|
||||
var Warn = "warn"
|
||||
var Critical = "critical"
|
||||
|
||||
type LintFunction func(recipe.Recipe) (bool, error)
|
||||
|
||||
@ -196,7 +194,7 @@ func LintForErrors(recipe recipe.Recipe) error {
|
||||
|
||||
ok, err := rule.Function(recipe)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lint %s: %s", rule.Ref, err)
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("lint error in %s configs: \"%s\" failed lint checks (%s)", recipe.Name, rule.Description, rule.Ref)
|
||||
@ -411,7 +409,7 @@ func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
|
||||
}
|
||||
|
||||
func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
|
||||
features, category, _, err := recipe.GetRecipeFeaturesAndCategory(r)
|
||||
features, category, err := recipe.GetRecipeFeaturesAndCategory(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -2,10 +2,10 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
charmLog "github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
@ -35,12 +35,41 @@ var DebugLevel = charmLog.DebugLevel
|
||||
var SetOutput = charmLog.SetOutput
|
||||
var SetReportCaller = charmLog.SetReportCaller
|
||||
|
||||
type f func() (tea.Model, error)
|
||||
func Styles() *charmLog.Styles {
|
||||
styles := charmLog.DefaultStyles()
|
||||
|
||||
func Without(fn f) (tea.Model, error) {
|
||||
l := Logger.GetLevel()
|
||||
Logger.SetLevel(math.MaxInt)
|
||||
m, err := fn()
|
||||
Logger.SetLevel(l)
|
||||
return m, err
|
||||
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
|
||||
}
|
||||
|
104
pkg/logs/logs.go
104
pkg/logs/logs.go
@ -1,104 +0,0 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type TailOpts struct {
|
||||
AppName string
|
||||
Services []string
|
||||
StdErr bool
|
||||
Since string
|
||||
Buffer *[]string
|
||||
ToBuffer bool
|
||||
Filters filters.Args
|
||||
}
|
||||
|
||||
// TailLogs gathers logs for the given app with optional service names to be
|
||||
// filtered on. These logs can be printed to os.Stdout or gathered to a buffer.
|
||||
func TailLogs(
|
||||
cl *dockerClient.Client,
|
||||
opts TailOpts,
|
||||
) error {
|
||||
sigIntCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigIntCh, os.Interrupt)
|
||||
defer signal.Stop(sigIntCh)
|
||||
|
||||
services, err := cl.ServiceList(
|
||||
context.Background(),
|
||||
types.ServiceListOptions{Filters: opts.Filters},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
waitCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
var wg sync.WaitGroup
|
||||
for _, service := range services {
|
||||
wg.Add(1)
|
||||
go func(serviceID string) {
|
||||
tail := "50"
|
||||
if opts.ToBuffer {
|
||||
// NOTE(d1): more logs from before deployment when analysing via file
|
||||
tail = "150"
|
||||
}
|
||||
|
||||
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
|
||||
ShowStderr: true,
|
||||
ShowStdout: !opts.StdErr,
|
||||
Since: opts.Since,
|
||||
Until: "",
|
||||
Timestamps: true,
|
||||
Follow: true,
|
||||
Tail: tail,
|
||||
Details: false,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
defer logs.Close()
|
||||
if opts.ToBuffer {
|
||||
buf := bufio.NewScanner(logs)
|
||||
for buf.Scan() {
|
||||
line := fmt.Sprintf("%s: %s", service.Spec.Name, buf.Text())
|
||||
*opts.Buffer = append(*opts.Buffer, line)
|
||||
}
|
||||
logs.Close()
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = io.Copy(os.Stdout, logs); err != nil && err != io.EOF {
|
||||
errCh <- fmt.Errorf("tailLogs: unable to copy buffer: %s", err)
|
||||
}
|
||||
}
|
||||
}(service.ID)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitCh:
|
||||
return nil
|
||||
case <-sigIntCh:
|
||||
return nil
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -3,34 +3,23 @@ package recipe
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
type EnsureContext struct {
|
||||
Chaos bool
|
||||
Offline bool
|
||||
IgnoreEnvVersion bool
|
||||
}
|
||||
|
||||
// Ensure makes sure the recipe exists, is up to date and has the specific
|
||||
// version checked out.
|
||||
func (r Recipe) Ensure(ctx EnsureContext) error {
|
||||
// Ensure makes sure the recipe exists, is up to date and has the latest version checked out.
|
||||
func (r Recipe) Ensure(chaos bool, offline bool) error {
|
||||
if err := r.EnsureExists(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Chaos {
|
||||
if chaos {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -38,19 +27,15 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ctx.Offline {
|
||||
if !offline {
|
||||
if err := r.EnsureUpToDate(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if r.EnvVersion != "" && !ctx.IgnoreEnvVersion {
|
||||
log.Debugf("ensuring env version %s", r.EnvVersion)
|
||||
if strings.Contains(r.EnvVersion, "+U") {
|
||||
log.Fatalf("can not redeploy chaos version (%s) without --chaos", r.EnvVersion)
|
||||
}
|
||||
|
||||
if _, err := r.EnsureVersion(r.EnvVersion); err != nil {
|
||||
if r.Version != "" {
|
||||
log.Debugf("ensuring version %s", r.Version)
|
||||
if _, err := r.EnsureVersion(r.Version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -67,6 +52,7 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
|
||||
// EnsureExists ensures that the recipe is locally cloned
|
||||
func (r Recipe) EnsureExists() error {
|
||||
if _, err := os.Stat(r.Dir); os.IsNotExist(err) {
|
||||
log.Debugf("%s does not exist, attemmpting to clone", r.Dir)
|
||||
if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -79,41 +65,6 @@ func (r Recipe) EnsureExists() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsChaosCommit determines if a version sttring is a chaos commit or not.
|
||||
func (r Recipe) IsChaosCommit(version string) (bool, error) {
|
||||
isChaosCommit := false
|
||||
|
||||
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
|
||||
return isChaosCommit, err
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(r.Dir)
|
||||
if err != nil {
|
||||
return isChaosCommit, err
|
||||
}
|
||||
|
||||
tags, err := repo.Tags()
|
||||
if err != nil {
|
||||
return isChaosCommit, err
|
||||
}
|
||||
|
||||
var tagRef plumbing.ReferenceName
|
||||
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
|
||||
if ref.Name().Short() == version {
|
||||
tagRef = ref.Name()
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return isChaosCommit, err
|
||||
}
|
||||
|
||||
if tagRef.String() == "" {
|
||||
isChaosCommit = true
|
||||
}
|
||||
|
||||
return isChaosCommit, nil
|
||||
}
|
||||
|
||||
// EnsureVersion checks whether a specific version exists for a recipe.
|
||||
func (r Recipe) EnsureVersion(version string) (bool, error) {
|
||||
isChaosCommit := false
|
||||
@ -186,7 +137,8 @@ func (r Recipe) EnsureIsClean() error {
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
return fmt.Errorf("%s (%s) has locally unstaged changes?", r.Name, r.Dir)
|
||||
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
|
||||
return fmt.Errorf(msg, r.Name, r.Dir)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -278,18 +230,8 @@ func (r Recipe) EnsureUpToDate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsDirty checks whether a recipe is dirty or not.
|
||||
func (r *Recipe) IsDirty() (bool, error) {
|
||||
isClean, err := gitPkg.IsClean(r.Dir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return !isClean, nil
|
||||
}
|
||||
|
||||
// ChaosVersion constructs a chaos mode recipe version.
|
||||
func (r *Recipe) ChaosVersion() (string, error) {
|
||||
func (r Recipe) ChaosVersion() (string, error) {
|
||||
var version string
|
||||
|
||||
head, err := r.Head()
|
||||
@ -299,12 +241,13 @@ func (r *Recipe) ChaosVersion() (string, error) {
|
||||
|
||||
version = formatter.SmallSHA(head.String())
|
||||
|
||||
dirty, err := r.IsDirty()
|
||||
isClean, err := gitPkg.IsClean(r.Dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return version, err
|
||||
}
|
||||
if dirty {
|
||||
return fmt.Sprintf("%s%s", version, config.DIRTY_DEFAULT), nil
|
||||
|
||||
if !isClean {
|
||||
version = fmt.Sprintf("%s+U", version)
|
||||
}
|
||||
|
||||
return version, nil
|
||||
@ -350,44 +293,29 @@ func (r Recipe) Tags() ([]string, error) {
|
||||
return tags, err
|
||||
}
|
||||
|
||||
sort.Slice(tags, func(i, j int) bool {
|
||||
version1, err := tagcmp.Parse(tags[i])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
version2, err := tagcmp.Parse(tags[j])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return version1.IsLessThan(version2)
|
||||
})
|
||||
|
||||
log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name)
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetRecipeVersions retrieves all recipe versions.
|
||||
func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
var warnMsg []string
|
||||
|
||||
func (r Recipe) GetRecipeVersions() (RecipeVersions, error) {
|
||||
versions := RecipeVersions{}
|
||||
|
||||
log.Debugf("git: opening repository in %s", r.Dir)
|
||||
log.Debugf("attempting to open git repository in %s", r.Dir)
|
||||
|
||||
repo, err := git.PlainOpen(r.Dir)
|
||||
if err != nil {
|
||||
return versions, warnMsg, nil
|
||||
return versions, err
|
||||
}
|
||||
|
||||
worktree, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return versions, warnMsg, nil
|
||||
return versions, err
|
||||
}
|
||||
|
||||
gitTags, err := repo.Tags()
|
||||
if err != nil {
|
||||
return versions, warnMsg, nil
|
||||
return versions, err
|
||||
}
|
||||
|
||||
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
|
||||
@ -405,7 +333,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("git checkout: %s in %s", ref.Name(), r.Dir)
|
||||
log.Debugf("successfully checked out %s in %s", ref.Name(), r.Dir)
|
||||
|
||||
config, err := r.GetComposeConfig(nil)
|
||||
if err != nil {
|
||||
@ -429,7 +357,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
case reference.NamedTagged:
|
||||
tag = img.(reference.NamedTagged).Tag()
|
||||
case reference.Named:
|
||||
warnMsg = append(warnMsg, fmt.Sprintf("%s service is missing image tag?", path))
|
||||
log.Warnf("%s service is missing image tag?", path)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -443,26 +371,19 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return versions, warnMsg, nil
|
||||
return versions, err
|
||||
}
|
||||
|
||||
_, err = gitPkg.CheckoutDefaultBranch(repo, r.Dir)
|
||||
if err != nil {
|
||||
return versions, warnMsg, nil
|
||||
return versions, err
|
||||
}
|
||||
|
||||
sortRecipeVersions(versions)
|
||||
|
||||
log.Debugf("collected %s for %s", versions, r.Dir)
|
||||
|
||||
var uniqueWarnings []string
|
||||
for _, w := range warnMsg {
|
||||
if !slices.Contains(uniqueWarnings, w) {
|
||||
uniqueWarnings = append(uniqueWarnings, w)
|
||||
}
|
||||
}
|
||||
|
||||
return versions, uniqueWarnings, nil
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
// Head retrieves latest HEAD metadata.
|
||||
|
@ -1,36 +0,0 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsDirty(t *testing.T) {
|
||||
r := Get("abra-test-recipe")
|
||||
|
||||
if err := r.EnsureExists(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
dirty, err := r.IsDirty()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.True(t, dirty)
|
||||
}
|
@ -2,18 +2,16 @@ package recipe
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
|
||||
"coopcloud.tech/abra/pkg/catalogue"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
@ -59,6 +57,11 @@ type RecipeMeta struct {
|
||||
Website string `json:"website"`
|
||||
}
|
||||
|
||||
// TopicMeta represents a list of topics for a repository.
|
||||
type TopicMeta struct {
|
||||
Topics []string `json:"topics"`
|
||||
}
|
||||
|
||||
// LatestVersion returns the latest version of a recipe.
|
||||
func (r RecipeMeta) LatestVersion() string {
|
||||
var version string
|
||||
@ -122,24 +125,17 @@ type Features struct {
|
||||
|
||||
func Get(name string) Recipe {
|
||||
version := ""
|
||||
versionRaw := ""
|
||||
if strings.Contains(name, ":") {
|
||||
split := strings.Split(name, ":")
|
||||
if len(split) > 2 {
|
||||
log.Fatalf("version seems invalid: %s", name)
|
||||
}
|
||||
name = split[0]
|
||||
|
||||
version = split[1]
|
||||
versionRaw = version
|
||||
if strings.HasSuffix(version, config.DIRTY_DEFAULT) {
|
||||
version = strings.Replace(split[1], config.DIRTY_DEFAULT, "", 1)
|
||||
log.Debugf("removed dirty suffix from .env version: %s -> %s", split[1], version)
|
||||
}
|
||||
}
|
||||
|
||||
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, name)
|
||||
sshURL := fmt.Sprintf(config.RECIPES_SSH_URL_TEMPLATE, name)
|
||||
sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, name)
|
||||
if strings.Contains(name, "/") {
|
||||
u, err := url.Parse(name)
|
||||
if err != nil {
|
||||
@ -155,10 +151,9 @@ func Get(name string) Recipe {
|
||||
|
||||
dir := path.Join(config.RECIPES_DIR, escapeRecipeName(name))
|
||||
|
||||
r := Recipe{
|
||||
return Recipe{
|
||||
Name: name,
|
||||
EnvVersion: version,
|
||||
EnvVersionRaw: versionRaw,
|
||||
Version: version,
|
||||
Dir: dir,
|
||||
GitURL: gitURL,
|
||||
SSHURL: sshURL,
|
||||
@ -168,21 +163,11 @@ func Get(name string) Recipe {
|
||||
SampleEnvPath: path.Join(dir, ".env.sample"),
|
||||
AbraShPath: path.Join(dir, "abra.sh"),
|
||||
}
|
||||
|
||||
dirty, err := r.IsDirty()
|
||||
if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
|
||||
log.Fatalf("failed to check git status of %s: %s", r.Name, err)
|
||||
}
|
||||
r.Dirty = dirty
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type Recipe struct {
|
||||
Name string
|
||||
EnvVersion string
|
||||
EnvVersionRaw string
|
||||
Dirty bool // NOTE(d1): git terminology for unstaged changes
|
||||
Version string
|
||||
Dir string
|
||||
GitURL string
|
||||
SSHURL string
|
||||
@ -193,21 +178,6 @@ type Recipe struct {
|
||||
AbraShPath string
|
||||
}
|
||||
|
||||
// String outputs a human-friendly string representation.
|
||||
func (r Recipe) String() string {
|
||||
out := fmt.Sprintf("{name: %s, ", r.Name)
|
||||
out += fmt.Sprintf("version : %s, ", r.EnvVersion)
|
||||
out += fmt.Sprintf("dirty: %v, ", r.Dirty)
|
||||
out += fmt.Sprintf("dir: %s, ", r.Dir)
|
||||
out += fmt.Sprintf("git url: %s, ", r.GitURL)
|
||||
out += fmt.Sprintf("ssh url: %s, ", r.SSHURL)
|
||||
out += fmt.Sprintf("compose: %s, ", r.ComposePath)
|
||||
out += fmt.Sprintf("readme: %s, ", r.ReadmePath)
|
||||
out += fmt.Sprintf("sample env: %s, ", r.SampleEnvPath)
|
||||
out += fmt.Sprintf("abra.sh: %s}", r.AbraShPath)
|
||||
return out
|
||||
}
|
||||
|
||||
func escapeRecipeName(recipeName string) string {
|
||||
recipeName = strings.ReplaceAll(recipeName, "/", "_")
|
||||
recipeName = strings.ReplaceAll(recipeName, ".", "_")
|
||||
@ -226,18 +196,16 @@ func GetRecipesLocal() ([]string, error) {
|
||||
return recipes, nil
|
||||
}
|
||||
|
||||
func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, []string, error) {
|
||||
var (
|
||||
category string
|
||||
warnMsgs []string
|
||||
feat = Features{}
|
||||
)
|
||||
func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, error) {
|
||||
feat := Features{}
|
||||
|
||||
log.Debugf("%s: attempt recipe metadata parse", r.ReadmePath)
|
||||
var category string
|
||||
|
||||
log.Debugf("attempting to open %s for recipe metadata parsing", r.ReadmePath)
|
||||
|
||||
readmeFS, err := ioutil.ReadFile(r.ReadmePath)
|
||||
if err != nil {
|
||||
return feat, category, warnMsgs, err
|
||||
return feat, category, err
|
||||
}
|
||||
|
||||
readmeMetadata, err := GetStringInBetween( // Find text between delimiters
|
||||
@ -246,7 +214,7 @@ func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, []string, error)
|
||||
"<!-- metadata -->", "<!-- endmetadata -->",
|
||||
)
|
||||
if err != nil {
|
||||
return feat, category, warnMsgs, err
|
||||
return feat, category, err
|
||||
}
|
||||
|
||||
readmeLines := strings.Split( // Array item from lines
|
||||
@ -290,25 +258,20 @@ func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, []string, error)
|
||||
)
|
||||
}
|
||||
if strings.Contains(val, "**Image**") {
|
||||
imageMetadata, warnings, err := GetImageMetadata(strings.TrimSpace(
|
||||
imageMetadata, err := GetImageMetadata(strings.TrimSpace(
|
||||
strings.TrimPrefix(val, "* **Image**:"),
|
||||
), r.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if len(warnings) > 0 {
|
||||
warnMsgs = append(warnMsgs, warnings...)
|
||||
}
|
||||
feat.Image = imageMetadata
|
||||
}
|
||||
}
|
||||
|
||||
return feat, category, warnMsgs, nil
|
||||
return feat, category, nil
|
||||
}
|
||||
|
||||
func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error) {
|
||||
var warnMsgs []string
|
||||
|
||||
func GetImageMetadata(imageRowString, recipeName string) (Image, error) {
|
||||
img := Image{}
|
||||
|
||||
imgFields := strings.Split(imageRowString, ",")
|
||||
@ -319,18 +282,11 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error
|
||||
|
||||
if len(imgFields) < 3 {
|
||||
if imageRowString != "" {
|
||||
warnMsgs = append(
|
||||
warnMsgs,
|
||||
fmt.Sprintf("%s: image meta has incorrect format: %s", recipeName, imageRowString),
|
||||
)
|
||||
log.Warnf("%s image meta has incorrect format: %s", recipeName, imageRowString)
|
||||
} else {
|
||||
warnMsgs = append(
|
||||
warnMsgs,
|
||||
fmt.Sprintf("%s: image meta is empty?", recipeName),
|
||||
)
|
||||
log.Warnf("%s image meta is empty?", recipeName)
|
||||
}
|
||||
|
||||
return img, warnMsgs, nil
|
||||
return img, nil
|
||||
}
|
||||
|
||||
img.Rating = imgFields[1]
|
||||
@ -340,17 +296,17 @@ func GetImageMetadata(imageRowString, recipeName string) (Image, []string, error
|
||||
|
||||
imageName, err := GetStringInBetween(recipeName, imgString, "[", "]")
|
||||
if err != nil {
|
||||
return img, warnMsgs, err
|
||||
log.Fatal(err)
|
||||
}
|
||||
img.Image = strings.ReplaceAll(imageName, "`", "")
|
||||
|
||||
imageURL, err := GetStringInBetween(recipeName, imgString, "(", ")")
|
||||
if err != nil {
|
||||
return img, warnMsgs, err
|
||||
log.Fatal(err)
|
||||
}
|
||||
img.URL = imageURL
|
||||
|
||||
return img, warnMsgs, nil
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// GetStringInBetween returns empty string if no start or end string found
|
||||
@ -541,11 +497,11 @@ type InternalTracker struct {
|
||||
type RepoCatalogue map[string]RepoMeta
|
||||
|
||||
// ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea.
|
||||
func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
|
||||
func ReadReposMetadata() (RepoCatalogue, error) {
|
||||
reposMeta := make(RepoCatalogue)
|
||||
|
||||
pageIdx := 1
|
||||
bar := formatter.CreateProgressbar(-1, "collecting recipe listing")
|
||||
bar := formatter.CreateProgressbar(-1, "retrieving recipe repos list from git.coopcloud.tech...")
|
||||
for {
|
||||
var reposList []RepoMeta
|
||||
|
||||
@ -558,32 +514,28 @@ func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
|
||||
}
|
||||
|
||||
if len(reposList) == 0 {
|
||||
if !debug {
|
||||
bar.Add(1)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
for idx, repo := range reposList {
|
||||
// NOTE(d1): the "example" recipe is a temporary special case
|
||||
// https://git.coopcloud.tech/toolshed/organising/issues/666
|
||||
if repo.Name == "example" {
|
||||
continue
|
||||
var topicMeta TopicMeta
|
||||
|
||||
topicsURL := getReposTopicUrl(repo.Name)
|
||||
if err := web.ReadJSON(topicsURL, &topicMeta); err != nil {
|
||||
return reposMeta, err
|
||||
}
|
||||
|
||||
if slices.Contains(topicMeta.Topics, "recipe") && repo.Name != "example" {
|
||||
reposMeta[repo.Name] = reposList[idx]
|
||||
}
|
||||
}
|
||||
|
||||
pageIdx++
|
||||
|
||||
if !debug {
|
||||
bar.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
if err := bar.Close(); err != nil {
|
||||
return reposMeta, err
|
||||
}
|
||||
fmt.Println() // newline for spinner
|
||||
|
||||
return reposMeta, nil
|
||||
}
|
||||
@ -645,7 +597,7 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
|
||||
}
|
||||
|
||||
// UpdateRepositories clones and updates all recipe repositories locally.
|
||||
func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) error {
|
||||
func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
|
||||
var barLength int
|
||||
if recipeName != "" {
|
||||
barLength = 1
|
||||
@ -653,9 +605,9 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
|
||||
barLength = len(repos)
|
||||
}
|
||||
|
||||
cloneLimiter := limit.New(3)
|
||||
cloneLimiter := limit.New(10)
|
||||
|
||||
retrieveBar := formatter.CreateProgressbar(barLength, "retrieving recipes")
|
||||
retrieveBar := formatter.CreateProgressbar(barLength, "ensuring recipes are cloned & up-to-date...")
|
||||
ch := make(chan string, barLength)
|
||||
for _, repoMeta := range repos {
|
||||
go func(rm RepoMeta) {
|
||||
@ -664,9 +616,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
|
||||
|
||||
if recipeName != "" && recipeName != rm.Name {
|
||||
ch <- rm.Name
|
||||
if !debug {
|
||||
retrieveBar.Add(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -675,9 +625,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
|
||||
}
|
||||
|
||||
ch <- rm.Name
|
||||
if !debug {
|
||||
retrieveBar.Add(1)
|
||||
}
|
||||
}(repoMeta)
|
||||
}
|
||||
|
||||
@ -685,11 +633,12 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string, debug bool) erro
|
||||
<-ch // wait for everything
|
||||
}
|
||||
|
||||
if err := retrieveBar.Close(); err != nil {
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
// getReposTopicUrl retrieves the repository specific topic listing.
|
||||
func getReposTopicUrl(repoName string) string {
|
||||
return fmt.Sprintf("https://git.coopcloud.tech/api/v1/repos/coop-cloud/%s/topics", repoName)
|
||||
}
|
||||
|
||||
// ensurePathExists ensures that a path exists.
|
||||
|
@ -33,8 +33,7 @@ func TestGet(t *testing.T) {
|
||||
name: "foo:1.2.3",
|
||||
recipe: Recipe{
|
||||
Name: "foo",
|
||||
EnvVersion: "1.2.3",
|
||||
EnvVersionRaw: "1.2.3",
|
||||
Version: "1.2.3",
|
||||
Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"),
|
||||
GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git",
|
||||
SSHURL: "ssh://git@git.coopcloud.tech:2222/coop-cloud/foo.git",
|
||||
@ -61,23 +60,7 @@ func TestGet(t *testing.T) {
|
||||
name: "mygit.org/myorg/cool-recipe:1.2.4",
|
||||
recipe: Recipe{
|
||||
Name: "mygit.org/myorg/cool-recipe",
|
||||
EnvVersion: "1.2.4",
|
||||
EnvVersionRaw: "1.2.4",
|
||||
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
|
||||
GitURL: "https://mygit.org/myorg/cool-recipe.git",
|
||||
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
|
||||
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
|
||||
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
|
||||
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
|
||||
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mygit.org/myorg/cool-recipe:1e83340e+U",
|
||||
recipe: Recipe{
|
||||
Name: "mygit.org/myorg/cool-recipe",
|
||||
EnvVersion: "1e83340e",
|
||||
EnvVersionRaw: "1e83340e+U",
|
||||
Version: "1.2.4",
|
||||
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
|
||||
GitURL: "https://mygit.org/myorg/cool-recipe.git",
|
||||
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
|
||||
|
@ -33,10 +33,6 @@ type Secret struct {
|
||||
// variable. For Example:
|
||||
// SECRET_FOO=v1 # length=12
|
||||
Length int
|
||||
// Charset comes from the charset modifier at the secret version environment
|
||||
// variable. For Example:
|
||||
// SECRET_FOO=v1 # charset=default,special
|
||||
Charset string
|
||||
// RemoteName is the name of the secret on the server. For example:
|
||||
// name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
|
||||
// With the following:
|
||||
@ -47,38 +43,38 @@ type Secret struct {
|
||||
RemoteName string
|
||||
}
|
||||
|
||||
// GeneratePassword generates passwords.
|
||||
func GeneratePassword(length uint, charset string) (string, error) {
|
||||
// GeneratePasswords generates passwords.
|
||||
func GeneratePasswords(count, length uint) ([]string, error) {
|
||||
passwords, err := passgen.GeneratePasswords(
|
||||
1,
|
||||
count,
|
||||
length,
|
||||
charset,
|
||||
passgen.AlphabetDefault,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("generated %s", strings.Join(passwords, ", "))
|
||||
|
||||
return passwords[0], nil
|
||||
return passwords, nil
|
||||
}
|
||||
|
||||
// GeneratePassphrase generates human readable and rememberable passphrases.
|
||||
func GeneratePassphrase() (string, error) {
|
||||
// GeneratePassphrases generates human readable and rememberable passphrases.
|
||||
func GeneratePassphrases(count uint) ([]string, error) {
|
||||
passphrases, err := passgen.GeneratePassphrases(
|
||||
1,
|
||||
count,
|
||||
passgen.PassphraseWordCountDefault,
|
||||
rune('-'),
|
||||
passgen.PassphraseCasingDefault,
|
||||
passgen.WordListDefault,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("generated %s", strings.Join(passphrases, ", "))
|
||||
|
||||
return passphrases[0], nil
|
||||
return passphrases, nil
|
||||
}
|
||||
|
||||
// ReadSecretsConfig reads secret names/versions from the recipe config. The
|
||||
@ -154,8 +150,6 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
||||
}
|
||||
value.Length = length
|
||||
}
|
||||
|
||||
value.Charset = resolveCharset(modifierValues["charset"])
|
||||
break
|
||||
}
|
||||
secretValues[secretId] = value
|
||||
@ -164,22 +158,6 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
||||
return secretValues, nil
|
||||
}
|
||||
|
||||
// resolveCharset sets the passgen Alphabet required for a secret
|
||||
func resolveCharset(input string) string {
|
||||
switch strings.ToLower(input) {
|
||||
case "special":
|
||||
return passgen.AlphabetSpecial
|
||||
case "safespecial":
|
||||
return "!@#%^&*_-+="
|
||||
case "default,special", "special,default":
|
||||
return passgen.AlphabetDefault + passgen.AlphabetSpecial
|
||||
case "default,safespecial", "safespecial,default":
|
||||
return passgen.AlphabetDefault + "!@#%^&*_-+="
|
||||
default:
|
||||
return passgen.AlphabetDefault // Fallback to default
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateSecrets generates secrets locally and sends them to a remote server for storage.
|
||||
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) {
|
||||
secretsGenerated := map[string]string{}
|
||||
@ -195,13 +173,13 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
||||
log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
|
||||
|
||||
if secret.Length > 0 {
|
||||
password, err := GeneratePassword(uint(secret.Length), secret.Charset)
|
||||
passwords, err := GeneratePasswords(1, uint(secret.Length))
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, password, server); err != nil {
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
log.Warnf("%s already exists", secret.RemoteName)
|
||||
ch <- nil
|
||||
@ -213,15 +191,15 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
secretsGenerated[secretName] = password
|
||||
secretsGenerated[secretName] = passwords[0]
|
||||
} else {
|
||||
passphrase, err := GeneratePassphrase()
|
||||
passphrases, err := GeneratePassphrases(1)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, passphrase, server); err != nil {
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
log.Warnf("%s already exists", secret.RemoteName)
|
||||
ch <- nil
|
||||
@ -233,7 +211,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
secretsGenerated[secretName] = passphrase
|
||||
secretsGenerated[secretName] = passphrases[0]
|
||||
}
|
||||
ch <- nil
|
||||
}(n, v)
|
||||
|
@ -17,37 +17,16 @@ func TestReadSecretsConfig(t *testing.T) {
|
||||
assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName)
|
||||
assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
|
||||
assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_one"].Charset)
|
||||
|
||||
// Has a length modifier
|
||||
assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version)
|
||||
assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_two"].Charset)
|
||||
|
||||
// Secret name does not include the secret id
|
||||
assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName)
|
||||
assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
|
||||
assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_three"].Charset)
|
||||
|
||||
// Has a length modifier and a charset=default,safespecial modifier
|
||||
assert.Equal(t, "test_example_com_test_pass_four_v1", secretsFromConfig["test_pass_four"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_four"].Version)
|
||||
assert.Equal(t, 12, secretsFromConfig["test_pass_four"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#%^&*_-+=", secretsFromConfig["test_pass_four"].Charset)
|
||||
|
||||
// Has a length modifier and a charset=default,special modifier
|
||||
assert.Equal(t, "test_example_com_test_pass_five_v1", secretsFromConfig["test_pass_five"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_five"].Version)
|
||||
assert.Equal(t, 12, secretsFromConfig["test_pass_five"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_five"].Charset)
|
||||
|
||||
// Has only a charset=default,special modifier, which gets setted but ignored in the generation
|
||||
assert.Equal(t, "test_example_com_test_pass_six_v1", secretsFromConfig["test_pass_six"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_six"].Version)
|
||||
assert.Equal(t, 0, secretsFromConfig["test_pass_six"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_six"].Charset)
|
||||
}
|
||||
|
||||
func TestReadSecretsConfigWithLongDomain(t *testing.T) {
|
||||
|
@ -1,6 +1,3 @@
|
||||
SECRET_TEST_PASS_ONE_VERSION=v2
|
||||
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
|
||||
SECRET_TEST_PASS_THREE_VERSION=v2
|
||||
SECRET_TEST_PASS_FOUR_VERSION=v1 # length=12 charset=default,safespecial
|
||||
SECRET_TEST_PASS_FIVE_VERSION=v1 # length=12 charset=default,special
|
||||
SECRET_TEST_PASS_SIX_VERSION=v1 # charset=default,special
|
||||
|
@ -8,9 +8,6 @@ services:
|
||||
- test_pass_one
|
||||
- test_pass_two
|
||||
- test_pass_three
|
||||
- test_pass_four
|
||||
- test_pass_five
|
||||
- test_pass_six
|
||||
|
||||
secrets:
|
||||
test_pass_one:
|
||||
@ -22,12 +19,3 @@ secrets:
|
||||
test_pass_three:
|
||||
external: true
|
||||
name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match
|
||||
test_pass_four:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_four_${SECRET_TEST_PASS_FOUR_VERSION}
|
||||
test_pass_five:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_five_${SECRET_TEST_PASS_FIVE_VERSION}
|
||||
test_pass_six:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_six_${SECRET_TEST_PASS_SIX_VERSION}
|
||||
|
@ -20,8 +20,6 @@ func Fatal(hostname string, err error) error {
|
||||
return fmt.Errorf("ssh auth: permission denied for %s", hostname)
|
||||
} else if strings.Contains(out, "Network is unreachable") {
|
||||
return fmt.Errorf("unable to connect to %s, please check your SSH config", hostname)
|
||||
} else if strings.Contains(out, "Is the docker daemon running") {
|
||||
return fmt.Errorf("docker: is the daemon running / your user has docker permissions?")
|
||||
}
|
||||
|
||||
return err
|
||||
|
353
pkg/ui/deploy.go
353
pkg/ui/deploy.go
@ -1,353 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/logs"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
)
|
||||
|
||||
var IsRunning bool
|
||||
|
||||
type statusMsg struct {
|
||||
stream stream
|
||||
jsonMsg jsonmessage.JSONMessage
|
||||
}
|
||||
|
||||
type progressCompleteMsg struct {
|
||||
stream stream
|
||||
failed bool
|
||||
}
|
||||
|
||||
type healthcheckMsg struct {
|
||||
stream stream
|
||||
health string
|
||||
}
|
||||
|
||||
type ServiceMeta struct {
|
||||
Name string
|
||||
ID string
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
appName string
|
||||
cl *dockerClient.Client
|
||||
count int
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
width int
|
||||
filters filters.Args
|
||||
|
||||
Streams *[]stream
|
||||
Logs *[]string
|
||||
Failed bool
|
||||
TimedOut bool
|
||||
Quit bool
|
||||
}
|
||||
|
||||
func (m Model) complete() bool {
|
||||
if m.count == len(*m.Streams) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
Name string
|
||||
Err error
|
||||
|
||||
decoder *json.Decoder
|
||||
id string
|
||||
reader *io.PipeReader
|
||||
writer *io.PipeWriter
|
||||
status string
|
||||
retries int
|
||||
health string
|
||||
rollback bool
|
||||
}
|
||||
|
||||
func (s stream) String() string {
|
||||
out := fmt.Sprintf("{decoder: %v, ", s.decoder)
|
||||
out += fmt.Sprintf("err: %v, ", s.Err)
|
||||
out += fmt.Sprintf("id: %s, ", s.id)
|
||||
out += fmt.Sprintf("name: %s, ", s.Name)
|
||||
out += fmt.Sprintf("reader: %v, ", s.reader)
|
||||
out += fmt.Sprintf("writer: %v, ", s.writer)
|
||||
out += fmt.Sprintf("status: %s, ", s.status)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s stream) progress(m Model) tea.Msg {
|
||||
if err := progress.ServiceProgress(m.ctx, m.cl, s.id, s.writer); err != nil {
|
||||
return progressCompleteMsg{
|
||||
stream: s,
|
||||
failed: true,
|
||||
}
|
||||
}
|
||||
|
||||
return progressCompleteMsg{stream: s}
|
||||
}
|
||||
|
||||
func (s stream) process() tea.Msg {
|
||||
var jsonMsg jsonmessage.JSONMessage
|
||||
|
||||
if err := s.decoder.Decode(&jsonMsg); err != nil {
|
||||
if err == io.EOF {
|
||||
// NOTE(d1): end processing messages
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return statusMsg{
|
||||
stream: s,
|
||||
jsonMsg: jsonMsg,
|
||||
}
|
||||
}
|
||||
|
||||
func (s stream) healthcheck(m Model) tea.Msg {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s", s.Name))
|
||||
|
||||
containers, err := m.cl.ContainerList(m.ctx, containerTypes.ListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
s.Err = err
|
||||
return healthcheckMsg{stream: s}
|
||||
}
|
||||
|
||||
if len(containers) == 0 {
|
||||
return healthcheckMsg{stream: s}
|
||||
}
|
||||
|
||||
container := containers[0]
|
||||
containerState, err := m.cl.ContainerInspect(m.ctx, container.ID)
|
||||
if err != nil {
|
||||
s.Err = err
|
||||
return healthcheckMsg{stream: s}
|
||||
}
|
||||
|
||||
var health string
|
||||
if containerState.State.Health != nil {
|
||||
health = containerState.State.Health.Status
|
||||
}
|
||||
|
||||
return healthcheckMsg{stream: s, health: health}
|
||||
}
|
||||
|
||||
func DeployInitialModel(
|
||||
ctx context.Context,
|
||||
cl *dockerClient.Client,
|
||||
services []ServiceMeta,
|
||||
appName string,
|
||||
timeout time.Duration,
|
||||
filters filters.Args,
|
||||
) Model {
|
||||
var streams []stream
|
||||
for _, service := range services {
|
||||
r, w := io.Pipe()
|
||||
d := json.NewDecoder(r)
|
||||
streams = append(streams, stream{
|
||||
Name: service.Name,
|
||||
id: service.ID,
|
||||
reader: r,
|
||||
writer: w,
|
||||
decoder: d,
|
||||
retries: -1, // NOTE(d1): skip first attempt
|
||||
health: "?",
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(streams, func(i, j int) bool {
|
||||
return streams[i].Name < streams[j].Name
|
||||
})
|
||||
|
||||
return Model{
|
||||
ctx: ctx,
|
||||
cl: cl,
|
||||
appName: appName,
|
||||
timeout: timeout,
|
||||
filters: filters,
|
||||
Streams: &streams,
|
||||
Logs: &[]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
for _, stream := range *m.Streams {
|
||||
cmds = append(
|
||||
cmds,
|
||||
[]tea.Cmd{
|
||||
func() tea.Msg { return stream.progress(m) },
|
||||
func() tea.Msg { return stream.process() },
|
||||
func() tea.Msg { return stream.healthcheck(m) },
|
||||
}...,
|
||||
)
|
||||
}
|
||||
|
||||
cmds = append(cmds, func() tea.Msg { return deployTimeout(m) })
|
||||
cmds = append(cmds, func() tea.Msg { return m.gatherLogs() })
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m Model) gatherLogs() tea.Msg {
|
||||
var services []string
|
||||
for _, s := range *m.Streams {
|
||||
services = append(services, s.Name)
|
||||
}
|
||||
|
||||
opts := logs.TailOpts{
|
||||
AppName: m.appName,
|
||||
Services: services,
|
||||
StdErr: true,
|
||||
Buffer: m.Logs,
|
||||
ToBuffer: true,
|
||||
Filters: m.filters,
|
||||
}
|
||||
|
||||
// NOTE(d1): not interested in log polling errors. if we don't see logs it
|
||||
// will hopefully be self-evident based on what happened in the deployment
|
||||
logs.TailLogs(m.cl, opts)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type timeoutMsg struct{}
|
||||
|
||||
func deployTimeout(m Model) tea.Msg {
|
||||
<-time.After(m.timeout)
|
||||
return timeoutMsg{}
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
m.Quit = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
|
||||
case progressCompleteMsg:
|
||||
if msg.failed {
|
||||
m.Failed = true
|
||||
}
|
||||
|
||||
m.count += 1
|
||||
|
||||
if m.complete() {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case timeoutMsg:
|
||||
m.TimedOut = true
|
||||
return m, tea.Quit
|
||||
|
||||
case healthcheckMsg:
|
||||
for idx, s := range *m.Streams {
|
||||
if s.id == msg.stream.id {
|
||||
h := "?"
|
||||
if s.health != "" {
|
||||
h = s.health
|
||||
}
|
||||
if msg.health != "" {
|
||||
h = msg.health
|
||||
}
|
||||
(*m.Streams)[idx].health = h
|
||||
}
|
||||
}
|
||||
|
||||
cmds = append(
|
||||
cmds,
|
||||
func() tea.Msg { return msg.stream.healthcheck(m) },
|
||||
)
|
||||
|
||||
case statusMsg:
|
||||
for idx, s := range *m.Streams {
|
||||
if s.id == msg.stream.id {
|
||||
|
||||
if msg.jsonMsg.ID == "rollback" {
|
||||
m.Failed = true
|
||||
(*m.Streams)[idx].rollback = true
|
||||
}
|
||||
|
||||
if msg.jsonMsg.ID != "overall progress" {
|
||||
newStatus := strings.ToLower(msg.jsonMsg.Status)
|
||||
currentStatus := (*m.Streams)[idx].status
|
||||
|
||||
if !strings.Contains(currentStatus, "starting") &&
|
||||
strings.Contains(newStatus, "starting") {
|
||||
(*m.Streams)[idx].retries += 1
|
||||
}
|
||||
|
||||
if (*m.Streams)[idx].rollback {
|
||||
if msg.jsonMsg.ID == "rollback" {
|
||||
(*m.Streams)[idx].status = newStatus
|
||||
}
|
||||
} else {
|
||||
(*m.Streams)[idx].status = newStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmds = append(
|
||||
cmds,
|
||||
func() tea.Msg { return msg.stream.process() },
|
||||
)
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
body := strings.Builder{}
|
||||
|
||||
for _, stream := range *m.Streams {
|
||||
split := strings.Split(stream.Name, "_")
|
||||
short := split[len(split)-1]
|
||||
|
||||
status := stream.status
|
||||
if strings.Contains(stream.status, "converged") && !stream.rollback {
|
||||
status = "succeeded"
|
||||
}
|
||||
if strings.Contains(stream.status, "rolled back") {
|
||||
status = "rolled back"
|
||||
}
|
||||
|
||||
retries := 0
|
||||
if stream.retries > 0 {
|
||||
retries = stream.retries
|
||||
}
|
||||
|
||||
output := fmt.Sprintf("%s: %s (retries: %v, healthcheck: %s)",
|
||||
formatter.BoldStyle.Render(short),
|
||||
status,
|
||||
retries,
|
||||
stream.health,
|
||||
)
|
||||
|
||||
body.WriteString(output)
|
||||
body.WriteString("\n")
|
||||
}
|
||||
|
||||
return body.String()
|
||||
}
|
@ -9,14 +9,14 @@ import (
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// RunExec runs a command on a remote container. io.Writer corresponds to the
|
||||
// command output.
|
||||
func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string,
|
||||
execOptions *container.ExecOptions) (io.Writer, error) {
|
||||
execConfig *types.ExecConfig) (io.Writer, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
// We need to check the tty _before_ we do the ContainerExecCreate, because
|
||||
@ -26,13 +26,13 @@ func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string
|
||||
if _, err := client.ContainerInspect(ctx, containerID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !execOptions.Detach {
|
||||
if err := dockerCli.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
|
||||
if !execConfig.Detach {
|
||||
if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
response, err := client.ContainerExecCreate(ctx, containerID, *execOptions)
|
||||
response, err := client.ContainerExecCreate(ctx, containerID, *execConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -42,40 +42,40 @@ func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string
|
||||
return nil, errors.New("exec ID empty")
|
||||
}
|
||||
|
||||
if execOptions.Detach {
|
||||
execStartCheck := container.ExecStartOptions{
|
||||
Detach: execOptions.Detach,
|
||||
Tty: execOptions.Tty,
|
||||
if execConfig.Detach {
|
||||
execStartCheck := types.ExecStartCheck{
|
||||
Detach: execConfig.Detach,
|
||||
Tty: execConfig.Tty,
|
||||
}
|
||||
return nil, client.ContainerExecStart(ctx, execID, execStartCheck)
|
||||
}
|
||||
return interactiveExec(ctx, dockerCli, client, execOptions, execID)
|
||||
return interactiveExec(ctx, dockerCli, client, execConfig, execID)
|
||||
}
|
||||
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclient.Client,
|
||||
execOpts *container.ExecOptions, execID string) (io.Writer, error) {
|
||||
execConfig *types.ExecConfig, execID string) (io.Writer, error) {
|
||||
// Interactive exec requested.
|
||||
var (
|
||||
out, stderr io.Writer
|
||||
in io.ReadCloser
|
||||
)
|
||||
|
||||
if execOpts.AttachStdin {
|
||||
if execConfig.AttachStdin {
|
||||
in = dockerCli.In()
|
||||
}
|
||||
if execOpts.AttachStdout {
|
||||
if execConfig.AttachStdout {
|
||||
out = dockerCli.Out()
|
||||
}
|
||||
if execOpts.AttachStderr {
|
||||
if execOpts.Tty {
|
||||
if execConfig.AttachStderr {
|
||||
if execConfig.Tty {
|
||||
stderr = dockerCli.Out()
|
||||
} else {
|
||||
stderr = dockerCli.Err()
|
||||
}
|
||||
}
|
||||
|
||||
execStartCheck := container.ExecStartOptions{
|
||||
Tty: execOpts.Tty,
|
||||
execStartCheck := types.ExecStartCheck{
|
||||
Tty: execConfig.Tty,
|
||||
}
|
||||
resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck)
|
||||
if err != nil {
|
||||
@ -94,15 +94,15 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclie
|
||||
outputStream: out,
|
||||
errorStream: stderr,
|
||||
resp: resp,
|
||||
tty: execOpts.Tty,
|
||||
detachKeys: execOpts.DetachKeys,
|
||||
tty: execConfig.Tty,
|
||||
detachKeys: execConfig.DetachKeys,
|
||||
}
|
||||
|
||||
return streamer.stream(ctx)
|
||||
}()
|
||||
}()
|
||||
|
||||
if execOpts.Tty && dockerCli.In().IsTerminal() {
|
||||
if execConfig.Tty && dockerCli.In().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, client, dockerCli, execID, true); err != nil {
|
||||
fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
)
|
||||
@ -51,13 +52,13 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str
|
||||
type networkMap map[string]composetypes.NetworkConfig
|
||||
|
||||
// Networks from the compose-file type to the engine API type
|
||||
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]networktypes.CreateOptions, []string) {
|
||||
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) {
|
||||
if networks == nil {
|
||||
networks = make(map[string]composetypes.NetworkConfig)
|
||||
}
|
||||
|
||||
externalNetworks := []string{}
|
||||
result := make(map[string]networktypes.CreateOptions)
|
||||
result := make(map[string]types.NetworkCreate)
|
||||
for internalName := range servicesNetworks {
|
||||
network := networks[internalName]
|
||||
if network.External.External {
|
||||
@ -65,7 +66,7 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
|
||||
continue
|
||||
}
|
||||
|
||||
createOpts := networktypes.CreateOptions{
|
||||
createOpts := types.NetworkCreate{
|
||||
Labels: AddStackLabel(namespace, network.Labels),
|
||||
Driver: network.Driver,
|
||||
Options: network.DriverOpts,
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -66,7 +67,7 @@ func TestNetworks(t *testing.T) {
|
||||
Name: "othername",
|
||||
},
|
||||
}
|
||||
expected := map[string]network.CreateOptions{
|
||||
expected := map[string]types.NetworkCreate{
|
||||
"foo_default": {
|
||||
Labels: map[string]string{
|
||||
LabelNamespace: "foo",
|
||||
|
@ -3,15 +3,11 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/rem
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
@ -21,34 +17,23 @@ import (
|
||||
|
||||
// RunRemove is the swarm implementation of docker stack remove
|
||||
func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
|
||||
sigIntCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigIntCh, os.Interrupt)
|
||||
defer signal.Stop(sigIntCh)
|
||||
|
||||
waitCh := make(chan struct{})
|
||||
errCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
var errs []string
|
||||
for _, namespace := range opts.Namespaces {
|
||||
services, err := GetStackServices(ctx, client, namespace)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
networks, err := getStackNetworks(ctx, client, namespace)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
var secrets []swarm.Secret
|
||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
|
||||
secrets, err = getStackSecrets(ctx, client, namespace)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,8 +41,7 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error
|
||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
|
||||
configs, err = getStackConfigs(ctx, client, namespace)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,32 +60,14 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("polling undeploy status")
|
||||
timeout, err := waitOnTasks(ctx, client, namespace)
|
||||
if timeout {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
err = waitOnTasks(ctx, client, namespace)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("failed to wait on tasks of stack: %s: %s", namespace, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
errCh <- errors.Errorf(strings.Join(errs, "\n"))
|
||||
return
|
||||
}
|
||||
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitCh:
|
||||
return nil
|
||||
case <-sigIntCh:
|
||||
return fmt.Errorf("skipping as requested, undeploy still in progress 🟠")
|
||||
case err := <-errCh:
|
||||
return err
|
||||
return errors.Errorf(strings.Join(errs, "\n"))
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -121,7 +87,7 @@ func removeServices(
|
||||
var hasError bool
|
||||
sort.Slice(services, sortServiceByName(services))
|
||||
for _, service := range services {
|
||||
log.Debugf("removing service %s", service.Spec.Name)
|
||||
log.Infof("removing service %s", service.Spec.Name)
|
||||
if err := client.ServiceRemove(ctx, service.ID); err != nil {
|
||||
hasError = true
|
||||
log.Fatalf("failed to remove service %s: %s", service.ID, err)
|
||||
@ -133,11 +99,11 @@ func removeServices(
|
||||
func removeNetworks(
|
||||
ctx context.Context,
|
||||
client *apiclient.Client,
|
||||
networks []network.Inspect,
|
||||
networks []types.NetworkResource,
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, network := range networks {
|
||||
log.Debugf("removing network %s", network.Name)
|
||||
log.Infof("removing network %s", network.Name)
|
||||
if err := client.NetworkRemove(ctx, network.ID); err != nil {
|
||||
hasError = true
|
||||
log.Fatalf("failed to remove network %s: %s", network.ID, err)
|
||||
@ -153,7 +119,7 @@ func removeSecrets(
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, secret := range secrets {
|
||||
log.Debugf("removing secret %s", secret.Spec.Name)
|
||||
log.Infof("removing secret %s", secret.Spec.Name)
|
||||
if err := client.SecretRemove(ctx, secret.ID); err != nil {
|
||||
hasError = true
|
||||
log.Fatalf("Failed to remove secret %s: %s", secret.ID, err)
|
||||
@ -169,7 +135,7 @@ func removeConfigs(
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, config := range configs {
|
||||
log.Debugf("removing config %s", config.Spec.Name)
|
||||
log.Infof("removing config %s", config.Spec.Name)
|
||||
if err := client.ConfigRemove(ctx, config.ID); err != nil {
|
||||
hasError = true
|
||||
log.Fatalf("failed to remove config %s: %s", config.ID, err)
|
||||
@ -203,23 +169,12 @@ func terminalState(state swarm.TaskState) bool {
|
||||
return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
|
||||
}
|
||||
|
||||
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) (bool, error) {
|
||||
var timedOut bool
|
||||
|
||||
log.Debugf("waiting on undeploy tasks (timeout=%v secs)", WaitTimeout)
|
||||
|
||||
go func() {
|
||||
t := time.Duration(WaitTimeout) * time.Second
|
||||
<-time.After(t)
|
||||
log.Debug("timed out on undeploy")
|
||||
timedOut = true
|
||||
}()
|
||||
|
||||
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) error {
|
||||
terminalStatesReached := 0
|
||||
for {
|
||||
tasks, err := getStackTasks(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get tasks: %w", err)
|
||||
return fmt.Errorf("failed to get tasks: %w", err)
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
@ -232,11 +187,6 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri
|
||||
if terminalStatesReached == len(tasks) {
|
||||
break
|
||||
}
|
||||
|
||||
if timedOut {
|
||||
return true, fmt.Errorf("deployment timed out 🟠")
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
return nil
|
||||
}
|
||||
|
@ -3,26 +3,24 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/swa
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
stdlibErr "errors"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/ui"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
"github.com/docker/cli/cli/command/stack/formatter"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
@ -108,22 +106,13 @@ type DeployMeta struct {
|
||||
ChaosVersion string // the --chaos deployment version
|
||||
}
|
||||
|
||||
func (d DeployMeta) String() string {
|
||||
var out string
|
||||
out += fmt.Sprintf("{isDeployed: %v, ", d.IsDeployed)
|
||||
out += fmt.Sprintf("version: %s, ", d.Version)
|
||||
out += fmt.Sprintf("isChaos: %v, ", d.IsChaos)
|
||||
out += fmt.Sprintf("chaosVersion: %s}", d.ChaosVersion)
|
||||
return out
|
||||
}
|
||||
|
||||
// IsDeployed gathers metadata about an app deployment.
|
||||
func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) (DeployMeta, error) {
|
||||
deployMeta := DeployMeta{
|
||||
IsDeployed: false,
|
||||
Version: "unknown",
|
||||
IsChaos: false,
|
||||
ChaosVersion: config.CHAOS_DEFAULT,
|
||||
ChaosVersion: "false", // NOTE(d1): match string type used on label
|
||||
}
|
||||
|
||||
filter := filters.NewArgs()
|
||||
@ -177,7 +166,7 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string)
|
||||
func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
||||
oldServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
log.Warnf("failed to list services: %s", err)
|
||||
log.Infof("failed to list services: %s", err)
|
||||
}
|
||||
|
||||
pruneServices := []swarm.Service{}
|
||||
@ -191,17 +180,7 @@ func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace conve
|
||||
}
|
||||
|
||||
// RunDeploy is the swarm implementation of docker stack deploy
|
||||
func RunDeploy(
|
||||
cl *dockerClient.Client,
|
||||
opts Deploy,
|
||||
cfg *composetypes.Config,
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
log.Info("initialising deployment")
|
||||
|
||||
func RunDeploy(cl *dockerClient.Client, opts Deploy, cfg *composetypes.Config, appName string, dontWait bool) error {
|
||||
if err := validateResolveImageFlag(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -211,16 +190,7 @@ func RunDeploy(
|
||||
opts.ResolveImage = ResolveImageNever
|
||||
}
|
||||
|
||||
return deployCompose(
|
||||
context.Background(),
|
||||
cl,
|
||||
opts,
|
||||
cfg,
|
||||
appName,
|
||||
serverName,
|
||||
dontWait,
|
||||
filters,
|
||||
)
|
||||
return deployCompose(context.Background(), cl, opts, cfg, appName, dontWait)
|
||||
}
|
||||
|
||||
// validateResolveImageFlag validates the opts.resolveImage command line option
|
||||
@ -233,16 +203,7 @@ func validateResolveImageFlag(opts *Deploy) error {
|
||||
}
|
||||
}
|
||||
|
||||
func deployCompose(
|
||||
ctx context.Context,
|
||||
cl *dockerClient.Client,
|
||||
opts Deploy,
|
||||
config *composetypes.Config,
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, config *composetypes.Config, appName string, dontWait bool) error {
|
||||
namespace := convert.NewNamespace(opts.Namespace)
|
||||
|
||||
if opts.Prune {
|
||||
@ -283,14 +244,7 @@ func deployCompose(
|
||||
return err
|
||||
}
|
||||
|
||||
serviceIDs, err := deployServices(
|
||||
ctx,
|
||||
cl,
|
||||
services,
|
||||
namespace,
|
||||
opts.SendRegistryAuth,
|
||||
opts.ResolveImage,
|
||||
)
|
||||
serviceIDs, err := deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -300,17 +254,14 @@ func deployCompose(
|
||||
return nil
|
||||
}
|
||||
|
||||
waitOpts := WaitOpts{
|
||||
Services: serviceIDs,
|
||||
AppName: appName,
|
||||
ServerName: serverName,
|
||||
Filters: filters,
|
||||
}
|
||||
log.Infof("waiting for %s to deploy... please hold 🤚", appName)
|
||||
|
||||
if err := WaitOnServices(ctx, cl, waitOpts); err != nil {
|
||||
if err := waitOnServices(ctx, cl, serviceIDs, appName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("successfully deployed %s", appName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -335,7 +286,7 @@ func validateExternalNetworks(ctx context.Context, client dockerClient.NetworkAP
|
||||
// local-scoped networks, so there's no need to inspect them.
|
||||
continue
|
||||
}
|
||||
network, err := client.NetworkInspect(ctx, networkName, networktypes.InspectOptions{})
|
||||
network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
|
||||
switch {
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy", networkName)
|
||||
@ -381,7 +332,7 @@ func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm
|
||||
}
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
// config does not exist, then we create a new one.
|
||||
log.Debugf("creating config %s", configSpec.Name)
|
||||
log.Infof("creating config %s", configSpec.Name)
|
||||
if _, err := cl.ConfigCreate(ctx, configSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
|
||||
}
|
||||
@ -392,13 +343,13 @@ func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm
|
||||
return nil
|
||||
}
|
||||
|
||||
func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, networks map[string]networktypes.CreateOptions) error {
|
||||
func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, networks map[string]types.NetworkCreate) error {
|
||||
existingNetworks, err := getStackNetworks(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingNetworkMap := make(map[string]networktypes.Inspect)
|
||||
existingNetworkMap := make(map[string]types.NetworkResource)
|
||||
for _, network := range existingNetworks {
|
||||
existingNetworkMap[network.Name] = network
|
||||
}
|
||||
@ -412,7 +363,7 @@ func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace conv
|
||||
createOpts.Driver = defaultNetworkDriver
|
||||
}
|
||||
|
||||
log.Debugf("creating network %s", name)
|
||||
log.Infof("creating network %s", name)
|
||||
if _, err := cl.NetworkCreate(ctx, name, createOpts); err != nil {
|
||||
return errors.Wrapf(err, "failed to create network %s", name)
|
||||
}
|
||||
@ -426,12 +377,10 @@ func deployServices(
|
||||
services map[string]swarm.ServiceSpec,
|
||||
namespace convert.Namespace,
|
||||
sendAuth bool,
|
||||
resolveImage string) ([]ui.ServiceMeta, error) {
|
||||
var servicesMeta []ui.ServiceMeta
|
||||
|
||||
resolveImage string) ([]string, error) {
|
||||
existingServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
return servicesMeta, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingServiceMap := make(map[string]swarm.Service)
|
||||
@ -439,6 +388,8 @@ func deployServices(
|
||||
existingServiceMap[service.Spec.Name] = service
|
||||
}
|
||||
|
||||
var serviceIDs []string
|
||||
|
||||
for internalName, serviceSpec := range services {
|
||||
var (
|
||||
name = namespace.Scope(internalName)
|
||||
@ -447,7 +398,7 @@ func deployServices(
|
||||
)
|
||||
|
||||
if service, exists := existingServiceMap[name]; exists {
|
||||
log.Debugf("updating %s", name)
|
||||
log.Infof("updating %s", name)
|
||||
|
||||
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
@ -489,12 +440,9 @@ func deployServices(
|
||||
log.Warn(warning)
|
||||
}
|
||||
|
||||
servicesMeta = append(servicesMeta, ui.ServiceMeta{
|
||||
Name: name,
|
||||
ID: service.ID,
|
||||
})
|
||||
serviceIDs = append(serviceIDs, service.ID)
|
||||
} else {
|
||||
log.Debugf("creating %s", name)
|
||||
log.Infof("creating %s", name)
|
||||
|
||||
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
@ -508,18 +456,15 @@ func deployServices(
|
||||
return nil, errors.Wrapf(err, "failed to create %s", name)
|
||||
}
|
||||
|
||||
servicesMeta = append(servicesMeta, ui.ServiceMeta{
|
||||
Name: name,
|
||||
ID: serviceCreateResponse.ID,
|
||||
})
|
||||
serviceIDs = append(serviceIDs, serviceCreateResponse.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return servicesMeta, nil
|
||||
return serviceIDs, nil
|
||||
}
|
||||
|
||||
func getStackNetworks(ctx context.Context, dockerclient client.APIClient, namespace string) ([]networktypes.Inspect, error) {
|
||||
return dockerclient.NetworkList(ctx, networktypes.ListOptions{Filters: getStackFilter(namespace)})
|
||||
func getStackNetworks(ctx context.Context, dockerclient client.APIClient, namespace string) ([]types.NetworkResource, error) {
|
||||
return dockerclient.NetworkList(ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)})
|
||||
}
|
||||
|
||||
func getStackSecrets(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Secret, error) {
|
||||
@ -530,89 +475,67 @@ func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespa
|
||||
return dockerclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
|
||||
}
|
||||
|
||||
func timestamp() string {
|
||||
ts := time.Now().UTC().Format(time.RFC3339)
|
||||
return strings.Replace(ts, ":", "", -1) // get rid of offensive colons
|
||||
}
|
||||
|
||||
type WaitOpts struct {
|
||||
AppName string
|
||||
Filters filters.Args
|
||||
NoLog bool
|
||||
Quiet bool
|
||||
ServerName string
|
||||
Services []ui.ServiceMeta
|
||||
}
|
||||
|
||||
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
|
||||
timeout := time.Duration(WaitTimeout) * time.Second
|
||||
model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
|
||||
tui := tea.NewProgram(model)
|
||||
|
||||
if !opts.Quiet {
|
||||
log.Info("polling deployment status")
|
||||
}
|
||||
|
||||
m, err := log.Without(
|
||||
func() (tea.Model, error) {
|
||||
return tui.Run()
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("waitOnServices: error running TUI: %s", err)
|
||||
}
|
||||
|
||||
deployModel := m.(ui.Model)
|
||||
if deployModel.TimedOut || deployModel.Failed || deployModel.Quit {
|
||||
func waitOnServices(ctx context.Context, cl *dockerClient.Client, serviceIDs []string, appName string) error {
|
||||
var errs []error
|
||||
|
||||
if deployModel.Failed {
|
||||
errs = append(errs, fmt.Errorf("deploy failed 🛑"))
|
||||
} else if deployModel.TimedOut {
|
||||
errs = append(errs, fmt.Errorf("deploy timed out 🟠"))
|
||||
} else {
|
||||
errs = append(errs, fmt.Errorf("deploy in progress 🟠"))
|
||||
}
|
||||
|
||||
for _, s := range *deployModel.Streams {
|
||||
if s.Err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %s", s.Name, s.Err))
|
||||
for _, serviceID := range serviceIDs {
|
||||
if err := WaitOnService(ctx, cl, serviceID, appName); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", serviceID, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(*deployModel.Logs) > 0 && !opts.NoLog {
|
||||
logsPath := filepath.Join(
|
||||
config.LOGS_DIR,
|
||||
opts.ServerName,
|
||||
fmt.Sprintf("%s_%s", opts.AppName, timestamp()),
|
||||
)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(config.LOGS_DIR, opts.ServerName), 0764); err != nil {
|
||||
return fmt.Errorf("waitOnServices: error creating log dir: %s", err)
|
||||
}
|
||||
|
||||
file, err := os.Create(logsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("waitOnServices: error opening file: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
s := strings.Join(*deployModel.Logs, "\n")
|
||||
if _, err := file.WriteString(s); err != nil {
|
||||
return fmt.Errorf("waitOnServices: writeFile: %s", err)
|
||||
}
|
||||
|
||||
errs = append(errs, fmt.Errorf("logs: %s", logsPath))
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return stdlibErr.Join(errs...)
|
||||
}
|
||||
|
||||
if !opts.Quiet {
|
||||
log.Info("deploy succeeded 🟢")
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
// https://github.com/docker/cli/blob/master/cli/command/service/helpers.go
|
||||
// https://github.com/docker/cli/blob/master/cli/command/service/progress/progress.go
|
||||
func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appName string) error {
|
||||
errChan := make(chan error, 1)
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
sigintChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(sigintChannel, os.Interrupt)
|
||||
defer signal.Stop(sigintChannel)
|
||||
|
||||
go func() {
|
||||
errChan <- progress.ServiceProgress(ctx, cl, serviceID, pipeWriter)
|
||||
}()
|
||||
|
||||
go io.Copy(ioutil.Discard, pipeReader)
|
||||
|
||||
timeout := time.Duration(WaitTimeout) * time.Second
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return err
|
||||
case <-sigintChannel:
|
||||
return fmt.Errorf(`
|
||||
Not waiting for %s to deploy. The deployment is ongoing...
|
||||
|
||||
If you want to stop the deployment, try:
|
||||
|
||||
abra app undeploy %s`, appName, appName)
|
||||
case <-time.After(timeout):
|
||||
return fmt.Errorf(`
|
||||
%s has not converged (%s second timeout reached).
|
||||
|
||||
This does not necessarily mean your deployment has failed, it may just be that
|
||||
the app is taking longer to deploy based on your server resources or network
|
||||
latency.
|
||||
|
||||
You can track latest deployment status with:
|
||||
|
||||
abra app ps %s
|
||||
|
||||
And inspect the logs with:
|
||||
|
||||
abra app logs %s
|
||||
`, appName, timeout, appName, appName)
|
||||
}
|
||||
}
|
||||
|
||||
// Copypasta from https://github.com/docker/cli/blob/master/cli/command/stack/swarm/list.go
|
||||
|
@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ABRA_VERSION="0.10.0-beta"
|
||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$ABRA_VERSION"
|
||||
RC_VERSION="0.10.0-beta"
|
||||
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION"
|
||||
ABRA_VERSION="0.9.0-beta"
|
||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
|
||||
RC_VERSION="0.8.0-rc1-beta"
|
||||
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
|
||||
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" == "--rc" ]; then
|
||||
@ -40,7 +40,7 @@ function install_abra_release {
|
||||
if ! type "wget" > /dev/null 2>&1; then
|
||||
echo "'wget' is not installed, cannot proceed..."
|
||||
echo "perhaps try installing manually via the releases URL?"
|
||||
echo "https://git.coopcloud.tech/toolshed/abra/releases"
|
||||
echo "https://git.coopcloud.tech/coop-cloud/abra/releases"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
@ -42,7 +42,7 @@ echo "========================================================================"
|
||||
echo "CLONING ABRA"
|
||||
echo "========================================================================"
|
||||
rm -rf abra
|
||||
git clone ssh://git@git.coopcloud.tech:2222/toolshed/abra.git
|
||||
git clone ssh://git@git.coopcloud.tech:2222/coop-cloud/abra.git
|
||||
cd abra
|
||||
git checkout main
|
||||
echo "========================================================================"
|
||||
|
@ -50,9 +50,6 @@ teardown(){
|
||||
assert_failure
|
||||
assert_output --partial 'locally unstaged changes'
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
@ -65,9 +62,6 @@ teardown(){
|
||||
run $ABRA app check "$TEST_APP_DOMAIN" --chaos
|
||||
assert_success
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
|
@ -53,9 +53,6 @@ teardown(){
|
||||
assert_failure
|
||||
assert_output --partial 'locally unstaged changes'
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
@ -69,9 +66,6 @@ teardown(){
|
||||
assert_success
|
||||
assert_output --partial 'baz'
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
|
@ -24,9 +24,6 @@ teardown(){
|
||||
_rm_remote "/etc/*.txt"
|
||||
|
||||
_rm "$BATS_TMPDIR/mydir"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
|
||||
@test "validate app argument" {
|
||||
@ -37,42 +34,6 @@ teardown(){
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "bail if unstaged changes and no --chaos" {
|
||||
_mkdir "$BATS_TMPDIR/mydir"
|
||||
_mkfile "$BATS_TMPDIR/mydir/myfile.txt" "foo"
|
||||
|
||||
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
|
||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/mydir" app:/etc
|
||||
assert_failure
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
|
||||
@test "do not bail if unstaged changes and --chaos" {
|
||||
_mkdir "$BATS_TMPDIR/mydir"
|
||||
_mkfile "$BATS_TMPDIR/mydir/myfile.txt" "foo"
|
||||
|
||||
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
|
||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/mydir" app:/etc --chaos
|
||||
assert_success
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
|
||||
@test "error if missing src/dest arguments" {
|
||||
run $ABRA app cp "$TEST_APP_DOMAIN"
|
||||
assert_failure
|
||||
|
@ -21,10 +21,8 @@ setup(){
|
||||
|
||||
teardown(){
|
||||
_reset_recipe
|
||||
_undeploy_app
|
||||
_undeploy_app2 "gitea.$TEST_SERVER"
|
||||
|
||||
_reset_app
|
||||
_undeploy_app
|
||||
_reset_tags
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
@ -48,9 +46,6 @@ teardown(){
|
||||
assert_success
|
||||
assert_output --partial 'foo'
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_failure
|
||||
assert_output --partial 'locally unstaged changes'
|
||||
@ -67,12 +62,10 @@ teardown(){
|
||||
assert_success
|
||||
assert_output --partial 'foo'
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--chaos --no-input --no-converge-checks
|
||||
assert_success
|
||||
assert_output --partial 'NEW CHAOS'
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@ -108,6 +101,8 @@ teardown(){
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "deploy latest commit if no published versions and no --chaos" {
|
||||
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
|
||||
|
||||
_remove_tags
|
||||
_wipe_env_version
|
||||
|
||||
@ -115,7 +110,7 @@ teardown(){
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --offline
|
||||
assert_success
|
||||
assert_output --partial "${_get_head_hash:0:8}"
|
||||
assert_output --partial "$latestCommit"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@ -133,6 +128,7 @@ teardown(){
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_success
|
||||
assert_output --partial "${wantHash:0:8}"
|
||||
assert_output --partial 'NEW CHAOS'
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@ -167,32 +163,6 @@ teardown(){
|
||||
assert_output --partial 'already deployed'
|
||||
}
|
||||
|
||||
@test "no re-deploy after chaos deploy without --force/--chaos" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks
|
||||
assert_failure
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --force
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "no re-deploy without --force" {
|
||||
_deploy_app
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks
|
||||
assert_failure
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "re-deploy deployed app if --force/--chaos" {
|
||||
_deploy_app
|
||||
@ -222,6 +192,19 @@ teardown(){
|
||||
run $ABRA app deploy "gitea.$TEST_SERVER" --no-input --no-converge-checks
|
||||
assert_success
|
||||
assert_output --partial "$latestVersion"
|
||||
|
||||
run $ABRA app undeploy "gitea.$TEST_SERVER" --no-input
|
||||
assert_success
|
||||
|
||||
run $ABRA app secret remove "gitea.$TEST_SERVER" --all --no-input
|
||||
assert_success
|
||||
|
||||
run $ABRA app volume remove "gitea.$TEST_SERVER" --no-input
|
||||
assert_success
|
||||
|
||||
run $ABRA app remove "gitea.$TEST_SERVER" --no-input
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/gitea.$TEST_SERVER.env"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@ -321,6 +304,7 @@ teardown(){
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
|
||||
assert_failure
|
||||
assert_output --partial 'unable to deploy, secrets not generated'
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@ -356,70 +340,10 @@ teardown(){
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
refute_output --partial 'no such file or directory'
|
||||
|
||||
_undeploy_app
|
||||
|
||||
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all
|
||||
assert_success
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "chaos version label includes dirty marker" {
|
||||
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_success
|
||||
|
||||
run $ABRA app labels "$TEST_APP_DOMAIN" --chaos
|
||||
assert_success
|
||||
assert_output --regexp 'chaos-version.*+U'
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "ignore env version checkout after deploy" {
|
||||
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
|
||||
assert_equal $(_get_current_hash) "$tagHash"
|
||||
|
||||
run $ABRA app check --ignore-env-version "$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
|
||||
assert_equal $(_get_current_hash) "$(_get_head_hash)"
|
||||
|
||||
run $ABRA app check "$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
|
||||
assert_equal $(_get_current_hash) "$tagHash"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "ignore env version on new deploy" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
|
||||
_undeploy_app
|
||||
|
||||
latestRelease=$(_latest_release)
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --ignore-env-version
|
||||
assert_success
|
||||
assert_output --partial "$latestRelease"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "no chaos version label if no chaos" {
|
||||
_deploy_app
|
||||
|
||||
run $ABRA app labels "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
refute_output --regexp "coop-cloud.abra-test-recipe.$TEST_SERVER.chaos-version"
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user