Compare commits

..

No commits in common. "main" and "test-refactor" have entirely different histories.

3949 changed files with 7875 additions and 1198484 deletions

View File

@ -3,17 +3,17 @@ kind: pipeline
name: coopcloud.tech/abra name: coopcloud.tech/abra
steps: steps:
- name: make check - name: make check
image: golang:1.22 image: golang:1.21
commands: commands:
- make check - make check
- name: make test - name: make test
image: golang:1.22 image: golang:1.21
environment: environment:
CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git ABRA_DIR: "/root/.abra"
commands: commands:
- mkdir -p $HOME/.abra - make build-abra
- git clone $CATL_URL $HOME/.abra/catalogue - ./abra help # show version, initialise $ABRA_DIR
- make test - make test
depends_on: depends_on:
- make check - make check
@ -29,7 +29,7 @@ steps:
event: tag event: tag
- name: release - name: release
image: goreleaser/goreleaser:v2.5.1 image: goreleaser/goreleaser:v1.24.0
environment: environment:
GITEA_TOKEN: GITEA_TOKEN:
from_secret: goreleaser_gitea_token from_secret: goreleaser_gitea_token
@ -47,42 +47,18 @@ steps:
image: plugins/docker image: plugins/docker
settings: settings:
auto_tag: true auto_tag: true
username: abra-bot username: 3wordchant
password: password:
from_secret: git_coopcloud_tech_token_abra_bot from_secret: git_coopcloud_tech_token_3wc
repo: git.coopcloud.tech/toolshed/abra repo: git.coopcloud.tech/coop-cloud/abra
tags: dev tags: dev
registry: git.coopcloud.tech registry: git.coopcloud.tech
when: when:
branch: event:
- main exclude:
- pull_request
depends_on: depends_on:
- make check - make check
- make test
- name: integration test
image: appleboy/drone-ssh
settings:
host:
- int.coopcloud.tech
username: abra
key:
from_secret: abra_int_private_key
port: 22
command_timeout: 60m
script_stop: true
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:
event:
- cron:
cron:
# @daily https://docs.drone.io/cron/
- integration
volumes: volumes:
- name: deps - name: deps

View File

@ -1,7 +1,7 @@
# integration test suite go env -w GOPRIVATE=coopcloud.tech
# export PASSWORD_STORE_DIR=$(pwd)/../../autonomic/passwords/passwords/
# export ABRA_DIR="$HOME/.abra_test" # export ABRA_DIR="$HOME/.abra_test"
# export ABRA_TEST_DOMAIN=test.example.com # export ABRA_TEST_DOMAIN=test.example.com
# export ABRA_CI=1 # export ABRA_SKIP_TEARDOWN=1 # for faster feedback when developing tests
# release automation
# export GITEA_TOKEN=

8
.gitea/ISSUE_TEMPLATE.md Normal file
View File

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

3
.gitignore vendored
View File

@ -2,7 +2,8 @@
.e2e.env .e2e.env
.envrc .envrc
.vscode/ .vscode/
/abra
/kadabra /kadabra
abra
dist/ dist/
tests/integration/.bats tests/integration/.bats
vendor/

View File

@ -49,8 +49,6 @@ builds:
- 5 - 5
- 6 - 6
- 7 - 7
gcflags:
- "all=-l -B"
ldflags: ldflags:
- "-X 'main.Commit={{ .Commit }}'" - "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'" - "-X 'main.Version={{ .Version }}'"

View File

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

View File

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

View File

@ -2,10 +2,9 @@ ABRA := ./cmd/abra
KADABRA := ./cmd/kadabra KADABRA := ./cmd/kadabra
COMMIT := $(shell git rev-list -1 HEAD) COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH) GOPATH := $(shell go env GOPATH)
GOVERSION := 1.22 GOVERSION := 1.21
LDFLAGS := "-X 'main.Commit=$(COMMIT)'" LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w" DIST_LDFLAGS := $(LDFLAGS)" -s -w"
GCFLAGS := "all=-l -B"
export GOPRIVATE=coopcloud.tech export GOPRIVATE=coopcloud.tech
@ -13,24 +12,22 @@ export GOPRIVATE=coopcloud.tech
all: format check build-abra test all: format check build-abra test
run-abra: run-abra:
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA) @go run -ldflags=$(LDFLAGS) $(ABRA)
run-kadabra: run-kadabra:
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA) @go run -ldflags=$(LDFLAGS) $(KADABRA)
install-abra: install-abra:
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA) @go install -ldflags=$(LDFLAGS) $(ABRA)
install-kadabra: install-kadabra:
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA) @go install -ldflags=$(LDFLAGS) $(KADABRA)
install: install-abra install-kadabra
build-abra: build-abra:
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA) @go build -v -ldflags=$(DIST_LDFLAGS) $(ABRA)
build-kadabra: build-kadabra:
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(KADABRA) @go build -v -ldflags=$(DIST_LDFLAGS) $(KADABRA)
build: build-abra build-kadabra build: build-abra build-kadabra
@ -45,10 +42,10 @@ clean:
@rm '$(GOPATH)/bin/kadabra' @rm '$(GOPATH)/bin/kadabra'
format: format:
@gofmt -s -w $$(find . -type f -name '*.go' | grep -v "/vendor/") @gofmt -s -w .
check: check:
@test -z $$(gofmt -l $$(find . -type f -name '*.go' | grep -v "/vendor/")) || \ @test -z $$(gofmt -l .) || \
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1) (echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
test: test:

View File

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

View File

@ -1,11 +1,37 @@
package app package app
import ( import (
"github.com/spf13/cobra" "github.com/urfave/cli"
) )
var AppCommand = &cobra.Command{ var AppCommand = cli.Command{
Use: "app [cmd] [args] [flags]", Name: "app",
Aliases: []string{"a"}, Aliases: []string{"a"},
Short: "Manage apps", Usage: "Manage apps",
ArgsUsage: "<domain>",
Description: "Functionality for managing the life cycle of your apps",
Subcommands: []cli.Command{
appBackupCommand,
appCheckCommand,
appCmdCommand,
appConfigCommand,
appCpCommand,
appDeployCommand,
appErrorsCommand,
appListCommand,
appLogsCommand,
appNewCommand,
appPsCommand,
appRemoveCommand,
appRestartCommand,
appRestoreCommand,
appRollbackCommand,
appRunCommand,
appSecretCommand,
appServicesCommand,
appUndeployCommand,
appUpgradeCommand,
appVersionCommand,
appVolumeCommand,
},
} }

View File

@ -6,302 +6,291 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppBackupListCommand = &cobra.Command{ var snapshot string
Use: "list <domain> [flags]", var snapshotFlag = &cli.StringFlag{
Aliases: []string{"ls"}, Name: "snapshot, s",
Short: "List the contents of a snapshot", Usage: "Lists specific snapshot",
Args: cobra.ExactArgs(1), Destination: &snapshot,
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)
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
log.Fatal(err)
}
execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
}
if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if showAllPaths {
log.Debugf("including SHOW_ALL=%v in backupbot exec invocation", showAllPaths)
execEnv = append(execEnv, fmt.Sprintf("SHOW_ALL=%v", showAllPaths))
}
if timestamps {
log.Debugf("including TIMESTAMPS=%v in backupbot exec invocation", timestamps)
execEnv = append(execEnv, fmt.Sprintf("TIMESTAMPS=%v", timestamps))
}
if _, err = internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
},
} }
var AppBackupDownloadCommand = &cobra.Command{ var includePath string
Use: "download <domain> [flags]", var includePathFlag = &cli.StringFlag{
Aliases: []string{"d"}, Name: "path, p",
Short: "Download a snapshot", Usage: "Include path",
Long: `Downloads a backup.tar.gz to the current working directory. Destination: &includePath,
}
"--volumes/-v" includes data contained in volumes alongide paths specified in var resticRepo string
"backupbot.backup.path" labels.`, var resticRepoFlag = &cli.StringFlag{
Args: cobra.ExactArgs(1), Name: "repo, r",
ValidArgsFunction: func( Usage: "Restic repository",
cmd *cobra.Command, Destination: &resticRepo,
args []string, }
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete() var appBackupListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
snapshotFlag,
includePathFlag,
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) Usage: "List all backups",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
targetContainer, err := internal.RetrieveBackupBotContainer(cl) targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
} }
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" { if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot)) execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
} }
if includePath != "" { if includePath != "" {
log.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath) logrus.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath)) execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
} }
if includeSecrets { if err := internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
log.Debugf("including SECRETS=%v in backupbot exec invocation", includeSecrets) logrus.Fatal(err)
execEnv = append(execEnv, fmt.Sprintf("SECRETS=%v", includeSecrets))
} }
if includeVolumes { return nil
log.Debugf("including VOLUMES=%v in backupbot exec invocation", includeVolumes) },
execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%v", includeVolumes)) }
var appBackupDownloadCommand = cli.Command{
Name: "download",
Aliases: []string{"d"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
snapshotFlag,
includePathFlag,
},
Before: internal.SubCommandBefore,
Usage: "Download a backup",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
} }
if _, err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil { if !internal.Chaos {
log.Fatal(err) if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil {
logrus.Fatal(err)
}
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" {
logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
}
if includePath != "" {
logrus.Debugf("including INCLUDE_PATH=%s in backupbot exec invocation", includePath)
execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
}
if err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil {
logrus.Fatal(err)
} }
remoteBackupDir := "/tmp/backup.tar.gz" remoteBackupDir := "/tmp/backup.tar.gz"
currentWorkingDir := "." currentWorkingDir := "."
if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil { if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
fmt.Println("backup successfully downloaded to current working directory")
return nil
}, },
} }
var AppBackupCreateCommand = &cobra.Command{ var appBackupCreateCommand = cli.Command{
Use: "create <domain> [flags]", Name: "create",
Aliases: []string{"c"}, Aliases: []string{"c"},
Short: "Create a new snapshot", Flags: []cli.Flag{
Args: cobra.ExactArgs(1), internal.DebugFlag,
ValidArgsFunction: func( internal.OfflineFlag,
cmd *cobra.Command, resticRepoFlag,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) Usage: "Create a new backup",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
targetContainer, err := internal.RetrieveBackupBotContainer(cl) targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
execEnv := []string{ execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
fmt.Sprintf("SERVICE=%s", app.Domain), if resticRepo != "" {
"MACHINE_LOGS=true", logrus.Debugf("including RESTIC_REPO=%s in backupbot exec invocation", resticRepo)
execEnv = append(execEnv, fmt.Sprintf("RESTIC_REPO=%s", resticRepo))
} }
if retries != "" { if err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil {
log.Debugf("including RETRIES=%s in backupbot exec invocation", retries) logrus.Fatal(err)
execEnv = append(execEnv, fmt.Sprintf("RETRIES=%s", retries))
} }
if _, err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil { return nil
log.Fatal(err)
}
}, },
} }
var AppBackupSnapshotsCommand = &cobra.Command{ var appBackupSnapshotsCommand = cli.Command{
Use: "snapshots <domain> [flags]", Name: "snapshots",
Aliases: []string{"s"}, Aliases: []string{"s"},
Short: "List all snapshots", Flags: []cli.Flag{
Args: cobra.ExactArgs(1), internal.DebugFlag,
ValidArgsFunction: func( internal.OfflineFlag,
cmd *cobra.Command, snapshotFlag,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) Usage: "List backup snapshots",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
targetContainer, err := internal.RetrieveBackupBotContainer(cl) targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
execEnv := []string{ execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
fmt.Sprintf("SERVICE=%s", app.Domain), if snapshot != "" {
"MACHINE_LOGS=true", logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
} }
if _, err = internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil { if err := internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil
}, },
} }
var AppBackupCommand = &cobra.Command{ var appBackupCommand = cli.Command{
Use: "backup [cmd] [args] [flags]", Name: "backup",
Aliases: []string{"b"}, Aliases: []string{"b"},
Short: "Manage app backups", Usage: "Manage app backups",
} ArgsUsage: "<domain>",
Subcommands: []cli.Command{
var ( appBackupListCommand,
snapshot string appBackupSnapshotsCommand,
retries string appBackupDownloadCommand,
includePath string appBackupCreateCommand,
showAllPaths bool },
timestamps bool
includeSecrets bool
includeVolumes bool
)
func init() {
AppBackupListCommand.Flags().StringVarP(
&snapshot,
"snapshot",
"s",
"",
"list specific snapshot",
)
AppBackupListCommand.Flags().BoolVarP(
&showAllPaths,
"all",
"a",
false,
"show all paths",
)
AppBackupListCommand.Flags().BoolVarP(
&timestamps,
"timestamps",
"t",
false,
"include timestamps",
)
AppBackupDownloadCommand.Flags().StringVarP(
&snapshot,
"snapshot",
"s",
"",
"list specific snapshot",
)
AppBackupDownloadCommand.Flags().StringVarP(
&includePath,
"path",
"p",
"",
"volumes path",
)
AppBackupDownloadCommand.Flags().BoolVarP(
&includeSecrets,
"secrets",
"S",
false,
"include secrets",
)
AppBackupDownloadCommand.Flags().BoolVarP(
&includeVolumes,
"volumes",
"v",
false,
"include volumes",
)
AppBackupDownloadCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppBackupCreateCommand.Flags().StringVarP(
&retries,
"retries",
"r",
"1",
"number of retry attempts",
)
AppBackupCreateCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
} }

View File

@ -1,22 +1,23 @@
package app package app
import ( import (
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
"github.com/charmbracelet/lipgloss" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppCheckCommand = &cobra.Command{ var appCheckCommand = cli.Command{
Use: "check <domain> [flags]", Name: "check",
Aliases: []string{"chk"}, Aliases: []string{"chk"},
Short: "Ensure an app is well configured", Usage: "Ensure an app is well configured",
Long: `Compare env vars in both the app ".env" and recipe ".env.sample" file. Description: `
This command compares env vars in both the app ".env" and recipe ".env.sample"
file.
The goal is to ensure that recipe ".env.sample" env vars are defined in your The goal is to ensure that recipe ".env.sample" env vars are defined in your
app ".env" file. Only env var definitions in the ".env.sample" which are app ".env" file. Only env var definitions in the ".env.sample" which are
@ -26,66 +27,55 @@ these env vars, then "check" will complain.
Recipe maintainers may or may not provide defaults for env vars within their Recipe maintainers may or may not provide defaults for env vars within their
recipes regardless of commenting or not (e.g. through the use of recipes regardless of commenting or not (e.g. through the use of
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`, ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
Args: cobra.ExactArgs(1), ArgsUsage: "<domain>",
ValidArgsFunction: func( Flags: []cli.Flag{
cmd *cobra.Command, internal.DebugFlag,
args []string, internal.ChaosFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.OfflineFlag,
return autocomplete.AppNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
table, err := formatter.CreateTable() if !internal.Chaos {
if err != nil { if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
table. if !internal.Offline {
Headers( if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
fmt.Sprintf("%s .env.sample", app.Recipe.Name), logrus.Fatal(err)
fmt.Sprintf("%s.env", app.Name),
).
StyleFunc(func(row, col int) lipgloss.Style {
switch {
case col == 1:
return lipgloss.NewStyle().Padding(0, 1, 0, 1).Align(lipgloss.Center)
default:
return lipgloss.NewStyle().Padding(0, 1, 0, 1)
} }
}) }
envVars, err := appPkg.CheckEnv(app) if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
tableCol := []string{"recipe env sample", "app env"}
table := formatter.CreateTable(tableCol)
envVars, err := config.CheckEnv(app)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, envVar := range envVars { for _, envVar := range envVars {
if envVar.Present { if envVar.Present {
val := []string{envVar.Name, "✅"} table.Append([]string{envVar.Name, "✅"})
table.Row(val...)
} else { } else {
val := []string{envVar.Name, "❌"} table.Append([]string{envVar.Name, "❌"})
table.Row(val...)
} }
} }
if err := formatter.PrintTable(table); err != nil { table.Render()
log.Fatal(err)
} return nil
}, },
} }
func init() {
AppCheckCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -5,117 +5,106 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"slices" "path"
"sort" "sort"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/config"
"github.com/spf13/cobra" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppCmdCommand = &cobra.Command{ var appCmdCommand = cli.Command{
Use: "command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]", Name: "command",
Aliases: []string{"cmd"}, Aliases: []string{"cmd"},
Short: "Run app commands", Usage: "Run app commands",
Long: `Run an app specific command. Description: `Run an app specific command.
These commands are bash functions, defined in the abra.sh of the recipe itself. These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local/-l". work station by passing "--local". Arguments can be passed into these functions
using the "-- <args>" syntax.
N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must Example:
be passed *before* the "--". It is possible to pass arguments without the "--"
as long as no dashes are present (i.e. "foo" works without "--", "-foo"
does not).`,
Example: ` # pass <cmd> args/flags without "--"
abra app cmd 1312.net app my_cmd_arg foo --user bar
# pass <cmd> args/flags with "--" abra app cmd example.com app create_user -- me@example.com
abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv `,
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
Flags: []cli.Flag{
internal.DebugFlag,
internal.LocalCmdFlag,
internal.RemoteUserFlag,
internal.TtyFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
Subcommands: []cli.Command{appCmdListCommand},
BashComplete: func(ctx *cli.Context) {
args := ctx.Args()
switch len(args) {
case 0:
autocomplete.AppNameComplete(ctx)
case 1:
autocomplete.ServiceNameComplete(args.Get(0))
case 2:
cmdNameComplete(args.Get(0))
}
},
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
# drop the [service] arg if using "--local/-l" if err := recipe.EnsureExists(app.Recipe); err != nil {
abra app cmd 1312.net my_cmd --local`, logrus.Fatal(err)
Args: func(cmd *cobra.Command, args []string) error { }
if local {
if !(len(args) >= 2) { if !internal.Chaos {
return errors.New("requires at least 2 arguments with --local/-l") if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
} }
if slices.Contains(os.Args, "--") { if !internal.Offline {
if cmd.ArgsLenAtDash() > 2 { if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
return errors.New("accepts at most 2 args with --local/-l") logrus.Fatal(err)
} }
} }
// NOTE(d1): it is unclear how to correctly validate this case if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
// logrus.Fatal(err)
// abra app cmd 1312.net app test_cmd_args foo --local
// FATAL <recipe> doesn't have a app function
//
// "app" should not be there, but there is no reliable way to detect arg
// count when the user can pass an arbitrary amount of recipe command
// arguments
return nil
}
if !(len(args) >= 3) {
return errors.New("requires at least 3 arguments")
}
return nil
},
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
if !local {
return autocomplete.ServiceNameComplete(args[0])
} }
return autocomplete.CommandNameComplete(args[0])
case 2:
if !local {
return autocomplete.CommandNameComplete(args[0])
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err)
} }
if local && remoteUser != "" { if internal.LocalCmd && internal.RemoteUser != "" {
log.Fatal("cannot use --local & --user together") internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
} }
hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local) hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil { abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
log.Fatalf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name) logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
} }
log.Fatal(err) logrus.Fatal(err)
} }
if local { if internal.LocalCmd {
cmdName := args[1] if !(len(c.Args()) >= 2) {
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
log.Fatal(err)
} }
log.Debugf("--local detected, running %s on local work station", cmdName) cmdName := c.Args().Get(1)
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("--local detected, running %s on local work station", cmdName)
var exportEnv string var exportEnv string
for k, v := range app.Env { for k, v := range app.Env {
@ -124,95 +113,70 @@ does not).`,
var sourceAndExec string var sourceAndExec string
if hasCmdArgs { if hasCmdArgs {
log.Debugf("parsed following command arguments: %s", parsedCmdArgs) logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName, parsedCmdArgs) sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName, parsedCmdArgs)
} else { } else {
log.Debug("did not detect any command arguments") logrus.Debug("did not detect any command arguments")
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName) sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName)
} }
shell := "/bin/bash" shell := "/bin/bash"
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
log.Debugf("%s does not exist locally, use /bin/sh as fallback", shell) logrus.Debugf("%s does not exist locally, use /bin/sh as fallback", shell)
shell = "/bin/sh" shell = "/bin/sh"
} }
cmd := exec.Command(shell, "-c", sourceAndExec) cmd := exec.Command(shell, "-c", sourceAndExec)
if err := internal.RunCmd(cmd); err != nil { if err := internal.RunCmd(cmd); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return
}
cmdName := args[2]
if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
log.Fatal(err)
}
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
matchingServiceName := false
targetServiceName := args[1]
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
log.Fatalf("no service %s for %s?", targetServiceName, app.Name)
}
log.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
if hasCmdArgs {
log.Debugf("parsed following command arguments: %s", parsedCmdArgs)
} else { } else {
log.Debug("did not detect any command arguments") if !(len(c.Args()) >= 3) {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments"))
}
targetServiceName := c.Args().Get(1)
cmdName := c.Args().Get(2)
if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
logrus.Fatal(err)
}
serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil {
logrus.Fatal(err)
}
matchingServiceName := false
for _, serviceName := range serviceNames {
if serviceName == targetServiceName {
matchingServiceName = true
}
}
if !matchingServiceName {
logrus.Fatalf("no service %s for %s?", targetServiceName, app.Name)
}
logrus.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
if hasCmdArgs {
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
} else {
logrus.Debug("did not detect any command arguments")
}
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
logrus.Fatal(err)
}
} }
cl, err := client.New(app.Server) return nil
if err != nil {
log.Fatal(err)
}
if err := internal.RunCmdRemote(
cl,
app,
requestTTY,
app.Recipe.AbraShPath,
targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
log.Fatal(err)
}
},
}
var AppCmdListCommand = &cobra.Command{
Use: "list <domain> [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 {
log.Fatal(err)
}
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
if err != nil {
log.Fatal(err)
}
sort.Strings(cmdNames)
for _, cmdName := range cmdNames {
fmt.Println(cmdName)
}
}, },
} }
@ -235,42 +199,75 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
return hasCmdArgs, parsedCmdArgs return hasCmdArgs, parsedCmdArgs
} }
var ( func cmdNameComplete(appName string) {
local bool app, err := app.Get(appName)
remoteUser string if err != nil {
requestTTY bool return
) }
cmdNames, _ := getShCmdNames(app)
func init() { if err != nil {
AppCmdCommand.Flags().BoolVarP( return
&local, }
"local", for _, n := range cmdNames {
"l", fmt.Println(n)
false, }
"run command locally", }
)
var appCmdListCommand = cli.Command{
AppCmdCommand.Flags().StringVarP( Name: "list",
&remoteUser, Aliases: []string{"ls"},
"user", Usage: "List all available commands",
"u", ArgsUsage: "<domain>",
"", Flags: []cli.Flag{
"request remote user", internal.DebugFlag,
) internal.OfflineFlag,
internal.ChaosFlag,
AppCmdCommand.Flags().BoolVarP( },
&requestTTY, BashComplete: autocomplete.AppNameComplete,
"tty", Before: internal.SubCommandBefore,
"t", Action: func(c *cli.Context) error {
false, app := internal.ValidateApp(c)
"request remote TTY",
) if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
AppCmdCommand.Flags().BoolVarP( }
&internal.Chaos,
"chaos", if !internal.Chaos {
"C", if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
false, logrus.Fatal(err)
"ignore uncommitted recipes changes", }
)
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
cmdNames, err := getShCmdNames(app)
if err != nil {
logrus.Fatal(err)
}
for _, cmdName := range cmdNames {
fmt.Println(cmdName)
}
return nil
},
}
func getShCmdNames(app config.App) ([]string, error) {
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
cmdNames, err := config.ReadAbraShCmdNames(abraShPath)
if err != nil {
return nil, err
}
sort.Strings(cmdNames)
return cmdNames, nil
} }

View File

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

View File

@ -1,57 +1,64 @@
package app package app
import ( import (
"errors"
"os" "os"
"os/exec" "os/exec"
appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppConfigCommand = &cobra.Command{ var appConfigCommand = cli.Command{
Use: "config <domain> [flags]", Name: "config",
Aliases: []string{"cfg"}, Aliases: []string{"cfg"},
Short: "Edit app config", Usage: "Edit app config",
Example: " abra config 1312.net", ArgsUsage: "<domain>",
Args: cobra.ExactArgs(1), Flags: []cli.Flag{
ValidArgsFunction: func( internal.DebugFlag,
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
files, err := appPkg.LoadAppFiles("") BashComplete: autocomplete.AppNameComplete,
if err != nil { Action: func(c *cli.Context) error {
log.Fatal(err) appName := c.Args().First()
if appName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no app provided"))
}
files, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
} }
appName := args[0]
appFile, exists := files[appName] appFile, exists := files[appName]
if !exists { if !exists {
log.Fatalf("cannot find app with name %s", appName) logrus.Fatalf("cannot find app with name %s", appName)
} }
ed, ok := os.LookupEnv("EDITOR") ed, ok := os.LookupEnv("EDITOR")
if !ok { if !ok {
edPrompt := &survey.Select{ edPrompt := &survey.Select{
Message: "which editor do you wish to use?", Message: "Which editor do you wish to use?",
Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"}, Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"},
} }
if err := survey.AskOne(edPrompt, &ed); err != nil { if err := survey.AskOne(edPrompt, &ed); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
c := exec.Command(ed, appFile.Path) cmd := exec.Command(ed, appFile.Path)
c.Stdin = os.Stdin cmd.Stdin = os.Stdin
c.Stdout = os.Stdout cmd.Stdout = os.Stdout
c.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := c.Run(); err != nil { if err := cmd.Run(); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil
}, },
} }

View File

@ -15,61 +15,65 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppCpCommand = &cobra.Command{ var appCpCommand = cli.Command{
Use: "cp <domain> <src> <dst> [flags]", Name: "cp",
Aliases: []string{"c"}, Aliases: []string{"c"},
Short: "Copy files to/from a deployed app service", ArgsUsage: "<domain> <src> <dst>",
Example: ` # copy myfile.txt to the root of the app service Flags: []cli.Flag{
abra app cp 1312.net myfile.txt app:/ internal.DebugFlag,
internal.NoInputFlag,
# copy that file back to your current working directory locally
abra app cp 1312.net app:/myfile.txt`,
Args: cobra.ExactArgs(3),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
default:
return nil, cobra.ShellCompDirectiveDefault
}
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) Usage: "Copy files to/from a deployed app service",
Description: `
Copy files to and from any app service file system.
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { If you want to copy a myfile.txt to the root of the app service:
log.Fatal(err)
abra app cp <domain> myfile.txt app:/
And if you want to copy that file back to your current working directory locally:
abra app cp <domain> app:/myfile.txt .
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
src := c.Args().Get(1)
dst := c.Args().Get(2)
if src == "" {
logrus.Fatal("missing <src> argument")
}
if dst == "" {
logrus.Fatal("missing <dest> argument")
} }
src := args[1]
dst := args[2]
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst) srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service) container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server) logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
if toContainer { if toContainer {
err = CopyToContainer(cl, container.ID, srcPath, dstPath) err = CopyToContainer(cl, container.ID, srcPath, dstPath)
@ -77,8 +81,10 @@ var AppCpCommand = &cobra.Command{
err = CopyFromContainer(cl, container.ID, srcPath, dstPath) err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
} }
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil
}, },
} }
@ -161,7 +167,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
return err return err
} }
log.Debugf("copy %s from local to %s on container", srcPath, dstPath) logrus.Debugf("copy %s from local to %s on container", srcPath, dstPath)
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil { if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
return err return err
@ -371,13 +377,3 @@ func moveFile(sourcePath, destPath string) error {
} }
return nil return nil
} }
func init() {
AppCpCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -5,354 +5,254 @@ import (
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
dockerClient "github.com/docker/docker/client" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/urfave/cli"
) )
var AppDeployCommand = &cobra.Command{ var appDeployCommand = cli.Command{
Use: "deploy <domain> [version] [flags]", Name: "deploy",
Aliases: []string{"d"}, Aliases: []string{"d"},
Short: "Deploy an app", Usage: "Deploy an app",
Long: `Deploy an app. ArgsUsage: "<domain> [<version>]",
Flags: []cli.Flag{
This command supports chaos operations. Use "--chaos/-C" to deploy your recipe internal.DebugFlag,
checkout as-is. Recipe commit hashes are also supported as values for internal.NoInputFlag,
"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`, internal.ForceFlag,
Example: ` # standard deployment internal.ChaosFlag,
abra app deploy 1312.net internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
# chaos deployment internal.OfflineFlag,
abra app deploy 1312.net --chaos
# deploy specific version
abra app deploy 1312.net 2.0.0+1.2.3
# deploy a specific git hash
abra app deploy 1312.net 886db76d`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
var ( Description: `
deployWarnMessages []string Deploy an app. It does not support incrementing the version of a deployed app,
toDeployVersion string for this you need to look at the "abra app upgrade <domain>" command.
isChaosCommit bool
toDeployChaosVersion = config.CHAOS_DEFAULT
)
app := internal.ValidateApp(args) You may pass "--force" to re-deploy the same version again. This can be useful
if the container runtime has gotten into a weird state.
if err := validateArgsAndFlags(args); err != nil { Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
log.Fatal(err) including unstaged changes and can be useful for live hacking and testing new
recipes.
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
specificVersion := c.Args().Get(1)
if specificVersion != "" && internal.Chaos {
logrus.Fatal("cannot use <version> and --chaos together")
} }
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := lint.LintForErrors(app.Recipe); err != nil { if !internal.Chaos {
log.Fatal(err) if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
} }
r, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
log.Debugf("checking whether %s is already deployed", app.StackName()) isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) { // NOTE(d1): check out specific version before dealing with secrets. This
log.Fatalf("%s is already deployed", app.Name) // is because we need to deal with GetComposeFiles under the hood and these
// files change from version to version which therefore affects which
// secrets might be generated
version := deployedVersion
if specificVersion != "" {
version = specificVersion
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
} }
if len(args) == 2 && args[1] != "" { secStats, err := secret.PollSecretsStatus(cl, app)
toDeployVersion = args[1] if err != nil {
logrus.Fatal(err)
} }
if !deployMeta.IsDeployed && for _, secStat := range secStats {
toDeployVersion == "" && if !secStat.CreatedOnRemote {
app.Recipe.EnvVersion != "" && !internal.IgnoreEnvVersion { logrus.Fatalf("unable to deploy, secrets not generated (%s)?", secStat.LocalName)
log.Debugf("new deployment, choosing .env version: %s", app.Recipe.EnvVersion) }
toDeployVersion = app.Recipe.EnvVersion
} }
if !internal.Chaos && toDeployVersion == "" { if isDeployed {
if err := getLatestVersionOrCommit(app, &toDeployVersion); err != nil { if internal.Force || internal.Chaos {
log.Fatal(err) logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
} else {
logrus.Fatalf("%s is already deployed", app.Name)
}
}
if !internal.Chaos && specificVersion == "" {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
}
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil {
logrus.Fatal(err)
}
if len(versions) == 0 && !internal.Chaos {
logrus.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline)
if err != nil {
logrus.Warn(err)
}
for _, recipeVersion := range recipeVersions {
for version := range recipeVersion {
versions = append(versions, version)
}
}
}
if len(versions) > 0 && !internal.Chaos {
version = versions[len(versions)-1]
logrus.Debugf("choosing %s as version to deploy", version)
if err := recipe.EnsureVersion(app.Recipe, version); err != nil {
logrus.Fatal(err)
}
} else {
head, err := git.GetRecipeHead(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
version = formatter.SmallSHA(head.String())
logrus.Warn("no versions detected, using latest commit")
} }
} }
if internal.Chaos { if internal.Chaos {
if err := getChaosVersion(app, &toDeployVersion, &toDeployChaosVersion); err != nil { logrus.Warnf("chaos mode engaged")
log.Fatal(err) var err error
} version, err = recipe.ChaosVersion(app.Recipe)
}
if !internal.Chaos {
isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
if isChaosCommit {
log.Debugf("assuming chaos commit: %s", toDeployVersion)
internal.Chaos = true
toDeployChaosVersion = toDeployVersion
toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
if err != nil {
log.Fatal(err)
}
} }
} }
if err := validateSecrets(cl, app); err != nil { abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
log.Fatal(err) abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
}
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for k, v := range abraShEnv { for k, v := range abraShEnv {
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
stackName := app.StackName()
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Composefiles: composeFiles, Composefiles: composeFiles,
Namespace: stackName, Namespace: stackName,
Prune: false, Prune: false,
ResolveImage: stack.ResolveImageAlways, ResolveImage: stack.ResolveImageAlways,
Detach: false,
} }
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env) compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
toDeployChaosVersionLabel := toDeployChaosVersion config.ExposeAllEnv(stackName, compose, app.Env)
if app.Recipe.Dirty { config.SetRecipeLabel(compose, stackName, app.Recipe)
toDeployChaosVersionLabel = formatter.AddDirtyMarker(toDeployChaosVersionLabel) config.SetChaosLabel(compose, stackName, internal.Chaos)
} config.SetChaosVersionLabel(compose, stackName, version)
config.SetUpdateLabel(compose, stackName, app.Env)
appPkg.ExposeAllEnv(stackName, compose, app.Env) envVars, err := config.CheckEnv(app)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersionLabel)
}
appPkg.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, envVar := range envVars { for _, envVar := range envVars {
if !envVar.Present { if !envVar.Present {
deployWarnMessages = append(deployWarnMessages, logrus.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
)
} }
} }
if err := internal.DeployOverview(app, version, "continue with deployment?"); err != nil {
logrus.Fatal(err)
}
if !internal.NoDomainChecks { 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 { if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
log.Debug("skipping domain checks, no DOMAIN=... configured") logrus.Warn("skipping domain checks as no DOMAIN=... configured for app")
} }
} else { } else {
log.Debug("skipping domain checks") logrus.Warn("skipping domain checks as requested")
} }
deployedVersion := config.NO_VERSION_DEFAULT stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
if deployMeta.IsDeployed {
deployedVersion = deployMeta.Version
}
toWriteVersion := toDeployVersion
if internal.Chaos || isChaosCommit {
toWriteVersion = toDeployChaosVersion
}
if err := internal.DeployOverview(
app,
deployWarnMessages,
deployedVersion,
deployMeta.ChaosVersion,
toDeployVersion,
toDeployChaosVersion,
toWriteVersion,
); err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil { if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"] postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
if ok && !internal.DontWaitConverge { if ok && !internal.DontWaitConverge {
log.Debugf("run the following post-deploy commands: %s", postDeployCmds) logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil { if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err) logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
} }
} }
return nil
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
}
}, },
} }
func getChaosVersion(app app.App, toDeployVersion, toDeployChaosVersion *string) error {
var err error
*toDeployChaosVersion, err = app.Recipe.ChaosVersion()
if err != nil {
return err
}
*toDeployVersion, err = app.Recipe.GetVersionLabelLocal()
if err != nil {
return err
}
return nil
}
func getLatestVersionOrCommit(app app.App, toDeployVersion *string) error {
versions, err := app.Recipe.Tags()
if err != nil {
return 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 {
return err
}
return nil
}
head, err := app.Recipe.Head()
if err != nil {
return err
}
*toDeployVersion = formatter.SmallSHA(head.String())
return nil
}
// validateArgsAndFlags ensures compatible args/flags.
func validateArgsAndFlags(args []string) error {
if len(args) == 2 && args[1] != "" && internal.Chaos {
return fmt.Errorf("cannot use [version] and --chaos together")
}
return nil
}
func validateSecrets(cl *dockerClient.Client, app app.App) error {
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
return err
}
for _, secStat := range secStats {
if !secStat.CreatedOnRemote {
return fmt.Errorf("secret not generated: %s", secStat.LocalName)
}
}
return nil
}
func init() {
AppDeployCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppDeployCommand.Flags().BoolVarP(
&internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
AppDeployCommand.Flags().BoolVarP(
&internal.NoDomainChecks,
"no-domain-checks",
"D",
false,
"disable public DNS checks",
)
AppDeployCommand.Flags().BoolVarP(
&internal.DontWaitConverge,
"no-converge-checks",
"c",
false,
"disable converge logic checks",
)
}

View File

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

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

@ -0,0 +1,142 @@
package app
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var appErrorsCommand = cli.Command{
Name: "errors",
Usage: "List errors for a deployed app",
ArgsUsage: "<domain>",
Description: `
List errors for a deployed app.
This is a best-effort implementation and an attempt to gather a number of tips
& tricks for finding errors together into one convenient command. When an app
is failing to deploy or having issues, it could be a lot of things.
This command currently takes into account:
Is the service deployed?
Is the service killed by an OOM error?
Is the service reporting an error (like in "ps --no-trunc" output)
Is the service healthcheck failing? what are the healthcheck logs?
Got any more ideas? Please let us know:
https://git.coopcloud.tech/coop-cloud/organising/issues/new/choose
This command is best accompanied by "abra app logs <domain>" which may reveal
further information which can help you debug the cause of an app failure via
the logs.
`,
Aliases: []string{"e"},
Flags: []cli.Flag{
internal.DebugFlag,
internal.WatchFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
if !internal.Watch {
if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err)
}
return nil
}
for {
if err := checkErrors(c, cl, app); err != nil {
logrus.Fatal(err)
}
time.Sleep(2 * time.Second)
}
},
}
func checkErrors(c *cli.Context, cl *dockerClient.Client, app config.App) error {
recipe, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil {
return err
}
for _, service := range recipe.Config.Services {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
if err != nil {
return err
}
if len(containers) == 0 {
logrus.Warnf("%s is not up, something seems wrong", service.Name)
continue
}
container := containers[0]
containerState, err := cl.ContainerInspect(context.Background(), container.ID)
if err != nil {
logrus.Fatal(err)
}
if containerState.State.OOMKilled {
logrus.Warnf("%s has been killed due to an out of memory error", service.Name)
}
if containerState.State.Error != "" {
logrus.Warnf("%s reports this error: %s", service.Name, containerState.State.Error)
}
if containerState.State.Health != nil {
if containerState.State.Health.Status != "healthy" {
logrus.Warnf("%s healthcheck status is %s", service.Name, containerState.State.Health.Status)
logrus.Warnf("%s healthcheck has failed %s times", service.Name, strconv.Itoa(containerState.State.Health.FailingStreak))
for _, log := range containerState.State.Health.Log {
logrus.Warnf("%s healthcheck logs: %s", service.Name, strings.TrimSpace(log.Output))
}
}
}
}
return nil
}
func getServiceName(names []string) string {
containerName := strings.Join(names, " ")
trimmed := strings.TrimPrefix(containerName, "/")
return strings.Split(trimmed, ".")[0]
}

View File

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

View File

@ -8,14 +8,37 @@ import (
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var status bool
var statusFlag = &cli.BoolFlag{
Name: "status, S",
Usage: "Show app deployment status",
Destination: &status,
}
var recipeFilter string
var recipeFlag = &cli.StringFlag{
Name: "recipe, r",
Value: "",
Usage: "Show apps of a specific recipe",
Destination: &recipeFilter,
}
var listAppServer string
var listAppServerFlag = &cli.StringFlag{
Name: "server, s",
Value: "",
Usage: "Show apps of a specific server",
Destination: &listAppServer,
}
type appStatus struct { type appStatus struct {
Server string `json:"server"` Server string `json:"server"`
Recipe string `json:"recipe"` Recipe string `json:"recipe"`
@ -38,36 +61,42 @@ type serverStatus struct {
UpgradeCount int `json:"upgradeCount"` UpgradeCount int `json:"upgradeCount"`
} }
var AppListCommand = &cobra.Command{ var appListCommand = cli.Command{
Use: "list [flags]", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Short: "List all managed apps", Usage: "List all managed apps",
Long: `Generate a report of all managed apps. Description: `
Read the local file system listing of apps and servers (e.g. ~/.abra/) to
generate a report of all your apps.
Use "--status/-S" flag to query all servers for the live deployment status.`, By passing the "--status/-S" flag, you can query all your servers for the
Example: ` # list apps of all servers without live status actual live deployment status. Depending on how many servers you manage, this
abra app ls can take some time.
`,
# list apps of a specific server with live status Flags: []cli.Flag{
abra app ls -s 1312.net -S internal.DebugFlag,
internal.MachineReadableFlag,
# list apps of all servers which match a specific recipe statusFlag,
abra app ls -r gitea`, listAppServerFlag,
Args: cobra.NoArgs, recipeFlag,
Run: func(cmd *cobra.Command, args []string) { internal.OfflineFlag,
appFiles, err := appPkg.LoadAppFiles(listAppServer) },
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
appFiles, err := config.LoadAppFiles(listAppServer)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
apps, err := appPkg.GetApps(appFiles, recipeFilter) apps, err := config.GetApps(appFiles, recipeFilter)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
sort.Sort(appPkg.ByServerAndRecipe(apps)) sort.Sort(config.ByServerAndRecipe(apps))
statuses := make(map[string]map[string]string) statuses := make(map[string]map[string]string)
var catl recipe.RecipeCatalogue
if status { if status {
alreadySeen := make(map[string]bool) alreadySeen := make(map[string]bool)
for _, app := range apps { for _, app := range apps {
@ -76,9 +105,14 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
} }
} }
statuses, err = appPkg.GetAppStatuses(apps, internal.MachineReadable) statuses, err = config.GetAppStatuses(apps, internal.MachineReadable)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
catl, err = recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil {
logrus.Fatal(err)
} }
} }
@ -96,7 +130,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
} }
} }
if app.Recipe.Name == recipeFilter || recipeFilter == "" { if app.Recipe == recipeFilter || recipeFilter == "" {
if recipeFilter != "" { if recipeFilter != "" {
// only count server if matches filter // only count server if matches filter
totalServersCount++ totalServersCount++
@ -143,20 +177,20 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
var newUpdates []string var newUpdates []string
if version != "unknown" { if version != "unknown" {
updates, err := app.Recipe.Tags() updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, update := range updates { for _, update := range updates {
parsedUpdate, err := tagcmp.Parse(update) parsedUpdate, err := tagcmp.Parse(update)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if update != version && parsedUpdate.IsGreaterThan(parsedVersion) { if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
@ -173,14 +207,14 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
stats.LatestCount++ stats.LatestCount++
} }
} else { } else {
newUpdates = internal.SortVersionsDesc(newUpdates) newUpdates = internal.ReverseStringList(newUpdates)
appStats.Upgrade = strings.Join(newUpdates, "\n") appStats.Upgrade = strings.Join(newUpdates, "\n")
stats.UpgradeCount++ stats.UpgradeCount++
} }
} }
appStats.Server = app.Server appStats.Server = app.Server
appStats.Recipe = app.Recipe.Name appStats.Recipe = app.Recipe
appStats.AppName = app.Name appStats.AppName = app.Name
appStats.Domain = app.Domain appStats.Domain = app.Domain
@ -192,12 +226,11 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
if internal.MachineReadable { if internal.MachineReadable {
jsonstring, err := json.Marshal(allStats) jsonstring, err := json.Marshal(allStats)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} else { } else {
fmt.Println(string(jsonstring)) fmt.Println(string(jsonstring))
} }
return nil
return
} }
alreadySeen := make(map[string]bool) alreadySeen := make(map[string]bool)
@ -208,118 +241,60 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
serverStat := allStats[app.Server] serverStat := allStats[app.Server]
headers := []string{"RECIPE", "DOMAIN", "SERVER"} tableCol := []string{"recipe", "domain"}
if status { if status {
headers = append(headers, []string{ tableCol = append(tableCol, []string{"status", "chaos", "version", "upgrade", "autoupdate"}...)
"STATUS",
"CHAOS",
"VERSION",
"UPGRADE",
"AUTOUPDATE"}...,
)
} }
table, err := formatter.CreateTable() table := formatter.CreateTable(tableCol)
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
var rows [][]string
for _, appStat := range serverStat.Apps { for _, appStat := range serverStat.Apps {
row := []string{appStat.Recipe, appStat.Domain, appStat.Server} tableRow := []string{appStat.Recipe, appStat.Domain}
if status { if status {
chaosStatus := appStat.Chaos chaosStatus := appStat.Chaos
if chaosStatus != "unknown" { if chaosStatus != "unknown" {
chaosEnabled, err := strconv.ParseBool(chaosStatus) chaosEnabled, err := strconv.ParseBool(chaosStatus)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if chaosEnabled && appStat.ChaosVersion != "unknown" { if chaosEnabled && appStat.ChaosVersion != "unknown" {
chaosStatus = appStat.ChaosVersion chaosStatus = appStat.ChaosVersion
} }
} }
tableRow = append(tableRow, []string{appStat.Status, chaosStatus, appStat.Version, appStat.Upgrade, appStat.AutoUpdate}...)
row = append(row, []string{
appStat.Status,
chaosStatus,
appStat.Version,
appStat.Upgrade,
appStat.AutoUpdate}...,
)
} }
table.Append(tableRow)
rows = append(rows, row)
} }
table.Rows(rows...) if table.NumLines() > 0 {
table.Render()
if len(rows) > 0 { if status {
if err := formatter.PrintTable(table); err != nil { fmt.Println(fmt.Sprintf(
log.Fatal(err) "server: %s | total apps: %v | versioned: %v | unversioned: %v | latest: %v | upgrade: %v",
app.Server,
serverStat.AppCount,
serverStat.VersionCount,
serverStat.UnversionedCount,
serverStat.LatestCount,
serverStat.UpgradeCount,
))
} else {
fmt.Println(fmt.Sprintf("server: %s | total apps: %v", app.Server, serverStat.AppCount))
} }
}
if len(allStats) > 1 && len(rows) > 0 { if len(allStats) > 1 && table.NumLines() > 0 {
fmt.Println() // newline separator for multiple servers fmt.Println() // newline separator for multiple servers
}
} }
alreadySeen[app.Server] = true alreadySeen[app.Server] = true
} }
if len(allStats) > 1 {
fmt.Println(fmt.Sprintf("total servers: %v | total apps: %v ", totalServersCount, totalAppsCount))
}
return nil
}, },
} }
var (
status bool
recipeFilter string
listAppServer string
)
func init() {
AppListCommand.Flags().BoolVarP(
&status,
"status",
"S",
false,
"show app deployment status",
)
AppListCommand.Flags().StringVarP(
&recipeFilter,
"recipe",
"r",
"",
"show apps of a specific recipe",
)
AppListCommand.RegisterFlagCompletionFunc(
"recipe",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
)
AppListCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
AppListCommand.Flags().StringVarP(
&listAppServer,
"server",
"s",
"",
"show apps of a specific server",
)
AppListCommand.RegisterFlagCompletionFunc(
"server",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
)
}

View File

@ -2,7 +2,6 @@ package app
import ( import (
"context" "context"
"fmt"
"io" "io"
"os" "os"
"slices" "slices"
@ -10,79 +9,72 @@ import (
"time" "time"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container" containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppLogsCommand = &cobra.Command{ var appLogsCommand = cli.Command{
Use: "logs <domain> [service] [flags]", Name: "logs",
Aliases: []string{"l"}, Aliases: []string{"l"},
Short: "Tail app logs", ArgsUsage: "<domain> [<service>]",
Args: cobra.RangeArgs(1, 2), Usage: "Tail app logs",
ValidArgsFunction: func( Flags: []cli.Flag{
cmd *cobra.Command, internal.StdErrOnlyFlag,
args []string, internal.SinceLogsFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.DebugFlag,
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.ServiceNameComplete(app.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
if err := app.Recipe.EnsureExists(); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName) isDeployed, _, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !deployMeta.IsDeployed { if !isDeployed {
log.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
var serviceNames []string serviceName := c.Args().Get(1)
if len(args) == 2 { serviceNames := []string{}
serviceNames = []string{args[1]} if serviceName != "" {
serviceNames = []string{serviceName}
}
err = tailLogs(cl, app, serviceNames)
if err != nil {
logrus.Fatal(err)
} }
if err = tailLogs(cl, app, serviceNames); err != nil { return nil
log.Fatal(err)
}
}, },
} }
// tailLogs prints logs for the given app with optional service names to be // tailLogs prints logs for the given app with optional service names to be
// filtered on. It also checks if the latest task is not runnning and then // filtered on. It also checks if the latest task is not runnning and then
// prints the past tasks. // prints the past tasks.
func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) error { func tailLogs(cl *dockerClient.Client, app config.App, serviceNames []string) error {
f, err := app.Filters(true, false, serviceNames...) f, err := app.Filters(true, false, serviceNames...)
if err != nil { if err != nil {
return err return err
@ -110,7 +102,7 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
lastTask := tasks[0].Status lastTask := tasks[0].Status
if lastTask.State != swarm.TaskStateRunning { if lastTask.State != swarm.TaskStateRunning {
for _, task := range tasks { for _, task := range tasks {
log.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err) logrus.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err)
} }
} }
} }
@ -121,8 +113,8 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
go func(serviceID string) { go func(serviceID string) {
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{ logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
ShowStderr: true, ShowStderr: true,
ShowStdout: !stdErr, ShowStdout: !internal.StdErrOnly,
Since: sinceLogs, Since: internal.SinceLogs,
Until: "", Until: "",
Timestamps: true, Timestamps: true,
Follow: true, Follow: true,
@ -130,13 +122,13 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
Details: false, Details: false,
}) })
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
defer logs.Close() defer logs.Close()
_, err = io.Copy(os.Stdout, logs) _, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
log.Fatal(err) logrus.Fatal(err)
} }
}(service.ID) }(service.ID)
} }
@ -146,26 +138,3 @@ func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) er
return nil return nil
} }
var (
stdErr bool
sinceLogs string
)
func init() {
AppLogsCommand.Flags().BoolVarP(
&stdErr,
"stderr",
"s",
false,
"only tail stderr",
)
AppLogsCommand.Flags().StringVarP(
&sinceLogs,
"since",
"S",
"",
"tail logs since YYYY-MM-DDTHH:MM:SSZ",
)
}

View File

@ -2,36 +2,32 @@ package app
import ( import (
"fmt" "fmt"
"path"
"coopcloud.tech/abra/cli/internal" "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/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/jsontable"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss/table"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var appNewDescription = `Creates a new app from a default recipe. var appNewDescription = `
Take a recipe and uses it to create a new app. This new app configuration is
This new app configuration is stored in your $ABRA_DIR directory under the stored in your ~/.abra directory under the appropriate server.
appropriate server.
This command does not deploy your app for you. You will need to run "abra app This command does not deploy your app for you. You will need to run "abra app
deploy <domain>" to do so. deploy <domain>" to do so.
You can see what recipes are available (i.e. values for the [recipe] argument) You can see what recipes are available (i.e. values for the <recipe> argument)
by running "abra recipe ls". by running "abra recipe ls".
Recipe commit hashes are supported values for "[version]".
Passing the "--secrets/-S" flag will automatically generate secrets for your Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These app and store them encrypted at rest on the chosen target server. These
generated secrets are only visible at generation time, so please take care to generated secrets are only visible at generation time, so please take care to
@ -39,215 +35,165 @@ store them somewhere safe.
You can use the "--pass/-P" to store these generated passwords locally in a You can use the "--pass/-P" to store these generated passwords locally in a
pass store (see passwordstore.org for more). The pass command must be available pass store (see passwordstore.org for more). The pass command must be available
on your $PATH.` on your $PATH.
`
var AppNewCommand = &cobra.Command{ var appNewCommand = cli.Command{
Use: "new [recipe] [version] [flags]", Name: "new",
Aliases: []string{"n"}, Aliases: []string{"n"},
Short: "Create a new app", Usage: "Create a new app",
Long: appNewDescription, Description: appNewDescription,
Args: cobra.RangeArgs(0, 2), Flags: []cli.Flag{
ValidArgsFunction: func( internal.DebugFlag,
cmd *cobra.Command, internal.NoInputFlag,
args []string, internal.NewAppServerFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.DomainFlag,
switch l := len(args); l { internal.PassFlag,
internal.SecretsFlag,
internal.OfflineFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "[<recipe>] [<version>]",
BashComplete: func(ctx *cli.Context) {
args := ctx.Args()
switch len(args) {
case 0: case 0:
return autocomplete.RecipeNameComplete() autocomplete.RecipeNameComplete(ctx)
case 1: case 1:
recipe := internal.ValidateRecipe(args, cmd.Name()) autocomplete.RecipeVersionComplete(ctx.Args().Get(0))
return autocomplete.RecipeVersionComplete(recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
} }
}, },
Run: func(cmd *cobra.Command, args []string) { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(args, cmd.Name()) recipe := internal.ValidateRecipe(c)
if len(args) == 2 && internal.Chaos {
log.Fatal("cannot use [version] and --chaos together")
}
var recipeVersion string
if len(args) == 2 {
recipeVersion = args[1]
}
chaosVersion := config.CHAOS_DEFAULT
if internal.Chaos {
var err error
chaosVersion, err = recipe.ChaosVersion()
if err != nil {
log.Fatal(err)
}
// NOTE(d1): rely on tags as there is no recipe.EnvVersion yet because
// the app has not been fully created. we rely on the local git state of
// the repository
tags, err := recipe.Tags()
if err != nil {
log.Fatal(err)
}
internal.SortVersionsDesc(tags)
if len(tags) == 0 {
// NOTE(d1): this is a new recipe with no released versions
recipeVersion = config.UNKNOWN_DEFAULT
} else {
recipeVersion = tags[len(tags)-1]
}
if err := recipe.IsDirty(); err != nil {
log.Fatal(err)
}
if !internal.Offline && !recipe.Dirty {
if err := recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
}
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(); err != nil { if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
if c.Args().Get(1) == "" {
var version string
var recipeVersions recipePkg.RecipeVersions recipeVersions, err := recipePkg.GetRecipeVersions(recipe.Name, internal.Offline)
if recipeVersion == "" {
var err error
recipeVersions, _, err = recipe.GetRecipeVersions()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
}
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
recipeVersion = tag
} }
if _, err := recipe.EnsureVersion(recipeVersion); err != nil { // NOTE(d1): determine whether recipe versions exist or not and check
log.Fatal(err) // out the latest version or current HEAD
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
version = tag
}
if err := recipePkg.EnsureVersion(recipe.Name, version); err != nil {
logrus.Fatal(err)
}
} else {
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
} }
} else { } else {
if err := recipe.EnsureLatest(); err != nil { if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
} }
if err := ensureServerFlag(); err != nil { if err := ensureServerFlag(); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := ensureDomainFlag(recipe, newAppServer); err != nil { if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
sanitisedAppName := appPkg.SanitiseAppName(appDomain) sanitisedAppName := config.SanitiseAppName(internal.Domain)
log.Debugf("%s sanitised as %s for new app", appDomain, sanitisedAppName) logrus.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
if err := appPkg.TemplateAppEnvSample( if err := config.TemplateAppEnvSample(
recipe, recipe.Name,
appDomain, internal.Domain,
newAppServer, internal.NewAppServer,
appDomain, internal.Domain,
); err != nil { ); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
var appSecrets AppSecrets var secrets AppSecrets
var secretsTable *table.Table var secretTable *jsontable.JSONTable
if generateSecrets { if internal.Secrets {
sampleEnv, err := recipe.SampleEnv() sampleEnv, err := recipe.SampleEnv()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
composeFiles, err := recipe.GetComposeFiles(sampleEnv) composeFiles, err := config.GetComposeFiles(recipe.Name, sampleEnv)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secretsConfig, err := secret.ReadSecretsConfig( envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
recipe.SampleEnvPath, secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, config.StackName(internal.Domain))
composeFiles,
appPkg.StackName(appDomain),
)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil { if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
cl, err := client.New(newAppServer) cl, err := client.New(internal.NewAppServer)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName) secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secretsTable, err = formatter.CreateTable() secretCols := []string{"Name", "Value"}
if err != nil { secretTable = formatter.CreateTable(secretCols)
log.Fatal(err) for name, val := range secrets {
} secretTable.Append([]string{name, val})
headers := []string{"NAME", "VALUE"}
secretsTable.Headers(headers...)
for name, val := range appSecrets {
secretsTable.Row(name, val)
} }
} }
if newAppServer == "default" { if internal.NewAppServer == "default" {
newAppServer = "local" internal.NewAppServer = "local"
} }
log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion) tableCol := []string{"server", "recipe", "domain"}
table := formatter.CreateTable(tableCol)
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
if len(appSecrets) > 0 { fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name))
rows := [][]string{} fmt.Println("")
for k, v := range appSecrets { table.Render()
rows = append(rows, []string{k, v}) fmt.Println("")
} fmt.Println("You can configure this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain))
fmt.Println("")
fmt.Println("You can deploy this app by running the following:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
overview := formatter.CreateOverview("SECRETS OVERVIEW", rows) if len(secrets) > 0 {
fmt.Println("")
fmt.Println(overview) fmt.Println("Here are your generated secrets:")
fmt.Println("")
log.Warnf( secretTable.Render()
"secrets are %s shown again, please save them %s", logrus.Warn("generated secrets are not shown again, please take note of them NOW")
formatter.BoldUnderlineStyle.Render("NOT"),
formatter.BoldUnderlineStyle.Render("NOW"),
)
} }
app, err := app.Get(appDomain) return nil
if err != nil {
log.Fatal(err)
}
if err := app.Recipe.IsDirty(); err != nil {
log.Fatal(err)
}
toWriteVersion := recipeVersion
if internal.Chaos || app.Recipe.Dirty {
toWriteVersion = chaosVersion
}
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
}
}, },
} }
@ -258,23 +204,23 @@ type AppSecrets map[string]string
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) { func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
// NOTE(d1): trim to match app.StackName() implementation // NOTE(d1): trim to match app.StackName() implementation
if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH { if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH {
log.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]) logrus.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH])
sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH] sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]
} }
secrets, err := secret.GenerateSecrets(cl, secretsConfig, newAppServer) secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if saveInPass { if internal.Pass {
for secretName := range secrets { for secretName := range secrets {
secretValue := secrets[secretName] secretValue := secrets[secretName]
if err := secret.PassInsertSecret( if err := secret.PassInsertSecret(
secretValue, secretValue,
secretName, secretName,
appDomain, internal.Domain,
newAppServer, internal.NewAppServer,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -286,17 +232,17 @@ func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secr
// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/ // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag(recipe recipePkg.Recipe, server string) error { func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
if appDomain == "" && !internal.NoInput { if internal.Domain == "" && !internal.NoInput {
prompt := &survey.Input{ prompt := &survey.Input{
Message: "Specify app domain", Message: "Specify app domain",
Default: fmt.Sprintf("%s.%s", recipe.Name, server), Default: fmt.Sprintf("%s.%s", recipe.Name, server),
} }
if err := survey.AskOne(prompt, &appDomain); err != nil { if err := survey.AskOne(prompt, &internal.Domain); err != nil {
return err return err
} }
} }
if appDomain == "" { if internal.Domain == "" {
return fmt.Errorf("no domain provided") return fmt.Errorf("no domain provided")
} }
@ -306,15 +252,15 @@ func ensureDomainFlag(recipe recipePkg.Recipe, server string) error {
// promptForSecrets asks if we should generate secrets for a new app. // promptForSecrets asks if we should generate secrets for a new app.
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error { func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
if len(secretsConfig) == 0 { if len(secretsConfig) == 0 {
log.Debugf("%s has no secrets to generate, skipping...", recipeName) logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
return nil return nil
} }
if !generateSecrets && !internal.NoInput { if !internal.Secrets && !internal.NoInput {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: "Generate app secrets?", Message: "Generate app secrets?",
} }
if err := survey.AskOne(prompt, &generateSecrets); err != nil { if err := survey.AskOne(prompt, &internal.Secrets); err != nil {
return err return err
} }
} }
@ -329,76 +275,19 @@ func ensureServerFlag() error {
return err return err
} }
if newAppServer == "" && !internal.NoInput { if internal.NewAppServer == "" && !internal.NoInput {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Select app server:", Message: "Select app server:",
Options: servers, Options: servers,
} }
if err := survey.AskOne(prompt, &newAppServer); err != nil { if err := survey.AskOne(prompt, &internal.NewAppServer); err != nil {
return err return err
} }
} }
if newAppServer == "" { if internal.NewAppServer == "" {
return fmt.Errorf("no server provided") return fmt.Errorf("no server provided")
} }
return nil return nil
} }
var (
newAppServer string
appDomain string
saveInPass bool
generateSecrets bool
)
func init() {
AppNewCommand.Flags().StringVarP(
&newAppServer,
"server",
"s",
"",
"specify server for new app",
)
AppNewCommand.RegisterFlagCompletionFunc(
"server",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
)
AppNewCommand.Flags().StringVarP(
&appDomain,
"domain",
"D",
"",
"domain name for app",
)
AppNewCommand.Flags().BoolVarP(
&saveInPass,
"pass",
"p",
false,
"store secrets in a local pass store",
)
AppNewCommand.Flags().BoolVarP(
&generateSecrets,
"secrets",
"S",
false,
"automatically generate secrets",
)
AppNewCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -4,78 +4,84 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings" "time"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
abraService "coopcloud.tech/abra/pkg/service" abraService "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/buger/goterm"
dockerFormatter "github.com/docker/cli/cli/command/formatter" dockerFormatter "github.com/docker/cli/cli/command/formatter"
containerTypes "github.com/docker/docker/api/types/container" containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppPsCommand = &cobra.Command{ var appPsCommand = cli.Command{
Use: "ps <domain> [flags]", Name: "ps",
Aliases: []string{"p"}, Aliases: []string{"p"},
Short: "Check app deployment status", Usage: "Check app status",
Args: cobra.ExactArgs(1), ArgsUsage: "<domain>",
ValidArgsFunction: func( Description: "Show a more detailed status output of a specific deployed app",
cmd *cobra.Command, Flags: []cli.Flag{
args []string, internal.MachineReadableFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.WatchFlag,
return autocomplete.AppNameComplete() internal.DebugFlag,
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { app := internal.ValidateApp(c)
log.Fatal(err)
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !deployMeta.IsDeployed { if !isDeployed {
log.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
chaosVersion := config.CHAOS_DEFAULT statuses, err := config.GetAppStatuses([]config.App{app}, true)
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok { if statusMeta, ok := statuses[app.StackName()]; ok {
if isChaos, exists := statusMeta["chaos"]; exists && isChaos == "true" { if _, exists := statusMeta["chaos"]; !exists {
if cVersion, exists := statusMeta["chaosVersion"]; exists { if err := recipe.EnsureVersion(app.Recipe, deployedVersion); err != nil {
chaosVersion = cVersion logrus.Fatal(err)
if strings.HasSuffix(chaosVersion, config.DIRTY_DEFAULT) {
chaosVersion = formatter.BoldDirtyDefault(chaosVersion)
}
} }
} }
} }
showPSOutput(app, cl, deployMeta.Version, chaosVersion) if !internal.Watch {
showPSOutput(c, app, cl)
return nil
}
goterm.Clear()
for {
goterm.MoveCursor(1, 1)
showPSOutput(c, app, cl)
goterm.Flush()
time.Sleep(2 * time.Second)
}
}, },
} }
// showPSOutput renders ps output. // showPSOutput renders ps output.
func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chaosVersion string) { func showPSOutput(c *cli.Context, app config.App, cl *dockerClient.Client) {
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
return return
} }
@ -85,13 +91,13 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
Prune: false, Prune: false,
ResolveImage: stack.ResolveImageAlways, ResolveImage: stack.ResolveImageAlways,
} }
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env) compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
return return
} }
var rows [][]string var tablerows [][]string
allContainerStats := make(map[string]map[string]string) allContainerStats := make(map[string]map[string]string)
for _, service := range compose.Services { for _, service := range compose.Services {
filters := filters.NewArgs() filters := filters.NewArgs()
@ -99,106 +105,51 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters}) containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
return return
} }
var containerStats map[string]string var containerStats map[string]string
if len(containers) == 0 { if len(containers) == 0 {
containerStats = map[string]string{ containerStats = map[string]string{
"version": deployedVersion, "service name": service.Name,
"chaos": chaosVersion, "image": "unknown",
"service": service.Name, "created": "unknown",
"image": "unknown", "status": "unknown",
"created": "unknown", "state": "unknown",
"status": "unknown", "ports": "unknown",
"state": "unknown",
"ports": "unknown",
} }
} else { } else {
container := containers[0] container := containers[0]
containerStats = map[string]string{ containerStats = map[string]string{
"version": deployedVersion, "service name": abraService.ContainerToServiceName(container.Names, app.StackName()),
"chaos": chaosVersion, "image": formatter.RemoveSha(container.Image),
"service": abraService.ContainerToServiceName(container.Names, app.StackName()), "created": formatter.HumanDuration(container.Created),
"image": formatter.RemoveSha(container.Image), "status": container.Status,
"created": formatter.HumanDuration(container.Created), "state": container.State,
"status": container.Status, "ports": dockerFormatter.DisplayablePorts(container.Ports),
"state": container.State,
"ports": dockerFormatter.DisplayablePorts(container.Ports),
} }
} }
allContainerStats[containerStats["service name"]] = containerStats
allContainerStats[containerStats["service"]] = containerStats var tablerow []string = []string{containerStats["service name"], containerStats["image"], containerStats["created"], containerStats["status"], containerStats["state"], containerStats["ports"]}
tablerows = append(tablerows, tablerow)
// NOTE(d1): don't clobber these variables for --machine output
dVersion := deployedVersion
cVersion := chaosVersion
if containerStats["service"] != "app" {
// NOTE(d1): don't repeat info which only relevant for the "app" service
dVersion = ""
cVersion = ""
}
row := []string{
containerStats["service"],
containerStats["image"],
dVersion,
cVersion,
containerStats["status"],
}
rows = append(rows, row)
} }
if internal.MachineReadable { if internal.MachineReadable {
rendered, err := json.Marshal(allContainerStats) jsonstring, err := json.Marshal(allContainerStats)
if err != nil { if err != nil {
log.Fatal("unable to convert to JSON: %s", err) logrus.Fatal(err)
} else {
fmt.Println(string(jsonstring))
} }
fmt.Println(string(rendered))
return return
} } else {
tableCol := []string{"service name", "image", "created", "status", "state", "ports"}
table, err := formatter.CreateTable() table := formatter.CreateTable(tableCol)
if err != nil { for _, row := range tablerows {
log.Fatal(err) table.Append(row)
} }
table.Render()
headers := []string{
"SERVICE",
"IMAGE",
"VERSION",
"CHAOS",
"STATUS",
}
table.
Headers(headers...).
Rows(rows...)
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
} }
} }
func init() {
AppPsCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
AppPsCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -3,23 +3,26 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"os" "os"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppRemoveCommand = &cobra.Command{ var appRemoveCommand = cli.Command{
Use: "remove <domain> [flags]", Name: "remove",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Short: "Remove all app data, locally and remotely", ArgsUsage: "<domain>",
Long: `Remove everything related to an app which is already undeployed. Usage: "Remove all app data, locally and remotely",
Description: `
This command removes everything related to an app which is already undeployed.
By default, it will prompt for confirmation before proceeding. All secrets, By default, it will prompt for confirmation before proceeding. All secrets,
volumes and the local app env file will be deleted. volumes and the local app env file will be deleted.
@ -34,53 +37,52 @@ Please note, if you delete the local app env file without removing volumes and
secrets first, Abra will *not* be able to help you remove them afterwards. secrets first, Abra will *not* be able to help you remove them afterwards.
To delete everything without prompt, use the "--force/-f" or the "--no-input/n" To delete everything without prompt, use the "--force/-f" or the "--no-input/n"
flag.`, flag.
Example: " abra app remove 1312.net", `,
Args: cobra.ExactArgs(1), Flags: []cli.Flag{
ValidArgsFunction: func( internal.ForceFlag,
cmd *cobra.Command, internal.DebugFlag,
args []string, internal.NoInputFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.OfflineFlag,
return autocomplete.AppNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { BashComplete: autocomplete.AppNameComplete,
app := internal.ValidateApp(args) Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if !internal.Force && !internal.NoInput { if !internal.Force && !internal.NoInput {
log.Warnf("ALERTA ALERTA: deleting %s data and config (local/remote)", app.Name)
response := false response := false
prompt := &survey.Confirm{Message: "are you sure?"} msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?"
prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !response { if !response {
log.Fatal("aborting as requested") logrus.Fatal("aborting as requested")
} }
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if deployMeta.IsDeployed { if isDeployed {
log.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name) logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
} }
fs, err := app.Filters(false, false) fs, err := app.Filters(false, false)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs}) secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secrets := make(map[string]string) secrets := make(map[string]string)
@ -95,22 +97,22 @@ flag.`,
for _, name := range secretNames { for _, name := range secretNames {
err := cl.SecretRemove(context.Background(), secrets[name]) err := cl.SecretRemove(context.Background(), secrets[name])
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
log.Info(fmt.Sprintf("secret: %s removed", name)) logrus.Info(fmt.Sprintf("secret: %s removed", name))
} }
} else { } else {
log.Info("no secrets to remove") logrus.Info("no secrets to remove")
} }
fs, err = app.Filters(false, true) fs, err = app.Filters(false, true)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, fs) volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, fs)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
volumeNames := client.GetVolumeNames(volumeList) volumeNames := client.GetVolumeNames(volumeList)
@ -120,25 +122,17 @@ flag.`,
log.Fatalf("removing volumes failed: %s", err) log.Fatalf("removing volumes failed: %s", err)
} }
log.Infof("%d volumes removed successfully", len(volumeNames)) logrus.Infof("%d volumes removed successfully", len(volumeNames))
} else { } else {
log.Info("no volumes to remove") logrus.Info("no volumes to remove")
} }
if err = os.Remove(app.Path); err != nil { if err = os.Remove(app.Path); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
log.Info(fmt.Sprintf("file: %s removed", app.Path)) logrus.Info(fmt.Sprintf("file: %s removed", app.Path))
return nil
}, },
} }
func init() {
AppRemoveCommand.Flags().BoolVarP(
&internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
}

View File

@ -2,132 +2,79 @@ package app
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
upstream "coopcloud.tech/abra/pkg/upstream/service" upstream "coopcloud.tech/abra/pkg/upstream/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppRestartCommand = &cobra.Command{ var appRestartCommand = cli.Command{
Use: "restart <domain> [[service] | --all-services] [flags]", Name: "restart",
Aliases: []string{"re"}, Aliases: []string{"re"},
Short: "Restart an app", Usage: "Restart an app",
Long: `This command restarts services within a deployed app. ArgsUsage: "<domain>",
Flags: []cli.Flag{
Run "abra app ps <domain>" to see a list of service names. internal.DebugFlag,
internal.OfflineFlag,
Pass "--all-services/-a" to restart all services.`,
Example: ` # restart a single app service
abra app restart 1312.net app
# restart all app services
abra app restart 1312.net -a`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
if !allServices {
return autocomplete.ServiceNameComplete(args[0])
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) Description: `This command restarts a service within a deployed app.`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { serviceNameShort := c.Args().Get(1)
log.Fatal(err) if serviceNameShort == "" {
} err := errors.New("missing service?")
internal.ShowSubcommandHelpAndError(c, err)
var serviceName string
if len(args) == 2 {
serviceName = args[1]
}
if serviceName == "" && !allServices {
log.Fatal("missing [service]")
}
if serviceName != "" && allServices {
log.Fatal("cannot use [service] and --all-services/-a together")
}
var serviceNames []string
if allServices {
var err error
serviceNames, err = appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
} else {
serviceNames = append(serviceNames, serviceName)
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !deployMeta.IsDeployed { if !isDeployed {
log.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
for _, serviceName := range serviceNames { serviceName := fmt.Sprintf("%s_%s", app.StackName(), serviceNameShort)
stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
log.Debugf("attempting to scale %s to 0", stackServiceName) logrus.Debugf("attempting to scale %s to 0 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 0); err != nil {
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil { logrus.Fatal(err)
log.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil {
log.Fatal(err)
}
log.Debugf("%s has been scaled to 0", stackServiceName)
log.Debugf("attempting to scale %s to 1", stackServiceName)
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 1); err != nil {
log.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil {
log.Fatal(err)
}
log.Debugf("%s has been scaled to 1", stackServiceName)
log.Infof("%s service successfully restarted", serviceName)
} }
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("%s has been scaled to 0 (restart logic)", serviceName)
logrus.Debugf("attempting to scale %s to 1 (restart logic)", serviceName)
if err := upstream.RunServiceScale(context.Background(), cl, serviceName, 1); err != nil {
logrus.Fatal(err)
}
if err := stack.WaitOnService(context.Background(), cl, serviceName, app.Name); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("%s has been scaled to 1 (restart logic)", serviceName)
logrus.Infof("%s service successfully restarted", serviceNameShort)
return nil
}, },
} }
var allServices bool
func init() {
AppRestartCommand.Flags().BoolVarP(
&allServices,
"all-services",
"a",
false,
"restart all services",
)
}

View File

@ -2,134 +2,81 @@ package app
import ( import (
"fmt" "fmt"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppRestoreCommand = &cobra.Command{ var targetPath string
Use: "restore <domain> [flags]", var targetPathFlag = &cli.StringFlag{
Aliases: []string{"rs"}, Name: "target, t",
Short: "Restore a snapshot", Usage: "Target path",
Long: `Snapshots are restored while apps are deployed. Destination: &targetPath,
}
Some restore scenarios may require service / app restarts.`, var appRestoreCommand = cli.Command{
Args: cobra.ExactArgs(1), Name: "restore",
ValidArgsFunction: func( Aliases: []string{"rs"},
cmd *cobra.Command, Usage: "Restore an app backup",
args []string, ArgsUsage: "<domain> <service>",
toComplete string) ([]string, cobra.ShellCompDirective) { Flags: []cli.Flag{
return autocomplete.AppNameComplete() internal.DebugFlag,
internal.OfflineFlag,
targetPathFlag,
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
targetContainer, err := internal.RetrieveBackupBotContainer(cl) targetContainer, err := internal.RetrieveBackupBotContainer(cl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
execEnv := []string{
fmt.Sprintf("SERVICE=%s", app.Domain),
"MACHINE_LOGS=true",
} }
execEnv := []string{fmt.Sprintf("SERVICE=%s", app.Domain)}
if snapshot != "" { if snapshot != "" {
log.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot) logrus.Debugf("including SNAPSHOT=%s in backupbot exec invocation", snapshot)
execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot)) execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
} }
if targetPath != "" { if targetPath != "" {
log.Debugf("including TARGET=%s in backupbot exec invocation", targetPath) logrus.Debugf("including TARGET=%s in backupbot exec invocation", targetPath)
execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath)) execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
} }
if internal.NoInput { if err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
log.Debugf("including NONINTERACTIVE=%v in backupbot exec invocation", internal.NoInput) logrus.Fatal(err)
execEnv = append(execEnv, fmt.Sprintf("NONINTERACTIVE=%v", internal.NoInput))
} }
if len(volumes) > 0 { return nil
allVolumes := strings.Join(volumes, ",")
log.Debugf("including VOLUMES=%s in backupbot exec invocation", allVolumes)
execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%s", allVolumes))
}
if len(services) > 0 {
allServices := strings.Join(services, ",")
log.Debugf("including CONTAINER=%s in backupbot exec invocation", allServices)
execEnv = append(execEnv, fmt.Sprintf("CONTAINER=%s", allServices))
}
if hooks {
log.Debugf("including NO_COMMANDS=%v in backupbot exec invocation", false)
execEnv = append(execEnv, fmt.Sprintf("NO_COMMANDS=%v", false))
}
if _, err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
log.Fatal(err)
}
}, },
} }
var (
targetPath string
hooks bool
services []string
volumes []string
)
func init() {
AppRestoreCommand.Flags().StringVarP(
&targetPath,
"target",
"t",
"/",
"target path",
)
AppRestoreCommand.Flags().StringArrayVarP(
&services,
"services",
"s",
[]string{},
"restore specific services",
)
AppRestoreCommand.Flags().StringArrayVarP(
&volumes,
"volumes",
"v",
[]string{},
"restore specific volumes",
)
AppRestoreCommand.Flags().BoolVarP(
&hooks,
"hooks",
"H",
false,
"enable pre/post-hook command execution",
)
AppRestoreCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
}

View File

@ -1,325 +1,240 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppRollbackCommand = &cobra.Command{ var appRollbackCommand = cli.Command{
Use: "rollback <domain> [version] [flags]", Name: "rollback",
Aliases: []string{"rl"}, Aliases: []string{"rl"},
Short: "Roll an app back to a previous version", Usage: "Roll an app back to a previous version",
Long: `This command rolls an app back to a previous version. ArgsUsage: "<domain> [<version>]",
Flags: []cli.Flag{
Unlike "abra app deploy", chaos operations are not supported here. Only recipe internal.DebugFlag,
versions are supported values for "[version]". internal.NoInputFlag,
internal.ForceFlag,
It is possible to "--force/-f" an downgrade if you want to re-deploy a specific internal.ChaosFlag,
version. internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
Only the deployed version is consulted when trying to determine what downgrades internal.OfflineFlag,
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.
A downgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`,
Example: ` # standard rollback
abra app rollback 1312.net
# rollback to specific version
abra app rollback 1312.net 2.0.0+1.2.3`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveError
}
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
var ( Description: `
downgradeWarnMessages []string This command rolls an app back to a previous version if one exists.
chosenDowngrade string
availableDowngrades []string
)
app := internal.ValidateApp(args) You may pass "--force/-f" to downgrade to the same version again. This can be
useful if the container runtime has gotten into a weird state.
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { This action could be destructive, please ensure you have a copy of your app
log.Fatal(err) data beforehand.
Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new
recipes.
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
specificVersion := c.Args().Get(1)
if specificVersion != "" && internal.Chaos {
logrus.Fatal("cannot use <version> and --chaos together")
}
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
r, err := recipe.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployMeta, err := ensureDeployed(cl, app) logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := lint.LintForErrors(app.Recipe); err != nil { if !isDeployed {
log.Fatal(err) logrus.Fatalf("%s is not deployed?", app.Name)
} }
versions, err := app.Recipe.Tags() catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
// NOTE(d1): we've no idea what the live deployment version is, so every versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
// possible downgrade can be shown. it's up to the user to make the choice if err != nil {
if deployMeta.Version == config.UNKNOWN_DEFAULT { logrus.Fatal(err)
availableDowngrades = versions
} }
if len(args) == 2 && args[1] != "" { if len(versions) == 0 && !internal.Chaos {
chosenDowngrade = args[1] logrus.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline)
if err := validateDowngradeVersionArg(chosenDowngrade, app, deployMeta); err != nil {
log.Fatal(err)
}
availableDowngrades = append(availableDowngrades, chosenDowngrade)
}
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenDowngrade == "" {
downgradeAvailable, err := ensureDowngradesAvailable(versions, &availableDowngrades, deployMeta)
if err != nil { if err != nil {
log.Fatal(err) logrus.Warn(err)
} }
for _, recipeVersion := range recipeVersions {
if !downgradeAvailable { for version := range recipeVersion {
log.Info("no available downgrades") versions = append(versions, version)
return }
} }
} }
if internal.Force || internal.NoInput || chosenDowngrade != "" { var availableDowngrades []string
if len(availableDowngrades) > 0 { if deployedVersion == "unknown" {
availableDowngrades = versions
logrus.Warnf("failed to determine deployed version of %s", app.Name)
}
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
logrus.Fatal(err)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
logrus.Fatal(err)
}
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) {
logrus.Fatalf("%s is not a downgrade for %s?", deployedVersion, specificVersion)
}
availableDowngrades = append(availableDowngrades, specificVersion)
}
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
for _, version := range versions {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
logrus.Fatal(err)
}
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
if parsedVersion.IsLessThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableDowngrades = append(availableDowngrades, version)
}
}
if len(availableDowngrades) == 0 && !internal.Force {
logrus.Info("no available downgrades, you're on oldest ✌️")
return nil
}
}
var chosenDowngrade string
if len(availableDowngrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput || specificVersion != "" {
chosenDowngrade = availableDowngrades[len(availableDowngrades)-1] chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
} logrus.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade)
} else { } else {
if err := chooseDowngrade(availableDowngrades, deployMeta, &chosenDowngrade); err != nil { prompt := &survey.Select{
log.Fatal(err) Message: fmt.Sprintf("Please select a downgrade (current version: %s):", deployedVersion),
Options: internal.ReverseStringList(availableDowngrades),
}
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {
return err
}
} }
} }
if internal.Force && if !internal.Chaos {
chosenDowngrade == "" && if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil {
deployMeta.Version != config.UNKNOWN_DEFAULT { logrus.Fatal(err)
chosenDowngrade = deployMeta.Version }
} }
if chosenDowngrade == "" { if internal.Chaos {
log.Fatal("unknown deployed version, unable to downgrade") logrus.Warn("chaos mode engaged")
var err error
chosenDowngrade, err = recipe.ChaosVersion(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
} }
log.Debugf("choosing %s as version to rollback", chosenDowngrade) abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
log.Fatal(err)
}
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for k, v := range abraShEnv { for k, v := range abraShEnv {
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
stackName := app.StackName()
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Composefiles: composeFiles, Composefiles: composeFiles,
Namespace: stackName, Namespace: stackName,
Prune: false, Prune: false,
ResolveImage: stack.ResolveImageAlways, ResolveImage: stack.ResolveImageAlways,
Detach: false,
} }
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
}
appPkg.SetUpdateLabel(compose, stackName, app.Env)
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
} }
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
config.SetUpdateLabel(compose, stackName, app.Env)
// NOTE(d1): no release notes implemeneted for rolling back // NOTE(d1): no release notes implemeneted for rolling back
if err := internal.NewVersionOverview( if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {
app, logrus.Fatal(err)
downgradeWarnMessages,
"rollback",
deployMeta.Version,
chaosVersion,
chosenDowngrade,
"",
); err != nil {
log.Fatal(err)
} }
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil { if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil { return 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)
if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = fmt.Sprintf(
"please select a downgrade (version: %s, chaos: %s):",
deployMeta.Version,
chaosVersion,
)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableDowngrades),
}
if err := survey.AskOne(prompt, chosenDowngrade); err != nil {
return err
}
return nil
}
// validateDownpgradeVersionArg validates the specific version.
func validateDowngradeVersionArg(
specificVersion string,
app app.App,
deployMeta stack.DeployMeta,
) error {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name)
}
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
}
if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return fmt.Errorf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion)
}
return nil
}
// ensureDowngradesAvailable ensures that there are available downgrades.
func ensureDowngradesAvailable(
versions []string,
availableDowngrades *[]string,
deployMeta stack.DeployMeta,
) (bool, error) {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return false, err
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return false, err
}
if parsedVersion.IsLessThan(parsedDeployedVersion) &&
!(parsedVersion.Equals(parsedDeployedVersion)) {
*availableDowngrades = append(*availableDowngrades, version)
}
}
if len(*availableDowngrades) == 0 && !internal.Force {
return false, nil
}
return true, nil
}
func init() {
AppRollbackCommand.Flags().BoolVarP(
&internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
AppRollbackCommand.Flags().BoolVarP(
&internal.NoDomainChecks,
"no-domain-checks",
"D",
false,
"disable public DNS checks",
)
AppRollbackCommand.Flags().BoolVarP(
&internal.DontWaitConverge, "no-converge-checks",
"c",
false,
"disable converge logic checks",
)
}

View File

@ -2,113 +2,99 @@ package app
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppRunCommand = &cobra.Command{ var user string
Use: "run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]", var userFlag = &cli.StringFlag{
Name: "user, u",
Value: "",
Destination: &user,
}
var noTTY bool
var noTTYFlag = &cli.BoolFlag{
Name: "no-tty, t",
Destination: &noTTY,
}
var appRunCommand = cli.Command{
Name: "run",
Aliases: []string{"r"}, Aliases: []string{"r"},
Short: "Run a command inside a service container", Flags: []cli.Flag{
Example: ` # run <cmd> with args/flags internal.DebugFlag,
abra app run 1312.net app -- ls -lha noTTYFlag,
userFlag,
# run <cmd> without args/flags
abra app run 1312.net app bash --user nobody
# run <cmd> with both kinds of args/flags
abra app run 1312.net app --user nobody -- ls -lha`,
Args: cobra.MinimumNArgs(3),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
return autocomplete.ServiceNameComplete(args[0])
case 2:
return autocomplete.CommandNameComplete(args[0])
default:
return nil, cobra.ShellCompDirectiveError
}
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) ArgsUsage: "<domain> <service> <args>...",
Usage: "Run a command in a service container",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if len(c.Args()) < 2 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <service> provided?"))
}
if len(c.Args()) < 3 {
internal.ShowSubcommandHelpAndError(c, errors.New("no <args> provided?"))
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
serviceName := args[1] serviceName := c.Args().Get(1)
stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName) stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", stackAndServiceName) filters.Add("name", stackAndServiceName)
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false) targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
userCmd := args[2:] cmd := c.Args()[2:]
execCreateOpts := types.ExecConfig{ execCreateOpts := types.ExecConfig{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,
Cmd: userCmd, Cmd: cmd,
Detach: false, Detach: false,
Tty: true, Tty: true,
} }
if runAsUser != "" { if user != "" {
execCreateOpts.User = runAsUser execCreateOpts.User = user
} }
if noTTY { if noTTY {
execCreateOpts.Tty = false execCreateOpts.Tty = false
} }
// FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli() dcli, err := command.NewDockerCli()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil
}, },
} }
var (
noTTY bool
runAsUser string
)
func init() {
AppRunCommand.Flags().BoolVarP(&noTTY,
"no-tty",
"t",
false,
"do not request a TTY",
)
AppRunCommand.Flags().StringVarP(
&runAsUser,
"user",
"u",
"",
"run command as user",
)
}

View File

@ -2,77 +2,106 @@ package app
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppSecretGenerateCommand = &cobra.Command{ var (
Use: "generate <domain> [[secret] [version] | --all] [flags]", allSecrets bool
Aliases: []string{"g"}, allSecretsFlag = &cli.BoolFlag{
Short: "Generate secrets", Name: "all, a",
Args: cobra.RangeArgs(1, 3), Destination: &allSecrets,
ValidArgsFunction: func( Usage: "Generate all secrets",
cmd *cobra.Command, }
args []string, )
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l { var (
case 0: rmAllSecrets bool
return autocomplete.AppNameComplete() rmAllSecretsFlag = &cli.BoolFlag{
case 1: Name: "all, a",
app, err := appPkg.Get(args[0]) Destination: &rmAllSecrets,
if err != nil { Usage: "Remove all secrets",
errMsg := fmt.Sprintf("autocomplete failed: %s", err) }
return []string{errMsg}, cobra.ShellCompDirectiveError )
}
return autocomplete.SecretComplete(app.Recipe.Name) var appSecretGenerateCommand = cli.Command{
default: Name: "generate",
return nil, cobra.ShellCompDirectiveDefault Aliases: []string{"g"},
} Usage: "Generate secrets",
ArgsUsage: "<domain> <secret> <version>",
Flags: []cli.Flag{
internal.DebugFlag,
allSecretsFlag,
internal.PassFlag,
internal.MachineReadableFlag,
internal.OfflineFlag,
internal.ChaosFlag,
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if len(args) == 1 && !generateAllSecrets { if !internal.Chaos {
log.Fatal("missing arguments [secret]/[version] or '--all'") if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
} }
if len(args) > 1 && generateAllSecrets { if len(c.Args()) == 1 && !allSecrets {
log.Fatal("cannot use '[secret] [version]' and '--all' together") err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err)
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if c.Args().Get(1) != "" && allSecrets {
err := errors.New("cannot use '<secret> <version>' and '--all' together")
internal.ShowSubcommandHelpAndError(c, err)
}
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName()) secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !generateAllSecrets { if !allSecrets {
secretName := args[1] secretName := c.Args().Get(1)
secretVersion := args[2] secretVersion := c.Args().Get(2)
s, ok := secrets[secretName] s, ok := secrets[secretName]
if !ok { if !ok {
log.Fatalf("%s doesn't exist in the env config?", secretName) logrus.Fatalf("%s doesn't exist in the env config?", secretName)
} }
s.Version = secretVersion s.Version = secretVersion
secrets = map[string]secret.Secret{ secrets = map[string]secret.Secret{
@ -82,217 +111,195 @@ var AppSecretGenerateCommand = &cobra.Command{
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server) secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if storeInPass { if internal.Pass {
for name, data := range secretVals { for name, data := range secretVals {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil { if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
} }
if len(secretVals) == 0 { if len(secretVals) == 0 {
log.Warn("no secrets generated") logrus.Warn("no secrets generated")
os.Exit(1) os.Exit(1)
} }
headers := []string{"NAME", "VALUE"} tableCol := []string{"name", "value"}
table, err := formatter.CreateTable() table := formatter.CreateTable(tableCol)
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
var rows [][]string
for name, val := range secretVals { for name, val := range secretVals {
row := []string{name, val} table.Append([]string{name, val})
rows = append(rows, row)
table.Row(row...)
} }
if internal.MachineReadable { if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows) table.JSONRender()
if err != nil { } else {
log.Fatal("unable to render to JSON: %s", err) table.Render()
}
fmt.Println(out)
return
} }
logrus.Warn("generated secrets are not shown again, please take note of them NOW")
if err := formatter.PrintTable(table); err != nil { return nil
log.Fatal(err)
}
log.Warnf(
"generated secrets %s shown again, please take note of them %s",
formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"),
)
}, },
} }
var AppSecretInsertCommand = &cobra.Command{ var appSecretInsertCommand = cli.Command{
Use: "insert <domain> <secret> <version> <data> [flags]", Name: "insert",
Aliases: []string{"i"}, Aliases: []string{"i"},
Short: "Insert secret", Usage: "Insert secret",
Long: `This command inserts a secret into an app environment. Flags: []cli.Flag{
internal.DebugFlag,
internal.PassFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command inserts a secret into an app environment.
This can be useful when you want to manually generate secrets for an app This can be useful when you want to manually generate secrets for an app
environment. Typically, you can let Abra generate them for you on app creation environment. Typically, you can let Abra generate them for you on app creation
(see "abra app new --secrets/-S" for more).`, (see "abra app new --secrets" for more).
Args: cobra.MinimumNArgs(4),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.SecretComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveDefault
}
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { Example:
log.Fatal(err)
abra app secret insert myapp db_pass v1 mySecretPassword
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
name := args[1] name := c.Args().Get(1)
version := args[2] version := c.Args().Get(2)
data := args[3] data := c.Args().Get(3)
if insertFromFile {
raw, err := os.ReadFile(data)
if err != nil {
log.Fatalf("reading secret from file: %s", err)
}
data = string(raw)
}
if trimInput {
data = strings.TrimSpace(data)
}
secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version) secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil { if err := client.StoreSecret(cl, secretName, data, app.Server); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
log.Infof("%s successfully stored on server", secretName) logrus.Infof("%s successfully stored on server", secretName)
if storeInPass { if internal.Pass {
if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil { if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
return nil
}, },
} }
// secretRm removes a secret. // secretRm removes a secret.
func secretRm(cl *dockerClient.Client, app appPkg.App, secretName, parsed string) error { func secretRm(cl *dockerClient.Client, app config.App, secretName, parsed string) error {
if err := cl.SecretRemove(context.Background(), secretName); err != nil { if err := cl.SecretRemove(context.Background(), secretName); err != nil {
return err return err
} }
log.Infof("deleted %s successfully from server", secretName) logrus.Infof("deleted %s successfully from server", secretName)
if removeFromPass { if internal.PassRemove {
if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil {
return err return err
} }
log.Infof("deleted %s successfully from local pass store", secretName) logrus.Infof("deleted %s successfully from local pass store", secretName)
} }
return nil return nil
} }
var AppSecretRmCommand = &cobra.Command{ var appSecretRmCommand = cli.Command{
Use: "remove <domain> [[secret] | --all] [flags]", Name: "remove",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Short: "Remove a secret", Usage: "Remove a secret",
Args: cobra.RangeArgs(1, 2), Flags: []cli.Flag{
ValidArgsFunction: func( internal.DebugFlag,
cmd *cobra.Command, internal.NoInputFlag,
args []string, rmAllSecretsFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.PassRemoveFlag,
switch l := len(args); l { internal.OfflineFlag,
case 0: internal.ChaosFlag,
return autocomplete.AppNameComplete()
case 1:
if !rmAllSecrets {
app, err := appPkg.Get(args[0])
if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.SecretComplete(app.Recipe.Name)
}
return nil, cobra.ShellCompDirectiveDefault
default:
return nil, cobra.ShellCompDirectiveError
}
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) ArgsUsage: "<domain> [<secret-name>]",
BashComplete: autocomplete.AppNameComplete,
Description: `
This command removes app secrets.
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { Example:
log.Fatal(err)
abra app secret remove myapp db_pass
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil {
logrus.Fatal(err)
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName()) secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if len(args) == 2 && rmAllSecrets { if c.Args().Get(1) != "" && rmAllSecrets {
log.Fatal("cannot use [secret] and --all/-a together") internal.ShowSubcommandHelpAndError(c, errors.New("cannot use '<secret-name>' and '--all' together"))
} }
if len(args) != 2 && !rmAllSecrets { if c.Args().Get(1) == "" && !rmAllSecrets {
log.Fatal("no secret(s) specified?") internal.ShowSubcommandHelpAndError(c, errors.New("no secret(s) specified?"))
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
filters, err := app.Filters(false, false) filters, err := app.Filters(false, false)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters}) secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
remoteSecretNames := make(map[string]bool) remoteSecretNames := make(map[string]bool)
@ -300,230 +307,122 @@ var AppSecretRmCommand = &cobra.Command{
remoteSecretNames[cont.Spec.Annotations.Name] = true remoteSecretNames[cont.Spec.Annotations.Name] = true
} }
var secretToRm string
if len(args) == 2 {
secretToRm = args[1]
}
match := false match := false
secretToRm := c.Args().Get(1)
for secretName, val := range secrets { for secretName, val := range secrets {
secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version) secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
if _, ok := remoteSecretNames[secretRemoteName]; ok { if _, ok := remoteSecretNames[secretRemoteName]; ok {
if secretToRm != "" { if secretToRm != "" {
if secretName == secretToRm { if secretName == secretToRm {
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil { if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return return nil
} }
} else { } else {
match = true match = true
if err := secretRm(cl, app, secretRemoteName, secretName); err != nil { if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
} }
} }
if !match && secretToRm != "" { if !match && secretToRm != "" {
log.Fatalf("%s doesn't exist on server?", secretToRm) logrus.Fatalf("%s doesn't exist on server?", secretToRm)
} }
if !match { if !match {
log.Fatal("no secrets to remove?") logrus.Fatal("no secrets to remove?")
} }
return nil
}, },
} }
var AppSecretLsCommand = &cobra.Command{ var appSecretLsCommand = cli.Command{
Use: "list <domain>", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Short: "List all secrets", Flags: []cli.Flag{
Args: cobra.MinimumNArgs(1), internal.DebugFlag,
ValidArgsFunction: func( internal.OfflineFlag,
cmd *cobra.Command, internal.ChaosFlag,
args []string, internal.MachineReadableFlag,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) Usage: "List all secrets",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { if err := recipe.EnsureExists(app.Recipe); err != nil {
log.Fatal(err) logrus.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
} }
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
headers := []string{"NAME", "VERSION", "GENERATED NAME", "CREATED ON SERVER"} tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table, err := formatter.CreateTable() table := formatter.CreateTable(tableCol)
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
secStats, err := secret.PollSecretsStatus(cl, app) secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
var rows [][]string
for _, secStat := range secStats { for _, secStat := range secStats {
row := []string{ tableRow := []string{
secStat.LocalName, secStat.LocalName,
secStat.Version, secStat.Version,
secStat.RemoteName, secStat.RemoteName,
strconv.FormatBool(secStat.CreatedOnRemote), strconv.FormatBool(secStat.CreatedOnRemote),
} }
table.Append(tableRow)
rows = append(rows, row)
table.Row(row...)
} }
if len(rows) > 0 { if table.NumLines() > 0 {
if internal.MachineReadable { if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows) table.JSONRender()
if err != nil { } else {
log.Fatal("unable to render to JSON: %s", err) table.Render()
}
fmt.Println(out)
return
} }
} else {
if err := formatter.PrintTable(table); err != nil { logrus.Warnf("no secrets stored for %s", app.Name)
log.Fatal(err)
}
return
} }
log.Warnf("no secrets stored for %s", app.Name) return nil
}, },
} }
var AppSecretCommand = &cobra.Command{ var appSecretCommand = cli.Command{
Use: "secret [cmd] [args] [flags]", Name: "secret",
Aliases: []string{"s"}, Aliases: []string{"s"},
Short: "Manage app secrets", Usage: "Manage app secrets",
} ArgsUsage: "<domain>",
Subcommands: []cli.Command{
var ( appSecretGenerateCommand,
storeInPass bool appSecretInsertCommand,
insertFromFile bool appSecretRmCommand,
trimInput bool appSecretLsCommand,
rmAllSecrets bool },
generateAllSecrets bool
removeFromPass bool
)
func init() {
AppSecretGenerateCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
AppSecretGenerateCommand.Flags().BoolVarP(
&storeInPass,
"pass",
"p",
false,
"store generated secrets in a local pass store",
)
AppSecretGenerateCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppSecretGenerateCommand.Flags().BoolVarP(
&generateAllSecrets,
"all",
"a",
false,
"generate all secrets",
)
AppSecretInsertCommand.Flags().BoolVarP(
&storeInPass,
"pass",
"p",
false,
"store generated secrets in a local pass store",
)
AppSecretInsertCommand.Flags().BoolVarP(
&insertFromFile,
"file",
"f",
false,
"treat input as a file",
)
AppSecretInsertCommand.Flags().BoolVarP(
&trimInput,
"trim",
"t",
false,
"trim input",
)
AppSecretInsertCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppSecretRmCommand.Flags().BoolVarP(
&rmAllSecrets,
"all",
"a",
false,
"remove all secrets",
)
AppSecretRmCommand.Flags().BoolVarP(
&removeFromPass,
"pass",
"p",
false,
"remove generated secrets from a local pass store",
)
AppSecretRmCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppSecretLsCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppSecretLsCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
} }

View File

@ -9,64 +9,53 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/service"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
containerTypes "github.com/docker/docker/api/types/container" containerTypes "github.com/docker/docker/api/types/container"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppServicesCommand = &cobra.Command{ var appServicesCommand = cli.Command{
Use: "services <domain> [flags]", Name: "services",
Aliases: []string{"sr"}, Aliases: []string{"sr"},
Short: "Display all services of an app", Usage: "Display all services of an app",
Args: cobra.ExactArgs(1), ArgsUsage: "<domain>",
ValidArgsFunction: func( Flags: []cli.Flag{
cmd *cobra.Command, internal.DebugFlag,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { app := internal.ValidateApp(c)
log.Fatal(err)
}
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !deployMeta.IsDeployed { if !isDeployed {
log.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("%s is not deployed?", app.Name)
} }
filters, err := app.Filters(true, true) filters, err := app.Filters(true, true)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters}) containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
table, err := formatter.CreateTable() tableCol := []string{"service name", "image"}
if err != nil { table := formatter.CreateTable(tableCol)
log.Fatal(err)
}
headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)"}
table.Headers(headers...)
var rows [][]string
for _, container := range containers { for _, container := range containers {
var containerNames []string var containerNames []string
for _, containerName := range container.Names { for _, containerName := range container.Names {
@ -77,20 +66,15 @@ var AppServicesCommand = &cobra.Command{
serviceShortName := service.ContainerToServiceName(container.Names, app.StackName()) serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName) serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
row := []string{ tableRow := []string{
serviceShortName,
serviceLongName, serviceLongName,
formatter.RemoveSha(container.Image),
} }
table.Append(tableRow)
rows = append(rows, row)
} }
table.Rows(rows...) table.Render()
if len(rows) > 0 { return nil
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
}
}, },
} }

View File

@ -3,103 +3,52 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppUndeployCommand = &cobra.Command{ var prune bool
Use: "undeploy <domain> [flags]",
Aliases: []string{"un"},
Short: "Undeploy an app",
Long: `This does not destroy any application data.
However, you should remain vigilant, as your swarm installation will consider var pruneFlag = &cli.BoolFlag{
any previously attached volumes as eligible for pruning once undeployed. Name: "prune, p",
Destination: &prune,
Passing "--prune/-p" does not remove those volumes.`, Usage: "Prunes unused containers, networks, and dangling images for an app",
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)
stackName := app.StackName()
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)
}
log.Debugf("checking whether %s is already deployed", stackName)
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)
}
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
toWriteVersion := deployMeta.Version
if deployMeta.IsChaos {
toWriteVersion = chaosVersion
}
if err := internal.UndeployOverview(
app,
deployMeta.Version,
chaosVersion,
toWriteVersion,
); err != nil {
log.Fatal(err)
}
rmOpts := stack.Remove{
Namespaces: []string{stackName},
Detach: false,
}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
log.Fatal(err)
}
if prune {
if err := pruneApp(cl, app); err != nil {
log.Fatal(err)
}
}
if err := app.WriteRecipeVersion(toWriteVersion, false); err != nil {
log.Fatalf("writing recipe version failed: %s", err)
}
},
} }
// pruneApp runs the equivalent of a "docker system prune" but only filtering // pruneApp runs the equivalent of a "docker system prune" but only filtering
// against resources connected with the app deployment. It is not a system wide // against resources connected with the app deployment. It is not a system wide
// prune. Volumes are not pruned to avoid unwated data loss. // prune. Volumes are not pruned to avoid unwated data loss.
func pruneApp(cl *dockerClient.Client, app appPkg.App) error { func pruneApp(c *cli.Context, cl *dockerClient.Client, app config.App) error {
stackName := app.StackName() stackName := app.StackName()
ctx := context.Background() ctx := context.Background()
for {
logrus.Debugf("polling for %s stack, waiting to be undeployed...", stackName)
services, err := stack.GetStackServices(ctx, cl, stackName)
if err != nil {
return err
}
if len(services) == 0 {
logrus.Debugf("%s undeployed, moving on with pruning logic", stackName)
time.Sleep(time.Second) // give runtime more time to tear down related state
break
}
time.Sleep(time.Second)
}
pruneFilters := filters.NewArgs() pruneFilters := filters.NewArgs()
stackSearch := fmt.Sprintf("%s*", stackName) stackSearch := fmt.Sprintf("%s*", stackName)
pruneFilters.Add("label", stackSearch) pruneFilters.Add("label", stackSearch)
@ -109,14 +58,14 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
} }
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed) logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(ctx, pruneFilters) nr, err := cl.NetworksPrune(ctx, pruneFilters)
if err != nil { if err != nil {
return err return err
} }
log.Infof("networks pruned: %d", len(nr.NetworksDeleted)) logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
ir, err := cl.ImagesPrune(ctx, pruneFilters) ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil { if err != nil {
@ -124,21 +73,66 @@ func pruneApp(cl *dockerClient.Client, app appPkg.App) error {
} }
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed) imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed) logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
return nil return nil
} }
var ( var appUndeployCommand = cli.Command{
prune bool Name: "undeploy",
) Aliases: []string{"un"},
ArgsUsage: "<domain>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
pruneFlag,
},
Before: internal.SubCommandBefore,
Usage: "Undeploy an app",
BashComplete: autocomplete.AppNameComplete,
Description: `
This does not destroy any of the application data.
func init() { However, you should remain vigilant, as your swarm installation will consider
AppUndeployCommand.Flags().BoolVarP( any previously attached volumes as eligible for pruning once undeployed.
&prune,
"prune", Passing "-p/--prune" does not remove those volumes.
"p", `,
false, Action: func(c *cli.Context) error {
"prune unused containers, networks, and dangling images", app := internal.ValidateApp(c)
) stackName := app.StackName()
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil {
logrus.Fatal(err)
}
if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name)
}
if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil {
logrus.Fatal(err)
}
rmOpts := stack.Remove{Namespaces: []string{app.StackName()}}
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
logrus.Fatal(err)
}
if prune {
if err := pruneApp(c, cl, app); err != nil {
logrus.Fatal(err)
}
}
return nil
},
} }

View File

@ -5,431 +5,300 @@ import (
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "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/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
dockerClient "github.com/docker/docker/client" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/urfave/cli"
) )
var AppUpgradeCommand = &cobra.Command{ var appUpgradeCommand = cli.Command{
Use: "upgrade <domain> [version] [flags]", Name: "upgrade",
Aliases: []string{"up"}, Aliases: []string{"up"},
Short: "Upgrade an app", Usage: "Upgrade an app",
Long: `Upgrade an app. ArgsUsage: "<domain> [<version>]",
Flags: []cli.Flag{
Unlike "abra app deploy", chaos operations are not supported here. Only recipe internal.DebugFlag,
versions are supported values for "[version]". internal.NoInputFlag,
internal.ForceFlag,
It is possible to "--force/-f" an upgrade if you want to re-deploy a specific internal.ChaosFlag,
version. internal.NoDomainChecksFlag,
internal.DontWaitConvergeFlag,
Only the deployed version is consulted when trying to determine what upgrades internal.OfflineFlag,
are available. The live deployment version is the "source of truth" in this internal.ReleaseNotesFlag,
case. The stored .env version is not consulted.
An upgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.AppNameComplete()
case 1:
app, err := appPkg.Get(args[0])
if err != nil {
errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return []string{errMsg}, cobra.ShellCompDirectiveError
}
return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default:
return nil, cobra.ShellCompDirectiveError
}
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
var ( Description: `
upgradeWarnMessages []string Upgrade an app. You can use it to choose and roll out a new upgrade to an
chosenUpgrade string existing app.
availableUpgrades []string
upgradeReleaseNotes string
)
app := internal.ValidateApp(args) This command specifically supports incrementing the version of running apps, as
opposed to "abra app deploy <domain>" which will not change the version of a
deployed app.
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { You may pass "--force/-f" to upgrade to the same version again. This can be
log.Fatal(err) useful if the container runtime has gotten into a weird state.
This action could be destructive, please ensure you have a copy of your app
data beforehand.
Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new
recipes.
`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
stackName := app.StackName()
specificVersion := c.Args().Get(1)
if specificVersion != "" && internal.Chaos {
logrus.Fatal("cannot use <version> and --chaos together")
} }
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
logrus.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
logrus.Fatal(err)
}
}
recipe, err := recipePkg.Get(app.Recipe, internal.Offline)
if err != nil {
logrus.Fatal(err)
}
if err := lint.LintForErrors(recipe); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployMeta, err := ensureDeployed(cl, app) isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := lint.LintForErrors(app.Recipe); err != nil { if !isDeployed {
log.Fatal(err) logrus.Fatalf("%s is not deployed?", app.Name)
} }
versions, err := app.Recipe.Tags() catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
// NOTE(d1): we've no idea what the live deployment version is, so every versions, err := recipePkg.GetRecipeCatalogueVersions(app.Recipe, catl)
// possible upgrade can be shown. it's up to the user to make the choice if err != nil {
if deployMeta.Version == config.UNKNOWN_DEFAULT { logrus.Fatal(err)
availableUpgrades = versions
} }
if len(args) == 2 && args[1] != "" { if len(versions) == 0 && !internal.Chaos {
chosenUpgrade = args[1] logrus.Warn("no published versions in catalogue, trying local recipe repository")
recipeVersions, err := recipePkg.GetRecipeVersions(app.Recipe, internal.Offline)
if err := validateUpgradeVersionArg(chosenUpgrade, app, deployMeta); err != nil {
log.Fatal(err)
}
availableUpgrades = append(availableUpgrades, chosenUpgrade)
}
if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenUpgrade == "" {
upgradeAvailable, err := ensureUpgradesAvailable(versions, &availableUpgrades, deployMeta)
if err != nil { if err != nil {
log.Fatal(err) logrus.Warn(err)
} }
for _, recipeVersion := range recipeVersions {
if !upgradeAvailable { for version := range recipeVersion {
log.Info("no available upgrades") versions = append(versions, version)
return }
} }
} }
if internal.Force || internal.NoInput || chosenUpgrade != "" { var availableUpgrades []string
if len(availableUpgrades) > 0 { if deployedVersion == "unknown" {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] availableUpgrades = versions
logrus.Warnf("failed to determine deployed version of %s", app.Name)
}
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
if err != nil {
logrus.Fatal(err)
} }
} else { parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err := chooseUpgrade(availableUpgrades, deployMeta, &chosenUpgrade); err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) {
logrus.Fatalf("%s is not an upgrade for %s?", deployedVersion, specificVersion)
}
availableUpgrades = append(availableUpgrades, specificVersion)
} }
if internal.Force && parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
chosenUpgrade == "" &&
deployMeta.Version != config.UNKNOWN_DEFAULT {
chosenUpgrade = deployMeta.Version
}
if chosenUpgrade == "" {
log.Fatal("unknown deployed version, unable to upgrade")
}
log.Debugf("choosing %s as version to upgrade", chosenUpgrade)
// NOTE(d1): if release notes written after git tag published, read them
// before we check out the tag and then they'll appear to be missing. this
// covers when we obviously will forget to write release notes before
// publishing
if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil {
log.Fatal(err)
}
if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
log.Fatal(err)
}
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
if deployedVersion != "unknown" && !internal.Chaos && specificVersion == "" {
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && !(parsedVersion.Equals(parsedDeployedVersion)) {
availableUpgrades = append(availableUpgrades, version)
}
}
if len(availableUpgrades) == 0 && !internal.Force {
logrus.Infof("no available upgrades, you're on latest (%s) ✌️", deployedVersion)
return nil
}
}
var chosenUpgrade string
if len(availableUpgrades) > 0 && !internal.Chaos {
if internal.Force || internal.NoInput || specificVersion != "" {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
} else {
prompt := &survey.Select{
Message: fmt.Sprintf("Please select an upgrade (current version: %s):", deployedVersion),
Options: internal.ReverseStringList(availableUpgrades),
}
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
return err
}
}
}
if internal.Force && chosenUpgrade == "" {
logrus.Warnf("%s is already upgraded to latest but continuing (--force/--chaos)", app.Name)
chosenUpgrade = deployedVersion
}
// if release notes written after git tag published, read them before we
// check out the tag and then they'll appear to be missing. this covers
// when we obviously will forget to write release notes before publishing
var releaseNotes string
if chosenUpgrade != "" {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
logrus.Fatal(err)
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
logrus.Fatal(err)
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := internal.GetReleaseNotes(app.Recipe, version)
if err != nil {
return err
}
if note != "" {
releaseNotes += fmt.Sprintf("%s\n", note)
}
}
}
}
if !internal.Chaos {
if err := recipePkg.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
logrus.Fatal(err)
}
}
if internal.Chaos {
logrus.Warn("chaos mode engaged")
var err error
chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe)
if err != nil {
logrus.Fatal(err)
}
}
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil {
logrus.Fatal(err)
} }
for k, v := range abraShEnv { for k, v := range abraShEnv {
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env) composeFiles, err := config.GetComposeFiles(app.Recipe, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
stackName := app.StackName()
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Composefiles: composeFiles, Composefiles: composeFiles,
Namespace: stackName, Namespace: stackName,
Prune: false, Prune: false,
ResolveImage: stack.ResolveImageAlways, ResolveImage: stack.ResolveImageAlways,
Detach: false,
} }
compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env)
compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
config.SetUpdateLabel(compose, stackName, app.Env)
appPkg.ExposeAllEnv(stackName, compose, app.Env) envVars, err := config.CheckEnv(app)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
}
appPkg.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, envVar := range envVars { for _, envVar := range envVars {
if !envVar.Present { if !envVar.Present {
upgradeWarnMessages = append(upgradeWarnMessages, logrus.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
)
} }
} }
if showReleaseNotes { if internal.ReleaseNotes {
fmt.Print(upgradeReleaseNotes) fmt.Println()
return fmt.Print(releaseNotes)
return nil
} }
chaosVersion := config.CHAOS_DEFAULT if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
if deployMeta.IsChaos { logrus.Fatal(err)
chaosVersion = deployMeta.ChaosVersion
if deployMeta.ChaosVersion == "" {
chaosVersion = config.UNKNOWN_DEFAULT
}
} }
if err := internal.NewVersionOverview( stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName)
app,
upgradeWarnMessages,
"upgrade",
deployMeta.Version,
chaosVersion,
chosenUpgrade,
upgradeReleaseNotes,
); err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil { if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"] postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
if ok && !internal.DontWaitConverge { if ok && !internal.DontWaitConverge {
log.Debugf("run the following post-deploy commands: %s", postDeployCmds) logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds)
if err := internal.PostCmds(cl, app, postDeployCmds); err != nil { if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
log.Fatalf("attempting to run post deploy commands, saw: %s", err) logrus.Fatalf("attempting to run post deploy commands, saw: %s", err)
} }
} }
if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil { return nil
log.Fatalf("writing recipe version failed: %s", err)
}
}, },
} }
// chooseUpgrade prompts the user to choose an upgrade interactively.
func chooseUpgrade(
availableUpgrades []string,
deployMeta stack.DeployMeta,
chosenUpgrade *string,
) error {
msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version)
if deployMeta.IsChaos {
chaosVersion := formatter.BoldDirtyDefault(deployMeta.ChaosVersion)
msg = fmt.Sprintf(
"please select an upgrade (version: %s, chaos: %s):",
deployMeta.Version,
chaosVersion,
)
}
prompt := &survey.Select{
Message: msg,
Options: internal.SortVersionsDesc(availableUpgrades),
}
if err := survey.AskOne(prompt, chosenUpgrade); err != nil {
return err
}
return nil
}
func getReleaseNotes(
app app.App,
versions []string,
chosenUpgrade string,
deployMeta stack.DeployMeta,
upgradeReleaseNotes *string,
) error {
parsedChosenUpgrade, err := tagcmp.Parse(chosenUpgrade)
if err != nil {
return err
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return err
}
for _, version := range internal.SortVersionsDesc(versions) {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return err
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := app.Recipe.GetReleaseNotes(version)
if err != nil {
return err
}
if note != "" {
*upgradeReleaseNotes += fmt.Sprintf("%s\n", note)
}
}
}
return nil
}
// ensureUpgradesAvailable ensures that there are available upgrades.
func ensureUpgradesAvailable(
versions []string,
availableUpgrades *[]string,
deployMeta stack.DeployMeta,
) (bool, error) {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return false, err
}
for _, version := range versions {
parsedVersion, err := tagcmp.Parse(version)
if err != nil {
return false, err
}
if parsedVersion.IsGreaterThan(parsedDeployedVersion) &&
!(parsedVersion.Equals(parsedDeployedVersion)) {
*availableUpgrades = append(*availableUpgrades, version)
}
}
if len(*availableUpgrades) == 0 && !internal.Force {
return false, nil
}
return true, nil
}
// validateUpgradeVersionArg validates the specific version.
func validateUpgradeVersionArg(
specificVersion string,
app app.App,
deployMeta stack.DeployMeta,
) error {
parsedSpecificVersion, err := tagcmp.Parse(specificVersion)
if err != nil {
return fmt.Errorf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name)
}
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
return err
}
if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) &&
!parsedSpecificVersion.Equals(parsedDeployedVersion) {
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force {
return fmt.Errorf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion)
}
return nil
}
// ensureDeployed ensures the app is deployed and if so, returns deployment
// meta info.
func ensureDeployed(cl *dockerClient.Client, app app.App) (stack.DeployMeta, error) {
log.Debugf("checking whether %s is already deployed", app.StackName())
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil {
return stack.DeployMeta{}, err
}
if !deployMeta.IsDeployed {
return stack.DeployMeta{}, fmt.Errorf("%s is not deployed?", app.Name)
}
return deployMeta, nil
}
var (
showReleaseNotes bool
)
func init() {
AppUpgradeCommand.Flags().BoolVarP(
&internal.Force,
"force",
"f",
false,
"perform action without further prompt",
)
AppUpgradeCommand.Flags().BoolVarP(
&internal.NoDomainChecks,
"no-domain-checks",
"D",
false,
"disable public DNS checks",
)
AppUpgradeCommand.Flags().BoolVarP(
&internal.DontWaitConverge, "no-converge-checks",
"c",
false,
"disable converge logic checks",
)
AppUpgradeCommand.Flags().BoolVarP(
&showReleaseNotes,
"releasenotes",
"r",
false,
"only show release notes",
)
}

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

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

View File

@ -2,120 +2,114 @@ package app
import ( import (
"context" "context"
"log"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var AppVolumeListCommand = &cobra.Command{ var appVolumeListCommand = cli.Command{
Use: "list <domain> [flags]", Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Short: "List volumes associated with an app", ArgsUsage: "<domain>",
Args: cobra.ExactArgs(1), Flags: []cli.Flag{
ValidArgsFunction: func( internal.DebugFlag,
cmd *cobra.Command, internal.NoInputFlag,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) Usage: "List volumes associated with an app",
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
filters, err := app.Filters(false, true) filters, err := app.Filters(false, true)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
volumes, err := client.GetVolumes(cl, context.Background(), app.Server, filters) volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
headers := []string{"NAME", "ON SERVER"} table := formatter.CreateTable([]string{"name", "created", "mounted"})
var volTable [][]string
table, err := formatter.CreateTable() for _, volume := range volumeList {
if err != nil { volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
log.Fatal(err) volTable = append(volTable, volRow)
} }
table.Headers(headers...) table.AppendBulk(volTable)
var rows [][]string if table.NumLines() > 0 {
for _, volume := range volumes { table.Render()
row := []string{volume.Name, volume.Mountpoint} } else {
rows = append(rows, row) logrus.Warnf("no volumes created for %s", app.Name)
} }
table.Rows(rows...) return nil
if len(rows) > 0 {
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
return
}
log.Warnf("no volumes created for %s", app.Name)
}, },
} }
var AppVolumeRemoveCommand = &cobra.Command{ var appVolumeRemoveCommand = cli.Command{
Use: "remove <domain> [flags]", Name: "remove",
Short: "Remove volume(s) associated with an app", Usage: "Remove volume(s) associated with an app",
Long: `Remove volumes associated with an app. Description: `
This command supports removing volumes associated with an app. The app in
The app in question must be undeployed before you try to remove volumes. See question must be undeployed before you try to remove volumes. See "abra app
"abra app undeploy <domain>" for more. undeploy <domain>" for more.
The command is interactive and will show a multiple select input which allows The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this you to make a seclection. Use the "?" key to see more help on navigating this
interface. interface.
Passing "--force/-f" will select all volumes for removal. Be careful.`, Passing "--force/-f" will select all volumes for removal. Be careful.
Aliases: []string{"rm"}, `,
Args: cobra.MinimumNArgs(1), ArgsUsage: "<domain>",
ValidArgsFunction: func( Aliases: []string{"rm"},
cmd *cobra.Command, Flags: []cli.Flag{
args []string, internal.DebugFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.NoInputFlag,
return autocomplete.AppNameComplete() internal.ForceFlag,
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
app := internal.ValidateApp(args) BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName()) isDeployed, _, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if deployMeta.IsDeployed { if isDeployed {
log.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name) logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name)
} }
filters, err := app.Filters(false, true) filters, err := app.Filters(false, true)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters) volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
volumeNames := client.GetVolumeNames(volumeList) volumeNames := client.GetVolumeNames(volumeList)
@ -129,7 +123,7 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
Default: volumeNames, Default: volumeNames,
} }
if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil { if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
@ -143,25 +137,22 @@ Passing "--force/-f" will select all volumes for removal. Be careful.`,
log.Fatalf("removing volumes failed: %s", err) log.Fatalf("removing volumes failed: %s", err)
} }
log.Infof("%d volumes removed successfully", len(volumesToRemove)) logrus.Infof("%d volumes removed successfully", len(volumesToRemove))
} else { } else {
log.Info("no volumes removed") logrus.Info("no volumes removed")
} }
return nil
}, },
} }
var AppVolumeCommand = &cobra.Command{ var appVolumeCommand = cli.Command{
Use: "volume [cmd] [args] [flags]", Name: "volume",
Aliases: []string{"vl"}, Aliases: []string{"vl"},
Short: "Manage app volumes", Usage: "Manage app volumes",
} ArgsUsage: "<domain>",
Subcommands: []cli.Command{
func init() { appVolumeListCommand,
AppVolumeRemoveCommand.Flags().BoolVarP( appVolumeRemoveCommand,
&internal.Force, },
"force",
"f",
false,
"perform action without further prompt",
)
} }

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"path" "path"
"slices"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
@ -13,102 +12,100 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var CatalogueGenerateCommand = &cobra.Command{ var catalogueGenerateCommand = cli.Command{
Use: "generate [recipe] [flags]", Name: "generate",
Aliases: []string{"g"}, Aliases: []string{"g"},
Short: "Generate the recipe catalogue", Usage: "Generate the recipe catalogue",
Long: `Generate a new copy of the recipe catalogue. Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PublishFlag,
internal.DryFlag,
internal.SkipUpdatesFlag,
internal.ChaosFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Description: `
Generate a new copy of the recipe catalogue which can be found on:
N.B. this command **will** wipe local unstaged changes from your local recipes https://recipes.coopcloud.tech (website that humans read)
if present. "--chaos/-C" on this command refers to the catalogue repository https://recipes.coopcloud.tech/recipes.json (JSON that Abra reads)
("$ABRA_DIR/catalogue") and not the recipes. Please take care not to lose your
changes. It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
listing, parses README.md and git tags to produce recipe metadata which is
loaded into the catalogue JSON file.
It is possible to generate new metadata for a single recipe by passing It is possible to generate new metadata for a single recipe by passing
[recipe]. The existing local catalogue will be updated, not overwritten. <recipe>. The existing local catalogue will be updated, not overwritten.
It is quite easy to get rate limited by Docker Hub when running this command. It is quite easy to get rate limited by Docker Hub when running this command.
If you have a Hub account you can "docker login" and Abra will automatically If you have a Hub account you can have Abra log you in to avoid this. Pass
use those details. "--user" and "--pass".
Push your new release to git.coopcloud.tech with "--publish/-p". This requires Push your new release to git.coopcloud.tech with "-p/--publish". This requires
that you have permission to git push to these repositories and have your SSH that you have permission to git push to these repositories and have your SSH
keys configured on your account.`, keys configured on your account.
Args: cobra.RangeArgs(0, 1), `,
ValidArgsFunction: func( ArgsUsage: "[<recipe>]",
cmd *cobra.Command, BashComplete: autocomplete.RecipeNameComplete,
args []string, Action: func(c *cli.Context) error {
toComplete string) ([]string, cobra.ShellCompDirective) { recipeName := c.Args().First()
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
var recipeName string
if len(args) > 0 {
recipeName = args[0]
}
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(args, cmd.Name()) internal.ValidateRecipe(c)
}
if err := catalogue.EnsureCatalogue(); err != nil {
log.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := catalogue.EnsureIsClean(); err != nil { if err := catalogue.EnsureIsClean(); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
repos, err := recipe.ReadReposMetadata(internal.Debug) repos, err := recipe.ReadReposMetadata()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
barLength := len(repos) var barLength int
var logMsg string
if recipeName != "" { if recipeName != "" {
barLength = 1 barLength = 1
logMsg = fmt.Sprintf("ensuring %v recipe is cloned & up-to-date", barLength)
} else {
barLength = len(repos)
logMsg = fmt.Sprintf("ensuring %v recipes are cloned & up-to-date, this could take some time...", barLength)
} }
if !skipUpdates { if !internal.SkipUpdates {
if err := recipe.UpdateRepositories(repos, recipeName, internal.Debug); err != nil { logrus.Warn(logMsg)
log.Fatal(err) if err := recipe.UpdateRepositories(repos, recipeName); err != nil {
logrus.Fatal(err)
} }
} }
var warnings []string
catl := make(recipe.RecipeCatalogue) catl := make(recipe.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(barLength, "collecting catalogue metadata") catlBar := formatter.CreateProgressbar(barLength, "generating catalogue metadata...")
for _, recipeMeta := range repos { for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name { if recipeName != "" && recipeName != recipeMeta.Name {
if !internal.Debug { catlBar.Add(1)
catlBar.Add(1)
}
continue continue
} }
r := recipe.Get(recipeMeta.Name) versions, err := recipe.GetRecipeVersions(recipeMeta.Name, internal.Offline)
versions, warnMsgs, err := r.GetRecipeVersions()
if err != nil { if err != nil {
warnings = append(warnings, err.Error()) logrus.Warn(err)
}
if len(warnMsgs) > 0 {
warnings = append(warnings, warnMsgs...)
} }
features, category, warnMsgs, err := recipe.GetRecipeFeaturesAndCategory(r) features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name)
if err != nil { if err != nil {
warnings = append(warnings, err.Error()) logrus.Warn(err)
}
if len(warnMsgs) > 0 {
warnings = append(warnings, warnMsgs...)
} }
catl[recipeMeta.Name] = recipe.RecipeMeta{ catl[recipeMeta.Name] = recipe.RecipeMeta{
@ -124,152 +121,103 @@ keys configured on your account.`,
Features: features, Features: features,
} }
if !internal.Debug { catlBar.Add(1)
catlBar.Add(1)
}
}
if err := catlBar.Close(); err != nil {
log.Fatal(err)
}
var uniqueWarnings []string
for _, w := range warnings {
if !slices.Contains(uniqueWarnings, w) {
uniqueWarnings = append(uniqueWarnings, w)
}
}
for _, warnMsg := range uniqueWarnings {
log.Warn(warnMsg)
} }
recipesJSON, err := json.MarshalIndent(catl, "", " ") recipesJSON, err := json.MarshalIndent(catl, "", " ")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if recipeName == "" { if recipeName == "" {
if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil { if err := ioutil.WriteFile(config.RECIPES_JSON, recipesJSON, 0764); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline) catlFS, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
catlFS[recipeName] = catl[recipeName] catlFS[recipeName] = catl[recipeName]
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ") updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil { if err := ioutil.WriteFile(config.RECIPES_JSON, updatedRecipesJSON, 0764); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
log.Infof("generated recipe catalogue: %s", config.RECIPES_JSON) logrus.Infof("generated new recipe catalogue in %s", config.RECIPES_JSON)
cataloguePath := path.Join(config.ABRA_DIR, "catalogue") cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if publishChanges { if internal.Publish {
isClean, err := gitPkg.IsClean(cataloguePath) isClean, err := gitPkg.IsClean(cataloguePath)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if isClean { if isClean {
if !internal.Dry { if !internal.Dry {
log.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath) logrus.Fatalf("no changes discovered in %s, nothing to publish?", cataloguePath)
} }
} }
msg := "chore: publish new catalogue release changes" msg := "chore: publish new catalogue release changes"
if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil { if err := gitPkg.Commit(cataloguePath, msg, internal.Dry); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
repo, err := git.PlainOpen(cataloguePath) repo, err := git.PlainOpen(cataloguePath)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
sshURL := fmt.Sprintf(config.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 { if err := gitPkg.CreateRemote(repo, "origin-ssh", sshURL, internal.Dry); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil { if err := gitPkg.Push(cataloguePath, "origin-ssh", false, internal.Dry); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
repo, err := git.PlainOpen(cataloguePath) repo, err := git.PlainOpen(cataloguePath)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
head, err := repo.Head() head, err := repo.Head()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !internal.Dry && publishChanges { if !internal.Dry && internal.Publish {
url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash()) url := fmt.Sprintf("%s/%s/commit/%s", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME, head.Hash())
log.Infof("new changes published: %s", url) logrus.Infof("new changes published: %s", url)
} }
if internal.Dry { if internal.Dry {
log.Info("dry run: no changes published") logrus.Info("dry run: no changes published")
} }
return nil
}, },
} }
// CatalogueCommand defines the `abra catalogue` command and sub-commands. // CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = &cobra.Command{ var CatalogueCommand = cli.Command{
Use: "catalogue [cmd] [args] [flags]", Name: "catalogue",
Short: "Manage the recipe catalogue", Usage: "Manage the recipe catalogue",
Aliases: []string{"c"}, Aliases: []string{"c"},
} ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue",
var ( Subcommands: []cli.Command{
publishChanges bool catalogueGenerateCommand,
skipUpdates bool },
)
func init() {
CatalogueGenerateCommand.Flags().BoolVarP(
&publishChanges,
"publish",
"p",
false,
"publish changes to git.coopcloud.tech",
)
CatalogueGenerateCommand.Flags().BoolVarP(
&internal.Dry,
"dry-run",
"r",
false,
"report changes that would be made",
)
CatalogueGenerateCommand.Flags().BoolVarP(
&skipUpdates,
"skip-updates",
"s",
false,
"skip updating recipe repositories",
)
CatalogueGenerateCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
} }

206
cli/cli.go Normal file
View File

@ -0,0 +1,206 @@
// Package cli provides the interface for the command-line.
package cli
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/autocomplete"
cataloguePkg "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/web"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// AutoCompleteCommand helps people set up auto-complete in their shells
var AutoCompleteCommand = cli.Command{
Name: "autocomplete",
Aliases: []string{"ac"},
Usage: "Configure shell autocompletion (recommended)",
Description: `
Set up auto-completion in your shell by downloading the relevant files and
laying out what additional information must be loaded. Supported shells are as
follows: bash, fish, fizsh & zsh.
Example:
abra autocomplete bash
`,
ArgsUsage: "<shell>",
Flags: []cli.Flag{
internal.DebugFlag,
},
Action: func(c *cli.Context) error {
shellType := c.Args().First()
if shellType == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no shell provided"))
}
supportedShells := map[string]bool{
"bash": true,
"zsh": true,
"fizsh": true,
"fish": true,
}
if _, ok := supportedShells[shellType]; !ok {
logrus.Fatalf("%s is not a supported shell right now, sorry", shellType)
}
if shellType == "fizsh" {
shellType = "zsh" // handled the same on the autocompletion side
}
autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion")
if err := os.Mkdir(autocompletionDir, 0764); err != nil {
if !os.IsExist(err) {
logrus.Fatal(err)
}
logrus.Debugf("%s already created", autocompletionDir)
}
autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType)
if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) {
url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType)
logrus.Infof("fetching %s", url)
if err := web.GetFile(autocompletionFile, url); err != nil {
logrus.Fatal(err)
}
}
switch shellType {
case "bash":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
sudo mkdir /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "zsh":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
sudo mkdir /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "fish":
fmt.Println(fmt.Sprintf(`
# Run the following commands to install auto-completion
sudo mkdir -p /etc/fish/completions
sudo cp %s /etc/fish/completions/abra
echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
}
return nil
},
}
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = cli.Command{
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade Abra itself",
Description: `
Upgrade Abra in-place with the latest stable or release candidate.
Pass "-r/--rc" to install the latest release candidate. Please bear in mind
that it may contain catastrophic bugs. Thank you very much for the testing
efforts!
`,
Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error {
mainURL := "https://install.abra.coopcloud.tech"
cmd := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if internal.RC {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
cmd = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
logrus.Debugf("attempting to run %s", cmd)
if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err)
}
return nil
},
}
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "abra",
Usage: `The Co-op Cloud command-line utility belt 🎩🐇
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{
app.AppCommand,
server.ServerCommand,
recipe.RecipeCommand,
catalogue.CatalogueCommand,
UpgradeCommand,
AutoCompleteCommand,
},
BashComplete: autocomplete.SubcommandComplete,
}
app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error {
paths := []string{
config.ABRA_DIR,
config.SERVERS_DIR,
config.RECIPES_DIR,
config.VENDOR_DIR,
config.BACKUP_DIR,
}
for _, path := range paths {
if err := os.Mkdir(path, 0764); err != nil {
if !os.IsExist(err) {
logrus.Fatal(err)
}
continue
}
}
if err := cataloguePkg.EnsureCatalogue(); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("abra version %s, commit %s", version, commit)
return nil
}
return app
}
// RunApp runs CLI abra app.
func RunApp(version, commit string) {
app := newAbraApp(version, commit)
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}

View File

@ -1,62 +0,0 @@
package cli
import (
"os"
"github.com/spf13/cobra"
)
var AutocompleteCommand = &cobra.Command{
Use: "autocomplete [bash|zsh|fish|powershell]",
Short: "Generate autocompletion script",
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
# macOS:
$ abra autocomplete bash | sudo tee $(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:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load autocompletions for each session, execute once:
$ abra autocomplete zsh > "${fpath[1]}/_abra"
# 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:
PS> abra autocomplete powershell > abra.ps1
# and source this file from your PowerShell profile.`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
},
}

View File

@ -2,18 +2,16 @@ package internal
import ( import (
"context" "context"
"fmt"
"io"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/service" "coopcloud.tech/abra/pkg/service"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
) )
// RetrieveBackupBotContainer gets the deployed backupbot container. // RetrieveBackupBotContainer gets the deployed backupbot container.
@ -21,10 +19,10 @@ func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error
ctx := context.Background() ctx := context.Background()
chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, NoInput) chosenService, err := service.GetServiceByLabel(ctx, cl, config.BackupbotLabel, NoInput)
if err != nil { if err != nil {
return types.Container{}, fmt.Errorf("no backupbot discovered, is it deployed?") return types.Container{}, err
} }
log.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name) logrus.Debugf("retrieved %s as backup enabled service", chosenService.Spec.Name)
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", chosenService.Spec.Name) filters.Add("name", chosenService.Spec.Name)
@ -42,11 +40,7 @@ func RetrieveBackupBotContainer(cl *dockerClient.Client) (types.Container, error
} }
// RunBackupCmdRemote runs a backup related command on a remote backupbot container. // RunBackupCmdRemote runs a backup related command on a remote backupbot container.
func RunBackupCmdRemote( func RunBackupCmdRemote(cl *dockerClient.Client, backupCmd string, containerID string, execEnv []string) error {
cl *dockerClient.Client,
backupCmd string,
containerID string,
execEnv []string) (io.Writer, error) {
execBackupListOpts := types.ExecConfig{ execBackupListOpts := types.ExecConfig{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
@ -57,18 +51,17 @@ func RunBackupCmdRemote(
Tty: true, Tty: true,
} }
log.Debugf("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts) logrus.Debugf("running backup %s on %s with exec config %v", backupCmd, containerID, execBackupListOpts)
// FIXME: avoid instantiating a new CLI // FIXME: avoid instantiating a new CLI
dcli, err := command.NewDockerCli() dcli, err := command.NewDockerCli()
if err != nil { if err != nil {
return nil, err return err
} }
out, err := container.RunExec(dcli, cl, containerID, &execBackupListOpts) if _, err := container.RunExec(dcli, cl, containerID, &execBackupListOpts); err != nil {
if err != nil { return err
return nil, err
} }
return out, nil return nil
} }

View File

@ -1,20 +1,261 @@
package internal package internal
var ( import (
// NOTE(d1): global "os"
Debug bool
NoInput bool
Offline bool
IgnoreEnvVersion bool
// NOTE(d1): sub-command specific logrusStack "github.com/Gurpartap/logrus-stack"
Chaos bool "github.com/sirupsen/logrus"
DontWaitConverge bool "github.com/urfave/cli"
Dry bool
Force bool
MachineReadable bool
Major bool
Minor bool
NoDomainChecks bool
Patch bool
) )
// Secrets stores the variable from SecretsFlag
var Secrets bool
// SecretsFlag turns on/off automatically generating secrets
var SecretsFlag = &cli.BoolFlag{
Name: "secrets, S",
Usage: "Automatically generate secrets",
Destination: &Secrets,
}
// Pass stores the variable from PassFlag
var Pass bool
// PassFlag turns on/off storing generated secrets in pass
var PassFlag = &cli.BoolFlag{
Name: "pass, p",
Usage: "Store the generated secrets in a local pass store",
Destination: &Pass,
}
// PassRemove stores the variable for PassRemoveFlag
var PassRemove bool
// PassRemoveFlag turns on/off removing generated secrets from pass
var PassRemoveFlag = &cli.BoolFlag{
Name: "pass, p",
Usage: "Remove generated secrets from a local pass store",
Destination: &PassRemove,
}
// Force force functionality without asking.
var Force bool
// ForceFlag turns on/off force functionality.
var ForceFlag = &cli.BoolFlag{
Name: "force, f",
Usage: "Perform action without further prompt. Use with care!",
Destination: &Force,
}
// Chaos engages chaos mode.
var Chaos bool
// ChaosFlag turns on/off chaos functionality.
var ChaosFlag = &cli.BoolFlag{
Name: "chaos, C",
Usage: "Proceed with uncommitted recipes changes. Use with care!",
Destination: &Chaos,
}
// Disable tty to run commands from script
var Tty bool
// TtyFlag turns on/off tty mode.
var TtyFlag = &cli.BoolFlag{
Name: "tty, T",
Usage: "Disables TTY mode to run this command from a script.",
Destination: &Tty,
}
var NoInput bool
var NoInputFlag = &cli.BoolFlag{
Name: "no-input, n",
Usage: "Toggle non-interactive mode",
Destination: &NoInput,
}
// Debug stores the variable from DebugFlag.
var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug, d",
Destination: &Debug,
Usage: "Show DEBUG messages",
}
// Offline stores the variable from OfflineFlag.
var Offline bool
// DebugFlag turns on/off offline mode.
var OfflineFlag = &cli.BoolFlag{
Name: "offline, o",
Destination: &Offline,
Usage: "Prefer offline & filesystem access when possible",
}
// ReleaseNotes stores the variable from ReleaseNotesFlag.
var ReleaseNotes bool
// ReleaseNotesFlag turns on/off printing only release notes when upgrading.
var ReleaseNotesFlag = &cli.BoolFlag{
Name: "releasenotes, r",
Destination: &ReleaseNotes,
Usage: "Only show release notes",
}
// MachineReadable stores the variable from MachineReadableFlag
var MachineReadable bool
// MachineReadableFlag turns on/off machine readable output where supported
var MachineReadableFlag = &cli.BoolFlag{
Name: "machine, m",
Destination: &MachineReadable,
Usage: "Output in a machine-readable format (where supported)",
}
// RC signifies the latest release candidate
var RC bool
// RCFlag chooses the latest release candidate for install
var RCFlag = &cli.BoolFlag{
Name: "rc, r",
Destination: &RC,
Usage: "Install the latest release candidate",
}
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major, x",
Usage: "Increase the major part of the version",
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor, y",
Usage: "Increase the minor part of the version",
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch, z",
Usage: "Increase the patch part of the version",
Destination: &Patch,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run, r",
Usage: "Only reports changes that would be made",
Destination: &Dry,
}
var Publish bool
var PublishFlag = &cli.BoolFlag{
Name: "publish, p",
Usage: "Publish changes to git.coopcloud.tech",
Destination: &Publish,
}
var Domain string
var DomainFlag = &cli.StringFlag{
Name: "domain, D",
Value: "",
Usage: "Choose a domain name",
Destination: &Domain,
}
var NewAppServer string
var NewAppServerFlag = &cli.StringFlag{
Name: "server, s",
Value: "",
Usage: "Show apps of a specific server",
Destination: &NewAppServer,
}
var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks, D",
Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks,
}
var StdErrOnly bool
var StdErrOnlyFlag = &cli.BoolFlag{
Name: "stderr, s",
Usage: "Only tail stderr",
Destination: &StdErrOnly,
}
var SinceLogs string
var SinceLogsFlag = &cli.StringFlag{
Name: "since, S",
Value: "",
Usage: "tail logs since YYYY-MM-DDTHH:MM:SSZ",
Destination: &SinceLogs,
}
var DontWaitConverge bool
var DontWaitConvergeFlag = &cli.BoolFlag{
Name: "no-converge-checks, c",
Usage: "Don't wait for converge logic checks",
Destination: &DontWaitConverge,
}
var Watch bool
var WatchFlag = &cli.BoolFlag{
Name: "watch, w",
Usage: "Watch status by polling repeatedly",
Destination: &Watch,
}
var OnlyErrors bool
var OnlyErrorFlag = &cli.BoolFlag{
Name: "errors, e",
Usage: "Only show errors",
Destination: &OnlyErrors,
}
var SkipUpdates bool
var SkipUpdatesFlag = &cli.BoolFlag{
Name: "skip-updates, s",
Usage: "Skip updating recipe repositories",
Destination: &SkipUpdates,
}
var AllTags bool
var AllTagsFlag = &cli.BoolFlag{
Name: "all-tags, a",
Usage: "List all tags, not just upgrades",
Destination: &AllTags,
}
var LocalCmd bool
var LocalCmdFlag = &cli.BoolFlag{
Name: "local, l",
Usage: "Run command locally",
Destination: &LocalCmd,
}
var RemoteUser string
var RemoteUserFlag = &cli.StringFlag{
Name: "user, u",
Value: "",
Usage: "User to run command within a service context",
Destination: &RemoteUser,
}
// SubCommandBefore wires up pre-action machinery (e.g. --debug handling).
func SubCommandBefore(c *cli.Context) error {
if Debug {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)
logrus.AddHook(logrusStack.StandardHook())
}
return nil
}

View File

@ -8,24 +8,20 @@ import (
"os/exec" "os/exec"
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container" containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/sirupsen/logrus"
) )
// RunCmdRemote executes an abra.sh command in the target service // RunCmdRemote executes an abra.sh command in the target service
func RunCmdRemote( func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, cmdName, cmdArgs string) error {
cl *dockerClient.Client,
app appPkg.App,
requestTTY bool,
abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName)) filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
@ -34,7 +30,7 @@ func RunCmdRemote(
return err return err
} }
log.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server) logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip} toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
content, err := archive.TarWithOptions(abraSh, toTarOpts) content, err := archive.TarWithOptions(abraSh, toTarOpts)
@ -65,7 +61,7 @@ func RunCmdRemote(
} }
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
log.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name) logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
shell = "/bin/sh" shell = "/bin/sh"
} }
@ -76,17 +72,17 @@ func RunCmdRemote(
cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)} cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
} }
log.Debugf("running command: %s", strings.Join(cmd, " ")) logrus.Debugf("running command: %s", strings.Join(cmd, " "))
if remoteUser != "" { if RemoteUser != "" {
log.Debugf("running command with user %s", remoteUser) logrus.Debugf("running command with user %s", RemoteUser)
execCreateOpts.User = remoteUser execCreateOpts.User = RemoteUser
} }
execCreateOpts.Cmd = cmd execCreateOpts.Cmd = cmd
execCreateOpts.Tty = requestTTY execCreateOpts.Tty = true
if !requestTTY { if Tty {
log.Debugf("not requesting a remote TTY") execCreateOpts.Tty = false
} }
if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {

View File

@ -2,54 +2,26 @@ package internal
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"sort" "path"
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
) )
var borderStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Padding(0, 1, 0, 1).
MaxWidth(79).
BorderForeground(lipgloss.Color("63"))
var headerStyle = lipgloss.NewStyle().
Underline(true).
Bold(true).
PaddingBottom(1)
var leftStyle = lipgloss.NewStyle().
Bold(true)
var rightStyle = lipgloss.NewStyle()
// horizontal is a JoinHorizontal helper function.
func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right)
}
// NewVersionOverview shows an upgrade or downgrade overview // NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview( func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error {
app appPkg.App, tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"}
warnMessages []string, table := formatter.CreateTable(tableCol)
kind,
deployedVersion,
deployedChaosVersion,
toDeployVersion,
releaseNotes string) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
} }
server := app.Server server := app.Server
@ -57,58 +29,14 @@ func NewVersionOverview(
server = "local" server = "local"
} }
domain := app.Domain table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
if domain == "" { table.Render()
domain = config.NO_DOMAIN_DEFAULT
}
upperKind := strings.ToUpper(kind) if releaseNotes != "" && newVersion != "" {
fmt.Println()
envVersion, err := recipe.GetEnvVersionRaw(app.Recipe.Name)
if err != nil {
return err
}
if envVersion == "" {
envVersion = config.NO_VERSION_DEFAULT
}
rows := [][]string{
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(deployedVersion)},
{"CHAOS ", formatter.BoldDirtyDefault(deployedChaosVersion)},
{upperKind, "---"},
{"VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Domain)), "---"},
{"CURRENT VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
}
overview := formatter.CreateOverview(
fmt.Sprintf("%s OVERVIEW", upperKind),
rows,
)
fmt.Println(overview)
if releaseNotes != "" && toDeployVersion != "" {
fmt.Print(releaseNotes) fmt.Print(releaseNotes)
} else { } else {
warnMessages = append( logrus.Warnf("no release notes available for %s", newVersion)
warnMessages,
fmt.Sprintf("no release notes available for %s", toDeployVersion),
)
}
for _, msg := range warnMessages {
log.Warn(msg)
} }
if NoInput { if NoInput {
@ -116,185 +44,49 @@ func NewVersionOverview(
} }
response := false response := false
prompt := &survey.Confirm{Message: "proceed?"} prompt := &survey.Confirm{
Message: "continue with deployment?",
}
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
return err return err
} }
if !response { if !response {
log.Fatal("deployment cancelled") logrus.Fatal("exiting as requested")
} }
return nil return nil
} }
// DeployOverview shows a deployment overview // GetReleaseNotes prints release notes for a recipe version
func DeployOverview( func GetReleaseNotes(recipeName, version string) (string, error) {
app appPkg.App, if version == "" {
warnMessages []string, return "", nil
deployedVersion string,
deployedChaosVersion string,
toDeployVersion,
toDeployChaosVersion string,
toWriteVersion string,
) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles
} }
server := app.Server fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version)
if app.Server == "default" {
server = "local" if _, err := os.Stat(fpath); !os.IsNotExist(err) {
releaseNotes, err := ioutil.ReadFile(fpath)
if err != nil {
return "", err
}
withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes))
return withTitle, nil
} }
domain := app.Domain return "", nil
if domain == "" {
domain = config.NO_DOMAIN_DEFAULT
}
if app.Recipe.Dirty {
toWriteVersion = formatter.AddDirtyMarker(toWriteVersion)
toDeployChaosVersion = formatter.AddDirtyMarker(toDeployChaosVersion)
}
recipeName, exists := app.Env["RECIPE"]
if !exists {
recipeName = app.Env["TYPE"]
}
envVersion, err := recipe.GetEnvVersionRaw(recipeName)
if err != nil {
return err
}
if envVersion == "" {
envVersion = config.NO_VERSION_DEFAULT
}
rows := [][]string{
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(deployedVersion)},
{"CHAOS", formatter.BoldDirtyDefault(deployedChaosVersion)},
{"NEW DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(toDeployVersion)},
{"CHAOS", formatter.BoldDirtyDefault(toDeployChaosVersion)},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"},
{"CURRENT VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toWriteVersion)},
}
overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows)
fmt.Println(overview)
for _, msg := range warnMessages {
log.Warn(msg)
}
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("deployment cancelled")
}
return nil
}
// UndeployOverview shows an undeployment overview
func UndeployOverview(
app appPkg.App,
deployedVersion,
deployedChaosVersion,
toWriteVersion 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
}
recipeName, exists := app.Env["RECIPE"]
if !exists {
recipeName = app.Env["TYPE"]
}
envVersion, err := recipe.GetEnvVersionRaw(recipeName)
if err != nil {
return err
}
if envVersion == "" {
envVersion = config.NO_VERSION_DEFAULT
}
rows := [][]string{
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"CURRENT DEPLOYMENT", "---"},
{"VERSION", formatter.BoldDirtyDefault(deployedVersion)},
{"CHAOS", formatter.BoldDirtyDefault(deployedChaosVersion)},
{fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"},
{"CURRENT VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW VERSION", formatter.BoldDirtyDefault(toWriteVersion)},
}
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 // PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format: // the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... " // "<service> <command> <arguments>|<service> <command> <arguments>|... "
func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { func PostCmds(cl *dockerClient.Client, app config.App, commands string) error {
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil { abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name)) return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name))
} }
return err return err
} }
@ -310,13 +102,13 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
if len(commandParts) > 2 { if len(commandParts) > 2 {
parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " ")) parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " "))
} }
log.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName) logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { if err := EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
return err return err
} }
serviceNames, err := appPkg.GetAppServiceNames(app.Name) serviceNames, err := config.GetAppServiceNames(app.Name)
if err != nil { if err != nil {
return err return err
} }
@ -332,35 +124,50 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name)) return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name))
} }
log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName) logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
requestTTY := true Tty = true
if err := RunCmdRemote( if err := RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
cl,
app,
requestTTY,
app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs, ""); err != nil {
return err return err
} }
} }
return nil return nil
} }
// SortVersionsDesc sorts versions in descending order. // DeployOverview shows a deployment overview
func SortVersionsDesc(versions []string) []string { func DeployOverview(app config.App, version, message string) error {
var tags []tagcmp.Tag tableCol := []string{"server", "recipe", "config", "domain", "version"}
table := formatter.CreateTable(tableCol)
for _, v := range versions { deployConfig := "compose.yml"
parsed, _ := tagcmp.Parse(v) // skips unsupported tags if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
tags = append(tags, parsed) deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
} }
sort.Sort(tagcmp.ByTagDesc(tags)) server := app.Server
if app.Server == "default" {
var desc []string server = "local"
for _, t := range tags {
desc = append(desc, t.String())
} }
return desc table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
table.Render()
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{
Message: message,
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatal("exiting as requested")
}
return nil
} }

View File

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

View File

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

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

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

10
cli/internal/list.go Normal file
View File

@ -0,0 +1,10 @@
package internal
// ReverseStringList reverses a list of a strings. Roll on Go generics.
func ReverseStringList(strings []string) []string {
for i, j := 0, len(strings)-1; i < j; i, j = i+1, j-1 {
strings[i], strings[j] = strings[j], strings[i]
}
return strings
}

View File

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

View File

@ -1,28 +1,27 @@
package internal package internal
import ( import (
"errors"
"strings" "strings"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
// ValidateRecipe ensures the recipe arg is valid. // ValidateRecipe ensures the recipe arg is valid.
func ValidateRecipe(args []string, cmdName string) recipe.Recipe { func ValidateRecipe(c *cli.Context) recipe.Recipe {
var recipeName string recipeName := c.Args().First()
if len(args) > 0 {
recipeName = args[0]
}
if recipeName == "" && !NoInput { if recipeName == "" && !NoInput {
var recipes []string var recipes []string
catl, err := recipe.ReadRecipeCatalogue(Offline) catl, err := recipe.ReadRecipeCatalogue(Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
knownRecipes := make(map[string]bool) knownRecipes := make(map[string]bool)
@ -32,7 +31,7 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
localRecipes, err := recipe.GetRecipesLocal() localRecipes, err := recipe.GetRecipesLocal()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, recipeLocal := range localRecipes { for _, recipeLocal := range localRecipes {
@ -50,63 +49,55 @@ func ValidateRecipe(args []string, cmdName string) recipe.Recipe {
Options: recipes, Options: recipes,
} }
if err := survey.AskOne(prompt, &recipeName); err != nil { if err := survey.AskOne(prompt, &recipeName); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
if recipeName == "" { if recipeName == "" {
log.Fatal("no recipe name provided") ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
} }
chosenRecipe := recipe.Get(recipeName) chosenRecipe, err := recipe.Get(recipeName, Offline)
err := chosenRecipe.EnsureExists()
if err != nil { if err != nil {
log.Fatal(err) if c.Command.Name == "generate" {
}
_, err = chosenRecipe.GetComposeConfig(nil)
if err != nil {
if cmdName == "generate" {
if strings.Contains(err.Error(), "missing a compose") { if strings.Contains(err.Error(), "missing a compose") {
log.Fatal(err) logrus.Fatal(err)
} }
log.Warn(err) logrus.Warn(err)
} else { } else {
if strings.Contains(err.Error(), "template_driver is not allowed") { if strings.Contains(err.Error(), "template_driver is not allowed") {
log.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName) logrus.Warnf("ensure %s recipe compose.* files include \"version: '3.8'\"", recipeName)
} }
log.Fatalf("unable to validate recipe: %s", err) logrus.Fatalf("unable to validate recipe: %s", err)
} }
} }
log.Debugf("validated %s as recipe argument", recipeName) logrus.Debugf("validated %s as recipe argument", recipeName)
return chosenRecipe return chosenRecipe
} }
// ValidateApp ensures the app name arg is valid. // ValidateApp ensures the app name arg is valid.
func ValidateApp(args []string) app.App { func ValidateApp(c *cli.Context) config.App {
if len(args) == 0 { appName := c.Args().First()
log.Fatal("no app provided")
}
appName := args[0] if appName == "" {
ShowSubcommandHelpAndError(c, errors.New("no app provided"))
}
app, err := app.Get(appName) app, err := app.Get(appName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
log.Debugf("validated %s as app argument", appName) logrus.Debugf("validated %s as app argument", appName)
return app return app
} }
// ValidateDomain ensures the domain name arg is valid. // ValidateDomain ensures the domain name arg is valid.
func ValidateDomain(args []string) string { func ValidateDomain(c *cli.Context) string {
var domainName string domainName := c.Args().First()
if len(args) > 0 {
domainName = args[0]
}
if domainName == "" && !NoInput { if domainName == "" && !NoInput {
prompt := &survey.Input{ prompt := &survey.Input{
@ -114,29 +105,40 @@ func ValidateDomain(args []string) string {
Default: "example.com", Default: "example.com",
} }
if err := survey.AskOne(prompt, &domainName); err != nil { if err := survey.AskOne(prompt, &domainName); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
if domainName == "" { if domainName == "" {
log.Fatal("no domain provided") ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
} }
log.Debugf("validated %s as domain argument", domainName) logrus.Debugf("validated %s as domain argument", domainName)
return domainName return domainName
} }
// ValidateServer ensures the server name arg is valid. // ValidateSubCmdFlags ensures flag order conforms to correct order
func ValidateServer(args []string) string { func ValidateSubCmdFlags(c *cli.Context) bool {
var serverName string for argIdx, arg := range c.Args() {
if len(args) > 0 { if !strings.HasPrefix(arg, "--") {
serverName = args[0] for _, flag := range c.Args()[argIdx:] {
if strings.HasPrefix(flag, "--") {
return false
}
}
}
} }
return true
}
// ValidateServer ensures the server name arg is valid.
func ValidateServer(c *cli.Context) string {
serverName := c.Args().First()
serverNames, err := config.ReadServerNames() serverNames, err := config.ReadServerNames()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if serverName == "" && !NoInput { if serverName == "" && !NoInput {
@ -145,7 +147,7 @@ func ValidateServer(args []string) string {
Options: serverNames, Options: serverNames,
} }
if err := survey.AskOne(prompt, &serverName); err != nil { if err := survey.AskOne(prompt, &serverName); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
@ -157,14 +159,14 @@ func ValidateServer(args []string) string {
} }
if serverName == "" { if serverName == "" {
log.Fatal("no server provided") ShowSubcommandHelpAndError(c, errors.New("no server provided"))
} }
if !matched { if !matched {
log.Fatal("server doesn't exist?") ShowSubcommandHelpAndError(c, errors.New("server doesn't exist?"))
} }
log.Debugf("validated %s as server argument", serverName) logrus.Debugf("validated %s as server argument", serverName)
return serverName return serverName
} }

View File

@ -1,29 +1,40 @@
package recipe package recipe
import ( import (
"path"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/urfave/cli"
) )
var RecipeDiffCommand = &cobra.Command{ var recipeDiffCommand = cli.Command{
Use: "diff <recipe> [flags]", Name: "diff",
Aliases: []string{"d"}, Usage: "Show unstaged changes in recipe config",
Short: "Show unstaged changes in recipe config", Description: "Due to limitations in our underlying Git dependency, this command requires /usr/bin/git.",
Long: "This command requires /usr/bin/git.", Aliases: []string{"d"},
Args: cobra.MinimumNArgs(1), ArgsUsage: "<recipe>",
ValidArgsFunction: func( Flags: []cli.Flag{
cmd *cobra.Command, internal.DebugFlag,
args []string, internal.NoInputFlag,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
r := internal.ValidateRecipe(args, cmd.Name()) BashComplete: autocomplete.RecipeNameComplete,
if err := gitPkg.DiffUnstaged(r.Dir); err != nil { Action: func(c *cli.Context) error {
log.Fatal(err) recipeName := c.Args().First()
if recipeName != "" {
internal.ValidateRecipe(c)
} }
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
logrus.Fatal(err)
}
return nil
}, },
} }

View File

@ -4,71 +4,47 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var RecipeFetchCommand = &cobra.Command{ var recipeFetchCommand = cli.Command{
Use: "fetch [recipe | --all] [flags]", Name: "fetch",
Aliases: []string{"f"}, Usage: "Fetch recipe(s)",
Short: "Clone recipe(s) locally", Aliases: []string{"f"},
Args: cobra.RangeArgs(0, 1), ArgsUsage: "[<recipe>]",
ValidArgsFunction: func( Description: "Retrieves all recipes if no <recipe> argument is passed",
cmd *cobra.Command, Flags: []cli.Flag{
args []string, internal.DebugFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.NoInputFlag,
return autocomplete.RecipeNameComplete() internal.OfflineFlag,
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
var recipeName string BashComplete: autocomplete.RecipeNameComplete,
if len(args) > 0 { Action: func(c *cli.Context) error {
recipeName = args[0] recipeName := c.Args().First()
}
if recipeName == "" && !fetchAllRecipes {
log.Fatal("missing [recipe] or --all/-a")
}
if recipeName != "" && fetchAllRecipes {
log.Fatal("cannot use [recipe] and --all/-a together")
}
ensureCtx := internal.GetEnsureContext()
if recipeName != "" { if recipeName != "" {
r := internal.ValidateRecipe(args, cmd.Name()) internal.ValidateRecipe(c)
if err := r.Ensure(ensureCtx); err != nil { if err := recipe.Ensure(recipeName); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return return nil
} }
catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline) catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...") catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
for recipeName := range catalogue { for recipeName := range catalogue {
r := recipe.Get(recipeName) if err := recipe.Ensure(recipeName); err != nil {
if err := r.Ensure(ensureCtx); err != nil { logrus.Error(err)
log.Error(err)
} }
catlBar.Add(1) catlBar.Add(1)
} }
return nil
}, },
} }
var (
fetchAllRecipes bool
)
func init() {
RecipeFetchCommand.Flags().BoolVarP(
&fetchAllRecipes,
"all",
"a",
false,
"fetch all recipes",
)
}

View File

@ -1,55 +1,63 @@
package recipe package recipe
import ( import (
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var RecipeLintCommand = &cobra.Command{ var recipeLintCommand = cli.Command{
Use: "lint <recipe> [flags]", Name: "lint",
Short: "Lint a recipe", Usage: "Lint a recipe",
Aliases: []string{"l"}, Aliases: []string{"l"},
Args: cobra.MinimumNArgs(1), ArgsUsage: "<recipe>",
ValidArgsFunction: func( Flags: []cli.Flag{
cmd *cobra.Command, internal.DebugFlag,
args []string, internal.OnlyErrorFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.OfflineFlag,
return autocomplete.RecipeNameComplete() internal.NoInputFlag,
internal.ChaosFlag,
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
recipe := internal.ValidateRecipe(args, cmd.Name()) BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil { if err := recipePkg.EnsureExists(recipe.Name); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
headers := []string{ if !internal.Chaos {
"ref", if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
"rule", logrus.Fatal(err)
"severity", }
"satisfied",
"skipped", if !internal.Offline {
"resolve", if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
} }
table, err := formatter.CreateTable() tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
if err != nil { table := formatter.CreateTable(tableCol)
log.Fatal(err)
}
table.Headers(headers...)
hasError := false hasError := false
var rows [][]string bar := formatter.CreateProgressbar(-1, "running recipe lint rules...")
var warnMessages []string
for level := range lint.LintRules { for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] { for _, rule := range lint.LintRules[level] {
if onlyError && rule.Level != "error" { if internal.OnlyErrors && rule.Level != "error" {
log.Debugf("skipping %s, does not have level \"error\"", rule.Ref) logrus.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
continue continue
} }
@ -67,7 +75,7 @@ var RecipeLintCommand = &cobra.Command{
if !skipped { if !skipped {
ok, err := rule.Function(recipe) ok, err := rule.Function(recipe)
if err != nil { if err != nil {
warnMessages = append(warnMessages, err.Error()) logrus.Warn(err)
} }
if !ok && rule.Level == "error" { if !ok && rule.Level == "error" {
@ -87,54 +95,28 @@ var RecipeLintCommand = &cobra.Command{
} }
} }
row := []string{ table.Append([]string{
rule.Ref, rule.Ref,
rule.Description, rule.Description,
rule.Level, rule.Level,
satisfiedOutput, satisfiedOutput,
skippedOutput, skippedOutput,
rule.HowToResolve, rule.HowToResolve,
} })
rows = append(rows, row) bar.Add(1)
table.Row(row...)
} }
} }
if len(rows) > 0 { if table.NumLines() > 0 {
if err := formatter.PrintTable(table); err != nil { fmt.Println()
log.Fatal(err) table.Render()
}
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
if hasError {
log.Warnf("critical errors present in %s config", recipe.Name)
}
} }
if hasError {
logrus.Warn("watch out, some critical errors are present in your recipe config")
}
return nil
}, },
} }
var (
onlyError bool
)
func init() {
RecipeLintCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
RecipeLintCommand.Flags().BoolVarP(
&onlyError,
"error",
"e",
false,
"only show errors",
)
}

View File

@ -8,46 +8,45 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var RecipeListCommand = &cobra.Command{ var pattern string
Use: "list", var patternFlag = &cli.StringFlag{
Short: "List recipes", Name: "pattern, p",
Value: "",
Usage: "Simple string to filter recipes",
Destination: &pattern,
}
var recipeListCommand = cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Args: cobra.NoArgs, Flags: []cli.Flag{
Run: func(cmd *cobra.Command, args []string) { internal.DebugFlag,
internal.MachineReadableFlag,
patternFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline) catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err.Error())
} }
recipes := catl.Flatten() recipes := catl.Flatten()
sort.Sort(recipe.ByRecipeName(recipes)) sort.Sort(recipe.ByRecipeName(recipes))
table, err := formatter.CreateTable() tableCol := []string{"name", "category", "status", "healthcheck", "backups", "email", "tests", "SSO"}
if err != nil { table := formatter.CreateTable(tableCol)
log.Fatal(err)
}
headers := []string{ len := 0
"name",
"category",
"status",
"healthcheck",
"backups",
"email",
"tests",
"SSO",
}
table.Headers(headers...)
var rows [][]string
for _, recipe := range recipes { for _, recipe := range recipes {
row := []string{ tableRow := []string{
recipe.Name, recipe.Name,
recipe.Category, recipe.Category,
strconv.Itoa(recipe.Features.Status), strconv.Itoa(recipe.Features.Status),
@ -60,50 +59,25 @@ var RecipeListCommand = &cobra.Command{
if pattern != "" { if pattern != "" {
if strings.Contains(recipe.Name, pattern) { if strings.Contains(recipe.Name, pattern) {
table.Row(row...) table.Append(tableRow)
rows = append(rows, row) len++
} }
} else { } else {
table.Row(row...) table.Append(tableRow)
rows = append(rows, row) len++
} }
} }
if len(rows) > 0 { if table.NumLines() > 0 {
if internal.MachineReadable { if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows) table.SetCaption(false, "")
if err != nil { table.JSONRender()
log.Fatal("unable to render to JSON: %s", err) } else {
} table.SetCaption(true, fmt.Sprintf("total recipes: %v", len))
fmt.Println(out) table.Render()
return
}
if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
} }
} }
return nil
}, },
} }
var (
pattern string
)
func init() {
RecipeListCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
RecipeListCommand.Flags().StringVarP(
&pattern,
"pattern",
"p",
"",
"filter by recipe",
)
}

View File

@ -2,17 +2,18 @@ package recipe
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path" "path"
"text/template" "text/template"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log" "github.com/sirupsen/logrus"
"coopcloud.tech/abra/pkg/recipe" "github.com/urfave/cli"
"github.com/spf13/cobra"
) )
// recipeMetadata is the recipe metadata for the README.md // recipeMetadata is the recipe metadata for the README.md
@ -29,61 +30,97 @@ type recipeMetadata struct {
SSO string SSO string
} }
var RecipeNewCommand = &cobra.Command{ var recipeNewCommand = cli.Command{
Use: "new <recipe> [flags]", Name: "new",
Aliases: []string{"n"}, Aliases: []string{"n"},
Short: "Create a new recipe", Flags: []cli.Flag{
Long: `A community managed recipe template is used.`, internal.DebugFlag,
Args: cobra.ExactArgs(1), internal.NoInputFlag,
ValidArgsFunction: func( internal.OfflineFlag,
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
recipeName := args[0] Usage: "Create a new recipe",
ArgsUsage: "<recipe>",
Description: `
Create a new recipe.
r := recipe.Get(recipeName) Abra uses the built-in example repository which is available here:
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
log.Fatalf("%s recipe directory already exists?", r.Dir) https://git.coopcloud.tech/coop-cloud/example
Files within the example repository make use of the Golang templating system
which Abra uses to inject values into the generated recipe folder (e.g. name of
recipe and domain in the sample environment config).
`,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
directory := path.Join(config.RECIPES_DIR, recipeName)
if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("%s recipe directory already exists?", directory)
} }
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL) url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(r.Dir, url); err != nil { if err := git.Clone(directory, url); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
gitRepo := path.Join(r.Dir, ".git") gitRepo := path.Join(config.RECIPES_DIR, recipeName, ".git")
if err := os.RemoveAll(gitRepo); err != nil { if err := os.RemoveAll(gitRepo); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
log.Debugf("removed .git repo in %s", gitRepo) logrus.Debugf("removed example git repo in %s", gitRepo)
meta := newRecipeMeta(recipeName) meta := newRecipeMeta(recipeName)
for _, path := range []string{r.ReadmePath, r.SampleEnvPath} { toParse := []string{
path.Join(config.RECIPES_DIR, recipeName, "README.md"),
path.Join(config.RECIPES_DIR, recipeName, ".env.sample"),
}
for _, path := range toParse {
tpl, err := template.ParseFiles(path) tpl, err := template.ParseFiles(path)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
var templated bytes.Buffer var templated bytes.Buffer
if err := tpl.Execute(&templated, meta); err != nil { if err := tpl.Execute(&templated, meta); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil { if err := ioutil.WriteFile(path, templated.Bytes(), 0644); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil { newGitRepo := path.Join(config.RECIPES_DIR, recipeName)
log.Fatal(err) if err := git.Init(newGitRepo, true); err != nil {
logrus.Fatal(err)
} }
log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)) fmt.Print(fmt.Sprintf(`
log.Info("happy hacking 🎉") Your new %s recipe has been created in %s.
In order to share your recipe, you can upload it the git repository to:
https://git.coopcloud.tech/coop-cloud/%s
If you're not sure how to do that, come chat with us:
https://docs.coopcloud.tech/intro/contact
See "abra recipe -h" for additional recipe maintainer commands.
Happy Hacking!
`, recipeName, path.Join(config.RECIPES_DIR, recipeName), recipeName))
return nil
}, },
} }
@ -102,26 +139,3 @@ func newRecipeMeta(recipeName string) recipeMetadata {
SSO: "No", SSO: "No",
} }
} }
var (
gitName string
gitEmail string
)
func init() {
RecipeNewCommand.Flags().StringVarP(
&gitName,
"git-name",
"N",
"",
"Git (user) name to do commits with",
)
RecipeNewCommand.Flags().StringVarP(
&gitEmail,
"git-email",
"e",
"",
"Git email name to do commits with",
)
}

View File

@ -1,19 +1,36 @@
package recipe package recipe
import "github.com/spf13/cobra" import (
"github.com/urfave/cli"
)
// RecipeCommand defines all recipe related sub-commands. // RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cobra.Command{ var RecipeCommand = cli.Command{
Use: "recipe [cmd] [args] [flags]", Name: "recipe",
Aliases: []string{"r"}, Aliases: []string{"r"},
Short: "Manage recipes", Usage: "Manage recipes",
Long: `A recipe is a blueprint for an app. ArgsUsage: "<recipe>",
Description: `
It is a bunch of config files which describe how to deploy and maintain an app. A recipe is a blueprint for an app. It is a bunch of config files which
Recipes are maintained by the Co-op Cloud community and you can use Abra to describe how to deploy and maintain an app. Recipes are maintained by the Co-op
read them, deploy them and create apps for you. Cloud community and you can use Abra to read them, deploy them and create apps
for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely sure the recipe is in good working order and the config upgraded in a timely
manner.`, manner. Abra supports convenient automation for recipe maintainenace, see the
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
`,
Subcommands: []cli.Command{
recipeFetchCommand,
recipeLintCommand,
recipeListCommand,
recipeNewCommand,
recipeReleaseCommand,
recipeSyncCommand,
recipeUpgradeCommand,
recipeVersionCommand,
recipeResetCommand,
recipeDiffCommand,
},
} }

View File

@ -10,25 +10,27 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var RecipeReleaseCommand = &cobra.Command{ var recipeReleaseCommand = cli.Command{
Use: "release <recipe> [version] [flags]", Name: "release",
Aliases: []string{"rl"}, Aliases: []string{"rl"},
Short: "Release a new recipe version", Usage: "Release a new recipe version",
Long: `Create a new version of a recipe. ArgsUsage: "<recipe> [<version>]",
Description: `
These versions are then published on the Co-op Cloud recipe catalogue. These Create a new version of a recipe. These versions are then published on the
versions take the following form: Co-op Cloud recipe catalogue. These versions take the following form:
a.b.c+x.y.z a.b.c+x.y.z
@ -42,104 +44,99 @@ recipe updates are properly communicated. I.e. developers of an app might
publish a minor version but that might lead to changes in the recipe which are publish a minor version but that might lead to changes in the recipe which are
major and therefore require intervention while doing the upgrade work. major and therefore require intervention while doing the upgrade work.
Publish your new release to git.coopcloud.tech with "--publish/-p". This Publish your new release to git.coopcloud.tech with "-p/--publish". This
requires that you have permission to git push to these repositories and have requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.`, your SSH keys configured on your account.
Args: cobra.RangeArgs(1, 2), `,
ValidArgsFunction: func( Flags: []cli.Flag{
cmd *cobra.Command, internal.DebugFlag,
args []string, internal.NoInputFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.DryFlag,
switch l := len(args); l { internal.MajorFlag,
case 0: internal.MinorFlag,
return autocomplete.RecipeNameComplete() internal.PatchFlag,
case 1: internal.PublishFlag,
return autocomplete.RecipeVersionComplete(args[0]) internal.OfflineFlag,
default:
return nil, cobra.ShellCompDirectiveDefault
}
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
recipe := internal.ValidateRecipe(args, cmd.Name()) BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
imagesTmp, err := getImageVersions(recipe) imagesTmp, err := getImageVersions(recipe)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
mainApp, err := internal.GetMainAppImage(recipe) mainApp, err := internal.GetMainAppImage(recipe)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
mainAppVersion := imagesTmp[mainApp] mainAppVersion := imagesTmp[mainApp]
if mainAppVersion == "" { if mainAppVersion == "" {
log.Fatalf("main app service version for %s is empty?", recipe.Name) logrus.Fatalf("main app service version for %s is empty?", recipe.Name)
}
var tagString string
if len(args) == 2 {
tagString = args[1]
} }
tagString := c.Args().Get(1)
if tagString != "" { if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil { if _, err := tagcmp.Parse(tagString); err != nil {
log.Fatalf("cannot parse %s, invalid tag specified?", tagString) logrus.Fatalf("cannot parse %s, invalid tag specified?", tagString)
} }
} }
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" { if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
log.Fatal("cannot specify tag and bump type at the same time") logrus.Fatal("cannot specify tag and bump type at the same time")
} }
if tagString != "" { if tagString != "" {
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil { if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
tags, err := recipe.Tags() tags, err := recipe.Tags()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) { if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
var err error var err error
tagString, err = getLabelVersion(recipe, false) tagString, err = getLabelVersion(recipe, false)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
isClean, err := gitPkg.IsClean(recipe.Dir) isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
if len(tags) > 0 { if len(tags) > 0 {
log.Warnf("previous git tags detected, assuming this is a new semver release") logrus.Warnf("previous git tags detected, assuming this is a new semver release")
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil { if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
log.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name) logrus.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil { if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanUpErr := cleanUpTag(recipe, tagString); err != nil { if cleanUpErr := cleanUpTag(tagString, recipe.Name); err != nil {
log.Fatal(cleanUpErr) logrus.Fatal(cleanUpErr)
} }
log.Fatal(err) logrus.Fatal(err)
} }
} }
return return nil
}, },
} }
@ -147,12 +144,8 @@ your SSH keys configured on your account.`,
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
services := make(map[string]string) services := make(map[string]string)
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return nil, err
}
missingTag := false missingTag := false
for _, service := range config.Services { for _, service := range recipe.Config.Services {
if service.Image == "" { if service.Image == "" {
continue continue
} }
@ -191,7 +184,8 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error { func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err error var err error
repo, err := git.PlainOpen(recipe.Dir) directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
return err return err
} }
@ -216,19 +210,19 @@ func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string
} }
if err := addReleaseNotes(recipe, tagString); err != nil { if err := addReleaseNotes(recipe, tagString); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := commitRelease(recipe, tagString); err != nil { if err := commitRelease(recipe, tagString); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := tagRelease(tagString, repo); err != nil { if err := tagRelease(tagString, repo); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := pushRelease(recipe, tagString); err != nil { if err := pushRelease(recipe, tagString); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil return nil
@ -252,14 +246,8 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
// addReleaseNotes checks if the release/next release note exists and moves the // addReleaseNotes checks if the release/next release note exists and moves the
// file to release/<tag>. // file to release/<tag>.
func addReleaseNotes(recipe recipe.Recipe, tag string) error { func addReleaseNotes(recipe recipe.Recipe, tag string) error {
releaseDir := path.Join(recipe.Dir, "release") repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
if _, err := os.Stat(releaseDir); errors.Is(err, os.ErrNotExist) { tagReleaseNotePath := path.Join(repoPath, "release", tag)
if err := os.Mkdir(releaseDir, 0755); err != nil {
return err
}
}
tagReleaseNotePath := path.Join(releaseDir, tag)
if _, err := os.Stat(tagReleaseNotePath); err == nil { if _, err := os.Stat(tagReleaseNotePath); err == nil {
// Release note for current tag already exist exists. // Release note for current tag already exist exists.
return nil return nil
@ -267,55 +255,49 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return err return err
} }
var addNextAsReleaseNotes bool nextReleaseNotePath := path.Join(repoPath, "release", "next")
nextReleaseNotePath := path.Join(releaseDir, "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil { if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag> // release/next note exists. Move it to release/<tag>
if internal.Dry { if internal.Dry {
log.Debugf("dry run: move release note from 'next' to %s", tag) logrus.Debugf("dry run: move release note from 'next' to %s", tag)
return nil return nil
} }
if !internal.NoInput { if !internal.NoInput {
prompt := &survey.Confirm{ prompt := &survey.Input{
Message: "Use release note in release/next?", Message: "Use release note in release/next?",
} }
var addReleaseNote bool
if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil { if err := survey.AskOne(prompt, &addReleaseNote); err != nil {
return err return err
} }
if !addReleaseNote {
if !addNextAsReleaseNotes {
return nil return nil
} }
} }
err := os.Rename(nextReleaseNotePath, tagReleaseNotePath)
if err := os.Rename(nextReleaseNotePath, tagReleaseNotePath); err != nil { if err != nil {
return err return err
} }
err = gitPkg.Add(repoPath, path.Join("release", "next"), internal.Dry)
if err := gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry); err != nil { if err != nil {
return err return err
} }
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry)
if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil { if err != nil {
return err return err
} }
} else if !errors.Is(err, os.ErrNotExist) { } else if !errors.Is(err, os.ErrNotExist) {
return err return err
} }
// NOTE(d1): No release note exists for the current release. Or, we've // No release note exists for the current release.
// already used release/next as the release note if internal.NoInput {
if internal.NoInput || addNextAsReleaseNotes {
return nil return nil
} }
prompt := &survey.Input{ prompt := &survey.Input{
Message: "Release Note (leave empty for no release note)", Message: "Release Note (leave empty for no release note)",
} }
var releaseNote string var releaseNote string
if err := survey.AskOne(prompt, &releaseNote); err != nil { if err := survey.AskOne(prompt, &releaseNote); err != nil {
return err return err
@ -325,11 +307,12 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return nil return nil
} }
if err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644); err != nil { err := os.WriteFile(tagReleaseNotePath, []byte(releaseNote), 0o644)
if err != nil {
return err return err
} }
err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry)
if err := gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry); err != nil { if err != nil {
return err return err
} }
@ -338,23 +321,24 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
func commitRelease(recipe recipe.Recipe, tag string) error { func commitRelease(recipe recipe.Recipe, tag string) error {
if internal.Dry { if internal.Dry {
log.Debugf("dry run: no changes committed") logrus.Debugf("dry run: no changes committed")
return nil return nil
} }
isClean, err := gitPkg.IsClean(recipe.Dir) isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil { if err != nil {
return err return err
} }
if isClean { if isClean {
if !internal.Dry { if !internal.Dry {
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir) return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir())
} }
} }
msg := fmt.Sprintf("chore: publish %s release", tag) msg := fmt.Sprintf("chore: publish %s release", tag)
if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil { repoPath := path.Join(config.RECIPES_DIR, recipe.Name)
if err := gitPkg.Commit(repoPath, msg, internal.Dry); err != nil {
return err return err
} }
@ -363,7 +347,7 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
func tagRelease(tagString string, repo *git.Repository) error { func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry { if internal.Dry {
log.Debugf("dry run: no git tag created (%s)", tagString) logrus.Debugf("dry run: no git tag created (%s)", tagString)
return nil return nil
} }
@ -383,42 +367,43 @@ func tagRelease(tagString string, repo *git.Repository) error {
} }
hash := formatter.SmallSHA(head.Hash().String()) hash := formatter.SmallSHA(head.Hash().String())
log.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash)) logrus.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash))
return nil return nil
} }
func pushRelease(recipe recipe.Recipe, tagString string) error { func pushRelease(recipe recipe.Recipe, tagString string) error {
if internal.Dry { if internal.Dry {
log.Info("dry run: no changes published") logrus.Info("dry run: no changes published")
return nil return nil
} }
if !publish && !internal.NoInput { if !internal.Publish && !internal.NoInput {
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: "publish new release?", Message: "publish new release?",
} }
if err := survey.AskOne(prompt, &publish); err != nil { if err := survey.AskOne(prompt, &internal.Publish); err != nil {
return err return err
} }
} }
if publish { if internal.Publish {
if err := recipe.Push(internal.Dry); err != nil { if err := recipe.Push(internal.Dry); err != nil {
return err return err
} }
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString) url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString)
log.Infof("new release published: %s", url) logrus.Infof("new release published: %s", url)
} else { } else {
log.Info("no -p/--publish passed, not publishing") logrus.Info("no -p/--publish passed, not publishing")
} }
return nil return nil
} }
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error { func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
repo, err := git.PlainOpen(recipe.Dir) directory := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
return err return err
} }
@ -483,7 +468,7 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
} }
if lastGitTag.String() == tagString { if lastGitTag.String() == tagString {
log.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString) logrus.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString)
} }
if !internal.NoInput { if !internal.NoInput {
@ -493,36 +478,37 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
var ok bool var ok bool
if err := survey.AskOne(prompt, &ok); err != nil { if err := survey.AskOne(prompt, &ok); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !ok { if !ok {
log.Fatal("exiting as requested") logrus.Fatal("exiting as requested")
} }
} }
if err := addReleaseNotes(recipe, tagString); err != nil { if err := addReleaseNotes(recipe, tagString); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := commitRelease(recipe, tagString); err != nil { if err := commitRelease(recipe, tagString); err != nil {
log.Fatalf("failed to commit changes: %s", err.Error()) logrus.Fatalf("failed to commit changes: %s", err.Error())
} }
if err := tagRelease(tagString, repo); err != nil { if err := tagRelease(tagString, repo); err != nil {
log.Fatalf("failed to tag release: %s", err.Error()) logrus.Fatalf("failed to tag release: %s", err.Error())
} }
if err := pushRelease(recipe, tagString); err != nil { if err := pushRelease(recipe, tagString); err != nil {
log.Fatalf("failed to publish new release: %s", err.Error()) logrus.Fatalf("failed to publish new release: %s", err.Error())
} }
return nil return nil
} }
// cleanUpTag removes a freshly created tag // cleanUpTag removes a freshly created tag
func cleanUpTag(recipe recipe.Recipe, tag string) error { func cleanUpTag(tag, recipeName string) error {
repo, err := git.PlainOpen(recipe.Dir) directory := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
return err return err
} }
@ -533,22 +519,22 @@ func cleanUpTag(recipe recipe.Recipe, tag string) error {
} }
} }
log.Debugf("removed freshly created tag %s", tag) logrus.Debugf("removed freshly created tag %s", tag)
return nil return nil
} }
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) { func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
initTag, err := recipe.GetVersionLabelLocal() initTag, err := recipePkg.GetVersionLabelLocal(recipe)
if err != nil { if err != nil {
return "", err return "", err
} }
if initTag == "" { if initTag == "" {
log.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name) logrus.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name)
} }
log.Warnf("discovered %s as currently synced recipe label", initTag) logrus.Warnf("discovered %s as currently synced recipe label", initTag)
if prompt && !internal.NoInput { if prompt && !internal.NoInput {
var response bool var response bool
@ -564,50 +550,3 @@ func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
return initTag, nil return initTag, nil
} }
var (
publish bool
)
func init() {
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Dry,
"dry-run",
"r",
false,
"report changes that would be made",
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Major,
"major",
"x",
false,
"increase the major part of the version",
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Minor,
"minor",
"y",
false,
"increase the minor part of the version",
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Patch,
"patch",
"z",
false,
"increase the patch part of the version",
)
RecipeReleaseCommand.Flags().BoolVarP(
&publish,
"publish",
"p",
false,
"publish changes to git.coopcloud.tech",
)
}

View File

@ -1,46 +1,56 @@
package recipe package recipe
import ( import (
"path"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var RecipeResetCommand = &cobra.Command{ var recipeResetCommand = cli.Command{
Use: "reset <recipe> [flags]", Name: "reset",
Aliases: []string{"rs"}, Usage: "Remove all unstaged changes from recipe config",
Short: "Remove all unstaged changes from recipe config", Description: "WARNING, this will delete your changes. Be Careful.",
Long: "WARNING: this will delete your changes. Be Careful.", Aliases: []string{"rs"},
Args: cobra.ExactArgs(1), ArgsUsage: "<recipe>",
ValidArgsFunction: func( Flags: []cli.Flag{
cmd *cobra.Command, internal.DebugFlag,
args []string, internal.NoInputFlag,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
r := internal.ValidateRecipe(args, cmd.Name()) BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
repo, err := git.PlainOpen(r.Dir) if recipeName != "" {
internal.ValidateRecipe(c)
}
repoPath := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(repoPath)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
ref, err := repo.Head() ref, err := repo.Head()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
worktree, err := repo.Worktree() worktree, err := repo.Worktree()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
opts := &git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset} opts := &git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset}
if err := worktree.Reset(opts); err != nil { if err := worktree.Reset(opts); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return nil
}, },
} }

View File

@ -2,75 +2,71 @@ package recipe
import ( import (
"fmt" "fmt"
"path"
"strconv" "strconv"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var RecipeSyncCommand = &cobra.Command{ var recipeSyncCommand = cli.Command{
Use: "sync <recipe> [version] [flags]", Name: "sync",
Aliases: []string{"s"}, Aliases: []string{"s"},
Short: "Sync recipe version label", Usage: "Sync recipe version label",
Long: `Generate labels for the main recipe service. ArgsUsage: "<recipe> [<version>]",
Flags: []cli.Flag{
By convention, the service named "app" using the following format: internal.DebugFlag,
internal.NoInputFlag,
internal.DryFlag,
internal.MajorFlag,
internal.MinorFlag,
internal.PatchFlag,
},
Before: internal.SubCommandBefore,
Description: `
Generate labels for the main recipe service (i.e. by convention, the service
named "app") which corresponds to the following format:
coop-cloud.${STACK_NAME}.version=<version> coop-cloud.${STACK_NAME}.version=<version>
Where [version] can be specifed on the command-line or Abra can attempt to Where <version> can be specifed on the command-line or Abra can attempt to
auto-generate it for you. The <recipe> configuration will be updated on the auto-generate it for you. The <recipe> configuration will be updated on the
local file system.`, local file system.
Args: cobra.RangeArgs(1, 2), `,
ValidArgsFunction: func( BashComplete: autocomplete.RecipeNameComplete,
cmd *cobra.Command, Action: func(c *cli.Context) error {
args []string, recipe := internal.ValidateRecipe(c)
toComplete string) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l {
case 0:
return autocomplete.RecipeNameComplete()
case 1:
return autocomplete.RecipeVersionComplete(args[0])
default:
return nil, cobra.ShellCompDirectiveError
}
},
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
mainApp, err := internal.GetMainAppImage(recipe) mainApp, err := internal.GetMainAppImage(recipe)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
imagesTmp, err := getImageVersions(recipe) imagesTmp, err := getImageVersions(recipe)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
mainAppVersion := imagesTmp[mainApp] mainAppVersion := imagesTmp[mainApp]
tags, err := recipe.Tags() tags, err := recipe.Tags()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
var nextTag string
if len(args) == 2 {
nextTag = args[1]
} }
nextTag := c.Args().Get(1)
if len(tags) == 0 && nextTag == "" { if len(tags) == 0 && nextTag == "" {
log.Warnf("no git tags found for %s", recipe.Name) logrus.Warnf("no git tags found for %s", recipe.Name)
if internal.NoInput { if internal.NoInput {
log.Fatalf("unable to continue, input required for initial version") logrus.Fatalf("unable to continue, input required for initial version")
} }
fmt.Println(fmt.Sprintf(` fmt.Println(fmt.Sprintf(`
The following options are two types of initial semantic version that you can The following options are two types of initial semantic version that you can
@ -97,7 +93,7 @@ likely to change.
} }
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil { if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion) nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
@ -106,26 +102,27 @@ likely to change.
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) { if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
latestRelease := tags[len(tags)-1] latestRelease := tags[len(tags)-1]
if err := internal.PromptBumpType("", latestRelease); err != nil { if err := internal.PromptBumpType("", latestRelease); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
if nextTag == "" { if nextTag == "" {
repo, err := git.PlainOpen(recipe.Dir) recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
repo, err := git.PlainOpen(recipeDir)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
var lastGitTag tagcmp.Tag var lastGitTag tagcmp.Tag
iter, err := repo.Tags() iter, err := repo.Tags()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := iter.ForEach(func(ref *plumbing.Reference) error { if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash()) obj, err := repo.TagObject(ref.Hash())
if err != nil { if err != nil {
log.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.") logrus.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.")
return err return err
} }
@ -142,7 +139,7 @@ likely to change.
return nil return nil
}); err != nil { }); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
// bumpType is used to decide what part of the tag should be incremented // bumpType is used to decide what part of the tag should be incremented
@ -150,7 +147,7 @@ likely to change.
if bumpType != 0 { if bumpType != 0 {
// a bitwise check if the number is a power of 2 // a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 { if (bumpType & (bumpType - 1)) != 0 {
log.Fatal("you can only use one version flag: --major, --minor or --patch") logrus.Fatal("you can only use one version flag: --major, --minor or --patch")
} }
} }
@ -159,14 +156,14 @@ likely to change.
if internal.Patch { if internal.Patch {
now, err := strconv.Atoi(newTag.Patch) now, err := strconv.Atoi(newTag.Patch)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = strconv.Itoa(now + 1) newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor { } else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor) now, err := strconv.Atoi(newTag.Minor)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = "0" newTag.Patch = "0"
@ -174,7 +171,7 @@ likely to change.
} else if internal.Major { } else if internal.Major {
now, err := strconv.Atoi(newTag.Major) now, err := strconv.Atoi(newTag.Major)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = "0" newTag.Patch = "0"
@ -184,67 +181,35 @@ likely to change.
} }
newTag.Metadata = mainAppVersion newTag.Metadata = mainAppVersion
log.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name) logrus.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
nextTag = newTag.String() nextTag = newTag.String()
} }
if _, err := tagcmp.Parse(nextTag); err != nil { if _, err := tagcmp.Parse(nextTag); err != nil {
log.Fatalf("invalid version %s specified", nextTag) logrus.Fatalf("invalid version %s specified", nextTag)
} }
mainService := "app" mainService := "app"
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag) label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if !internal.Dry { if !internal.Dry {
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil { if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} else { } else {
log.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name) logrus.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name)
} }
isClean, err := gitPkg.IsClean(recipe.Dir) isClean, err := gitPkg.IsClean(recipe.Dir())
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
return nil
}, },
} }
func init() {
RecipeSyncCommand.Flags().BoolVarP(
&internal.Dry,
"dry-run",
"r",
false,
"report changes that would be made",
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Major,
"major",
"x",
false,
"increase the major part of the version",
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Minor,
"minor",
"y",
false,
"increase the minor part of the version",
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Patch,
"patch",
"z",
false,
"increase the patch part of the version",
)
}

View File

@ -12,14 +12,15 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
type imgPin struct { type imgPin struct {
@ -27,8 +28,8 @@ type imgPin struct {
version tagcmp.Tag version tagcmp.Tag
} }
// anUpgrade represents a single service upgrade (as within a recipe), and the // anUpgrade represents a single service upgrade (as within a recipe), and the list of tags that it can be upgraded to,
// list of tags that it can be upgraded to, for serialization purposes. // for serialization purposes.
type anUpgrade struct { type anUpgrade struct {
Service string `json:"service"` Service string `json:"service"`
Image string `json:"image"` Image string `json:"image"`
@ -36,13 +37,14 @@ type anUpgrade struct {
UpgradeTags []string `json:"upgrades"` UpgradeTags []string `json:"upgrades"`
} }
var RecipeUpgradeCommand = &cobra.Command{ var recipeUpgradeCommand = cli.Command{
Use: "upgrade <recipe> [flags]", Name: "upgrade",
Aliases: []string{"u"}, Aliases: []string{"u"},
Short: "Upgrade recipe image tags", Usage: "Upgrade recipe image tags",
Long: `Upgrade a given <recipe> configuration. Description: `
Parse all image tags within the given <recipe> configuration and prompt with
It will update the relevant compose file tags on the local file system. more recent tags to upgrade to. It will update the relevant compose file tags
on the local file system.
Some image tags cannot be parsed because they do not follow some sort of Some image tags cannot be parsed because they do not follow some sort of
semver-like convention. In this case, all possible tags will be listed and it semver-like convention. In this case, all possible tags will be listed and it
@ -52,26 +54,46 @@ The command is interactive and will show a select input which allows you to
make a seclection. Use the "?" key to see more help on navigating this make a seclection. Use the "?" key to see more help on navigating this
interface. interface.
You may invoke this command in "wizard" mode and be prompted for input.`, You may invoke this command in "wizard" mode and be prompted for input:
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil { abra recipe upgrade
log.Fatal(err) `,
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PatchFlag,
internal.MinorFlag,
internal.MajorFlag,
internal.MachineReadableFlag,
internal.AllTagsFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
} }
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch) bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 { if bumpType != 0 {
// a bitwise check if the number is a power of 2 // a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 { if (bumpType & (bumpType - 1)) != 0 {
log.Fatal("you can only use one of: --major, --minor, --patch.") logrus.Fatal("you can only use one of: --major, --minor, --patch.")
} }
} }
@ -84,25 +106,26 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
// check for versions file and load pinned versions // check for versions file and load pinned versions
versionsPresent := false versionsPresent := false
versionsPath := path.Join(recipe.Dir, "versions") recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
servicePins := make(map[string]imgPin) versionsPath := path.Join(recipeDir, "versions")
var servicePins = make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil { if _, err := os.Stat(versionsPath); err == nil {
log.Debugf("found versions file for %s", recipe.Name) logrus.Debugf("found versions file for %s", recipe.Name)
file, err := os.Open(versionsPath) file, err := os.Open(versionsPath)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
splitLine := strings.Split(line, " ") splitLine := strings.Split(line, " ")
if splitLine[0] != "pin" || len(splitLine) != 3 { if splitLine[0] != "pin" || len(splitLine) != 3 {
log.Fatalf("malformed version pin specification: %s", line) logrus.Fatalf("malformed version pin specification: %s", line)
} }
pinSlice := strings.Split(splitLine[2], ":") pinSlice := strings.Split(splitLine[2], ":")
pinTag, err := tagcmp.Parse(pinSlice[1]) pinTag, err := tagcmp.Parse(pinSlice[1])
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
pin := imgPin{ pin := imgPin{
image: pinSlice[0], image: pinSlice[0],
@ -111,50 +134,45 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
servicePins[splitLine[1]] = pin servicePins[splitLine[1]] = pin
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
log.Error(err) logrus.Error(err)
} }
versionsPresent = true versionsPresent = true
} else { } else {
log.Debugf("did not find versions file for %s", recipe.Name) logrus.Debugf("did not find versions file for %s", recipe.Name)
} }
config, err := recipe.GetComposeConfig(nil) for _, service := range recipe.Config.Services {
if err != nil {
log.Fatal(err)
}
for _, service := range config.Services {
img, err := reference.ParseNormalizedNamed(service.Image) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
regVersions, err := client.GetRegistryTags(img) regVersions, err := client.GetRegistryTags(img)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
image := reference.Path(img) image := reference.Path(img)
log.Debugf("retrieved %s from remote registry for %s", regVersions, image) logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
image = formatter.StripTagMeta(image) image = formatter.StripTagMeta(image)
switch img.(type) { switch img.(type) {
case reference.NamedTagged: case reference.NamedTagged:
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
log.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag()) logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
} }
default: default:
log.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name) logrus.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name)
continue continue
} }
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag()) tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil { if err != nil {
log.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name) logrus.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)
continue continue
} }
log.Debugf("parsed %s for %s", tag, service.Name) logrus.Debugf("parsed %s for %s", tag, service.Name)
var compatible []tagcmp.Tag var compatible []tagcmp.Tag
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
@ -168,18 +186,18 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
} }
log.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name) logrus.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
sort.Sort(tagcmp.ByTagDesc(compatible)) sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && !allTags { if len(compatible) == 0 && !internal.AllTags {
log.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag)) logrus.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
continue // skip on to the next tag and don't update any compose files continue // skip on to the next tag and don't update any compose files
} }
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline) catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
compatibleStrings := []string{"skip"} compatibleStrings := []string{"skip"}
@ -195,7 +213,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
} }
log.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name) logrus.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name)
var upgradeTag string var upgradeTag string
_, ok := servicePins[service.Name] _, ok := servicePins[service.Name]
@ -212,13 +230,13 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
} }
if contains { if contains {
log.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString) logrus.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
} else { } else {
log.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString) logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
continue continue
} }
} else { } else {
log.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()) logrus.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
continue continue
} }
} else { } else {
@ -226,7 +244,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
for _, upTag := range compatible { for _, upTag := range compatible {
upElement, err := tag.UpgradeDelta(upTag) upElement, err := tag.UpgradeDelta(upTag)
if err != nil { if err != nil {
return return err
} }
delta := upElement.UpgradeType() delta := upElement.UpgradeType()
if delta <= bumpType { if delta <= bumpType {
@ -235,15 +253,15 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
} }
if upgradeTag == "" { if upgradeTag == "" {
log.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image) logrus.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image)
continue continue
} }
} else { } else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag) msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || allTags { if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags {
tag := img.(reference.NamedTagged).Tag() tag := img.(reference.NamedTagged).Tag()
if !allTags { if !internal.AllTags {
log.Warn(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag)) logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag))
} }
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag) msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{"skip"} compatibleStrings = []string{"skip"}
@ -281,7 +299,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
Options: compatibleStrings, Options: compatibleStrings,
} }
if err := survey.AskOne(prompt, &upgradeTag); err != nil { if err := survey.AskOne(prompt, &upgradeTag); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
} }
@ -289,14 +307,14 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
if upgradeTag != "skip" { if upgradeTag != "skip" {
ok, err := recipe.UpdateTag(image, upgradeTag) ok, err := recipe.UpdateTag(image, upgradeTag)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if ok { if ok {
log.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image) logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
} }
} else { } else {
if !internal.NoInput { if !internal.NoInput {
log.Warnf("not upgrading %s, skipping as requested", image) logrus.Warnf("not upgrading %s, skipping as requested", image)
} }
} }
} }
@ -305,77 +323,33 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
if internal.MachineReadable { if internal.MachineReadable {
jsonstring, err := json.Marshal(upgradeList) jsonstring, err := json.Marshal(upgradeList)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
fmt.Println(string(jsonstring)) fmt.Println(string(jsonstring))
return return nil
} }
for _, upgrade := range upgradeList { for _, upgrade := range upgradeList {
log.Infof("can upgrade service: %s, image: %s, tag: %s ::", upgrade.Service, upgrade.Image, upgrade.Tag) logrus.Infof("can upgrade service: %s, image: %s, tag: %s ::\n", upgrade.Service, upgrade.Image, upgrade.Tag)
for _, utag := range upgrade.UpgradeTags { for _, utag := range upgrade.UpgradeTags {
log.Infof(" %s", utag) logrus.Infof(" %s\n", utag)
} }
} }
} }
isClean, err := gitPkg.IsClean(recipe.Dir) isClean, err := gitPkg.IsClean(recipeDir)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
return nil
}, },
} }
var (
allTags bool
)
func init() {
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Major,
"major",
"x",
false,
"increase the major part of the version",
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Minor,
"minor",
"y",
false,
"increase the minor part of the version",
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Patch,
"patch",
"z",
false,
"increase the patch part of the version",
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
RecipeUpgradeCommand.Flags().BoolVarP(
&allTags,
"all-tags",
"a",
false,
"list all tags, not just upgrades",
)
}

View File

@ -7,129 +7,82 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var RecipeVersionCommand = &cobra.Command{ func sortServiceByName(versions [][]string) func(i, j int) bool {
Use: "versions <recipe> [flags]", return func(i, j int) bool {
Aliases: []string{"v"}, // NOTE(d1): corresponds to the `tableCol` definition below
Short: "List recipe versions", if versions[i][1] == "app" {
Args: cobra.ExactArgs(1), return true
ValidArgsFunction: func( }
cmd *cobra.Command, return versions[i][1] < versions[j][1]
args []string, }
toComplete string) ([]string, cobra.ShellCompDirective) { }
return autocomplete.RecipeNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string
recipe := internal.ValidateRecipe(args, cmd.Name()) var recipeVersionCommand = cli.Command{
Name: "versions",
Aliases: []string{"v"},
Usage: "List recipe versions",
ArgsUsage: "<recipe>",
Description: "Versions are read from the recipe catalogue.",
Flags: []cli.Flag{
internal.DebugFlag,
internal.OfflineFlag,
internal.NoInputFlag,
internal.MachineReadableFlag,
},
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
recipeMeta, ok := catl[recipe.Name] recipeMeta, ok := catl[recipe.Name]
if !ok { if !ok {
warnMessages = append(warnMessages, "retrieved versions from local recipe repository") logrus.Fatalf("%s is not published on the catalogue?", recipe.Name)
recipeVersions, warnMsg, err := recipe.GetRecipeVersions()
if err != nil {
warnMessages = append(warnMessages, err.Error())
}
if len(warnMsg) > 0 {
warnMessages = append(warnMessages, warnMsg...)
}
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
} }
if len(recipeMeta.Versions) == 0 { if len(recipeMeta.Versions) == 0 {
log.Fatalf("%s has no published versions?", recipe.Name) logrus.Fatalf("%s has no catalogue published versions?", recipe.Name)
} }
tableCols := []string{"version", "service", "image", "tag"}
aggregated_table := formatter.CreateTable(tableCols)
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- { for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
table, err := formatter.CreateTable() table := formatter.CreateTable(tableCols)
if err != nil {
log.Fatal(err)
}
table.Headers("SERVICE", "IMAGE", "TAG", "VERSION")
for version, meta := range recipeMeta.Versions[i] { for version, meta := range recipeMeta.Versions[i] {
var allRows [][]string var versions [][]string
var rows [][]string
for service, serviceMeta := range meta { for service, serviceMeta := range meta {
recipeVersion := version versions = append(versions, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
if service != "app" {
recipeVersion = ""
}
rows = append(rows, []string{
service,
serviceMeta.Image,
serviceMeta.Tag,
recipeVersion,
})
allRows = append(allRows, []string{
version,
service,
serviceMeta.Image,
serviceMeta.Tag,
recipeVersion,
})
} }
sort.Slice(rows, sortServiceByName(rows)) sort.Slice(versions, sortServiceByName(versions))
table.Rows(rows...) for _, version := range versions {
table.Append(version)
aggregated_table.Append(version)
}
if !internal.MachineReadable { if !internal.MachineReadable {
if err := formatter.PrintTable(table); err != nil { table.SetAutoMergeCellsByColumnIndex([]int{0})
log.Fatal(err) table.SetAlignment(tablewriter.ALIGN_LEFT)
} table.Render()
continue fmt.Println()
}
if internal.MachineReadable {
sort.Slice(allRows, sortServiceByName(allRows))
headers := []string{"VERSION", "SERVICE", "NAME", "TAG"}
out, err := formatter.ToJSON(headers, allRows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
continue
} }
} }
} }
if internal.MachineReadable {
if !internal.MachineReadable { aggregated_table.JSONRender()
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
} }
return nil
}, },
} }
func sortServiceByName(versions [][]string) func(i, j int) bool {
return func(i, j int) bool {
return versions[i][0] < versions[j][0]
}
}
func init() {
RecipeVersionCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
}

View File

@ -1,218 +0,0 @@
package cli
import (
"fmt"
"os"
"coopcloud.tech/abra/cli/app"
"coopcloud.tech/abra/cli/catalogue"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/cli/recipe"
"coopcloud.tech/abra/cli/server"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
charmLog "github.com/charmbracelet/log"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
func Run(version, commit string) {
rootCmd := &cobra.Command{
Use: "abra [cmd] [args] [flags]",
Short: "The Co-op Cloud command-line utility belt 🎩🐇",
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
ValidArgs: []string{
"app",
"autocomplete",
"catalogue",
"man",
"recipe",
"server",
"upgrade",
},
PersistentPreRun: func(cmd *cobra.Command, args []string) {
paths := []string{
config.ABRA_DIR,
config.SERVERS_DIR,
config.RECIPES_DIR,
config.VENDOR_DIR, // TODO(d1): remove > 0.9.x
config.BACKUP_DIR, // TODO(d1): remove > 0.9.x
}
for _, path := range paths {
if err := os.Mkdir(path, 0764); err != nil {
if !os.IsExist(err) {
log.Fatal(err)
}
continue
}
}
log.Logger.SetStyles(charmLog.DefaultStyles())
charmLog.SetDefault(log.Logger)
if internal.MachineReadable {
log.SetOutput(os.Stderr)
}
if internal.Debug {
log.SetLevel(log.DebugLevel)
log.SetOutput(os.Stderr)
log.SetReportCaller(true)
}
log.Debugf("abra version %s, commit %s", version, commit)
},
}
rootCmd.CompletionOptions.DisableDefaultCmd = true
manCommand := &cobra.Command{
Use: "man [flags]",
Aliases: []string{"m"},
Short: "Generate manpage",
Example: ` # generate the man pages into /usr/local/share/man/man1
sudo abra man
sudo mandb
# read the man pages
man abra
man abra-app-deploy`,
Run: func(cmd *cobra.Command, args []string) {
header := &doc.GenManHeader{
Title: "ABRA",
Section: "1",
}
manDir := "/usr/local/share/man/man1"
if _, err := os.Stat(manDir); os.IsNotExist(err) {
log.Fatalf("unable to proceed, '%s' does not exist?")
}
err := doc.GenManTree(rootCmd, header, manDir)
if err != nil {
log.Fatal(err)
}
log.Info("don't forget to run 'sudo mandb'")
},
}
rootCmd.PersistentFlags().BoolVarP(
&internal.Debug,
"debug",
"d",
false,
"show debug messages",
)
rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput,
"no-input",
"n",
false,
"toggle non-interactive mode",
)
rootCmd.PersistentFlags().BoolVarP(
&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,
)
server.ServerCommand.AddCommand(
server.ServerAddCommand,
server.ServerListCommand,
server.ServerPruneCommand,
server.ServerRemoveCommand,
)
recipe.RecipeCommand.AddCommand(
recipe.RecipeDiffCommand,
recipe.RecipeFetchCommand,
recipe.RecipeLintCommand,
recipe.RecipeListCommand,
recipe.RecipeNewCommand,
recipe.RecipeReleaseCommand,
recipe.RecipeResetCommand,
recipe.RecipeSyncCommand,
recipe.RecipeUpgradeCommand,
recipe.RecipeVersionCommand,
)
rootCmd.AddCommand(
UpgradeCommand,
AutocompleteCommand,
manCommand,
app.AppCommand,
catalogue.CatalogueCommand,
server.ServerCommand,
recipe.RecipeCommand,
)
app.AppCmdCommand.AddCommand(
app.AppCmdListCommand,
)
app.AppSecretCommand.AddCommand(
app.AppSecretGenerateCommand,
app.AppSecretInsertCommand,
app.AppSecretRmCommand,
app.AppSecretLsCommand,
)
app.AppVolumeCommand.AddCommand(
app.AppVolumeListCommand,
app.AppVolumeRemoveCommand,
)
app.AppBackupCommand.AddCommand(
app.AppBackupListCommand,
app.AppBackupDownloadCommand,
app.AppBackupCreateCommand,
app.AppBackupSnapshotsCommand,
)
app.AppCommand.AddCommand(
app.AppBackupCommand,
app.AppCheckCommand,
app.AppCmdCommand,
app.AppConfigCommand,
app.AppCpCommand,
app.AppDeployCommand,
app.AppListCommand,
app.AppLogsCommand,
app.AppNewCommand,
app.AppPsCommand,
app.AppRemoveCommand,
app.AppRestartCommand,
app.AppRestoreCommand,
app.AppRollbackCommand,
app.AppRunCommand,
app.AppSecretCommand,
app.AppServicesCommand,
app.AppUndeployCommand,
app.AppUpgradeCommand,
app.AppVolumeCommand,
app.AppLabelsCommand,
app.AppEnvCommand,
)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

View File

@ -1,149 +1,51 @@
package server package server
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context" contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/server" "coopcloud.tech/abra/pkg/server"
sshPkg "coopcloud.tech/abra/pkg/ssh" sshPkg "coopcloud.tech/abra/pkg/ssh"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var ServerAddCommand = &cobra.Command{ var local bool
Use: "add [[server] | --local] [flags]", var localFlag = &cli.BoolFlag{
Aliases: []string{"a"}, Name: "local, l",
Short: "Add a new server", Usage: "Use local server",
Long: `Add a new server to your configuration so that it can be managed by Abra. Destination: &local,
Abra relies on the standard SSH command-line and ~/.ssh/config for client
connection details. You must configure an entry per-host in your ~/.ssh/config
for each server:
Host 1312.net 1312
Hostname 1312.net
User antifa
Port 12345
IdentityFile ~/.ssh/antifa@somewhere
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local
developer machine. The domain is then set to "default".`,
Example: " abra server add 1312.net",
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
if !local {
return autocomplete.ServerNameComplete()
}
return nil, cobra.ShellCompDirectiveDefault
},
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 && local {
log.Fatal("cannot use [server] and --local together")
}
if len(args) == 0 && !local {
log.Fatal("missing argument or --local/-l flag")
}
name := "default"
if !local {
name = internal.ValidateDomain(args)
}
// NOTE(d1): reasonable 5 second timeout for connections which can't
// succeed. The connection is attempted twice, so this results in 10
// seconds.
timeout := client.WithTimeout(5)
if local {
created, err := createServerDir(name)
if err != nil {
log.Fatal(err)
}
log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
log.Fatal(err)
}
if created {
log.Info("local server successfully added")
} else {
log.Warn("local server already exists")
}
return
}
_, err := createServerDir(name)
if err != nil {
log.Fatal(err)
}
created, err := newContext(name)
if err != nil {
cleanUp(name)
log.Fatalf("unable to create local context: %s", err)
}
log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil {
cleanUp(name)
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 {
log.Infof("%s successfully added", name)
if _, err := dns.EnsureIPv4(name); err != nil {
log.Warnf("unable to resolve IPv4 for %s", name)
}
return
}
log.Warnf("%s already exists", name)
},
} }
// cleanUp cleans up the partially created context/client details for a failed func cleanUp(domainName string) {
// "server add" attempt. if domainName != "default" {
func cleanUp(name string) { logrus.Infof("cleaning up context for %s", domainName)
if name != "default" { if err := client.DeleteContext(domainName); err != nil {
log.Debugf("serverAdd: cleanUp: cleaning up context for %s", name) logrus.Fatal(err)
if err := client.DeleteContext(name); err != nil {
log.Fatal(err)
} }
} }
serverDir := filepath.Join(config.SERVERS_DIR, name) logrus.Infof("attempting to clean up server directory for %s", domainName)
serverDir := filepath.Join(config.SERVERS_DIR, domainName)
files, err := config.GetAllFilesInDirectory(serverDir) files, err := config.GetAllFilesInDirectory(serverDir)
if err != nil { if err != nil {
log.Fatalf("serverAdd: cleanUp: unable to list files in %s: %s", serverDir, err) logrus.Fatalf("unable to list files in %s: %s", serverDir, err)
} }
if len(files) > 0 { if len(files) > 0 {
log.Debugf("serverAdd: cleanUp: %s is not empty, aborting cleanup", serverDir) logrus.Warnf("aborting clean up of %s because it is not empty", serverDir)
return return
} }
if err := os.RemoveAll(serverDir); err != nil { if err := os.RemoveAll(serverDir); err != nil {
log.Fatalf("serverAdd: cleanUp: failed to remove %s: %s", serverDir, err) logrus.Fatal(err)
} }
} }
@ -151,54 +53,124 @@ func cleanUp(name string) {
// Docker manages SSH connection details. These are stored to disk in // Docker manages SSH connection details. These are stored to disk in
// ~/.docker. Abra can manage this completely for the user, so it's an // ~/.docker. Abra can manage this completely for the user, so it's an
// implementation detail. // implementation detail.
func newContext(name string) (bool, error) { func newContext(c *cli.Context, domainName string) error {
store := contextPkg.NewDefaultDockerContextStore() store := contextPkg.NewDefaultDockerContextStore()
contexts, err := store.Store.List() contexts, err := store.Store.List()
if err != nil { if err != nil {
return false, err return err
} }
for _, context := range contexts { for _, context := range contexts {
if context.Name == name { if context.Name == domainName {
log.Debugf("context for %s already exists", name) logrus.Debugf("context for %s already exists", domainName)
return false, nil return nil
} }
} }
log.Debugf("creating context with domain %s", name) logrus.Debugf("creating context with domain %s", domainName)
if err := client.CreateContext(name); err != nil { if err := client.CreateContext(domainName); err != nil {
return false, nil return err
} }
return true, nil return nil
} }
// createServerDir creates the ~/.abra/servers/... directory for a new server. // createServerDir creates the ~/.abra/servers/... directory for a new server.
func createServerDir(name string) (bool, error) { func createServerDir(domainName string) error {
if err := server.CreateServerDir(name); err != nil { if err := server.CreateServerDir(domainName); err != nil {
if !os.IsExist(err) { if !os.IsExist(err) {
return false, err return err
} }
logrus.Debugf("server dir for %s already created", domainName)
log.Debugf("server dir for %s already created", name)
return false, nil
} }
return true, nil return nil
} }
var ( var serverAddCommand = cli.Command{
local bool Name: "add",
) Aliases: []string{"a"},
Usage: "Add a server to your configuration",
Description: `
Add a new server to your configuration so that it can be managed by Abra.
func init() { Abra uses the SSH command-line to discover connection details for your server.
ServerAddCommand.Flags().BoolVarP( It is advised to configure an entry per-host in your ~/.ssh/config for each
&local, server. For example:
"local",
"l", Host example.com
false, Hostname example.com
"use local server", User exampleUser
) Port 12345
IdentityFile ~/.ssh/example@somewhere
Abra can then load SSH connection details from this configuratiion with:
abra server add example.com
If "--local" is passed, then Abra assumes that the current local server is
intended as the target server. This is useful when you want to have your entire
Co-op Cloud config located on the server itself, and not on your local
developer machine.
`,
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
localFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain>",
Action: func(c *cli.Context) error {
if len(c.Args()) > 0 && local || !internal.ValidateSubCmdFlags(c) {
err := errors.New("cannot use <domain> and --local together")
internal.ShowSubcommandHelpAndError(c, err)
}
var domainName string
if local {
domainName = "default"
} else {
domainName = internal.ValidateDomain(c)
}
if local {
if err := createServerDir(domainName); err != nil {
logrus.Fatal(err)
}
logrus.Infof("attempting to create client for %s", domainName)
if _, err := client.New(domainName); err != nil {
cleanUp(domainName)
logrus.Fatal(err)
}
logrus.Info("local server added")
return nil
}
if _, err := dns.EnsureIPv4(domainName); err != nil {
logrus.Fatal(err)
}
if err := createServerDir(domainName); err != nil {
logrus.Fatal(err)
}
if err := newContext(c, domainName); err != nil {
logrus.Fatal(err)
}
logrus.Infof("attempting to create client for %s", domainName)
if _, err := client.New(domainName); err != nil {
cleanUp(domainName)
logrus.Debugf("failed to construct client for %s, saw %s", domainName, err.Error())
logrus.Fatal(sshPkg.Fatal(domainName, err))
}
logrus.Infof("%s added", domainName)
return nil
},
} }

View File

@ -1,103 +1,110 @@
package server package server
import ( import (
"fmt"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context" "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/cli/cli/connhelper/ssh" "github.com/docker/cli/cli/connhelper/ssh"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var ServerListCommand = &cobra.Command{ var problemsFilter bool
Use: "list [flags]",
var problemsFilterFlag = &cli.BoolFlag{
Name: "problems, p",
Usage: "Show only servers with potential connection problems",
Destination: &problemsFilter,
}
var serverListCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Short: "List managed servers", Usage: "List managed servers",
Args: cobra.NoArgs, Flags: []cli.Flag{
Run: func(cmd *cobra.Command, args []string) { problemsFilterFlag,
dockerContextStore := contextPkg.NewDefaultDockerContextStore() internal.DebugFlag,
internal.MachineReadableFlag,
internal.OfflineFlag,
},
Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error {
dockerContextStore := context.NewDefaultDockerContextStore()
contexts, err := dockerContextStore.Store.List() contexts, err := dockerContextStore.Store.List()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
table, err := formatter.CreateTable() tableColumns := []string{"name", "host", "user", "port"}
if err != nil { table := formatter.CreateTable(tableColumns)
log.Fatal(err)
}
headers := []string{"NAME", "HOST"}
table.Headers(headers...)
serverNames, err := config.ReadServerNames() serverNames, err := config.ReadServerNames()
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
var rows [][]string
for _, serverName := range serverNames { for _, serverName := range serverNames {
var row []string var row []string
for _, dockerCtx := range contexts { for _, ctx := range contexts {
endpoint, err := contextPkg.GetContextEndpoint(dockerCtx) endpoint, err := context.GetContextEndpoint(ctx)
if err != nil && strings.Contains(err.Error(), "does not exist") { if err != nil && strings.Contains(err.Error(), "does not exist") {
// No local context found, we can continue safely // No local context found, we can continue safely
continue continue
} }
if dockerCtx.Name == serverName { if ctx.Name == serverName {
sp, err := ssh.ParseURL(endpoint) sp, err := ssh.ParseURL(endpoint)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if sp.Host == "" { if sp.Host == "" {
sp.Host = "unknown" sp.Host = "unknown"
} }
if sp.User == "" {
sp.User = "unknown"
}
if sp.Port == "" {
sp.Port = "unknown"
}
row = []string{serverName, sp.Host} row = []string{serverName, sp.Host, sp.User, sp.Port}
rows = append(rows, row)
} }
} }
if len(row) == 0 { if len(row) == 0 {
if serverName == "default" { if serverName == "default" {
row = []string{serverName, "local"} row = []string{serverName, "local", "n/a", "n/a"}
} else { } else {
row = []string{serverName, "unknown"} row = []string{serverName, "unknown", "unknown", "unknown"}
} }
rows = append(rows, row)
} }
table.Row(row...) if problemsFilter {
for _, val := range row {
if val == "unknown" {
table.Append(row)
break
}
}
} else {
table.Append(row)
}
} }
if internal.MachineReadable { if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows) table.JSONRender()
if err != nil { } else {
log.Fatal("unable to render to JSON: %s", err) if problemsFilter && table.NumLines() == 0 {
logrus.Info("all servers wired up correctly 👏")
} else {
table.Render()
} }
fmt.Println(out)
return
} }
if err := formatter.PrintTable(table); err != nil { return nil
log.Fatal(err)
}
}, },
} }
func init() {
ServerListCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
false,
"print machine-readable output",
)
}

View File

@ -1,102 +1,103 @@
package server package server
import ( import (
"context"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
var ServerPruneCommand = &cobra.Command{ var allFilter bool
Use: "prune <server> [flags]",
Aliases: []string{"p"},
Short: "Prune resources on a server",
Long: `Prunes unused containers, networks, and dangling images.
Use "--volumes/-v" to remove volumes that are not associated with a deployed var allFilterFlag = &cli.BoolFlag{
app. This can result in unwanted data loss if not used carefully.`, Name: "all, a",
Args: cobra.ExactArgs(1), Usage: "Remove all unused images not just dangling ones",
ValidArgsFunction: func( Destination: &allFilter,
cmd *cobra.Command, }
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) { var volumesFilter bool
return autocomplete.ServerNameComplete()
var volumesFilterFlag = &cli.BoolFlag{
Name: "volumes, v",
Usage: "Prune volumes. This will remove app data, Be Careful!",
Destination: &volumesFilter,
}
var serverPruneCommand = cli.Command{
Name: "prune",
Aliases: []string{"p"},
Usage: "Prune a managed server; Runs a docker system prune",
Description: `
Prunes unused containers, networks, and dangling images.
If passing "-v/--volumes" then volumes not connected to a deployed app will
also be removed. This can result in unwanted data loss if not used carefully.
`,
ArgsUsage: "[<server>]",
Flags: []cli.Flag{
allFilterFlag,
volumesFilterFlag,
internal.DebugFlag,
internal.OfflineFlag,
internal.NoInputFlag,
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
serverName := internal.ValidateServer(args) BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
cl, err := client.New(serverName) cl, err := client.New(serverName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
var filterArgs filters.Args var args filters.Args
cr, err := cl.ContainersPrune(cmd.Context(), filterArgs) ctx := context.Background()
cr, err := cl.ContainersPrune(ctx, args)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed) cntSpaceReclaimed := formatter.ByteCountSI(cr.SpaceReclaimed)
log.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed) logrus.Infof("containers pruned: %d; space reclaimed: %s", len(cr.ContainersDeleted), cntSpaceReclaimed)
nr, err := cl.NetworksPrune(cmd.Context(), filterArgs) nr, err := cl.NetworksPrune(ctx, args)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
log.Infof("networks pruned: %d", len(nr.NetworksDeleted)) logrus.Infof("networks pruned: %d", len(nr.NetworksDeleted))
pruneFilters := filters.NewArgs() pruneFilters := filters.NewArgs()
if allFilter { if allFilter {
log.Debugf("removing all images, not only dangling ones") logrus.Debugf("removing all images, not only dangling ones")
pruneFilters.Add("dangling", "false") pruneFilters.Add("dangling", "false")
} }
ir, err := cl.ImagesPrune(cmd.Context(), pruneFilters) ir, err := cl.ImagesPrune(ctx, pruneFilters)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed) imgSpaceReclaimed := formatter.ByteCountSI(ir.SpaceReclaimed)
log.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed) logrus.Infof("images pruned: %d; space reclaimed: %s", len(ir.ImagesDeleted), imgSpaceReclaimed)
if volumesFilter { if volumesFilter {
vr, err := cl.VolumesPrune(cmd.Context(), filterArgs) vr, err := cl.VolumesPrune(ctx, args)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed) volSpaceReclaimed := formatter.ByteCountSI(vr.SpaceReclaimed)
log.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed) logrus.Infof("volumes pruned: %d; space reclaimed: %s", len(vr.VolumesDeleted), volSpaceReclaimed)
} }
return return nil
}, },
} }
var (
allFilter bool
volumesFilter bool
)
func init() {
ServerPruneCommand.Flags().BoolVarP(
&allFilter,
"all",
"a",
false,
"remove all unused images",
)
ServerPruneCommand.Flags().BoolVarP(
&volumesFilter,
"volumes",
"v",
false,
"remove volumes",
)
}

View File

@ -8,39 +8,41 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/urfave/cli"
) )
var ServerRemoveCommand = &cobra.Command{ var serverRemoveCommand = cli.Command{
Use: "remove <server> [flags]", Name: "remove",
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Short: "Remove a managed server", ArgsUsage: "<server>",
Long: `Remove a managed server. Usage: "Remove a managed server",
Description: `Remove a managed server.
Abra will remove the internal bookkeeping ($ABRA_DIR/servers/...) and Abra will remove the internal bookkeeping (~/.abra/servers/...) and underlying
underlying client connection context. This server will then be lost in time, client connection context. This server will then be lost in time, like tears in
like tears in rain.`, rain.
Args: cobra.ExactArgs(1), `,
ValidArgsFunction: func( Flags: []cli.Flag{
cmd *cobra.Command, internal.DebugFlag,
args []string, internal.NoInputFlag,
toComplete string) ([]string, cobra.ShellCompDirective) { internal.OfflineFlag,
return autocomplete.ServerNameComplete()
}, },
Run: func(cmd *cobra.Command, args []string) { Before: internal.SubCommandBefore,
serverName := internal.ValidateServer(args) BashComplete: autocomplete.ServerNameComplete,
Action: func(c *cli.Context) error {
serverName := internal.ValidateServer(c)
if err := client.DeleteContext(serverName); err != nil { if err := client.DeleteContext(serverName); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, serverName)); err != nil { if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, serverName)); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
log.Infof("%s is now lost in time, like tears in rain", serverName) logrus.Infof("server at %s has been lost in time, like tears in rain", serverName)
return return nil
}, },
} }

View File

@ -1,10 +1,18 @@
package server package server
import "github.com/spf13/cobra" import (
"github.com/urfave/cli"
)
// ServerCommand defines the `abra server` command and its subcommands // ServerCommand defines the `abra server` command and its subcommands
var ServerCommand = &cobra.Command{ var ServerCommand = cli.Command{
Use: "server [cmd] [args] [flags]", Name: "server",
Aliases: []string{"s"}, Aliases: []string{"s"},
Short: "Manage servers", Usage: "Manage servers",
Subcommands: []cli.Command{
serverAddCommand,
serverListCommand,
serverRemoveCommand,
serverPruneCommand,
},
} }

View File

@ -8,128 +8,143 @@ import (
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
charmLog "github.com/charmbracelet/log"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
"github.com/spf13/cobra"
"coopcloud.tech/abra/pkg/log" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
const SERVER = "localhost" const SERVER = "localhost"
// NotifyCommand checks for available upgrades. var majorUpdate bool
var NotifyCommand = &cobra.Command{ var majorFlag = &cli.BoolFlag{
Use: "notify [flags]", Name: "major, m",
Usage: "Also check for major updates",
Destination: &majorUpdate,
}
var updateAll bool
var allFlag = &cli.BoolFlag{
Name: "all, a",
Usage: "Update all deployed apps",
Destination: &updateAll,
}
// Notify checks for available upgrades
var Notify = cli.Command{
Name: "notify",
Aliases: []string{"n"}, Aliases: []string{"n"},
Short: "Check for available upgrades", Usage: "Check for available upgrades",
Long: `Notify on new versions for deployed apps. Flags: []cli.Flag{
internal.DebugFlag,
If a new patch/minor version is available, a notification is printed. majorFlag,
internal.OfflineFlag,
Use "--major/-m" to include new major versions.`, },
Args: cobra.NoArgs, Before: internal.SubCommandBefore,
Run: func(cmd *cobra.Command, args []string) { Description: `
It reads the deployed app versions and looks for new versions in the recipe
catalogue. If a new patch/minor version is available, a notification is
printed. To include major versions use the --major flag.
`,
Action: func(c *cli.Context) error {
cl, err := client.New("default") cl, err := client.New("default")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
stacks, err := stack.GetStacks(cl) stacks, err := stack.GetStacks(cl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, stackInfo := range stacks { for _, stackInfo := range stacks {
stackName := stackInfo.Name stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe") recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
if recipeName != "" { if recipeName != "" {
_, err = getLatestUpgrade(cl, stackName, recipeName) _, err = getLatestUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
} }
return nil
}, },
} }
// UpgradeCommand upgrades apps. // UpgradeApp upgrades apps.
var UpgradeCommand = &cobra.Command{ var UpgradeApp = cli.Command{
Use: "upgrade [[stack] [recipe] | --all] [flags]", Name: "upgrade",
Aliases: []string{"u"}, Aliases: []string{"u"},
Short: "Upgrade apps", Usage: "Upgrade apps",
Long: `Upgrade an app by specifying stack name and recipe. ArgsUsage: "<stack-name> <recipe>",
Flags: []cli.Flag{
Use "--all" to upgrade every deployed app. internal.DebugFlag,
internal.ChaosFlag,
For each app with auto updates enabled, the deployed version is compared with majorFlag,
the current recipe catalogue version. If a new patch/minor version is allFlag,
available, the app is upgraded. internal.OfflineFlag,
},
To include major versions use the "--major/-m" flag. You probably don't want Before: internal.SubCommandBefore,
that as it will break things. Only apps that are not deployed with "--chaos/-C" Description: `
are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it Upgrade an app by specifying its stack name and recipe. By passing "--all"
with care.`, instead, every deployed app is upgraded. For each apps with enabled auto
Args: cobra.RangeArgs(0, 2), updates the deployed version is compared with the current recipe catalogue
// TODO(d1): complete stack/recipe version. If a new patch/minor version is available, the app is upgraded. To
// ValidArgsFunction: func( include major versions use the "--major" flag. Don't do that, it will probably
// cmd *cobra.Command, break things. Only apps that are not deployed with "--chaos" are upgraded, to
// args []string, update chaos deployments use the "--chaos" flag. Use it with care.
// toComplete string) ([]string, cobra.ShellCompDirective) { `,
// }, Action: func(c *cli.Context) error {
Run: func(cmd *cobra.Command, args []string) {
cl, err := client.New("default") cl, err := client.New("default")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
}
if !updateAll && len(args) != 2 {
log.Fatal("missing arguments or --all/-a flag")
} }
if !updateAll { if !updateAll {
stackName := args[0] stackName := c.Args().Get(0)
recipeName := args[1] recipeName := c.Args().Get(1)
err = tryUpgrade(cl, stackName, recipeName) err = tryUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
return return nil
} }
stacks, err := stack.GetStacks(cl) stacks, err := stack.GetStacks(cl)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
for _, stackInfo := range stacks { for _, stackInfo := range stacks {
stackName := stackInfo.Name stackName := stackInfo.Name
recipeName, err := getLabel(cl, stackName, "recipe") recipeName, err := getLabel(cl, stackName, "recipe")
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
err = tryUpgrade(cl, stackName, recipeName) err = tryUpgrade(cl, stackName, recipeName)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
return nil
}, },
} }
@ -150,7 +165,7 @@ func getLabel(cl *dockerclient.Client, stackName string, label string) (string,
} }
} }
log.Debugf("no %s label found for %s", label, stackName) logrus.Debugf("no %s label found for %s", label, stackName)
return "", nil return "", nil
} }
@ -171,13 +186,13 @@ func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool
return value, nil return value, nil
} }
log.Debugf("boolean label %s could not be found for %s, set default to false.", label, stackName) logrus.Debugf("Boolean label %s could not be found for %s, set default to false.", label, stackName)
return false, nil return false, nil
} }
// getEnv reads env variables from docker services. // getEnv reads env variables from docker services.
func getEnv(cl *dockerclient.Client, stackName string) (envfile.AppEnv, error) { func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) {
envMap := make(map[string]string) envMap := make(map[string]string)
filter := filters.NewArgs() filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName)) filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
@ -192,12 +207,12 @@ func getEnv(cl *dockerclient.Client, stackName string) (envfile.AppEnv, error) {
for _, envString := range envList { for _, envString := range envList {
splitString := strings.SplitN(envString, "=", 2) splitString := strings.SplitN(envString, "=", 2)
if len(splitString) != 2 { if len(splitString) != 2 {
log.Debugf("can't separate key from value: %s (this variable is probably unset)", envString) logrus.Debugf("can't separate key from value: %s (this variable is probably unset)", envString)
continue continue
} }
k := splitString[0] k := splitString[0]
v := splitString[1] v := splitString[1]
log.Debugf("for %s read env %s with value: %s from docker service", stackName, k, v) logrus.Debugf("For %s read env %s with value: %s from docker service", stackName, k, v)
envMap[k] = v envMap[k] = v
} }
} }
@ -219,14 +234,14 @@ func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName stri
} }
if len(availableUpgrades) == 0 { if len(availableUpgrades) == 0 {
log.Debugf("no available upgrades for %s", stackName) logrus.Debugf("no available upgrades for %s", stackName)
return "", nil return "", nil
} }
var chosenUpgrade string var chosenUpgrade string
if len(availableUpgrades) > 0 { if len(availableUpgrades) > 0 {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
log.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade) logrus.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade)
} }
return chosenUpgrade, nil return chosenUpgrade, nil
@ -234,22 +249,22 @@ func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName stri
// getDeployedVersion returns the currently deployed version of an app. // getDeployedVersion returns the currently deployed version of an app.
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) { func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
log.Debugf("retrieve deployed version whether %s is already deployed", stackName) logrus.Debugf("Retrieve deployed version whether %s is already deployed", stackName)
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName) isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
if err != nil { if err != nil {
return "", err return "", err
} }
if !deployMeta.IsDeployed { if !isDeployed {
return "", fmt.Errorf("%s is not deployed?", stackName) return "", fmt.Errorf("%s is not deployed?", stackName)
} }
if deployMeta.Version == "unknown" { if deployedVersion == "unknown" {
return "", fmt.Errorf("failed to determine deployed version of %s", stackName) return "", fmt.Errorf("failed to determine deployed version of %s", stackName)
} }
return deployMeta.Version, nil return deployedVersion, nil
} }
// getAvailableUpgrades returns all available versions of an app that are newer // getAvailableUpgrades returns all available versions of an app that are newer
@ -268,7 +283,7 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
} }
if len(versions) == 0 { if len(versions) == 0 {
log.Warnf("no published releases for %s in the recipe catalogue?", recipeName) logrus.Warnf("no published releases for %s in the recipe catalogue?", recipeName)
return nil, nil return nil, nil
} }
@ -289,32 +304,34 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName
return nil, err return nil, err
} }
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || includeMajorUpdates) { if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) {
availableUpgrades = append(availableUpgrades, version) availableUpgrades = append(availableUpgrades, version)
} }
} }
log.Debugf("available updates for %s: %s", stackName, availableUpgrades) logrus.Debugf("Available updates for %s: %s", stackName, availableUpgrades)
return availableUpgrades, nil return availableUpgrades, nil
} }
// processRecipeRepoVersion clones, pulls, checks out the version and lints the // processRecipeRepoVersion clones, pulls, checks out the version and lints the
// recipe repository. // recipe repository.
func processRecipeRepoVersion(r recipe.Recipe, version string) error { func processRecipeRepoVersion(recipeName, version string) error {
if err := r.EnsureExists(); err != nil { if err := recipe.EnsureExists(recipeName); err != nil {
return err return err
} }
if err := r.EnsureUpToDate(); err != nil { if err := recipe.EnsureUpToDate(recipeName); err != nil {
return err return err
} }
if _, err := r.EnsureVersion(version); err != nil { if err := recipe.EnsureVersion(recipeName, version); err != nil {
return err return err
} }
if err := lint.LintForErrors(r); err != nil { if r, err := recipe.Get(recipeName, internal.Offline); err != nil {
return err
} else if err := lint.LintForErrors(r); err != nil {
return err return err
} }
@ -322,14 +339,15 @@ func processRecipeRepoVersion(r recipe.Recipe, version string) error {
} }
// mergeAbraShEnv merges abra.sh env vars into the app env vars. // mergeAbraShEnv merges abra.sh env vars into the app env vars.
func mergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error { func mergeAbraShEnv(recipeName string, env config.AppEnv) error {
abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath) abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh")
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
return err return err
} }
for k, v := range abraShEnv { for k, v := range abraShEnv {
log.Debugf("read v:%s k: %s", v, k) logrus.Debugf("read v:%s k: %s", v, k)
env[k] = v env[k] = v
} }
@ -337,33 +355,32 @@ func mergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
} }
// createDeployConfig merges and enriches the compose config for the deployment. // createDeployConfig merges and enriches the compose config for the deployment.
func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) { func createDeployConfig(recipeName string, stackName string, env config.AppEnv) (*composetypes.Config, stack.Deploy, error) {
env["STACK_NAME"] = stackName env["STACK_NAME"] = stackName
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Namespace: stackName, Namespace: stackName,
Prune: false, Prune: false,
ResolveImage: stack.ResolveImageAlways, ResolveImage: stack.ResolveImageAlways,
Detach: false,
} }
composeFiles, err := r.GetComposeFiles(env) composeFiles, err := config.GetComposeFiles(recipeName, env)
if err != nil { if err != nil {
return nil, deployOpts, err return nil, deployOpts, err
} }
deployOpts.Composefiles = composeFiles deployOpts.Composefiles = composeFiles
compose, err := appPkg.GetAppComposeConfig(stackName, deployOpts, env) compose, err := config.GetAppComposeConfig(stackName, deployOpts, env)
if err != nil { if err != nil {
return nil, deployOpts, err return nil, deployOpts, err
} }
appPkg.ExposeAllEnv(stackName, compose, env) config.ExposeAllEnv(stackName, compose, env)
// after the upgrade the deployment won't be in chaos state anymore // after the upgrade the deployment won't be in chaos state anymore
appPkg.SetChaosLabel(compose, stackName, false) config.SetChaosLabel(compose, stackName, false)
appPkg.SetRecipeLabel(compose, stackName, r.Name) config.SetRecipeLabel(compose, stackName, recipeName)
appPkg.SetUpdateLabel(compose, stackName, env) config.SetUpdateLabel(compose, stackName, env)
return compose, deployOpts, nil return compose, deployOpts, nil
} }
@ -371,7 +388,7 @@ func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (
// tryUpgrade performs the upgrade if all the requirements are fulfilled. // tryUpgrade performs the upgrade if all the requirements are fulfilled.
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error { func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
if recipeName == "" { if recipeName == "" {
log.Debugf("don't update %s due to missing recipe name", stackName) logrus.Debugf("don't update %s due to missing recipe name", stackName)
return nil return nil
} }
@ -381,7 +398,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
if chaos && !internal.Chaos { if chaos && !internal.Chaos {
log.Debugf("don't update %s due to chaos deployment", stackName) logrus.Debugf("don't update %s due to chaos deployment", stackName)
return nil return nil
} }
@ -391,7 +408,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
if !updatesEnabled { if !updatesEnabled {
log.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName) logrus.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName)
return nil return nil
} }
@ -401,7 +418,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
if upgradeVersion == "" { if upgradeVersion == "" {
log.Debugf("don't update %s due to no new version", stackName) logrus.Debugf("don't update %s due to no new version", stackName)
return nil return nil
} }
@ -411,122 +428,71 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
} }
// upgrade performs all necessary steps to upgrade an app. // upgrade performs all necessary steps to upgrade an app.
func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string) error { func upgrade(cl *dockerclient.Client, stackName, recipeName,
upgradeVersion string) error {
env, err := getEnv(cl, stackName) env, err := getEnv(cl, stackName)
if err != nil { if err != nil {
return err return err
} }
app := appPkg.App{ app := config.App{
Name: stackName, Name: stackName,
Recipe: recipe.Get(recipeName), Recipe: recipeName,
Server: SERVER, Server: SERVER,
Env: env, Env: env,
} }
r := recipe.Get(recipeName) if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil {
if err = processRecipeRepoVersion(r, upgradeVersion); err != nil {
return err return err
} }
if err = mergeAbraShEnv(app.Recipe, app.Env); err != nil { if err = mergeAbraShEnv(recipeName, app.Env); err != nil {
return err return err
} }
compose, deployOpts, err := createDeployConfig(r, stackName, app.Env) compose, deployOpts, err := createDeployConfig(recipeName, stackName, app.Env)
if err != nil { if err != nil {
return err return err
} }
log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion) logrus.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
err = stack.RunDeploy(cl, deployOpts, compose, stackName, true) err = stack.RunDeploy(cl, deployOpts, compose, stackName, true)
return err return err
} }
func newKadabraApp(version, commit string) *cobra.Command { func newAbraApp(version, commit string) *cli.App {
rootCmd := &cobra.Command{ app := &cli.App{
Use: "kadabra [cmd] [flags]", Name: "kadabra",
Usage: `The Co-op Cloud auto-updater
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Short: "The Co-op Cloud auto-updater 🤖 🚀", Commands: []cli.Command{
PersistentPreRun: func(cmd *cobra.Command, args []string) { Notify,
log.Logger.SetStyles(charmLog.DefaultStyles()) UpgradeApp,
charmLog.SetDefault(log.Logger)
if internal.Debug {
log.SetLevel(log.DebugLevel)
log.SetOutput(os.Stderr)
log.SetReportCaller(true)
}
log.Debugf("kadabra version %s, commit %s", version, commit)
}, },
} }
rootCmd.PersistentFlags().BoolVarP( app.Before = func(c *cli.Context) error {
&internal.Debug, "debug", "d", false, logrus.Debugf("kadabra version %s, commit %s", version, commit)
"show debug messages", return nil
) }
rootCmd.PersistentFlags().BoolVarP( return app
&internal.NoInput, "no-input", "n", false,
"toggle non-interactive mode",
)
rootCmd.AddCommand(
NotifyCommand,
UpgradeCommand,
)
return rootCmd
} }
// RunApp runs CLI abra app. // RunApp runs CLI abra app.
func RunApp(version, commit string) { func RunApp(version, commit string) {
app := newKadabraApp(version, commit) app := newAbraApp(version, commit)
if err := app.Execute(); err != nil { if err := app.Run(os.Args); err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
} }
var (
includeMajorUpdates bool
updateAll bool
)
func init() {
NotifyCommand.Flags().BoolVarP(
&includeMajorUpdates,
"major",
"m",
false,
"check for major updates",
)
UpgradeCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
UpgradeCommand.Flags().BoolVarP(
&includeMajorUpdates,
"major",
"m",
false,
"check for major updates",
)
UpgradeCommand.Flags().BoolVarP(
&updateAll,
"all",
"a",
false,
"update all deployed apps",
)
}

View File

@ -1,56 +0,0 @@
// Package cli provides the interface for the command-line.
package cli
import (
"fmt"
"os/exec"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cobra.Command{
Use: "upgrade [flags]",
Aliases: []string{"u"},
Short: "Upgrade abra",
Long: `Upgrade abra in-place with the latest stable or release candidate.
By default, the latest stable release is downloaded.
Use "--rc/-r" to install the latest release candidate. Please bear in mind that
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
for the testing efforts 💗`,
Example: " abra upgrade --rc",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
mainURL := "https://install.abra.coopcloud.tech"
c := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if releaseCandidate {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
c = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
log.Debugf("attempting to run %s", c)
if err := internal.RunCmd(c); err != nil {
log.Fatal(err)
}
},
}
var (
releaseCandidate bool
)
func init() {
UpgradeCommand.Flags().BoolVarP(
&releaseCandidate,
"rc",
"r",
false,
"install release candidate (may contain bugs)",
)
}

View File

@ -19,5 +19,5 @@ func main() {
Commit = " " Commit = " "
} }
cli.Run(Version, Commit) cli.RunApp(Version, Commit)
} }

135
go.mod
View File

@ -1,150 +1,133 @@
module coopcloud.tech/abra module coopcloud.tech/abra
go 1.22.7 go 1.21
toolchain go1.23.1
require ( require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb 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/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/lipgloss v1.0.0 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/charmbracelet/log v0.4.0 github.com/distribution/distribution v2.8.3+incompatible
github.com/distribution/reference v0.6.0 github.com/docker/cli v26.1.4+incompatible
github.com/docker/cli v27.4.1+incompatible github.com/docker/docker v26.1.4+incompatible
github.com/docker/docker v27.4.1+incompatible
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.13.1 github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.6.0
github.com/moby/sys/signal v0.7.1 github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.5.2 github.com/moby/term v0.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.17.1 github.com/schollz/progressbar/v3 v3.14.4
golang.org/x/term v0.28.0 github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1 gotest.tools/v3 v3.5.1
) )
require ( require (
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/x/ansi v0.6.0 // indirect github.com/cloudflare/circl v1.3.9 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.1 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.17.9 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mmcloughlin/avo v0.6.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/moby/sys/user v0.1.0 // indirect
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/morikuni/aec v1.0.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/runc v1.1.13 // indirect github.com/opencontainers/runc v1.1.13 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pjbgf/sha1cd v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.61.0 // indirect github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect go.opentelemetry.io/otel v1.27.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect go.opentelemetry.io/otel/metric v1.27.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect go.opentelemetry.io/otel/sdk v1.27.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.27.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect go.opentelemetry.io/otel/trace v1.27.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect golang.org/x/crypto v0.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect golang.org/x/net v0.26.0 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/term v0.21.0 // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/sync v0.10.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
golang.org/x/text v0.21.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect
golang.org/x/time v0.9.0 // indirect google.golang.org/grpc v1.64.0 // indirect
golang.org/x/tools v0.29.0 // indirect google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/grpc v1.69.2 // indirect
google.golang.org/protobuf v1.36.2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require ( require (
github.com/buger/goterm v1.0.4
github.com/containerd/containerd v1.7.18 // indirect
github.com/containers/image v3.0.2+incompatible github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1 github.com/decentral1se/passgen v1.0.1
github.com/docker/docker-credential-helpers v0.8.2 // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 github.com/hashicorp/go-retryablehttp v0.7.7
github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_golang v1.19.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1 // indirect
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.9.0
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli v1.22.15
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.29.0 golang.org/x/sys v0.21.0
) )

312
go.sum
View File

@ -24,19 +24,19 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb h1:Ws6WEwKXeaYEkfdkX6AqX1XLPuaCeyStEtxbmEJPllk= coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb h1:Ws6WEwKXeaYEkfdkX6AqX1XLPuaCeyStEtxbmEJPllk=
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c h1:oeKnUB79PKYD8D0/unYuu7MRcWryQQWOns8+JL+acrs= git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE=
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/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 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 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 v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
@ -49,9 +49,12 @@ github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 h1:vdT7QwBhJJEVNFMBNhRSFDRCB6O16T28VhvqRgqFyn8=
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4/go.mod h1:SvXOG8ElV28oAiG9zv91SDe5+9PfIr7PPccpr8YyXNs=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
@ -73,14 +76,16 @@ github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn
github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=
github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=
github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38=
github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= 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/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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 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/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= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
@ -101,10 +106,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -119,6 +120,8 @@ github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc= github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0 h1:s7+5BfS4WFJoVF9pnB8kBk03S7pZXRdKamnV0FOl5Sc=
@ -127,28 +130,18 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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.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/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= 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.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -161,8 +154,9 @@ 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/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 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 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/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= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
@ -201,6 +195,8 @@ github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09Zvgq
github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
@ -276,9 +272,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/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@ -286,8 +281,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= 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= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
@ -302,20 +297,21 @@ github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8l
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/distribution/distribution v2.8.3+incompatible h1:RlpEXBLq/WPXYvBYMDAmBX/SnhD67qwtvW/DzKc8pAo=
github.com/distribution/distribution v2.8.3+incompatible/go.mod h1:EgLm2NgWtdKgzF9NpMzUKgzmR7AMmb0VQi2B+ZzDRjc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI= github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8=
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
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-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+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 v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4= github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU=
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= 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/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
@ -341,8 +337,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= 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 v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@ -355,6 +351,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 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/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
@ -373,16 +371,16 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYis
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 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 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc= 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 v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -391,8 +389,6 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@ -414,8 +410,6 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE= github.com/go-sql-driver/mysql v1.3.0 h1:pgwjLi/dvffoP9aabwkT3AKpXQM93QARkjFhDDqC1UE=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
@ -439,8 +433,8 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -499,8 +493,6 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
@ -520,10 +512,9 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= 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/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= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -574,6 +565,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
@ -585,8 +577,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.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.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.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 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.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= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -602,12 +594,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@ -626,8 +614,9 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
@ -651,8 +640,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY=
github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
@ -663,20 +650,16 @@ github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2J
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI=
github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
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.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@ -685,10 +668,7 @@ 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 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 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/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
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-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= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
@ -697,6 +677,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -713,8 +695,8 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@ -754,8 +736,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s= 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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -771,8 +753,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.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.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.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 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-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@ -786,8 +768,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/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= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@ -808,16 +790,15 @@ 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 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/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.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= 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/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U= github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4= github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= 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.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
@ -834,8 +815,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.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 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/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= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
@ -870,6 +851,9 @@ github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -877,8 +861,11 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
@ -896,6 +883,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.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM=
github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0=
github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
@ -934,33 +923,29 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0/go.mod h1:xJntEd2KL6Qdg5lwp97HMLQDVeAhrYxmzFseAMDPQ8I=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
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.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
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 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A=
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU= go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI=
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q= go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.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/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -985,10 +970,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-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/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= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -999,10 +984,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1025,8 +1006,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -1068,10 +1048,11 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 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= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1089,8 +1070,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -1155,6 +1137,7 @@ golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1168,19 +1151,22 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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-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-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.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/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= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1190,18 +1176,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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/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-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-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-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.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1247,10 +1233,7 @@ golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4X
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1299,14 +1282,10 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA= google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 h1:MuYw1wJzT+ZkybKfaOXKp5hJiZDn2iHaXRw0mRYdHSc=
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY= google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4/go.mod h1:px9SlOOZBg1wM1zdnr8jEL4CNGUBZ+ZKYtNPApNQc4c=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 h1:Di6ANFilr+S60a4S61ZM00vLdw0IrQOSMS2/6mrnOU0=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -1326,8 +1305,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 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= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1341,10 +1320,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
@ -1384,7 +1361,6 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=

View File

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

View File

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

View File

@ -2,123 +2,101 @@ package autocomplete
import ( import (
"fmt" "fmt"
"sort"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/config"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/sirupsen/logrus"
"github.com/urfave/cli"
) )
// AppNameComplete copletes app names. // AppNameComplete copletes app names.
func AppNameComplete() ([]string, cobra.ShellCompDirective) { func AppNameComplete(c *cli.Context) {
appFiles, err := app.LoadAppFiles("") appNames, err := config.GetAppNames()
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) logrus.Warn(err)
return []string{err}, cobra.ShellCompDirectiveError
} }
var appNames []string if c.NArg() > 0 {
for appName := range appFiles { return
appNames = append(appNames, appName)
} }
return appNames, cobra.ShellCompDirectiveDefault for _, a := range appNames {
fmt.Println(a)
}
} }
func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) { func ServiceNameComplete(appName string) {
serviceNames, err := app.GetAppServiceNames(appName) serviceNames, err := config.GetAppServiceNames(appName)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) return
return []string{err}, cobra.ShellCompDirectiveError }
for _, s := range serviceNames {
fmt.Println(s)
} }
return serviceNames, cobra.ShellCompDirectiveDefault
} }
// RecipeNameComplete completes recipe names. // RecipeNameComplete completes recipe names.
func RecipeNameComplete() ([]string, cobra.ShellCompDirective) { func RecipeNameComplete(c *cli.Context) {
catl, err := recipe.ReadRecipeCatalogue(false) catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) logrus.Warn(err)
return []string{err}, cobra.ShellCompDirectiveError }
if c.NArg() > 0 {
return
} }
var recipeNames []string
for name := range catl { for name := range catl {
recipeNames = append(recipeNames, name) fmt.Println(name)
} }
return recipeNames, cobra.ShellCompDirectiveDefault
} }
// RecipeVersionComplete completes versions for the recipe. // RecipeVersionComplete completes versions for the recipe.
func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) { func RecipeVersionComplete(recipeName string) {
catl, err := recipe.ReadRecipeCatalogue(true) catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) logrus.Warn(err)
return []string{err}, cobra.ShellCompDirectiveError
} }
var recipeVersions []string
for _, v := range catl[recipeName].Versions { for _, v := range catl[recipeName].Versions {
for v2 := range v { for v2 := range v {
recipeVersions = append(recipeVersions, v2) fmt.Println(v2)
} }
} }
return recipeVersions, cobra.ShellCompDirectiveDefault
} }
// ServerNameComplete completes server names. // ServerNameComplete completes server names.
func ServerNameComplete() ([]string, cobra.ShellCompDirective) { func ServerNameComplete(c *cli.Context) {
files, err := app.LoadAppFiles("") files, err := config.LoadAppFiles("")
if err != nil { if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err) logrus.Fatal(err)
return []string{err}, cobra.ShellCompDirectiveError }
if c.NArg() > 0 {
return
} }
var serverNames []string
for _, appFile := range files { for _, appFile := range files {
serverNames = append(serverNames, appFile.Server) fmt.Println(appFile.Server)
} }
return serverNames, cobra.ShellCompDirectiveDefault
} }
// CommandNameComplete completes recipe commands. // SubcommandComplete completes sub-commands.
func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) { func SubcommandComplete(c *cli.Context) {
app, err := app.Get(appName) if c.NArg() > 0 {
if err != nil { return
err := fmt.Sprintf("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
} }
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) subcmds := []string{
if err != nil { "app",
err := fmt.Sprintf("autocomplete failed: %s", err) "autocomplete",
return []string{err}, cobra.ShellCompDirectiveError "catalogue",
"recipe",
"server",
"upgrade",
} }
sort.Strings(cmdNames) for _, cmd := range subcmds {
fmt.Println(cmd)
return cmdNames, cobra.ShellCompDirectiveDefault }
}
// SecretsComplete completes recipe secrets.
func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
r := recipe.Get(recipeName)
config, err := r.GetComposeConfig(nil)
if err != nil {
err := fmt.Sprintf("autocomplete failed: %s", err)
return []string{err}, cobra.ShellCompDirectiveError
}
var secretNames []string
for name := range config.Secrets {
secretNames = append(secretNames, name)
}
return secretNames, cobra.ShellCompDirectiveDefault
} }

View File

@ -8,20 +8,21 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
) )
// EnsureCatalogue ensures that the catalogue is cloned locally & present. // EnsureCatalogue ensures that the catalogue is cloned locally & present.
func EnsureCatalogue() error { func EnsureCatalogue() error {
catalogueDir := path.Join(config.ABRA_DIR, "catalogue") catalogueDir := path.Join(config.ABRA_DIR, "catalogue")
if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) { if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) {
log.Debugf("catalogue is missing, retrieving now") logrus.Warnf("local recipe catalogue is missing, retrieving now")
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME) url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME)
if err := gitPkg.Clone(catalogueDir, url); err != nil { if err := gitPkg.Clone(catalogueDir, url); err != nil {
return err return err
} }
logrus.Debugf("cloned catalogue repository to %s", catalogueDir)
} }
return nil return nil
@ -56,7 +57,7 @@ func EnsureUpToDate() error {
if len(remotes) == 0 { if len(remotes) == 0 {
msg := "cannot ensure %s is up-to-date, no git remotes configured" msg := "cannot ensure %s is up-to-date, no git remotes configured"
log.Debugf(msg, config.CATALOGUE_DIR) logrus.Debugf(msg, config.CATALOGUE_DIR)
return nil return nil
} }
@ -81,7 +82,7 @@ func EnsureUpToDate() error {
} }
} }
log.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR) logrus.Debugf("fetched latest git changes for %s", config.CATALOGUE_DIR)
return nil return nil
} }

View File

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

View File

@ -5,11 +5,11 @@ import (
"fmt" "fmt"
"coopcloud.tech/abra/pkg/context" "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/log"
commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn" commandconnPkg "coopcloud.tech/abra/pkg/upstream/commandconn"
dConfig "github.com/docker/cli/cli/config" dConfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/docker"
contextStore "github.com/docker/cli/cli/context/store" contextStore "github.com/docker/cli/cli/context/store"
"github.com/sirupsen/logrus"
) )
type Context = contextStore.Metadata type Context = contextStore.Metadata
@ -22,7 +22,7 @@ func CreateContext(contextName string) error {
return err return err
} }
log.Debugf("created the %s context", contextName) logrus.Debugf("created the %s context", contextName)
return nil return nil
} }

View File

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

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

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

View File

@ -1,124 +0,0 @@
package config
import (
"os"
"path"
"path/filepath"
"coopcloud.tech/abra/pkg/log"
"gopkg.in/yaml.v3"
)
// LoadAbraConfig returns the abra configuration. It tries to find a abra
// configuration file (see findAbraConfig for lookup logic). When no
// configuration was found it returns the default config.
func LoadAbraConfig() Abra {
wd, _ := os.Getwd()
configFile := findAbraConfig(wd)
if configFile == "" {
log.Debugf("no config file found")
return Abra{}
}
data, err := os.ReadFile(configFile)
if err != nil {
// Do nothing, when an error occurs
log.Debugf("error reading config file: %s", err)
return Abra{}
}
config := Abra{}
err = yaml.Unmarshal(data, &config)
if err != nil {
// Do nothing, when an error occurs
log.Debugf("error loading config file: %s", err)
return Abra{}
}
log.Debugf("config file loaded from: %s", configFile)
config.configPath = filepath.Dir(configFile)
return config
}
// findAbraConfig recursively looks for a abra.y(a)ml file in the given directory.
// When the file was not found it calls the function again with the parent
// directory until the home directory is hit. When no abra config was found it
// returns an empty string.
func findAbraConfig(dir string) string {
dir, err := filepath.Abs(dir)
if err != nil {
return ""
}
if dir == os.ExpandEnv("$HOME") || dir == "/" {
return ""
}
p := path.Join(dir, "abra.yaml")
if _, err := os.Stat(p); err == nil {
return p
}
p = path.Join(dir, "abra.yml")
if _, err := os.Stat(p); err == nil {
return p
}
return findAbraConfig(filepath.Dir(dir))
}
// Abra defines the configuration file for abra.
type Abra struct {
configPath string
AbraDir string `yaml:"abraDir"`
}
// GetAbraDir returns the abra dir. It has the following logic:
// 1. check if $ABRA_DIR is set
// 2. check if abraDir was set in a config file
// 3. use $HOME/.abra when above two options failed
func (a Abra) GetAbraDir() string {
if dir, exists := os.LookupEnv("ABRA_DIR"); exists && dir != "" {
log.Debug("read abra dir from $ABRA_DIR")
return dir
}
if a.AbraDir != "" {
log.Debug("read abra dir from config file")
if path.IsAbs(a.AbraDir) {
return a.AbraDir
}
// Make the path absolute
return path.Join(a.configPath, a.AbraDir)
}
log.Debug("using default abra dir")
return os.ExpandEnv("$HOME/.abra")
}
func (a Abra) GetServersDir() string { return path.Join(a.GetAbraDir(), "servers") }
func (a Abra) GetRecipesDir() string { return path.Join(a.GetAbraDir(), "recipes") }
func (a Abra) GetVendorDir() string { return path.Join(a.GetAbraDir(), "vendor") }
func (a Abra) GetBackupDir() string { return path.Join(a.GetAbraDir(), "backups") }
func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catalogue") }
var config = LoadAbraConfig()
var (
ABRA_DIR = config.GetAbraDir()
SERVERS_DIR = config.GetServersDir()
RECIPES_DIR = config.GetRecipesDir()
VENDOR_DIR = config.GetVendorDir()
BACKUP_DIR = config.GetBackupDir()
CATALOGUE_DIR = config.GetCatalogueDir()
RECIPES_JSON = path.Join(config.GetCatalogueDir(), "recipes.json")
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
TOOLSHED_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/toolshed/%s.git"
RECIPES_SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
// NOTE(d1): please note, this was done purely out of laziness on our part
// AFAICR. it's easy to punt the value into the label because that is what is
// expects. it's not particularly useful in terms of UI/UX but hey, nobody
// complained yet!
CHAOS_DEFAULT = "false"
DIRTY_DEFAULT = "+U"
NO_DOMAIN_DEFAULT = "N/A"
NO_VERSION_DEFAULT = "N/A"
UNKNOWN_DEFAULT = "unknown"
)

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,12 +6,12 @@ import (
"strings" "strings"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container" containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
) )
// GetContainer retrieves a container. If noInput is false and the retrievd // GetContainer retrieves a container. If noInput is false and the retrievd
@ -43,7 +43,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
return types.Container{}, err return types.Container{}, err
} }
log.Warnf("ambiguous container list received, prompting for input") logrus.Warnf("ambiguous container list received, prompting for input")
var response string var response string
prompt := &survey.Select{ prompt := &survey.Select{
@ -64,7 +64,7 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
} }
} }
log.Fatal("failed to match chosen container") logrus.Panic("failed to match chosen container")
} }
return containers[0], nil return containers[0], nil

View File

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

View File

@ -17,8 +17,8 @@ func TestEnsureDomainsResolveSameIPv4(t *testing.T) {
// within the federation. if you're here because of a failing test, try // within the federation. if you're here because of a failing test, try
// `dig +short <domain>` to ensure stuff matches first! If flakyness // `dig +short <domain>` to ensure stuff matches first! If flakyness
// becomes an issue we can look into mocking // becomes an issue we can look into mocking
{"docs.coopcloud.tech", "swarm-0.coopcloud.tech", true},
{"docs.coopcloud.tech", "coopcloud.tech", true}, {"docs.coopcloud.tech", "coopcloud.tech", true},
{"docs.coopcloud.tech", "swarm.autonomic.zone", true},
// NOTE(d1): special case handling for "--local" // NOTE(d1): special case handling for "--local"
{"", "default", true}, {"", "default", true},

View File

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

View File

@ -1,30 +1,18 @@
package formatter package formatter
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/docker/go-units" "github.com/docker/go-units"
"golang.org/x/term" // "github.com/olekukonko/tablewriter"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
"github.com/schollz/progressbar/v3" "github.com/schollz/progressbar/v3"
"github.com/sirupsen/logrus"
) )
var BoldStyle = lipgloss.NewStyle().
Bold(true)
var BoldUnderlineStyle = lipgloss.NewStyle().
Bold(true).
Underline(true)
func ShortenID(str string) string { func ShortenID(str string) string {
return str[:12] return str[:12]
} }
@ -46,168 +34,11 @@ func HumanDuration(timestamp int64) string {
} }
// CreateTable prepares a table layout for output. // CreateTable prepares a table layout for output.
func CreateTable() (*table.Table, error) { func CreateTable(columns []string) *jsontable.JSONTable {
var ( table := jsontable.NewJSONTable(os.Stdout)
renderer = lipgloss.NewRenderer(os.Stdout) table.SetAutoWrapText(false)
headerStyle = renderer.NewStyle().Bold(true).Align(lipgloss.Center) table.SetHeader(columns)
cellStyle = renderer.NewStyle().Padding(0, 1) return table
borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
)
table := table.New().
Border(lipgloss.ThickBorder()).
BorderStyle(borderStyle).
StyleFunc(func(row, col int) lipgloss.Style {
var style lipgloss.Style
switch {
case row == table.HeaderRow:
return headerStyle
default:
style = cellStyle
}
return style
})
return table, nil
}
func PrintTable(t *table.Table) error {
if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" {
// NOTE(d1): no width limits for CI testing since we test against outputs
log.Debug("detected ABRA_CI=1")
fmt.Println(t)
return nil
}
tWidth, _ := lipgloss.Size(t.String())
width, _, err := term.GetSize(0)
if err != nil {
return err
}
if tWidth > width {
t.Width(width - 10)
}
fmt.Println(t)
return nil
}
// horizontal is a JoinHorizontal helper function.
func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Right, left, mid, right)
}
func CreateOverview(header string, rows [][]string) string {
var borderStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Padding(0, 1, 0, 1).
BorderForeground(lipgloss.Color("63"))
var headerStyle = lipgloss.NewStyle().
Underline(true).
Bold(true).
PaddingBottom(1)
var leftStyle = lipgloss.NewStyle()
var rightStyle = lipgloss.NewStyle()
var longest int
for _, row := range rows {
if len(row[0]) > longest {
longest = len(row[0])
}
}
var renderedRows []string
for _, row := range rows {
if len(row) < 2 {
continue
}
if len(row) > 2 {
panic("CreateOverview: only accepts rows of len == 2")
}
lenOffset := 4
if len(row[0]) < longest {
lenOffset += longest - len(row[0])
}
offset := ""
for range lenOffset {
offset = offset + " "
}
rendered := horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1]))
if row[1] == "---" {
rendered = horizontal(
leftStyle.
Bold(true).
Underline(true).
PaddingTop(1).
Render(row[0]),
offset,
rightStyle.Render(""),
)
}
renderedRows = append(renderedRows, rendered)
}
body := strings.Builder{}
body.WriteString(
borderStyle.Render(
lipgloss.JoinVertical(
lipgloss.Center,
headerStyle.Render(header),
lipgloss.JoinVertical(
lipgloss.Left,
renderedRows...,
),
),
),
)
return body.String()
}
// ToJSON converts a lipgloss.Table to JSON representation. It's not a robust
// implementation and mainly caters for our current use case which is basically
// a bunch of strings. See https://github.com/charmbracelet/lipgloss/issues/335
// for the real thing (hopefully).
func ToJSON(headers []string, rows [][]string) (string, error) {
var buff bytes.Buffer
buff.Write([]byte("["))
for idx, row := range rows {
payload := make(map[string]string)
for idx, header := range headers {
payload[strings.ToLower(header)] = row[idx]
}
serialized, err := json.Marshal(payload)
if err != nil {
return "", err
}
buff.Write(serialized)
if idx < (len(rows) - 1) {
buff.Write([]byte(","))
}
}
buff.Write([]byte("]"))
return buff.String(), nil
} }
// CreateProgressbar generates a progress bar // CreateProgressbar generates a progress bar
@ -217,6 +48,7 @@ func CreateProgressbar(length int, title string) *progressbar.ProgressBar {
progressbar.OptionClearOnFinish(), progressbar.OptionClearOnFinish(),
progressbar.OptionSetPredictTime(false), progressbar.OptionSetPredictTime(false),
progressbar.OptionShowCount(), progressbar.OptionShowCount(),
progressbar.OptionFullWidth(),
progressbar.OptionSetDescription(title), progressbar.OptionSetDescription(title),
) )
} }
@ -234,7 +66,7 @@ func StripTagMeta(image string) string {
} }
if originalImage != image { if originalImage != image {
log.Debugf("stripped %s to %s for parsing", originalImage, image) logrus.Debugf("stripped %s to %s for parsing", originalImage, image)
} }
return image return image
@ -257,18 +89,3 @@ func ByteCountSI(b uint64) string {
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
} }
// BoldDirtyDefault ensures a dirty modifier is rendered in bold.
func BoldDirtyDefault(v string) string {
if strings.HasSuffix(v, config.DIRTY_DEFAULT) {
vBold := BoldStyle.Render(config.DIRTY_DEFAULT)
v = strings.Replace(v, config.DIRTY_DEFAULT, vBold, 1)
}
return v
}
// AddDirtyMarker adds the dirty marker to a version string.
func AddDirtyMarker(v string) string {
return fmt.Sprintf("%s%s", v, config.DIRTY_DEFAULT)
}

View File

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

View File

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

View File

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

View File

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

View File

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

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