diff --git a/cli/app/deploy.go b/cli/app/deploy.go index a984f70e..e8bbde75 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -1,8 +1,28 @@ package app import ( + "context" + "fmt" + "io/ioutil" + "os" + "path" + "strings" + "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" + + "coopcloud.tech/abra/pkg/client" + "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/dns" + "coopcloud.tech/abra/pkg/formatter" + "coopcloud.tech/abra/pkg/git" + "coopcloud.tech/abra/pkg/lint" + "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/abra/pkg/runtime" + "coopcloud.tech/abra/pkg/upstream/stack" + "github.com/AlecAivazis/survey/v2" + dockerClient "github.com/docker/docker/client" + "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -31,6 +51,326 @@ Chas 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. `, - Action: internal.DeployAction, BashComplete: autocomplete.AppNameComplete, + Action: func(c *cli.Context) error { + app := internal.ValidateApp(c) + stackName := app.StackName() + conf := runtime.New() + + cl, err := client.New(app.Server) + if err != nil { + logrus.Fatal(err) + } + + if !internal.Chaos { + if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil { + logrus.Fatal(err) + } + } + + r, err := recipe.Get(app.Recipe, conf) + if err != nil { + logrus.Fatal(err) + } + + if err := lint.LintForErrors(r); err != nil { + logrus.Fatal(err) + } + + logrus.Debugf("checking whether %s is already deployed", stackName) + + isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName) + if err != nil { + logrus.Fatal(err) + } + + if isDeployed { + if internal.Force || internal.Chaos { + logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name) + } else { + logrus.Fatalf("%s is already deployed", app.Name) + } + } + + version := deployedVersion + if version == "unknown" && !internal.Chaos { + catl, err := recipe.ReadRecipeCatalogue() + if err != nil { + logrus.Fatal(err) + } + versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl) + if err != nil { + logrus.Fatal(err) + } + if len(versions) > 0 { + version = versions[len(versions)-1] + logrus.Debugf("choosing %s as version to deploy", version) + if err := recipe.EnsureVersion(app.Recipe, version); err != nil { + logrus.Fatal(err) + } + } else { + head, err := git.GetRecipeHead(app.Recipe) + if err != nil { + logrus.Fatal(err) + } + version = formatter.SmallSHA(head.String()) + logrus.Warn("no versions detected, using latest commit") + if err := recipe.EnsureLatest(app.Recipe, conf); err != nil { + logrus.Fatal(err) + } + } + } + + if version == "unknown" && !internal.Chaos { + logrus.Debugf("choosing %s as version to deploy", version) + if err := recipe.EnsureVersion(app.Recipe, version); err != nil { + logrus.Fatal(err) + } + } + + if version != "unknown" && !internal.Chaos { + if err := recipe.EnsureVersion(app.Recipe, version); err != nil { + logrus.Fatal(err) + } + } + + if internal.Chaos { + logrus.Warnf("chaos mode engaged") + var err error + version, err = recipe.ChaosVersion(app.Recipe) + if err != nil { + logrus.Fatal(err) + } + } + + abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") + abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) + if err != nil { + logrus.Fatal(err) + } + for k, v := range abraShEnv { + app.Env[k] = v + } + + composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env) + if err != nil { + logrus.Fatal(err) + } + deployOpts := stack.Deploy{ + Composefiles: composeFiles, + Namespace: stackName, + Prune: false, + ResolveImage: stack.ResolveImageAlways, + } + compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env) + if err != nil { + logrus.Fatal(err) + } + config.ExposeAllEnv(stackName, compose, app.Env) + config.SetRecipeLabel(compose, stackName, app.Recipe) + config.SetChaosLabel(compose, stackName, internal.Chaos) + config.SetChaosVersionLabel(compose, stackName, version) + config.SetUpdateLabel(compose, stackName, app.Env) + + if err := DeployOverview(app, version, "continue with deployment?"); err != nil { + logrus.Fatal(err) + } + + if !internal.NoDomainChecks { + domainName, ok := app.Env["DOMAIN"] + if ok { + if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil { + logrus.Fatal(err) + } + } else { + logrus.Warn("skipping domain checks as no DOMAIN=... configured for app") + } + } else { + logrus.Warn("skipping domain checks as requested") + } + + stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName) + if err != nil { + logrus.Fatal(err) + } + logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout) + + if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil { + logrus.Fatal(err) + } + + postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"] + if ok && !internal.DontWaitConverge { + logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds) + if err := PostCmds(cl, app, postDeployCmds); err != nil { + logrus.Fatalf("attempting to run post deploy commands, saw: %s", err) + } + } + return nil + }, +} + +// PostCmds parses a string of commands and executes them inside of the respective services +// the commands string must have the following format: +// " | |... " +func PostCmds(cl *dockerClient.Client, app config.App, commands string) error { + abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh") + if _, err := os.Stat(abraSh); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name)) + } + return err + } + + for _, command := range strings.Split(commands, "|") { + commandParts := strings.Split(command, " ") + if len(commandParts) < 2 { + return fmt.Errorf(fmt.Sprintf("not enough arguments: %s", command)) + } + targetServiceName := commandParts[0] + cmdName := commandParts[1] + parsedCmdArgs := "" + if len(commandParts) > 2 { + parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " ")) + } + logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName) + + if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil { + return err + } + + serviceNames, err := config.GetAppServiceNames(app.Name) + if err != nil { + return err + } + + matchingServiceName := false + for _, serviceName := range serviceNames { + if serviceName == targetServiceName { + matchingServiceName = true + } + } + + if !matchingServiceName { + return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name)) + } + + logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName) + + internal.Tty = true + if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil { + return err + } + } + return nil +} + +// DeployOverview shows a deployment overview +func DeployOverview(app config.App, version, message string) error { + tableCol := []string{"server", "recipe", "config", "domain", "version"} + table := formatter.CreateTable(tableCol) + + deployConfig := "compose.yml" + if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { + deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") + } + + server := app.Server + if app.Server == "default" { + server = "local" + } + + table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version}) + table.Render() + + if internal.NoInput { + return nil + } + + response := false + prompt := &survey.Confirm{ + Message: message, + } + + if err := survey.AskOne(prompt, &response); err != nil { + return err + } + + if !response { + logrus.Fatal("exiting as requested") + } + + return nil +} + +// NewVersionOverview shows an upgrade or downgrade overview +func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error { + tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"} + table := formatter.CreateTable(tableCol) + + deployConfig := "compose.yml" + if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { + deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") + } + + server := app.Server + if app.Server == "default" { + server = "local" + } + + table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion}) + table.Render() + + if releaseNotes == "" { + var err error + releaseNotes, err = GetReleaseNotes(app.Recipe, newVersion) + if err != nil { + return err + } + } + + if releaseNotes != "" && newVersion != "" { + fmt.Println() + fmt.Println(fmt.Sprintf("%s release notes:\n\n%s", newVersion, releaseNotes)) + } else { + logrus.Warnf("no release notes available for %s", newVersion) + } + + if internal.NoInput { + return nil + } + + response := false + prompt := &survey.Confirm{ + Message: "continue with deployment?", + } + + if err := survey.AskOne(prompt, &response); err != nil { + return err + } + + if !response { + logrus.Fatal("exiting as requested") + } + + return nil +} + +// GetReleaseNotes prints release notes for a recipe version +func GetReleaseNotes(recipeName, version string) (string, error) { + if version == "" { + return "", nil + } + + fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version) + + if _, err := os.Stat(fpath); !os.IsNotExist(err) { + releaseNotes, err := ioutil.ReadFile(fpath) + if err != nil { + return "", err + } + return string(releaseNotes), nil + } + + return "", nil } diff --git a/cli/app/rollback.go b/cli/app/rollback.go index 756d4729..4d537310 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -184,7 +184,7 @@ recipes. config.SetChaosVersionLabel(compose, stackName, chosenDowngrade) config.SetUpdateLabel(compose, stackName, app.Env) - if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil { + if err := NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil { logrus.Fatal(err) } diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go index 0aa4e72a..0bcc1afd 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -117,7 +117,7 @@ Passing "-p/--prune" does not remove those volumes. logrus.Fatalf("%s is not deployed?", app.Name) } - if err := internal.DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil { + if err := DeployOverview(app, deployedVersion, "continue with undeploy?"); err != nil { logrus.Fatal(err) } diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index 3c39baf7..9e0ef6be 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -151,7 +151,7 @@ recipes. // if release notes written after git tag published, read them before we // check out the tag and then they'll appear to be missing. this covers // when we obviously will forget to write release notes before publishing - releaseNotes, err := internal.GetReleaseNotes(app.Recipe, chosenUpgrade) + releaseNotes, err := GetReleaseNotes(app.Recipe, chosenUpgrade) if err != nil { return err } @@ -200,7 +200,7 @@ recipes. config.SetChaosVersionLabel(compose, stackName, chosenUpgrade) config.SetUpdateLabel(compose, stackName, app.Env) - if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil { + if err := NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil { logrus.Fatal(err) } @@ -217,7 +217,7 @@ recipes. postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"] if ok && !internal.DontWaitConverge { logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds) - if err := internal.PostCmds(cl, app, postDeployCmds); err != nil { + if err := PostCmds(cl, app, postDeployCmds); err != nil { logrus.Fatalf("attempting to run post deploy commands, saw: %s", err) } } diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go deleted file mode 100644 index e86e896b..00000000 --- a/cli/internal/deploy.go +++ /dev/null @@ -1,348 +0,0 @@ -package internal - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path" - "strings" - - "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/config" - "coopcloud.tech/abra/pkg/dns" - "coopcloud.tech/abra/pkg/formatter" - "coopcloud.tech/abra/pkg/git" - "coopcloud.tech/abra/pkg/lint" - "coopcloud.tech/abra/pkg/recipe" - "coopcloud.tech/abra/pkg/runtime" - "coopcloud.tech/abra/pkg/upstream/stack" - "github.com/AlecAivazis/survey/v2" - dockerClient "github.com/docker/docker/client" - "github.com/sirupsen/logrus" - "github.com/urfave/cli" -) - -// DeployAction is the main command-line action for this package -func DeployAction(c *cli.Context) error { - app := ValidateApp(c) - stackName := app.StackName() - conf := runtime.New() - - cl, err := client.New(app.Server) - if err != nil { - logrus.Fatal(err) - } - - if !Chaos { - if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil { - logrus.Fatal(err) - } - } - - r, err := recipe.Get(app.Recipe, conf) - if err != nil { - logrus.Fatal(err) - } - - if err := lint.LintForErrors(r); err != nil { - logrus.Fatal(err) - } - - logrus.Debugf("checking whether %s is already deployed", stackName) - - isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName) - if err != nil { - logrus.Fatal(err) - } - - if isDeployed { - if Force || Chaos { - logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", app.Name) - } else { - logrus.Fatalf("%s is already deployed", app.Name) - } - } - - version := deployedVersion - if version == "unknown" && !Chaos { - catl, err := recipe.ReadRecipeCatalogue() - if err != nil { - logrus.Fatal(err) - } - versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl) - if err != nil { - logrus.Fatal(err) - } - if len(versions) > 0 { - version = versions[len(versions)-1] - logrus.Debugf("choosing %s as version to deploy", version) - if err := recipe.EnsureVersion(app.Recipe, version); err != nil { - logrus.Fatal(err) - } - } else { - head, err := git.GetRecipeHead(app.Recipe) - if err != nil { - logrus.Fatal(err) - } - version = formatter.SmallSHA(head.String()) - logrus.Warn("no versions detected, using latest commit") - if err := recipe.EnsureLatest(app.Recipe, conf); err != nil { - logrus.Fatal(err) - } - } - } - - if version == "unknown" && !Chaos { - logrus.Debugf("choosing %s as version to deploy", version) - if err := recipe.EnsureVersion(app.Recipe, version); err != nil { - logrus.Fatal(err) - } - } - - if version != "unknown" && !Chaos { - if err := recipe.EnsureVersion(app.Recipe, version); err != nil { - logrus.Fatal(err) - } - } - - if Chaos { - logrus.Warnf("chaos mode engaged") - var err error - version, err = recipe.ChaosVersion(app.Recipe) - if err != nil { - logrus.Fatal(err) - } - } - - abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") - abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) - if err != nil { - logrus.Fatal(err) - } - for k, v := range abraShEnv { - app.Env[k] = v - } - - composeFiles, err := config.GetAppComposeFiles(app.Recipe, app.Env) - if err != nil { - logrus.Fatal(err) - } - deployOpts := stack.Deploy{ - Composefiles: composeFiles, - Namespace: stackName, - Prune: false, - ResolveImage: stack.ResolveImageAlways, - } - compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env) - if err != nil { - logrus.Fatal(err) - } - config.ExposeAllEnv(stackName, compose, app.Env) - config.SetRecipeLabel(compose, stackName, app.Recipe) - config.SetChaosLabel(compose, stackName, Chaos) - config.SetChaosVersionLabel(compose, stackName, version) - config.SetUpdateLabel(compose, stackName, app.Env) - - if err := DeployOverview(app, version, "continue with deployment?"); err != nil { - logrus.Fatal(err) - } - - if !NoDomainChecks { - domainName, ok := app.Env["DOMAIN"] - if ok { - if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil { - logrus.Fatal(err) - } - } else { - logrus.Warn("skipping domain checks as no DOMAIN=... configured for app") - } - } else { - logrus.Warn("skipping domain checks as requested") - } - - stack.WaitTimeout, err = config.GetTimeoutFromLabel(compose, stackName) - if err != nil { - logrus.Fatal(err) - } - logrus.Debugf("set waiting timeout to %d s", stack.WaitTimeout) - - if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, DontWaitConverge); err != nil { - logrus.Fatal(err) - } - - postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"] - if ok && !DontWaitConverge { - logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds) - if err := PostCmds(cl, app, postDeployCmds); err != nil { - logrus.Fatalf("attempting to run post deploy commands, saw: %s", err) - } - } - - return nil -} - -// PostCmds parses a string of commands and executes them inside of the respective services -// the commands string must have the following format: -// " | |... " -func PostCmds(cl *dockerClient.Client, app config.App, commands string) error { - abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh") - if _, err := os.Stat(abraSh); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name)) - } - return err - } - - for _, command := range strings.Split(commands, "|") { - commandParts := strings.Split(command, " ") - if len(commandParts) < 2 { - return fmt.Errorf(fmt.Sprintf("not enough arguments: %s", command)) - } - targetServiceName := commandParts[0] - cmdName := commandParts[1] - parsedCmdArgs := "" - if len(commandParts) > 2 { - parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " ")) - } - logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName) - - if err := EnsureCommand(abraSh, app.Recipe, cmdName); err != nil { - return err - } - - serviceNames, err := config.GetAppServiceNames(app.Name) - if err != nil { - return err - } - - matchingServiceName := false - for _, serviceName := range serviceNames { - if serviceName == targetServiceName { - matchingServiceName = true - } - } - - if !matchingServiceName { - return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name)) - } - - logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName) - - Tty = true - if err := RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil { - return err - } - } - return nil -} - -// DeployOverview shows a deployment overview -func DeployOverview(app config.App, version, message string) error { - tableCol := []string{"server", "recipe", "config", "domain", "version"} - table := formatter.CreateTable(tableCol) - - deployConfig := "compose.yml" - if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { - deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") - } - - server := app.Server - if app.Server == "default" { - server = "local" - } - - table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version}) - table.Render() - - if NoInput { - return nil - } - - response := false - prompt := &survey.Confirm{ - Message: message, - } - - if err := survey.AskOne(prompt, &response); err != nil { - return err - } - - if !response { - logrus.Fatal("exiting as requested") - } - - return nil -} - -// NewVersionOverview shows an upgrade or downgrade overview -func NewVersionOverview(app config.App, currentVersion, newVersion, releaseNotes string) error { - tableCol := []string{"server", "recipe", "config", "domain", "current version", "to be deployed"} - table := formatter.CreateTable(tableCol) - - deployConfig := "compose.yml" - if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { - deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") - } - - server := app.Server - if app.Server == "default" { - server = "local" - } - - table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion}) - table.Render() - - if releaseNotes == "" { - var err error - releaseNotes, err = GetReleaseNotes(app.Recipe, newVersion) - if err != nil { - return err - } - } - - if releaseNotes != "" && newVersion != "" { - fmt.Println() - fmt.Println(fmt.Sprintf("%s release notes:\n\n%s", newVersion, releaseNotes)) - } else { - logrus.Warnf("no release notes available for %s", newVersion) - } - - if NoInput { - return nil - } - - response := false - prompt := &survey.Confirm{ - Message: "continue with deployment?", - } - - if err := survey.AskOne(prompt, &response); err != nil { - return err - } - - if !response { - logrus.Fatal("exiting as requested") - } - - return nil -} - -// GetReleaseNotes prints release notes for a recipe version -func GetReleaseNotes(recipeName, version string) (string, error) { - if version == "" { - return "", nil - } - - fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version) - - if _, err := os.Stat(fpath); !os.IsNotExist(err) { - releaseNotes, err := ioutil.ReadFile(fpath) - if err != nil { - return "", err - } - return string(releaseNotes), nil - } - - return "", nil -}