0
0
forked from toolshed/abra

Merge remote-tracking branch 'upstream/main' into deploy-relax-force

This commit is contained in:
p4u1 2024-12-30 16:43:47 +01:00
commit 5a4dac7e76
40 changed files with 607 additions and 345 deletions

View File

@ -46,7 +46,10 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
} }
table. table.
Headers("RECIPE ENV SAMPLE", "APP ENV"). Headers(
fmt.Sprintf("%s .env.sample", app.Recipe.Name),
fmt.Sprintf("%s.env", app.Name),
).
StyleFunc(func(row, col int) lipgloss.Style { StyleFunc(func(row, col int) lipgloss.Style {
switch { switch {
case col == 1: case col == 1:
@ -71,7 +74,9 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
} }
} }
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
}, },
} }

View File

@ -54,8 +54,8 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.RecipeVersionComplete(app.Recipe.Name) return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default: default:
@ -112,19 +112,19 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
// is because we need to deal with GetComposeFiles under the hood and these // is because we need to deal with GetComposeFiles under the hood and these
// files change from version to version which therefore affects which // files change from version to version which therefore affects which
// secrets might be generated // secrets might be generated
version := deployMeta.Version toDeployVersion := deployMeta.Version
if specificVersion != "" { if specificVersion != "" {
version = specificVersion toDeployVersion = specificVersion
log.Debugf("choosing %s as version to deploy", version) log.Debugf("choosing %s as version to deploy", toDeployVersion)
var err error var err error
isChaosCommit, err = app.Recipe.EnsureVersion(version) isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if isChaosCommit { if isChaosCommit {
log.Debugf("assuming '%s' is a chaos commit", version) log.Debugf("assuming '%s' is a chaos commit", toDeployVersion)
internal.Chaos = true internal.Chaos = true
} }
} }
@ -147,9 +147,9 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
} }
if len(versions) > 0 && !internal.Chaos { if len(versions) > 0 && !internal.Chaos {
version = versions[len(versions)-1] toDeployVersion = versions[len(versions)-1]
log.Debugf("choosing %s as version to deploy", version) log.Debugf("choosing %s as version to deploy", toDeployVersion)
if _, err := app.Recipe.EnsureVersion(version); err != nil { if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
@ -157,25 +157,22 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
version = formatter.SmallSHA(head.String()) toDeployVersion = formatter.SmallSHA(head.String())
warnMessages = append(warnMessages, fmt.Sprintf("no versions detected, using latest commit"))
} }
} }
chaosVersion := config.CHAOS_DEFAULT toDeployChaosVersion := config.CHAOS_DEFAULT
if internal.Chaos { if internal.Chaos {
warnMessages = append(warnMessages, "chaos mode engaged")
if isChaosCommit { if isChaosCommit {
chaosVersion = specificVersion toDeployChaosVersion = specificVersion
versionLabelLocal, err := app.Recipe.GetVersionLabelLocal() versionLabelLocal, err := app.Recipe.GetVersionLabelLocal()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
version = versionLabelLocal toDeployVersion = versionLabelLocal
} else { } else {
var err error var err error
chaosVersion, err = app.Recipe.ChaosVersion() toDeployChaosVersion, err = app.Recipe.ChaosVersion()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -189,11 +186,11 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
log.Fatal(err) log.Fatal(err)
} }
if appStatus.Chaos && !internal.Chaos { if appStatus.Chaos && !internal.Chaos {
log.Fatalf("%s is deployed from a chaos version. Are you sure the local changes are in the current version (%s)?", app.Name, version) log.Fatalf("%s is deployed from a chaos version. Are you sure the local changes are in the current version (%s)?", app.Name, toDeployVersion)
} }
if version != "" && appStatus.Version != "" { if toDeployVersion != "" && appStatus.Version != "" {
localVersion, err := tagcmp.Parse(version) localVersion, err := tagcmp.Parse(toDeployVersion)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -203,7 +200,7 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
} }
if localVersion.IsLessThan(remoteVersion) { if localVersion.IsLessThan(remoteVersion) {
log.Fatalf("%s is deployed at %s. Are you sure you want to downgrade to %s?", app.Name, appStatus.Version, version) log.Fatalf("%s is deployed at %s. Are you sure you want to downgrade to %s?", app.Name, appStatus.Version, toDeployVersion)
} }
} }
} }
@ -237,7 +234,7 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
appPkg.ExposeAllEnv(stackName, compose, app.Env) appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, chaosVersion) appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion)
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)
envVars, err := appPkg.CheckEnv(app) envVars, err := appPkg.CheckEnv(app)
@ -260,13 +257,24 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
warnMessages = append(warnMessages, "skipping domain checks as no DOMAIN=... configured for app") log.Debug("skipping domain checks as no DOMAIN=... configured for app")
} }
} else { } else {
warnMessages = append(warnMessages, "skipping domain checks as requested") log.Debug("skipping domain checks as requested")
} }
if err := internal.DeployOverview(app, warnMessages, version, chaosVersion); err != nil { deployedVersion := config.NO_VERSION_DEFAULT
if deployMeta.IsDeployed {
deployedVersion = deployMeta.Version
}
if err := internal.DeployOverview(
app,
warnMessages,
deployedVersion,
deployMeta.ChaosVersion,
toDeployVersion,
toDeployChaosVersion); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -288,9 +296,9 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`,
} }
} }
app.Recipe.Version = version app.Recipe.Version = toDeployVersion
if chaosVersion != config.CHAOS_DEFAULT { if toDeployChaosVersion != config.CHAOS_DEFAULT {
app.Recipe.Version = chaosVersion app.Recipe.Version = toDeployChaosVersion
} }
log.Debugf("choosing %s as version to save to env file", app.Recipe.Version) log.Debugf("choosing %s as version to save to env file", app.Recipe.Version)
if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil {

View File

@ -173,7 +173,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
stats.LatestCount++ stats.LatestCount++
} }
} else { } else {
newUpdates = internal.ReverseStringList(newUpdates) newUpdates = internal.SortVersionsDesc(newUpdates)
appStats.Upgrade = strings.Join(newUpdates, "\n") appStats.Upgrade = strings.Join(newUpdates, "\n")
stats.UpgradeCount++ stats.UpgradeCount++
} }
@ -208,7 +208,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
serverStat := allStats[app.Server] serverStat := allStats[app.Server]
headers := []string{"RECIPE", "DOMAIN"} headers := []string{"RECIPE", "DOMAIN", "SERVER"}
if status { if status {
headers = append(headers, []string{ headers = append(headers, []string{
"STATUS", "STATUS",
@ -228,7 +228,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
var rows [][]string var rows [][]string
for _, appStat := range serverStat.Apps { for _, appStat := range serverStat.Apps {
row := []string{appStat.Recipe, appStat.Domain} row := []string{appStat.Recipe, appStat.Domain, appStat.Server}
if status { if status {
chaosStatus := appStat.Chaos chaosStatus := appStat.Chaos
if chaosStatus != "unknown" { if chaosStatus != "unknown" {
@ -256,20 +256,8 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
table.Rows(rows...) table.Rows(rows...)
if len(rows) > 0 { if len(rows) > 0 {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
if status {
fmt.Println(fmt.Sprintf(
"SERVER: %s | TOTAL APPS: %v | VERSIONED: %v | UNVERSIONED: %v | LATEST : %v | UPGRADE: %v",
app.Server,
serverStat.AppCount,
serverStat.VersionCount,
serverStat.UnversionedCount,
serverStat.LatestCount,
serverStat.UpgradeCount,
))
} else {
log.Infof("SERVER: %s TOTAL APPS: %v", app.Server, serverStat.AppCount)
} }
if len(allStats) > 1 && len(rows) > 0 { if len(allStats) > 1 && len(rows) > 0 {
@ -279,12 +267,6 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
alreadySeen[app.Server] = true alreadySeen[app.Server] = true
} }
if len(allStats) > 1 {
totalServers := formatter.BoldStyle.Render("TOTAL SERVERS")
totalApps := formatter.BoldStyle.Render("TOTAL APPS")
log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount)
}
}, },
} }

View File

@ -2,6 +2,7 @@ package app
import ( import (
"context" "context"
"fmt"
"io" "io"
"os" "os"
"slices" "slices"
@ -37,8 +38,8 @@ var AppLogsCommand = &cobra.Command{
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.ServiceNameComplete(app.Name) return autocomplete.ServiceNameComplete(app.Name)
default: default:

View File

@ -64,56 +64,56 @@ var AppNewCommand = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
recipe := internal.ValidateRecipe(args, cmd.Name()) recipe := internal.ValidateRecipe(args, cmd.Name())
if len(args) == 2 && internal.Chaos {
log.Fatal("cannot use [version] and --chaos together")
}
var recipeVersion string var recipeVersion string
if !internal.Chaos { if len(args) == 2 {
if err := recipe.EnsureIsClean(); err != nil { recipeVersion = args[1]
log.Fatal(err) }
}
chaosVersion := config.CHAOS_DEFAULT
if internal.Chaos {
recipeVersion = chaosVersion
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(); err != nil { if err := recipe.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
}
if len(args) == 2 { if !internal.Chaos {
recipeVersion = args[1] if err := recipe.EnsureIsClean(); err != nil {
} log.Fatal(err)
if recipeVersion == "" {
recipeVersions, err := recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
recipeVersion = tag
}
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
} else {
if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
} else {
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
} }
} }
if internal.Chaos && recipeVersion == "" { var recipeVersions recipePkg.RecipeVersions
if recipeVersion == "" {
var err error var err error
recipeVersion, err = recipe.ChaosVersion() recipeVersions, err = recipe.GetRecipeVersions()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if len(recipeVersions) > 0 {
latest := recipeVersions[len(recipeVersions)-1]
for tag := range latest {
recipeVersion = tag
}
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
log.Fatal(err)
}
} else {
if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err)
}
}
if err := ensureServerFlag(); err != nil { if err := ensureServerFlag(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -187,37 +187,20 @@ var AppNewCommand = &cobra.Command{
newAppServer = "local" newAppServer = "local"
} }
table, err := formatter.CreateTable() log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion)
if err != nil {
log.Fatal(err)
}
headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"}
table.Headers(headers...)
table.Row(newAppServer, appDomain, recipe.Name, recipeVersion)
log.Infof("new app '%s' created 🌞", recipe.Name)
fmt.Println("")
fmt.Println(table)
fmt.Println("")
fmt.Println("Configure this app:")
fmt.Println(fmt.Sprintf("\n abra app config %s", appDomain))
fmt.Println("")
fmt.Println("Deploy this app:")
fmt.Println(fmt.Sprintf("\n abra app deploy %s", appDomain))
if len(appSecrets) > 0 { if len(appSecrets) > 0 {
fmt.Println("") rows := [][]string{}
fmt.Println("Generated secrets:") for k, v := range appSecrets {
fmt.Println("") rows = append(rows, []string{k, v})
fmt.Println(secretsTable) }
overview := formatter.CreateOverview("SECRETS OVERVIEW", rows)
fmt.Println(overview)
log.Warnf( log.Warnf(
"generated secrets %s shown again, please take note of them %s", "secrets are %s shown again, please save them %s",
formatter.BoldStyle.Render("NOT"), formatter.BoldStyle.Render("NOT"),
formatter.BoldStyle.Render("NOW"), formatter.BoldStyle.Render("NOW"),
) )

View File

@ -128,24 +128,35 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
allContainerStats[containerStats["service"]] = containerStats allContainerStats[containerStats["service"]] = containerStats
// NOTE(d1): don't clobber these variables for --machine output
dVersion := deployedVersion
cVersion := chaosVersion
if containerStats["service"] != "app" {
// NOTE(d1): don't repeat info which only relevant for the "app" service
dVersion = ""
cVersion = ""
}
row := []string{ row := []string{
containerStats["service"], containerStats["service"],
containerStats["image"], containerStats["image"],
containerStats["created"], dVersion,
cVersion,
containerStats["status"], containerStats["status"],
containerStats["state"],
containerStats["ports"],
} }
rows = append(rows, row) rows = append(rows, row)
} }
if internal.MachineReadable { if internal.MachineReadable {
jsonstring, err := json.Marshal(allContainerStats) rendered, err := json.Marshal(allContainerStats)
if err != nil { if err != nil {
log.Fatal("unable to convert to JSON: %s", err) log.Fatal("unable to convert to JSON: %s", err)
} }
fmt.Println(string(jsonstring))
fmt.Println(string(rendered))
return return
} }
@ -157,19 +168,18 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
headers := []string{ headers := []string{
"SERVICE", "SERVICE",
"IMAGE", "IMAGE",
"CREATED", "VERSION",
"CHAOS",
"STATUS", "STATUS",
"STATE",
"PORTS",
} }
table. table.
Headers(headers...). Headers(headers...).
Rows(rows...) Rows(rows...)
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
log.Infof("VERSION: %s CHAOS: %s", deployedVersion, chaosVersion) }
} }
func init() { func init() {

View File

@ -46,8 +46,8 @@ beforehand.`,
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.RecipeVersionComplete(app.Recipe.Name) return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default: default:
@ -167,7 +167,7 @@ beforehand.`,
prompt := &survey.Select{ prompt := &survey.Select{
Message: msg, Message: msg,
Options: internal.ReverseStringList(availableDowngrades), Options: internal.SortVersionsDesc(availableDowngrades),
} }
if err := survey.AskOne(prompt, &chosenDowngrade); err != nil { if err := survey.AskOne(prompt, &chosenDowngrade); err != nil {

View File

@ -34,8 +34,8 @@ var AppSecretGenerateCommand = &cobra.Command{
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.SecretComplete(app.Recipe.Name) return autocomplete.SecretComplete(app.Recipe.Name)
default: default:
@ -127,7 +127,9 @@ var AppSecretGenerateCommand = &cobra.Command{
return return
} }
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
log.Warnf( log.Warnf(
"generated secrets %s shown again, please take note of them %s", "generated secrets %s shown again, please take note of them %s",
@ -157,8 +159,8 @@ environment. Typically, you can let Abra generate them for you on app creation
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.SecretComplete(app.Recipe.Name) return autocomplete.SecretComplete(app.Recipe.Name)
default: default:
@ -243,8 +245,8 @@ var AppSecretRmCommand = &cobra.Command{
if !rmAllSecrets { if !rmAllSecrets {
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.SecretComplete(app.Recipe.Name) return autocomplete.SecretComplete(app.Recipe.Name)
} }
@ -394,7 +396,10 @@ var AppSecretLsCommand = &cobra.Command{
return return
} }
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
return return
} }

View File

@ -63,7 +63,7 @@ var AppServicesCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)", "IMAGE"} headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)"}
table.Headers(headers...) table.Headers(headers...)
var rows [][]string var rows [][]string
@ -80,7 +80,6 @@ var AppServicesCommand = &cobra.Command{
row := []string{ row := []string{
serviceShortName, serviceShortName,
serviceLongName, serviceLongName,
formatter.RemoveSha(container.Image),
} }
rows = append(rows, row) rows = append(rows, row)
@ -89,7 +88,9 @@ var AppServicesCommand = &cobra.Command{
table.Rows(rows...) table.Rows(rows...)
if len(rows) > 0 { if len(rows) > 0 {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
} }
}, },
} }

View File

@ -21,7 +21,7 @@ var AppUndeployCommand = &cobra.Command{
Use: "undeploy <app> [flags]", Use: "undeploy <app> [flags]",
Aliases: []string{"un"}, Aliases: []string{"un"},
Short: "Undeploy an app", Short: "Undeploy an app",
Long: `This does not destroy any of the application data. Long: `This does not destroy any application data.
However, you should remain vigilant, as your swarm installation will consider However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed. any previously attached volumes as eligible for pruning once undeployed.
@ -59,7 +59,10 @@ Passing "--prune/-p" does not remove those volumes.`,
chaosVersion = deployMeta.ChaosVersion chaosVersion = deployMeta.ChaosVersion
} }
if err := internal.DeployOverview(app, []string{}, deployMeta.Version, chaosVersion); err != nil { if err := internal.UndeployOverview(
app,
deployMeta.Version,
chaosVersion); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -76,6 +79,11 @@ Passing "--prune/-p" does not remove those volumes.`,
log.Fatal(err) log.Fatal(err)
} }
} }
log.Debugf("choosing %s as version to save to env file", deployMeta.Version)
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
log.Fatalf("writing undeployed recipe version in env file: %s", err)
}
}, },
} }

View File

@ -40,8 +40,8 @@ beforehand.`,
case 1: case 1:
app, err := appPkg.Get(args[0]) app, err := appPkg.Get(args[0])
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) errMsg := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveDefault return []string{errMsg}, cobra.ShellCompDirectiveError
} }
return autocomplete.RecipeVersionComplete(app.Recipe.Name) return autocomplete.RecipeVersionComplete(app.Recipe.Name)
default: default:
@ -159,7 +159,7 @@ beforehand.`,
prompt := &survey.Select{ prompt := &survey.Select{
Message: msg, Message: msg,
Options: internal.ReverseStringList(availableUpgrades), Options: internal.SortVersionsDesc(availableUpgrades),
} }
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
@ -250,7 +250,6 @@ beforehand.`,
} }
if showReleaseNotes { if showReleaseNotes {
fmt.Println()
fmt.Print(releaseNotes) fmt.Print(releaseNotes)
return return
} }

View File

@ -2,7 +2,6 @@ package app
import ( import (
"context" "context"
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
@ -43,7 +42,7 @@ var AppVolumeListCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
headers := []string{"name", "created", "mounted"} headers := []string{"NAME", "ON SERVER"}
table, err := formatter.CreateTable() table, err := formatter.CreateTable()
if err != nil { if err != nil {
@ -54,14 +53,16 @@ var AppVolumeListCommand = &cobra.Command{
var rows [][]string var rows [][]string
for _, volume := range volumes { for _, volume := range volumes {
row := []string{volume.Name, volume.CreatedAt, volume.Mountpoint} row := []string{volume.Name, volume.Mountpoint}
rows = append(rows, row) rows = append(rows, row)
} }
table.Rows(rows...) table.Rows(rows...)
if len(rows) > 0 { if len(rows) > 0 {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
return return
} }

View File

@ -3,10 +3,14 @@ package internal
import ( import (
"fmt" "fmt"
"os" "os"
"sort"
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
@ -20,7 +24,8 @@ var borderStyle = lipgloss.NewStyle().
var headerStyle = lipgloss.NewStyle(). var headerStyle = lipgloss.NewStyle().
Underline(true). Underline(true).
Bold(true) Bold(true).
PaddingBottom(1)
var leftStyle = lipgloss.NewStyle(). var leftStyle = lipgloss.NewStyle().
Bold(true) Bold(true)
@ -37,13 +42,13 @@ func NewVersionOverview(
app appPkg.App, app appPkg.App,
warnMessages []string, warnMessages []string,
kind, kind,
currentVersion, deployedVersion,
chaosVersion, deployedChaosVersion,
newVersion, toDeployVersion,
releaseNotes string) error { releaseNotes string) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") deployConfig = composeFiles
} }
server := app.Server server := app.Server
@ -51,32 +56,35 @@ func NewVersionOverview(
server = "local" server = "local"
} }
body := strings.Builder{} domain := app.Domain
body.WriteString( if domain == "" {
borderStyle.Render( domain = config.NO_DOMAIN_DEFAULT
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 != "" { rows := [][]string{
fmt.Println() []string{"APP", domain},
[]string{"RECIPE", app.Recipe.Name},
[]string{"SERVER", server},
[]string{"DEPLOYED", deployedVersion},
[]string{"CURRENT CHAOS ", deployedChaosVersion},
[]string{fmt.Sprintf("TO %s", strings.ToUpper(kind)), toDeployVersion},
[]string{"CONFIG", deployConfig},
}
overview := formatter.CreateOverview(
fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind)),
rows,
)
fmt.Println(overview)
if releaseNotes != "" && toDeployVersion != "" {
fmt.Print(releaseNotes) fmt.Print(releaseNotes)
} else { } else {
warnMessages = append(warnMessages, fmt.Sprintf("no release notes available for %s", newVersion)) warnMessages = append(
warnMessages,
fmt.Sprintf("no release notes available for %s", toDeployVersion),
)
} }
for _, msg := range warnMessages { for _, msg := range warnMessages {
@ -101,10 +109,16 @@ func NewVersionOverview(
} }
// DeployOverview shows a deployment overview // DeployOverview shows a deployment overview
func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion string) error { func DeployOverview(
app appPkg.App,
warnMessages []string,
deployedVersion string,
deployedChaosVersion string,
toDeployVersion,
toDeployChaosVersion string) error {
deployConfig := "compose.yml" deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") deployConfig = composeFiles
} }
server := app.Server server := app.Server
@ -112,25 +126,25 @@ func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion
server = "local" server = "local"
} }
body := strings.Builder{} domain := app.Domain
body.WriteString( if domain == "" {
borderStyle.Render( domain = config.NO_DOMAIN_DEFAULT
lipgloss.JoinVertical( }
lipgloss.Center,
headerStyle.Render("DEPLOY OVERVIEW"), rows := [][]string{
lipgloss.JoinVertical( []string{"APP", domain},
lipgloss.Left, []string{"RECIPE", app.Recipe.Name},
horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)), []string{"SERVER", server},
horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)), []string{"DEPLOYED", deployedVersion},
horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)), []string{"CURRENT CHAOS ", deployedChaosVersion},
horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)), []string{"TO DEPLOY", toDeployVersion},
horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(version)), []string{"NEW CHAOS", toDeployChaosVersion},
horizontal(leftStyle.Render("CHAOS"), " ", rightStyle.Padding(0).Render(chaosVersion)), []string{"CONFIG", deployConfig},
), }
),
), overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows)
)
fmt.Println(body.String()) fmt.Println(overview)
for _, msg := range warnMessages { for _, msg := range warnMessages {
log.Warn(msg) log.Warn(msg)
@ -153,6 +167,56 @@ func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion
return nil return nil
} }
// UndeployOverview shows an undeployment overview
func UndeployOverview(
app appPkg.App,
version,
chaosVersion string) error {
deployConfig := "compose.yml"
if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
deployConfig = composeFiles
}
server := app.Server
if app.Server == "default" {
server = "local"
}
domain := app.Domain
if domain == "" {
domain = config.NO_DOMAIN_DEFAULT
}
rows := [][]string{
[]string{"APP", domain},
[]string{"RECIPE", app.Recipe.Name},
[]string{"SERVER", server},
[]string{"DEPLOYED", version},
[]string{"CHAOS", chaosVersion},
[]string{"CONFIG", deployConfig},
}
overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows)
fmt.Println(overview)
if NoInput {
return nil
}
response := false
prompt := &survey.Confirm{Message: "proceed?"}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
log.Fatal("undeploy cancelled")
}
return nil
}
// PostCmds parses a string of commands and executes them inside of the respective services // PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format: // the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... " // "<service> <command> <arguments>|<service> <command> <arguments>|... "
@ -210,3 +274,22 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
} }
return nil return nil
} }
// SortVersionsDesc sorts versions in descending order.
func SortVersionsDesc(versions []string) []string {
var tags []tagcmp.Tag
for _, v := range versions {
parsed, _ := tagcmp.Parse(v) // skips unsupported tags
tags = append(tags, parsed)
}
sort.Sort(tagcmp.ByTagDesc(tags))
var desc []string
for _, t := range tags {
desc = append(desc, t.String())
}
return desc
}

View File

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

View File

@ -10,10 +10,9 @@ import (
) )
var RecipeFetchCommand = &cobra.Command{ var RecipeFetchCommand = &cobra.Command{
Use: "fetch [recipe] [flags]", Use: "fetch [recipe | --all] [flags]",
Aliases: []string{"f"}, Aliases: []string{"f"},
Short: "Fetch recipe(s)", Short: "Clone recipe(s) locally",
Long: "Retrieves all recipes if no [recipe] argument is passed.",
Args: cobra.RangeArgs(0, 1), Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -27,6 +26,14 @@ var RecipeFetchCommand = &cobra.Command{
recipeName = args[0] recipeName = args[0]
} }
if recipeName == "" && !fetchAllRecipes {
log.Fatal("missing [recipe] or --all/-a")
}
if recipeName != "" && fetchAllRecipes {
log.Fatal("cannot use [recipe] and --all/-a together")
}
if recipeName != "" { if recipeName != "" {
r := internal.ValidateRecipe(args, cmd.Name()) r := internal.ValidateRecipe(args, cmd.Name())
if err := r.Ensure(false, false); err != nil { if err := r.Ensure(false, false); err != nil {
@ -50,3 +57,17 @@ var RecipeFetchCommand = &cobra.Command{
} }
}, },
} }
var (
fetchAllRecipes bool
)
func init() {
RecipeFetchCommand.Flags().BoolVarP(
&fetchAllRecipes,
"all",
"a",
false,
"fetch all recipes",
)
}

View File

@ -1,8 +1,6 @@
package recipe package recipe
import ( import (
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
@ -104,7 +102,9 @@ var RecipeLintCommand = &cobra.Command{
} }
if len(rows) > 0 { if len(rows) > 0 {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
for _, warnMsg := range warnMessages { for _, warnMsg := range warnMessages {
log.Warn(warnMsg) log.Warn(warnMsg)

View File

@ -79,8 +79,9 @@ var RecipeListCommand = &cobra.Command{
return return
} }
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Infof("total recipes: %v", len(rows)) log.Fatal(err)
}
} }
}, },
} }

View File

@ -252,7 +252,14 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
// addReleaseNotes checks if the release/next release note exists and moves the // addReleaseNotes checks if the release/next release note exists and moves the
// file to release/<tag>. // file to release/<tag>.
func addReleaseNotes(recipe recipe.Recipe, tag string) error { func addReleaseNotes(recipe recipe.Recipe, tag string) error {
tagReleaseNotePath := path.Join(recipe.Dir, "release", tag) releaseDir := path.Join(recipe.Dir, "release")
if _, err := os.Stat(releaseDir); errors.Is(err, os.ErrNotExist) {
if err := os.Mkdir(releaseDir, 0755); err != nil {
return err
}
}
tagReleaseNotePath := path.Join(releaseDir, tag)
if _, err := os.Stat(tagReleaseNotePath); err == nil { if _, err := os.Stat(tagReleaseNotePath); err == nil {
// Release note for current tag already exist exists. // Release note for current tag already exist exists.
return nil return nil
@ -260,7 +267,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return err return err
} }
nextReleaseNotePath := path.Join(recipe.Dir, "release", "next") nextReleaseNotePath := path.Join(releaseDir, "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil { if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag> // release/next note exists. Move it to release/<tag>
if internal.Dry { if internal.Dry {

View File

@ -55,15 +55,32 @@ var RecipeVersionCommand = &cobra.Command{
log.Fatal(err) log.Fatal(err)
} }
table.Headers("SERVICE", "NAME", "TAG") table.Headers("SERVICE", "IMAGE", "TAG", "VERSION")
for version, meta := range recipeMeta.Versions[i] { for version, meta := range recipeMeta.Versions[i] {
var allRows [][]string var allRows [][]string
var rows [][]string var rows [][]string
for service, serviceMeta := range meta { for service, serviceMeta := range meta {
rows = append(rows, []string{service, serviceMeta.Image, serviceMeta.Tag}) recipeVersion := version
allRows = append(allRows, []string{version, service, serviceMeta.Image, serviceMeta.Tag}) if service != "app" {
recipeVersion = ""
}
rows = append(rows, []string{
service,
serviceMeta.Image,
serviceMeta.Tag,
recipeVersion,
})
allRows = append(allRows, []string{
version,
service,
serviceMeta.Image,
serviceMeta.Tag,
recipeVersion,
})
} }
sort.Slice(rows, sortServiceByName(rows)) sort.Slice(rows, sortServiceByName(rows))
@ -71,9 +88,9 @@ var RecipeVersionCommand = &cobra.Command{
table.Rows(rows...) table.Rows(rows...)
if !internal.MachineReadable { if !internal.MachineReadable {
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Infof("VERSION: %s", version) log.Fatal(err)
fmt.Println() }
continue continue
} }
@ -100,11 +117,7 @@ var RecipeVersionCommand = &cobra.Command{
func sortServiceByName(versions [][]string) func(i, j int) bool { func sortServiceByName(versions [][]string) func(i, j int) bool {
return func(i, j int) bool { return func(i, j int) bool {
// NOTE(d1): corresponds to the `tableCol` definition below return versions[i][0] < versions[j][0]
if versions[i][1] == "app" {
return true
}
return versions[i][1] < versions[j][1]
} }
} }

View File

@ -88,10 +88,6 @@ developer machine. The domain is then set to "default".`,
return return
} }
if _, err := dns.EnsureIPv4(name); err != nil {
log.Warn(err)
}
_, err := createServerDir(name) _, err := createServerDir(name)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -100,21 +96,28 @@ developer machine. The domain is then set to "default".`,
created, err := newContext(name) created, err := newContext(name)
if err != nil { if err != nil {
cleanUp(name) cleanUp(name)
log.Fatal(err) log.Fatalf("unable to create local context: %s", err)
} }
log.Debugf("attempting to create client for %s", name) log.Debugf("attempting to create client for %s", name)
if _, err := client.New(name, timeout); err != nil { if _, err := client.New(name, timeout); err != nil {
cleanUp(name) cleanUp(name)
log.Fatal(sshPkg.Fatal(name, err)) log.Debugf("ssh %s error: %s", name, sshPkg.Fatal(name, err))
log.Fatalf("can't ssh to %s, make sure \"ssh %s\" works", name, name)
} }
if created { if created {
log.Infof("%s successfully added", name) log.Infof("%s successfully added", name)
} else {
log.Warnf("%s already exists", name) if _, err := dns.EnsureIPv4(name); err != nil {
log.Warnf("unable to resolve IPv4 for %s", name)
}
return
} }
log.Warnf("%s already exists", name)
}, },
} }

View File

@ -86,7 +86,9 @@ var ServerListCommand = &cobra.Command{
return return
} }
fmt.Println(table) if err := formatter.PrintTable(table); err != nil {
log.Fatal(err)
}
}, },
} }

View File

@ -634,7 +634,7 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error {
} }
if !skipped { if !skipped {
log.Infof("version %s saved to %s.env", version, a.Domain) log.Debugf("version %s saved to %s.env", version, a.Domain)
} else { } else {
log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain) log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain)
} }

View File

@ -1,21 +1,26 @@
package autocomplete package autocomplete
import ( import (
"fmt"
"sort" "sort"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// AppNameComplete copletes app names. // AppNameComplete copletes app names.
func AppNameComplete() ([]string, cobra.ShellCompDirective) { func AppNameComplete() ([]string, cobra.ShellCompDirective) {
appNames, err := app.GetAppNames() appFiles, err := app.LoadAppFiles("")
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) err := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
}
var appNames []string
for appName := range appFiles {
appNames = append(appNames, appName)
} }
return appNames, cobra.ShellCompDirectiveDefault return appNames, cobra.ShellCompDirectiveDefault
@ -24,8 +29,8 @@ func AppNameComplete() ([]string, cobra.ShellCompDirective) {
func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) { func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
serviceNames, err := app.GetAppServiceNames(appName) serviceNames, err := app.GetAppServiceNames(appName)
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) err := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
return serviceNames, cobra.ShellCompDirectiveDefault return serviceNames, cobra.ShellCompDirectiveDefault
@ -35,8 +40,8 @@ func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
func RecipeNameComplete() ([]string, cobra.ShellCompDirective) { func RecipeNameComplete() ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(false) catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) err := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
var recipeNames []string var recipeNames []string
@ -51,8 +56,8 @@ func RecipeNameComplete() ([]string, cobra.ShellCompDirective) {
func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) { func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
catl, err := recipe.ReadRecipeCatalogue(false) catl, err := recipe.ReadRecipeCatalogue(false)
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) err := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
var recipeVersions []string var recipeVersions []string
@ -69,8 +74,8 @@ func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirectiv
func ServerNameComplete() ([]string, cobra.ShellCompDirective) { func ServerNameComplete() ([]string, cobra.ShellCompDirective) {
files, err := app.LoadAppFiles("") files, err := app.LoadAppFiles("")
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) err := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
var serverNames []string var serverNames []string
@ -85,14 +90,14 @@ func ServerNameComplete() ([]string, cobra.ShellCompDirective) {
func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) { func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) {
app, err := app.Get(appName) app, err := app.Get(appName)
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) err := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) err := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
sort.Strings(cmdNames) sort.Strings(cmdNames)
@ -106,8 +111,8 @@ func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) {
config, err := r.GetComposeConfig(nil) config, err := r.GetComposeConfig(nil)
if err != nil { if err != nil {
log.Debugf("autocomplete failed: %s", err) err := fmt.Sprintf("autocomplete failed: %s", err)
return nil, cobra.ShellCompDirectiveError return []string{err}, cobra.ShellCompDirectiveError
} }
var secretNames []string var secretNames []string

View File

@ -107,5 +107,13 @@ var (
REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json" CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json"
SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git" SSH_URL_TEMPLATE = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git"
CHAOS_DEFAULT = "false"
// NOTE(d1): please note, this was done purely out of laziness on our part
// AFAICR. it's easy to punt the value into the label because that is what is
// expects. it's not particularly useful in terms of UI/UX but hey, nobody
// complained yet!
CHAOS_DEFAULT = "false"
NO_DOMAIN_DEFAULT = "N/A"
NO_VERSION_DEFAULT = "N/A"
) )

View File

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

View File

@ -43,33 +43,122 @@ func HumanDuration(timestamp int64) string {
// CreateTable prepares a table layout for output. // CreateTable prepares a table layout for output.
func CreateTable() (*table.Table, error) { func CreateTable() (*table.Table, error) {
var (
renderer = lipgloss.NewRenderer(os.Stdout)
headerStyle = renderer.NewStyle().Bold(true).Align(lipgloss.Center)
cellStyle = renderer.NewStyle().Padding(0, 1)
borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
)
table := table.New(). table := table.New().
Border(lipgloss.ThickBorder()). Border(lipgloss.ThickBorder()).
BorderStyle( BorderStyle(borderStyle).
lipgloss.NewStyle(). StyleFunc(func(row, col int) lipgloss.Style {
Foreground(lipgloss.Color("63")), var style lipgloss.Style
)
switch {
case row == table.HeaderRow:
return headerStyle
default:
style = cellStyle
}
return style
})
return table, nil
}
func PrintTable(t *table.Table) error {
if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" { if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" {
// NOTE(d1): no width limits for CI testing since we test against outputs // NOTE(d1): no width limits for CI testing since we test against outputs
log.Debug("detected ABRA_CI=1") log.Debug("detected ABRA_CI=1")
return table, nil fmt.Println(t)
return nil
} }
tWidth, _ := lipgloss.Size(t.String())
width, _, err := term.GetSize(0) width, _, err := term.GetSize(0)
if err != nil { if err != nil {
return nil, err return err
} }
if width-10 < 79 { if tWidth > width {
// NOTE(d1): maintain standard minimum width t.Width(width - 10)
table.Width(79)
} else {
// NOTE(d1): tests show that this produces stable border drawing
table.Width(width - 10)
} }
return table, nil fmt.Println(t)
return nil
}
// horizontal is a JoinHorizontal helper function.
func horizontal(left, mid, right string) string {
return lipgloss.JoinHorizontal(lipgloss.Right, left, mid, right)
}
func CreateOverview(header string, rows [][]string) string {
var borderStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Padding(0, 1, 0, 1).
MaxWidth(79).
BorderForeground(lipgloss.Color("63"))
var headerStyle = lipgloss.NewStyle().
Underline(true).
Bold(true).
PaddingBottom(1)
var leftStyle = lipgloss.NewStyle().
Bold(true)
var rightStyle = lipgloss.NewStyle()
var longest int
for _, row := range rows {
if len(row[0]) > longest {
longest = len(row[0])
}
}
var renderedRows []string
for _, row := range rows {
if len(row) > 2 {
panic("CreateOverview: only accepts rows of len == 2")
}
lenOffset := 4
if len(row[0]) < longest {
lenOffset += longest - len(row[0])
}
offset := ""
for range lenOffset {
offset = offset + " "
}
renderedRows = append(
renderedRows,
horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1])),
)
}
body := strings.Builder{}
body.WriteString(
borderStyle.Render(
lipgloss.JoinVertical(
lipgloss.Center,
headerStyle.Render(header),
lipgloss.JoinVertical(
lipgloss.Left,
renderedRows...,
),
),
),
)
return body.String()
} }
// ToJSON converts a lipgloss.Table to JSON representation. It's not a robust // ToJSON converts a lipgloss.Table to JSON representation. It's not a robust

View File

@ -137,6 +137,13 @@ var LintRules = map[string][]LintRule{
HowToResolve: "name a servce 'app'", HowToResolve: "name a servce 'app'",
Function: LintAppService, Function: LintAppService,
}, },
{
Ref: "R015",
Level: "error",
Description: "deploy labels stanza present",
HowToResolve: "include \"deploy: labels: ...\" stanza",
Function: LintDeployLabelsPresent,
},
{ {
Ref: "R010", Ref: "R010",
Level: "error", Level: "error",
@ -269,6 +276,21 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
return false, nil return false, nil
} }
func LintDeployLabelsPresent(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return false, err
}
for _, service := range config.Services {
if service.Name == "app" && service.Deploy.Labels != nil {
return true, nil
}
}
return false, nil
}
func LintHealthchecks(recipe recipe.Recipe) (bool, error) { func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
config, err := recipe.GetComposeConfig(nil) config, err := recipe.GetComposeConfig(nil)
if err != nil { if err != nil {

View File

@ -6,6 +6,7 @@ import (
"path" "path"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
) )
func (r Recipe) SampleEnv() (map[string]string, error) { func (r Recipe) SampleEnv() (map[string]string, error) {
@ -29,7 +30,10 @@ func (r Recipe) GetReleaseNotes(version string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes))
title := formatter.BoldStyle.Render(fmt.Sprintf("%s release notes:", version))
withTitle := fmt.Sprintf("%s\n%s\n", title, releaseNotes)
return withTitle, nil return withTitle, nil
} }

View File

@ -247,7 +247,7 @@ func (r Recipe) ChaosVersion() (string, error) {
} }
if !isClean { if !isClean {
version = fmt.Sprintf("%s + unstaged changes", version) version = fmt.Sprintf("%s+U", version)
} }
return version, nil return version, nil

View File

@ -65,7 +65,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--chaos --no-input --no-converge-checks --chaos --no-input --no-converge-checks
assert_success assert_success
assert_output --partial 'chaos' assert_output --partial 'NEW CHAOS'
} }
# bats test_tags=slow # bats test_tags=slow
@ -111,8 +111,6 @@ teardown(){
--no-input --no-converge-checks --offline --no-input --no-converge-checks --offline
assert_success assert_success
assert_output --partial "$latestCommit" assert_output --partial "$latestCommit"
assert_output --partial 'using latest commit'
refute_output --partial 'chaos'
} }
# bats test_tags=slow # bats test_tags=slow
@ -130,7 +128,7 @@ teardown(){
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
assert_output --partial "${wantHash:0:8}" assert_output --partial "${wantHash:0:8}"
assert_output --partial 'chaos' assert_output --partial 'NEW CHAOS'
} }
# bats test_tags=slow # bats test_tags=slow
@ -172,12 +170,10 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --force --no-input --no-converge-checks --force
assert_success assert_success
assert_output --partial 'already deployed'
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --chaos --no-input --no-converge-checks --chaos
assert_success assert_success
assert_output --partial 'already deployed'
} }
# bats test_tags=slow # bats test_tags=slow
@ -228,7 +224,6 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success assert_success
assert_output --partial 'no DOMAIN=... configured for app'
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success assert_success
@ -262,7 +257,6 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks --no-domain-checks --no-input --no-converge-checks --no-domain-checks
assert_success assert_success
assert_output --partial 'skipping domain checks as requested'
} }
@test "error if specific version does not exist" { @test "error if specific version does not exist" {

View File

@ -60,11 +60,6 @@ teardown(){
assert_output --partial "$TEST_SERVER" assert_output --partial "$TEST_SERVER"
assert_output --partial "foo.com" assert_output --partial "foo.com"
run $ABRA app ls --server foo.com
assert_success
refute_output --partial "SERVER: $TEST_SERVER"
assert_output --partial "SERVER: foo.com"
run rm -rf "$ABRA_DIR/servers/foo.com" run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success assert_success
assert_not_exists "$ABRA_DIR/servers/foo.com" assert_not_exists "$ABRA_DIR/servers/foo.com"
@ -94,33 +89,6 @@ teardown(){
assert_output --partial "foo-recipe" assert_output --partial "foo-recipe"
} }
@test "server stats are correct" {
run $ABRA app ls
assert_success
assert_output --partial "SERVER: $TEST_SERVER"
assert_output --partial "TOTAL APPS: 1"
run mkdir -p "$ABRA_DIR/servers/foo.com"
assert_success
assert_exists "$ABRA_DIR/servers/foo.com"
run cp \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \
"$ABRA_DIR/servers/foo.com/app.foo.com.env"
assert_exists "$ABRA_DIR/servers/foo.com/app.foo.com.env"
run $ABRA app ls
assert_success
assert_output --partial "$TEST_SERVER"
assert_output --partial "foo.com"
assert_output --partial "TOTAL SERVERS: 2"
assert_output --partial "TOTAL APPS: 2"
run rm -rf "$ABRA_DIR/servers/foo.com"
assert_success
assert_not_exists "$ABRA_DIR/servers/foo.com"
}
@test "output is machine readable" { @test "output is machine readable" {
run $ABRA app ls --machine run $ABRA app ls --machine

View File

@ -173,8 +173,6 @@ teardown(){
--domain "$TEST_APP_DOMAIN" \ --domain "$TEST_APP_DOMAIN" \
--secrets --secrets
assert_success assert_success
assert_output --partial 'generated secrets'
assert_output --partial 'test_pass_one'
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run $ABRA app secret ls "$TEST_APP_DOMAIN" run $ABRA app secret ls "$TEST_APP_DOMAIN"

View File

@ -45,5 +45,4 @@ teardown(){
sanitisedDomainName="${TEST_APP_DOMAIN//./_}" sanitisedDomainName="${TEST_APP_DOMAIN//./_}"
assert_output --partial "$sanitisedDomainName_app" assert_output --partial "$sanitisedDomainName_app"
assert_output --partial "nginx"
} }

View File

@ -107,10 +107,10 @@ teardown(){
# bats test_tags=slow # bats test_tags=slow
@test "undeploy chaos deployment" { @test "undeploy chaos deployment" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \ run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos
--no-input --no-converge-checks --chaos
_undeploy_app run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
# NOTE(d1): ensure chaos undeploy # NOTE(d1): ensure chaos undeploy
assert_output --partial ${_get_current_hash:0:8} assert_output --partial ${_get_current_hash:0:8}

View File

@ -0,0 +1,60 @@
#!/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 "undeploy 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
run sed -i "s/TYPE=$TEST_RECIPE:.*/TYPE=$TEST_RECIPE/g" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app undeploy "$TEST_APP_DOMAIN" --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
}
# bats test_tags=slow
@test "chaos commit written to env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
assert_success
run grep -q "TYPE=$TEST_RECIPE:${_get_current_hash:0:8}" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -15,12 +15,14 @@ teardown_file(){
setup(){ setup(){
load "$PWD/tests/integration/helpers/common" load "$PWD/tests/integration/helpers/common"
_common_setup _common_setup
_ensure_catalogue
} }
teardown(){ teardown(){
_reset_recipe _reset_recipe
_reset_app _reset_app
_undeploy_app _undeploy_app
_reset_app # NOTE(d1): _undeploy_app writes version
} }
@test "validate app argument" { @test "validate app argument" {

View File

@ -11,7 +11,7 @@ setup() {
assert_success assert_success
assert_not_exists "$ABRA_DIR/recipes/matrix-synapse" assert_not_exists "$ABRA_DIR/recipes/matrix-synapse"
run $ABRA recipe fetch run $ABRA recipe fetch --all
assert_success assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse" assert_exists "$ABRA_DIR/recipes/matrix-synapse"
} }
@ -25,3 +25,13 @@ setup() {
assert_success assert_success
assert_exists "$ABRA_DIR/recipes/matrix-synapse" assert_exists "$ABRA_DIR/recipes/matrix-synapse"
} }
@test "error if missing args/flags" {
run $ABRA recipe fetch
assert_failure
}
@test "error if single recipe and --all" {
run $ABRA recipe fetch matrix-synapse --all
assert_failure
}

View File

@ -5,13 +5,6 @@ setup() {
_common_setup _common_setup
} }
@test "recipe list" {
run $ABRA recipe list
assert_success
NUM_RECIPES=$(jq length "$ABRA_DIR/catalogue/recipes.json")
assert_output --partial "total recipes: $NUM_RECIPES"
}
@test "recipe list with pattern" { @test "recipe list with pattern" {
run $ABRA recipe list --pattern cloud run $ABRA recipe list --pattern cloud
assert_success assert_success

View File

@ -48,11 +48,9 @@ teardown(){
--server "$TEST_SERVER" \ --server "$TEST_SERVER" \
--domain "foobar.$TEST_SERVER" --domain "foobar.$TEST_SERVER"
assert_success assert_success
assert_output --partial "new app 'foobar' created"
run $ABRA app deploy "foobar.$TEST_SERVER" --no-input run $ABRA app deploy "foobar.$TEST_SERVER" --no-input
assert_success assert_success
assert_output --partial 'using latest commit'
} }
@test "create new recipe with git credentials" { @test "create new recipe with git credentials" {

View File

@ -32,13 +32,3 @@ setup() {
assert_success assert_success
assert_output "$latestVersion" assert_output "$latestVersion"
} }
@test "app is first service listed" {
run bash -c '$ABRA recipe versions gitea --machine | jq -r ".[0].service" | uniq'
assert_success
assert_output 'app'
run bash -c '$ABRA recipe versions gitea --machine | jq -r ".[1].service" | uniq'
assert_success
assert_output 'db'
}