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 }