305 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package internal
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"os"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	appPkg "coopcloud.tech/abra/pkg/app"
 | 
						|
	"coopcloud.tech/abra/pkg/config"
 | 
						|
	"coopcloud.tech/abra/pkg/formatter"
 | 
						|
	"coopcloud.tech/abra/pkg/i18n"
 | 
						|
	"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)
 | 
						|
}
 | 
						|
 | 
						|
func formatComposeFiles(composeFiles string) string {
 | 
						|
	return strings.ReplaceAll(composeFiles, ":", "\n")
 | 
						|
}
 | 
						|
 | 
						|
// DeployOverview shows a deployment overview
 | 
						|
func DeployOverview(
 | 
						|
	app appPkg.App,
 | 
						|
	deployedVersion string,
 | 
						|
	toDeployVersion string,
 | 
						|
	releaseNotes string,
 | 
						|
	warnMessages []string,
 | 
						|
	secrets string,
 | 
						|
	configs string,
 | 
						|
	images string,
 | 
						|
) error {
 | 
						|
	deployConfig := "compose.yml"
 | 
						|
	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok {
 | 
						|
		deployConfig = formatComposeFiles(composeFiles)
 | 
						|
	}
 | 
						|
 | 
						|
	server := app.Server
 | 
						|
	if app.Server == "default" {
 | 
						|
		server = "local"
 | 
						|
	}
 | 
						|
 | 
						|
	domain := app.Domain
 | 
						|
	if domain == "" {
 | 
						|
		domain = config.NO_DOMAIN_DEFAULT
 | 
						|
	}
 | 
						|
 | 
						|
	envVersion := app.Recipe.EnvVersionRaw
 | 
						|
	if envVersion == "" {
 | 
						|
		envVersion = config.NO_VERSION_DEFAULT
 | 
						|
	}
 | 
						|
 | 
						|
	rows := [][]string{
 | 
						|
		{i18n.G("DOMAIN"), domain},
 | 
						|
		{i18n.G("RECIPE"), app.Recipe.Name},
 | 
						|
		{i18n.G("SERVER"), server},
 | 
						|
		{i18n.G("CONFIG"), deployConfig},
 | 
						|
		{"", ""},
 | 
						|
		{i18n.G("CURRENT DEPLOYMENT"), formatter.BoldDirtyDefault(deployedVersion)},
 | 
						|
		{i18n.G("ENV VERSION"), formatter.BoldDirtyDefault(envVersion)},
 | 
						|
		{i18n.G("NEW DEPLOYMENT"), formatter.BoldDirtyDefault(toDeployVersion)},
 | 
						|
		{"", ""},
 | 
						|
		{i18n.G("IMAGES"), images},
 | 
						|
	}
 | 
						|
 | 
						|
	if len(secrets) > 0 {
 | 
						|
		secretsRows := [][]string{
 | 
						|
			{"", ""},
 | 
						|
			{i18n.G("SECRETS"), secrets},
 | 
						|
		}
 | 
						|
		rows = append(rows, secretsRows...)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(configs) > 0 {
 | 
						|
		configsRows := [][]string{
 | 
						|
			{"", ""},
 | 
						|
			{i18n.G("CONFIGS"), configs},
 | 
						|
		}
 | 
						|
		rows = append(rows, configsRows...)
 | 
						|
	}
 | 
						|
 | 
						|
	deployType := getDeployType(deployedVersion, toDeployVersion)
 | 
						|
	overview := formatter.CreateOverview(i18n.G("%s OVERVIEW", deployType), rows)
 | 
						|
 | 
						|
	fmt.Println(overview)
 | 
						|
 | 
						|
	if releaseNotes != "" {
 | 
						|
		fmt.Print(releaseNotes)
 | 
						|
	}
 | 
						|
 | 
						|
	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(i18n.G("deployment cancelled"))
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func getDeployType(currentVersion, newVersion string) string {
 | 
						|
	if newVersion == config.NO_DOMAIN_DEFAULT {
 | 
						|
		return i18n.G("UNDEPLOY")
 | 
						|
	}
 | 
						|
	if strings.Contains(newVersion, "+U") {
 | 
						|
		return i18n.G("CHAOS DEPLOY")
 | 
						|
	}
 | 
						|
	if strings.Contains(currentVersion, "+U") {
 | 
						|
		return i18n.G("UNCHAOS DEPLOY")
 | 
						|
	}
 | 
						|
	if currentVersion == newVersion {
 | 
						|
		return ("REDEPLOY")
 | 
						|
	}
 | 
						|
	if currentVersion == config.NO_VERSION_DEFAULT {
 | 
						|
		return i18n.G("NEW DEPLOY")
 | 
						|
	}
 | 
						|
	currentParsed, err := tagcmp.Parse(currentVersion)
 | 
						|
	if err != nil {
 | 
						|
		return i18n.G("DEPLOY")
 | 
						|
	}
 | 
						|
	newParsed, err := tagcmp.Parse(newVersion)
 | 
						|
	if err != nil {
 | 
						|
		return i18n.G("DEPLOY")
 | 
						|
	}
 | 
						|
	if currentParsed.IsLessThan(newParsed) {
 | 
						|
		return i18n.G("UPGRADE")
 | 
						|
	}
 | 
						|
	return i18n.G("DOWNGRADE")
 | 
						|
}
 | 
						|
 | 
						|
// MoveOverview shows a overview before moving an app to a different server
 | 
						|
func MoveOverview(
 | 
						|
	app appPkg.App,
 | 
						|
	newServer string,
 | 
						|
	secrets []string,
 | 
						|
	volumes []string,
 | 
						|
) {
 | 
						|
	server := app.Server
 | 
						|
	if app.Server == "default" {
 | 
						|
		server = "local"
 | 
						|
	}
 | 
						|
 | 
						|
	domain := app.Domain
 | 
						|
	if domain == "" {
 | 
						|
		domain = config.NO_DOMAIN_DEFAULT
 | 
						|
	}
 | 
						|
 | 
						|
	secretsOverview := strings.Join(secrets, "\n")
 | 
						|
	if len(secrets) == 0 {
 | 
						|
		secretsOverview = config.NO_SECRETS_DEFAULT
 | 
						|
	}
 | 
						|
 | 
						|
	volumesOverview := strings.Join(volumes, "\n")
 | 
						|
	if len(volumes) == 0 {
 | 
						|
		volumesOverview = config.NO_VOLUMES_DEFAULT
 | 
						|
	}
 | 
						|
 | 
						|
	rows := [][]string{
 | 
						|
		{i18n.G("DOMAIN"), domain},
 | 
						|
		{i18n.G("RECIPE"), app.Recipe.Name},
 | 
						|
		{i18n.G("OLD SERVER"), server},
 | 
						|
		{i18n.G("NEW SERVER"), newServer},
 | 
						|
		{i18n.G("SECRETS"), secretsOverview},
 | 
						|
		{i18n.G("VOLUMES"), volumesOverview},
 | 
						|
	}
 | 
						|
 | 
						|
	overview := formatter.CreateOverview(i18n.G("MOVE OVERVIEW"), rows)
 | 
						|
 | 
						|
	fmt.Println(overview)
 | 
						|
}
 | 
						|
 | 
						|
func PromptProcced() error {
 | 
						|
	if NoInput {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	if Dry {
 | 
						|
		return errors.New(i18n.G("dry run"))
 | 
						|
	}
 | 
						|
 | 
						|
	response := false
 | 
						|
	prompt := &survey.Confirm{Message: i18n.G("proceed?")}
 | 
						|
	if err := survey.AskOne(prompt, &response); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if !response {
 | 
						|
		return errors.New(i18n.G("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:
 | 
						|
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
 | 
						|
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 errors.New(i18n.G("%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 errors.New(i18n.G("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.Info(i18n.G("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("no service %s for %s?", targetServiceName, app.Name)
 | 
						|
		}
 | 
						|
 | 
						|
		log.Debug(i18n.G("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
 | 
						|
}
 |