0
0
forked from toolshed/abra

Compare commits

..

97 Commits

Author SHA1 Message Date
7a99e0485b fix: app ls -S didn't show updates sometimes (#561) 2025-05-27 17:24:12 +02:00
2fbef41a3a
test: clean up properly 2025-04-24 14:29:54 +02:00
6fb41e5300 fix: dont parse chaos version
See toolshed/abra#547
2025-04-24 11:24:14 +00:00
1432f480c7 fix: -T/--tty disables TTY remote request
See toolshed/abra#499
2025-04-24 08:57:53 +00:00
83af39771b
test: drop unused tagHash 2025-04-24 10:57:34 +02:00
4d1333202e
test: flaky test when no RC is available
Fixes https://build.coopcloud.tech/toolshed/abra/2760/1/5
2025-04-24 10:33:49 +02:00
55c24f070c
feat: cancel git clone ops gracefully
See toolshed/abra#528
2025-04-22 22:56:10 +02:00
229e8eb9da feat: --ssh/--force for recipe fetch
See toolshed/organising#546
2025-04-22 09:35:36 +00:00
b3ab95750e
fix: trim final newline on release note
Follow-up toolshed/abra#544
2025-04-22 09:23:28 +02:00
de009921a2 fix: show release notes once
See toolshed/abra#543
2025-04-22 05:47:03 +00:00
d081bbaefa
feat: auto select single server
See toolshed/organising#513
2025-04-21 21:06:29 +02:00
515b5466ca
docs: add missing arg
See toolshed/abra#540
2025-04-21 20:17:39 +02:00
6965799bdc
chore: publish 0.10.0-beta 2025-04-21 19:11:31 +02:00
f75c9a6259
test: clean up test server correctly
Fixes https://build.coopcloud.tech/toolshed/abra/2723/1/5
2025-04-21 19:03:49 +02:00
a43a092ba7 fix: fetch recipe for "app list -S" 2025-04-19 07:28:15 +00:00
fa084a61d2 fix(lint): Improves error message if a lint rule errors
This was detected while debugging toolshed/abra#534
2025-04-16 05:12:19 +00:00
895a7fe7d6
fix: don't overwrite recipeVersion
Fixes https://build.coopcloud.tech/toolshed/abra/2709/1/5
2025-04-15 10:51:53 +02:00
742a726778
fix: latest commit for new recipe version
See toolshed/abra#527
2025-04-14 23:55:19 +02:00
2b9a185aff
build: go mod tidy 2025-03-23 11:10:05 +01:00
b7c1e87c0b
build: go mod vendor 2025-03-23 11:08:17 +01:00
cdfb8a08bb
chore: publish 0.10.0-rc2-beta 2025-03-23 11:05:03 +01:00
8943cea13f
test: get latest via helper 2025-03-23 11:00:04 +01:00
6d64e0edd3
fix: sshPkg.Fatal has more nuance
See toolshed/abra#507
2025-03-23 10:27:58 +01:00
47045ca8f1 feat: improved deploy progress reporting
See toolshed/abra#478
2025-03-23 09:13:36 +00:00
d0f982456e add charset modifier to secret generation (#521)
since we need special chars in passwords for a recipe we are working on, i have added the option to specify a charset in the same way as the length can be setted.
i did not change anything in the behaviour, so if length is not specified, the charset gets ignored whether it is there or not.

you can specify the following:
`charset=default` - Results in passgen.AlphabetDefault being used
`charset=special` - Results in passgen.AlphabetSpecial being used
`charset=safespecial` - Results in `!@#%^&*_-+=` being used (so it is AlphabetSpecial without the dollar sign)
`charset=default,special` or `charset=special,default` - Results in passgen.AlphabetDefault + passgen.AlphabetSpecial being used
`charset=default,safespecial` or `charset=safespecial,default` - Results in passgen.AlphabetDefault + `!@#%^&*_-+=` being used ((so it is AlphabetSpecial without the dollar sign)

PR for the docs: toolshed/docs.coopcloud.tech#271

Co-authored-by: p4u1 <p4u1@noreply.git.coopcloud.tech>
Reviewed-on: toolshed/abra#521
Reviewed-by: p4u1 <p4u1@noreply.git.coopcloud.tech>
Co-authored-by: Apfelwurm <Alexander@volzit.de>
Co-committed-by: Apfelwurm <Alexander@volzit.de>
2025-03-21 10:29:21 +00:00
80ad6c6681 fix(app): Properly detects release notes added after a release when upgrading an app (#523)
Fixes toolshed/abra#488

Reviewed-on: toolshed/abra#523
Reviewed-by: decentral1se <decentral1se@noreply.git.coopcloud.tech>
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2025-03-21 10:25:46 +00:00
cb63cfe9c2
refactor: chaos redundant, shorter message 2025-03-16 13:54:07 +01:00
d1e49d17ce
test: on-demand integration tests 2025-03-16 13:22:50 +01:00
1574aa0631
refactor!: status between service/image
See toolshed/abra#487 (comment)
2025-03-16 12:42:09 +01:00
1723025fbf
build: go 1.24
We were running behind and there were quite some deprecations to update.
This was mostly in the upstream copy/pasta package but seems quite
minimal.
2025-03-16 12:31:45 +01:00
a2b678caf6
test: reset after undeploy for a clean env version
Follows toolshed/abra#510.
See https://build.coopcloud.tech/toolshed/abra/2620/1/5.
2025-03-16 11:49:38 +01:00
0a371ec360 fix: integration tests 2025-03-13 08:31:11 +01:00
e58a716fe1 feat(deploy): Simplifies deploy overview (#508)
This simplifies the deploy overview, to only show 3 version fields:
- CURRENT DEPLOYMENT
- CURRENT ENV
- NEW DEPLOYMENT

It also fixes a few errors around version detection

Reviewed-on: toolshed/abra#508
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2025-03-12 16:13:24 +00:00
d09a19a385 fix: Adds chaos flag to restart command 2025-02-11 10:01:44 +00:00
cee808ff06 fix: Changes how the deploy version is detected in app deploy command 2025-02-11 10:01:44 +00:00
4326d1d259 fix: Sorts git tags with tagcmp 2025-02-11 10:01:44 +00:00
b976872f77 fix(overview): Adds linebreak after compose file in deploy overview 2025-02-11 09:57:09 +00:00
7b6ea76437 fix(secret): Checks for enough arguments 2025-02-11 09:55:03 +00:00
9069758969 fix(cmd): Uses uppercase t for tty shorthand flag 2025-02-10 15:13:26 +01:00
15d6b1a2a5 fix: app new with chaos should just take the local repo as it is (#495)
Fixes toolshed/abra#494

Reviewed-on: toolshed/abra#495
Co-authored-by: p4u1 <p4u1_f4u1@riseup.net>
Co-committed-by: p4u1 <p4u1_f4u1@riseup.net>
2025-02-10 14:00:42 +00:00
8a7fe4ca07
fix: prompt, skip adding if next present
toolshed/abra#486
2025-01-17 17:46:41 +01:00
64ad60663f test: adjust for new abra-test-recipe version
See toolshed/abra#470
2025-01-09 13:14:47 +00:00
cb3f46b46e fix: redirect to stderr for machine output
See toolshed/abra#477
2025-01-09 11:23:36 +00:00
41e514ae9a
test: reset after deploy 2025-01-09 11:54:39 +01:00
086b4828ff
docs: better comments, remove redundant output check 2025-01-09 11:54:38 +01:00
ed263854d4
fix: show N/A if env version unknown
See toolshed/abra#478
2025-01-09 11:54:37 +01:00
eb6fe4ba6e
fix: dont set chaos label if no chaos
See toolshed/abra#478
2025-01-09 11:54:36 +01:00
993172d31b test: ensure .env version written 2025-01-08 13:42:35 +00:00
c70b6e72a7 test: ensure unstaged changes preserved 2025-01-08 13:42:35 +00:00
22e4dd7fca fix: app new from chaos changes
See toolshed/abra#471
2025-01-08 13:42:35 +00:00
b6009057a8
docs: note temp autocomplete, less whitespace 2025-01-08 12:10:17 +01:00
b978f04910
fix: use "sudo tee" to avoid permissions error
See toolshed/abra#474
2025-01-08 12:09:51 +01:00
3ac29d54d9
chore: go update des/vendor 2025-01-07 16:59:56 +01:00
877c17fab5
test: re-enable this one 2025-01-05 16:46:48 +01:00
f01fd26ce3
test: git status output 2025-01-05 16:46:38 +01:00
273c165a41
docs: --chaos/-C handling for catalogue generate 2025-01-05 16:46:20 +01:00
c88fc66c99
test: moar chaos stability 😌 [ci skip] 2025-01-05 16:12:06 +01:00
9b271a6963
docs: moar authors [ci skip] 2025-01-05 15:53:17 +01:00
8af87aa382
chore: upgrade goreleaser 2025-01-05 12:47:46 +01:00
ac0b9cd052
chore: new RC 2025-01-05 12:42:42 +01:00
4923984e84
fix: not flaky catalogue generate
See toolshed/abra#464
2025-01-05 12:08:10 +01:00
2bc77de751
test: ensure main branch on new recipe 2025-01-05 10:38:34 +01:00
b3a2402cec
chore: remove redundant logging 2025-01-05 10:38:24 +01:00
a773fd4256
chore: spacing 2025-01-05 10:38:13 +01:00
b1a0d54bd3
fix: default to main then master 2025-01-05 10:37:30 +01:00
3869d6bce9
Revert "test: try uppercase naming (following UI)"
This reverts commit 0ff07ab224e3e2195c502b729b09da87d1312957.

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

View File

@ -3,14 +3,14 @@ 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.24
commands: commands:
- make check - make check
- name: make test - name: make test
image: golang:1.22 image: golang:1.24
environment: environment:
CATL_URL: https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git
commands: commands:
- mkdir -p $HOME/.abra - mkdir -p $HOME/.abra
- git clone $CATL_URL $HOME/.abra/catalogue - git clone $CATL_URL $HOME/.abra/catalogue
@ -29,7 +29,7 @@ steps:
event: tag event: tag
- name: release - name: release
image: goreleaser/goreleaser:v1.24.0 image: goreleaser/goreleaser:v2.5.1
environment: environment:
GITEA_TOKEN: GITEA_TOKEN:
from_secret: goreleaser_gitea_token from_secret: goreleaser_gitea_token
@ -47,10 +47,10 @@ steps:
image: plugins/docker image: plugins/docker
settings: settings:
auto_tag: true auto_tag: true
username: 3wordchant username: abra-bot
password: password:
from_secret: git_coopcloud_tech_token_3wc from_secret: git_coopcloud_tech_token_abra_bot
repo: git.coopcloud.tech/coop-cloud/abra repo: git.coopcloud.tech/toolshed/abra
tags: dev tags: dev
registry: git.coopcloud.tech registry: git.coopcloud.tech
when: when:
@ -60,7 +60,7 @@ steps:
- make check - make check
- make test - make test
- name: integration test - name: on-demand integration test
image: appleboy/drone-ssh image: appleboy/drone-ssh
settings: settings:
host: host:
@ -74,7 +74,31 @@ steps:
request_pty: true request_pty: true
script: script:
- | - |
wget https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
chmod +x run-ci-int
sh run-ci-int
when:
ref:
- refs/heads/int-*
depends_on:
- make check
- make test
- name: nightly integration test
image: appleboy/drone-ssh
settings:
host:
- int.coopcloud.tech
username: abra
key:
from_secret: abra_int_private_key
port: 22
command_timeout: 60m
script_stop: true
request_pty: true
script:
- |
wget https://git.coopcloud.tech/toolshed/abra/raw/branch/main/scripts/tests/run-ci-int -O run-ci-int
chmod +x run-ci-int chmod +x run-ci-int
sh run-ci-int sh run-ci-int
when: when:
@ -87,3 +111,8 @@ steps:
volumes: volumes:
- name: deps - name: deps
temp: {} temp: {}
trigger:
action:
exclude:
- synchronized

View File

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

View File

@ -4,6 +4,7 @@
> 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
@ -17,3 +18,5 @@
- 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.24-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,7 +2,7 @@ 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.24
LDFLAGS := "-X 'main.Commit=$(COMMIT)'" LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
DIST_LDFLAGS := $(LDFLAGS)" -s -w" DIST_LDFLAGS := $(LDFLAGS)" -s -w"
GCFLAGS := "all=-l -B" GCFLAGS := "all=-l -B"

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import (
) )
var AppCmdCommand = &cobra.Command{ var AppCmdCommand = &cobra.Command{
Use: "command <app> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]", Use: "command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
Aliases: []string{"cmd"}, Aliases: []string{"cmd"},
Short: "Run app commands", Short: "Run app commands",
Long: `Run an app specific command. Long: `Run an app specific command.
@ -92,7 +92,7 @@ does not).`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -183,7 +183,7 @@ does not).`,
if err := internal.RunCmdRemote( if err := internal.RunCmdRemote(
cl, cl,
app, app,
requestTTY, disableTTY,
app.Recipe.AbraShPath, app.Recipe.AbraShPath,
targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil { targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
log.Fatal(err) log.Fatal(err)
@ -192,14 +192,14 @@ does not).`,
} }
var AppCmdListCommand = &cobra.Command{ var AppCmdListCommand = &cobra.Command{
Use: "list <app> [flags]", Use: "list <domain> [flags]",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Short: "List all available commands", Short: "List all available commands",
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -238,7 +238,7 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
var ( var (
local bool local bool
remoteUser string remoteUser string
requestTTY bool disableTTY bool
) )
func init() { func init() {
@ -259,11 +259,11 @@ func init() {
) )
AppCmdCommand.Flags().BoolVarP( AppCmdCommand.Flags().BoolVarP(
&requestTTY, &disableTTY,
"tty", "tty",
"t", "T",
false, false,
"request remote TTY", "disable remote TTY",
) )
AppCmdCommand.Flags().BoolVarP( AppCmdCommand.Flags().BoolVarP(

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import (
"coopcloud.tech/abra/pkg/log" "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" containertypes "github.com/docker/docker/api/types/container"
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"
@ -26,14 +26,14 @@ import (
) )
var AppCpCommand = &cobra.Command{ var AppCpCommand = &cobra.Command{
Use: "cp <app> <src> <dst> [flags]", Use: "cp <domain> <src> <dst> [flags]",
Aliases: []string{"c"}, Aliases: []string{"c"},
Short: "Copy files to/from a deployed app service", Short: "Copy files to/from a deployed app service",
Example: ` # copy myfile.txt to the root of the app service Example: ` # copy myfile.txt to the root of the app service
abra app cp 1312.net myfile.txt app:/ abra app cp 1312.net myfile.txt app:/
# copy that file back to your current working directory locally # copy that file back to your current working directory locally
abra app cp 1312.net app:/myfile.txt`, abra app cp 1312.net app:/myfile.txt ./`,
Args: cobra.ExactArgs(3), Args: cobra.ExactArgs(3),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -49,7 +49,7 @@ var AppCpCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -134,7 +134,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
if err != nil { if err != nil {
return err return err
} }
if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{ if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,
@ -162,7 +162,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
} }
log.Debugf("copy %s from local to %s on container", srcPath, dstPath) log.Debugf("copy %s from local to %s on container", srcPath, dstPath)
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} copyOpts := containertypes.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
} }
@ -173,7 +173,7 @@ func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath stri
if err != nil { if err != nil {
return err return err
} }
if _, err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{ if _, err := container.RunExec(dcli, cl, containerID, &containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,

View File

@ -2,10 +2,11 @@ package app
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
@ -18,18 +19,19 @@ import (
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppDeployCommand = &cobra.Command{ var AppDeployCommand = &cobra.Command{
Use: "deploy <app> [version] [flags]", Use: "deploy <domain> [version] [flags]",
Aliases: []string{"d"}, Aliases: []string{"d"},
Short: "Deploy an app", Short: "Deploy an app",
Long: `Deploy an app. Long: `Deploy an app.
This command supports chaos operations. Use "--chaos/-c" to deploy your recipe This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported values for "[version]". checkout as-is. Recipe commit hashes are also supported as values for
Please note, "upgrade"/"rollback" do not support chaos operations.`, "[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`,
Example: ` # standard deployment Example: ` # standard deployment
abra app deploy 1312.net abra app deploy 1312.net
@ -45,7 +47,8 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
args []string, args []string,
toComplete string) ([]string, cobra.ShellCompDirective) { toComplete string,
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l { switch l := len(args); l {
case 0: case 0:
return autocomplete.AppNameComplete() return autocomplete.AppNameComplete()
@ -61,124 +64,55 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
} }
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var warnMessages []string var (
deployWarnMessages []string
toDeployVersion string
)
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
stackName := app.StackName()
ok, err := validateChaosXORVersion(args) if err := validateArgsAndFlags(args); err != nil {
if !ok {
log.Fatalf(err.Error())
}
specificVersion := getSpecifiedVersion(args)
if specificVersion != "" {
log.Debugf("overriding env file version (%s) with %s", app.Recipe.Version, specificVersion)
app.Recipe.Version = specificVersion
}
if specificVersion == "" && app.Recipe.Version != "" && !internal.Chaos {
log.Debugf("retrieved %s as version from env file", app.Recipe.Version)
specificVersion = app.Recipe.Version
}
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if err := lint.LintForErrors(app.Recipe); err != nil { if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("checking whether %s is already deployed", stackName)
cl, err := client.New(app.Server) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName) log.Debugf("checking whether %s is already deployed", app.StackName())
deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// NOTE(d1): handles "<version> as git hash" use case
var isChaosCommit bool
// NOTE(d1): check out specific version before dealing with secrets. This
// is because we need to deal with GetComposeFiles under the hood and these
// files change from version to version which therefore affects which
// secrets might be generated
toDeployVersion := deployMeta.Version
if specificVersion != "" {
toDeployVersion = specificVersion
log.Debugf("choosing %s as version to deploy", toDeployVersion)
var err error
isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil {
log.Fatal(err)
}
if isChaosCommit {
log.Debugf("assuming '%s' is a chaos commit", toDeployVersion)
internal.Chaos = true
}
}
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
log.Fatal(err)
}
for _, secStat := range secStats {
if !secStat.CreatedOnRemote {
log.Fatalf("unable to deploy, secrets not generated (%s)?", secStat.LocalName)
}
}
if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) { if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
log.Fatalf("%s is already deployed", app.Name) log.Fatalf("%s is already deployed", app.Name)
} }
if !internal.Chaos && specificVersion == "" { toDeployVersion, err = getDeployVersion(args, deployMeta, app)
versions, err := app.Recipe.Tags() if err != nil {
if err != nil { log.Fatal(fmt.Errorf("get deploy version: %s", err))
log.Fatal(err) }
}
if len(versions) > 0 && !internal.Chaos { if !internal.Chaos {
toDeployVersion = versions[len(versions)-1] _, err = app.Recipe.EnsureVersion(toDeployVersion)
log.Debugf("choosing %s as version to deploy", toDeployVersion) if err != nil {
if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil { log.Fatalf("ensure recipe: %s", err)
log.Fatal(err)
}
} else {
head, err := app.Recipe.Head()
if err != nil {
log.Fatal(err)
}
toDeployVersion = formatter.SmallSHA(head.String())
} }
} }
toDeployChaosVersion := config.CHAOS_DEFAULT if err := lint.LintForErrors(app.Recipe); err != nil {
if internal.Chaos { log.Fatal(err)
if isChaosCommit { }
toDeployChaosVersion = specificVersion
versionLabelLocal, err := app.Recipe.GetVersionLabelLocal() if err := validateSecrets(cl, app); err != nil {
if err != nil { log.Fatal(err)
log.Fatal(err)
}
toDeployVersion = versionLabelLocal
} else {
var err error
toDeployChaosVersion, err = app.Recipe.ChaosVersion()
if err != nil {
log.Fatal(err)
}
}
} }
abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
@ -194,6 +128,7 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
log.Fatal(err) log.Fatal(err)
} }
stackName := app.StackName()
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
Composefiles: composeFiles, Composefiles: composeFiles,
Namespace: stackName, Namespace: stackName,
@ -209,8 +144,11 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
appPkg.ExposeAllEnv(stackName, compose, app.Env) appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion) if internal.Chaos {
appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
}
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)
appPkg.SetVersionLabel(compose, stackName, toDeployVersion)
envVars, err := appPkg.CheckEnv(app) envVars, err := appPkg.CheckEnv(app)
if err != nil { if err != nil {
@ -219,23 +157,22 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
for _, envVar := range envVars { for _, envVar := range envVars {
if !envVar.Present { if !envVar.Present {
warnMessages = append(warnMessages, deployWarnMessages = append(deployWarnMessages,
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain), fmt.Sprintf("%s missing from %s.env", envVar.Name, app.Domain),
) )
} }
} }
if !internal.NoDomainChecks { if !internal.NoDomainChecks {
domainName, ok := app.Env["DOMAIN"] if domainName, ok := app.Env["DOMAIN"]; ok {
if ok {
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil { if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
log.Debug("skipping domain checks as no DOMAIN=... configured for app") log.Debug("skipping domain checks, no DOMAIN=... configured")
} }
} else { } else {
log.Debug("skipping domain checks as requested") log.Debug("skipping domain checks")
} }
deployedVersion := config.NO_VERSION_DEFAULT deployedVersion := config.NO_VERSION_DEFAULT
@ -245,11 +182,11 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
if err := internal.DeployOverview( if err := internal.DeployOverview(
app, app,
warnMessages,
deployedVersion, deployedVersion,
deployMeta.ChaosVersion,
toDeployVersion, toDeployVersion,
toDeployChaosVersion); err != nil { "",
deployWarnMessages,
); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -257,9 +194,28 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("set waiting timeout to %d s", stack.WaitTimeout)
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil { log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
log.Fatal(err)
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
log.Fatal(err)
}
if err := stack.RunDeploy(
cl,
deployOpts,
compose,
app.Name,
app.Server,
internal.DontWaitConverge,
f,
); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -271,31 +227,92 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
} }
} }
app.Recipe.Version = toDeployVersion if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil {
if toDeployChaosVersion != config.CHAOS_DEFAULT { log.Fatalf("writing recipe version failed: %s", err)
app.Recipe.Version = toDeployChaosVersion
}
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
} }
}, },
} }
// validateChaosXORVersion xor checks version/chaos mode func getLatestVersionOrCommit(app app.App) (string, error) {
func validateChaosXORVersion(args []string) (bool, error) { versions, err := app.Recipe.Tags()
if getSpecifiedVersion(args) != "" && internal.Chaos { if err != nil {
return false, errors.New("cannot use <version> and --chaos together") return "", err
} }
return true, nil
if len(versions) > 0 && !internal.Chaos {
return versions[len(versions)-1], nil
}
head, err := app.Recipe.Head()
if err != nil {
return "", err
}
return formatter.SmallSHA(head.String()), nil
} }
// getSpecifiedVersion retrieves the specific version if available // validateArgsAndFlags ensures compatible args/flags.
func getSpecifiedVersion(args []string) string { func validateArgsAndFlags(args []string) error {
if len(args) >= 2 { if len(args) == 2 && args[1] != "" && internal.Chaos {
return args[1] return fmt.Errorf("cannot use [version] and --chaos together")
} }
return ""
return nil
}
func validateSecrets(cl *dockerClient.Client, app app.App) error {
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
return err
}
for _, secStat := range secStats {
if !secStat.CreatedOnRemote {
return fmt.Errorf("secret not generated: %s", secStat.LocalName)
}
}
return nil
}
func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app app.App) (string, error) {
// Chaos mode overrides everything
if internal.Chaos {
v, err := app.Recipe.ChaosVersion()
if err != nil {
return "", err
}
log.Debugf("version: taking chaos version: %s", v)
return v, nil
}
// Check if the deploy version is set with a cli argument
if len(cliArgs) == 2 && cliArgs[1] != "" {
log.Debugf("version: taking version from cli arg: %s", cliArgs[1])
return cliArgs[1], nil
}
// Check if the recipe has a version in the .env file
if app.Recipe.EnvVersion != "" && !internal.IgnoreEnvVersion {
if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") {
return "", fmt.Errorf("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw)
}
log.Debugf("version: taking version from .env file: %s", app.Recipe.EnvVersion)
return app.Recipe.EnvVersion, nil
}
// Take deployed version
if deployMeta.IsDeployed {
log.Debugf("version: taking deployed version: %s", deployMeta.Version)
return deployMeta.Version, nil
}
v, err := getLatestVersionOrCommit(app)
log.Debugf("version: taking new recipe version: %s", v)
if err != nil {
return "", err
}
return v, nil
} }
func init() { func init() {
@ -328,6 +345,6 @@ func init() {
"no-converge-checks", "no-converge-checks",
"c", "c",
false, false,
"do not wait for converge logic checks", "disable converge logic checks",
) )
} }

View File

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

View File

@ -1,51 +0,0 @@
package app
import (
"fmt"
"path/filepath"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
var AppDiffCommand = &cobra.Command{
Use: "diff <app> [flags]",
Aliases: []string{"df"},
Short: "Show diff of app env changes",
Long: `This command requires /usr/bin/git.`,
Example: " abra app diff 1312.net",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
gitDir := gitPkg.FindDir(app.Path)
if gitDir == "" {
log.Fatal(fmt.Errorf("no git repo found for %s", app.Name))
}
fpath := app.Path
realPath, err := filepath.EvalSymlinks(fpath)
if err != nil {
log.Fatalf("unable to app env: broken symlink: %s", fpath)
}
fpath = realPath
diff, err := gitPkg.DiffUnstaged(gitDir, fpath)
if err != nil {
log.Fatalf("unable to diff %s: %s", app.Name, err)
}
if diff != "" {
fmt.Print(diff)
}
},
}

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

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

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

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

View File

@ -142,10 +142,14 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
appStats.AutoUpdate = autoUpdate appStats.AutoUpdate = autoUpdate
var newUpdates []string var newUpdates []string
if version != "unknown" { if version != "unknown" && chaos == "false" {
if err := app.Recipe.EnsureExists(); err != nil {
log.Fatalf("unable to clone %s: %s", app.Name, err)
}
updates, err := app.Recipe.Tags() updates, err := app.Recipe.Tags()
if err != nil { if err != nil {
log.Fatal(err) log.Fatalf("unable to retrieve tags for %s: %s", app.Name, err)
} }
parsedVersion, err := tagcmp.Parse(version) parsedVersion, err := tagcmp.Parse(version)

View File

@ -3,28 +3,19 @@ package app
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"os"
"slices"
"sync"
"time"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/logs"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppLogsCommand = &cobra.Command{ var AppLogsCommand = &cobra.Command{
Use: "logs <app> [service] [flags]", Use: "logs <domain> [service] [flags]",
Aliases: []string{"l"}, Aliases: []string{"l"},
Short: "Tail app logs", Short: "Tail app logs",
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
@ -73,80 +64,25 @@ var AppLogsCommand = &cobra.Command{
serviceNames = []string{args[1]} serviceNames = []string{args[1]}
} }
if err = tailLogs(cl, app, serviceNames); err != nil { f, err := app.Filters(true, false, serviceNames...)
if err != nil {
log.Fatal(err)
}
opts := logs.TailOpts{
AppName: app.Name,
Services: serviceNames,
StdErr: stdErr,
Since: sinceLogs,
Filters: f,
}
if err := logs.TailLogs(cl, opts); err != nil {
log.Fatal(err) log.Fatal(err)
} }
}, },
} }
// tailLogs prints logs for the given app with optional service names to be
// filtered on. It also checks if the latest task is not runnning and then
// prints the past tasks.
func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) error {
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
return err
}
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: f})
if err != nil {
return err
}
var wg sync.WaitGroup
for _, service := range services {
filters := filters.NewArgs()
filters.Add("name", service.Spec.Name)
tasks, err := cl.TaskList(context.Background(), types.TaskListOptions{Filters: f})
if err != nil {
return err
}
if len(tasks) > 0 {
// Need to sort the tasks by the CreatedAt field in the inverse order.
// Otherwise they are in the reversed order and not sorted properly.
slices.SortFunc[[]swarm.Task](tasks, func(t1, t2 swarm.Task) int {
return int(t2.Meta.CreatedAt.Unix() - t1.Meta.CreatedAt.Unix())
})
lastTask := tasks[0].Status
if lastTask.State != swarm.TaskStateRunning {
for _, task := range tasks {
log.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err)
}
}
}
// Collect the logs in a go routine, so the logs from all services are
// collected in parallel.
wg.Add(1)
go func(serviceID string) {
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
ShowStderr: true,
ShowStdout: !stdErr,
Since: sinceLogs,
Until: "",
Timestamps: true,
Follow: true,
Tail: "20",
Details: false,
})
if err != nil {
log.Fatal(err)
}
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
log.Fatal(err)
}
}(service.ID)
}
// Wait for all log streams to be closed.
wg.Wait()
return nil
}
var ( var (
stdErr bool stdErr bool
sinceLogs string sinceLogs string

View File

@ -25,7 +25,7 @@ This new app configuration is stored in your $ABRA_DIR directory under the
appropriate server. appropriate server.
This command does not deploy your app for you. You will need to run "abra app This command does not deploy your app for you. You will need to run "abra app
deploy <app>" to do so. deploy <domain>" to do so.
You can see what recipes are available (i.e. values for the [recipe] argument) You can see what recipes are available (i.e. values for the [recipe] argument)
by running "abra recipe ls". by running "abra recipe ls".
@ -75,43 +75,50 @@ var AppNewCommand = &cobra.Command{
chaosVersion := config.CHAOS_DEFAULT chaosVersion := config.CHAOS_DEFAULT
if internal.Chaos { if internal.Chaos {
recipeVersion = chaosVersion
if !internal.Offline {
if err := recipe.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
}
}
var recipeVersions recipePkg.RecipeVersions
if recipeVersion == "" {
var err error var err error
recipeVersions, err = recipe.GetRecipeVersions() chaosVersion, err = recipe.ChaosVersion()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
}
if len(recipeVersions) > 0 { recipeVersion = chaosVersion
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
recipeVersion = tag
}
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
} else { } else {
if err := recipe.EnsureLatest(); err != nil { if err := recipe.EnsureIsClean(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
var recipeVersions recipePkg.RecipeVersions
if recipeVersion == "" {
var err error
recipeVersions, _, err = recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
}
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
recipeVersion = tag
}
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
} else {
if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
if recipeVersion == "" {
head, err := recipe.Head()
if err != nil {
log.Fatalf("failed to retrieve latest commit for %s: %s", recipe.Name, err)
}
recipeVersion = formatter.SmallSHA(head.String())
}
}
} }
if err := ensureServerFlag(); err != nil { if err := ensureServerFlag(); err != nil {
@ -187,7 +194,7 @@ var AppNewCommand = &cobra.Command{
newAppServer = "local" newAppServer = "local"
} }
log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion) log.Infof("%s created (version: %s)", appDomain, recipeVersion)
if len(appSecrets) > 0 { if len(appSecrets) > 0 {
rows := [][]string{} rows := [][]string{}
@ -201,8 +208,8 @@ var AppNewCommand = &cobra.Command{
log.Warnf( log.Warnf(
"secrets are %s shown again, please save them %s", "secrets are %s shown again, please save them %s",
formatter.BoldStyle.Render("NOT"), formatter.BoldUnderlineStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"), formatter.BoldUnderlineStyle.Render("NOW"),
) )
} }
@ -211,9 +218,8 @@ var AppNewCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
log.Debugf("choosing %s as version to save to env file", recipeVersion)
if err := app.WriteRecipeVersion(recipeVersion, false); err != nil { if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err) log.Fatalf("writing recipe version failed: %s", err)
} }
}, },
} }
@ -296,6 +302,12 @@ func ensureServerFlag() error {
return err return err
} }
if len(servers) == 1 {
newAppServer = servers[0]
log.Infof("single server detected, choosing %s automatically", newAppServer)
return nil
}
if newAppServer == "" && !internal.NoInput { if newAppServer == "" && !internal.NoInput {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Select app server:", Message: "Select app server:",

View File

@ -4,6 +4,8 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
@ -22,9 +24,9 @@ import (
) )
var AppPsCommand = &cobra.Command{ var AppPsCommand = &cobra.Command{
Use: "ps <app> [flags]", Use: "ps <domain> [flags]",
Aliases: []string{"p"}, Aliases: []string{"p"},
Short: "Check app status", Short: "Check app deployment status",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -35,7 +37,7 @@ var AppPsCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -57,9 +59,11 @@ var AppPsCommand = &cobra.Command{
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true) statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok { if statusMeta, ok := statuses[app.StackName()]; ok {
if isChaos, exists := statusMeta["chaos"]; exists && isChaos == "true" { if isChaos, exists := statusMeta["chaos"]; exists && isChaos == "true" {
chaosVersion, err = app.Recipe.ChaosVersion() if cVersion, exists := statusMeta["chaosVersion"]; exists {
if err != nil { chaosVersion = cVersion
log.Fatal(err) if strings.HasSuffix(chaosVersion, config.DIRTY_DEFAULT) {
chaosVersion = formatter.BoldDirtyDefault(chaosVersion)
}
} }
} }
} }
@ -88,9 +92,14 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
return return
} }
services := compose.Services
sort.Slice(services, func(i, j int) bool {
return services[i].Name < services[j].Name
})
var rows [][]string var rows [][]string
allContainerStats := make(map[string]map[string]string) allContainerStats := make(map[string]map[string]string)
for _, service := range compose.Services { for _, service := range services {
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name)) filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
@ -140,10 +149,10 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
row := []string{ row := []string{
containerStats["service"], containerStats["service"],
containerStats["status"],
containerStats["image"], containerStats["image"],
dVersion, dVersion,
cVersion, cVersion,
containerStats["status"],
} }
rows = append(rows, row) rows = append(rows, row)
@ -167,10 +176,10 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
headers := []string{ headers := []string{
"SERVICE", "SERVICE",
"STATUS",
"IMAGE", "IMAGE",
"VERSION", "VERSION",
"CHAOS", "CHAOS",
"STATUS",
} }
table. table.

View File

@ -1,110 +0,0 @@
package app
import (
"fmt"
"path/filepath"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"
)
var AppPushCommand = &cobra.Command{
Use: "push <app> [flags]",
Aliases: []string{"pu"},
Short: "Push app changes to a remote",
Long: `Run "abra app pull <app>" beforehand to reduce conflicts.`,
Example: "abra app push 1312.net",
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args)
gitDir := gitPkg.FindDir(app.Path)
if gitDir == "" {
log.Fatal(fmt.Errorf("no git repo found for %s", app.Name))
}
appDir := filepath.Dir(app.Path)
log.Infof("%s currently has these unstaged changes 👇", app.Name)
diff, err := gitPkg.DiffUnstaged(appDir, app.Path)
if err != nil {
log.Fatal(err)
}
if diff == "" {
log.Infof("no diff for %s, nothing to push", app.Name)
return
}
fmt.Print(diff)
var confirmPush bool
if !internal.NoInput {
prompt := &survey.Confirm{
Message: "push these changes?",
}
if err := survey.AskOne(prompt, &confirmPush); err != nil {
log.Fatal(err)
}
}
if msg == "" && !internal.NoInput {
prompt := &survey.Input{
Message: "commit message?",
}
if err := survey.AskOne(prompt, &msg); err != nil {
log.Fatal(err)
}
}
if msg == "" {
log.Fatal("missing --msg/-m")
}
if confirmPush || internal.NoInput {
fname := filepath.Base(app.Path)
if err := gitPkg.CommitFile(gitDir, fname, msg, internal.Dry); err != nil {
log.Fatal(err)
}
if err := gitPkg.Push(gitDir, "origin", false, internal.Dry); err != nil {
log.Fatal(err)
}
log.Info("changes pushed successfully 🦋")
}
},
}
var (
msg string
)
func init() {
AppPushCommand.Flags().StringVarP(
&msg,
"msg",
"m",
"",
"commit message",
)
AppPushCommand.Flags().BoolVarP(
&internal.Dry,
"dry-run",
"r",
false,
"report changes that would be made",
)
}

View File

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

View File

@ -9,18 +9,20 @@ 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/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/ui"
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/docker/docker/api/types"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppRestartCommand = &cobra.Command{ var AppRestartCommand = &cobra.Command{
Use: "restart <app> [[service] | --all-services] [flags]", Use: "restart <domain> [[service] | --all-services] [flags]",
Aliases: []string{"re"}, Aliases: []string{"re"},
Short: "Restart an app", Short: "Restart an app",
Long: `This command restarts services within a deployed app. Long: `This command restarts services within a deployed app.
Run "abra app ps <app>" to see a list of service names. Run "abra app ps <domain>" to see a list of service names.
Pass "--all-services/-a" to restart all services.`, Pass "--all-services/-a" to restart all services.`,
Example: ` # restart a single app service Example: ` # restart a single app service
@ -48,7 +50,7 @@ Pass "--all-services/-a" to restart all services.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
app := internal.ValidateApp(args) app := internal.ValidateApp(args)
if err := app.Recipe.Ensure(false, false); err != nil { if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -93,13 +95,36 @@ Pass "--all-services/-a" to restart all services.`,
for _, serviceName := range serviceNames { for _, serviceName := range serviceNames {
stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName) stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
service, _, err := cl.ServiceInspectWithRaw(
context.Background(),
stackServiceName,
types.ServiceInspectOptions{},
)
if err != nil {
log.Fatal(err)
}
log.Debugf("attempting to scale %s to 0", stackServiceName) log.Debugf("attempting to scale %s to 0", stackServiceName)
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil { if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil { f, err := app.Filters(true, false, serviceName)
if err != nil {
log.Fatal(err)
}
waitOpts := stack.WaitOpts{
Services: []ui.ServiceMeta{{Name: stackServiceName, ID: service.ID}},
AppName: app.Name,
ServerName: app.Server,
Filters: f,
NoLog: true,
Quiet: true,
}
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -110,7 +135,7 @@ Pass "--all-services/-a" to restart all services.`,
log.Fatal(err) log.Fatal(err)
} }
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil { if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -123,6 +148,13 @@ Pass "--all-services/-a" to restart all services.`,
var allServices bool var allServices bool
func init() { func init() {
AppRestartCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
false,
"ignore uncommitted recipes changes",
)
AppRestartCommand.Flags().BoolVarP( AppRestartCommand.Flags().BoolVarP(
&allServices, &allServices,
"all-services", "all-services",

View File

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

View File

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

View File

@ -11,13 +11,13 @@ import (
"coopcloud.tech/abra/pkg/log" "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" containertypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var AppRunCommand = &cobra.Command{ var AppRunCommand = &cobra.Command{
Use: "run <app> <service> <cmd> [[args] [flags] | [flags] -- [args]]", Use: "run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]",
Aliases: []string{"r"}, Aliases: []string{"r"},
Short: "Run a command inside a service container", Short: "Run a command inside a service container",
Example: ` # run <cmd> with args/flags Example: ` # run <cmd> with args/flags
@ -64,7 +64,7 @@ var AppRunCommand = &cobra.Command{
} }
userCmd := args[2:] userCmd := args[2:]
execCreateOpts := types.ExecConfig{ execCreateOpts := containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import (
) )
var AppUndeployCommand = &cobra.Command{ var AppUndeployCommand = &cobra.Command{
Use: "undeploy <app> [flags]", Use: "undeploy <domain> [flags]",
Aliases: []string{"un"}, Aliases: []string{"un"},
Short: "Undeploy an app", Short: "Undeploy an app",
Long: `This does not destroy any application data. Long: `This does not destroy any application data.
@ -54,20 +54,36 @@ Passing "--prune/-p" does not remove those volumes.`,
log.Fatalf("%s is not deployed?", app.Name) log.Fatalf("%s is not deployed?", app.Name)
} }
chaosVersion := config.CHAOS_DEFAULT if err := internal.DeployOverview(
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
if err := internal.UndeployOverview(
app, app,
deployMeta.Version, deployMeta.Version,
chaosVersion); err != nil { config.NO_DOMAIN_DEFAULT,
"",
nil,
); err != nil {
log.Fatal(err) log.Fatal(err)
} }
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
if err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
if err != nil {
log.Fatal(err)
}
log.Info("initialising undeploy")
rmOpts := stack.Remove{ rmOpts := stack.Remove{
Namespaces: []string{app.StackName()}, Namespaces: []string{stackName},
Detach: false, Detach: false,
} }
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil { if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
@ -80,9 +96,10 @@ Passing "--prune/-p" does not remove those volumes.`,
} }
} }
log.Debugf("choosing %s as version to save to env file", deployMeta.Version) log.Info("undeploy succeeded 🟢")
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil { if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
log.Fatalf("writing undeployed recipe version in env file: %s", err) log.Fatalf("writing recipe version failed: %s", err)
} }
}, },
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import (
"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"
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"
) )
@ -47,7 +48,7 @@ func RunBackupCmdRemote(
backupCmd string, backupCmd string,
containerID string, containerID string,
execEnv []string) (io.Writer, error) { execEnv []string) (io.Writer, error) {
execBackupListOpts := types.ExecConfig{ execBackupListOpts := containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,

View File

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

View File

@ -14,7 +14,7 @@ import (
"coopcloud.tech/abra/pkg/log" "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" 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/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
@ -24,7 +24,7 @@ import (
func RunCmdRemote( func RunCmdRemote(
cl *dockerClient.Client, cl *dockerClient.Client,
app appPkg.App, app appPkg.App,
requestTTY bool, disableTTY bool,
abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error { 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))
@ -42,7 +42,7 @@ func RunCmdRemote(
return err return err
} }
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} copyOpts := containertypes.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil { if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
return err return err
} }
@ -55,7 +55,7 @@ func RunCmdRemote(
shell := "/bin/bash" shell := "/bin/bash"
findShell := []string{"test", "-e", shell} findShell := []string{"test", "-e", shell}
execCreateOpts := types.ExecConfig{ execCreateOpts := containertypes.ExecOptions{
AttachStderr: true, AttachStderr: true,
AttachStdin: true, AttachStdin: true,
AttachStdout: true, AttachStdout: true,
@ -84,8 +84,10 @@ func RunCmdRemote(
} }
execCreateOpts.Cmd = cmd execCreateOpts.Cmd = cmd
execCreateOpts.Tty = requestTTY
if !requestTTY { execCreateOpts.Tty = true
if disableTTY {
execCreateOpts.Tty = false
log.Debugf("not requesting a remote TTY") log.Debugf("not requesting a remote TTY")
} }

View File

@ -37,88 +37,21 @@ func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right) return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right)
} }
// NewVersionOverview shows an upgrade or downgrade overview func formatComposeFiles(composeFiles string) string {
func NewVersionOverview( return strings.ReplaceAll(composeFiles, ":", "\n")
app appPkg.App,
warnMessages []string,
kind,
deployedVersion,
deployedChaosVersion,
toDeployVersion,
releaseNotes string) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles
}
server := app.Server
if app.Server == "default" {
server = "local"
}
domain := app.Domain
if domain == "" {
domain = config.NO_DOMAIN_DEFAULT
}
rows := [][]string{
[]string{"APP", domain},
[]string{"RECIPE", app.Recipe.Name},
[]string{"SERVER", server},
[]string{"DEPLOYED", deployedVersion},
[]string{"CURRENT CHAOS ", deployedChaosVersion},
[]string{fmt.Sprintf("TO %s", strings.ToUpper(kind)), toDeployVersion},
[]string{"CONFIG", deployConfig},
}
overview := formatter.CreateOverview(
fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind)),
rows,
)
fmt.Println(overview)
if releaseNotes != "" && toDeployVersion != "" {
fmt.Print(releaseNotes)
} else {
warnMessages = append(
warnMessages,
fmt.Sprintf("no release notes available for %s", toDeployVersion),
)
}
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
} }
// DeployOverview shows a deployment overview // DeployOverview shows a deployment overview
func DeployOverview( func DeployOverview(
app appPkg.App, app appPkg.App,
warnMessages []string,
deployedVersion string, deployedVersion string,
deployedChaosVersion string, toDeployVersion string,
toDeployVersion, releaseNotes string,
toDeployChaosVersion string) error { warnMessages []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 = formatComposeFiles(composeFiles)
} }
server := app.Server server := app.Server
@ -131,21 +64,31 @@ func DeployOverview(
domain = config.NO_DOMAIN_DEFAULT domain = config.NO_DOMAIN_DEFAULT
} }
rows := [][]string{ envVersion := app.Recipe.EnvVersionRaw
[]string{"APP", domain}, if envVersion == "" {
[]string{"RECIPE", app.Recipe.Name}, envVersion = config.NO_VERSION_DEFAULT
[]string{"SERVER", server},
[]string{"DEPLOYED", deployedVersion},
[]string{"CURRENT CHAOS ", deployedChaosVersion},
[]string{"TO DEPLOY", toDeployVersion},
[]string{"NEW CHAOS", toDeployChaosVersion},
[]string{"CONFIG", deployConfig},
} }
overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows) rows := [][]string{
{"DOMAIN", domain},
{"RECIPE", app.Recipe.Name},
{"SERVER", server},
{"CONFIG", deployConfig},
{"", ""},
{"CURRENT DEPLOYMENT", formatter.BoldDirtyDefault(deployedVersion)},
{"ENV VERSION", formatter.BoldDirtyDefault(envVersion)},
{"NEW DEPLOYMENT", formatter.BoldDirtyDefault(toDeployVersion)},
}
deployType := getDeployType(deployedVersion, toDeployVersion)
overview := formatter.CreateOverview(fmt.Sprintf("%s OVERVIEW", deployType), rows)
fmt.Println(overview) fmt.Println(overview)
if releaseNotes != "" {
fmt.Print(releaseNotes)
}
for _, msg := range warnMessages { for _, msg := range warnMessages {
log.Warn(msg) log.Warn(msg)
} }
@ -167,54 +110,34 @@ func DeployOverview(
return nil return nil
} }
// UndeployOverview shows an undeployment overview func getDeployType(currentVersion, newVersion string) string {
func UndeployOverview( if newVersion == config.NO_DOMAIN_DEFAULT {
app appPkg.App, return "UNDEPLOY"
version,
chaosVersion string) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles
} }
if strings.Contains(newVersion, "+U") {
server := app.Server return "CHAOS DEPLOY"
if app.Server == "default" {
server = "local"
} }
if strings.Contains(currentVersion, "+U") {
domain := app.Domain return "UNCHAOS DEPLOY"
if domain == "" {
domain = config.NO_DOMAIN_DEFAULT
} }
if currentVersion == newVersion {
rows := [][]string{ return "REDEPLOY"
[]string{"APP", domain},
[]string{"RECIPE", app.Recipe.Name},
[]string{"SERVER", server},
[]string{"DEPLOYED", version},
[]string{"CHAOS", chaosVersion},
[]string{"CONFIG", deployConfig},
} }
if currentVersion == config.NO_VERSION_DEFAULT {
overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows) return "NEW DEPLOY"
fmt.Println(overview)
if NoInput {
return nil
} }
currentParsed, err := tagcmp.Parse(currentVersion)
response := false if err != nil {
prompt := &survey.Confirm{Message: "proceed?"} return "DEPLOY"
if err := survey.AskOne(prompt, &response); err != nil {
return err
} }
newParsed, err := tagcmp.Parse(newVersion)
if !response { if err != nil {
log.Fatal("undeploy cancelled") return "DEPLOY"
} }
if currentParsed.IsLessThan(newParsed) {
return nil return "UPGRADE"
}
return "DOWNGRADE"
} }
// 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

View File

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

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

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

View File

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

View File

@ -1,8 +1,6 @@
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"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
@ -24,12 +22,8 @@ var RecipeDiffCommand = &cobra.Command{
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
r := internal.ValidateRecipe(args, cmd.Name()) r := internal.ValidateRecipe(args, cmd.Name())
diff, err := gitPkg.DiffUnstaged(r.Dir, "") if err := gitPkg.DiffUnstaged(r.Dir); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if diff != "" {
fmt.Print(diff)
}
}, },
} }

View File

@ -1,11 +1,15 @@
package recipe package recipe
import ( import (
"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/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
gitCfg "github.com/go-git/go-git/v5/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -13,7 +17,16 @@ var RecipeFetchCommand = &cobra.Command{
Use: "fetch [recipe | --all] [flags]", Use: "fetch [recipe | --all] [flags]",
Aliases: []string{"f"}, Aliases: []string{"f"},
Short: "Clone recipe(s) locally", Short: "Clone recipe(s) locally",
Long: `Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`,
Args: cobra.RangeArgs(0, 1), Args: cobra.RangeArgs(0, 1),
Example: ` # fetch from recipe catalogue
abra recipe fetch gitea
# fetch from remote recipe
abra recipe fetch git.foo.org/recipes/myrecipe
# fetch with ssh remote for hacking
abra recipe fetch gitea --ssh`,
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
args []string, args []string,
@ -34,11 +47,41 @@ var RecipeFetchCommand = &cobra.Command{
log.Fatal("cannot use [recipe] and --all/-a together") log.Fatal("cannot use [recipe] and --all/-a together")
} }
ensureCtx := internal.GetEnsureContext()
if recipeName != "" { if recipeName != "" {
r := internal.ValidateRecipe(args, cmd.Name()) r := recipe.Get(recipeName)
if err := r.Ensure(false, false); err != nil { if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
log.Fatal(err) if !force {
log.Warnf("%s is already fetched", r.Name)
return
}
} }
r = internal.ValidateRecipe(args, cmd.Name())
if sshRemote {
if r.SSHURL == "" {
log.Warnf("unable to discover SSH remote for %s", r.Name)
return
}
repo, err := git.PlainOpen(r.Dir)
if err != nil {
log.Fatalf("unable to open %s: %s", r.Dir, err)
}
if err = repo.DeleteRemote("origin"); err != nil {
log.Fatalf("unable to remove default remote in %s: %s", r.Dir, err)
}
if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{
Name: "origin",
URLs: []string{r.SSHURL},
}); err != nil {
log.Fatalf("unable to set SSH remote in %s: %s", r.Dir, err)
}
}
return return
} }
@ -50,7 +93,7 @@ var RecipeFetchCommand = &cobra.Command{
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...") catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
for recipeName := range catalogue { for recipeName := range catalogue {
r := recipe.Get(recipeName) r := recipe.Get(recipeName)
if err := r.Ensure(false, false); err != nil { if err := r.Ensure(ensureCtx); err != nil {
log.Error(err) log.Error(err)
} }
catlBar.Add(1) catlBar.Add(1)
@ -60,6 +103,8 @@ var RecipeFetchCommand = &cobra.Command{
var ( var (
fetchAllRecipes bool fetchAllRecipes bool
sshRemote bool
force bool
) )
func init() { func init() {
@ -70,4 +115,20 @@ func init() {
false, false,
"fetch all recipes", "fetch all recipes",
) )
RecipeFetchCommand.Flags().BoolVarP(
&sshRemote,
"ssh",
"s",
false,
"automatically set ssh remote",
)
RecipeFetchCommand.Flags().BoolVarP(
&force,
"force",
"f",
false,
"force re-fetch",
)
} }

View File

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

View File

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

View File

@ -118,13 +118,9 @@ your SSH keys configured on your account.`,
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
diff, err := gitPkg.DiffUnstaged(recipe.Dir, "") if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if diff != "" {
fmt.Print(diff)
}
} }
if len(tags) > 0 { if len(tags) > 0 {
@ -271,6 +267,8 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return err return err
} }
var addNextAsReleaseNotes bool
nextReleaseNotePath := path.Join(releaseDir, "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>
@ -280,38 +278,37 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
} }
if !internal.NoInput { if !internal.NoInput {
prompt := &survey.Input{ prompt := &survey.Confirm{
Message: "Use release note in release/next?", Message: "Use release note in release/next?",
} }
var addReleaseNote bool
if err := survey.AskOne(prompt, &addReleaseNote); err != nil { if err := survey.AskOne(prompt, &addNextAsReleaseNotes); 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(recipe.Dir, 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(recipe.Dir, 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
} }
// No release note exists for the current release. // NOTE(d1): No release note exists for the current release. Or, we've
if internal.NoInput { // already used release/next as the release note
if internal.NoInput || addNextAsReleaseNotes {
return nil return nil
} }

View File

@ -208,13 +208,9 @@ likely to change.
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
diff, err := gitPkg.DiffUnstaged(recipe.Dir, "") if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if diff != "" {
fmt.Print(diff)
}
} }
}, },
} }

View File

@ -63,7 +63,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name()) recipe := internal.ValidateRecipe(args, cmd.Name())
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -327,13 +327,9 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
diff, err := gitPkg.DiffUnstaged(recipe.Dir, "") if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if diff != "" {
fmt.Print(diff)
}
} }
}, },
} }

View File

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

View File

@ -35,6 +35,7 @@ func Run(version, commit string) {
config.ABRA_DIR, config.ABRA_DIR,
config.SERVERS_DIR, config.SERVERS_DIR,
config.RECIPES_DIR, config.RECIPES_DIR,
config.LOGS_DIR,
config.VENDOR_DIR, // TODO(d1): remove > 0.9.x config.VENDOR_DIR, // TODO(d1): remove > 0.9.x
config.BACKUP_DIR, // TODO(d1): remove > 0.9.x config.BACKUP_DIR, // TODO(d1): remove > 0.9.x
} }
@ -48,9 +49,13 @@ func Run(version, commit string) {
} }
} }
log.Logger.SetStyles(log.Styles()) log.Logger.SetStyles(charmLog.DefaultStyles())
charmLog.SetDefault(log.Logger) charmLog.SetDefault(log.Logger)
if internal.MachineReadable {
log.SetOutput(os.Stderr)
}
if internal.Debug { if internal.Debug {
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
log.SetOutput(os.Stderr) log.SetOutput(os.Stderr)
@ -95,20 +100,37 @@ func Run(version, commit string) {
} }
rootCmd.PersistentFlags().BoolVarP( rootCmd.PersistentFlags().BoolVarP(
&internal.Debug, "debug", "d", false, &internal.Debug,
"debug",
"d",
false,
"show debug messages", "show debug messages",
) )
rootCmd.PersistentFlags().BoolVarP( rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput, "no-input", "n", false, &internal.NoInput,
"no-input",
"n",
false,
"toggle non-interactive mode", "toggle non-interactive mode",
) )
rootCmd.PersistentFlags().BoolVarP( rootCmd.PersistentFlags().BoolVarP(
&internal.Offline, "offline", "o", false, &internal.Offline,
"offline",
"o",
false,
"prefer offline & filesystem access", "prefer offline & filesystem access",
) )
rootCmd.PersistentFlags().BoolVarP(
&internal.IgnoreEnvVersion,
"ignore-env-version",
"i",
false,
"ignore .env version checkout",
)
catalogue.CatalogueCommand.AddCommand( catalogue.CatalogueCommand.AddCommand(
catalogue.CatalogueGenerateCommand, catalogue.CatalogueGenerateCommand,
) )
@ -187,8 +209,8 @@ func Run(version, commit string) {
app.AppUndeployCommand, app.AppUndeployCommand,
app.AppUpgradeCommand, app.AppUpgradeCommand,
app.AppVolumeCommand, app.AppVolumeCommand,
app.AppDiffCommand, app.AppLabelsCommand,
app.AppPushCommand, app.AppEnvCommand,
) )
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {

View File

@ -103,8 +103,7 @@ developer machine. The domain is then set to "default".`,
if _, err := client.New(name, timeout); err != nil { if _, err := client.New(name, timeout); err != nil {
cleanUp(name) cleanUp(name)
log.Debugf("ssh %s error: %s", name, sshPkg.Fatal(name, err)) log.Fatalf("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 { if created {

View File

@ -441,7 +441,25 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion) log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
err = stack.RunDeploy(cl, deployOpts, compose, stackName, true) serviceNames, err := appPkg.GetAppServiceNames(app.Name)
if err != nil {
return err
}
f, err := app.Filters(true, false, serviceNames...)
if err != nil {
return err
}
err = stack.RunDeploy(
cl,
deployOpts,
compose,
stackName,
app.Server,
true,
f,
)
return err return err
} }
@ -452,7 +470,7 @@ func newKadabraApp(version, commit string) *cobra.Command {
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Short: "The Co-op Cloud auto-updater 🤖 🚀", Short: "The Co-op Cloud auto-updater 🤖 🚀",
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.Logger.SetStyles(log.Styles()) log.Logger.SetStyles(charmLog.DefaultStyles())
charmLog.SetDefault(log.Logger) charmLog.SetDefault(log.Logger)
if internal.Debug { if internal.Debug {

114
go.mod
View File

@ -1,49 +1,50 @@
module coopcloud.tech/abra module coopcloud.tech/abra
go 1.22.7 go 1.23.0
toolchain go1.23.1 toolchain go1.23.1
replace github.com/urfave/cli/v3 => github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44
require ( require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/bubbletea v1.3.4
github.com/charmbracelet/log v0.4.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.1
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli v27.4.1+incompatible github.com/docker/cli v28.0.1+incompatible
github.com/docker/docker v27.4.1+incompatible github.com/docker/docker v28.0.1+incompatible
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.12.0 github.com/go-git/go-git/v5 v5.14.0
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.7.0
github.com/moby/sys/signal v0.7.1 github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.0 github.com/moby/term v0.5.2
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.18.0
github.com/urfave/cli/v3 v3.0.0-alpha9 golang.org/x/term v0.30.0
golang.org/x/term v0.27.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1 gotest.tools/v3 v3.5.2
) )
require ( require (
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.1 // indirect
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.4.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/cloudflare/circl v1.5.0 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/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.6 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // 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/docker/distribution v2.8.3+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
@ -51,11 +52,12 @@ require (
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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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/fsnotify/fsnotify v1.6.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.0 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
@ -63,16 +65,17 @@ require (
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-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // 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.18.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // 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.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // 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
@ -83,47 +86,50 @@ require (
github.com/moby/sys/user v0.3.0 // indirect github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/sys/userns v0.1.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/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // 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.63.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/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.6 // 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
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.33.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.33.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/sdk v1.33.0 // indirect go.opentelemetry.io/otel/sdk v1.35.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.33.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.4.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect
golang.org/x/crypto v0.31.0 // indirect golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/net v0.33.0 // indirect golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.12.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.8.0 // indirect golang.org/x/time v0.11.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
google.golang.org/grpc v1.69.2 // indirect google.golang.org/grpc v1.71.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect google.golang.org/protobuf v1.36.5 // 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
) )
@ -132,19 +138,19 @@ require (
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.9.3 // 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.6.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_golang v1.21.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.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
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.28.0 golang.org/x/sys v0.31.0
) )

294
go.sum
View File

@ -27,16 +27,16 @@ coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb/go.mod h1:ESVm0wQKcbcFi
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE= git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c h1:oeKnUB79PKYD8D0/unYuu7MRcWryQQWOns8+JL+acrs=
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q= git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c/go.mod h1:fQuhwrpg6qb9NlFXKYi/LysWu1wxjraS8sxyW12CUF0=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
@ -79,8 +79,8 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 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.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
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=
@ -131,21 +131,26 @@ github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
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/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA= github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/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=
@ -163,8 +168,8 @@ github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2u
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/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.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-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=
@ -278,8 +283,6 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
@ -289,10 +292,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.4 h1:VBWugsJh2ZxJmLFSM06/0qzQyiQX2Qs0ViKrUAcqdZ8= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.3.4/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/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=
@ -311,23 +312,19 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs=
github.com/docker/cli v27.3.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution 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/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 h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/docker-credential-helpers v0.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.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
@ -350,8 +347,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
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=
@ -362,6 +359,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@ -382,16 +381,16 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYis
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
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=
@ -488,8 +487,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -529,12 +528,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.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed 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=
@ -596,8 +592,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.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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=
@ -629,13 +625,14 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@ -682,8 +679,8 @@ github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 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.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 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=
@ -692,8 +689,12 @@ 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/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/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 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=
@ -730,8 +731,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
@ -761,8 +762,8 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
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=
@ -778,8 +779,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.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
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=
@ -793,10 +794,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.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-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=
@ -817,16 +816,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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4= github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
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=
@ -843,8 +841,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.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
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=
@ -859,8 +857,8 @@ github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@ -869,8 +867,9 @@ github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
@ -905,8 +904,6 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44 h1:BeSTAZEDkDVNv9EOrycIGCkEg+6EhRRgSsbdc93Q3OM=
github.com/urfave/cli/v3 v3.0.0-alpha9.1.0.20241019193437-5053ec708a44/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
@ -927,6 +924,8 @@ github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -947,49 +946,29 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0 h1:IJFEoHiytixx8cMiVAO+GmHR6Frwu+u5Ur8njpFO6Ac=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.32.0/go.mod h1:3rHrKNtLIoS0oZwkY2vxi+oJcwFRWdtUyRII+so45p8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0 h1:9kV11HXBHZAvuPUZxmMWrH8hZn/6UnHX4K0mu36vNsU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.32.0/go.mod h1:JyA0FHXe22E1NeNiHmVp7kFHglnexDQ7uRWDiiJ1hKQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.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=
@ -1014,10 +993,8 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-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=
@ -1028,10 +1005,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -1095,10 +1070,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-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=
@ -1116,10 +1089,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-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=
@ -1189,6 +1160,7 @@ golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -1196,20 +1168,15 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.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=
@ -1219,18 +1186,16 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-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.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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=
@ -1324,14 +1289,10 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -1351,10 +1312,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.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-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=
@ -1368,10 +1327,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.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/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=
@ -1411,12 +1368,11 @@ 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=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

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

View File

@ -198,3 +198,29 @@ 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)
}

View File

@ -44,6 +44,16 @@ func SetChaosVersionLabel(compose *composetypes.Config, stackName string, chaosV
} }
} }
func SetVersionLabel(compose *composetypes.Config, stackName string, version string) {
for _, service := range compose.Services {
if service.Name == "app" {
log.Debugf("set label 'coop-cloud.%s.version' to %v for %s", stackName, version, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
service.Deploy.Labels[labelKey] = version
}
}
}
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the // 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 // auto update process for this app. The default if this variable is not set is to disable
// the auto update process. // the auto update process.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -192,7 +192,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("WITH_COMMENT env var should be present in .env.sample") t.Fatal("SECRET_TEST_PASS_TWO_VERSION env var should be present in .env.sample")
} }
if strings.Contains(envVar, "length") { if strings.Contains(envVar, "length") {

View File

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

View File

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

View File

@ -1,9 +1,10 @@
package git package git
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "os/signal"
"strings" "strings"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
@ -11,39 +12,94 @@ import (
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
) )
// Clone runs a git clone which accounts for different default branches. // gitCloneIgnoreErr checks whether we can ignore a git clone error or not.
func gitCloneIgnoreErr(err error) bool {
if strings.Contains(err.Error(), "authentication required") {
return true
}
if strings.Contains(err.Error(), "remote repository is empty") {
return true
}
return false
}
// Clone runs a git clone which accounts for different default branches. This
// function respects Ctrl+C (SIGINT) calls from the user, cancelling the
// context and deleting the (typically) half-baked clone of the repository.
// This avoids broken state for future clone / recipe ops.
func Clone(dir, url string) error { func Clone(dir, url string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) { ctx := context.Background()
log.Debugf("%s does not exist, attempting to git clone from %s", dir, url) ctx, cancelCtx := context.WithCancel(ctx)
_, err := git.PlainClone(dir, false, &git.CloneOptions{ sigIntCh := make(chan os.Signal, 1)
URL: url, signal.Notify(sigIntCh, os.Interrupt)
Tags: git.AllTags, defer func() {
ReferenceName: plumbing.ReferenceName("refs/heads/master"), signal.Stop(sigIntCh)
SingleBranch: true, cancelCtx()
}) }()
if err != nil {
log.Debugf("cloning %s default branch failed, attempting from main branch", url)
_, err := git.PlainClone(dir, false, &git.CloneOptions{ errCh := make(chan error)
go func() {
if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Debugf("git clone: %s", url)
_, err := git.PlainCloneContext(ctx, 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/main"),
SingleBranch: true, SingleBranch: true,
}) })
if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir)
errCh <- nil
}
if err := ctx.Err(); err != nil {
errCh <- fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
}
if err != nil { if err != nil {
if strings.Contains(err.Error(), "authentication required") { log.Debug("git clone: main branch failed, attempting master branch")
name := filepath.Base(dir)
return fmt.Errorf("unable to clone %s, does %s exist?", name, url) _, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
SingleBranch: true,
})
if err != nil && gitCloneIgnoreErr(err) {
log.Debugf("git clone: %s cloned successfully", dir)
errCh <- nil
} }
return err if err != nil {
errCh <- err
}
} }
log.Debugf("git clone: %s cloned successfully", dir)
} else {
log.Debugf("git clone: %s already exists", dir)
} }
log.Debugf("%s has been git cloned successfully", dir) errCh <- nil
} else { }()
log.Debugf("%s already exists", dir)
select {
case <-sigIntCh:
cancelCtx()
fmt.Println() // NOTE(d1): newline after ^C
if err := os.RemoveAll(dir); err != nil {
return fmt.Errorf("unable to clean up git clone of %s: %s", dir, err)
}
return fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
case err := <-errCh:
return err
} }
return nil return nil

48
pkg/git/clone_test.go Normal file
View File

@ -0,0 +1,48 @@
package git
import (
"fmt"
"os"
"path"
"syscall"
"testing"
"coopcloud.tech/abra/pkg/config"
)
func TestClone(t *testing.T) {
dir := path.Join(config.RECIPES_DIR, "gitea")
os.RemoveAll(dir)
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
if err := Clone(dir, gitURL); err != nil {
t.Fatalf("unable to git clone gitea: %s", err)
}
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
t.Fatal("gitea repo was not cloned successfully")
}
}
func TestCancelGitClone(t *testing.T) {
dir := path.Join(config.RECIPES_DIR, "gitea")
os.RemoveAll(dir)
go func() {
p, err := os.FindProcess(os.Getpid())
if err != nil {
t.Fatalf("unable to find current process: %s", err)
}
p.Signal(syscall.SIGINT)
}()
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
if err := Clone(dir, gitURL); err == nil {
t.Fatal("cloning should have been interrupted")
}
if _, err := os.Stat(dir); err != nil && !os.IsNotExist(err) {
t.Fatal("recipe repo was not deleted")
}
}

View File

@ -45,39 +45,3 @@ func Commit(repoPath, commitMessage string, dryRun bool) error {
return nil return nil
} }
// CommitFile commits a specific file.
func CommitFile(repoPath, filePath, commitMessage string, dryRun bool) error {
if commitMessage == "" {
return fmt.Errorf("no commit message specified?")
}
commitRepo, err := git.PlainOpen(repoPath)
if err != nil {
return err
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
return err
}
if !dryRun {
if _, err := commitWorktree.Add(filePath); err != nil {
return fmt.Errorf("unable to add %s: %s", filePath, err)
}
}
opts := &git.CommitOptions{}
if !dryRun {
_, err = commitWorktree.Commit(commitMessage, opts)
if err != nil {
return err
}
log.Debug("git changes commited")
} else {
log.Debug("dry run: no changes commited")
}
return nil
}

View File

@ -3,7 +3,6 @@ package git
import ( import (
"fmt" "fmt"
"os/exec" "os/exec"
"strings"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
) )
@ -11,8 +10,8 @@ import (
// getGitDiffArgs builds the `git diff` invocation args. It removes the usage // getGitDiffArgs builds the `git diff` invocation args. It removes the usage
// of a pager and ensures that colours are specified even when Git might detect // of a pager and ensures that colours are specified even when Git might detect
// otherwise. // otherwise.
func getGitDiffArgs(repoPath, fname string) []string { func getGitDiffArgs(repoPath string) []string {
args := []string{ return []string{
"-C", "-C",
repoPath, repoPath,
"--no-pager", "--no-pager",
@ -20,29 +19,24 @@ func getGitDiffArgs(repoPath, fname string) []string {
"color.diff=always", "color.diff=always",
"diff", "diff",
} }
if fname != "" {
args = append(args, fname)
}
return args
} }
// DiffUnstaged shows a `git diff`. Due to limitations in the underlying go-git // DiffUnstaged shows a `git diff`. Due to limitations in the underlying go-git
// library, this implementation requires the /usr/bin/git binary. // library, this implementation requires the /usr/bin/git binary. It gracefully
func DiffUnstaged(path, fname string) (string, error) { // skips if it cannot find the command on the system.
func DiffUnstaged(path string) error {
if _, err := exec.LookPath("git"); err != nil { if _, err := exec.LookPath("git"); err != nil {
return "", fmt.Errorf("missing /usr/bin/git command? cannot output diff") log.Warnf("unable to locate git command, cannot output diff")
return nil
} }
gitDiffArgs := getGitDiffArgs(path, fname) gitDiffArgs := getGitDiffArgs(path)
log.Debugf("running: git %s", strings.Join(gitDiffArgs, " "))
diff, err := exec.Command("git", gitDiffArgs...).Output() diff, err := exec.Command("git", gitDiffArgs...).Output()
if err != nil { if err != nil {
return "", err return nil
} }
return string(diff), nil fmt.Print(string(diff))
return nil
} }

View File

@ -1,35 +0,0 @@
package git
import (
"os"
"path"
"path/filepath"
"coopcloud.tech/abra/pkg/log"
)
func FindDir(dir string) string {
dir, err := filepath.Abs(dir)
if err != nil {
return ""
}
realPath, err := filepath.EvalSymlinks(dir)
if err != nil {
log.Warn("unable to find git repo: broken symlink: %s", dir)
return ""
}
dir = realPath
if dir == os.ExpandEnv("$HOME/.abra") || dir == os.ExpandEnv("$ABRA_DIR") || dir == "/" {
return ""
}
p := path.Join(dir, ".git")
if _, err := os.Stat(p); err == nil {
return path.Dir(p)
}
return FindDir(filepath.Dir(dir))
}

View File

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

View File

@ -1,41 +0,0 @@
package git
import (
"errors"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
)
// Pull pulls the latest changes in.
func Pull(repoDir string, dryRun bool) error {
if dryRun {
log.Debugf("dry run: no git changes pulled in %s", repoDir)
return nil
}
repo, err := git.PlainOpen(repoDir)
if err != nil {
return err
}
opts := &git.PullOptions{
RemoteName: "origin", // NOTE(d1): what could go wrong 🤡
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
if err := worktree.Pull(opts); err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return err
} else if err != nil && errors.Is(err, git.NoErrAlreadyUpToDate) {
log.Debugf("skipping pulling changes at %s", repoDir)
return nil
}
log.Debugf("git changes pulled in at %s", repoDir)
return nil
}

View File

@ -27,7 +27,7 @@ func Push(repoDir string, remote string, tags bool, dryRun bool) error {
return err return err
} }
log.Debug("git changes pushed") log.Debugf("git changes pushed")
if tags { if tags {
opts.RefSpecs = append(opts.RefSpecs, config.RefSpec("+refs/tags/*:refs/tags/*")) opts.RefSpecs = append(opts.RefSpecs, config.RefSpec("+refs/tags/*:refs/tags/*"))
@ -36,7 +36,7 @@ func Push(repoDir string, remote string, tags bool, dryRun bool) error {
return err return err
} }
log.Debug("git tags pushed") log.Debugf("git tags pushed")
} }
return nil return nil

View File

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

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

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

View File

@ -15,8 +15,10 @@ import (
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
) )
var Warn = "warn" var (
var Critical = "critical" Warn = "warn"
Critical = "critical"
)
type LintFunction func(recipe.Recipe) (bool, error) type LintFunction func(recipe.Recipe) (bool, error)
@ -194,7 +196,7 @@ func LintForErrors(recipe recipe.Recipe) error {
ok, err := rule.Function(recipe) ok, err := rule.Function(recipe)
if err != nil { if err != nil {
return err return fmt.Errorf("lint %s: %s", rule.Ref, err)
} }
if !ok { if !ok {
return fmt.Errorf("lint error in %s configs: \"%s\" failed lint checks (%s)", recipe.Name, rule.Description, rule.Ref) return fmt.Errorf("lint error in %s configs: \"%s\" failed lint checks (%s)", recipe.Name, rule.Description, rule.Ref)
@ -409,7 +411,7 @@ func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
} }
func LintMetadataFilledIn(r recipe.Recipe) (bool, error) { func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
features, category, err := recipe.GetRecipeFeaturesAndCategory(r) features, category, _, err := recipe.GetRecipeFeaturesAndCategory(r)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@ -2,10 +2,10 @@
package log package log
import ( import (
"math"
"os" "os"
"strings"
"github.com/charmbracelet/lipgloss" tea "github.com/charmbracelet/bubbletea"
charmLog "github.com/charmbracelet/log" charmLog "github.com/charmbracelet/log"
) )
@ -35,41 +35,12 @@ var DebugLevel = charmLog.DebugLevel
var SetOutput = charmLog.SetOutput var SetOutput = charmLog.SetOutput
var SetReportCaller = charmLog.SetReportCaller var SetReportCaller = charmLog.SetReportCaller
func Styles() *charmLog.Styles { type f func() (tea.Model, error)
styles := charmLog.DefaultStyles()
styles.Levels = map[charmLog.Level]lipgloss.Style{ func Without(fn f) (tea.Model, error) {
charmLog.DebugLevel: lipgloss.NewStyle(). l := Logger.GetLevel()
SetString(strings.ToUpper(DebugLevel.String())). Logger.SetLevel(math.MaxInt)
Bold(true). m, err := fn()
Padding(0, 1, 0, 1). Logger.SetLevel(l)
Background(lipgloss.Color("63")). return m, err
Foreground(lipgloss.Color("15")),
charmLog.InfoLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.InfoLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("86")).
Foreground(lipgloss.Color("16")),
charmLog.WarnLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.WarnLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("192")).
Foreground(lipgloss.Color("16")),
charmLog.ErrorLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.ErrorLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("204")).
Foreground(lipgloss.Color("15")),
charmLog.FatalLevel: lipgloss.NewStyle().
SetString(strings.ToUpper(charmLog.FatalLevel.String())).
Bold(true).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("134")).
Foreground(lipgloss.Color("15")),
}
return styles
} }

104
pkg/logs/logs.go Normal file
View File

@ -0,0 +1,104 @@
package logs
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/signal"
"sync"
"github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
)
type TailOpts struct {
AppName string
Services []string
StdErr bool
Since string
Buffer *[]string
ToBuffer bool
Filters filters.Args
}
// TailLogs gathers logs for the given app with optional service names to be
// filtered on. These logs can be printed to os.Stdout or gathered to a buffer.
func TailLogs(
cl *dockerClient.Client,
opts TailOpts,
) error {
sigIntCh := make(chan os.Signal, 1)
signal.Notify(sigIntCh, os.Interrupt)
defer signal.Stop(sigIntCh)
services, err := cl.ServiceList(
context.Background(),
types.ServiceListOptions{Filters: opts.Filters},
)
if err != nil {
return err
}
errCh := make(chan error)
waitCh := make(chan struct{})
go func() {
var wg sync.WaitGroup
for _, service := range services {
wg.Add(1)
go func(serviceID string) {
tail := "50"
if opts.ToBuffer {
// NOTE(d1): more logs from before deployment when analysing via file
tail = "150"
}
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
ShowStderr: true,
ShowStdout: !opts.StdErr,
Since: opts.Since,
Until: "",
Timestamps: true,
Follow: true,
Tail: tail,
Details: false,
})
if err == nil {
defer logs.Close()
if opts.ToBuffer {
buf := bufio.NewScanner(logs)
for buf.Scan() {
line := fmt.Sprintf("%s: %s", service.Spec.Name, buf.Text())
*opts.Buffer = append(*opts.Buffer, line)
}
logs.Close()
return
}
if _, err = io.Copy(os.Stdout, logs); err != nil && err != io.EOF {
errCh <- fmt.Errorf("tailLogs: unable to copy buffer: %s", err)
}
}
}(service.ID)
}
wg.Wait()
close(waitCh)
}()
select {
case <-waitCh:
return nil
case <-sigIntCh:
return nil
case err := <-errCh:
return err
}
return nil
}

View File

@ -3,23 +3,34 @@ package recipe
import ( import (
"fmt" "fmt"
"os" "os"
"slices"
"sort"
"strings" "strings"
"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/log"
"coopcloud.tech/tagcmp"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
) )
// Ensure makes sure the recipe exists, is up to date and has the latest version checked out. type EnsureContext struct {
func (r Recipe) Ensure(chaos bool, offline bool) error { Chaos bool
Offline bool
IgnoreEnvVersion bool
}
// Ensure makes sure the recipe exists, is up to date and has the specific
// version checked out.
func (r Recipe) Ensure(ctx EnsureContext) error {
if err := r.EnsureExists(); err != nil { if err := r.EnsureExists(); err != nil {
return err return err
} }
if chaos { if ctx.Chaos {
return nil return nil
} }
@ -27,15 +38,19 @@ func (r Recipe) Ensure(chaos bool, offline bool) error {
return err return err
} }
if !offline { if !ctx.Offline {
if err := r.EnsureUpToDate(); err != nil { if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if r.Version != "" { if r.EnvVersion != "" && !ctx.IgnoreEnvVersion {
log.Debugf("ensuring version %s", r.Version) log.Debugf("ensuring env version %s", r.EnvVersion)
if _, err := r.EnsureVersion(r.Version); err != nil { if strings.Contains(r.EnvVersion, "+U") {
log.Fatalf("can not redeploy chaos version (%s) without --chaos", r.EnvVersion)
}
if _, err := r.EnsureVersion(r.EnvVersion); err != nil {
return err return err
} }
@ -52,7 +67,6 @@ func (r Recipe) Ensure(chaos bool, offline bool) error {
// EnsureExists ensures that the recipe is locally cloned // EnsureExists ensures that the recipe is locally cloned
func (r Recipe) EnsureExists() error { func (r Recipe) EnsureExists() error {
if _, err := os.Stat(r.Dir); os.IsNotExist(err) { if _, err := os.Stat(r.Dir); os.IsNotExist(err) {
log.Debugf("%s does not exist, attemmpting to clone", r.Dir)
if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil { if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil {
return err return err
} }
@ -65,6 +79,41 @@ func (r Recipe) EnsureExists() error {
return nil return nil
} }
// IsChaosCommit determines if a version sttring is a chaos commit or not.
func (r Recipe) IsChaosCommit(version string) (bool, error) {
isChaosCommit := false
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
return isChaosCommit, err
}
repo, err := git.PlainOpen(r.Dir)
if err != nil {
return isChaosCommit, err
}
tags, err := repo.Tags()
if err != nil {
return isChaosCommit, err
}
var tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
if ref.Name().Short() == version {
tagRef = ref.Name()
}
return nil
}); err != nil {
return isChaosCommit, err
}
if tagRef.String() == "" {
isChaosCommit = true
}
return isChaosCommit, nil
}
// EnsureVersion checks whether a specific version exists for a recipe. // EnsureVersion checks whether a specific version exists for a recipe.
func (r Recipe) EnsureVersion(version string) (bool, error) { func (r Recipe) EnsureVersion(version string) (bool, error) {
isChaosCommit := false isChaosCommit := false
@ -137,8 +186,7 @@ func (r Recipe) EnsureIsClean() error {
} }
if !isClean { if !isClean {
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding" return fmt.Errorf("%s (%s) has locally unstaged changes?", r.Name, r.Dir)
return fmt.Errorf(msg, r.Name, r.Dir)
} }
return nil return nil
@ -230,8 +278,18 @@ func (r Recipe) EnsureUpToDate() error {
return nil return nil
} }
// IsDirty checks whether a recipe is dirty or not.
func (r *Recipe) IsDirty() (bool, error) {
isClean, err := gitPkg.IsClean(r.Dir)
if err != nil {
return false, err
}
return !isClean, nil
}
// ChaosVersion constructs a chaos mode recipe version. // ChaosVersion constructs a chaos mode recipe version.
func (r Recipe) ChaosVersion() (string, error) { func (r *Recipe) ChaosVersion() (string, error) {
var version string var version string
head, err := r.Head() head, err := r.Head()
@ -241,13 +299,12 @@ func (r Recipe) ChaosVersion() (string, error) {
version = formatter.SmallSHA(head.String()) version = formatter.SmallSHA(head.String())
isClean, err := gitPkg.IsClean(r.Dir) dirty, err := r.IsDirty()
if err != nil { if err != nil {
return version, err return "", err
} }
if dirty {
if !isClean { return fmt.Sprintf("%s%s", version, config.DIRTY_DEFAULT), nil
version = fmt.Sprintf("%s+U", version)
} }
return version, nil return version, nil
@ -293,29 +350,44 @@ func (r Recipe) Tags() ([]string, error) {
return tags, err return tags, err
} }
sort.Slice(tags, func(i, j int) bool {
version1, err := tagcmp.Parse(tags[i])
if err != nil {
return false
}
version2, err := tagcmp.Parse(tags[j])
if err != nil {
return false
}
return version1.IsLessThan(version2)
})
log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name) log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name)
return tags, nil return tags, nil
} }
// GetRecipeVersions retrieves all recipe versions. // GetRecipeVersions retrieves all recipe versions.
func (r Recipe) GetRecipeVersions() (RecipeVersions, error) { func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
var warnMsg []string
versions := RecipeVersions{} versions := RecipeVersions{}
log.Debugf("attempting to open git repository in %s", r.Dir)
log.Debugf("git: opening repository in %s", r.Dir)
repo, err := git.PlainOpen(r.Dir) repo, err := git.PlainOpen(r.Dir)
if err != nil { if err != nil {
return versions, err return versions, warnMsg, nil
} }
worktree, err := repo.Worktree() worktree, err := repo.Worktree()
if err != nil { if err != nil {
return versions, err return versions, warnMsg, nil
} }
gitTags, err := repo.Tags() gitTags, err := repo.Tags()
if err != nil { if err != nil {
return versions, err return versions, warnMsg, nil
} }
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
@ -333,7 +405,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, error) {
return err return err
} }
log.Debugf("successfully checked out %s in %s", ref.Name(), r.Dir) log.Debugf("git checkout: %s in %s", ref.Name(), r.Dir)
config, err := r.GetComposeConfig(nil) config, err := r.GetComposeConfig(nil)
if err != nil { if err != nil {
@ -357,7 +429,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, error) {
case reference.NamedTagged: case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag() tag = img.(reference.NamedTagged).Tag()
case reference.Named: case reference.Named:
log.Warnf("%s service is missing image tag?", path) warnMsg = append(warnMsg, fmt.Sprintf("%s service is missing image tag?", path))
continue continue
} }
@ -371,19 +443,26 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, error) {
return nil return nil
}); err != nil { }); err != nil {
return versions, err return versions, warnMsg, nil
} }
_, err = gitPkg.CheckoutDefaultBranch(repo, r.Dir) _, err = gitPkg.CheckoutDefaultBranch(repo, r.Dir)
if err != nil { if err != nil {
return versions, err return versions, warnMsg, nil
} }
sortRecipeVersions(versions) sortRecipeVersions(versions)
log.Debugf("collected %s for %s", versions, r.Dir) log.Debugf("collected %s for %s", versions, r.Dir)
return versions, nil var uniqueWarnings []string
for _, w := range warnMsg {
if !slices.Contains(uniqueWarnings, w) {
uniqueWarnings = append(uniqueWarnings, w)
}
}
return versions, uniqueWarnings, nil
} }
// Head retrieves latest HEAD metadata. // Head retrieves latest HEAD metadata.

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

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

View File

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

View File

@ -33,7 +33,8 @@ func TestGet(t *testing.T) {
name: "foo:1.2.3", name: "foo:1.2.3",
recipe: Recipe{ recipe: Recipe{
Name: "foo", Name: "foo",
Version: "1.2.3", EnvVersion: "1.2.3",
EnvVersionRaw: "1.2.3",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"), Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"),
GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git", GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git",
SSHURL: "ssh://git@git.coopcloud.tech:2222/coop-cloud/foo.git", SSHURL: "ssh://git@git.coopcloud.tech:2222/coop-cloud/foo.git",
@ -60,7 +61,23 @@ func TestGet(t *testing.T) {
name: "mygit.org/myorg/cool-recipe:1.2.4", name: "mygit.org/myorg/cool-recipe:1.2.4",
recipe: Recipe{ recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe", Name: "mygit.org/myorg/cool-recipe",
Version: "1.2.4", EnvVersion: "1.2.4",
EnvVersionRaw: "1.2.4",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",
ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"),
ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"),
SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"),
AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"),
},
},
{
name: "mygit.org/myorg/cool-recipe:1e83340e+U",
recipe: Recipe{
Name: "mygit.org/myorg/cool-recipe",
EnvVersion: "1e83340e",
EnvVersionRaw: "1e83340e+U",
Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"), Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"),
GitURL: "https://mygit.org/myorg/cool-recipe.git", GitURL: "https://mygit.org/myorg/cool-recipe.git",
SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git", SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git",

View File

@ -33,6 +33,10 @@ type Secret struct {
// variable. For Example: // variable. For Example:
// SECRET_FOO=v1 # length=12 // SECRET_FOO=v1 # length=12
Length int Length int
// Charset comes from the charset modifier at the secret version environment
// variable. For Example:
// SECRET_FOO=v1 # charset=default,special
Charset string
// RemoteName is the name of the secret on the server. For example: // RemoteName is the name of the secret on the server. For example:
// name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION} // name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
// With the following: // With the following:
@ -43,38 +47,38 @@ type Secret struct {
RemoteName string RemoteName string
} }
// GeneratePasswords generates passwords. // GeneratePassword generates passwords.
func GeneratePasswords(count, length uint) ([]string, error) { func GeneratePassword(length uint, charset string) (string, error) {
passwords, err := passgen.GeneratePasswords( passwords, err := passgen.GeneratePasswords(
count, 1,
length, length,
passgen.AlphabetDefault, charset,
) )
if err != nil { if err != nil {
return nil, err return "", err
} }
log.Debugf("generated %s", strings.Join(passwords, ", ")) log.Debugf("generated %s", strings.Join(passwords, ", "))
return passwords, nil return passwords[0], nil
} }
// GeneratePassphrases generates human readable and rememberable passphrases. // GeneratePassphrase generates human readable and rememberable passphrases.
func GeneratePassphrases(count uint) ([]string, error) { func GeneratePassphrase() (string, error) {
passphrases, err := passgen.GeneratePassphrases( passphrases, err := passgen.GeneratePassphrases(
count, 1,
passgen.PassphraseWordCountDefault, passgen.PassphraseWordCountDefault,
rune('-'), rune('-'),
passgen.PassphraseCasingDefault, passgen.PassphraseCasingDefault,
passgen.WordListDefault, passgen.WordListDefault,
) )
if err != nil { if err != nil {
return nil, err return "", err
} }
log.Debugf("generated %s", strings.Join(passphrases, ", ")) log.Debugf("generated %s", strings.Join(passphrases, ", "))
return passphrases, nil return passphrases[0], nil
} }
// ReadSecretsConfig reads secret names/versions from the recipe config. The // ReadSecretsConfig reads secret names/versions from the recipe config. The
@ -150,6 +154,8 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
} }
value.Length = length value.Length = length
} }
value.Charset = resolveCharset(modifierValues["charset"])
break break
} }
secretValues[secretId] = value secretValues[secretId] = value
@ -158,6 +164,22 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
return secretValues, nil return secretValues, nil
} }
// resolveCharset sets the passgen Alphabet required for a secret
func resolveCharset(input string) string {
switch strings.ToLower(input) {
case "special":
return passgen.AlphabetSpecial
case "safespecial":
return "!@#%^&*_-+="
case "default,special", "special,default":
return passgen.AlphabetDefault + passgen.AlphabetSpecial
case "default,safespecial", "safespecial,default":
return passgen.AlphabetDefault + "!@#%^&*_-+="
default:
return passgen.AlphabetDefault // Fallback to default
}
}
// GenerateSecrets generates secrets locally and sends them to a remote server for storage. // GenerateSecrets generates secrets locally and sends them to a remote server for storage.
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) { func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) {
secretsGenerated := map[string]string{} secretsGenerated := map[string]string{}
@ -173,13 +195,13 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server) log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
if secret.Length > 0 { if secret.Length > 0 {
passwords, err := GeneratePasswords(1, uint(secret.Length)) password, err := GeneratePassword(uint(secret.Length), secret.Charset)
if err != nil { if err != nil {
ch <- err ch <- err
return return
} }
if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil { if err := client.StoreSecret(cl, secret.RemoteName, password, server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists", secret.RemoteName) log.Warnf("%s already exists", secret.RemoteName)
ch <- nil ch <- nil
@ -191,15 +213,15 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
secretsGenerated[secretName] = passwords[0] secretsGenerated[secretName] = password
} else { } else {
passphrases, err := GeneratePassphrases(1) passphrase, err := GeneratePassphrase()
if err != nil { if err != nil {
ch <- err ch <- err
return return
} }
if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil { if err := client.StoreSecret(cl, secret.RemoteName, passphrase, server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists", secret.RemoteName) log.Warnf("%s already exists", secret.RemoteName)
ch <- nil ch <- nil
@ -211,7 +233,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
secretsGenerated[secretName] = passphrases[0] secretsGenerated[secretName] = passphrase
} }
ch <- nil ch <- nil
}(n, v) }(n, v)

View File

@ -17,16 +17,37 @@ func TestReadSecretsConfig(t *testing.T) {
assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName) assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName)
assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version) assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length) assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_one"].Charset)
// Has a length modifier // Has a length modifier
assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName) assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName)
assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version) assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version)
assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length) assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_two"].Charset)
// Secret name does not include the secret id // Secret name does not include the secret id
assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName) assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName)
assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version) assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length) assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_three"].Charset)
// Has a length modifier and a charset=default,safespecial modifier
assert.Equal(t, "test_example_com_test_pass_four_v1", secretsFromConfig["test_pass_four"].RemoteName)
assert.Equal(t, "v1", secretsFromConfig["test_pass_four"].Version)
assert.Equal(t, 12, secretsFromConfig["test_pass_four"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#%^&*_-+=", secretsFromConfig["test_pass_four"].Charset)
// Has a length modifier and a charset=default,special modifier
assert.Equal(t, "test_example_com_test_pass_five_v1", secretsFromConfig["test_pass_five"].RemoteName)
assert.Equal(t, "v1", secretsFromConfig["test_pass_five"].Version)
assert.Equal(t, 12, secretsFromConfig["test_pass_five"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_five"].Charset)
// Has only a charset=default,special modifier, which gets setted but ignored in the generation
assert.Equal(t, "test_example_com_test_pass_six_v1", secretsFromConfig["test_pass_six"].RemoteName)
assert.Equal(t, "v1", secretsFromConfig["test_pass_six"].Version)
assert.Equal(t, 0, secretsFromConfig["test_pass_six"].Length)
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_six"].Charset)
} }
func TestReadSecretsConfigWithLongDomain(t *testing.T) { func TestReadSecretsConfigWithLongDomain(t *testing.T) {

View File

@ -1,3 +1,6 @@
SECRET_TEST_PASS_ONE_VERSION=v2 SECRET_TEST_PASS_ONE_VERSION=v2
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10 SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
SECRET_TEST_PASS_THREE_VERSION=v2 SECRET_TEST_PASS_THREE_VERSION=v2
SECRET_TEST_PASS_FOUR_VERSION=v1 # length=12 charset=default,safespecial
SECRET_TEST_PASS_FIVE_VERSION=v1 # length=12 charset=default,special
SECRET_TEST_PASS_SIX_VERSION=v1 # charset=default,special

View File

@ -8,6 +8,9 @@ services:
- test_pass_one - test_pass_one
- test_pass_two - test_pass_two
- test_pass_three - test_pass_three
- test_pass_four
- test_pass_five
- test_pass_six
secrets: secrets:
test_pass_one: test_pass_one:
@ -19,3 +22,12 @@ secrets:
test_pass_three: test_pass_three:
external: true external: true
name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match
test_pass_four:
external: true
name: ${STACK_NAME}_test_pass_four_${SECRET_TEST_PASS_FOUR_VERSION}
test_pass_five:
external: true
name: ${STACK_NAME}_test_pass_five_${SECRET_TEST_PASS_FIVE_VERSION}
test_pass_six:
external: true
name: ${STACK_NAME}_test_pass_six_${SECRET_TEST_PASS_SIX_VERSION}

View File

@ -20,6 +20,8 @@ func Fatal(hostname string, err error) error {
return fmt.Errorf("ssh auth: permission denied for %s", hostname) return fmt.Errorf("ssh auth: permission denied for %s", hostname)
} else if strings.Contains(out, "Network is unreachable") { } else if strings.Contains(out, "Network is unreachable") {
return fmt.Errorf("unable to connect to %s, please check your SSH config", hostname) return fmt.Errorf("unable to connect to %s, please check your SSH config", hostname)
} else if strings.Contains(out, "Is the docker daemon running") {
return fmt.Errorf("docker: is the daemon running / your user has docker permissions?")
} }
return err return err

353
pkg/ui/deploy.go Normal file
View File

@ -0,0 +1,353 @@
package ui
import (
"context"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"time"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/logs"
tea "github.com/charmbracelet/bubbletea"
"github.com/docker/cli/cli/command/service/progress"
containerTypes "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/jsonmessage"
)
var IsRunning bool
type statusMsg struct {
stream stream
jsonMsg jsonmessage.JSONMessage
}
type progressCompleteMsg struct {
stream stream
failed bool
}
type healthcheckMsg struct {
stream stream
health string
}
type ServiceMeta struct {
Name string
ID string
}
type Model struct {
appName string
cl *dockerClient.Client
count int
ctx context.Context
timeout time.Duration
width int
filters filters.Args
Streams *[]stream
Logs *[]string
Failed bool
TimedOut bool
Quit bool
}
func (m Model) complete() bool {
if m.count == len(*m.Streams) {
return true
}
return false
}
type stream struct {
Name string
Err error
decoder *json.Decoder
id string
reader *io.PipeReader
writer *io.PipeWriter
status string
retries int
health string
rollback bool
}
func (s stream) String() string {
out := fmt.Sprintf("{decoder: %v, ", s.decoder)
out += fmt.Sprintf("err: %v, ", s.Err)
out += fmt.Sprintf("id: %s, ", s.id)
out += fmt.Sprintf("name: %s, ", s.Name)
out += fmt.Sprintf("reader: %v, ", s.reader)
out += fmt.Sprintf("writer: %v, ", s.writer)
out += fmt.Sprintf("status: %s, ", s.status)
return out
}
func (s stream) progress(m Model) tea.Msg {
if err := progress.ServiceProgress(m.ctx, m.cl, s.id, s.writer); err != nil {
return progressCompleteMsg{
stream: s,
failed: true,
}
}
return progressCompleteMsg{stream: s}
}
func (s stream) process() tea.Msg {
var jsonMsg jsonmessage.JSONMessage
if err := s.decoder.Decode(&jsonMsg); err != nil {
if err == io.EOF {
// NOTE(d1): end processing messages
return nil
}
}
return statusMsg{
stream: s,
jsonMsg: jsonMsg,
}
}
func (s stream) healthcheck(m Model) tea.Msg {
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s", s.Name))
containers, err := m.cl.ContainerList(m.ctx, containerTypes.ListOptions{Filters: filters})
if err != nil {
s.Err = err
return healthcheckMsg{stream: s}
}
if len(containers) == 0 {
return healthcheckMsg{stream: s}
}
container := containers[0]
containerState, err := m.cl.ContainerInspect(m.ctx, container.ID)
if err != nil {
s.Err = err
return healthcheckMsg{stream: s}
}
var health string
if containerState.State.Health != nil {
health = containerState.State.Health.Status
}
return healthcheckMsg{stream: s, health: health}
}
func DeployInitialModel(
ctx context.Context,
cl *dockerClient.Client,
services []ServiceMeta,
appName string,
timeout time.Duration,
filters filters.Args,
) Model {
var streams []stream
for _, service := range services {
r, w := io.Pipe()
d := json.NewDecoder(r)
streams = append(streams, stream{
Name: service.Name,
id: service.ID,
reader: r,
writer: w,
decoder: d,
retries: -1, // NOTE(d1): skip first attempt
health: "?",
})
}
sort.Slice(streams, func(i, j int) bool {
return streams[i].Name < streams[j].Name
})
return Model{
ctx: ctx,
cl: cl,
appName: appName,
timeout: timeout,
filters: filters,
Streams: &streams,
Logs: &[]string{},
}
}
func (m Model) Init() tea.Cmd {
var cmds []tea.Cmd
for _, stream := range *m.Streams {
cmds = append(
cmds,
[]tea.Cmd{
func() tea.Msg { return stream.progress(m) },
func() tea.Msg { return stream.process() },
func() tea.Msg { return stream.healthcheck(m) },
}...,
)
}
cmds = append(cmds, func() tea.Msg { return deployTimeout(m) })
cmds = append(cmds, func() tea.Msg { return m.gatherLogs() })
return tea.Batch(cmds...)
}
func (m Model) gatherLogs() tea.Msg {
var services []string
for _, s := range *m.Streams {
services = append(services, s.Name)
}
opts := logs.TailOpts{
AppName: m.appName,
Services: services,
StdErr: true,
Buffer: m.Logs,
ToBuffer: true,
Filters: m.filters,
}
// NOTE(d1): not interested in log polling errors. if we don't see logs it
// will hopefully be self-evident based on what happened in the deployment
logs.TailLogs(m.cl, opts)
return nil
}
type timeoutMsg struct{}
func deployTimeout(m Model) tea.Msg {
<-time.After(m.timeout)
return timeoutMsg{}
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
m.Quit = true
return m, tea.Quit
}
case tea.WindowSizeMsg:
m.width = msg.Width
case progressCompleteMsg:
if msg.failed {
m.Failed = true
}
m.count += 1
if m.complete() {
return m, tea.Quit
}
case timeoutMsg:
m.TimedOut = true
return m, tea.Quit
case healthcheckMsg:
for idx, s := range *m.Streams {
if s.id == msg.stream.id {
h := "?"
if s.health != "" {
h = s.health
}
if msg.health != "" {
h = msg.health
}
(*m.Streams)[idx].health = h
}
}
cmds = append(
cmds,
func() tea.Msg { return msg.stream.healthcheck(m) },
)
case statusMsg:
for idx, s := range *m.Streams {
if s.id == msg.stream.id {
if msg.jsonMsg.ID == "rollback" {
m.Failed = true
(*m.Streams)[idx].rollback = true
}
if msg.jsonMsg.ID != "overall progress" {
newStatus := strings.ToLower(msg.jsonMsg.Status)
currentStatus := (*m.Streams)[idx].status
if !strings.Contains(currentStatus, "starting") &&
strings.Contains(newStatus, "starting") {
(*m.Streams)[idx].retries += 1
}
if (*m.Streams)[idx].rollback {
if msg.jsonMsg.ID == "rollback" {
(*m.Streams)[idx].status = newStatus
}
} else {
(*m.Streams)[idx].status = newStatus
}
}
}
}
cmds = append(
cmds,
func() tea.Msg { return msg.stream.process() },
)
}
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
body := strings.Builder{}
for _, stream := range *m.Streams {
split := strings.Split(stream.Name, "_")
short := split[len(split)-1]
status := stream.status
if strings.Contains(stream.status, "converged") && !stream.rollback {
status = "succeeded"
}
if strings.Contains(stream.status, "rolled back") {
status = "rolled back"
}
retries := 0
if stream.retries > 0 {
retries = stream.retries
}
output := fmt.Sprintf("%s: %s (retries: %v, healthcheck: %s)",
formatter.BoldStyle.Render(short),
status,
retries,
stream.health,
)
body.WriteString(output)
body.WriteString("\n")
}
return body.String()
}

View File

@ -9,14 +9,14 @@ import (
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container"
apiclient "github.com/docker/docker/client" apiclient "github.com/docker/docker/client"
) )
// RunExec runs a command on a remote container. io.Writer corresponds to the // RunExec runs a command on a remote container. io.Writer corresponds to the
// command output. // command output.
func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string, func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string,
execConfig *types.ExecConfig) (io.Writer, error) { execOptions *container.ExecOptions) (io.Writer, error) {
ctx := context.Background() ctx := context.Background()
// We need to check the tty _before_ we do the ContainerExecCreate, because // We need to check the tty _before_ we do the ContainerExecCreate, because
@ -26,13 +26,13 @@ func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string
if _, err := client.ContainerInspect(ctx, containerID); err != nil { if _, err := client.ContainerInspect(ctx, containerID); err != nil {
return nil, err return nil, err
} }
if !execConfig.Detach { if !execOptions.Detach {
if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil { if err := dockerCli.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
return nil, err return nil, err
} }
} }
response, err := client.ContainerExecCreate(ctx, containerID, *execConfig) response, err := client.ContainerExecCreate(ctx, containerID, *execOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -42,40 +42,40 @@ func RunExec(dockerCli command.Cli, client *apiclient.Client, containerID string
return nil, errors.New("exec ID empty") return nil, errors.New("exec ID empty")
} }
if execConfig.Detach { if execOptions.Detach {
execStartCheck := types.ExecStartCheck{ execStartCheck := container.ExecStartOptions{
Detach: execConfig.Detach, Detach: execOptions.Detach,
Tty: execConfig.Tty, Tty: execOptions.Tty,
} }
return nil, client.ContainerExecStart(ctx, execID, execStartCheck) return nil, client.ContainerExecStart(ctx, execID, execStartCheck)
} }
return interactiveExec(ctx, dockerCli, client, execConfig, execID) return interactiveExec(ctx, dockerCli, client, execOptions, execID)
} }
func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclient.Client, func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclient.Client,
execConfig *types.ExecConfig, execID string) (io.Writer, error) { execOpts *container.ExecOptions, execID string) (io.Writer, error) {
// Interactive exec requested. // Interactive exec requested.
var ( var (
out, stderr io.Writer out, stderr io.Writer
in io.ReadCloser in io.ReadCloser
) )
if execConfig.AttachStdin { if execOpts.AttachStdin {
in = dockerCli.In() in = dockerCli.In()
} }
if execConfig.AttachStdout { if execOpts.AttachStdout {
out = dockerCli.Out() out = dockerCli.Out()
} }
if execConfig.AttachStderr { if execOpts.AttachStderr {
if execConfig.Tty { if execOpts.Tty {
stderr = dockerCli.Out() stderr = dockerCli.Out()
} else { } else {
stderr = dockerCli.Err() stderr = dockerCli.Err()
} }
} }
execStartCheck := types.ExecStartCheck{ execStartCheck := container.ExecStartOptions{
Tty: execConfig.Tty, Tty: execOpts.Tty,
} }
resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck) resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck)
if err != nil { if err != nil {
@ -94,15 +94,15 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, client *apiclie
outputStream: out, outputStream: out,
errorStream: stderr, errorStream: stderr,
resp: resp, resp: resp,
tty: execConfig.Tty, tty: execOpts.Tty,
detachKeys: execConfig.DetachKeys, detachKeys: execOpts.DetachKeys,
} }
return streamer.stream(ctx) return streamer.stream(ctx)
}() }()
}() }()
if execConfig.Tty && dockerCli.In().IsTerminal() { if execOpts.Tty && dockerCli.In().IsTerminal() {
if err := MonitorTtySize(ctx, client, dockerCli, execID, true); err != nil { if err := MonitorTtySize(ctx, client, dockerCli, execID, true); err != nil {
fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
} }

View File

@ -5,7 +5,6 @@ import (
"strings" "strings"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
networktypes "github.com/docker/docker/api/types/network" networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
) )
@ -52,13 +51,13 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str
type networkMap map[string]composetypes.NetworkConfig type networkMap map[string]composetypes.NetworkConfig
// Networks from the compose-file type to the engine API type // Networks from the compose-file type to the engine API type
func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) { func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]networktypes.CreateOptions, []string) {
if networks == nil { if networks == nil {
networks = make(map[string]composetypes.NetworkConfig) networks = make(map[string]composetypes.NetworkConfig)
} }
externalNetworks := []string{} externalNetworks := []string{}
result := make(map[string]types.NetworkCreate) result := make(map[string]networktypes.CreateOptions)
for internalName := range servicesNetworks { for internalName := range servicesNetworks {
network := networks[internalName] network := networks[internalName]
if network.External.External { if network.External.External {
@ -66,7 +65,7 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
continue continue
} }
createOpts := types.NetworkCreate{ createOpts := networktypes.CreateOptions{
Labels: AddStackLabel(namespace, network.Labels), Labels: AddStackLabel(namespace, network.Labels),
Driver: network.Driver, Driver: network.Driver,
Options: network.DriverOpts, Options: network.DriverOpts,

View File

@ -4,7 +4,6 @@ import (
"testing" "testing"
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/network" "github.com/docker/docker/api/types/network"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
@ -67,7 +66,7 @@ func TestNetworks(t *testing.T) {
Name: "othername", Name: "othername",
}, },
} }
expected := map[string]types.NetworkCreate{ expected := map[string]network.CreateOptions{
"foo_default": { "foo_default": {
Labels: map[string]string{ Labels: map[string]string{
LabelNamespace: "foo", LabelNamespace: "foo",

View File

@ -3,11 +3,15 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/rem
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"os/signal"
"sort" "sort"
"strings" "strings"
"time"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@ -17,57 +21,87 @@ import (
// RunRemove is the swarm implementation of docker stack remove // RunRemove is the swarm implementation of docker stack remove
func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error { func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
var errs []string sigIntCh := make(chan os.Signal, 1)
for _, namespace := range opts.Namespaces { signal.Notify(sigIntCh, os.Interrupt)
services, err := GetStackServices(ctx, client, namespace) defer signal.Stop(sigIntCh)
if err != nil {
return err
}
networks, err := getStackNetworks(ctx, client, namespace) waitCh := make(chan struct{})
if err != nil { errCh := make(chan error)
return err
}
var secrets []swarm.Secret go func() {
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") { var errs []string
secrets, err = getStackSecrets(ctx, client, namespace) for _, namespace := range opts.Namespaces {
services, err := GetStackServices(ctx, client, namespace)
if err != nil { if err != nil {
return err errCh <- err
return
}
networks, err := getStackNetworks(ctx, client, namespace)
if err != nil {
errCh <- err
return
}
var secrets []swarm.Secret
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
secrets, err = getStackSecrets(ctx, client, namespace)
if err != nil {
errCh <- err
return
}
}
var configs []swarm.Config
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
configs, err = getStackConfigs(ctx, client, namespace)
if err != nil {
errCh <- err
return
}
}
if len(services)+len(networks)+len(secrets)+len(configs) == 0 {
log.Warnf("nothing found in stack: %s", namespace)
continue
}
hasError := removeServices(ctx, client, services)
hasError = removeSecrets(ctx, client, secrets) || hasError
hasError = removeConfigs(ctx, client, configs) || hasError
hasError = removeNetworks(ctx, client, networks) || hasError
if hasError {
errs = append(errs, fmt.Sprintf("failed to remove some resources from stack: %s", namespace))
continue
}
log.Info("polling undeploy status")
timeout, err := waitOnTasks(ctx, client, namespace)
if timeout {
errs = append(errs, err.Error())
} else {
if err != nil {
errs = append(errs, fmt.Sprintf("failed to wait on tasks of stack: %s: %s", namespace, err))
}
} }
} }
var configs []swarm.Config if len(errs) > 0 {
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") { errCh <- errors.Errorf(strings.Join(errs, "\n"))
configs, err = getStackConfigs(ctx, client, namespace) return
if err != nil {
return err
}
} }
if len(services)+len(networks)+len(secrets)+len(configs) == 0 { close(waitCh)
log.Warnf("nothing found in stack: %s", namespace) }()
continue
}
hasError := removeServices(ctx, client, services) select {
hasError = removeSecrets(ctx, client, secrets) || hasError case <-waitCh:
hasError = removeConfigs(ctx, client, configs) || hasError return nil
hasError = removeNetworks(ctx, client, networks) || hasError case <-sigIntCh:
return fmt.Errorf("skipping as requested, undeploy still in progress 🟠")
if hasError { case err := <-errCh:
errs = append(errs, fmt.Sprintf("failed to remove some resources from stack: %s", namespace)) return err
continue
}
err = waitOnTasks(ctx, client, namespace)
if err != nil {
errs = append(errs, fmt.Sprintf("failed to wait on tasks of stack: %s: %s", namespace, err))
}
}
if len(errs) > 0 {
return errors.Errorf(strings.Join(errs, "\n"))
} }
return nil return nil
@ -87,7 +121,7 @@ func removeServices(
var hasError bool var hasError bool
sort.Slice(services, sortServiceByName(services)) sort.Slice(services, sortServiceByName(services))
for _, service := range services { for _, service := range services {
log.Infof("removing service %s", service.Spec.Name) log.Debugf("removing service %s", service.Spec.Name)
if err := client.ServiceRemove(ctx, service.ID); err != nil { if err := client.ServiceRemove(ctx, service.ID); err != nil {
hasError = true hasError = true
log.Fatalf("failed to remove service %s: %s", service.ID, err) log.Fatalf("failed to remove service %s: %s", service.ID, err)
@ -99,11 +133,11 @@ func removeServices(
func removeNetworks( func removeNetworks(
ctx context.Context, ctx context.Context,
client *apiclient.Client, client *apiclient.Client,
networks []types.NetworkResource, networks []network.Inspect,
) bool { ) bool {
var hasError bool var hasError bool
for _, network := range networks { for _, network := range networks {
log.Infof("removing network %s", network.Name) log.Debugf("removing network %s", network.Name)
if err := client.NetworkRemove(ctx, network.ID); err != nil { if err := client.NetworkRemove(ctx, network.ID); err != nil {
hasError = true hasError = true
log.Fatalf("failed to remove network %s: %s", network.ID, err) log.Fatalf("failed to remove network %s: %s", network.ID, err)
@ -119,7 +153,7 @@ func removeSecrets(
) bool { ) bool {
var hasError bool var hasError bool
for _, secret := range secrets { for _, secret := range secrets {
log.Infof("removing secret %s", secret.Spec.Name) log.Debugf("removing secret %s", secret.Spec.Name)
if err := client.SecretRemove(ctx, secret.ID); err != nil { if err := client.SecretRemove(ctx, secret.ID); err != nil {
hasError = true hasError = true
log.Fatalf("Failed to remove secret %s: %s", secret.ID, err) log.Fatalf("Failed to remove secret %s: %s", secret.ID, err)
@ -135,7 +169,7 @@ func removeConfigs(
) bool { ) bool {
var hasError bool var hasError bool
for _, config := range configs { for _, config := range configs {
log.Infof("removing config %s", config.Spec.Name) log.Debugf("removing config %s", config.Spec.Name)
if err := client.ConfigRemove(ctx, config.ID); err != nil { if err := client.ConfigRemove(ctx, config.ID); err != nil {
hasError = true hasError = true
log.Fatalf("failed to remove config %s: %s", config.ID, err) log.Fatalf("failed to remove config %s: %s", config.ID, err)
@ -169,12 +203,23 @@ func terminalState(state swarm.TaskState) bool {
return numberedStates[state] > numberedStates[swarm.TaskStateRunning] return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
} }
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) error { func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) (bool, error) {
var timedOut bool
log.Debugf("waiting on undeploy tasks (timeout=%v secs)", WaitTimeout)
go func() {
t := time.Duration(WaitTimeout) * time.Second
<-time.After(t)
log.Debug("timed out on undeploy")
timedOut = true
}()
terminalStatesReached := 0 terminalStatesReached := 0
for { for {
tasks, err := getStackTasks(ctx, client, namespace) tasks, err := getStackTasks(ctx, client, namespace)
if err != nil { if err != nil {
return fmt.Errorf("failed to get tasks: %w", err) return false, fmt.Errorf("failed to get tasks: %w", err)
} }
for _, task := range tasks { for _, task := range tasks {
@ -187,6 +232,11 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri
if terminalStatesReached == len(tasks) { if terminalStatesReached == len(tasks) {
break break
} }
if timedOut {
return true, fmt.Errorf("deployment timed out 🟠")
}
} }
return nil
return false, nil
} }

View File

@ -3,24 +3,26 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/swa
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"io/ioutil"
"os" "os"
"os/signal" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
stdlibErr "errors" stdlibErr "errors"
tea "github.com/charmbracelet/bubbletea"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/ui"
"coopcloud.tech/abra/pkg/upstream/convert" "coopcloud.tech/abra/pkg/upstream/convert"
"github.com/docker/cli/cli/command/service/progress"
"github.com/docker/cli/cli/command/stack/formatter" "github.com/docker/cli/cli/command/stack/formatter"
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/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@ -106,13 +108,22 @@ type DeployMeta struct {
ChaosVersion string // the --chaos deployment version ChaosVersion string // the --chaos deployment version
} }
func (d DeployMeta) String() string {
var out string
out += fmt.Sprintf("{isDeployed: %v, ", d.IsDeployed)
out += fmt.Sprintf("version: %s, ", d.Version)
out += fmt.Sprintf("isChaos: %v, ", d.IsChaos)
out += fmt.Sprintf("chaosVersion: %s}", d.ChaosVersion)
return out
}
// IsDeployed gathers metadata about an app deployment. // IsDeployed gathers metadata about an app deployment.
func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) (DeployMeta, error) { func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) (DeployMeta, error) {
deployMeta := DeployMeta{ deployMeta := DeployMeta{
IsDeployed: false, IsDeployed: false,
Version: "unknown", Version: "unknown",
IsChaos: false, IsChaos: false,
ChaosVersion: "false", // NOTE(d1): match string type used on label ChaosVersion: config.CHAOS_DEFAULT,
} }
filter := filters.NewArgs() filter := filters.NewArgs()
@ -166,7 +177,7 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string)
func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, services map[string]struct{}) { func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, services map[string]struct{}) {
oldServices, err := GetStackServices(ctx, cl, namespace.Name()) oldServices, err := GetStackServices(ctx, cl, namespace.Name())
if err != nil { if err != nil {
log.Infof("failed to list services: %s", err) log.Warnf("failed to list services: %s", err)
} }
pruneServices := []swarm.Service{} pruneServices := []swarm.Service{}
@ -180,7 +191,17 @@ func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace conve
} }
// RunDeploy is the swarm implementation of docker stack deploy // RunDeploy is the swarm implementation of docker stack deploy
func RunDeploy(cl *dockerClient.Client, opts Deploy, cfg *composetypes.Config, appName string, dontWait bool) error { func RunDeploy(
cl *dockerClient.Client,
opts Deploy,
cfg *composetypes.Config,
appName string,
serverName string,
dontWait bool,
filters filters.Args,
) error {
log.Info("initialising deployment")
if err := validateResolveImageFlag(&opts); err != nil { if err := validateResolveImageFlag(&opts); err != nil {
return err return err
} }
@ -190,7 +211,16 @@ func RunDeploy(cl *dockerClient.Client, opts Deploy, cfg *composetypes.Config, a
opts.ResolveImage = ResolveImageNever opts.ResolveImage = ResolveImageNever
} }
return deployCompose(context.Background(), cl, opts, cfg, appName, dontWait) return deployCompose(
context.Background(),
cl,
opts,
cfg,
appName,
serverName,
dontWait,
filters,
)
} }
// validateResolveImageFlag validates the opts.resolveImage command line option // validateResolveImageFlag validates the opts.resolveImage command line option
@ -203,7 +233,16 @@ func validateResolveImageFlag(opts *Deploy) error {
} }
} }
func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, config *composetypes.Config, appName string, dontWait bool) error { func deployCompose(
ctx context.Context,
cl *dockerClient.Client,
opts Deploy,
config *composetypes.Config,
appName string,
serverName string,
dontWait bool,
filters filters.Args,
) error {
namespace := convert.NewNamespace(opts.Namespace) namespace := convert.NewNamespace(opts.Namespace)
if opts.Prune { if opts.Prune {
@ -244,7 +283,14 @@ func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, co
return err return err
} }
serviceIDs, err := deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage) serviceIDs, err := deployServices(
ctx,
cl,
services,
namespace,
opts.SendRegistryAuth,
opts.ResolveImage,
)
if err != nil { if err != nil {
return err return err
} }
@ -254,13 +300,16 @@ func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, co
return nil return nil
} }
log.Infof("waiting for %s to deploy... please hold 🤚", appName) waitOpts := WaitOpts{
Services: serviceIDs,
if err := waitOnServices(ctx, cl, serviceIDs, appName); err != nil { AppName: appName,
return err ServerName: serverName,
Filters: filters,
} }
log.Infof("successfully deployed %s", appName) if err := WaitOnServices(ctx, cl, waitOpts); err != nil {
return err
}
return nil return nil
} }
@ -286,7 +335,7 @@ func validateExternalNetworks(ctx context.Context, client dockerClient.NetworkAP
// local-scoped networks, so there's no need to inspect them. // local-scoped networks, so there's no need to inspect them.
continue continue
} }
network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{}) network, err := client.NetworkInspect(ctx, networkName, networktypes.InspectOptions{})
switch { switch {
case dockerClient.IsErrNotFound(err): case dockerClient.IsErrNotFound(err):
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy", networkName) return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed, which you can do by running this on the server: docker network create -d overlay proxy", networkName)
@ -332,7 +381,7 @@ func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm
} }
case dockerClient.IsErrNotFound(err): case dockerClient.IsErrNotFound(err):
// config does not exist, then we create a new one. // config does not exist, then we create a new one.
log.Infof("creating config %s", configSpec.Name) log.Debugf("creating config %s", configSpec.Name)
if _, err := cl.ConfigCreate(ctx, configSpec); err != nil { if _, err := cl.ConfigCreate(ctx, configSpec); err != nil {
return errors.Wrapf(err, "failed to create config %s", configSpec.Name) return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
} }
@ -343,13 +392,13 @@ func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm
return nil return nil
} }
func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, networks map[string]types.NetworkCreate) error { func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, networks map[string]networktypes.CreateOptions) error {
existingNetworks, err := getStackNetworks(ctx, cl, namespace.Name()) existingNetworks, err := getStackNetworks(ctx, cl, namespace.Name())
if err != nil { if err != nil {
return err return err
} }
existingNetworkMap := make(map[string]types.NetworkResource) existingNetworkMap := make(map[string]networktypes.Inspect)
for _, network := range existingNetworks { for _, network := range existingNetworks {
existingNetworkMap[network.Name] = network existingNetworkMap[network.Name] = network
} }
@ -363,7 +412,7 @@ func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace conv
createOpts.Driver = defaultNetworkDriver createOpts.Driver = defaultNetworkDriver
} }
log.Infof("creating network %s", name) log.Debugf("creating network %s", name)
if _, err := cl.NetworkCreate(ctx, name, createOpts); err != nil { if _, err := cl.NetworkCreate(ctx, name, createOpts); err != nil {
return errors.Wrapf(err, "failed to create network %s", name) return errors.Wrapf(err, "failed to create network %s", name)
} }
@ -377,10 +426,12 @@ func deployServices(
services map[string]swarm.ServiceSpec, services map[string]swarm.ServiceSpec,
namespace convert.Namespace, namespace convert.Namespace,
sendAuth bool, sendAuth bool,
resolveImage string) ([]string, error) { resolveImage string) ([]ui.ServiceMeta, error) {
var servicesMeta []ui.ServiceMeta
existingServices, err := GetStackServices(ctx, cl, namespace.Name()) existingServices, err := GetStackServices(ctx, cl, namespace.Name())
if err != nil { if err != nil {
return nil, err return servicesMeta, err
} }
existingServiceMap := make(map[string]swarm.Service) existingServiceMap := make(map[string]swarm.Service)
@ -388,8 +439,6 @@ func deployServices(
existingServiceMap[service.Spec.Name] = service existingServiceMap[service.Spec.Name] = service
} }
var serviceIDs []string
for internalName, serviceSpec := range services { for internalName, serviceSpec := range services {
var ( var (
name = namespace.Scope(internalName) name = namespace.Scope(internalName)
@ -398,7 +447,7 @@ func deployServices(
) )
if service, exists := existingServiceMap[name]; exists { if service, exists := existingServiceMap[name]; exists {
log.Infof("updating %s", name) log.Debugf("updating %s", name)
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth} updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
@ -440,9 +489,12 @@ func deployServices(
log.Warn(warning) log.Warn(warning)
} }
serviceIDs = append(serviceIDs, service.ID) servicesMeta = append(servicesMeta, ui.ServiceMeta{
Name: name,
ID: service.ID,
})
} else { } else {
log.Infof("creating %s", name) log.Debugf("creating %s", name)
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth} createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
@ -456,15 +508,18 @@ func deployServices(
return nil, errors.Wrapf(err, "failed to create %s", name) return nil, errors.Wrapf(err, "failed to create %s", name)
} }
serviceIDs = append(serviceIDs, serviceCreateResponse.ID) servicesMeta = append(servicesMeta, ui.ServiceMeta{
Name: name,
ID: serviceCreateResponse.ID,
})
} }
} }
return serviceIDs, nil return servicesMeta, nil
} }
func getStackNetworks(ctx context.Context, dockerclient client.APIClient, namespace string) ([]types.NetworkResource, error) { func getStackNetworks(ctx context.Context, dockerclient client.APIClient, namespace string) ([]networktypes.Inspect, error) {
return dockerclient.NetworkList(ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)}) return dockerclient.NetworkList(ctx, networktypes.ListOptions{Filters: getStackFilter(namespace)})
} }
func getStackSecrets(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Secret, error) { func getStackSecrets(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Secret, error) {
@ -475,67 +530,89 @@ func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespa
return dockerclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)}) return dockerclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
} }
func waitOnServices(ctx context.Context, cl *dockerClient.Client, serviceIDs []string, appName string) error { func timestamp() string {
var errs []error ts := time.Now().UTC().Format(time.RFC3339)
return strings.Replace(ts, ":", "", -1) // get rid of offensive colons
}
for _, serviceID := range serviceIDs { type WaitOpts struct {
if err := WaitOnService(ctx, cl, serviceID, appName); err != nil { AppName string
errs = append(errs, fmt.Errorf("%s: %w", serviceID, err)) Filters filters.Args
} NoLog bool
Quiet bool
ServerName string
Services []ui.ServiceMeta
}
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
timeout := time.Duration(WaitTimeout) * time.Second
model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
tui := tea.NewProgram(model)
if !opts.Quiet {
log.Info("polling deployment status")
} }
if len(errs) > 0 { m, err := log.Without(
func() (tea.Model, error) {
return tui.Run()
},
)
if err != nil {
return fmt.Errorf("waitOnServices: error running TUI: %s", err)
}
deployModel := m.(ui.Model)
if deployModel.TimedOut || deployModel.Failed || deployModel.Quit {
var errs []error
if deployModel.Failed {
errs = append(errs, fmt.Errorf("deploy failed 🛑"))
} else if deployModel.TimedOut {
errs = append(errs, fmt.Errorf("deploy timed out 🟠"))
} else {
errs = append(errs, fmt.Errorf("deploy in progress 🟠"))
}
for _, s := range *deployModel.Streams {
if s.Err != nil {
errs = append(errs, fmt.Errorf("%s: %s", s.Name, s.Err))
}
}
if len(*deployModel.Logs) > 0 && !opts.NoLog {
logsPath := filepath.Join(
config.LOGS_DIR,
opts.ServerName,
fmt.Sprintf("%s_%s", opts.AppName, timestamp()),
)
if err := os.MkdirAll(filepath.Join(config.LOGS_DIR, opts.ServerName), 0764); err != nil {
return fmt.Errorf("waitOnServices: error creating log dir: %s", err)
}
file, err := os.Create(logsPath)
if err != nil {
return fmt.Errorf("waitOnServices: error opening file: %s", err)
}
defer file.Close()
s := strings.Join(*deployModel.Logs, "\n")
if _, err := file.WriteString(s); err != nil {
return fmt.Errorf("waitOnServices: writeFile: %s", err)
}
errs = append(errs, fmt.Errorf("logs: %s", logsPath))
}
return stdlibErr.Join(errs...) return stdlibErr.Join(errs...)
} }
return nil if !opts.Quiet {
} log.Info("deploy succeeded 🟢")
// https://github.com/docker/cli/blob/master/cli/command/service/helpers.go
// https://github.com/docker/cli/blob/master/cli/command/service/progress/progress.go
func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appName string) error {
errChan := make(chan error, 1)
pipeReader, pipeWriter := io.Pipe()
sigintChannel := make(chan os.Signal, 1)
signal.Notify(sigintChannel, os.Interrupt)
defer signal.Stop(sigintChannel)
go func() {
errChan <- progress.ServiceProgress(ctx, cl, serviceID, pipeWriter)
}()
go io.Copy(ioutil.Discard, pipeReader)
timeout := time.Duration(WaitTimeout) * time.Second
select {
case err := <-errChan:
return err
case <-sigintChannel:
return fmt.Errorf(`
Not waiting for %s to deploy. The deployment is ongoing...
If you want to stop the deployment, try:
abra app undeploy %s`, appName, appName)
case <-time.After(timeout):
return fmt.Errorf(`
%s has not converged (%s second timeout reached).
This does not necessarily mean your deployment has failed, it may just be that
the app is taking longer to deploy based on your server resources or network
latency.
You can track latest deployment status with:
abra app ps %s
And inspect the logs with:
abra app logs %s
`, appName, timeout, appName, appName)
} }
return nil
} }
// Copypasta from https://github.com/docker/cli/blob/master/cli/command/stack/swarm/list.go // Copypasta from https://github.com/docker/cli/blob/master/cli/command/stack/swarm/list.go

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,8 +21,10 @@ setup(){
teardown(){ teardown(){
_reset_recipe _reset_recipe
_reset_app
_undeploy_app _undeploy_app
_undeploy_app2 "gitea.$TEST_SERVER"
_reset_app
_reset_tags _reset_tags
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo" run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
@ -46,6 +48,9 @@ teardown(){
assert_success assert_success
assert_output --partial 'foo' assert_output --partial 'foo'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_failure assert_failure
assert_output --partial 'locally unstaged changes' assert_output --partial 'locally unstaged changes'
@ -62,10 +67,12 @@ teardown(){
assert_success assert_success
assert_output --partial 'foo' assert_output --partial 'foo'
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_equal "$(_git_status)" "?? foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--chaos --no-input --no-converge-checks --chaos --no-input --no-converge-checks
assert_success assert_success
assert_output --partial 'NEW CHAOS'
} }
# bats test_tags=slow # bats test_tags=slow
@ -101,8 +108,6 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "deploy latest commit if no published versions and no --chaos" { @test "deploy latest commit if no published versions and no --chaos" {
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
_remove_tags _remove_tags
_wipe_env_version _wipe_env_version
@ -110,7 +115,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --offline --no-input --no-converge-checks --offline
assert_success assert_success
assert_output --partial "$latestCommit" assert_output --partial "${_get_head_hash:0:8}"
} }
# bats test_tags=slow # bats test_tags=slow
@ -128,7 +133,6 @@ teardown(){
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
assert_output --partial "${wantHash:0:8}" assert_output --partial "${wantHash:0:8}"
assert_output --partial 'NEW CHAOS'
} }
# bats test_tags=slow # bats test_tags=slow
@ -163,6 +167,32 @@ teardown(){
assert_output --partial 'already deployed' assert_output --partial 'already deployed'
} }
@test "no re-deploy after chaos deploy without --force/--chaos" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_failure
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force
assert_success
}
@test "no re-deploy without --force" {
_deploy_app
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
assert_failure
}
# bats test_tags=slow # bats test_tags=slow
@test "re-deploy deployed app if --force/--chaos" { @test "re-deploy deployed app if --force/--chaos" {
_deploy_app _deploy_app
@ -192,19 +222,6 @@ teardown(){
run $ABRA app deploy "gitea.$TEST_SERVER" --no-input --no-converge-checks run $ABRA app deploy "gitea.$TEST_SERVER" --no-input --no-converge-checks
assert_success assert_success
assert_output --partial "$latestVersion" assert_output --partial "$latestVersion"
run $ABRA app undeploy "gitea.$TEST_SERVER" --no-input
assert_success
run $ABRA app secret remove "gitea.$TEST_SERVER" --all --no-input
assert_success
run $ABRA app volume remove "gitea.$TEST_SERVER" --no-input
assert_success
run $ABRA app remove "gitea.$TEST_SERVER" --no-input
assert_success
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/gitea.$TEST_SERVER.env"
} }
# bats test_tags=slow # bats test_tags=slow
@ -304,7 +321,6 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure assert_failure
assert_output --partial 'unable to deploy, secrets not generated'
} }
# bats test_tags=slow # bats test_tags=slow
@ -340,10 +356,70 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \ run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" \
--no-input --no-converge-checks --no-input --no-converge-checks
assert_success assert_success
refute_output --partial 'no such file or directory'
_undeploy_app _undeploy_app
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all run $ABRA app secret rm "$TEST_APP_DOMAIN" --all
assert_success assert_success
} }
# bats test_tags=slow
@test "chaos version label includes dirty marker" {
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app labels "$TEST_APP_DOMAIN" --chaos
assert_success
assert_output --regexp 'chaos-version.*+U'
}
# bats test_tags=slow
@test "ignore env version checkout after deploy" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
assert_equal $(_get_current_hash) "$tagHash"
run $ABRA app check --ignore-env-version "$TEST_APP_DOMAIN"
assert_success
assert_equal $(_get_current_hash) "$(_get_head_hash)"
run $ABRA app check "$TEST_APP_DOMAIN"
assert_success
assert_equal $(_get_current_hash) "$tagHash"
}
# bats test_tags=slow
@test "ignore env version on new deploy" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks
assert_success
_undeploy_app
latestRelease=$(_latest_release)
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --ignore-env-version
assert_success
assert_output --partial "$latestRelease"
}
# bats test_tags=slow
@test "no chaos version label if no chaos" {
_deploy_app
run $ABRA app labels "$TEST_APP_DOMAIN" --no-input
assert_success
refute_output --regexp "coop-cloud.abra-test-recipe.$TEST_SERVER.chaos-version"
}

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