diff --git a/cli/app/app.go b/cli/app/app.go index 0388fd23..00343cbe 100644 --- a/cli/app/app.go +++ b/cli/app/app.go @@ -5,11 +5,10 @@ import ( ) var AppCommand = cli.Command{ - Name: "app", - Aliases: []string{"a"}, - Usage: "Manage apps", - ArgsUsage: "", - Description: "Functionality for managing the life cycle of your apps", + Name: "app", + Aliases: []string{"a"}, + Usage: "Manage apps", + ArgsUsage: "", Subcommands: []cli.Command{ appBackupCommand, appCheckCommand, diff --git a/cli/app/cmd.go b/cli/app/cmd.go index 52dc03eb..c70fbf56 100644 --- a/cli/app/cmd.go +++ b/cli/app/cmd.go @@ -29,10 +29,11 @@ They can be run within the context of a service (e.g. app) or locally on your work station by passing "--local". Arguments can be passed into these functions using the "-- " syntax. -Example: +**WARNING**: options must be passed directly after the sub-command "cmd". - abra app cmd example.com app create_user -- me@example.com -`, +EXAMPLE: + + abra app cmd --local example.com app create_user -- me@example.com`, ArgsUsage: " [] [-- ]", Flags: []cli.Flag{ internal.DebugFlag, diff --git a/cli/app/config.go b/cli/app/config.go index 11322703..a21c25f7 100644 --- a/cli/app/config.go +++ b/cli/app/config.go @@ -43,7 +43,7 @@ var appConfigCommand = cli.Command{ ed, ok := os.LookupEnv("EDITOR") if !ok { edPrompt := &survey.Select{ - Message: "Which editor do you wish to use?", + Message: "which editor do you wish to use?", Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"}, } if err := survey.AskOne(edPrompt, &ed); err != nil { diff --git a/cli/app/deploy.go b/cli/app/deploy.go index ce011ce0..46249174 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -35,17 +35,18 @@ var appDeployCommand = cli.Command{ internal.OfflineFlag, }, Before: internal.SubCommandBefore, - Description: ` -Deploy an app. It does not support incrementing the version of a deployed app, -for this you need to look at the "abra app upgrade " command. + Description: `Deploy an app. -You may pass "--force" to re-deploy the same version again. This can be useful -if the container runtime has gotten into a weird state. +This command supports chaos operations. Use "--chaos" to deploy your recipe +checkout as-is. Recipe commit hashes are also supported values for +"[]". Please note, "upgrade"/"rollback" do not support chaos +operations. -Chaos mode ("--chaos") will deploy your local checkout of a recipe as-is, -including unstaged changes and can be useful for live hacking and testing new -recipes. -`, +EXAMPLE: + + abra app deploy foo.example.com + abra app deploy foo.example.com 1.2.3+3.2.1 + abra app deploy foo.example.com 1e83340e`, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) @@ -76,6 +77,9 @@ recipes. log.Fatal(err) } + // NOTE(d1): handles " as git hash" use case + var isChaosCommit bool + // NOTE(d1): check out specific version before dealing with secrets. This // is because we need to deal with GetComposeFiles under the hood and these // files change from version to version which therefore affects which @@ -84,9 +88,17 @@ recipes. if specificVersion != "" { version = specificVersion log.Debugf("choosing %s as version to deploy", version) - if err := app.Recipe.EnsureVersion(version); err != nil { + + var err error + isChaosCommit, err = app.Recipe.EnsureVersion(version) + if err != nil { log.Fatal(err) } + + if isChaosCommit { + log.Debugf("assuming '%s' is a chaos commit", version) + internal.Chaos = true + } } secStats, err := secret.PollSecretsStatus(cl, app) @@ -119,10 +131,10 @@ recipes. } if len(versions) == 0 && !internal.Chaos { - log.Warn("no published versions in catalogue, trying local recipe repository") + log.Debug("no published versions in catalogue, trying local recipe repository") recipeVersions, err := app.Recipe.GetRecipeVersions(internal.Offline) if err != nil { - log.Warn(err) + log.Fatal(err) } for _, recipeVersion := range recipeVersions { for version := range recipeVersion { @@ -134,7 +146,7 @@ recipes. 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 { + if _, err := app.Recipe.EnsureVersion(version); err != nil { log.Fatal(err) } } else { @@ -150,10 +162,20 @@ recipes. chaosVersion := "false" if internal.Chaos { log.Warnf("chaos mode engaged") - var err error - chaosVersion, err = app.Recipe.ChaosVersion() - if err != nil { - log.Fatal(err) + + if isChaosCommit { + chaosVersion = specificVersion + versionLabelLocal, err := app.Recipe.GetVersionLabelLocal() + if err != nil { + log.Fatal(err) + } + version = versionLabelLocal + } else { + var err error + chaosVersion, err = app.Recipe.ChaosVersion() + if err != nil { + log.Fatal(err) + } } } diff --git a/cli/app/list.go b/cli/app/list.go index 0ddea59f..407a39cb 100644 --- a/cli/app/list.go +++ b/cli/app/list.go @@ -77,8 +77,7 @@ generate a report of all your apps. By passing the "--status/-S" flag, you can query all your servers for the actual live deployment status. Depending on how many servers you manage, this -can take some time. -`, +can take some time.`, Flags: []cli.Flag{ internal.DebugFlag, internal.MachineReadableFlag, diff --git a/cli/app/new.go b/cli/app/new.go index c9e8e9bf..2ef010bc 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -19,8 +19,8 @@ import ( ) var appNewDescription = ` -Take a recipe and uses it to create a new app. This new app configuration is -stored in your ~/.abra directory under the appropriate server. +Creates a new app from a default recipe. This new app configuration is stored +in your $ABRA_DIR directory under the appropriate server. This command does not deploy your app for you. You will need to run "abra app deploy " to do so. @@ -35,8 +35,7 @@ store them somewhere safe. You can use the "--pass/-P" to store these generated passwords locally in a pass store (see passwordstore.org for more). The pass command must be available -on your $PATH. -` +on your $PATH.` var appNewCommand = cli.Command{ Name: "new", @@ -92,7 +91,7 @@ var appNewCommand = cli.Command{ version = tag } - if err := recipe.EnsureVersion(version); err != nil { + if _, err := recipe.EnsureVersion(version); err != nil { log.Fatal(err) } } else { @@ -101,7 +100,7 @@ var appNewCommand = cli.Command{ } } } else { - if err := recipe.EnsureVersion(c.Args().Get(1)); err != nil { + if _, err := recipe.EnsureVersion(c.Args().Get(1)); err != nil { log.Fatal(err) } } @@ -174,22 +173,25 @@ var appNewCommand = cli.Command{ table := formatter.CreateTable(tableCol) table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain}) - fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name)) + log.Infof("new app '%s' created 🌞", recipe.Name) + fmt.Println("") table.Render() fmt.Println("") - fmt.Println("You can configure this app by running the following:") + + fmt.Println("Configure this app:") fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain)) + fmt.Println("") - fmt.Println("You can deploy this app by running the following:") + fmt.Println("Deploy this app:") fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain)) if len(secrets) > 0 { fmt.Println("") - fmt.Println("Here are your generated secrets:") + fmt.Println("Generated secrets:") fmt.Println("") secretTable.Render() - log.Warn("generated secrets are not shown again, please take note of them NOW") + log.Warn("Generated secrets are not shown again, please take note of them NOW") } return nil diff --git a/cli/app/ps.go b/cli/app/ps.go index 0af9b69c..537c4018 100644 --- a/cli/app/ps.go +++ b/cli/app/ps.go @@ -55,7 +55,7 @@ var appPsCommand = cli.Command{ if statusMeta, ok := statuses[app.StackName()]; ok { isChaos, exists := statusMeta["chaos"] if exists && isChaos == "false" { - if err := app.Recipe.EnsureVersion(deployMeta.Version); err != nil { + if _, err := app.Recipe.EnsureVersion(deployMeta.Version); err != nil { log.Fatal(err) } } else { diff --git a/cli/app/remove.go b/cli/app/remove.go index 9bf4164c..b6476f20 100644 --- a/cli/app/remove.go +++ b/cli/app/remove.go @@ -36,8 +36,7 @@ Please note, if you delete the local app env file without removing volumes and secrets first, Abra will *not* be able to help you remove them afterwards. To delete everything without prompt, use the "--force/-f" or the "--no-input/n" -flag. -`, +flag.`, Flags: []cli.Flag{ internal.ForceFlag, internal.DebugFlag, diff --git a/cli/app/restart.go b/cli/app/restart.go index 91a192c9..1acf7ca2 100644 --- a/cli/app/restart.go +++ b/cli/app/restart.go @@ -33,10 +33,9 @@ Run "abra app ps " to see a list of service names. Pass "--all-services/-a" to restart all services. -Example: +EXAMPLE: - abra app restart example.com app -`, + abra app restart example.com app`, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) diff --git a/cli/app/rollback.go b/cli/app/rollback.go index ebb8c169..91a4e6dd 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -34,14 +34,18 @@ var appRollbackCommand = cli.Command{ }, Before: internal.SubCommandBefore, Description: ` -This command rolls an app back to a previous version if one exists. +This command rolls an app back to a previous version. -You may pass "--force/-f" to downgrade to the same version again. This can be -useful if the container runtime has gotten into a weird state. +Unlike "deploy", chaos operations are not supported here. Only recipe versions +are supported values for "[]". -This action could be destructive, please ensure you have a copy of your app -data beforehand. -`, +A rollback can be destructive, please ensure you have a copy of your app data +beforehand. + +EXAMPLE: + + abra app rollback foo.example.com + abra app rollback foo.example.com 1.2.3+3.2.1`, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) @@ -82,10 +86,10 @@ data beforehand. } if len(versions) == 0 { - log.Warn("no published versions in catalogue, trying local recipe repository") + log.Debug("no published versions in catalogue, trying local recipe repository") recipeVersions, err := app.Recipe.GetRecipeVersions(internal.Offline) if err != nil { - log.Warn(err) + log.Fatal(err) } for _, recipeVersion := range recipeVersions { @@ -105,15 +109,19 @@ data beforehand. if specificVersion != "" { parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) if err != nil { - log.Fatal(err) + log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name) } parsedSpecificVersion, err := tagcmp.Parse(specificVersion) if err != nil { - log.Fatal(err) + log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name) } - if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) { + if parsedSpecificVersion.IsGreaterThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) { + log.Fatalf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion) + } + + if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force { log.Fatalf("%s is not a downgrade for %s?", deployMeta.Version, specificVersion) } @@ -121,7 +129,7 @@ data beforehand. } if deployMeta.Version != "unknown" && specificVersion == "" { - if deployMeta.IsChaos == "true" { + if deployMeta.IsChaos { log.Warn("attempting to rollback a chaos deployment") } @@ -154,7 +162,7 @@ data beforehand. log.Debugf("choosing %s as version to downgrade to (--force/--no-input)", chosenDowngrade) } else { msg := fmt.Sprintf("please select a downgrade (version: %s):", deployMeta.Version) - if deployMeta.IsChaos == "true" { + if deployMeta.IsChaos { msg = fmt.Sprintf("please select a downgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion) } @@ -170,7 +178,7 @@ data beforehand. } log.Debugf("choosing %s as version to rollback", chosenDowngrade) - if err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil { + if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil { log.Fatal(err) } @@ -206,8 +214,8 @@ data beforehand. appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade) appPkg.SetUpdateLabel(compose, stackName, app.Env) - chaosVersion := deployMeta.IsChaos - if deployMeta.IsChaos == "true" { + chaosVersion := "false" + if deployMeta.IsChaos { chaosVersion = deployMeta.ChaosVersion } diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go index 1563ca60..342532a3 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -78,8 +78,7 @@ This does not destroy any of the application data. However, you should remain vigilant, as your swarm installation will consider any previously attached volumes as eligible for pruning once undeployed. -Passing "-p/--prune" does not remove those volumes. -`, +Passing "-p/--prune" does not remove those volumes.`, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) stackName := app.StackName() @@ -100,8 +99,8 @@ Passing "-p/--prune" does not remove those volumes. log.Fatalf("%s is not deployed?", app.Name) } - chaosVersion := deployMeta.IsChaos - if deployMeta.IsChaos == "true" { + chaosVersion := "false" + if deployMeta.IsChaos { chaosVersion = deployMeta.ChaosVersion } diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index bb7a7775..54b79730 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -34,15 +34,18 @@ var appUpgradeCommand = cli.Command{ }, Before: internal.SubCommandBefore, Description: ` -Upgrade an app. You can use it to choose and roll out a new upgrade to a -deployed app. +Upgrade an app. -You may pass "--force/-f" to upgrade to the same version again. This can be -useful if the container runtime has gotten into a weird state. +Unlike "deploy", chaos operations are not supported here. Only recipe versions +are supported values for "[]". -This action could be destructive, please ensure you have a copy of your app -data beforehand. -`, +An upgrade can be destructive, please ensure you have a copy of your app data +beforehand. + +EXAMPLE: + + abra app upgrade foo.example.com + abra app upgrade foo.example.com 1.2.3+3.2.1`, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) @@ -83,10 +86,10 @@ data beforehand. } if len(versions) == 0 { - log.Warn("no published versions in catalogue, trying local recipe repository") + log.Debug("no published versions in catalogue, trying local recipe repository") recipeVersions, err := app.Recipe.GetRecipeVersions(internal.Offline) if err != nil { - log.Warn(err) + log.Fatal(err) } for _, recipeVersion := range recipeVersions { for version := range recipeVersion { @@ -105,15 +108,21 @@ data beforehand. if specificVersion != "" { parsedDeployedVersion, err := tagcmp.Parse(deployMeta.Version) if err != nil { - log.Fatal(err) + log.Fatalf("'%s' is not a known version for %s", deployMeta.Version, app.Recipe.Name) } parsedSpecificVersion, err := tagcmp.Parse(specificVersion) if err != nil { - log.Fatal(err) + log.Fatalf("'%s' is not a known version for %s", specificVersion, app.Recipe.Name) } - if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) || parsedSpecificVersion.Equals(parsedDeployedVersion) { + + if parsedSpecificVersion.IsLessThan(parsedDeployedVersion) && !parsedSpecificVersion.Equals(parsedDeployedVersion) { log.Fatalf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion) } + + if parsedSpecificVersion.Equals(parsedDeployedVersion) && !internal.Force { + log.Fatalf("%s is not an upgrade for %s?", deployMeta.Version, specificVersion) + } + availableUpgrades = append(availableUpgrades, specificVersion) } @@ -123,7 +132,7 @@ data beforehand. } if deployMeta.Version != "unknown" && specificVersion == "" { - if deployMeta.IsChaos == "true" { + if deployMeta.IsChaos { log.Warn("attempting to upgrade a chaos deployment") } @@ -150,7 +159,7 @@ data beforehand. log.Debugf("choosing %s as version to upgrade to", chosenUpgrade) } else { msg := fmt.Sprintf("please select an upgrade (version: %s):", deployMeta.Version) - if deployMeta.IsChaos == "true" { + if deployMeta.IsChaos { msg = fmt.Sprintf("please select an upgrade (version: %s, chaosVersion: %s):", deployMeta.Version, deployMeta.ChaosVersion) } @@ -197,7 +206,7 @@ data beforehand. } log.Debugf("choosing %s as version to upgrade", chosenUpgrade) - if err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil { + if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil { log.Fatal(err) } @@ -250,8 +259,8 @@ data beforehand. return nil } - chaosVersion := deployMeta.IsChaos - if deployMeta.IsChaos == "true" { + chaosVersion := "false" + if deployMeta.IsChaos { chaosVersion = deployMeta.ChaosVersion } diff --git a/cli/app/volume.go b/cli/app/volume.go index d9d2f68a..31569394 100644 --- a/cli/app/volume.go +++ b/cli/app/volume.go @@ -73,8 +73,7 @@ The command is interactive and will show a multiple select input which allows you to make a seclection. Use the "?" key to see more help on navigating this interface. -Passing "--force/-f" will select all volumes for removal. Be careful. -`, +Passing "--force/-f" will select all volumes for removal. Be careful.`, ArgsUsage: "", Aliases: []string{"rm"}, Flags: []cli.Flag{ diff --git a/cli/catalogue/catalogue.go b/cli/catalogue/catalogue.go index e3a81e02..bcfe3505 100644 --- a/cli/catalogue/catalogue.go +++ b/cli/catalogue/catalogue.go @@ -33,14 +33,7 @@ var catalogueGenerateCommand = cli.Command{ }, Before: internal.SubCommandBefore, Description: ` -Generate a new copy of the recipe catalogue which can be found on: - - https://recipes.coopcloud.tech (website that humans read) - https://recipes.coopcloud.tech/recipes.json (JSON that Abra reads) - -It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository -listing, parses README.md and git tags to produce recipe metadata which is -loaded into the catalogue JSON file. +Generate a new copy of the recipe catalogue. It is possible to generate new metadata for a single recipe by passing . The existing local catalogue will be updated, not overwritten. @@ -51,8 +44,7 @@ If you have a Hub account you can have Abra log you in to avoid this. Pass Push your new release to git.coopcloud.tech with "-p/--publish". This requires that you have permission to git push to these repositories and have your SSH -keys configured on your account. -`, +keys configured on your account.`, ArgsUsage: "[]", BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { @@ -213,11 +205,10 @@ keys configured on your account. // CatalogueCommand defines the `abra catalogue` command and sub-commands. var CatalogueCommand = cli.Command{ - Name: "catalogue", - Usage: "Manage the recipe catalogue", - Aliases: []string{"c"}, - ArgsUsage: "", - Description: "This command helps recipe packagers interact with the recipe catalogue", + Name: "catalogue", + Usage: "Manage the recipe catalogue", + Aliases: []string{"c"}, + ArgsUsage: "", Subcommands: []cli.Command{ catalogueGenerateCommand, }, diff --git a/cli/cli.go b/cli/cli.go index a3dfe4e2..6b971ade 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -25,16 +25,15 @@ import ( var AutoCompleteCommand = cli.Command{ Name: "autocomplete", Aliases: []string{"ac"}, - Usage: "Configure shell autocompletion (recommended)", + Usage: "Configure shell autocompletion", Description: ` -Set up auto-completion in your shell by downloading the relevant files and -laying out what additional information must be loaded. Supported shells are as -follows: bash, fish, fizsh & zsh. +Set up shell auto-completion. -Example: +Supported shells are: bash, fish, fizsh & zsh. - abra autocomplete bash -`, +EXAMPLE: + + abra autocomplete bash`, ArgsUsage: "", Flags: []cli.Flag{ internal.DebugFlag, @@ -81,7 +80,7 @@ Example: switch shellType { case "bash": fmt.Println(fmt.Sprintf(` -# Run the following commands to install auto-completion +# run the following commands to install auto-completion sudo mkdir /etc/bash_completion.d/ sudo cp %s /etc/bash_completion.d/abra echo "source /etc/bash_completion.d/abra" >> ~/.bashrc @@ -89,19 +88,19 @@ echo "source /etc/bash_completion.d/abra" >> ~/.bashrc `, autocompletionFile)) case "zsh": fmt.Println(fmt.Sprintf(` -# Run the following commands to install auto-completion +# run the following commands to install auto-completion sudo mkdir /etc/zsh/completion.d/ sudo cp %s /etc/zsh/completion.d/abra echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc -# To test, run the following: "abra app " - you should see command completion! +# to test, run the following: "abra app " - you should see command completion! `, autocompletionFile)) case "fish": fmt.Println(fmt.Sprintf(` -# Run the following commands to install auto-completion +# run the following commands to install auto-completion sudo mkdir -p /etc/fish/completions sudo cp %s /etc/fish/completions/abra echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish -# To test, run the following: "abra app " - you should see command completion! +# to test, run the following: "abra app " - you should see command completion! `, autocompletionFile)) } @@ -113,14 +112,18 @@ echo "source /etc/fish/completions/abra" >> ~/.config/fish/config.fish var UpgradeCommand = cli.Command{ Name: "upgrade", Aliases: []string{"u"}, - Usage: "Upgrade Abra itself", + Usage: "Upgrade abra", Description: ` -Upgrade Abra in-place with the latest stable or release candidate. +Upgrade abra in-place with the latest stable or release candidate. -Pass "-r/--rc" to install the latest release candidate. Please bear in mind -that it may contain catastrophic bugs. Thank you very much for the testing -efforts! -`, +Use "-r/--rc" to install the latest release candidate. Please bear in mind that +it may contain absolutely catastrophic deal-breaker bugs. Thank you very much +for the testing efforts 💗 + +EXAMPLE: + + abra upgrade + abra upgrade --rc`, Flags: []cli.Flag{internal.RCFlag}, Action: func(c *cli.Context) error { mainURL := "https://install.abra.coopcloud.tech" @@ -144,7 +147,7 @@ efforts! func newAbraApp(version, commit string) *cli.App { app := &cli.App{ Name: "abra", - Usage: `The Co-op Cloud command-line utility belt 🎩🐇 + Usage: `the Co-op Cloud command-line utility belt 🎩🐇 ____ ____ _ _ / ___|___ ___ _ __ / ___| | ___ _ _ __| | | | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' | diff --git a/cli/recipe/diff.go b/cli/recipe/diff.go index fa687650..e53b3e8f 100644 --- a/cli/recipe/diff.go +++ b/cli/recipe/diff.go @@ -5,14 +5,13 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" "github.com/urfave/cli" ) var recipeDiffCommand = cli.Command{ Name: "diff", Usage: "Show unstaged changes in recipe config", - Description: "Due to limitations in our underlying Git dependency, this command requires /usr/bin/git.", + Description: "This command requires /usr/bin/git.", Aliases: []string{"d"}, ArgsUsage: "", Flags: []cli.Flag{ @@ -22,12 +21,7 @@ var recipeDiffCommand = cli.Command{ Before: internal.SubCommandBefore, BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { - recipeName := c.Args().First() - r := recipe.Get(recipeName) - - if recipeName != "" { - internal.ValidateRecipe(c) - } + r := internal.ValidateRecipe(c) if err := gitPkg.DiffUnstaged(r.Dir); err != nil { log.Fatal(err) diff --git a/cli/recipe/new.go b/cli/recipe/new.go index f55335f5..76f53aed 100644 --- a/cli/recipe/new.go +++ b/cli/recipe/new.go @@ -48,12 +48,7 @@ Create a new recipe. Abra uses the built-in example repository which is available here: - https://git.coopcloud.tech/coop-cloud/example - -Files within the example repository make use of the Golang templating system -which Abra uses to inject values into the generated recipe folder (e.g. name of -recipe and domain in the sample environment config). -`, + https://git.coopcloud.tech/coop-cloud/example`, Action: func(c *cli.Context) error { recipeName := c.Args().First() r := recipe.Get(recipeName) @@ -100,22 +95,8 @@ recipe and domain in the sample environment config). log.Fatal(err) } - fmt.Print(fmt.Sprintf(` -Your new %s recipe has been created in %s. - -In order to share your recipe, you can upload it the git repository to: - - https://git.coopcloud.tech/coop-cloud/%s - -If you're not sure how to do that, come chat with us: - - https://docs.coopcloud.tech/intro/contact - -See "abra recipe -h" for additional recipe maintainer commands. - -Happy Hacking! - -`, recipeName, path.Join(r.Dir), recipeName)) + log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)) + log.Info("happy hacking 🎉") return nil }, diff --git a/cli/recipe/recipe.go b/cli/recipe/recipe.go index ad3b6365..ed61f712 100644 --- a/cli/recipe/recipe.go +++ b/cli/recipe/recipe.go @@ -18,9 +18,7 @@ for you. Anyone who uses a recipe can become a maintainer. Maintainers typically make sure the recipe is in good working order and the config upgraded in a timely -manner. Abra supports convenient automation for recipe maintainenace, see the -"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands. -`, +manner.`, Subcommands: []cli.Command{ recipeFetchCommand, recipeLintCommand, diff --git a/cli/recipe/release.go b/cli/recipe/release.go index ab743ced..2bb17d70 100644 --- a/cli/recipe/release.go +++ b/cli/recipe/release.go @@ -45,8 +45,7 @@ major and therefore require intervention while doing the upgrade work. Publish your new release to git.coopcloud.tech with "-p/--publish". This requires that you have permission to git push to these repositories and have -your SSH keys configured on your account. -`, +your SSH keys configured on your account.`, Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, diff --git a/cli/recipe/reset.go b/cli/recipe/reset.go index 915f3382..282fcf66 100644 --- a/cli/recipe/reset.go +++ b/cli/recipe/reset.go @@ -12,7 +12,7 @@ import ( var recipeResetCommand = cli.Command{ Name: "reset", Usage: "Remove all unstaged changes from recipe config", - Description: "WARNING, this will delete your changes. Be Careful.", + Description: "WARNING: this will delete your changes. Be Careful.", Aliases: []string{"rs"}, ArgsUsage: "", Flags: []cli.Flag{ diff --git a/cli/recipe/sync.go b/cli/recipe/sync.go index f7432380..3ab18492 100644 --- a/cli/recipe/sync.go +++ b/cli/recipe/sync.go @@ -37,8 +37,7 @@ named "app") which corresponds to the following format: Where can be specifed on the command-line or Abra can attempt to auto-generate it for you. The configuration will be updated on the -local file system. -`, +local file system.`, BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { recipe := internal.ValidateRecipe(c) diff --git a/cli/recipe/upgrade.go b/cli/recipe/upgrade.go index 4defb186..759d3c5c 100644 --- a/cli/recipe/upgrade.go +++ b/cli/recipe/upgrade.go @@ -53,10 +53,11 @@ The command is interactive and will show a select input which allows you to make a seclection. Use the "?" key to see more help on navigating this interface. -You may invoke this command in "wizard" mode and be prompted for input: +You may invoke this command in "wizard" mode and be prompted for input. - abra recipe upgrade -`, +EXAMPLE: + + abra recipe upgrade`, ArgsUsage: "", Flags: []cli.Flag{ internal.DebugFlag, diff --git a/cli/recipe/version.go b/cli/recipe/version.go index 971ee18a..74efbd34 100644 --- a/cli/recipe/version.go +++ b/cli/recipe/version.go @@ -24,11 +24,10 @@ func sortServiceByName(versions [][]string) func(i, j int) bool { } var recipeVersionCommand = cli.Command{ - Name: "versions", - Aliases: []string{"v"}, - Usage: "List recipe versions", - ArgsUsage: "", - Description: "Versions are read from the recipe catalogue.", + Name: "versions", + Aliases: []string{"v"}, + Usage: "List recipe versions", + ArgsUsage: "", Flags: []cli.Flag{ internal.DebugFlag, internal.OfflineFlag, diff --git a/cli/server/add.go b/cli/server/add.go index 4952b95c..c5592ffd 100644 --- a/cli/server/add.go +++ b/cli/server/add.go @@ -94,7 +94,7 @@ func createServerDir(name string) (bool, error) { var serverAddCommand = cli.Command{ Name: "add", Aliases: []string{"a"}, - Usage: "Add a server to your configuration", + Usage: "Add a new server to your configuration", Description: ` Add a new server to your configuration so that it can be managed by Abra. @@ -121,8 +121,7 @@ You can also pass "--no-domain-checks/-D" flag to use any arbitrary name instead of a real domain. The host will be resolved with the "Hostname" entry of your ~/.ssh/config. Checks for a valid online domain will be skipped: - abra server add -D example -`, + abra server add -D example`, Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, diff --git a/cli/server/prune.go b/cli/server/prune.go index b670b642..54b44ee8 100644 --- a/cli/server/prune.go +++ b/cli/server/prune.go @@ -31,13 +31,12 @@ var volumesFilterFlag = &cli.BoolFlag{ var serverPruneCommand = cli.Command{ Name: "prune", Aliases: []string{"p"}, - Usage: "Prune a managed server; Runs a docker system prune", + Usage: "Prune resources on a server", Description: ` Prunes unused containers, networks, and dangling images. -If passing "-v/--volumes" then volumes not connected to a deployed app will -also be removed. This can result in unwanted data loss if not used carefully. - `, +Use "-v/--volumes" to remove volumes that are not associated with a deployed +app. This can result in unwanted data loss if not used carefully.`, ArgsUsage: "[]", Flags: []cli.Flag{ allFilterFlag, diff --git a/cli/server/remove.go b/cli/server/remove.go index 788a0917..8975cdc4 100644 --- a/cli/server/remove.go +++ b/cli/server/remove.go @@ -17,12 +17,12 @@ var serverRemoveCommand = cli.Command{ Aliases: []string{"rm"}, ArgsUsage: "", Usage: "Remove a managed server", - Description: `Remove a managed server. + Description: ` +Remove a managed server. Abra will remove the internal bookkeeping (~/.abra/servers/...) and underlying client connection context. This server will then be lost in time, like tears in -rain. -`, +rain.`, Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, diff --git a/cli/updater/updater.go b/cli/updater/updater.go index bd5faf40..fd2d321e 100644 --- a/cli/updater/updater.go +++ b/cli/updater/updater.go @@ -54,10 +54,12 @@ var Notify = cli.Command{ }, Before: internal.SubCommandBefore, Description: ` -It reads the deployed app versions and looks for new versions in the recipe -catalogue. If a new patch/minor version is available, a notification is -printed. To include major versions use the --major flag. -`, +Read the deployed app versions and look for new versions in the recipe +catalogue. + +If a new patch/minor version is available, a notification is printed. + +Use "--major" to include new major versions.`, Action: func(c *cli.Context) error { cl, err := client.New("default") if err != nil { @@ -103,14 +105,17 @@ var UpgradeApp = cli.Command{ }, Before: internal.SubCommandBefore, Description: ` -Upgrade an app by specifying its stack name and recipe. By passing "--all" -instead, every deployed app is upgraded. For each apps with enabled auto -updates the deployed version is compared with the current recipe catalogue -version. If a new patch/minor version is available, the app is upgraded. To -include major versions use the "--major" flag. Don't do that, it will probably -break things. Only apps that are not deployed with "--chaos" are upgraded, to -update chaos deployments use the "--chaos" flag. Use it with care. -`, +Upgrade an app by specifying stack name and recipe. + +Use "--all" to upgrade every deployed app. + +For each app with auto updates enabled, the deployed version is compared with +the current recipe catalogue version. If a new patch/minor version is +available, the app is upgraded. + +To include major versions use the "--major" flag. You probably don't want that +as it will break things. Only apps that are not deployed with "--chaos" are +upgraded, to update chaos deployments use the "--chaos" flag. Use it with care.`, Action: func(c *cli.Context) error { cl, err := client.New("default") if err != nil { @@ -327,7 +332,7 @@ func processRecipeRepoVersion(r recipe.Recipe, version string) error { return err } - if err := r.EnsureVersion(version); err != nil { + if _, err := r.EnsureVersion(version); err != nil { return err } diff --git a/pkg/recipe/git.go b/pkg/recipe/git.go index 0d6ddc63..95cb87c9 100644 --- a/pkg/recipe/git.go +++ b/pkg/recipe/git.go @@ -57,21 +57,23 @@ func (r Recipe) EnsureExists() error { } // EnsureVersion checks whether a specific version exists for a recipe. -func (r Recipe) EnsureVersion(version string) error { +func (r Recipe) EnsureVersion(version string) (bool, error) { + isChaosCommit := false + recipeDir := path.Join(config.RECIPES_DIR, r.Name) if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { - return err + return isChaosCommit, err } repo, err := git.PlainOpen(recipeDir) if err != nil { - return err + return isChaosCommit, err } tags, err := repo.Tags() if err != nil { - return nil + return isChaosCommit, err } var parsedTags []string @@ -83,7 +85,7 @@ func (r Recipe) EnsureVersion(version string) error { } return nil }); err != nil { - return err + return isChaosCommit, err } joinedTags := strings.Join(parsedTags, ", ") @@ -91,27 +93,33 @@ func (r Recipe) EnsureVersion(version string) error { log.Debugf("read %s as tags for recipe %s", joinedTags, r.Name) } + var opts *git.CheckoutOptions if tagRef.String() == "" { - return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", r.Name, version) + log.Debugf("attempting to checkout '%s' as chaos commit", version) + + hash, err := repo.ResolveRevision(plumbing.Revision(version)) + if err != nil { + log.Fatalf("unable to resolve '%s': %s", version, err) + } + + opts = &git.CheckoutOptions{Hash: *hash, Create: false, Force: true} + isChaosCommit = true + } else { + opts = &git.CheckoutOptions{Branch: tagRef, Create: false, Force: true} } worktree, err := repo.Worktree() if err != nil { - return err + return isChaosCommit, nil } - opts := &git.CheckoutOptions{ - Branch: tagRef, - Create: false, - Force: true, - } if err := worktree.Checkout(opts); err != nil { - return err + return isChaosCommit, nil } log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), recipeDir) - return nil + return isChaosCommit, nil } // EnsureIsClean makes sure that the recipe repository has no unstaged changes. diff --git a/pkg/upstream/stack/stack.go b/pkg/upstream/stack/stack.go index a7c0fb5f..8576f61f 100644 --- a/pkg/upstream/stack/stack.go +++ b/pkg/upstream/stack/stack.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "os" "os/signal" + "strconv" "strings" "time" @@ -101,7 +102,7 @@ func GetDeployedServicesByName(ctx context.Context, cl *dockerClient.Client, sta type DeployMeta struct { IsDeployed bool // whether the app is deployed or not Version string // the deployed version - IsChaos string // whether or not the deployment is --chaos + IsChaos bool // whether or not the deployment is --chaos ChaosVersion string // the --chaos deployment version } @@ -110,7 +111,7 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) deployMeta := DeployMeta{ IsDeployed: false, Version: "unknown", - IsChaos: "false", // NOTE(d1): match string type used on label + IsChaos: false, ChaosVersion: "false", // NOTE(d1): match string type used on label } @@ -137,7 +138,11 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string) labelKey = fmt.Sprintf("coop-cloud.%s.chaos", stackName) if isChaos, ok := service.Spec.Labels[labelKey]; ok { - deployMeta.IsChaos = isChaos + boolVal, err := strconv.ParseBool(isChaos) + if err != nil { + return deployMeta, fmt.Errorf("unable to parse '%s' value as bool: %s", labelKey, err) + } + deployMeta.IsChaos = boolVal } labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", stackName) diff --git a/tests/integration/app_deploy.bats b/tests/integration/app_deploy.bats index 541c8405..57d0ad07 100644 --- a/tests/integration/app_deploy.bats +++ b/tests/integration/app_deploy.bats @@ -356,3 +356,12 @@ teardown(){ run $ABRA app secret rm "$TEST_APP_DOMAIN" --all --chaos assert_success } + +# bats test_tags=slow +@test "deploy chaos commit" { + tagHash=$(_get_tag_hash "0.1.0+1.20.0") + + run $ABRA app deploy "$TEST_APP_DOMAIN" "$tagHash" --no-input --no-converge-checks + assert_success + assert_output --partial 'chaos mode' +} diff --git a/tests/integration/app_rollback.bats b/tests/integration/app_rollback.bats index 0ee13a5e..f1ec824d 100644 --- a/tests/integration/app_rollback.bats +++ b/tests/integration/app_rollback.bats @@ -111,6 +111,26 @@ teardown(){ assert_output --partial "0.1.0+1.20.0" } +# bats test_tags=slow +@test "force rollback to previous version" { + run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks + assert_success + assert_output --partial '0.1.1+1.20.2' + + run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks + assert_success + assert_output --partial "0.1.0+1.20.0" + + run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks + assert_failure + assert_output --partial "not a downgrade" + + run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \ + --no-input --no-converge-checks --force + assert_success + assert_output --partial "0.1.0+1.20.0" +} + # bats test_tags=slow @test "rollback to a version 2 tags behind" { run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks @@ -145,3 +165,14 @@ teardown(){ refute_output --partial "${tagHash:0:8}" assert_output --partial "false" } + +# bats test_tags=slow +@test "chaos commit rollback not possible" { + _deploy_app + + tagHash=$(_get_tag_hash "0.2.0+1.21.0") + + run $ABRA app rollback "$TEST_APP_DOMAIN" "$tagHash" --no-input --no-converge-checks + assert_failure + assert_output --partial "not a known version" +} diff --git a/tests/integration/app_upgrade.bats b/tests/integration/app_upgrade.bats index 4ca59f32..4bbfd96c 100644 --- a/tests/integration/app_upgrade.bats +++ b/tests/integration/app_upgrade.bats @@ -97,6 +97,26 @@ teardown(){ assert_output --partial '0.2.0+1.21.0' } +# bats test_tags=slow +@test "force upgrade specific version" { + run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks + assert_success + assert_output --partial '0.1.0+1.20.0' + + run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks + assert_success + assert_output --partial '0.2.0+1.21.0' + + run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks + assert_failure + assert_output --partial 'not an upgrade' + + run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" \ + --no-input --no-converge-checks --force + assert_success + assert_output --partial '0.2.0+1.21.0' +} + # bats test_tags=slow @test "upgrade to latest" { run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks @@ -189,3 +209,15 @@ teardown(){ refute_output --partial "${tagHash:0:8}" assert_output --partial "false" } + +@test "chaos commit upgrade not possible" { + run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks + assert_success + assert_output --partial '0.1.0+1.20.0' + + tagHash=$(_get_tag_hash "0.2.0+1.21.0") + + run $ABRA app upgrade "$TEST_APP_DOMAIN" "$tagHash" --no-input --no-converge-checks + assert_failure + assert_output --partial "not a known version" +} diff --git a/tests/integration/recipe_new.bats b/tests/integration/recipe_new.bats index 3a269be6..b0e74faf 100644 --- a/tests/integration/recipe_new.bats +++ b/tests/integration/recipe_new.bats @@ -16,6 +16,14 @@ setup(){ } teardown(){ + if [[ -f "$ABRA_DIR/servers/$TEST_SERVER/foobar.$TEST_SERVER.env" ]]; then + run $ABRA app undeploy "foobar.$TEST_SERVER" --no-input + assert_success + + run $ABRA app rm "foobar.$TEST_SERVER" --no-input + assert_success + fi + if [[ -d "$ABRA_DIR/recipes/foobar" ]]; then run rm -rf "$ABRA_DIR/recipes/foobar" assert_success @@ -25,26 +33,32 @@ teardown(){ @test "create new recipe" { run $ABRA recipe new foobar assert_success - assert_output --partial 'Your new foobar recipe has been created' + assert_output --partial "new recipe 'foobar' created" assert_exists "$ABRA_DIR/recipes/foobar" } +# bats test_tags=slow @test "create new app from new recipe" { run $ABRA recipe new foobar assert_success + assert_exists "$ABRA_DIR/recipes/foobar" run $ABRA app new foobar \ --no-input \ --server "$TEST_SERVER" \ --domain "foobar.$TEST_SERVER" assert_success - assert_output --partial 'A new foobar app has been created!' + 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" { run $ABRA recipe new foobar --git-name fooUser --git-email foo@example.com assert_success - assert_output --partial 'Your new foobar recipe has been created' + assert_output --partial "new recipe 'foobar' created" assert_exists "$ABRA_DIR/recipes/foobar" run bash -c 'git -C "$ABRA_DIR/recipes/foobar" log -n 1'