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" ) 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() // horizontal is a JoinHorizontal helper function. func horizontal(left, mid, right string) string { return lipgloss.JoinHorizontal(lipgloss.Left, left, mid, right) } // NewVersionOverview shows an upgrade or downgrade overview func NewVersionOverview( app appPkg.App, warnMessages []string, kind, deployedVersion, deployedChaosVersion, toDeployVersion, releaseNotes string) error { deployConfig := "compose.yml" if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { deployConfig = composeFiles } server := app.Server if app.Server == "default" { server = "local" } domain := app.Domain if domain == "" { domain = config.NO_DOMAIN_DEFAULT } upperKind := strings.ToUpper(kind) rows := [][]string{ {"APP", domain}, {"RECIPE", app.Recipe.Name}, {"SERVER", server}, {"CONFIG", deployConfig}, {"CURRENT DEPLOYMENT", "---"}, {"VERSION", deployedVersion}, {"CHAOS ", deployedChaosVersion}, {upperKind, "---"}, {"VERSION", toDeployVersion}, {fmt.Sprintf("%s.ENV", strings.ToUpper(app.Domain)), "---"}, {"CURRENT VERSION", app.Recipe.EnvVersion}, {"NEW VERSION", toDeployVersion}, } overview := formatter.CreateOverview( fmt.Sprintf("%s OVERVIEW", upperKind), rows, ) fmt.Println(overview) if releaseNotes != "" && toDeployVersion != "" { fmt.Print(releaseNotes) } else { warnMessages = append( warnMessages, fmt.Sprintf("no release notes available for %s", toDeployVersion), ) } for _, msg := range warnMessages { log.Warn(msg) } 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("deployment cancelled") } return nil } // DeployOverview shows a deployment overview func DeployOverview( app appPkg.App, warnMessages []string, deployedVersion string, deployedChaosVersion string, toDeployVersion, toDeployChaosVersion string, toWriteVersion string, ) error { deployConfig := "compose.yml" if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { deployConfig = composeFiles } server := app.Server if app.Server == "default" { server = "local" } domain := app.Domain if domain == "" { domain = config.NO_DOMAIN_DEFAULT } deployedChaosVersion = formatter.BoldDirtyDefault(deployedChaosVersion) if app.Recipe.Dirty { toDeployChaosVersion = formatter.BoldDirtyDefault(toDeployChaosVersion) } rows := [][]string{ {"APP", domain}, {"RECIPE", app.Recipe.Name}, {"SERVER", server}, {"CONFIG", deployConfig}, {"CURRENT DEPLOYMENT", "---"}, {"VERSION", deployedVersion}, {"CHAOS", deployedChaosVersion}, {"NEW DEPLOYMENT", "---"}, {"VERSION", toDeployVersion}, {"CHAOS", toDeployChaosVersion}, {fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"}, {"CURRENT VERSION", app.Recipe.EnvVersion}, {"NEW VERSION", toWriteVersion}, } overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows) fmt.Println(overview) for _, msg := range warnMessages { log.Warn(msg) } 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("deployment cancelled") } return nil } // UndeployOverview shows an undeployment overview func UndeployOverview( app appPkg.App, version, chaosVersion string) error { deployConfig := "compose.yml" if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { deployConfig = composeFiles } server := app.Server if app.Server == "default" { server = "local" } domain := app.Domain if domain == "" { domain = config.NO_DOMAIN_DEFAULT } if app.Recipe.Dirty { chaosVersion = formatter.BoldDirtyDefault(chaosVersion) } rows := [][]string{ {"APP", domain}, {"RECIPE", app.Recipe.Name}, {"SERVER", server}, {"CONFIG", deployConfig}, {"CURRENT DEPLOYMENT", "---"}, {"DEPLOYED", version}, {"CHAOS", chaosVersion}, {fmt.Sprintf("%s.ENV", strings.ToUpper(app.Name)), "---"}, {"CURRENT VERSION", app.Recipe.EnvVersion}, {"NEW VERSION", version}, } 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: // " | |... " func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { if _, err := os.Stat(app.Recipe.AbraShPath); err != nil { if os.IsNotExist(err) { return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", app.Recipe.AbraShPath, 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:], " ")) } log.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName) if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { return err } serviceNames, err := appPkg.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)) } log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName) requestTTY := true if err := RunCmdRemote( cl, app, requestTTY, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs, ""); err != nil { return err } } 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 }