From 97959ef5da19027db72566087c7c16f89cc969c0 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 15:30:43 +0100 Subject: [PATCH 01/15] refactor!: vertical render & UI/UX fixes See https://git.coopcloud.tech/coop-cloud/abra/pulls/454 --- cli/app/check.go | 9 ++- cli/app/deploy.go | 62 ++++++++++---------- cli/app/list.go | 26 ++------- cli/app/new.go | 102 +++++++++++++++----------------- cli/app/ps.go | 32 +++++++---- cli/app/secret.go | 9 ++- cli/app/services.go | 7 ++- cli/app/undeploy.go | 5 +- cli/app/volume.go | 9 +-- cli/internal/deploy.go | 107 +++++++++++++++++++++++++++------- cli/recipe/lint.go | 6 +- cli/recipe/list.go | 5 +- cli/recipe/version.go | 34 +++++++---- cli/server/list.go | 4 +- pkg/app/app.go | 2 +- pkg/formatter/formatter.go | 115 ++++++++++++++++++++++++++++++++----- pkg/recipe/git.go | 2 +- 17 files changed, 352 insertions(+), 184 deletions(-) diff --git a/cli/app/check.go b/cli/app/check.go index b48fb599..ac042f0a 100644 --- a/cli/app/check.go +++ b/cli/app/check.go @@ -46,7 +46,10 @@ ${FOO:} syntax). "check" does not confirm or deny this for you.`, } 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 { switch { case col == 1: @@ -71,7 +74,9 @@ ${FOO:} syntax). "check" does not confirm or deny this for you.`, } } - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } }, } diff --git a/cli/app/deploy.go b/cli/app/deploy.go index e9473c51..6841923d 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -110,19 +110,19 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, // is because we need to deal with GetComposeFiles under the hood and these // files change from version to version which therefore affects which // secrets might be generated - version := deployMeta.Version + toDeployVersion := deployMeta.Version if specificVersion != "" { - version = specificVersion - log.Debugf("choosing %s as version to deploy", version) + toDeployVersion = specificVersion + log.Debugf("choosing %s as version to deploy", toDeployVersion) var err error - isChaosCommit, err = app.Recipe.EnsureVersion(version) + isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion) if err != nil { log.Fatal(err) } if isChaosCommit { - log.Debugf("assuming '%s' is a chaos commit", version) + log.Debugf("assuming '%s' is a chaos commit", toDeployVersion) internal.Chaos = true } } @@ -138,12 +138,8 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, } } - if deployMeta.IsDeployed { - if internal.Force || internal.Chaos { - warnMessages = append(warnMessages, fmt.Sprintf("%s is already deployed", app.Name)) - } else { - log.Fatalf("%s is already deployed", app.Name) - } + if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) { + log.Fatalf("%s is already deployed", app.Name) } if !internal.Chaos && specificVersion == "" { @@ -153,9 +149,9 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, } if len(versions) > 0 && !internal.Chaos { - version = versions[len(versions)-1] - log.Debugf("choosing %s as version to deploy", version) - if _, err := app.Recipe.EnsureVersion(version); err != nil { + toDeployVersion = versions[len(versions)-1] + log.Debugf("choosing %s as version to deploy", toDeployVersion) + if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil { log.Fatal(err) } } else { @@ -163,25 +159,22 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, if err != nil { log.Fatal(err) } - version = formatter.SmallSHA(head.String()) - warnMessages = append(warnMessages, fmt.Sprintf("no versions detected, using latest commit")) + toDeployVersion = formatter.SmallSHA(head.String()) } } - chaosVersion := config.CHAOS_DEFAULT + toDeployChaosVersion := config.CHAOS_DEFAULT if internal.Chaos { - warnMessages = append(warnMessages, "chaos mode engaged") - if isChaosCommit { - chaosVersion = specificVersion + toDeployChaosVersion = specificVersion versionLabelLocal, err := app.Recipe.GetVersionLabelLocal() if err != nil { log.Fatal(err) } - version = versionLabelLocal + toDeployVersion = versionLabelLocal } else { var err error - chaosVersion, err = app.Recipe.ChaosVersion() + toDeployChaosVersion, err = app.Recipe.ChaosVersion() if err != nil { log.Fatal(err) } @@ -216,7 +209,7 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, appPkg.ExposeAllEnv(stackName, compose, app.Env) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetChaosLabel(compose, stackName, internal.Chaos) - appPkg.SetChaosVersionLabel(compose, stackName, chaosVersion) + appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion) appPkg.SetUpdateLabel(compose, stackName, app.Env) envVars, err := appPkg.CheckEnv(app) @@ -239,13 +232,24 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, log.Fatal(err) } } 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 { - 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 := "N/A" + if deployMeta.IsDeployed { + deployedVersion = deployMeta.Version + } + + if err := internal.DeployOverview( + app, + warnMessages, + deployedVersion, + deployMeta.ChaosVersion, + toDeployVersion, + toDeployChaosVersion); err != nil { log.Fatal(err) } @@ -267,9 +271,9 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, } } - app.Recipe.Version = version - if chaosVersion != config.CHAOS_DEFAULT { - app.Recipe.Version = chaosVersion + app.Recipe.Version = toDeployVersion + if toDeployChaosVersion != config.CHAOS_DEFAULT { + app.Recipe.Version = toDeployChaosVersion } log.Debugf("choosing %s as version to save to env file", app.Recipe.Version) if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { diff --git a/cli/app/list.go b/cli/app/list.go index d48c0dcb..be0753e7 100644 --- a/cli/app/list.go +++ b/cli/app/list.go @@ -208,7 +208,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, serverStat := allStats[app.Server] - headers := []string{"RECIPE", "DOMAIN"} + headers := []string{"RECIPE", "DOMAIN", "SERVER"} if status { headers = append(headers, []string{ "STATUS", @@ -228,7 +228,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, var rows [][]string for _, appStat := range serverStat.Apps { - row := []string{appStat.Recipe, appStat.Domain} + row := []string{appStat.Recipe, appStat.Domain, appStat.Server} if status { chaosStatus := appStat.Chaos if chaosStatus != "unknown" { @@ -256,20 +256,8 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, 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", - 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 err := formatter.PrintTable(table); err != nil { + log.Fatal(err) } 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 } - - 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) - } }, } diff --git a/cli/app/new.go b/cli/app/new.go index 8a0b53b1..4d9b7ddd 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -64,7 +64,6 @@ var AppNewCommand = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { recipe := internal.ValidateRecipe(args, cmd.Name()) - var recipeVersion string if !internal.Chaos { if err := recipe.EnsureIsClean(); err != nil { log.Fatal(err) @@ -74,41 +73,47 @@ var AppNewCommand = &cobra.Command{ log.Fatal(err) } } + } - if len(args) == 2 { - recipeVersion = args[1] - } + var recipeVersion string + if len(args) == 2 { + recipeVersion = args[1] + } - 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) - } + var recipeVersions recipePkg.RecipeVersions + if recipeVersion == "" { + var err error + recipeVersions, err = recipe.GetRecipeVersions() + if err != nil { + log.Fatal(err) } } - if internal.Chaos && recipeVersion == "" { + 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 !internal.Chaos && recipeVersion != "" { + if _, err := recipe.EnsureVersion(recipeVersion); err != nil { + log.Fatal(err) + } + } + + chaosVersion := config.CHAOS_DEFAULT + if internal.Chaos { var err error - recipeVersion, err = recipe.ChaosVersion() + chaosVersion, err = recipe.ChaosVersion() if err != nil { log.Fatal(err) } @@ -187,37 +192,20 @@ var AppNewCommand = &cobra.Command{ newAppServer = "local" } - table, err := formatter.CreateTable() - 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)) + log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion) if len(appSecrets) > 0 { - fmt.Println("") - fmt.Println("Generated secrets:") - fmt.Println("") - fmt.Println(secretsTable) + rows := [][]string{} + for k, v := range appSecrets { + rows = append(rows, []string{k, v}) + } + + overview := formatter.CreateOverview("SECRETS OVERVIEW", rows) + + fmt.Println(overview) 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("NOW"), ) diff --git a/cli/app/ps.go b/cli/app/ps.go index d818766c..a1ff5f33 100644 --- a/cli/app/ps.go +++ b/cli/app/ps.go @@ -128,24 +128,35 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao 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{ containerStats["service"], containerStats["image"], - containerStats["created"], + dVersion, + cVersion, containerStats["status"], - containerStats["state"], - containerStats["ports"], } rows = append(rows, row) } if internal.MachineReadable { - jsonstring, err := json.Marshal(allContainerStats) + rendered, err := json.Marshal(allContainerStats) if err != nil { log.Fatal("unable to convert to JSON: %s", err) } - fmt.Println(string(jsonstring)) + + fmt.Println(string(rendered)) + return } @@ -157,19 +168,18 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao headers := []string{ "SERVICE", "IMAGE", - "CREATED", + "VERSION", + "CHAOS", "STATUS", - "STATE", - "PORTS", } table. Headers(headers...). Rows(rows...) - fmt.Println(table) - - log.Infof("VERSION: %s CHAOS: %s", deployedVersion, chaosVersion) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } } func init() { diff --git a/cli/app/secret.go b/cli/app/secret.go index 511135e7..5931c344 100644 --- a/cli/app/secret.go +++ b/cli/app/secret.go @@ -127,7 +127,9 @@ var AppSecretGenerateCommand = &cobra.Command{ return } - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } log.Warnf( "generated secrets %s shown again, please take note of them %s", @@ -394,7 +396,10 @@ var AppSecretLsCommand = &cobra.Command{ return } - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } + return } diff --git a/cli/app/services.go b/cli/app/services.go index 8e47beb2..b3313374 100644 --- a/cli/app/services.go +++ b/cli/app/services.go @@ -63,7 +63,7 @@ var AppServicesCommand = &cobra.Command{ log.Fatal(err) } - headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)", "IMAGE"} + headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)"} table.Headers(headers...) var rows [][]string @@ -80,7 +80,6 @@ var AppServicesCommand = &cobra.Command{ row := []string{ serviceShortName, serviceLongName, - formatter.RemoveSha(container.Image), } rows = append(rows, row) @@ -89,7 +88,9 @@ var AppServicesCommand = &cobra.Command{ table.Rows(rows...) if len(rows) > 0 { - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } } }, } diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go index 32709beb..e107e127 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -59,7 +59,10 @@ Passing "--prune/-p" does not remove those volumes.`, 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) } diff --git a/cli/app/volume.go b/cli/app/volume.go index e7c64079..9813ba84 100644 --- a/cli/app/volume.go +++ b/cli/app/volume.go @@ -2,7 +2,6 @@ package app import ( "context" - "fmt" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" @@ -43,7 +42,7 @@ var AppVolumeListCommand = &cobra.Command{ log.Fatal(err) } - headers := []string{"name", "created", "mounted"} + headers := []string{"NAME", "ON SERVER"} table, err := formatter.CreateTable() if err != nil { @@ -54,14 +53,16 @@ var AppVolumeListCommand = &cobra.Command{ var rows [][]string for _, volume := range volumes { - row := []string{volume.Name, volume.CreatedAt, volume.Mountpoint} + row := []string{volume.Name, volume.Mountpoint} rows = append(rows, row) } table.Rows(rows...) if len(rows) > 0 { - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } return } diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index 669c1053..d3fe5a29 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -6,6 +6,7 @@ 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" @@ -20,7 +21,8 @@ var borderStyle = lipgloss.NewStyle(). var headerStyle = lipgloss.NewStyle(). Underline(true). - Bold(true) + Bold(true). + PaddingBottom(1) var leftStyle = lipgloss.NewStyle(). Bold(true) @@ -51,6 +53,11 @@ func NewVersionOverview( server = "local" } + domain := app.Domain + if domain == "" { + domain = "N/A" + } + body := strings.Builder{} body.WriteString( borderStyle.Render( @@ -60,7 +67,7 @@ func NewVersionOverview( lipgloss.JoinVertical( lipgloss.Left, horizontal(leftStyle.Render("SERVER"), " ", rightStyle.Render(server)), - horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(app.Domain)), + horizontal(leftStyle.Render("DOMAIN"), " ", rightStyle.Render(domain)), horizontal(leftStyle.Render("RECIPE"), " ", rightStyle.Render(app.Recipe.Name)), horizontal(leftStyle.Render("CONFIG"), " ", rightStyle.Render(deployConfig)), horizontal(leftStyle.Render("VERSION"), " ", rightStyle.Render(currentVersion)), @@ -101,7 +108,13 @@ func NewVersionOverview( } // 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" if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") @@ -112,25 +125,25 @@ func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion 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()) + domain := app.Domain + if domain == "" { + domain = "N/A" + } + + rows := [][]string{ + []string{"APP", domain}, + []string{"RECIPE", app.Recipe.Name}, + []string{"SERVER", server}, + []string{"DEPLOYED", deployedVersion}, + []string{"CURRENT CHAOS ", deployedChaosVersion}, + []string{"TO DEPLOY", toDeployVersion}, + []string{"NEW CHAOS", toDeployChaosVersion}, + []string{"CONFIG", deployConfig}, + } + + overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows) + + fmt.Println(overview) for _, msg := range warnMessages { log.Warn(msg) @@ -153,6 +166,56 @@ func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion 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 = strings.Join(strings.Split(composeFiles, ":"), "\n") + } + + server := app.Server + if app.Server == "default" { + server = "local" + } + + domain := app.Domain + if domain == "" { + domain = "N/A" + } + + 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 // the commands string must have the following format: // " | |... " diff --git a/cli/recipe/lint.go b/cli/recipe/lint.go index 3cf7e8fb..030816de 100644 --- a/cli/recipe/lint.go +++ b/cli/recipe/lint.go @@ -1,8 +1,6 @@ package recipe import ( - "fmt" - "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/formatter" @@ -104,7 +102,9 @@ var RecipeLintCommand = &cobra.Command{ } if len(rows) > 0 { - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } for _, warnMsg := range warnMessages { log.Warn(warnMsg) diff --git a/cli/recipe/list.go b/cli/recipe/list.go index e7fce3d0..e0b44c08 100644 --- a/cli/recipe/list.go +++ b/cli/recipe/list.go @@ -79,8 +79,9 @@ var RecipeListCommand = &cobra.Command{ return } - fmt.Println(table) - log.Infof("total recipes: %v", len(rows)) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } } }, } diff --git a/cli/recipe/version.go b/cli/recipe/version.go index dc66f247..cc3dbc34 100644 --- a/cli/recipe/version.go +++ b/cli/recipe/version.go @@ -55,15 +55,32 @@ var RecipeVersionCommand = &cobra.Command{ log.Fatal(err) } - table.Headers("SERVICE", "NAME", "TAG") + table.Headers("SERVICE", "IMAGE", "TAG", "VERSION") for version, meta := range recipeMeta.Versions[i] { var allRows [][]string var rows [][]string for service, serviceMeta := range meta { - rows = append(rows, []string{service, serviceMeta.Image, serviceMeta.Tag}) - allRows = append(allRows, []string{version, service, serviceMeta.Image, serviceMeta.Tag}) + recipeVersion := version + 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)) @@ -71,8 +88,9 @@ var RecipeVersionCommand = &cobra.Command{ table.Rows(rows...) if !internal.MachineReadable { - fmt.Println(table) - log.Infof("VERSION: %s", version) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } fmt.Println() continue } @@ -100,11 +118,7 @@ var RecipeVersionCommand = &cobra.Command{ func sortServiceByName(versions [][]string) func(i, j int) bool { return func(i, j int) bool { - // NOTE(d1): corresponds to the `tableCol` definition below - if versions[i][1] == "app" { - return true - } - return versions[i][1] < versions[j][1] + return versions[i][0] < versions[j][0] } } diff --git a/cli/server/list.go b/cli/server/list.go index 8eeb177a..dd3fd211 100644 --- a/cli/server/list.go +++ b/cli/server/list.go @@ -86,7 +86,9 @@ var ServerListCommand = &cobra.Command{ return } - fmt.Println(table) + if err := formatter.PrintTable(table); err != nil { + log.Fatal(err) + } }, } diff --git a/pkg/app/app.go b/pkg/app/app.go index 25c00a99..a1083f8b 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -615,7 +615,7 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error { } 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 { log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain) } diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index 56969513..a6279452 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -43,33 +43,122 @@ func HumanDuration(timestamp int64) string { // CreateTable prepares a table layout for output. 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(). Border(lipgloss.ThickBorder()). - BorderStyle( - lipgloss.NewStyle(). - Foreground(lipgloss.Color("63")), - ) + BorderStyle(borderStyle). + StyleFunc(func(row, col int) lipgloss.Style { + var style lipgloss.Style + switch { + case row == table.HeaderRow: + return headerStyle + default: + style = cellStyle + } + + return style + }) + + return table, nil +} + +func PrintTable(t *table.Table) error { if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" { // NOTE(d1): no width limits for CI testing since we test against outputs log.Debug("detected ABRA_CI=1") - return table, nil + fmt.Println(t) + return nil } + tWidth, _ := lipgloss.Size(t.String()) + width, _, err := term.GetSize(0) if err != nil { - return nil, err + return 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) + if tWidth > width { + t.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 diff --git a/pkg/recipe/git.go b/pkg/recipe/git.go index 7a695fd3..8da23e8b 100644 --- a/pkg/recipe/git.go +++ b/pkg/recipe/git.go @@ -247,7 +247,7 @@ func (r Recipe) ChaosVersion() (string, error) { } if !isClean { - version = fmt.Sprintf("%s + unstaged changes", version) + version = fmt.Sprintf("%s+U", version) } return version, nil From dc4c6784cb4f34937f97a48f1701867bebaa9664 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 16:39:58 +0100 Subject: [PATCH 02/15] test: integration test patches --- cli/app/new.go | 39 +++++++++++++---------------- tests/integration/app_deploy.bats | 10 ++------ tests/integration/app_list.bats | 32 ----------------------- tests/integration/app_new.bats | 2 -- tests/integration/app_services.bats | 1 - tests/integration/recipe_list.bats | 7 ------ 6 files changed, 19 insertions(+), 72 deletions(-) diff --git a/cli/app/new.go b/cli/app/new.go index 4d9b7ddd..a9c63945 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -64,10 +64,19 @@ var AppNewCommand = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { recipe := internal.ValidateRecipe(args, cmd.Name()) - if !internal.Chaos { - if err := recipe.EnsureIsClean(); err != nil { - log.Fatal(err) - } + if len(args) == 2 && internal.Chaos { + log.Fatal("cannot use [version] and --chaos together") + } + + var recipeVersion string + if len(args) == 2 { + recipeVersion = args[1] + } + + chaosVersion := config.CHAOS_DEFAULT + if internal.Chaos { + recipeVersion = chaosVersion + if !internal.Offline { if err := recipe.EnsureUpToDate(); err != nil { log.Fatal(err) @@ -75,9 +84,10 @@ var AppNewCommand = &cobra.Command{ } } - var recipeVersion string - if len(args) == 2 { - recipeVersion = args[1] + if !internal.Chaos { + if err := recipe.EnsureIsClean(); err != nil { + log.Fatal(err) + } } var recipeVersions recipePkg.RecipeVersions @@ -104,21 +114,6 @@ var AppNewCommand = &cobra.Command{ } } - if !internal.Chaos && recipeVersion != "" { - if _, err := recipe.EnsureVersion(recipeVersion); err != nil { - log.Fatal(err) - } - } - - chaosVersion := config.CHAOS_DEFAULT - if internal.Chaos { - var err error - chaosVersion, err = recipe.ChaosVersion() - if err != nil { - log.Fatal(err) - } - } - if err := ensureServerFlag(); err != nil { log.Fatal(err) } diff --git a/tests/integration/app_deploy.bats b/tests/integration/app_deploy.bats index 3594afa6..41e85423 100644 --- a/tests/integration/app_deploy.bats +++ b/tests/integration/app_deploy.bats @@ -65,7 +65,7 @@ teardown(){ run $ABRA app deploy "$TEST_APP_DOMAIN" \ --chaos --no-input --no-converge-checks assert_success - assert_output --partial 'chaos' + assert_output --partial 'NEW CHAOS' } # bats test_tags=slow @@ -111,8 +111,6 @@ teardown(){ --no-input --no-converge-checks --offline assert_success assert_output --partial "$latestCommit" - assert_output --partial 'using latest commit' - refute_output --partial 'chaos' } # bats test_tags=slow @@ -130,7 +128,7 @@ teardown(){ --no-input --no-converge-checks --chaos assert_success assert_output --partial "${wantHash:0:8}" - assert_output --partial 'chaos' + assert_output --partial 'NEW CHAOS' } # bats test_tags=slow @@ -172,12 +170,10 @@ teardown(){ run $ABRA app deploy "$TEST_APP_DOMAIN" \ --no-input --no-converge-checks --force assert_success - 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' } # bats test_tags=slow @@ -228,7 +224,6 @@ teardown(){ run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks assert_success - assert_output --partial 'no DOMAIN=... configured for app' run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input assert_success @@ -262,7 +257,6 @@ teardown(){ run $ABRA app deploy "$TEST_APP_DOMAIN" \ --no-input --no-converge-checks --no-domain-checks assert_success - assert_output --partial 'skipping domain checks as requested' } @test "error if specific version does not exist" { diff --git a/tests/integration/app_list.bats b/tests/integration/app_list.bats index 61c09e92..2cc4ec55 100644 --- a/tests/integration/app_list.bats +++ b/tests/integration/app_list.bats @@ -60,11 +60,6 @@ teardown(){ assert_output --partial "$TEST_SERVER" 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" assert_success assert_not_exists "$ABRA_DIR/servers/foo.com" @@ -94,33 +89,6 @@ teardown(){ 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" { run $ABRA app ls --machine diff --git a/tests/integration/app_new.bats b/tests/integration/app_new.bats index c3be967c..24068131 100644 --- a/tests/integration/app_new.bats +++ b/tests/integration/app_new.bats @@ -173,8 +173,6 @@ teardown(){ --domain "$TEST_APP_DOMAIN" \ --secrets assert_success - assert_output --partial 'generated secrets' - assert_output --partial 'test_pass_one' assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" run $ABRA app secret ls "$TEST_APP_DOMAIN" diff --git a/tests/integration/app_services.bats b/tests/integration/app_services.bats index 2d770cb0..532bda5b 100644 --- a/tests/integration/app_services.bats +++ b/tests/integration/app_services.bats @@ -45,5 +45,4 @@ teardown(){ sanitisedDomainName="${TEST_APP_DOMAIN//./_}" assert_output --partial "$sanitisedDomainName_app" - assert_output --partial "nginx" } diff --git a/tests/integration/recipe_list.bats b/tests/integration/recipe_list.bats index ed2b7eb1..d1e1e1f7 100644 --- a/tests/integration/recipe_list.bats +++ b/tests/integration/recipe_list.bats @@ -5,13 +5,6 @@ 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" { run $ABRA recipe list --pattern cloud assert_success From 866c5c45360b27c39eae0fd4910dceb72044b43a Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 17:16:53 +0100 Subject: [PATCH 03/15] test: even moar integration suite patches --- tests/integration/recipe_new.bats | 2 -- tests/integration/recipe_version.bats | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/tests/integration/recipe_new.bats b/tests/integration/recipe_new.bats index b0e74faf..a7e96c45 100644 --- a/tests/integration/recipe_new.bats +++ b/tests/integration/recipe_new.bats @@ -48,11 +48,9 @@ teardown(){ --server "$TEST_SERVER" \ --domain "foobar.$TEST_SERVER" assert_success - assert_output --partial "new app 'foobar' created" run $ABRA app deploy "foobar.$TEST_SERVER" --no-input assert_success - assert_output --partial 'using latest commit' } @test "create new recipe with git credentials" { diff --git a/tests/integration/recipe_version.bats b/tests/integration/recipe_version.bats index 38f1ccb9..390c0620 100644 --- a/tests/integration/recipe_version.bats +++ b/tests/integration/recipe_version.bats @@ -32,13 +32,3 @@ setup() { assert_success 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' -} From a0da5299feb141e043b3060f1ec1590a9898d442 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 16:59:08 +0100 Subject: [PATCH 04/15] feat: write undeploy version See https://git.coopcloud.tech/toolshed/organising/issues/633 --- cli/app/undeploy.go | 7 ++- tests/integration/app_undeploy.bats | 6 +- .../integration/app_undeploy_env_version.bats | 60 +++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 tests/integration/app_undeploy_env_version.bats diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go index e107e127..3d1a99a1 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -21,7 +21,7 @@ var AppUndeployCommand = &cobra.Command{ Use: "undeploy [flags]", Aliases: []string{"un"}, 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 any previously attached volumes as eligible for pruning once undeployed. @@ -79,6 +79,11 @@ Passing "--prune/-p" does not remove those volumes.`, 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) + } }, } diff --git a/tests/integration/app_undeploy.bats b/tests/integration/app_undeploy.bats index d4c5d7b2..eba63388 100644 --- a/tests/integration/app_undeploy.bats +++ b/tests/integration/app_undeploy.bats @@ -107,10 +107,10 @@ teardown(){ # bats test_tags=slow @test "undeploy chaos deployment" { - run $ABRA app deploy "$TEST_APP_DOMAIN" \ - --no-input --no-converge-checks --chaos + run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos - _undeploy_app + run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input + assert_success # NOTE(d1): ensure chaos undeploy assert_output --partial ${_get_current_hash:0:8} diff --git a/tests/integration/app_undeploy_env_version.bats b/tests/integration/app_undeploy_env_version.bats new file mode 100644 index 00000000..0662c4c1 --- /dev/null +++ b/tests/integration/app_undeploy_env_version.bats @@ -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 +} From 27f68b1dc590bf6cf7a7235ae0f7013f40869c98 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 20:55:25 +0100 Subject: [PATCH 05/15] refactor!: recipe fetch [recipe | --all] See https://git.coopcloud.tech/toolshed/organising/issues/639 --- cli/recipe/fetch.go | 27 ++++++++++++++++++++++++--- tests/integration/recipe_fetch.bats | 12 +++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/cli/recipe/fetch.go b/cli/recipe/fetch.go index 1a37c42c..7840dc0f 100644 --- a/cli/recipe/fetch.go +++ b/cli/recipe/fetch.go @@ -10,10 +10,9 @@ import ( ) var RecipeFetchCommand = &cobra.Command{ - Use: "fetch [recipe] [flags]", + Use: "fetch [recipe | --all] [flags]", Aliases: []string{"f"}, - Short: "Fetch recipe(s)", - Long: "Retrieves all recipes if no [recipe] argument is passed.", + Short: "Clone recipe(s) locally", Args: cobra.RangeArgs(0, 1), ValidArgsFunction: func( cmd *cobra.Command, @@ -27,6 +26,14 @@ var RecipeFetchCommand = &cobra.Command{ 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 != "" { r := internal.ValidateRecipe(args, cmd.Name()) 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", + ) +} diff --git a/tests/integration/recipe_fetch.bats b/tests/integration/recipe_fetch.bats index d2415355..8e296760 100644 --- a/tests/integration/recipe_fetch.bats +++ b/tests/integration/recipe_fetch.bats @@ -11,7 +11,7 @@ setup() { assert_success assert_not_exists "$ABRA_DIR/recipes/matrix-synapse" - run $ABRA recipe fetch + run $ABRA recipe fetch --all assert_success assert_exists "$ABRA_DIR/recipes/matrix-synapse" } @@ -25,3 +25,13 @@ setup() { assert_success 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 +} From 3f32dbb1a3903eeeab092bb37c1067c5d926cdd4 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 21:17:51 +0100 Subject: [PATCH 06/15] fix: better "server add" failure See https://git.coopcloud.tech/toolshed/organising/issues/570 --- cli/server/add.go | 19 +++++++++++-------- pkg/dns/dns.go | 5 ++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cli/server/add.go b/cli/server/add.go index 24788dc1..2a99b650 100644 --- a/cli/server/add.go +++ b/cli/server/add.go @@ -88,10 +88,6 @@ developer machine. The domain is then set to "default".`, return } - if _, err := dns.EnsureIPv4(name); err != nil { - log.Warn(err) - } - _, err := createServerDir(name) if err != nil { log.Fatal(err) @@ -100,21 +96,28 @@ developer machine. The domain is then set to "default".`, created, err := newContext(name) if err != nil { cleanUp(name) - log.Fatal(err) + log.Fatalf("unable to create local context: %s", err) } log.Debugf("attempting to create client for %s", name) if _, err := client.New(name, timeout); err != nil { cleanUp(name) - log.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 { 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) }, } diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go index 62a6f588..40497d66 100644 --- a/pkg/dns/dns.go +++ b/pkg/dns/dns.go @@ -9,12 +9,11 @@ import ( func EnsureIPv4(domainName string) (string, error) { ipv4, err := net.ResolveIPAddr("ip4", domainName) 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 { - return "", fmt.Errorf("unable to resolve ipv4 address for %s", domainName) + return "", fmt.Errorf("%s: no IPv4 available", domainName) } return ipv4.String(), nil From 03000c25e0248c3e46bbc7bdbe483b88e3d53c20 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 21:54:14 +0100 Subject: [PATCH 07/15] refactor: parametrize default value --- cli/app/deploy.go | 2 +- cli/internal/deploy.go | 7 ++++--- pkg/config/abra.go | 10 +++++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cli/app/deploy.go b/cli/app/deploy.go index 6841923d..3fc06e16 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -238,7 +238,7 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, log.Debug("skipping domain checks as requested") } - deployedVersion := "N/A" + deployedVersion := config.NO_VERSION_DEFAULT if deployMeta.IsDeployed { deployedVersion = deployMeta.Version } diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index d3fe5a29..5485b91f 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -6,6 +6,7 @@ import ( "strings" appPkg "coopcloud.tech/abra/pkg/app" + "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "github.com/AlecAivazis/survey/v2" @@ -55,7 +56,7 @@ func NewVersionOverview( domain := app.Domain if domain == "" { - domain = "N/A" + domain = config.NO_DOMAIN_DEFAULT } body := strings.Builder{} @@ -127,7 +128,7 @@ func DeployOverview( domain := app.Domain if domain == "" { - domain = "N/A" + domain = config.NO_DOMAIN_DEFAULT } rows := [][]string{ @@ -183,7 +184,7 @@ func UndeployOverview( domain := app.Domain if domain == "" { - domain = "N/A" + domain = config.NO_DOMAIN_DEFAULT } rows := [][]string{ diff --git a/pkg/config/abra.go b/pkg/config/abra.go index 1a7c6e03..8be06fea 100644 --- a/pkg/config/abra.go +++ b/pkg/config/abra.go @@ -107,5 +107,13 @@ 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" + + // 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" ) From 8ac31330bec9693ef29ef9fd85129ddf979eeab2 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 21:55:02 +0100 Subject: [PATCH 08/15] fix: error out if missing "deploy.labels" See https://git.coopcloud.tech/toolshed/organising/issues/643 --- pkg/lint/recipe.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pkg/lint/recipe.go b/pkg/lint/recipe.go index 51ee392f..32dd268c 100644 --- a/pkg/lint/recipe.go +++ b/pkg/lint/recipe.go @@ -137,6 +137,13 @@ var LintRules = map[string][]LintRule{ HowToResolve: "name a servce 'app'", Function: LintAppService, }, + { + Ref: "R015", + Level: "error", + Description: "deploy labels stanza present", + HowToResolve: "include \"deploy: labels: ...\" stanza", + Function: LintDeployLabelsPresent, + }, { Ref: "R010", Level: "error", @@ -269,6 +276,21 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) { 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) { config, err := recipe.GetComposeConfig(nil) if err != nil { From fab93a559a65a46ae97fec513b1e0030c4648be5 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 22:22:13 +0100 Subject: [PATCH 09/15] fix: more robust autocomplete + error handling See https://git.coopcloud.tech/toolshed/organising/issues/652 --- cli/app/deploy.go | 4 ++-- cli/app/logs.go | 5 ++-- cli/app/rollback.go | 4 ++-- cli/app/secret.go | 12 +++++----- cli/app/upgrade.go | 4 ++-- pkg/autocomplete/autocomplete.go | 41 ++++++++++++++++++-------------- 6 files changed, 38 insertions(+), 32 deletions(-) diff --git a/cli/app/deploy.go b/cli/app/deploy.go index 3fc06e16..6a8738fe 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -52,8 +52,8 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, case 1: app, err := appPkg.Get(args[0]) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveDefault + errMsg := fmt.Sprintf("autocomplete failed: %s", err) + return []string{errMsg}, cobra.ShellCompDirectiveError } return autocomplete.RecipeVersionComplete(app.Recipe.Name) default: diff --git a/cli/app/logs.go b/cli/app/logs.go index 9b2ff18b..b0cfa269 100644 --- a/cli/app/logs.go +++ b/cli/app/logs.go @@ -2,6 +2,7 @@ package app import ( "context" + "fmt" "io" "os" "slices" @@ -37,8 +38,8 @@ var AppLogsCommand = &cobra.Command{ case 1: app, err := appPkg.Get(args[0]) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveDefault + errMsg := fmt.Sprintf("autocomplete failed: %s", err) + return []string{errMsg}, cobra.ShellCompDirectiveError } return autocomplete.ServiceNameComplete(app.Name) default: diff --git a/cli/app/rollback.go b/cli/app/rollback.go index da4057a6..3535a393 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -46,8 +46,8 @@ beforehand.`, case 1: app, err := appPkg.Get(args[0]) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveDefault + errMsg := fmt.Sprintf("autocomplete failed: %s", err) + return []string{errMsg}, cobra.ShellCompDirectiveError } return autocomplete.RecipeVersionComplete(app.Recipe.Name) default: diff --git a/cli/app/secret.go b/cli/app/secret.go index 5931c344..9364a6da 100644 --- a/cli/app/secret.go +++ b/cli/app/secret.go @@ -34,8 +34,8 @@ var AppSecretGenerateCommand = &cobra.Command{ case 1: app, err := appPkg.Get(args[0]) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveDefault + errMsg := fmt.Sprintf("autocomplete failed: %s", err) + return []string{errMsg}, cobra.ShellCompDirectiveError } return autocomplete.SecretComplete(app.Recipe.Name) default: @@ -159,8 +159,8 @@ environment. Typically, you can let Abra generate them for you on app creation case 1: app, err := appPkg.Get(args[0]) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveDefault + errMsg := fmt.Sprintf("autocomplete failed: %s", err) + return []string{errMsg}, cobra.ShellCompDirectiveError } return autocomplete.SecretComplete(app.Recipe.Name) default: @@ -245,8 +245,8 @@ var AppSecretRmCommand = &cobra.Command{ if !rmAllSecrets { app, err := appPkg.Get(args[0]) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveDefault + errMsg := fmt.Sprintf("autocomplete failed: %s", err) + return []string{errMsg}, cobra.ShellCompDirectiveError } return autocomplete.SecretComplete(app.Recipe.Name) } diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index 2756dae6..7e5624cf 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -40,8 +40,8 @@ beforehand.`, case 1: app, err := appPkg.Get(args[0]) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveDefault + errMsg := fmt.Sprintf("autocomplete failed: %s", err) + return []string{errMsg}, cobra.ShellCompDirectiveError } return autocomplete.RecipeVersionComplete(app.Recipe.Name) default: diff --git a/pkg/autocomplete/autocomplete.go b/pkg/autocomplete/autocomplete.go index dc4c6a6f..93f8eb9d 100644 --- a/pkg/autocomplete/autocomplete.go +++ b/pkg/autocomplete/autocomplete.go @@ -1,21 +1,26 @@ package autocomplete import ( + "fmt" "sort" "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app" - "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" "github.com/spf13/cobra" ) // AppNameComplete copletes app names. func AppNameComplete() ([]string, cobra.ShellCompDirective) { - appNames, err := app.GetAppNames() + appFiles, err := app.LoadAppFiles("") if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveError + err := fmt.Sprintf("autocomplete failed: %s", err) + return []string{err}, cobra.ShellCompDirectiveError + } + + var appNames []string + for appName := range appFiles { + appNames = append(appNames, appName) } return appNames, cobra.ShellCompDirectiveDefault @@ -24,8 +29,8 @@ func AppNameComplete() ([]string, cobra.ShellCompDirective) { func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) { serviceNames, err := app.GetAppServiceNames(appName) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveError + err := fmt.Sprintf("autocomplete failed: %s", err) + return []string{err}, cobra.ShellCompDirectiveError } return serviceNames, cobra.ShellCompDirectiveDefault @@ -35,8 +40,8 @@ func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) { func RecipeNameComplete() ([]string, cobra.ShellCompDirective) { catl, err := recipe.ReadRecipeCatalogue(false) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveError + err := fmt.Sprintf("autocomplete failed: %s", err) + return []string{err}, cobra.ShellCompDirectiveError } var recipeNames []string @@ -51,8 +56,8 @@ func RecipeNameComplete() ([]string, cobra.ShellCompDirective) { func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) { catl, err := recipe.ReadRecipeCatalogue(false) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveError + err := fmt.Sprintf("autocomplete failed: %s", err) + return []string{err}, cobra.ShellCompDirectiveError } var recipeVersions []string @@ -69,8 +74,8 @@ func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirectiv func ServerNameComplete() ([]string, cobra.ShellCompDirective) { files, err := app.LoadAppFiles("") if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveError + err := fmt.Sprintf("autocomplete failed: %s", err) + return []string{err}, cobra.ShellCompDirectiveError } var serverNames []string @@ -85,14 +90,14 @@ func ServerNameComplete() ([]string, cobra.ShellCompDirective) { func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) { app, err := app.Get(appName) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveError + err := fmt.Sprintf("autocomplete failed: %s", err) + return []string{err}, cobra.ShellCompDirectiveError } cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveError + err := fmt.Sprintf("autocomplete failed: %s", err) + return []string{err}, cobra.ShellCompDirectiveError } sort.Strings(cmdNames) @@ -106,8 +111,8 @@ func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) { config, err := r.GetComposeConfig(nil) if err != nil { - log.Debugf("autocomplete failed: %s", err) - return nil, cobra.ShellCompDirectiveError + err := fmt.Sprintf("autocomplete failed: %s", err) + return []string{err}, cobra.ShellCompDirectiveError } var secretNames []string From 7ec61c6d033aba9e9e66a0a31e9128bde4291700 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 23:10:22 +0100 Subject: [PATCH 10/15] fix: sort versions upgrade/rollback/list See https://git.coopcloud.tech/toolshed/organising/issues/649 --- cli/app/list.go | 2 +- cli/app/rollback.go | 2 +- cli/app/upgrade.go | 2 +- cli/internal/deploy.go | 21 +++++++++++++++++++++ cli/internal/list.go | 10 ---------- 5 files changed, 24 insertions(+), 13 deletions(-) delete mode 100644 cli/internal/list.go diff --git a/cli/app/list.go b/cli/app/list.go index be0753e7..443a5422 100644 --- a/cli/app/list.go +++ b/cli/app/list.go @@ -173,7 +173,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, stats.LatestCount++ } } else { - newUpdates = internal.ReverseStringList(newUpdates) + newUpdates = internal.SortVersionsDesc(newUpdates) appStats.Upgrade = strings.Join(newUpdates, "\n") stats.UpgradeCount++ } diff --git a/cli/app/rollback.go b/cli/app/rollback.go index 3535a393..09ca206d 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -167,7 +167,7 @@ beforehand.`, prompt := &survey.Select{ Message: msg, - Options: internal.ReverseStringList(availableDowngrades), + Options: internal.SortVersionsDesc(availableDowngrades), } if err := survey.AskOne(prompt, &chosenDowngrade); err != nil { diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index 7e5624cf..bee884ef 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -159,7 +159,7 @@ beforehand.`, prompt := &survey.Select{ Message: msg, - Options: internal.ReverseStringList(availableUpgrades), + Options: internal.SortVersionsDesc(availableUpgrades), } if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index 5485b91f..79a7fac9 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -3,12 +3,14 @@ package internal import ( "fmt" "os" + "sort" "strings" appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" + "coopcloud.tech/tagcmp" "github.com/AlecAivazis/survey/v2" "github.com/charmbracelet/lipgloss" dockerClient "github.com/docker/docker/client" @@ -274,3 +276,22 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { } 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 +} diff --git a/cli/internal/list.go b/cli/internal/list.go deleted file mode 100644 index d2a89527..00000000 --- a/cli/internal/list.go +++ /dev/null @@ -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 -} From 356e527f1fdf642aa47b9413e5e72d13a682a5d7 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 23:35:47 +0100 Subject: [PATCH 11/15] refactor!: upgrade/rollback vertical render / ui fixes See https://git.coopcloud.tech/toolshed/organising/issues/658 --- cli/app/upgrade.go | 1 - cli/internal/deploy.go | 56 ++++++++++++++++++++---------------------- pkg/recipe/files.go | 6 ++++- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index bee884ef..d1f84ac4 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -250,7 +250,6 @@ beforehand.`, } if showReleaseNotes { - fmt.Println() fmt.Print(releaseNotes) return } diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index 79a7fac9..37aed079 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -42,13 +42,13 @@ func NewVersionOverview( app appPkg.App, warnMessages []string, kind, - currentVersion, - chaosVersion, - newVersion, + deployedVersion, + deployedChaosVersion, + toDeployVersion, releaseNotes string) error { deployConfig := "compose.yml" if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { - deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") + deployConfig = composeFiles } server := app.Server @@ -61,32 +61,30 @@ func NewVersionOverview( domain = config.NO_DOMAIN_DEFAULT } - 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(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()) + rows := [][]string{ + []string{"APP", domain}, + []string{"RECIPE", app.Recipe.Name}, + []string{"SERVER", server}, + []string{"DEPLOYED", deployedVersion}, + []string{"CURRENT CHAOS ", deployedChaosVersion}, + []string{fmt.Sprintf("TO %s", strings.ToUpper(kind)), toDeployVersion}, + []string{"CONFIG", deployConfig}, + } - if releaseNotes != "" && newVersion != "" { - fmt.Println() + overview := formatter.CreateOverview( + fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind)), + rows, + ) + + fmt.Println(overview) + + if releaseNotes != "" && toDeployVersion != "" { fmt.Print(releaseNotes) } else { - warnMessages = append(warnMessages, fmt.Sprintf("no release notes available for %s", newVersion)) + warnMessages = append( + warnMessages, + fmt.Sprintf("no release notes available for %s", toDeployVersion), + ) } for _, msg := range warnMessages { @@ -120,7 +118,7 @@ func DeployOverview( toDeployChaosVersion string) error { deployConfig := "compose.yml" if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { - deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") + deployConfig = composeFiles } server := app.Server @@ -176,7 +174,7 @@ func UndeployOverview( chaosVersion string) error { deployConfig := "compose.yml" if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { - deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") + deployConfig = composeFiles } server := app.Server diff --git a/pkg/recipe/files.go b/pkg/recipe/files.go index f092fff3..d3408fa4 100644 --- a/pkg/recipe/files.go +++ b/pkg/recipe/files.go @@ -6,6 +6,7 @@ import ( "path" "coopcloud.tech/abra/pkg/envfile" + "coopcloud.tech/abra/pkg/formatter" ) func (r Recipe) SampleEnv() (map[string]string, error) { @@ -29,7 +30,10 @@ func (r Recipe) GetReleaseNotes(version string) (string, error) { if err != nil { 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 } From 9f189680f3856fb837f67d3ef5256c2ac54fc0d4 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 28 Dec 2024 23:47:50 +0100 Subject: [PATCH 12/15] fix: less newline --- cli/recipe/version.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/recipe/version.go b/cli/recipe/version.go index cc3dbc34..3487dfb0 100644 --- a/cli/recipe/version.go +++ b/cli/recipe/version.go @@ -91,7 +91,6 @@ var RecipeVersionCommand = &cobra.Command{ if err := formatter.PrintTable(table); err != nil { log.Fatal(err) } - fmt.Println() continue } From 9a4414fd132242441bc7fa377984139d9027397c Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sun, 29 Dec 2024 00:14:16 +0100 Subject: [PATCH 13/15] test: fix failing upgrade test --- tests/integration/app_upgrade.bats | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/app_upgrade.bats b/tests/integration/app_upgrade.bats index 231683f3..77cd72e9 100644 --- a/tests/integration/app_upgrade.bats +++ b/tests/integration/app_upgrade.bats @@ -21,6 +21,7 @@ teardown(){ _reset_recipe _reset_app _undeploy_app + _reset_app # NOTE(d1): _undeploy_app writes version } @test "validate app argument" { From 3727c7fa78525ee030d7a07f579665f9583c54b6 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sun, 29 Dec 2024 00:44:47 +0100 Subject: [PATCH 14/15] test: ensure catalogue --- tests/integration/app_upgrade.bats | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/app_upgrade.bats b/tests/integration/app_upgrade.bats index 77cd72e9..3599add4 100644 --- a/tests/integration/app_upgrade.bats +++ b/tests/integration/app_upgrade.bats @@ -15,6 +15,7 @@ teardown_file(){ setup(){ load "$PWD/tests/integration/helpers/common" _common_setup + _ensure_catalogue } teardown(){ From 74108b0dd91c8c0322e821c3aeaf6b3e5f704e37 Mon Sep 17 00:00:00 2001 From: Philipp Rothmann Date: Sat, 28 Dec 2024 17:06:35 +0100 Subject: [PATCH 15/15] fix: create release dir in recipe if not exists #660 --- cli/recipe/release.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cli/recipe/release.go b/cli/recipe/release.go index f1a07d40..660de687 100644 --- a/cli/recipe/release.go +++ b/cli/recipe/release.go @@ -252,7 +252,14 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) { // addReleaseNotes checks if the release/next release note exists and moves the // file to release/. 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 { // Release note for current tag already exist exists. return nil @@ -260,7 +267,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { return err } - nextReleaseNotePath := path.Join(recipe.Dir, "release", "next") + nextReleaseNotePath := path.Join(releaseDir, "next") if _, err := os.Stat(nextReleaseNotePath); err == nil { // release/next note exists. Move it to release/ if internal.Dry {