Compare commits

...

37 Commits

Author SHA1 Message Date
1bdf1fcfae wip: refactor!: migrate to cobra 2024-07-18 21:39:10 +02:00
827edcb0da test: full width for CI testing [ci skip]
Also clean up the .env.sample.
2024-07-18 11:03:02 +02:00
05489a129c test: re-create serer for setup [ci skip] 2024-07-17 14:32:53 +02:00
c02e11eb0a test: fix order of teardown [ci skip] 2024-07-17 14:15:03 +02:00
8b8e158664 test: int suite fixes 2024-07-17 14:05:46 +02:00
e5a6dea10c fix: catch ctrl-c again; less cryptic logging 2024-07-17 10:09:09 +02:00
1132b09b5b fix: error out for invalid env versions 2024-07-17 10:08:51 +02:00
b2436174b0 chore: more logging for env versions 2024-07-17 10:08:32 +02:00
ea10019068 fix: "secret insert" respects env version 2024-07-17 10:08:13 +02:00
9b0b3c2e4c fix: override version from CLI
See coop-cloud/organising#541
2024-07-17 10:07:47 +02:00
8084bff104 test: env version tests
See coop-cloud/organising#541
2024-07-17 10:06:46 +02:00
d22e2c38ce test: just build main 2024-07-17 08:29:58 +02:00
e945087f79 test: env version writing tests 2024-07-17 08:27:12 +02:00
7734dd555d fix: spacer between multiple versions 2024-07-17 02:12:26 +02:00
aedf5e5ff7 fix: dont write commented out versions
See coop-cloud/organising#626
2024-07-17 01:56:28 +02:00
95c598d030 feat: "app new" supports writing env files
And, automagically, chaos commit hashes.
2024-07-17 01:45:19 +02:00
56068362e8 fix: write versions on deploy/upgrade/rollback
See coop-cloud/organising#625
2024-07-17 01:29:49 +02:00
cf14731b46 refactor: "false" -> CHAOS_DEFAULT 2024-07-17 01:23:12 +02:00
486cfa68d8 test: explode on failures
Closes coop-cloud/organising#623
2024-07-17 00:16:47 +02:00
1718903834 test: reset recipe before undeploying [ci skip] 2024-07-17 00:00:06 +02:00
eb9894e5bb test: dont clone if exists [ci skip] 2024-07-16 23:51:28 +02:00
a2116774e8 test: ensure catalogue in place [ci skip] 2024-07-16 23:46:02 +02:00
d2efdf8bf5 test: adjust output checking [ci skip] 2024-07-16 23:39:10 +02:00
b15c05929c test: adjust output checking [ci skip] 2024-07-16 23:32:12 +02:00
f167a91868 test: skip for now, it's flaking again [ci skip]
We need to solve coop-cloud/organising#541
2024-07-16 23:28:18 +02:00
8cded8752a test: ensure correct server for diffing [ci skip] 2024-07-16 23:25:17 +02:00
d1876e2fae test: do exact diff of JSON for integration
See coop-cloud/organising#627
2024-07-16 23:19:36 +02:00
e42a1bca29 fix: add chaos/deploy versiosn back to ps output
Fix to support alakazam parsing
2024-07-16 22:47:47 +02:00
b5493ba059 refactor: CreateTable2 -> CreateTable [ci skip] 2024-07-16 22:45:03 +02:00
a41a36b8fd fix: dont lock existing version on rollback
Otherwise, we can't select previous versions.
2024-07-16 17:35:15 +02:00
de006782b6 refactor: tablewriter -> lipgloss
Also the jsontable impl. is dropped also. Output is unchanged.
2024-07-16 16:22:47 +02:00
f28cffe6d8 refactor: vertical deploy overview 2024-07-16 09:37:10 +02:00
d3ede0f0f6 refactor: logging with background/padding 2024-07-15 22:55:02 +02:00
ae4653f5e3 build: add full install target [ci skip] 2024-07-13 15:30:38 +02:00
f
7f0a74d3c3 fix: source autocompletion on the current terminal 2024-07-11 12:02:38 -03:00
f
e99114e695 fix: setup should be run once 2024-07-11 12:02:22 -03:00
f
b1208f9db5 fix: sometimes the completion directories already exist 2024-07-11 12:01:21 -03:00
56 changed files with 1356 additions and 726 deletions

View File

@ -71,7 +71,6 @@ steps:
port: 22
command_timeout: 60m
script_stop: true
envs: [ DRONE_SOURCE_BRANCH ]
request_pty: true
script:
- |

View File

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

View File

@ -23,6 +23,8 @@ install-abra:
install-kadabra:
@go install -ldflags=$(LDFLAGS) $(KADABRA)
install: install-abra install-kadabra
build-abra:
@go build -v -ldflags=$(DIST_LDFLAGS) $(ABRA)

View File

@ -1,11 +1,14 @@
package app
import (
"fmt"
"coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/charmbracelet/lipgloss"
"github.com/urfave/cli"
)
@ -40,8 +43,21 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
log.Fatal(err)
}
tableCol := []string{"recipe env sample", "app env"}
table := formatter.CreateTable(tableCol)
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.
Headers("RECIPE ENV SAMPLE", "APP ENV").
StyleFunc(func(row, col int) lipgloss.Style {
switch {
case col == 1:
return lipgloss.NewStyle().Padding(0, 1, 0, 1).Align(lipgloss.Center)
default:
return lipgloss.NewStyle().Padding(0, 1, 0, 1)
}
})
envVars, err := appPkg.CheckEnv(app)
if err != nil {
@ -50,13 +66,15 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
for _, envVar := range envVars {
if envVar.Present {
table.Append([]string{envVar.Name, "✅"})
val := []string{envVar.Name, "✅"}
table.Row(val...)
} else {
table.Append([]string{envVar.Name, "❌"})
val := []string{envVar.Name, "❌"}
table.Row(val...)
}
}
table.Render()
fmt.Println(table)
return nil
},

View File

@ -2,9 +2,11 @@ package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/secret"
@ -47,17 +49,26 @@ EXAMPLE:
abra app deploy foo.example.com 1e83340e`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string
app := internal.ValidateApp(c)
stackName := app.StackName()
specificVersion := c.Args().Get(1)
if specificVersion == "" {
specificVersion = app.Recipe.Version
}
if specificVersion != "" && internal.Chaos {
log.Fatal("cannot use <version> and --chaos together")
}
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)
}
@ -115,7 +126,7 @@ EXAMPLE:
if deployMeta.IsDeployed {
if internal.Force || internal.Chaos {
log.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name)
warnMessages = append(warnMessages, fmt.Sprintf("%s is already deployed", app.Name))
} else {
log.Fatalf("%s is already deployed", app.Name)
}
@ -139,13 +150,13 @@ EXAMPLE:
log.Fatal(err)
}
version = formatter.SmallSHA(head.String())
log.Warn("no versions detected, using latest commit")
warnMessages = append(warnMessages, fmt.Sprintf("no versions detected, using latest commit"))
}
}
chaosVersion := "false"
chaosVersion := config.CHAOS_DEFAULT
if internal.Chaos {
log.Warnf("chaos mode engaged")
warnMessages = append(warnMessages, "chaos mode engaged")
if isChaosCommit {
chaosVersion = specificVersion
@ -201,14 +212,12 @@ EXAMPLE:
for _, envVar := range envVars {
if !envVar.Present {
log.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
warnMessages = append(warnMessages,
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain),
)
}
}
if err := internal.DeployOverview(app, version, chaosVersion, "continue with deployment?"); err != nil {
log.Fatal(err)
}
if !internal.NoDomainChecks {
domainName, ok := app.Env["DOMAIN"]
if ok {
@ -216,10 +225,14 @@ EXAMPLE:
log.Fatal(err)
}
} else {
log.Warn("skipping domain checks as no DOMAIN=... configured for app")
warnMessages = append(warnMessages, "skipping domain checks as no DOMAIN=... configured for app")
}
} else {
log.Warn("skipping domain checks as requested")
warnMessages = append(warnMessages, "skipping domain checks as requested")
}
if err := internal.DeployOverview(app, warnMessages, version, chaosVersion); err != nil {
log.Fatal(err)
}
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
@ -240,12 +253,15 @@ EXAMPLE:
}
}
if app.Recipe.Version != "" && specificVersion != "" && specificVersion != app.Recipe.Version {
err := app.WriteRecipeVersion(specificVersion)
if err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
app.Recipe.Version = version
if chaosVersion != config.CHAOS_DEFAULT {
app.Recipe.Version = chaosVersion
}
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
return nil
},
}

View File

@ -239,15 +239,27 @@ can take some time.`,
serverStat := allStats[app.Server]
tableCol := []string{"recipe", "domain"}
headers := []string{"RECIPE", "DOMAIN"}
if status {
tableCol = append(tableCol, []string{"status", "chaos", "version", "upgrade", "autoupdate"}...)
headers = append(headers, []string{
"STATUS",
"CHAOS",
"VERSION",
"UPGRADE",
"AUTOUPDATE"}...,
)
}
table := formatter.CreateTable(tableCol)
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
var rows [][]string
for _, appStat := range serverStat.Apps {
tableRow := []string{appStat.Recipe, appStat.Domain}
row := []string{appStat.Recipe, appStat.Domain}
if status {
chaosStatus := appStat.Chaos
if chaosStatus != "unknown" {
@ -259,17 +271,27 @@ can take some time.`,
chaosStatus = appStat.ChaosVersion
}
}
tableRow = append(tableRow, []string{appStat.Status, chaosStatus, appStat.Version, appStat.Upgrade, appStat.AutoUpdate}...)
row = append(row, []string{
appStat.Status,
chaosStatus,
appStat.Version,
appStat.Upgrade,
appStat.AutoUpdate}...,
)
}
table.Append(tableRow)
rows = append(rows, row)
}
if table.NumLines() > 0 {
table.Render()
table.Rows(rows...)
if len(rows) > 0 {
fmt.Println(table)
if status {
fmt.Println(fmt.Sprintf(
"server: %s | total apps: %v | versioned: %v | unversioned: %v | latest: %v | upgrade: %v",
"SERVER: %s | TOTAL APPS: %v | VERSIONED: %v | UNVERSIONED: %v | LATEST : %v | UPGRADE: %v",
app.Server,
serverStat.AppCount,
serverStat.VersionCount,
@ -278,19 +300,21 @@ can take some time.`,
serverStat.UpgradeCount,
))
} else {
fmt.Println(fmt.Sprintf("server: %s | total apps: %v", app.Server, serverStat.AppCount))
log.Infof("SERVER: %s TOTAL APPS: %v", app.Server, serverStat.AppCount)
}
}
if len(allStats) > 1 && table.NumLines() > 0 {
fmt.Println() // newline separator for multiple servers
if len(allStats) > 1 && len(rows) > 0 {
fmt.Println() // newline separator for multiple servers
}
}
alreadySeen[app.Server] = true
}
if len(allStats) > 1 {
fmt.Println(fmt.Sprintf("total servers: %v | total apps: %v ", totalServersCount, totalAppsCount))
totalServers := formatter.BoldStyle.Render("TOTAL SERVERS")
totalApps := formatter.BoldStyle.Render("TOTAL APPS")
log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount)
}
return nil

View File

@ -4,16 +4,17 @@ import (
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/jsontable"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss/table"
dockerClient "github.com/docker/docker/client"
"github.com/urfave/cli"
)
@ -28,6 +29,8 @@ deploy <domain>" to do so.
You can see what recipes are available (i.e. values for the <recipe> argument)
by running "abra recipe ls".
Recipe commit hashes are supported values for "[<version>]".
Passing the "--secrets/-S" flag will automatically generate secrets for your
app and store them encrypted at rest on the chosen target server. These
generated secrets are only visible at generation time, so please take care to
@ -66,6 +69,7 @@ var appNewCommand = cli.Command{
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
var version string
if !internal.Chaos {
if err := recipe.EnsureIsClean(); err != nil {
log.Fatal(err)
@ -75,16 +79,13 @@ var appNewCommand = cli.Command{
log.Fatal(err)
}
}
if c.Args().Get(1) == "" {
var version string
if c.Args().Get(1) == "" {
recipeVersions, err := recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
// NOTE(d1): determine whether recipe versions exist or not and check
// out the latest version or current HEAD
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
@ -100,7 +101,8 @@ var appNewCommand = cli.Command{
}
}
} else {
if _, err := recipe.EnsureVersion(c.Args().Get(1)); err != nil {
version = c.Args().Get(1)
if _, err := recipe.EnsureVersion(version); err != nil {
log.Fatal(err)
}
}
@ -127,7 +129,7 @@ var appNewCommand = cli.Command{
}
var secrets AppSecrets
var secretTable *jsontable.JSONTable
var secretsTable *table.Table
if internal.Secrets {
sampleEnv, err := recipe.SampleEnv()
if err != nil {
@ -158,10 +160,16 @@ var appNewCommand = cli.Command{
log.Fatal(err)
}
secretCols := []string{"Name", "Value"}
secretTable = formatter.CreateTable(secretCols)
secretsTable, err = formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{"NAME", "VALUE"}
secretsTable.Headers(headers...)
for name, val := range secrets {
secretTable.Append([]string{name, val})
secretsTable.Row(name, val)
}
}
@ -169,14 +177,20 @@ var appNewCommand = cli.Command{
internal.NewAppServer = "local"
}
tableCol := []string{"server", "recipe", "domain"}
table := formatter.CreateTable(tableCol)
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"}
table.Headers(headers...)
table.Row(internal.NewAppServer, internal.Domain, recipe.Name, version)
log.Infof("new app '%s' created 🌞", recipe.Name)
fmt.Println("")
table.Render()
fmt.Println(table)
fmt.Println("")
fmt.Println("Configure this app:")
@ -190,8 +204,23 @@ var appNewCommand = cli.Command{
fmt.Println("")
fmt.Println("Generated secrets:")
fmt.Println("")
secretTable.Render()
log.Warn("generated secrets are not shown again, please take note of them NOW")
fmt.Println(secretsTable)
log.Warnf(
"generated secrets %s shown again, please take note of them %s",
formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"),
)
}
app, err := app.Get(internal.Domain)
if err != nil {
log.Fatal(err)
}
log.Debugf("choosing %s as version to save to env file", version)
if err := app.WriteRecipeVersion(version); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
return nil

View File

@ -9,6 +9,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
abraService "coopcloud.tech/abra/pkg/service"
@ -52,7 +53,7 @@ var appPsCommand = cli.Command{
log.Fatalf("%s is not deployed?", app.Name)
}
chaosVersion := "false"
chaosVersion := config.CHAOS_DEFAULT
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok {
isChaos, exists := statusMeta["chaos"]
@ -94,7 +95,7 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
return
}
var tablerows [][]string
var rows [][]string
allContainerStats := make(map[string]map[string]string)
for _, service := range compose.Services {
filters := filters.NewArgs()
@ -134,9 +135,7 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
allContainerStats[containerStats["service"]] = containerStats
tablerow := []string{
deployedVersion,
chaosVersion,
row := []string{
containerStats["service"],
containerStats["image"],
containerStats["created"],
@ -145,25 +144,37 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
containerStats["ports"],
}
tablerows = append(tablerows, tablerow)
rows = append(rows, row)
}
if internal.MachineReadable {
jsonstring, err := json.Marshal(allContainerStats)
if err != nil {
log.Fatal(err)
log.Fatal("unable to convert to JSON: %s", err)
}
fmt.Println(string(jsonstring))
return
}
tableCol := []string{"version", "chaos", "service", "image", "created", "status", "state", "ports"}
table := formatter.CreateTable(tableCol)
for _, row := range tablerows {
table.Append(row)
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.SetAutoMergeCellsByColumnIndex([]int{0, 1})
table.Render()
headers := []string{
"SERVICE",
"IMAGE",
"CREATED",
"STATUS",
"STATE",
"PORTS",
}
table.
Headers(headers...).
Rows(rows...)
fmt.Println(table)
log.Infof("VERSION: %s CHAOS: %s", deployedVersion, chaosVersion)
}

View File

@ -49,12 +49,14 @@ flag.`,
app := internal.ValidateApp(c)
if !internal.Force && !internal.NoInput {
log.Warnf("ALERTA ALERTA: this will completely remove %s data and config locally and remotely", app.Name)
response := false
msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?"
prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)}
prompt := &survey.Confirm{Message: "are you sure?"}
if err := survey.AskOne(prompt, &response); err != nil {
log.Fatal(err)
}
if !response {
log.Fatal("aborting as requested")
}

View File

@ -6,6 +6,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/lint"
stack "coopcloud.tech/abra/pkg/upstream/stack"
@ -47,9 +48,17 @@ EXAMPLE:
abra app rollback foo.example.com 1.2.3+3.2.1`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string
app := internal.ValidateApp(c)
stackName := app.StackName()
specificVersion := c.Args().Get(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)
}
@ -82,13 +91,9 @@ EXAMPLE:
var availableDowngrades []string
if deployMeta.Version == "unknown" {
availableDowngrades = versions
log.Warnf("failed to determine deployed version of %s", app.Name)
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
}
specificVersion := c.Args().Get(1)
if specificVersion == "" {
specificVersion = app.Recipe.Version
}
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
@ -113,7 +118,7 @@ EXAMPLE:
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
log.Warn("attempting to rollback a chaos deployment")
warnMessages = append(warnMessages, fmt.Sprintf("attempting to rollback a chaos deployment"))
}
for _, version := range versions {
@ -197,13 +202,20 @@ EXAMPLE:
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
appPkg.SetUpdateLabel(compose, stackName, app.Env)
chaosVersion := "false"
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
// NOTE(d1): no release notes implemeneted for rolling back
if err := internal.NewVersionOverview(app, deployMeta.Version, chaosVersion, chosenDowngrade, ""); err != nil {
if err := internal.NewVersionOverview(
app,
warnMessages,
"rollback",
deployMeta.Version,
chaosVersion,
chosenDowngrade,
""); err != nil {
log.Fatal(err)
}
@ -211,11 +223,10 @@ EXAMPLE:
log.Fatal(err)
}
if app.Recipe.Version != "" {
err := app.WriteRecipeVersion(chosenDowngrade)
if err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
app.Recipe.Version = chosenDowngrade
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
return nil

View File

@ -115,18 +115,37 @@ var appSecretGenerateCommand = cli.Command{
os.Exit(1)
}
tableCol := []string{"name", "value"}
table := formatter.CreateTable(tableCol)
headers := []string{"NAME", "VALUE"}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
var rows [][]string
for name, val := range secretVals {
table.Append([]string{name, val})
row := []string{name, val}
rows = append(rows, row)
table.Row(row...)
}
if internal.MachineReadable {
table.JSONRender()
} else {
table.Render()
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
}
log.Warn("generated secrets are not shown again, please take note of them NOW")
fmt.Println(table)
log.Warnf(
"generated secrets %s shown again, please take note of them %s",
formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"),
)
return nil
},
@ -141,6 +160,7 @@ var appSecretInsertCommand = cli.Command{
internal.PassFlag,
internal.FileFlag,
internal.TrimFlag,
internal.ChaosFlag,
},
Before: internal.SubCommandBefore,
ArgsUsage: "<domain> <secret-name> <version> <data>",
@ -159,6 +179,9 @@ Example:
`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if len(c.Args()) != 4 {
internal.ShowSubcommandHelpAndError(c, errors.New("missing arguments?"))
@ -345,34 +368,48 @@ var appSecretLsCommand = cli.Command{
log.Fatal(err)
}
tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"}
table := formatter.CreateTable(tableCol)
headers := []string{"NAME", "VERSION", "GENERATED NAME", "CREATED ON SERVER"}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
secStats, err := secret.PollSecretsStatus(cl, app)
if err != nil {
log.Fatal(err)
}
var rows [][]string
for _, secStat := range secStats {
tableRow := []string{
row := []string{
secStat.LocalName,
secStat.Version,
secStat.RemoteName,
strconv.FormatBool(secStat.CreatedOnRemote),
}
table.Append(tableRow)
rows = append(rows, row)
table.Row(row...)
}
if table.NumLines() > 0 {
if len(rows) > 0 {
if internal.MachineReadable {
table.JSONRender()
} else {
table.Render()
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
}
} else {
log.Warnf("no secrets stored for %s", app.Name)
fmt.Println(table)
return nil
}
log.Warnf("no secrets stored for %s", app.Name)
return nil
},
}

View File

@ -56,9 +56,15 @@ var appServicesCommand = cli.Command{
log.Fatal(err)
}
tableCol := []string{"service name", "image"}
table := formatter.CreateTable(tableCol)
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)", "IMAGE"}
table.Headers(headers...)
var rows [][]string
for _, container := range containers {
var containerNames []string
for _, containerName := range container.Names {
@ -69,14 +75,20 @@ var appServicesCommand = cli.Command{
serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)
tableRow := []string{
row := []string{
serviceShortName,
serviceLongName,
formatter.RemoveSha(container.Image),
}
table.Append(tableRow)
rows = append(rows, row)
}
table.Render()
table.Rows(rows...)
if len(rows) > 0 {
fmt.Println(table)
}
return nil
},

View File

@ -8,6 +8,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
stack "coopcloud.tech/abra/pkg/upstream/stack"
@ -102,12 +103,12 @@ Passing "-p/--prune" does not remove those volumes.`,
log.Fatalf("%s is not deployed?", app.Name)
}
chaosVersion := "false"
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
if err := internal.DeployOverview(app, deployMeta.Version, chaosVersion, "continue with undeploy?"); err != nil {
if err := internal.DeployOverview(app, []string{}, deployMeta.Version, chaosVersion); err != nil {
log.Fatal(err)
}

View File

@ -8,6 +8,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
@ -47,9 +48,17 @@ EXAMPLE:
abra app upgrade foo.example.com 1.2.3+3.2.1`,
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string
app := internal.ValidateApp(c)
stackName := app.StackName()
specificVersion := c.Args().Get(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)
}
@ -82,10 +91,9 @@ EXAMPLE:
var availableUpgrades []string
if deployMeta.Version == "unknown" {
availableUpgrades = versions
log.Warnf("failed to determine deployed version of %s", app.Name)
warnMessages = append(warnMessages, fmt.Sprintf("failed to determine deployed version of %s", app.Name))
}
specificVersion := c.Args().Get(1)
if specificVersion != "" {
parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version)
if err != nil {
@ -114,7 +122,7 @@ EXAMPLE:
if deployMeta.Version != "unknown" && specificVersion == "" {
if deployMeta.IsChaos {
log.Warn("attempting to upgrade a chaos deployment")
warnMessages = append(warnMessages, fmt.Sprintf("attempting to upgrade a chaos deployment"))
}
for _, version := range versions {
@ -156,7 +164,7 @@ EXAMPLE:
}
if internal.Force && chosenUpgrade == "" {
log.Warnf("%s is already upgraded to latest but continuing (--force)", app.Name)
warnMessages = append(warnMessages, fmt.Sprintf("%s is already upgraded to latest", app.Name))
chosenUpgrade = deployMeta.Version
}
@ -230,7 +238,9 @@ EXAMPLE:
for _, envVar := range envVars {
if !envVar.Present {
log.Warnf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain)
warnMessages = append(warnMessages,
fmt.Sprintf("env var %s missing from %s.env, present in recipe .env.sample", envVar.Name, app.Domain),
)
}
}
@ -240,12 +250,19 @@ EXAMPLE:
return nil
}
chaosVersion := "false"
chaosVersion := config.CHAOS_DEFAULT
if deployMeta.IsChaos {
chaosVersion = deployMeta.ChaosVersion
}
if err := internal.NewVersionOverview(app, deployMeta.Version, chaosVersion, chosenUpgrade, releaseNotes); err != nil {
if err := internal.NewVersionOverview(
app,
warnMessages,
"upgrade",
deployMeta.Version,
chaosVersion,
chosenUpgrade,
releaseNotes); err != nil {
log.Fatal(err)
}
@ -267,11 +284,10 @@ EXAMPLE:
}
}
if app.Recipe.Version != "" {
err := app.WriteRecipeVersion(chosenUpgrade)
if err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
app.Recipe.Version = chosenUpgrade
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version); err != nil {
log.Fatalf("writing new recipe version in env file: %s", err)
}
return nil

View File

@ -2,6 +2,7 @@ package app
import (
"context"
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
@ -37,26 +38,35 @@ var appVolumeListCommand = cli.Command{
log.Fatal(err)
}
volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
volumes, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
if err != nil {
log.Fatal(err)
}
table := formatter.CreateTable([]string{"name", "created", "mounted"})
var volTable [][]string
for _, volume := range volumeList {
volRow := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
volTable = append(volTable, volRow)
headers := []string{"name", "created", "mounted"}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.AppendBulk(volTable)
table.Headers(headers...)
if table.NumLines() > 0 {
table.Render()
} else {
log.Warnf("no volumes created for %s", app.Name)
var rows [][]string
for _, volume := range volumes {
row := []string{volume.Name, volume.CreatedAt, volume.Mountpoint}
rows = append(rows, row)
}
table.Rows(rows...)
if len(rows) > 0 {
fmt.Println(table)
return nil
}
log.Warnf("no volumes created for %s", app.Name)
return nil
},
}

View File

@ -18,6 +18,7 @@ import (
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/web"
charmLog "github.com/charmbracelet/log"
"github.com/spf13/cobra"
"github.com/urfave/cli"
)
@ -80,26 +81,29 @@ EXAMPLE:
switch shellType {
case "bash":
fmt.Println(fmt.Sprintf(`
# run the following commands to install auto-completion
sudo mkdir /etc/bash_completion.d/
# run the following commands once to install auto-completion
sudo mkdir -p /etc/bash_completion.d/
sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
source /etc/bash_completion.d/abra
# To test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "zsh":
fmt.Println(fmt.Sprintf(`
# run the following commands to install auto-completion
sudo mkdir /etc/zsh/completion.d/
# run the following commands to once install auto-completion
sudo mkdir -p /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
source /etc/zsh/completion.d/abra
# to test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
case "fish":
fmt.Println(fmt.Sprintf(`
# run the following commands to install auto-completion
# run the following commands once to install auto-completion
sudo mkdir -p /etc/fish/completions
sudo cp %s /etc/fish/completions/abra
echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
source /etc/fish/completions/abra
# to test, run the following: "abra app <hit tab key>" - you should see command completion!
`, autocompletionFile))
}
@ -109,38 +113,30 @@ echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish
}
// UpgradeCommand upgrades abra in-place.
var UpgradeCommand = cli.Command{
Name: "upgrade",
var UpgradeCommand = &cobra.Command{
Use: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade abra",
Description: `
Upgrade abra in-place with the latest stable or release candidate.
Short: "Upgrade abra",
Example: "abra upgrade",
Long: `Upgrade abra in-place with the latest stable or release candidate.
Use "-r/--rc" to install the latest release candidate. Please bear in mind that
it may contain absolutely catastrophic deal-breaker bugs. Thank you very much
for the testing efforts 💗
EXAMPLE:
abra upgrade
abra upgrade --rc`,
Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error {
for the testing efforts 💗`,
Run: func(cmd *cobra.Command, args []string) {
mainURL := "https://install.abra.coopcloud.tech"
cmd := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
comm := exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash", mainURL))
if internal.RC {
releaseCandidateURL := "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer"
cmd = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
comm = exec.Command("bash", "-c", fmt.Sprintf("wget -q -O- %s | bash -s -- --rc", releaseCandidateURL))
}
log.Debugf("attempting to run %s", cmd)
if err := internal.RunCmd(cmd); err != nil {
if err := internal.RunCmd(comm); err != nil {
log.Fatal(err)
}
return nil
},
}
@ -161,7 +157,6 @@ func newAbraApp(version, commit string) *cli.App {
server.ServerCommand,
recipe.RecipeCommand,
catalogue.CatalogueCommand,
UpgradeCommand,
AutoCompleteCommand,
},
BashComplete: autocomplete.SubcommandComplete,
@ -187,7 +182,9 @@ func newAbraApp(version, commit string) *cli.App {
}
}
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger)
log.Debugf("abra version %s, commit %s", version, commit)
return nil
@ -204,3 +201,7 @@ func RunApp(version, commit string) {
log.Fatal(err)
}
}
func init() {
UpgradeCommand.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -6,17 +6,41 @@ import (
"strings"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss"
dockerClient "github.com/docker/docker/client"
)
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion, releaseNotes string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos", "to deploy"}
table := formatter.CreateTable(tableCol)
var borderStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Padding(0, 1, 0, 1).
MaxWidth(79).
BorderForeground(lipgloss.Color("63"))
var headerStyle = lipgloss.NewStyle().
Underline(true).
Bold(true)
var leftStyle = lipgloss.NewStyle().
Bold(true)
var rightStyle = lipgloss.NewStyle()
// horizontal is a JoinHorizontal helper function.
func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right)
}
// NewVersionOverview shows an upgrade or downgrade overview
func NewVersionOverview(
app appPkg.App,
warnMessages []string,
kind,
currentVersion,
chaosVersion,
newVersion,
releaseNotes string) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
@ -27,22 +51,36 @@ func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion
server = "local"
}
table.Append([]string{
server,
app.Recipe.Name,
deployConfig,
app.Domain,
currentVersion,
chaosVersion,
newVersion,
})
table.Render()
body := strings.Builder{}
body.WriteString(
borderStyle.Render(
lipgloss.JoinVertical(
lipgloss.Center,
headerStyle.Render(fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind))),
lipgloss.JoinVertical(
lipgloss.Left,
horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)),
horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)),
horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)),
horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)),
horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(currentVersion)),
horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Render(chaosVersion)),
horizontal(leftStyle.Render("DEPLOY"), " ", rightStyle.Padding(0).Render(newVersion)),
),
),
),
)
fmt.Println(body.String())
if releaseNotes != "" && newVersion != "" {
fmt.Println()
fmt.Print(releaseNotes)
} else {
log.Warnf("no release notes available for %s", newVersion)
warnMessages = append(warnMessages, fmt.Sprintf("no release notes available for %s", newVersion))
}
for _, msg := range warnMessages {
log.Warn(msg)
}
if NoInput {
@ -50,16 +88,66 @@ func NewVersionOverview(app appPkg.App, currentVersion, chaosVersion, newVersion
}
response := false
prompt := &survey.Confirm{
Message: "continue with deployment?",
}
prompt := &survey.Confirm{Message: "proceed?"}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal("exiting as requested")
log.Fatal("deployment cancelled")
}
return nil
}
// DeployOverview shows a deployment overview
func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion string) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
server := app.Server
if app.Server == "default" {
server = "local"
}
body := strings.Builder{}
body.WriteString(
borderStyle.Render(
lipgloss.JoinVertical(
lipgloss.Center,
headerStyle.Render("DEPLOY OVERVIEW"),
lipgloss.JoinVertical(
lipgloss.Left,
horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)),
horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)),
horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)),
horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)),
horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(version)),
horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Padding(0).Render(chaosVersion)),
),
),
),
)
fmt.Println(body.String())
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
@ -118,48 +206,3 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
}
return nil
}
// DeployOverview shows a deployment overview
func DeployOverview(app appPkg.App, version, chaosVersion, message string) error {
tableCol := []string{"server", "recipe", "config", "domain", "version", "chaos"}
table := formatter.CreateTable(tableCol)
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n")
}
server := app.Server
if app.Server == "default" {
server = "local"
}
table.Append([]string{
server,
app.Recipe.Name,
deployConfig,
app.Domain,
version,
chaosVersion,
})
table.Render()
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{
Message: message,
}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal("exiting as requested")
}
return nil
}

46
cli/newcli.go Normal file
View File

@ -0,0 +1,46 @@
package cli
import (
"fmt"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
var (
Debug bool
Offline bool
NoInput bool
)
func RunApp2(version, commit string) {
rootCmd := &cobra.Command{
Use: "abra",
Short: "The Co-op Cloud command-line utility belt 🎩🐇",
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
PreRun: func(cmd *cobra.Command, args []string) {
log.Info("HELLO LOGGING")
},
}
rootCmd.PersistentFlags().BoolVarP(
&Debug, "debug", "d", false,
"show debug messages",
)
rootCmd.PersistentFlags().BoolVarP(
&NoInput, "no-input", "n", false,
"toggle non-interactive mode",
)
rootCmd.PersistentFlags().BoolVarP(
&Offline, "offline", "o", false,
"prefer offline & filesystem access",
)
rootCmd.AddCommand(UpgradeCommand)
if err := rootCmd.Execute(); err != nil {
log.Fatal(err)
}
}

View File

@ -32,11 +32,25 @@ var recipeLintCommand = cli.Command{
log.Fatal(err)
}
tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
table := formatter.CreateTable(tableCol)
headers := []string{
"ref",
"rule",
"severity",
"satisfied",
"skipped",
"resolve",
}
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers(headers...)
hasError := false
bar := formatter.CreateProgressbar(-1, "running recipe lint rules...")
var rows [][]string
var warnMessages []string
for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] {
if internal.OnlyErrors && rule.Level != "error" {
@ -58,7 +72,7 @@ var recipeLintCommand = cli.Command{
if !skipped {
ok, err := rule.Function(recipe)
if err != nil {
log.Warn(err)
warnMessages = append(warnMessages, err.Error())
}
if !ok && rule.Level == "error" {
@ -78,26 +92,30 @@ var recipeLintCommand = cli.Command{
}
}
table.Append([]string{
row := []string{
rule.Ref,
rule.Description,
rule.Level,
satisfiedOutput,
skippedOutput,
rule.HowToResolve,
})
}
bar.Add(1)
rows = append(rows, row)
table.Row(row...)
}
}
if table.NumLines() > 0 {
fmt.Println()
table.Render()
}
if len(rows) > 0 {
fmt.Println(table)
if hasError {
log.Warn("watch out, some critical errors are present in your recipe config")
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
if hasError {
log.Warnf("critical errors present in %s config", recipe.Name)
}
}
return nil

View File

@ -41,12 +41,27 @@ var recipeListCommand = cli.Command{
recipes := catl.Flatten()
sort.Sort(recipe.ByRecipeName(recipes))
tableCol := []string{"name", "category", "status", "healthcheck", "backups", "email", "tests", "SSO"}
table := formatter.CreateTable(tableCol)
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
len := 0
headers := []string{
"name",
"category",
"status",
"healthcheck",
"backups",
"email",
"tests",
"SSO",
}
table.Headers(headers...)
var rows [][]string
for _, recipe := range recipes {
tableRow := []string{
row := []string{
recipe.Name,
recipe.Category,
strconv.Itoa(recipe.Features.Status),
@ -59,23 +74,27 @@ var recipeListCommand = cli.Command{
if pattern != "" {
if strings.Contains(recipe.Name, pattern) {
table.Append(tableRow)
len++
table.Row(row...)
rows = append(rows, row)
}
} else {
table.Append(tableRow)
len++
table.Row(row...)
rows = append(rows, row)
}
}
if table.NumLines() > 0 {
if len(rows) > 0 {
if internal.MachineReadable {
table.SetCaption(false, "")
table.JSONRender()
} else {
table.SetCaption(true, fmt.Sprintf("total recipes: %v", len))
table.Render()
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
}
fmt.Println(table)
log.Infof("total recipes: %v", len(rows))
}
return nil

View File

@ -9,7 +9,6 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/olekukonko/tablewriter"
"github.com/urfave/cli"
)
@ -37,6 +36,8 @@ var recipeVersionCommand = cli.Command{
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error {
var warnMessages []string
recipe := internal.ValidateRecipe(c)
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
@ -46,47 +47,65 @@ var recipeVersionCommand = cli.Command{
recipeMeta, ok := catl[recipe.Name]
if !ok {
log.Warn("no published versions in catalogue, trying local recipe repository")
warnMessages = append(warnMessages, "retrieved versions from local recipe repository")
recipeVersions, err := recipe.GetRecipeVersions()
if err != nil {
log.Warn(err)
warnMessages = append(warnMessages, err.Error())
}
recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions}
}
if len(recipeMeta.Versions) == 0 {
log.Fatalf("%s has no catalogue published versions?", recipe.Name)
log.Fatalf("%s has no published versions?", recipe.Name)
}
tableCols := []string{"version", "service", "image", "tag"}
aggregated_table := formatter.CreateTable(tableCols)
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
table := formatter.CreateTable(tableCols)
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
table.Headers("SERVICE", "NAME", "TAG")
for version, meta := range recipeMeta.Versions[i] {
var versions [][]string
var allRows [][]string
var rows [][]string
for service, serviceMeta := range meta {
versions = append(versions, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
rows = append(rows, []string{service, serviceMeta.Image, serviceMeta.Tag})
allRows = append(allRows, []string{version, service, serviceMeta.Image, serviceMeta.Tag})
}
sort.Slice(versions, sortServiceByName(versions))
sort.Slice(rows, sortServiceByName(rows))
for _, version := range versions {
table.Append(version)
aggregated_table.Append(version)
}
table.Rows(rows...)
if !internal.MachineReadable {
table.SetAutoMergeCellsByColumnIndex([]int{0})
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.Render()
fmt.Println(table)
log.Infof("VERSION: %s", version)
fmt.Println()
continue
}
if internal.MachineReadable {
sort.Slice(allRows, sortServiceByName(allRows))
headers := []string{"VERSION", "SERVICE", "NAME", "TAG"}
out, err := formatter.ToJSON(headers, allRows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
continue
}
}
}
if internal.MachineReadable {
aggregated_table.JSONRender()
if !internal.MachineReadable {
for _, warnMsg := range warnMessages {
log.Warn(warnMsg)
}
}
return nil

View File

@ -1,6 +1,7 @@
package server
import (
"fmt"
"strings"
"coopcloud.tech/abra/cli/internal"
@ -29,14 +30,20 @@ var serverListCommand = cli.Command{
log.Fatal(err)
}
tableColumns := []string{"name", "host"}
table := formatter.CreateTable(tableColumns)
table, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
headers := []string{"NAME", "HOST"}
table.Headers(headers...)
serverNames, err := config.ReadServerNames()
if err != nil {
log.Fatal(err)
}
var rows [][]string
for _, serverName := range serverNames {
var row []string
for _, ctx := range contexts {
@ -57,6 +64,7 @@ var serverListCommand = cli.Command{
}
row = []string{serverName, sp.Host}
rows = append(rows, row)
}
}
@ -66,17 +74,22 @@ var serverListCommand = cli.Command{
} else {
row = []string{serverName, "unknown"}
}
rows = append(rows, row)
}
table.Append(row)
table.Row(row...)
}
if internal.MachineReadable {
table.JSONRender()
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
}
fmt.Println(out)
return nil
}
table.Render()
fmt.Println(table)
return nil
},

View File

@ -487,8 +487,11 @@ func newAbraApp(version, commit string) *cli.App {
}
app.Before = func(c *cli.Context) error {
log.Logger.SetStyles(log.Styles())
charmLog.SetDefault(log.Logger)
log.Debugf("kadabra version %s, commit %s", version, commit)
return nil
}

View File

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

9
go.mod
View File

@ -6,6 +6,7 @@ require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/lipgloss v0.11.1
github.com/charmbracelet/log v0.4.0
github.com/distribution/reference v0.6.0
github.com/docker/cli v27.0.3+incompatible
@ -15,9 +16,9 @@ require (
github.com/google/go-cmp v0.6.0
github.com/moby/sys/signal v0.7.0
github.com/moby/term v0.5.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.14.4
golang.org/x/term v0.22.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.1
)
@ -32,8 +33,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/lipgloss v0.11.0 // indirect
github.com/charmbracelet/x/ansi v0.1.2 // indirect
github.com/charmbracelet/x/ansi v0.1.3 // indirect
github.com/cloudflare/circl v1.3.9 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
@ -105,7 +105,6 @@ require (
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
@ -131,7 +130,7 @@ require (
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli v1.22.15

11
go.sum
View File

@ -135,12 +135,12 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
github.com/charmbracelet/lipgloss v0.11.1 h1:a8KgVPHa7kOoP95vm2tQQrjD2AKhbWmfr4uJ2RW6kNk=
github.com/charmbracelet/lipgloss v0.11.1/go.mod h1:beLlcmkF7MWA+5UrKKIRo/VJ21xGXr7YJ9miWfdMRIU=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY=
github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/ansi v0.1.3 h1:RBh/eleNWML5R524mjUF0yVRePTwqN9tPtV+DPgO5Lw=
github.com/charmbracelet/x/ansi v0.1.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
@ -621,7 +621,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
@ -687,8 +686,6 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

View File

@ -576,6 +576,7 @@ func (a App) WriteRecipeVersion(version string) error {
}
defer file.Close()
skipped := false
scanner := bufio.NewScanner(file)
lines := []string{}
for scanner.Scan() {
@ -584,6 +585,18 @@ func (a App) WriteRecipeVersion(version string) error {
lines = append(lines, line)
continue
}
if strings.HasPrefix(line, "#") {
lines = append(lines, line)
continue
}
if strings.Contains(line, version) {
skipped = true
lines = append(lines, line)
continue
}
splitted := strings.Split(line, ":")
line = fmt.Sprintf("%s:%s", splitted[0], version)
lines = append(lines, line)
@ -593,5 +606,15 @@ func (a App) WriteRecipeVersion(version string) error {
log.Fatal(err)
}
return os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm)
if err := os.WriteFile(a.Path, []byte(strings.Join(lines, "\n")), os.ModePerm); err != nil {
log.Fatal(err)
}
if !skipped {
log.Infof("version %s saved to %s.env", version, a.Domain)
} else {
log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain)
}
return nil
}

View File

@ -107,4 +107,5 @@ var (
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
CHAOS_DEFAULT = "false"
)

View File

@ -1,18 +1,26 @@
package formatter
import (
"bytes"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/docker/go-units"
// "github.com/olekukonko/tablewriter"
"coopcloud.tech/abra/pkg/jsontable"
"golang.org/x/term"
"coopcloud.tech/abra/pkg/log"
"github.com/schollz/progressbar/v3"
)
var BoldStyle = lipgloss.NewStyle().
Bold(true).
Underline(true)
func ShortenID(str string) string {
return str[:12]
}
@ -34,11 +42,67 @@ func HumanDuration(timestamp int64) string {
}
// CreateTable prepares a table layout for output.
func CreateTable(columns []string) *jsontable.JSONTable {
table := jsontable.NewJSONTable(os.Stdout)
table.SetAutoWrapText(false)
table.SetHeader(columns)
return table
func CreateTable() (*table.Table, error) {
table := table.New().
Border(lipgloss.ThickBorder()).
BorderStyle(
lipgloss.NewStyle().
Foreground(lipgloss.Color("63")),
)
if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" {
// NOTE(d1): no width limits for CI testing since we test against outputs
log.Debug("detected ABRA_CI=1")
return table, nil
}
width, _, err := term.GetSize(0)
if err != nil {
return nil, err
}
if width-10 < 79 {
// NOTE(d1): maintain standard minimum width
table.Width(79)
} else {
// NOTE(d1): tests show that this produces stable border drawing
table.Width(width - 10)
}
return table, nil
}
// ToJSON converts a lipgloss.Table to JSON representation. It's not a robust
// implementation and mainly caters for our current use case which is basically
// a bunch of strings. See https://github.com/charmbracelet/lipgloss/issues/335
// for the real thing (hopefully).
func ToJSON(headers []string, rows [][]string) (string, error) {
var buff bytes.Buffer
buff.Write([]byte("["))
for idx, row := range rows {
payload := make(map[string]string)
for idx, header := range headers {
payload[strings.ToLower(header)] = row[idx]
}
serialized, err := json.Marshal(payload)
if err != nil {
return "", err
}
buff.Write(serialized)
if idx < (len(rows) - 1) {
buff.Write([]byte(","))
}
}
buff.Write([]byte("]"))
return buff.String(), nil
}
// CreateProgressbar generates a progress bar

View File

@ -1,211 +0,0 @@
package jsontable
import (
"fmt"
"io"
"strings"
"github.com/olekukonko/tablewriter"
)
// A quick-and-dirty proxy/emulator of tablewriter to enable more easy machine readable output
// - Does not strictly support types, just quoted or unquoted values
// - Does not support nested values.
// If a datalabel is set with SetDataLabel(true, "..."), that will be used as the key for teh data of the table,
// otherwise if the caption is set with SetCaption(true, "..."), the data label will be set to the default of
// "rows", otherwise the table will output as a JSON list.
//
// Proxys all actions through to the tablewriter except addrow and addbatch, which it does at render time
//
type JSONTable struct {
out io.Writer
colsize int
rows [][]string
keys []string
quoted []bool // hack to do output typing, quoted vs. unquoted
hasDataLabel bool
dataLabel string
hasCaption bool
caption string // the actual caption
hasCaptionLabel bool
captionLabel string // the key in the dictionary for the caption
tbl *tablewriter.Table
}
func writeChar(w io.Writer, c byte) {
w.Write([]byte{c})
}
func NewJSONTable(writer io.Writer) *JSONTable {
t := &JSONTable{
out: writer,
colsize: 0,
rows: [][]string{},
keys: []string{},
quoted: []bool{},
hasDataLabel: false,
dataLabel: "rows",
hasCaption: false,
caption: "",
hasCaptionLabel: false,
captionLabel: "caption",
tbl: tablewriter.NewWriter(writer),
}
return t
}
func (t *JSONTable) NumLines() int {
// JSON only but reflects a shared state.
return len(t.rows)
}
func (t *JSONTable) SetHeader(keys []string) {
// Set the keys value which will assign each column to the keys.
// Note that we'll ignore values that are beyond the length of the keys list
t.colsize = len(keys)
t.keys = []string{}
for _, k := range keys {
t.keys = append(t.keys, k)
t.quoted = append(t.quoted, true)
}
t.tbl.SetHeader(keys)
}
func (t *JSONTable) SetColumnQuoting(quoting []bool) {
// Specify which columns are quoted or unquoted in output
// JSON only
for i := 0; i < t.colsize; i++ {
t.quoted[i] = quoting[i]
}
}
func (t *JSONTable) Append(row []string) {
// We'll just append whatever to the rows list. If they fix the keys after appending rows, it'll work as
// expected.
// We should detect if the row is narrower than the key list tho.
// JSON only (but we use the rows later when rendering a regular table)
t.rows = append(t.rows, row)
}
func (t *JSONTable) Render() {
// Load the table with rows and render.
// Proxy only
for _, row := range t.rows {
t.tbl.Append(row)
}
t.tbl.Render()
}
func (t *JSONTable) _JSONRenderInner() {
// JSON only
// Render the list of dictionaries to the writer.
//// inner render loop
writeChar(t.out, '[')
for rowidx, row := range t.rows {
if rowidx != 0 {
writeChar(t.out, ',')
}
writeChar(t.out, '{')
for keyidx, key := range t.keys {
key := strings.ToLower(key)
key = strings.ReplaceAll(key, " ", "-")
value := "nil"
if keyidx < len(row) {
value = row[keyidx]
}
if keyidx != 0 {
writeChar(t.out, ',')
}
if t.quoted[keyidx] {
fmt.Fprintf(t.out, "\"%s\":\"%s\"", key, value)
} else {
fmt.Fprintf(t.out, "\"%s\":%s", key, value)
}
}
writeChar(t.out, '}')
}
writeChar(t.out, ']')
}
func (t *JSONTable) JSONRender() {
// write JSON table to output
// JSON only
if t.hasDataLabel || t.hasCaption {
// dict mode
writeChar(t.out, '{')
if t.hasCaption {
fmt.Fprintf(t.out, "\"%s\":\"%s\",", t.captionLabel, t.caption)
}
fmt.Fprintf(t.out, "\"%s\":", t.dataLabel)
}
// write list
t._JSONRenderInner()
if t.hasDataLabel || t.hasCaption {
// dict mode
writeChar(t.out, '}')
}
}
func (t *JSONTable) SetCaption(caption bool, captionText ...string) {
t.hasCaption = caption
if len(captionText) == 1 {
t.caption = captionText[0]
}
t.tbl.SetCaption(caption, captionText...)
}
func (t *JSONTable) SetCaptionLabel(captionLabel bool, captionLabelText ...string) {
// JSON only
t.hasCaptionLabel = captionLabel
if len(captionLabelText) == 1 {
t.captionLabel = captionLabelText[0]
}
}
func (t *JSONTable) SetDataLabel(dataLabel bool, dataLabelText ...string) {
// JSON only
t.hasDataLabel = dataLabel
if len(dataLabelText) == 1 {
t.dataLabel = dataLabelText[0]
}
}
func (t *JSONTable) AppendBulk(rows [][]string) {
// JSON only but reflects shared state
for _, row := range rows {
t.Append(row)
}
}
// Stuff we should implement but we just proxy for now.
func (t *JSONTable) SetAutoMergeCellsByColumnIndex(cols []int) {
// FIXME
t.tbl.SetAutoMergeCellsByColumnIndex(cols)
}
// Stuff we should implement but we just proxy for now.
func (t *JSONTable) SetAlignment(align int) {
// FIXME
t.tbl.SetAlignment(align)
}
func (t *JSONTable) SetAutoMergeCells(auto bool) {
// FIXME
t.tbl.SetAutoMergeCells(auto)
}
// Stub functions
func (t *JSONTable) SetAutoWrapText(auto bool) {
t.tbl.SetAutoWrapText(auto)
return
}

View File

@ -1,83 +0,0 @@
package jsontable
import (
"testing"
"bytes"
"encoding/json"
"github.com/olekukonko/tablewriter"
)
var TestLine = []string{"1", "2"}
var TestGroup = [][]string{{"1", "2", "3"}, {"a", "teohunteohu", "c", "d"}, {"☺", "☹"}}
var TestKeys = []string{"key0", "key1", "key2"}
// test creation
func TestNewTable(t *testing.T) {
var b bytes.Buffer
tbl := NewJSONTable(&b)
if tbl.NumLines() != 0 {
t.Fatalf("Something went weird when making table (should have 0 lines)")
}
}
// test adding things
func TestTableAdd(t *testing.T) {
var b bytes.Buffer
tbl := NewJSONTable(&b)
tbl.Append(TestLine)
if tbl.NumLines() != 1 {
t.Fatalf("Appending a line does not result in a length of 1.")
}
tbl.AppendBulk(TestGroup)
numlines := tbl.NumLines()
if numlines != (len(TestGroup) + 1) {
t.Fatalf("Appending two lines does not result in a length of 4 (length is %d).", numlines)
}
}
// test JSON output is parsable
func TestJsonParsable(t *testing.T) {
var b bytes.Buffer
tbl := NewJSONTable(&b)
tbl.AppendBulk(TestGroup)
tbl.SetHeader(TestKeys)
tbl.JSONRender()
var son []map[string]interface{}
err := json.Unmarshal(b.Bytes(), &son)
if err != nil {
t.Fatalf("Did not produce parsable JSON: %s", err.Error())
}
}
// test identical commands to a tablewriter and jsontable produce the same rendered output
func TestTableWriter(t *testing.T) {
var bjson bytes.Buffer
var btable bytes.Buffer
tbl := NewJSONTable(&bjson)
tbl.AppendBulk(TestGroup)
tbl.SetHeader(TestKeys)
tbl.Render()
wtbl := tablewriter.NewWriter(&btable)
wtbl.AppendBulk(TestGroup)
wtbl.SetHeader(TestKeys)
wtbl.Render()
if bytes.Compare(bjson.Bytes(), btable.Bytes()) != 0 {
t.Fatalf("JSON table and TableWriter produce non-identical outputs.\n%s\n%s", bjson.Bytes(), btable.Bytes())
}
}
/// FIXME test different output formats when captions etc. are added

View File

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

View File

@ -26,20 +26,26 @@ func (r Recipe) Ensure(chaos bool, offline bool) error {
if err := r.EnsureIsClean(); err != nil {
return err
}
if !offline {
if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err)
}
}
if r.Version != "" {
log.Debugf("ensuring version %s", r.Version)
if _, err := r.EnsureVersion(r.Version); err != nil {
return err
}
} else {
if err := r.EnsureLatest(); err != nil {
return err
}
return nil
}
if err := r.EnsureLatest(); err != nil {
return err
}
return nil
}

View File

@ -127,6 +127,9 @@ func Get(name string) Recipe {
version := ""
if strings.Contains(name, ":") {
split := strings.Split(name, ":")
if len(split) > 2 {
log.Fatalf("version seems invalid: %s", name)
}
name = split[0]
version = split[1]
}

View File

@ -181,7 +181,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists, moving on...", secret.RemoteName)
log.Warnf("%s already exists", secret.RemoteName)
ch <- nil
} else {
ch <- err
@ -201,7 +201,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") {
log.Warnf("%s already exists, moving on...", secret.RemoteName)
log.Warnf("%s already exists", secret.RemoteName)
ch <- nil
} else {
ch <- err

View File

@ -175,6 +175,7 @@ func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace conve
pruneServices = append(pruneServices, service)
}
}
removeServices(ctx, cl, pruneServices)
}
@ -255,10 +256,12 @@ func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, co
log.Infof("waiting for %s to deploy... please hold 🤚", appName)
if err := waitOnServices(ctx, cl, serviceIDs, appName); err == nil {
log.Infof("successfully deployed %s", appName)
if err := waitOnServices(ctx, cl, serviceIDs, appName); err != nil {
return err
}
log.Infof("successfully deployed %s", appName)
return nil
}
@ -395,7 +398,7 @@ func deployServices(
)
if service, exists := existingServiceMap[name]; exists {
log.Infof("updating service %s (id: %s)", name, service.ID)
log.Infof("updating %s", name)
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
@ -430,7 +433,7 @@ func deployServices(
response, err := cl.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
if err != nil {
return nil, errors.Wrapf(err, "failed to update service %s", name)
return nil, errors.Wrapf(err, "failed to update %s", name)
}
for _, warning := range response.Warnings {
@ -439,7 +442,7 @@ func deployServices(
serviceIDs = append(serviceIDs, service.ID)
} else {
log.Infof("creating service %s", name)
log.Infof("creating %s", name)
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
@ -450,7 +453,7 @@ func deployServices(
serviceCreateResponse, err := cl.ServiceCreate(ctx, serviceSpec, createOpts)
if err != nil {
return nil, errors.Wrapf(err, "failed to create service %s", name)
return nil, errors.Wrapf(err, "failed to create %s", name)
}
serviceIDs = append(serviceIDs, serviceCreateResponse.ID)
@ -510,13 +513,14 @@ func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appN
case err := <-errChan:
return err
case <-sigintChannel:
return fmt.Errorf(fmt.Sprintf(`
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))
abra app undeploy %s`, appName, appName)
case <-time.After(timeout):
return fmt.Errorf(fmt.Sprintf(`
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
@ -530,7 +534,7 @@ You can track latest deployment status with:
And inspect the logs with:
abra app logs %s
`, appName, timeout, appName, appName))
`, appName, timeout, appName, appName)
}
}
@ -548,7 +552,7 @@ func GetStacks(cl *dockerClient.Client) ([]*formatter.Stack, error) {
labels := service.Spec.Labels
name, ok := labels[convert.LabelNamespace]
if !ok {
return nil, errors.Errorf("cannot get label %s for service %s",
return nil, errors.Errorf("cannot get label %s for %s",
convert.LabelNamespace, service.ID)
}
ztack, ok := m[name]

View File

@ -87,7 +87,7 @@ function install_abra_release {
x=$(echo $PATH | grep $HOME/.local/bin)
if [ $? -ne 0 ]; then
echo "$(tput setaf 3)WARNING: $HOME/.local/bin/ is not in \$PATH! If you want to run abra by just typing "abra" you should add it to your \$PATH! To do that run:$(tput sgr0)"
echo "$(tput setaf 3)WARNING: $HOME/.local/bin/ is not in \$PATH! If you want to run abra by just typing "abra" you should add it to your \$PATH! To do that run this once and restart your terminal:$(tput sgr0)"
p=$HOME/.local/bin
com="echo PATH=\$PATH:$p"
if [[ $SHELL =~ "bash" ]]; then

View File

@ -7,10 +7,9 @@
# destroys resources on the swarm server you run it against. This is for
# setup/teardown for the integration test suite.
#
# export DRONE_SOURCE_BRANCH=<your-branch-name>
# ./run-ci-int
set +e
set -eu
echo "========================================================================"
echo "WIPING DOCKER RESOURCES FOR A CLEAN SLATE"
@ -45,17 +44,7 @@ echo "========================================================================"
rm -rf abra
git clone ssh://git@git.coopcloud.tech:2222/coop-cloud/abra.git
cd abra
echo "========================================================================"
echo "========================================================================"
echo "FETCHING ABRA BRANCH FOR TESTING"
echo "========================================================================"
if [ -z "$DRONE_SOURCE_BRANCH" ]; then
DRONE_SOURCE_BRANCH="main"
fi
git fetch --all
git checkout $DRONE_SOURCE_BRANCH
git checkout main
echo "========================================================================"
echo "========================================================================"
@ -83,6 +72,7 @@ echo "========================================================================"
export ABRA_DIR="$HOME/.abra_test"
export TERM=xterm
export TEST_SERVER=default
export ABRA_CI=1
rm -rf "$ABRA_DIR"
bats -Tp tests/integration --filter-tags \!dns --print-output-on-failure

View File

@ -20,6 +20,7 @@ setup(){
teardown(){
_reset_recipe
_reset_tags
}
@test "validate app argument" {
@ -82,19 +83,17 @@ teardown(){
}
@test "ensure recipe not up to date if --offline" {
wantHash=$(_get_n_hash 1)
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~1
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
assert_success
assert_equal $(_get_current_hash) "$wantHash"
# NOTE(d1): we can't quite tell if this will fail or not in the future, so,
# since it isn't an important part of what we're testing here, we don't check
# it
# NOTE(d1): don't assert success because it might flake
run $ABRA app check "$TEST_APP_DOMAIN" --offline
assert_equal $(_get_current_hash) "$wantHash"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
}
@test "error if missing .env.sample" {
@ -118,3 +117,20 @@ teardown(){
assert_success
assert_output --partial '❌'
}
# bats test_tags=slow
@test "respects env version" {
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
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app check "$TEST_APP_DOMAIN"
assert_success
assert_equal $(_get_current_hash) "$tagHash"
}

View File

@ -19,8 +19,9 @@ setup(){
}
teardown(){
_undeploy_app
_reset_recipe
_reset_tags
_undeploy_app
}
# bats test_tags=slow
@ -105,20 +106,18 @@ test_cmd_export"
}
@test "ensure recipe not up to date if --offline" {
wantHash=$(_get_n_hash 3)
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
assert_success
assert_equal $(_get_current_hash) "$wantHash"
run $ABRA app cmd --local --offline "$TEST_APP_DOMAIN" test_cmd
assert_success
assert_output --partial 'baz'
assert_equal $(_get_current_hash) $wantHash
_reset_recipe "$TEST_RECIPE"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
}
@test "error if missing arguments without passing --local" {
@ -187,6 +186,24 @@ test_cmd_export"
assert_output --partial 'baz'
}
# bats test_tags=slow
@test "respects env version" {
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
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app cmd "$TEST_APP_DOMAIN" app test_cmd
assert_success
assert_output --partial 'baz'
assert_equal $(_get_current_hash) "$tagHash"
}
# bats test_tags=slow
@test "error if missing service" {
_deploy_app

View File

@ -16,12 +16,13 @@ teardown_file(){
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_undeploy_app
_reset_recipe
_reset_app
_undeploy_app
_reset_tags
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
@ -86,19 +87,18 @@ teardown(){
# bats test_tags=slow
@test "ensure recipe not up to date if --offline" {
wantHash=$(_get_n_hash 3)
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
assert_success
assert_equal $(_get_current_hash) "$wantHash"
# NOTE(d1): need to use --chaos to force same commit
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos --offline
--no-input --no-converge-checks --offline
assert_success
assert_equal $(_get_current_hash) "$wantHash"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
}
# bats test_tags=slow
@ -106,6 +106,7 @@ teardown(){
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
_remove_tags
_wipe_env_version
# NOTE(d1): need to pass --offline to stop tags being pulled again
run $ABRA app deploy "$TEST_APP_DOMAIN" \
@ -125,6 +126,8 @@ teardown(){
assert_equal $(_get_current_hash) "$wantHash"
_wipe_env_version
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
@ -171,14 +174,12 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force
assert_success
assert_output --partial 'already deployed but continuing'
assert_output --partial '--force'
assert_output --partial 'already deployed'
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos
assert_success
assert_output --partial 'already deployed but continuing'
assert_output --partial '--chaos'
assert_output --partial 'already deployed'
}
# bats test_tags=slow
@ -351,39 +352,6 @@ teardown(){
_undeploy_app
# TODO(d1): use of `--chaos` is a hack while the following is not fixed
# https://git.coopcloud.tech/coop-cloud/organising/issues/620
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all --chaos
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all
assert_success
}
# bats test_tags=slow
@test "deploy chaos commit" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run $ABRA app deploy "$TEST_APP_DOMAIN" "$tagHash" --no-input --no-converge-checks
assert_success
assert_output --partial 'chaos mode'
}
# bats test_tags=slow
@test "deploy remote recipe" {
run sed -i 's/TYPE=abra-test-recipe/RECIPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "git.coopcloud.tech/coop-cloud/abra-test-recipe"
}
# bats test_tags=slow
@test "deploy remote recipe with version" {
run sed -i 's/TYPE=abra-test-recipe/RECIPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial '0.2.0+1.21.0'
}

View File

@ -0,0 +1,99 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_reset_recipe
_undeploy_app
_reset_app
}
# bats test_tags=slow
@test "deploy version written to env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "redeploy overwrites env version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --force
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "chaos commit written to env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "1e83340e" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:1e83340e" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
# bats test_tags=slow
@test "redeploy reads from env version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
_undeploy_app
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial '0.1.0+1.20.0'
}
# bats test_tags=slow
@test "specific version overrides env version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --force --debug
assert_success
assert_output --partial "overriding env file version"
run grep -q "TYPE=$TEST_RECIPE:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -0,0 +1,73 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_reset_recipe
_undeploy_app
_reset_app
}
# bats test_tags=slow
@test "deploy remote recipe" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "git.coopcloud.tech/coop-cloud/abra-test-recipe"
}
# bats test_tags=slow
@test "deploy remote recipe with version" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial '0.2.0+1.21.0'
}
# bats test_tags=slow
@test "deploy remote recipe with chaos commit" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:1e83340e/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial '1e83340e'
}
# bats test_tags=slow
@test "remote recipe version written to env" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
run grep -q "TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:$(_latest_release)" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -0,0 +1,44 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
teardown(){
_reset_app
}
@test "badly formatted env version bails out" {
run sed -i 's/TYPE=abra-test-recipe/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure
assert_output --partial 'seems invalid'
}
@test "invalid env version bails out" {
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:DOESNTEXIST/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_failure
assert_output --partial 'not found'
}

View File

@ -62,8 +62,8 @@ teardown(){
run $ABRA app ls --server foo.com
assert_success
refute_output --partial "server: $TEST_SERVER |"
assert_output --partial "server: foo.com |"
refute_output --partial "SERVER: $TEST_SERVER"
assert_output --partial "SERVER: foo.com"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success
@ -97,8 +97,8 @@ teardown(){
@test "server stats are correct" {
run $ABRA app ls
assert_success
assert_output --partial "server: $TEST_SERVER"
assert_output --partial "total apps: 1"
assert_output --partial "SERVER: $TEST_SERVER"
assert_output --partial "TOTAL APPS: 1"
run mkdir -p "$ABRA_DIR/servers/foo.com"
assert_success
@ -113,8 +113,8 @@ teardown(){
assert_success
assert_output --partial "$TEST_SERVER"
assert_output --partial "foo.com"
assert_output --partial "total servers: 2"
assert_output --partial "total apps: 2"
assert_output --partial "TOTAL SERVERS: 2"
assert_output --partial "TOTAL APPS: 2"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success

View File

@ -39,17 +39,41 @@ teardown(){
_get_head_hash
_get_current_hash
assert_equal "$headHash" "$currentHash"
run grep -q "TYPE=$TEST_RECIPE:$(_latest_release)" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "create new app with version" {
run $ABRA app new "$TEST_RECIPE" 0.1.1+1.20.2 \
run $ABRA app new "$TEST_RECIPE" 0.3.0+1.21.0 \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_equal $(_get_tag_hash 0.1.1+1.20.2) $(_get_current_hash)
assert_equal $(_get_tag_hash 0.3.0+1.21.0) $(_get_current_hash)
run grep -q "TYPE=$TEST_RECIPE:0.3.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "create new app with chaos commit" {
run $ABRA app new "$TEST_RECIPE" 1e83340e \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
currentHash=$(_get_current_hash)
assert_equal 1e83340e ${currentHash:0:8}
run grep -q "TYPE=$TEST_RECIPE:1e83340e" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "does not overwrite existing env files" {

View File

@ -41,13 +41,11 @@ teardown(){
@test "show ps report" {
_deploy_app
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA app ps "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'app'
assert_output --partial 'healthy'
assert_output --partial "$latestRelease"
assert_output --partial $(_latest_release)
assert_output --partial 'false' # not a chaos deploy
}

View File

@ -58,18 +58,18 @@ teardown(){
}
@test "ensure recipe not up to date if --offline" {
wantHash=$(_get_n_hash 3)
_ensure_env_version "0.1.0+1.20.0"
latestRelease=$(_latest_release)
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d "$latestRelease"
assert_success
assert_equal $(_get_current_hash) "$wantHash"
run $ABRA app rollback "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --offline
assert_failure
assert_equal $(_get_current_hash) $wantHash
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l
refute_output --partial "$latestRelease"
}
@test "error if not already deployed" {

View File

@ -0,0 +1,44 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_undeploy_app
_reset_recipe
}
@test "rollback writes version to env file" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks
assert_success
assert_output --partial "0.2.0+1.21.0"
run grep -q "TYPE=abra-test-recipe:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks --debug
assert_success
assert_output --partial "0.1.0+1.20.0"
assert_output --partial "overriding env file version"
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -217,7 +217,8 @@ teardown(){
run bash -c "echo bar >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app secret insert \
--file "$TEST_APP_DOMAIN" test_pass_one v1 "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
--chaos \
--file "$TEST_APP_DOMAIN" test_pass_one v1 "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_success
assert_output --partial 'successfully stored on server'
@ -317,9 +318,10 @@ teardown(){
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_success
run $ABRA app secret ls "$TEST_APP_DOMAIN" --machine
run bash -c '$ABRA app secret ls "$TEST_APP_DOMAIN" --machine \
| jq -r ".[] | select(.name==\"test_pass_two\") | .version"'
assert_success
assert_output --partial '"created-on-server":"true"'
assert_output --partial 'v1'
}
@test "ls: bail if unstaged changes and no --chaos" {

View File

@ -0,0 +1,93 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
# NOTE(d1): create new app without secrets
run $ABRA app new "$TEST_RECIPE" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
}
teardown_file(){
_rm_app
_rm_server
_reset_recipe
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_reset_recipe
_reset_app
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all --no-input
}
@test "generate: respect env version" {
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_success
assert_equal $(_get_current_hash) "$tagHash"
}
@test "insert: respect env version" {
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app secret insert "$TEST_APP_DOMAIN" test_pass_one v1 foo
assert_success
assert_output --partial 'successfully stored on server'
assert_equal $(_get_current_hash) "$tagHash"
}
@test "rm: respect env version" {
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_success
run $ABRA app secret rm "$TEST_APP_DOMAIN" --all
assert_success
assert_equal $(_get_current_hash) "$tagHash"
}
@test "ls: respect env version" {
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:0.2.0+1.21.0/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app secret generate "$TEST_APP_DOMAIN" --all
assert_success
run $ABRA app secret ls "$TEST_APP_DOMAIN"
assert_success
assert_output --partial 'true'
assert_equal $(_get_current_hash) "$tagHash"
}

View File

@ -18,8 +18,9 @@ setup(){
}
teardown(){
_undeploy_app
_reset_recipe
_reset_app
_undeploy_app
}
@test "validate app argument" {
@ -123,11 +124,9 @@ teardown(){
assert_success
assert_output --partial '0.1.0+1.20.0'
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "$latestRelease"
assert_output --partial "$(_latest_release)"
}
# bats test_tags=slow
@ -136,11 +135,9 @@ teardown(){
assert_success
assert_output --partial '0.1.1+1.20.2'
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "$latestRelease"
assert_output --partial "$(_latest_release)"
assert_output --partial 'release notes baz' # 0.2.0+1.21.0
refute_output --partial 'release notes bar' # 0.1.1+1.20.2
}
@ -164,11 +161,9 @@ teardown(){
assert_success
assert_output --partial '0.1.0+1.20.0'
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --partial "$latestRelease"
assert_output --partial "$(_latest_release)"
assert_output --partial 'release notes bar' # 0.1.1+1.20.2
assert_output --partial 'release notes baz' # 0.2.0+1.21.0
}

View File

@ -0,0 +1,43 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
_new_app
}
teardown_file(){
_rm_app
_rm_server
}
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
}
teardown(){
_undeploy_app
_reset_recipe
}
@test "upgrade writes version to env file" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
assert_success
assert_output --partial '0.1.0+1.20.0'
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \
--no-input --no-converge-checks --debug
assert_success
assert_output --partial "0.2.0+1.21.0"
assert_output --partial "overriding env file version"
run grep -q "TYPE=abra-test-recipe:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -1,5 +1,9 @@
#!/usr/bin/env bash
_latest_release(){
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
}
_fetch_recipe() {
if [[ ! -d "$ABRA_DIR/recipes/$TEST_RECIPE" ]]; then
run mkdir -p "$ABRA_DIR/recipes"
@ -19,10 +23,29 @@ _reset_recipe(){
}
_ensure_latest_version(){
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
latestRelease=$(_latest_release)
if [ ! $latestRelease = "$1" ]; then
echo "expected latest recipe version of '$1', saw: $latestRelease"
return 1
fi
}
_ensure_catalogue(){
if [[ ! -d "$ABRA_DIR/catalogue" ]]; then
run git clone https://git.coopcloud.tech/coop-cloud/recipes-catalogue-json.git $ABRA_DIR/catalogue
assert_success
fi
}
_ensure_env_version(){
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe:$1/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
_wipe_env_version(){
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=abra-test-recipe/g' \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -1,10 +1,17 @@
#!/usr/bin/env bash
setup_file(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_ensure_catalogue
}
setup() {
load "$PWD/tests/integration/helpers/common"
_common_setup
}
@test "recipe versions" {
run $ABRA recipe versions gitea
assert_success
@ -12,11 +19,9 @@ setup() {
}
@test "local tags used if no catalogue entry" {
latestRelease=$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | tail -n 1)
run $ABRA recipe versions "$TEST_RECIPE"
assert_success
assert_output --partial "$latestRelease"
assert_output --partial "$(_latest_release)"
}
@test "versions listed in correct order" {

View File

@ -13,6 +13,7 @@ teardown_file(){
setup(){
load "$PWD/tests/integration/helpers/common"
_common_setup
_add_server
}
teardown(){
@ -61,12 +62,13 @@ teardown(){
}
@test "machine readable output" {
run "$ABRA" server ls --machine
if [ ! "$TEST_SERVER" = "default" ]; then
skip "can only diff output against 'default' server (local)"
fi
output=$("$ABRA" server ls --machine)
run diff \
<(jq -S "." <(echo "$output")) \
<(jq -S "." <(echo '[{"host":"local","name":"default"}]'))
assert_success
expectedOutput='[{"name":"'
expectedOutput+="$TEST_SERVER"
expectedOutput+='"'
assert_output --partial "$expectedOutput"
}