app

package
v0.0.0-...-7a73504 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2026 License: GPL-2.0 Imports: 52 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// Copy a src file to a dest file. The src and dest file names are the same.
	//  <dir_src>/<file> + <dir_dst>/<file> -> <dir_dst>/<file>
	CopyModeFileToFile = CopyMode(iota)
	// Copy a src file to a dest file. The src and dest file names are  not the same.
	//  <dir_src>/<file_src> + <dir_dst>/<file_dst> -> <dir_dst>/<file_dst>
	CopyModeFileToFileRename
	// Copy a src file to dest directory. The dest file gets created in the dest
	// folder with the src filename.
	//  <dir_src>/<file> + <dir_dst> -> <dir_dst>/<file>
	CopyModeFileToDir
	// Copy a src directory to dest directory.
	//  <dir_src> + <dir_dst> -> <dir_dst>/<dir_src>
	CopyModeDirToDir
	// Copy all files in the src directory to the dest directory. This works recursively.
	//  <dir_src>/ + <dir_dst> -> <dir_dst>/<files_from_dir_src>
	CopyModeFilesToDir
)

Variables

View Source
var (
	ErrCopyDirToFile  = errors.New(i18n.G("can't copy dir to file"))
	ErrDstDirNotExist = errors.New(i18n.G("destination directory does not exist"))
)
View Source
var AppBackupCommand = &cobra.Command{

	Use:     i18n.G("backup [cmd] [args] [flags]"),
	Aliases: strings.Split(appBackupAliases, ","),

	Short: i18n.G("Manage app backups"),
}
View Source
var AppBackupCreateCommand = &cobra.Command{

	Use:     i18n.G("create <domain> [flags]"),
	Aliases: strings.Split(appBackupCreateAliases, ","),

	Short: i18n.G("Create a new snapshot"),
	Args:  cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		targetContainer, err := internal.RetrieveBackupBotContainer(cl)
		if err != nil {
			log.Fatal(err)
		}

		execEnv := []string{
			fmt.Sprintf("SERVICE=%s", app.Domain),
			"MACHINE_LOGS=true",
		}

		if retries != "" {
			log.Debug(i18n.G("including RETRIES=%s in backupbot exec invocation", retries))
			execEnv = append(execEnv, fmt.Sprintf("RETRIES=%s", retries))
		}

		if _, err := internal.RunBackupCmdRemote(cl, "create", targetContainer.ID, execEnv); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppBackupDownloadCommand = &cobra.Command{

	Use:     i18n.G("download <domain> [flags]"),
	Aliases: strings.Split(appBackupDownloadAliases, ","),

	Short: i18n.G("Download a snapshot"),
	Long: i18n.G(`Downloads a backup.tar.gz to the current working directory.

"--volumes/-v" includes data contained in volumes alongide paths specified in
"backupbot.backup.path" labels.`),
	Args: cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		targetContainer, err := internal.RetrieveBackupBotContainer(cl)
		if err != nil {
			log.Fatal(err)
		}

		execEnv := []string{
			fmt.Sprintf("SERVICE=%s", app.Domain),
			"MACHINE_LOGS=true",
		}

		if snapshot != "" {
			log.Debug(i18n.G("including SNAPSHOT=%s in backupbot exec invocation", snapshot))
			execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
		}

		if includePath != "" {
			log.Debug(i18n.G("including INCLUDE_PATH=%s in backupbot exec invocation", includePath))
			execEnv = append(execEnv, fmt.Sprintf("INCLUDE_PATH=%s", includePath))
		}

		if includeSecrets {
			log.Debug(i18n.G("including SECRETS=%v in backupbot exec invocation", includeSecrets))
			execEnv = append(execEnv, fmt.Sprintf("SECRETS=%v", includeSecrets))
		}

		if includeVolumes {
			log.Debug(i18n.G("including VOLUMES=%v in backupbot exec invocation", includeVolumes))
			execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%v", includeVolumes))
		}

		if _, err := internal.RunBackupCmdRemote(cl, "download", targetContainer.ID, execEnv); err != nil {
			log.Fatal(err)
		}

		remoteBackupDir := "/tmp/backup.tar.gz"
		currentWorkingDir := "."
		if err = CopyFromContainer(cl, targetContainer.ID, remoteBackupDir, currentWorkingDir); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppBackupListCommand = &cobra.Command{

	Use:     i18n.G("list <domain> [flags]"),
	Aliases: strings.Split(appBackupListAliases, ","),

	Short: i18n.G("List the contents of a snapshot"),
	Args:  cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		targetContainer, err := internal.RetrieveBackupBotContainer(cl)
		if err != nil {
			log.Fatal(err)
		}

		execEnv := []string{
			fmt.Sprintf("SERVICE=%s", app.Domain),
			"MACHINE_LOGS=true",
		}

		if snapshot != "" {
			log.Debug(i18n.G("including SNAPSHOT=%s in backupbot exec invocation", snapshot))
			execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
		}

		if showAllPaths {
			log.Debug(i18n.G("including SHOW_ALL=%v in backupbot exec invocation", showAllPaths))
			execEnv = append(execEnv, fmt.Sprintf("SHOW_ALL=%v", showAllPaths))
		}

		if timestamps {
			log.Debug(i18n.G("including TIMESTAMPS=%v in backupbot exec invocation", timestamps))
			execEnv = append(execEnv, fmt.Sprintf("TIMESTAMPS=%v", timestamps))
		}

		if _, err = internal.RunBackupCmdRemote(cl, "ls", targetContainer.ID, execEnv); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppBackupSnapshotsCommand = &cobra.Command{

	Use:     i18n.G("snapshots <domain> [flags]"),
	Aliases: strings.Split(appBackupSnapshotsAliases, ","),

	Short: i18n.G("List all snapshots"),
	Args:  cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		targetContainer, err := internal.RetrieveBackupBotContainer(cl)
		if err != nil {
			log.Fatal(err)
		}

		execEnv := []string{
			fmt.Sprintf("SERVICE=%s", app.Domain),
			"MACHINE_LOGS=true",
		}

		if _, err = internal.RunBackupCmdRemote(cl, "snapshots", targetContainer.ID, execEnv); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppCheckCommand = &cobra.Command{

	Use:     i18n.G("check <domain> [flags]"),
	Aliases: strings.Split(appCheckAliases, ","),

	Short: i18n.G("Ensure an app is well configured"),
	Long: i18n.G(`Compare env vars in both the app ".env" and recipe ".env.sample" file.

The goal is to ensure that recipe ".env.sample" env vars are defined in your
app ".env" file. Only env var definitions in the ".env.sample" which are
uncommented, e.g. "FOO=bar" are checked. If an app ".env" file does not include
these env vars, then "check" will complain.

Recipe maintainers may or may not provide defaults for env vars within their
recipes regardless of commenting or not (e.g. through the use of
${FOO:<default>} syntax). "check" does not confirm or deny this for you.`),
	Args: cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		table, err := formatter.CreateTable()
		if err != nil {
			log.Fatal(err)
		}

		table.
			Headers(
				fmt.Sprintf("%s .env.sample", app.Recipe.Name),
				fmt.Sprintf("%s.env", app.Name),
			).
			StyleFunc(func(row, col int) lipgloss.Style {
				switch {
				case col == 1:
					return lipgloss.NewStyle().Padding(0, 1, 0, 1).Align(lipgloss.Center)
				default:
					return lipgloss.NewStyle().Padding(0, 1, 0, 1)
				}
			})

		envVars, err := appPkg.CheckEnv(app)
		if err != nil {
			log.Fatal(err)
		}

		for _, envVar := range envVars {
			if envVar.Present {
				val := []string{envVar.Name, "✅"}
				table.Row(val...)
			} else {
				val := []string{envVar.Name, "❌"}
				table.Row(val...)
			}
		}

		if err := formatter.PrintTable(table); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppCmdCommand = &cobra.Command{

	Use:     i18n.G("command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]"),
	Aliases: strings.Split(appCmdAliases, ","),

	Short: i18n.G("Run app commands"),
	Long: i18n.G(`Run an app specific command.

These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local/-l".

N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must
be passed *before* the "--". It is possible to pass arguments without the "--"
as long as no dashes are present (i.e. "foo" works without "--", "-foo"
does not).`),
	Example: i18n.G(`  # pass <cmd> args/flags without "--"
  abra app cmd 1312.net app my_cmd_arg foo --user bar

  # pass <cmd> args/flags with "--"
  abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv

  # drop the [service] arg if using "--local/-l"
  abra app cmd 1312.net my_cmd --local`),
	Args: func(cmd *cobra.Command, args []string) error {
		if local {
			if !(len(args) >= 2) {
				return errors.New(i18n.G("requires at least 2 arguments with --local/-l"))
			}

			if slices.Contains(os.Args, "--") {
				if cmd.ArgsLenAtDash() > 2 {
					return errors.New(i18n.G("accepts at most 2 args with --local/-l"))
				}
			}

			return nil
		}

		if !(len(args) >= 3) {
			return errors.New(i18n.G("requires at least 3 arguments"))
		}

		return nil
	},
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			if !local {
				return autocomplete.ServiceNameComplete(args[0])
			}
			return autocomplete.CommandNameComplete(args[0])
		case 2:
			if !local {
				return autocomplete.CommandNameComplete(args[0])
			}
			return nil, cobra.ShellCompDirectiveDefault
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		if local && remoteUser != "" {
			log.Fatal(i18n.G("cannot use --local & --user together"))
		}

		hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local)

		if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
			if os.IsNotExist(err) {
				log.Fatal(i18n.G("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name))
			}
			log.Fatal(err)
		}

		if local {
			cmdName := args[1]
			if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
				log.Fatal(err)
			}

			log.Debug(i18n.G("--local detected, running %s on local work station", cmdName))

			var exportEnv string
			for k, v := range app.Env {
				exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v)
			}

			var sourceAndExec string
			if hasCmdArgs {
				log.Debug(i18n.G("parsed following command arguments: %s", parsedCmdArgs))
				sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName, parsedCmdArgs)
			} else {
				log.Debug(i18n.G("did not detect any command arguments"))
				sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName)
			}

			shell := "/bin/bash"
			if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
				log.Debug(i18n.G("%s does not exist locally, use /bin/sh as fallback", shell))
				shell = "/bin/sh"
			}
			cmd := exec.Command(shell, "-c", sourceAndExec)

			if err := internal.RunCmd(cmd); err != nil {
				log.Fatal(err)
			}

			return
		}

		cmdName := args[2]
		if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
			log.Fatal(err)
		}

		serviceNames, err := appPkg.GetAppServiceNames(app.Name)
		if err != nil {
			log.Fatal(err)
		}

		matchingServiceName := false
		targetServiceName := args[1]
		for _, serviceName := range serviceNames {
			if serviceName == targetServiceName {
				matchingServiceName = true
			}
		}

		if !matchingServiceName {
			log.Fatal(i18n.G("no service %s for %s?", targetServiceName, app.Name))
		}

		log.Debug(i18n.G("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName))

		if hasCmdArgs {
			log.Debug(i18n.G("parsed following command arguments: %s", parsedCmdArgs))
		} else {
			log.Debug(i18n.G("did not detect any command arguments"))
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		if err := internal.RunCmdRemote(
			cl,
			app,
			disableTTY,
			app.Recipe.AbraShPath,
			targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppCmdListCommand = &cobra.Command{

	Use:     i18n.G("list <domain> [flags]"),
	Aliases: strings.Split(appCmdListAliases, ","),

	Short: i18n.G("List all available commands"),
	Args:  cobra.MinimumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
		if err != nil {
			log.Fatal(err)
		}

		sort.Strings(cmdNames)

		for _, cmdName := range cmdNames {
			fmt.Println(cmdName)
		}
	},
}
View Source
var AppCommand = &cobra.Command{

	Use:     i18n.G("app [cmd] [args] [flags]"),
	Aliases: strings.Split(appAliases, ","),

	Short: i18n.G("Manage apps"),
}
View Source
var AppConfigCommand = &cobra.Command{

	Use:     i18n.G("config <domain> [flags]"),
	Aliases: strings.Split(appConfigAliases, ","),

	Short:   i18n.G("Edit app config"),
	Example: i18n.G("  abra config 1312.net"),
	Args:    cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		files, err := appPkg.LoadAppFiles("")
		if err != nil {
			log.Fatal(err)
		}

		appName := args[0]
		appFile, exists := files[appName]
		if !exists {
			log.Fatal(i18n.G("cannot find app with name %s", appName))
		}

		ed, ok := os.LookupEnv("EDITOR")
		if !ok {
			edPrompt := &survey.Select{
				Message: i18n.G("which editor do you wish to use?"),
				Options: []string{"vi", "vim", "nvim", "nano", "pico", "emacs"},
			}
			if err := survey.AskOne(edPrompt, &ed); err != nil {
				log.Fatal(err)
			}
		}

		c := exec.Command(ed, appFile.Path)
		c.Stdin = os.Stdin
		c.Stdout = os.Stdout
		c.Stderr = os.Stderr
		if err := c.Run(); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppCpCommand = &cobra.Command{

	Use:     i18n.G("cp <domain> <src> <dst> [flags]"),
	Aliases: strings.Split(appCpAliases, ","),

	Short: i18n.G("Copy files to/from a deployed app service"),
	Example: i18n.G(`  # copy myfile.txt to the root of the app service
  abra app cp 1312.net myfile.txt app:/

  # copy that file back to your current working directory locally
  abra app cp 1312.net app:/myfile.txt ./`),
	Args: cobra.ExactArgs(3),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		src := args[1]
		dst := args[2]
		srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
		if err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
		if err != nil {
			log.Fatal(err)
		}
		log.Debug(i18n.G("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server))

		if toContainer {
			err = CopyToContainer(cl, container.ID, srcPath, dstPath)
		} else {
			err = CopyFromContainer(cl, container.ID, srcPath, dstPath)
		}
		if err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppDeployCommand = &cobra.Command{

	Use:     i18n.G("deploy <domain> [version] [flags]"),
	Aliases: strings.Split(appDeployAliases, ","),

	Short: i18n.G("Deploy an app"),
	Long: i18n.G(`Deploy an app.

This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
checkout as-is. Recipe commit hashes are also supported as values for
"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`),
	Example: i18n.G(`  # standard deployment
  abra app deploy 1312.net

  # chaos deployment
  abra app deploy 1312.net --chaos
  
  # deploy specific version
  abra app deploy 1312.net 2.0.0+1.2.3

  # deploy a specific git hash
  abra app deploy 1312.net 886db76d`),
	Args: cobra.RangeArgs(1, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string,
	) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			app, err := appPkg.Get(args[0])
			if err != nil {
				errMsg := i18n.G("autocomplete failed: %s", err)
				return []string{errMsg}, cobra.ShellCompDirectiveError
			}
			return autocomplete.RecipeVersionComplete(app.Recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		var (
			deployWarnMessages []string
			toDeployVersion    string
		)

		app := internal.ValidateApp(args)

		if err := validateArgsAndFlags(args); err != nil {
			log.Fatal(err)
		}

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		log.Debug(i18n.G("checking whether %s is already deployed", app.StackName()))

		deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if deployMeta.IsDeployed && !(internal.Force || internal.Chaos) {
			log.Fatal(i18n.G("%s is already deployed", app.Name))
		}

		toDeployVersion, err = getDeployVersion(args, deployMeta, app)
		if err != nil {
			log.Fatal(err)
		}

		isChaosCommit, err := app.Recipe.IsChaosCommit(toDeployVersion)
		if err != nil {
			log.Fatal(i18n.G("unable to determine if %s is a chaos commit: %s", toDeployVersion, err))
		}

		if !isChaosCommit && !tagcmp.IsParsable(toDeployVersion) {
			log.Fatal(i18n.G("unable to parse deploy version: %s", toDeployVersion))
		}

		if !internal.Chaos {
			isChaosCommit, err := app.Recipe.EnsureVersion(toDeployVersion)
			if err != nil {
				log.Fatal(i18n.G("ensure recipe: %s", err))
			}
			if isChaosCommit {
				log.Warnf(i18n.G("version '%s' appears to be a chaos commit, but --chaos/-C was not provided", toDeployVersion))
				internal.Chaos = true
			}
		}

		if err := lint.LintForErrors(app.Recipe); err != nil {
			if internal.Chaos {
				log.Warn(err)
			} else {
				log.Fatal(err)
			}
		}

		if err := validateSecrets(cl, app); err != nil {
			log.Fatal(err)
		}

		if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
			log.Fatal(err)
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		stackName := app.StackName()
		deployOpts := stack.Deploy{
			Composefiles: composeFiles,
			Namespace:    stackName,
			Prune:        false,
			ResolveImage: stack.ResolveImageAlways,
			Detach:       false,
		}
		compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
		if err != nil {
			log.Fatal(err)
		}

		appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
		appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
		if internal.Chaos {
			appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
		}

		versionLabel := toDeployVersion
		if internal.Chaos {
			for _, service := range compose.Services {
				if service.Name == "app" {
					labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)

					versionLabel = service.Deploy.Labels[labelKey]
				}
			}
		}
		appPkg.SetVersionLabel(compose, stackName, versionLabel)

		newRecipeWithDeployVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, toDeployVersion)
		appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDeployVersion)

		envVars, err := appPkg.CheckEnv(app)
		if err != nil {
			log.Fatal(err)
		}

		for _, envVar := range envVars {
			if !envVar.Present {
				deployWarnMessages = append(deployWarnMessages,
					i18n.G("%s missing from %s.env", envVar.Name, app.Domain),
				)
			}
		}

		if !internal.NoDomainChecks {
			if domainName, ok := app.Env["DOMAIN"]; ok {
				if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
					log.Fatal(err)
				}
			} else {
				log.Debug(i18n.G("skipping domain checks, no DOMAIN=... configured"))
			}
		} else {
			log.Debug(i18n.G("skipping domain checks"))
		}

		deployedVersion := config.MISSING_DEFAULT
		if deployMeta.IsDeployed {
			deployedVersion = deployMeta.Version
			if deployMeta.IsChaos {
				deployedVersion = deployMeta.ChaosVersion
			}
		}

		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		if err := internal.DeployOverview(
			app,
			deployedVersion,
			toDeployVersion,
			"",
			deployWarnMessages,
			secretInfo,
			configInfo,
			imageInfo,
		); err != nil {
			log.Fatal(err)
		}

		stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
		if err != nil {
			log.Fatal(err)
		}

		serviceNames, err := appPkg.GetAppServiceNames(app.Name)
		if err != nil {
			log.Fatal(err)
		}

		f, err := app.Filters(true, false, serviceNames...)
		if err != nil {
			log.Fatal(err)
		}

		if err := stack.RunDeploy(
			cl,
			deployOpts,
			compose,
			app.Name,
			app.Server,
			internal.DontWaitConverge,
			internal.NoInput,
			f,
		); err != nil {
			log.Fatal(err)
		}

		postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"]
		if ok && !internal.DontWaitConverge {
			log.Debug(i18n.G("run the following post-deploy commands: %s", postDeployCmds))
			if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
				log.Fatal(i18n.G("attempting to run post deploy commands, saw: %s", err))
			}
		}

		if err := app.WriteRecipeVersion(toDeployVersion, false); err != nil {
			log.Fatal(i18n.G("writing recipe version failed: %s", err))
		}
	},
}
View Source
var AppEnvCommand = &cobra.Command{

	Use:     i18n.G("env [cmd] [args] [flags]"),
	Aliases: strings.Split(appEnvAliases, ","),

	Short: i18n.G("Manage app environment values"),
}
View Source
var AppEnvListCommand = &cobra.Command{

	Use:     i18n.G("list <domain> [flags]"),
	Aliases: strings.Split(appEnvListAliases, ","),

	Short:   i18n.G("List all app environment values"),
	Example: i18n.G("  abra app env list 1312.net"),
	Args:    cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		var envKeys []string
		for k := range app.Env {
			envKeys = append(envKeys, k)
		}

		sort.Strings(envKeys)

		var rows [][]string
		for _, k := range envKeys {
			rows = append(rows, []string{k, app.Env[k]})
		}

		overview := formatter.CreateOverview(i18n.G("ENV OVERVIEW"), rows)
		fmt.Println(overview)
	},
}
View Source
var AppEnvPullCommand = &cobra.Command{

	Use:     i18n.G("pull <domain> [flags]"),
	Aliases: strings.Split(appEnvPullAliases, ","),

	Short: i18n.G("Pull app environment values from a deployed app"),
	Long: i18n.G(`Pull app environment values from a deploymed app.

A convenient command for when you've lost your app environment file or want to
synchronize your local app environment values with what is deployed live.`),
	Example: i18n.G(`  # pull existing .env file and overwrite local values
  abra app env pull 1312.net --force

  # pull lost app .env file
  abra app env pull my.gitea.net --server 1312.net`),
	Args: cobra.MaximumNArgs(2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		appName := args[0]

		appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
		if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
			log.Fatal(i18n.G("%s already exists?", appEnvPath))
		}

		if server == "" {
			log.Fatal(i18n.G("unable to determine server of app %s, please pass --server/-s", appName))
		}

		serverDir := filepath.Join(config.SERVERS_DIR, server)
		if _, err := os.Stat(serverDir); os.IsNotExist(err) {
			log.Fatal(i18n.G("unknown server %s, run \"abra server add %s\"?", server, server))
		}

		store := contextPkg.NewDefaultDockerContextStore()
		contexts, err := store.Store.List()
		if err != nil {
			log.Fatal(i18n.G("unable to look up server context for %s: %s", server, err))
		}

		var contextCreated bool
		if server == "default" {
			contextCreated = true
		}

		for _, context := range contexts {
			if context.Name == server {
				contextCreated = true
			}
		}

		if !contextCreated {
			log.Fatal(i18n.G("%s missing context, run \"abra server add %s\"?", server, server))
		}

		cl, err := client.New(server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := stack.IsDeployed(context.Background(), cl, appPkg.StackName(appName))
		if err != nil {
			log.Fatal(err)
		}

		if !deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s is not deployed?", appName))
		}

		filters := filters.NewArgs()
		filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(appName), "app"))
		targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, internal.NoInput)
		if err != nil {
			log.Fatal(i18n.G("unable to retrieve container for %s: %s", appName, err))
		}

		inspectResult, err := cl.ContainerInspect(context.Background(), targetContainer.ID)
		if err != nil {
			log.Fatal(i18n.G("unable to inspect container for %s: %s", appName, err))
		}

		deploymentEnv := make(map[string]string)
		for _, envVar := range inspectResult.Config.Env {
			split := strings.SplitN(envVar, "=", 2)
			if len(split) != 2 {
				log.Debug(i18n.G("no value attached to %s", envVar))
				continue
			}

			key, val := split[0], split[1]
			deploymentEnv[key] = val
		}

		log.Debug(i18n.G("pulled env values from %s deployment: %s", appName, deploymentEnv))

		var (
			recipeEnvVar string
			recipeKey    string
		)

		if r, ok := deploymentEnv["TYPE"]; ok {
			recipeKey = "TYPE"
			recipeEnvVar = r
		}

		if r, ok := deploymentEnv["RECIPE"]; ok {
			recipeKey = "RECIPE"
			recipeEnvVar = r
		}

		if recipeEnvVar == "" {
			log.Fatal(i18n.G("unable to determine recipe type from %s, env: %v", appName, inspectResult.Config.Env))
		}

		var recipeName = recipeEnvVar
		if strings.Contains(recipeEnvVar, ":") {
			split := strings.Split(recipeEnvVar, ":")
			recipeName = split[0]
		}

		recipe := internal.ValidateRecipe(
			[]string{recipeName},
			cmd.Name(),
		)

		version := deployMeta.Version
		if deployMeta.IsChaos {
			version = deployMeta.ChaosVersion
		}

		if _, err := recipe.EnsureVersion(version); err != nil {
			log.Fatal(err)
		}

		mergedEnv, err := recipe.SampleEnv()
		if err != nil {
			log.Fatal(err)
		}

		log.Debug(i18n.G("retrieved env values from .env.sample of %s: %s", recipe.Name, mergedEnv))

		for k, v := range deploymentEnv {
			mergedEnv[k] = v
		}

		if !strings.Contains(recipeEnvVar, ":") {
			mergedEnv[recipeKey] = fmt.Sprintf("%s:%s", mergedEnv[recipeKey], version)
		}

		log.Debug(i18n.G("final merged env values for %s are: %s", appName, mergedEnv))

		envSample, err := os.ReadFile(recipe.SampleEnvPath)
		if err != nil {
			log.Fatal(err)
		}

		err = os.WriteFile(appEnvPath, envSample, 0o664)
		if err != nil {
			log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
		}

		read, err := os.ReadFile(appEnvPath)
		if err != nil {
			log.Fatal(i18n.G("unable to read new env %s: %s", appEnvPath, err))
		}

		sampleEnv, err := recipe.SampleEnv()
		if err != nil {
			log.Fatal(err)
		}

		var composeFileUpdated bool
		newContents := string(read)
		for key, val := range mergedEnv {
			if sampleEnv[key] == val {
				continue
			}

			if key == "COMPOSE_FILE" {
				composeFileUpdated = true
				continue
			}

			if m, _ := regexp.MatchString(fmt.Sprintf(`#%s=`, key), newContents); m {
				log.Debug(i18n.G("uncommenting %s", key))
				re := regexp.MustCompile(fmt.Sprintf(`#%s=`, key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
			}

			if m, _ := regexp.MatchString(fmt.Sprintf(`# %s=`, key), newContents); m {
				log.Debug(i18n.G("uncommenting %s", key))
				re := regexp.MustCompile(fmt.Sprintf(`# %s=`, key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
			}

			if m, _ := regexp.MatchString(fmt.Sprintf(`%s=".*"`, key), newContents); m {
				log.Debug(i18n.G(`inserting %s="%s" (double quotes)`, key, val))
				re := regexp.MustCompile(fmt.Sprintf(`%s=".*"`, key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s="%s"`, key, val))
				continue
			}

			if m, _ := regexp.MatchString(fmt.Sprintf(`%s='.*'`, key), newContents); m {
				log.Debug(i18n.G(`inserting %s='%s' (single quotes)`, key, val))
				re := regexp.MustCompile(fmt.Sprintf(`%s='.*'`, key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s='%s'`, key, val))
				continue
			}

			if m, _ := regexp.MatchString(fmt.Sprintf("%s=.*", key), newContents); m {
				log.Debug(i18n.G("inserting %s=%s (no quotes)", key, val))
				re := regexp.MustCompile(fmt.Sprintf("%s=.*", key))
				newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=%s", key, val))
			}
		}

		err = os.WriteFile(appEnvPath, []byte(newContents), 0)
		if err != nil {
			log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
		}

		log.Info(i18n.G("%s successfully created", appEnvPath))

		if composeFileUpdated {
			log.Warn(i18n.G("manual update required: COMPOSE_FILE=\"%s\"", mergedEnv["COMPOSE_FILE"]))
		}
	},
}
View Source
var AppLabelsCommand = &cobra.Command{

	Use:     i18n.G("labels <domain> [flags]"),
	Aliases: strings.Split(appLabelsAliases, ","),

	Short:   i18n.G("Show deployment labels"),
	Long:    i18n.G("Both local recipe and live deployment labels are shown."),
	Example: "  " + i18n.G("abra app labels 1312.net"),
	Args:    cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		remoteLabels, err := getLabels(cl, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		rows := [][]string{
			{i18n.G("DEPLOYED LABELS"), "---"},
		}

		remoteLabelKeys := make([]string, 0, len(remoteLabels))
		for k := range remoteLabels {
			remoteLabelKeys = append(remoteLabelKeys, k)
		}

		sort.Strings(remoteLabelKeys)

		for _, k := range remoteLabelKeys {
			rows = append(rows, []string{
				k,
				remoteLabels[k],
			})
		}

		if len(remoteLabelKeys) == 0 {
			rows = append(rows, []string{i18n.G("unknown")})
		}

		rows = append(rows, []string{i18n.G("RECIPE LABELS"), "---"})

		config, err := app.Recipe.GetComposeConfig(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		var localLabelKeys []string
		var appServiceConfig composetypes.ServiceConfig
		for _, service := range config.Services {
			if service.Name == "app" {
				appServiceConfig = service

				for k := range service.Deploy.Labels {
					localLabelKeys = append(localLabelKeys, k)
				}
			}
		}

		sort.Strings(localLabelKeys)

		for _, k := range localLabelKeys {
			rows = append(rows, []string{
				k,
				appServiceConfig.Deploy.Labels[k],
			})
		}

		overview := formatter.CreateOverview(i18n.G("LABELS OVERVIEW"), rows)
		fmt.Println(overview)
	},
}
View Source
var AppListCommand = &cobra.Command{

	Use:     i18n.G("list [flags]"),
	Aliases: strings.Split(appListAliases, ","),

	Short: i18n.G("List all managed apps"),
	Long: i18n.G(`Generate a report of all managed apps.

Use "--status/-S" flag to query all servers for the live deployment status.`),
	Example: i18n.G(`  # list apps of all servers without live status
  abra app ls

  # list apps of a specific server with live status
  abra app ls -s 1312.net -S

  # list apps of all servers which match a specific recipe
  abra app ls -r gitea`),
	Args: cobra.NoArgs,
	Run: func(cmd *cobra.Command, args []string) {
		appFiles, err := appPkg.LoadAppFiles(listAppServer)
		if err != nil {
			log.Fatal(err)
		}

		apps, err := appPkg.GetApps(appFiles, recipeFilter)
		if err != nil {
			log.Fatal(err)
		}

		sort.Sort(appPkg.ByServerAndRecipe(apps))

		statuses := make(map[string]map[string]string)
		if status {
			alreadySeen := make(map[string]bool)
			for _, app := range apps {
				if _, ok := alreadySeen[app.Server]; !ok {
					alreadySeen[app.Server] = true
				}
			}

			statuses, err = appPkg.GetAppStatuses(apps, internal.MachineReadable)
			if err != nil {
				log.Fatal(err)
			}
		}

		var totalServersCount int
		var totalAppsCount int
		allStats := make(map[string]serverStatus)
		for _, app := range apps {
			var stats serverStatus
			var ok bool
			if stats, ok = allStats[app.Server]; !ok {
				stats = serverStatus{}
				if recipeFilter == "" {

					totalServersCount++
				}
			}

			if app.Recipe.Name == recipeFilter || recipeFilter == "" {
				if recipeFilter != "" {

					totalServersCount++
				}

				appStats := appStatus{}
				stats.AppCount++
				totalAppsCount++

				if status {
					if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
						log.Fatal(err)
					}

					status := i18n.G("unknown")
					version := i18n.G("unknown")
					chaos := i18n.G("unknown")
					chaosVersion := i18n.G("unknown")
					if statusMeta, ok := statuses[app.StackName()]; ok {
						if currentVersion, exists := statusMeta["version"]; exists {
							if currentVersion != "" {
								version = currentVersion
							}
						}
						if chaosDeploy, exists := statusMeta["chaos"]; exists {
							chaos = chaosDeploy
						}
						if chaosDeployVersion, exists := statusMeta["chaosVersion"]; exists {
							chaosVersion = chaosDeployVersion
						}
						if statusMeta["status"] != "" {
							status = statusMeta["status"]
						}
						stats.VersionCount++
					} else {
						stats.UnversionedCount++
					}

					appStats.Status = status
					appStats.Chaos = chaos
					appStats.ChaosVersion = chaosVersion
					appStats.Version = version

					var newUpdates []string
					if version != "unknown" && chaos == "false" {
						if err := app.Recipe.EnsureExists(); err != nil {
							log.Fatal(i18n.G("unable to clone %s: %s", app.Name, err))
						}

						updates, err := app.Recipe.Tags()
						if err != nil {
							log.Fatal(i18n.G("unable to retrieve tags for %s: %s", app.Name, err))
						}

						parsedVersion, err := tagcmp.Parse(version)
						if err != nil {
							log.Fatal(err)
						}

						for _, update := range updates {
							if ok := tagcmp.IsParsable(update); !ok {
								log.Debug(i18n.G("unable to parse %s, skipping as upgrade option", update))
								continue
							}

							parsedUpdate, err := tagcmp.Parse(update)
							if err != nil {
								log.Fatal(err)
							}

							if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
								newUpdates = append(newUpdates, update)
							}
						}
					}

					if len(newUpdates) == 0 {
						if version == "unknown" {
							appStats.Upgrade = i18n.G("unknown")
						} else {
							appStats.Upgrade = i18n.G("latest")
							stats.LatestCount++
						}
					} else {
						newUpdates = internal.SortVersionsDesc(newUpdates)
						appStats.Upgrade = strings.Join(newUpdates, "\n")
						stats.UpgradeCount++
					}
				}

				appStats.Server = app.Server
				appStats.Recipe = app.Recipe.Name
				appStats.AppName = app.Name
				appStats.Domain = app.Domain

				stats.Apps = append(stats.Apps, appStats)
			}
			allStats[app.Server] = stats
		}

		if internal.MachineReadable {
			jsonstring, err := json.Marshal(allStats)
			if err != nil {
				log.Fatal(err)
			} else {
				fmt.Println(string(jsonstring))
			}

			return
		}

		alreadySeen := make(map[string]bool)
		for _, app := range apps {
			if _, ok := alreadySeen[app.Server]; ok {
				continue
			}

			serverStat := allStats[app.Server]

			headers := []string{i18n.G("RECIPE"), i18n.G("DOMAIN"), i18n.G("SERVER")}
			if status {
				headers = append(headers, []string{
					i18n.G("STATUS"),
					i18n.G("CHAOS"),
					i18n.G("VERSION"),
					i18n.G("UPGRADE"),
				}...,
				)
			}

			table, err := formatter.CreateTable()
			if err != nil {
				log.Fatal(err)
			}

			table.Headers(headers...)

			var rows [][]string
			for _, appStat := range serverStat.Apps {
				row := []string{appStat.Recipe, appStat.Domain, appStat.Server}
				if status {
					chaosStatus := appStat.Chaos
					if chaosStatus != "unknown" {
						chaosEnabled, err := strconv.ParseBool(chaosStatus)
						if err != nil {
							log.Fatal(err)
						}
						if chaosEnabled && appStat.ChaosVersion != "unknown" {
							chaosStatus = appStat.ChaosVersion
						}
					}

					row = append(row, []string{
						appStat.Status,
						chaosStatus,
						appStat.Version,
						appStat.Upgrade}...,
					)
				}

				rows = append(rows, row)
			}

			table.Rows(rows...)

			if len(rows) > 0 {
				if err := formatter.PrintTable(table); err != nil {
					log.Fatal(err)
				}

				if len(allStats) > 1 && len(rows) > 0 {
					fmt.Println()
				}
			}

			alreadySeen[app.Server] = true
		}
	},
}
View Source
var AppLogsCommand = &cobra.Command{

	Use:     i18n.G("logs <domain> [service] [flags]"),
	Aliases: strings.Split(appLogsAliases, ","),

	Short: i18n.G("Tail app logs"),
	Args:  cobra.RangeArgs(1, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			app, err := appPkg.Get(args[0])
			if err != nil {
				return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
			}
			return autocomplete.ServiceNameComplete(app.Name)
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)
		stackName := app.StackName()

		if err := app.Recipe.EnsureExists(); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
		if err != nil {
			log.Fatal(err)
		}

		if !deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s is not deployed?", app.Name))
		}

		var serviceNames []string
		if len(args) == 2 {
			serviceNames = []string{args[1]}
		}

		f, err := app.Filters(true, false, serviceNames...)
		if err != nil {
			log.Fatal(err)
		}

		opts := logs.TailOpts{
			AppName:  app.Name,
			Services: serviceNames,
			StdErr:   stdErr,
			Since:    sinceLogs,
			Filters:  f,
		}

		if err := logs.TailLogs(cl, opts); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppMoveCommand = &cobra.Command{

	Use:     i18n.G("move <domain> <server> [flags]"),
	Aliases: strings.Split(appMoveAliases, ","),

	Short: i18n.G("Moves an app to a different server"),
	Long: i18n.G(`Move an app to a differnt server.

This command will migrate an app config and copy secrets and volumes from the
old server to the new one. The app MUST be deployed on the old server before
doing the move. The app will be undeployed from the current server but not
deployed on the new server.

The "tar" command is required on both the old and new server as well as "sudo"
permissions. The "rsync" command is required on your local machine for
transferring volumes.

Do not forget to update your DNS records. Don't panic, it might take a while
for the dust to settle after you move an app. If anything goes wrong, you can
always move the app config file to the original server and deploy it there
again. No data is removed from the old server.

Use "--dry-run/-r" to see which secrets and volumes will be moved.`),
	Example: i18n.G(`  # move an app
  abra app move nextcloud.1312.net myserver.com`),
	Args: cobra.RangeArgs(1, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string,
	) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			return autocomplete.ServerNameComplete()
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if len(args) <= 1 {
			log.Fatal(i18n.G("no server provided?"))
		}
		newServer := internal.ValidateServer([]string{args[1]})

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		currentServerClient, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := stack.IsDeployed(context.Background(), currentServerClient, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if !deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s must first be deployed on %s before moving", app.Name, app.Server))
		}

		resources, err := getAppResources(currentServerClient, app)
		if err != nil {
			log.Fatal(i18n.G("unable to retrieve %s resources on %s: %s", app.Name, app.Server, err))
		}

		internal.MoveOverview(app, newServer, resources.SecretNames(), resources.VolumeNames())
		if err := internal.PromptProcced(); err != nil {
			log.Fatal(i18n.G("bailing out: %s", err))
		}

		log.Info(i18n.G("undeploying %s on %s", app.Name, app.Server))
		rmOpts := stack.Remove{
			Namespaces: []string{app.StackName()},
			Detach:     false,
		}
		if err := stack.RunRemove(context.Background(), currentServerClient, rmOpts); err != nil {
			log.Fatal(i18n.G("failed to remove app from %s: %s", err, app.Server))
		}

		newServerClient, err := client.New(newServer)
		if err != nil {
			log.Fatal(err)
		}

		for _, s := range resources.SecretList {
			sname := strings.Split(strings.TrimPrefix(s.Spec.Name, app.StackName()+"_"), "_")
			secretName := strings.Join(sname[:len(sname)-1], "_")
			data := resources.Secrets[secretName]
			if err := client.StoreSecret(newServerClient, s.Spec.Name, data); err != nil {
				if strings.Contains(err.Error(), "already exists") {
					log.Info(i18n.G("skipping secret (because it already exists) on %s: %s", s.Spec.Name, newServer))
					continue
				}
				log.Fatal(i18n.G("failed to store secret on %s: %s", err, newServer))
			}
			log.Info(i18n.G("created secret on %s: %s", s.Spec.Name, newServer))
		}

		for _, v := range resources.Volumes {
			log.Info(i18n.G("moving volume %s from %s to %s", v.Name, app.Server, newServer))

			log.Debug(i18n.G("creating volume %s on %s", v.Name, newServer))
			_, err := newServerClient.VolumeCreate(context.Background(), volume.CreateOptions{
				Name:   v.Name,
				Driver: v.Driver,
			})
			if err != nil {
				log.Fatal(i18n.G("failed to create volume %s on %s: %s", v.Name, newServer, err))
			}

			filename := fmt.Sprintf("%s_outgoing.tar.gz", v.Name)
			log.Debug(i18n.G("creating %s on %s", filename, app.Server))
			tarCmd := fmt.Sprintf("sudo tar --same-owner -czhpf %s -C /var/lib/docker/volumes %s", filename, v.Name)
			cmd := exec.Command("ssh", app.Server, "-tt", tarCmd)
			if out, err := cmd.CombinedOutput(); err != nil {
				log.Fatal(i18n.G("%s failed on %s: output:%s err:%s", tarCmd, app.Server, string(out), err))
			}

			log.Debug(i18n.G("rsyncing %s from %s to local machine", filename, app.Server))
			cmd = exec.Command("rsync", "-a", "-v", fmt.Sprintf("%s:%s", app.Server, filename), filename)
			if out, err := cmd.CombinedOutput(); err != nil {
				log.Fatal(i18n.G("failed to copy %s from %s to local machine: output:%s err:%s", filename, app.Server, string(out), err))
			}

			log.Debug(i18n.G("rsyncing %s to %s from local machine", filename, filename, newServer))
			cmd = exec.Command("rsync", "-a", "-v", filename, fmt.Sprintf("%s:%s", newServer, filename))
			if out, err := cmd.CombinedOutput(); err != nil {
				log.Fatal(i18n.G("failed to copy %s from local machine to %s: output:%s err:%s", filename, newServer, string(out), err))
			}

			log.Debug(i18n.G("extracting %s on %s", filename, newServer))
			tarExtractCmd := fmt.Sprintf("sudo tar --same-owner -xzpf %s -C /var/lib/docker/volumes", filename)
			cmd = exec.Command("ssh", newServer, "-tt", tarExtractCmd)
			if out, err := cmd.CombinedOutput(); err != nil {
				log.Fatal(i18n.G("%s failed to extract %s on %s: output:%s err:%s", tarExtractCmd, filename, newServer, string(out), err))
			}

			log.Debug(i18n.G("removing %s from %s", filename, newServer))
			cmd = exec.Command("ssh", newServer, "-tt", fmt.Sprintf("sudo rm -rf %s", filename))
			if out, err := cmd.CombinedOutput(); err != nil {
				log.Fatal(i18n.G("failed to remove %s from %s: output:%s err:%s", filename, newServer, string(out), err))
			}

			log.Debug(i18n.G("removing %s from %s", filename, app.Server))
			cmd = exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo rm -rf %s", filename))
			if out, err := cmd.CombinedOutput(); err != nil {
				log.Fatal(i18n.G("failed to remove %s from %s: output:%s err:%s", filename, app.Server, string(out), err))
			}

			log.Debug(i18n.G("removing %s from local machine", filename))
			cmd = exec.Command("rm", "-r", "-f", filename)
			if out, err := cmd.CombinedOutput(); err != nil {
				log.Fatal(i18n.G("failed to remove %s on local machine: output:%s err:%s", filename, string(out), err))
			}
		}

		newServerPath := fmt.Sprintf("%s/servers/%s/%s.env", config.ABRA_DIR, newServer, app.Name)
		log.Info(i18n.G("migrating app config from %s to %s", app.Server, newServerPath))
		if err := copyFile(app.Path, newServerPath); err != nil {
			log.Fatal(i18n.G("failed to migrate app config: %s", err))
		}

		if err := os.Remove(app.Path); err != nil {
			log.Fatal(i18n.G("unable to remove %s: %s", app.Path, err))
		}

		log.Info(i18n.G("%s was successfully moved from %s to %s 🎉", app.Name, app.Server, newServer))
	},
}
View Source
var AppNewCommand = &cobra.Command{

	Use:     i18n.G("new [recipe] [version] [flags]"),
	Aliases: strings.Split(appNewAliases, ","),

	Short: i18n.G("Create a new app"),
	Long:  appNewDescription,
	Args:  cobra.RangeArgs(0, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.RecipeNameComplete()
		case 1:
			recipe := internal.ValidateRecipe(args, cmd.Name())
			return autocomplete.RecipeVersionComplete(recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		recipe := internal.ValidateRecipe(args, cmd.Name())

		if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		if len(args) == 2 && internal.Chaos {
			log.Fatal(i18n.G("cannot use [version] and --chaos together"))
		}

		var recipeVersion string
		if len(args) == 2 {
			recipeVersion = args[1]
		}

		chaosVersion := config.CHAOS_DEFAULT
		if internal.Chaos {
			var err error
			chaosVersion, err = recipe.ChaosVersion()
			if err != nil {
				log.Fatal(err)
			}

			recipeVersion = chaosVersion
		} else {
			if err := recipe.EnsureIsClean(); err != nil {
				log.Fatal(err)
			}

			var recipeVersions recipePkg.RecipeVersions
			if recipeVersion == "" {
				var err error
				var warnings []string
				recipeVersions, warnings, err = recipe.GetRecipeVersions()
				if err != nil {
					log.Fatal(err)
				}
				for _, warning := range warnings {
					log.Warn(warning)
				}
			}

			if len(recipeVersions) > 0 {
				latest := recipeVersions[len(recipeVersions)-1]
				for tag := range latest {
					recipeVersion = tag
				}

				log.Debug(i18n.G("selected recipe version: %s (from %d available versions)", recipeVersion, len(recipeVersions)))

				if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
					log.Fatal(err)
				}
			} else {
				if err := recipe.EnsureLatest(); err != nil {
					log.Fatal(err)
				}

				if recipeVersion == "" {
					head, err := recipe.Head()
					if err != nil {
						log.Fatal(i18n.G("failed to retrieve latest commit for %s: %s", recipe.Name, err))
					}

					recipeVersion = formatter.SmallSHA(head.String())
				}
			}
		}

		if err := ensureServerFlag(); err != nil {
			log.Fatal(err)
		}

		if err := ensureDomainFlag(recipe, newAppServer); err != nil {
			log.Fatal(err)
		}

		sanitisedAppName := appPkg.SanitiseAppName(appDomain)
		log.Debug(i18n.G("%s sanitised as %s for new app", appDomain, sanitisedAppName))

		if err := appPkg.TemplateAppEnvSample(
			recipe,
			appDomain,
			newAppServer,
			appDomain,
		); err != nil {
			log.Fatal(err)
		}

		sampleEnv, err := recipe.SampleEnv()
		if err != nil {
			log.Fatal(err)
		}

		composeFiles, err := recipe.GetComposeFiles(sampleEnv)
		if err != nil {
			log.Fatal(err)
		}

		secretsConfig, err := secret.ReadSecretsConfig(
			recipe.SampleEnvPath,
			composeFiles,
			appPkg.StackName(appDomain),
		)
		if err != nil {
			log.Fatal(err)
		}

		var appSecrets AppSecrets
		if generateSecrets {
			if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
				log.Fatal(err)
			}

			cl, err := client.New(newAppServer)
			if err != nil {
				log.Fatal(err)
			}

			appSecrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
			if err != nil {
				log.Fatal(err)
			}
		}

		if newAppServer == "default" {
			newAppServer = "local"
		}

		log.Info(i18n.G("%s created (version: %s)", appDomain, recipeVersion))

		if len(secretsConfig) > 0 {
			var (
				hasSecretToGenerate bool
				hasSecretToSkip     bool
			)

			for _, secretConfig := range secretsConfig {
				if secretConfig.SkipGenerate {
					hasSecretToSkip = true
					continue
				}

				hasSecretToGenerate = true
			}

			if hasSecretToGenerate && !generateSecrets {
				log.Warn(i18n.G("%s requires secret generation before deploy, run \"abra app secret generate %s --all\"", recipe.Name, appDomain))
			}

			if hasSecretToSkip {
				log.Warn(i18n.G("%s requires secret insertion before deploy (#generate=false)", recipe.Name))
			}
		}

		if len(appSecrets) > 0 {
			rows := [][]string{}
			for k, v := range appSecrets {
				rows = append(rows, []string{k, v})
			}

			overview := formatter.CreateOverview(i18n.G("SECRETS OVERVIEW"), rows)

			fmt.Println(overview)

			log.Warn(i18n.G(
				"secrets are %s shown again, please save them %s",
				formatter.BoldUnderlineStyle.Render("NOT"),
				formatter.BoldUnderlineStyle.Render("NOW"),
			))
		}

		app, err := app.Get(appDomain)
		if err != nil {
			log.Fatal(err)
		}

		if err := app.WriteRecipeVersion(recipeVersion, false); err != nil {
			log.Fatal(i18n.G("writing recipe version failed: %s", err))
		}
	},
}
View Source
var AppPsCommand = &cobra.Command{

	Use:     i18n.G("ps <domain> [flags]"),
	Aliases: strings.Split(appPsAliases, ","),

	Short: i18n.G("Check app deployment status"),
	Args:  cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if !deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s is not deployed?", app.Name))
		}

		chaosVersion := config.CHAOS_DEFAULT
		statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
		if statusMeta, ok := statuses[app.StackName()]; ok {
			if isChaos, exists := statusMeta["chaos"]; exists && isChaos == "true" {
				if cVersion, exists := statusMeta["chaosVersion"]; exists {
					chaosVersion = cVersion
					if strings.HasSuffix(chaosVersion, config.DIRTY_DEFAULT) {
						chaosVersion = formatter.BoldDirtyDefault(chaosVersion)
					}
				}
			}
		}

		showPSOutput(app, cl, deployMeta.Version, chaosVersion)
	},
}
View Source
var AppRemoveCommand = &cobra.Command{

	Use:     i18n.G("remove <domain> [flags]"),
	Aliases: strings.Split(appRemoveAliases, ","),

	Short: i18n.G("Remove all app data, locally and remotely"),
	Long: i18n.G(`Remove everything related to an app which is already undeployed.

By default, it will prompt for confirmation before proceeding. All secrets,
volumes and the local app env file will be deleted.

Only run this command when you are sure you want to completely remove the app
and all associated app data. This is a destructive action, Be Careful!

If you would like to delete specific volumes or secrets, please use removal
sub-commands under "app volume" and "app secret" instead.

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.`),
	Example: i18n.G("  abra app remove 1312.net"),
	Args:    cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if !internal.Force && !internal.NoInput {
			log.Warn(i18n.G("ALERTA ALERTA: deleting %s data and config (local/remote)", app.Name))

			response := false
			prompt := &survey.Confirm{Message: i18n.G("are you sure?")}
			if err := survey.AskOne(prompt, &response); err != nil {
				log.Fatal(err)
			}

			if !response {
				log.Fatal(i18n.G("aborting as requested"))
			}
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
		if err != nil {
			log.Fatal(err)
		}
		if deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name))
		}

		fs, err := app.Filters(false, false)
		if err != nil {
			log.Fatal(err)
		}

		configs, err := client.GetConfigs(cl, context.Background(), app.Server, fs)
		if err != nil {
			log.Fatal(err)
		}
		configNames := client.GetConfigNames(configs)

		if len(configNames) > 0 {
			if err := client.RemoveConfigs(cl, context.Background(), configNames, internal.Force); err != nil {
				log.Fatal(i18n.G("removing configs failed: %s", err))
			}

			log.Info(i18n.G("%d config(s) removed successfully", len(configNames)))
		} else {
			log.Info(i18n.G("no configs to remove"))
		}

		secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: fs})
		if err != nil {
			log.Fatal(err)
		}

		secrets := make(map[string]string)
		var secretNames []string

		for _, cont := range secretList {
			secrets[cont.Spec.Annotations.Name] = cont.ID
			secretNames = append(secretNames, cont.Spec.Annotations.Name)
		}

		if len(secrets) > 0 {
			for _, name := range secretNames {
				err := cl.SecretRemove(context.Background(), secrets[name])
				if err != nil {
					log.Fatal(err)
				}
				log.Info(i18n.G("secret: %s removed", name))
			}
		} else {
			log.Info(i18n.G("no secrets to remove"))
		}

		fs, err = app.Filters(false, true)
		if err != nil {
			log.Fatal(err)
		}

		volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, fs)
		if err != nil {
			log.Fatal(err)
		}
		volumeNames := client.GetVolumeNames(volumeList)

		if len(volumeNames) > 0 {
			err := client.RemoveVolumes(cl, context.Background(), volumeNames, internal.Force, 5)
			if err != nil {
				log.Fatal(i18n.G("removing volumes failed: %s", err))
			}

			log.Info(i18n.G("%d volume(s) removed successfully", len(volumeNames)))
		} else {
			log.Info(i18n.G("no volumes to remove"))
		}

		if err = os.Remove(app.Path); err != nil {
			log.Fatal(err)
		}

		log.Info(i18n.G("file: %s removed", app.Path))
	},
}
View Source
var AppRestartCommand = &cobra.Command{

	Use:     i18n.G("restart <domain> [[service] | --all-services] [flags]"),
	Aliases: strings.Split(appRestartAliases, ","),

	Short: i18n.G("Restart an app"),
	Long: i18n.G(`This command restarts services within a deployed app.

Run "abra app ps <domain>" to see a list of service names.

Pass "--all-services/-a" to restart all services.`),
	Example: i18n.G(`  # restart a single app service
  abra app restart 1312.net app

  # restart all app services
  abra app restart 1312.net -a`),
	Args: cobra.RangeArgs(1, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			if !allServices {
				return autocomplete.ServiceNameComplete(args[0])
			}
			return nil, cobra.ShellCompDirectiveDefault
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		var serviceName string
		if len(args) == 2 {
			serviceName = args[1]
		}

		if serviceName == "" && !allServices {
			log.Fatal(i18n.G("missing [service]"))
		}

		if serviceName != "" && allServices {
			log.Fatal(i18n.G("cannot use [service] and --all-services/-a together"))
		}

		var serviceNames []string
		if allServices {
			var err error
			serviceNames, err = appPkg.GetAppServiceNames(app.Name)
			if err != nil {
				log.Fatal(err)
			}
		} else {
			serviceNames = append(serviceNames, serviceName)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if !deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s is not deployed?", app.Name))
		}

		for _, serviceName := range serviceNames {
			stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)

			service, _, err := cl.ServiceInspectWithRaw(
				context.Background(),
				stackServiceName,
				types.ServiceInspectOptions{},
			)
			if err != nil {
				log.Fatal(err)
			}

			log.Debug(i18n.G("attempting to scale %s to 0", stackServiceName))

			if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil {
				log.Fatal(err)
			}

			f, err := app.Filters(true, false, serviceName)
			if err != nil {
				log.Fatal(err)
			}

			waitOpts := stack.WaitOpts{
				Services:   []ui.ServiceMeta{{Name: stackServiceName, ID: service.ID}},
				AppName:    app.Name,
				ServerName: app.Server,
				Filters:    f,
				NoInput:    internal.NoInput,
				NoLog:      true,
				Quiet:      true,
			}

			if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
				log.Fatal(err)
			}

			log.Debug(i18n.G("%s has been scaled to 0", stackServiceName))
			log.Debug(i18n.G("attempting to scale %s to 1", stackServiceName))

			if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 1); err != nil {
				log.Fatal(err)
			}

			if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
				log.Fatal(err)
			}

			log.Debug(i18n.G("%s has been scaled to 1", stackServiceName))
			log.Info(i18n.G("%s service successfully restarted", serviceName))
		}
	},
}
View Source
var AppRestoreCommand = &cobra.Command{

	Use:     i18n.G("restore <domain> [flags]"),
	Aliases: strings.Split(appRestoreAliases, ","),

	Short: i18n.G("Restore a snapshot"),
	Long: i18n.G(`Snapshots are restored while apps are deployed.

Some restore scenarios may require service / app restarts.`),
	Args: cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		targetContainer, err := internal.RetrieveBackupBotContainer(cl)
		if err != nil {
			log.Fatal(err)
		}

		execEnv := []string{
			fmt.Sprintf("SERVICE=%s", app.Domain),
			"MACHINE_LOGS=true",
		}

		if snapshot != "" {
			log.Debug(i18n.G("including SNAPSHOT=%s in backupbot exec invocation", snapshot))
			execEnv = append(execEnv, fmt.Sprintf("SNAPSHOT=%s", snapshot))
		}

		if targetPath != "" {
			log.Debug(i18n.G("including TARGET=%s in backupbot exec invocation", targetPath))
			execEnv = append(execEnv, fmt.Sprintf("TARGET=%s", targetPath))
		}

		if internal.NoInput {
			log.Debug(i18n.G("including NONINTERACTIVE=%v in backupbot exec invocation", internal.NoInput))
			execEnv = append(execEnv, fmt.Sprintf("NONINTERACTIVE=%v", internal.NoInput))
		}

		if len(volumes) > 0 {
			allVolumes := strings.Join(volumes, ",")
			log.Debug(i18n.G("including VOLUMES=%s in backupbot exec invocation", allVolumes))
			execEnv = append(execEnv, fmt.Sprintf("VOLUMES=%s", allVolumes))
		}

		if len(services) > 0 {
			allServices := strings.Join(services, ",")
			log.Debug(i18n.G("including CONTAINER=%s in backupbot exec invocation", allServices))
			execEnv = append(execEnv, fmt.Sprintf("CONTAINER=%s", allServices))
		}

		if hooks {
			log.Debug(i18n.G("including NO_COMMANDS=%v in backupbot exec invocation", false))
			execEnv = append(execEnv, fmt.Sprintf("NO_COMMANDS=%v", false))
		}

		if _, err := internal.RunBackupCmdRemote(cl, "restore", targetContainer.ID, execEnv); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppRollbackCommand = &cobra.Command{

	Use:     i18n.G("rollback <domain> [version] [flags]"),
	Aliases: strings.Split(appRollbackAliases, ","),

	Short: i18n.G("Roll an app back to a previous version"),
	Long: i18n.G(`This command rolls an app back to a previous version.

Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]".

It is possible to "--force/-f" an downgrade if you want to re-deploy a specific
version.

Only the deployed version is consulted when trying to determine what downgrades
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.

A downgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`),
	Example: i18n.G(` # standard rollback
  abra app rollback 1312.net

  # rollback to specific version
  abra app rollback 1312.net 2.0.0+1.2.3`),
	Args: cobra.RangeArgs(1, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			app, err := appPkg.Get(args[0])
			if err != nil {
				return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
			}
			return autocomplete.RecipeVersionComplete(app.Recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		var (
			downgradeWarnMessages []string
			chosenDowngrade       string
			availableDowngrades   []string
		)

		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := ensureDeployed(cl, app)
		if err != nil {
			log.Fatal(err)
		}

		if err := lint.LintForErrors(app.Recipe); err != nil {
			log.Fatal(err)
		}

		versions, err := app.Recipe.Tags()
		if err != nil {
			log.Fatal(err)
		}

		if deployMeta.Version == config.UNKNOWN_DEFAULT {
			availableDowngrades = versions
		}

		if len(args) == 2 && args[1] != "" {
			chosenDowngrade = args[1]

			if err := validateDowngradeVersionArg(chosenDowngrade, app, deployMeta); err != nil {
				log.Fatal(err)
			}

			availableDowngrades = append(availableDowngrades, chosenDowngrade)
		}

		if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenDowngrade == "" {
			downgradeAvailable, err := ensureDowngradesAvailable(versions, &availableDowngrades, deployMeta)
			if err != nil {
				log.Fatal(err)
			}

			if !downgradeAvailable {
				log.Info(i18n.G("no available downgrades"))
				return
			}
		}

		if internal.Force || internal.NoInput || chosenDowngrade != "" {
			if len(availableDowngrades) > 0 {
				chosenDowngrade = availableDowngrades[len(availableDowngrades)-1]
			}
		} else {
			if err := chooseDowngrade(availableDowngrades, deployMeta, &chosenDowngrade); err != nil {
				log.Fatal(err)
			}
		}

		if internal.Force &&
			chosenDowngrade == "" &&
			deployMeta.Version != config.UNKNOWN_DEFAULT {
			chosenDowngrade = deployMeta.Version
		}

		if chosenDowngrade == "" {
			log.Fatal(i18n.G("unknown deployed version, unable to downgrade"))
		}

		log.Debug(i18n.G("choosing %s as version to rollback", chosenDowngrade))

		if _, err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
			log.Fatal(err)
		}

		if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
			log.Fatal(err)
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		stackName := app.StackName()
		deployOpts := stack.Deploy{
			Composefiles: composeFiles,
			Namespace:    stackName,
			Prune:        false,
			ResolveImage: stack.ResolveImageAlways,
			Detach:       false,
		}

		compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
		if err != nil {
			log.Fatal(err)
		}

		newRecipeWithDowngradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenDowngrade)
		appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDowngradeVersion)

		appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
		appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
		if internal.Chaos {
			appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
		}

		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		deployedVersion := deployMeta.Version
		if deployMeta.IsChaos {
			deployedVersion = deployMeta.ChaosVersion
		}

		if err := internal.DeployOverview(
			app,
			deployedVersion,
			chosenDowngrade,
			"",
			downgradeWarnMessages,
			secretInfo,
			configInfo,
			imageInfo,
		); err != nil {
			log.Fatal(err)
		}

		stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
		if err != nil {
			log.Fatal(err)
		}

		serviceNames, err := appPkg.GetAppServiceNames(app.Name)
		if err != nil {
			log.Fatal(err)
		}

		f, err := app.Filters(true, false, serviceNames...)
		if err != nil {
			log.Fatal(err)
		}

		if err := stack.RunDeploy(
			cl,
			deployOpts,
			compose,
			stackName,
			app.Server,
			internal.DontWaitConverge,
			internal.NoInput,
			f,
		); err != nil {
			log.Fatal(err)
		}

		if err := app.WriteRecipeVersion(chosenDowngrade, false); err != nil {
			log.Fatal(i18n.G("writing recipe version failed: %s", err))
		}
	},
}
View Source
var AppRunCommand = &cobra.Command{

	Use:     i18n.G("run <domain> <service> <cmd> [[args] [flags] | [flags] -- [args]]"),
	Aliases: strings.Split(appRunAliases, ","),

	Short: i18n.G("Run a command inside a service container"),
	Example: i18n.G(`  # run <cmd> with args/flags
  abra app run 1312.net app -- ls -lha

  # run <cmd> without args/flags
  abra app run 1312.net app bash --user nobody

  # run <cmd> with both kinds of args/flags 
  abra app run 1312.net app --user nobody -- ls -lha`),
	Args: cobra.MinimumNArgs(3),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			return autocomplete.ServiceNameComplete(args[0])
		case 2:
			return autocomplete.CommandNameComplete(args[0])
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		serviceName := args[1]
		stackAndServiceName := fmt.Sprintf("^%s_%s", app.StackName(), serviceName)

		filters := filters.NewArgs()
		filters.Add("name", stackAndServiceName)

		targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, false)
		if err != nil {
			log.Fatal(err)
		}

		userCmd := args[2:]
		execCreateOpts := containertypes.ExecOptions{
			AttachStderr: true,
			AttachStdin:  true,
			AttachStdout: true,
			Cmd:          userCmd,
			Detach:       false,
			Tty:          true,
		}

		if runAsUser != "" {
			execCreateOpts.User = runAsUser
		}
		if noTTY {
			execCreateOpts.Tty = false
		}

		dcli, err := command.NewDockerCli()
		if err != nil {
			log.Fatal(err)
		}

		if _, err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
			log.Fatal(err)
		}
	},
}
View Source
var AppSecretCommand = &cobra.Command{

	Use:     i18n.G("secret [cmd] [args] [flags]"),
	Aliases: []string{i18n.G("s")},

	Short: i18n.G("Manage app secrets"),
}
View Source
var AppSecretGenerateCommand = &cobra.Command{

	Use:     i18n.G("generate <domain> [[secret] [version] | --all] [flags]"),
	Aliases: strings.Split(appSecretGenerateAliases, ","),

	Short: i18n.G("Generate secrets"),
	Args:  cobra.RangeArgs(1, 3),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string,
	) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			app, err := appPkg.Get(args[0])
			if err != nil {
				return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
			}
			return autocomplete.SecretComplete(app.Recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		if len(args) <= 2 && !generateAllSecrets {
			log.Fatal(i18n.G("missing arguments [secret]/[version] or '--all'"))
		}

		if len(args) > 2 && generateAllSecrets {
			log.Fatal(i18n.G("cannot use '[secret] [version]' and '--all' together"))
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if !generateAllSecrets {
			secretName := args[1]
			secretVersion := args[2]
			s, ok := secrets[secretName]
			if !ok {
				log.Fatal(i18n.G("%s doesn't exist in the env config?", secretName))
			}
			s.Version = secretVersion
			secrets = map[string]secret.Secret{
				secretName: s,
			}
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
		if err != nil {
			log.Fatal(err)
		}

		if storeInPass {
			for name, data := range secretVals {
				if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
					log.Fatal(err)
				}
			}
		}

		if len(secretVals) == 0 {
			log.Warn(i18n.G("no secrets generated"))
			os.Exit(1)
		}

		headers := []string{i18n.G("NAME"), i18n.G("VALUE")}
		table, err := formatter.CreateTable()
		if err != nil {
			log.Fatal(err)
		}

		table.Headers(headers...)

		var rows [][]string
		for name, val := range secretVals {
			row := []string{name, val}
			rows = append(rows, row)
			table.Row(row...)
		}

		if internal.MachineReadable {
			out, err := formatter.ToJSON(headers, rows)
			if err != nil {
				log.Fatal(i18n.G("unable to render to JSON: %s", err))
			}
			fmt.Println(out)
			return
		}

		if err := formatter.PrintTable(table); err != nil {
			log.Fatal(err)
		}

		log.Warn(i18n.G(
			"generated secrets %s shown again, please take note of them %s",
			formatter.BoldStyle.Render(i18n.G("NOT")),
			formatter.BoldStyle.Render(i18n.G("NOW")),
		))
	},
}
View Source
var AppSecretInsertCommand = &cobra.Command{

	Use:     i18n.G("insert <domain> <secret> <version> [<data>] [flags]"),
	Aliases: strings.Split(appSecretInsertAliases, ","),

	Short: i18n.G("Insert secret"),
	Long: i18n.G(`This command inserts a secret into an app environment.

Arbitrary secret insertion is not supported. Secrets that are inserted must
match those configured in the recipe beforehand.

This command can be useful when you want to manually generate secrets for an app
environment. Typically, you can let Abra generate them for you on app creation
(see "abra app new --secrets/-S" for more).`),
	Example: i18n.G(`  # insert regular secret
  abra app secret insert 1312.net my_secret v1 mySuperSecret

  # insert secret as file
  abra app secret insert 1312.net my_secret v1 secret.txt -f

  # insert secret from stdin
  echo "mmySuperSecret" | abra app secret insert 1312.net my_secret v1`),
	Args: cobra.MinimumNArgs(3),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string,
	) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			app, err := appPkg.Get(args[0])
			if err != nil {
				return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
			}
			return autocomplete.SecretComplete(app.Recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveDefault
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		name := args[1]
		version := args[2]
		data, err := readSecretData(args)
		if err != nil {
			log.Fatal(err)
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		var isRecipeSecret bool
		for secretName := range secrets {
			if secretName == name {
				isRecipeSecret = true
			}
		}
		if !isRecipeSecret {
			log.Fatal(i18n.G("no secret %s available for recipe %s?", name, app.Recipe.Name))
		}

		if insertFromFile {
			raw, err := os.ReadFile(data)
			if err != nil {
				log.Fatal(i18n.G("reading secret from file: %s", err))
			}
			data = string(raw)
		}

		if trimInput {
			data = strings.TrimSpace(data)
		}

		secretName := fmt.Sprintf("%s_%s_%s", app.StackName(), name, version)
		if err := client.StoreSecret(cl, secretName, data); err != nil {
			log.Fatal(err)
		}

		log.Info(i18n.G("%s successfully stored on server", secretName))

		if storeInPass {
			if err := secret.PassInsertSecret(data, name, app.Name, app.Server); err != nil {
				log.Fatal(err)
			}
		}
	},
}
View Source
var AppSecretLsCommand = &cobra.Command{

	Use:     i18n.G("list <domain>"),
	Aliases: strings.Split(appSecretLsAliases, ","),

	Short: i18n.G("List all secrets"),
	Args:  cobra.MinimumNArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string,
	) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		headers := []string{i18n.G("NAME"), i18n.G("VERSION"), i18n.G("GENERATED NAME"), i18n.G("CREATED ON SERVER")}
		table, err := formatter.CreateTable()
		if err != nil {
			log.Fatal(err)
		}

		table.Headers(headers...)

		secStats, err := secret.PollSecretsStatus(cl, app)
		if err != nil {
			log.Fatal(err)
		}

		sort.Slice(secStats, func(i, j int) bool {
			return secStats[i].LocalName < secStats[j].LocalName
		})
		var rows [][]string
		for _, secStat := range secStats {
			row := []string{
				secStat.LocalName,
				secStat.Version,
				secStat.RemoteName,
				strconv.FormatBool(secStat.CreatedOnRemote),
			}

			rows = append(rows, row)
			table.Row(row...)
		}

		if len(rows) > 0 {
			if internal.MachineReadable {
				out, err := formatter.ToJSON(headers, rows)
				if err != nil {
					log.Fatal(i18n.G("unable to render to JSON: %s", err))
				}
				fmt.Println(out)
				return
			}

			if err := formatter.PrintTable(table); err != nil {
				log.Fatal(err)
			}

			return
		}

		log.Warn(i18n.G("no secrets stored for %s", app.Name))
	},
}
View Source
var AppSecretRmCommand = &cobra.Command{

	Use:     i18n.G("remove <domain> [[secret] | --all] [flags]"),
	Aliases: strings.Split(appSecretRemoveAliases, ","),

	Short: i18n.G("Remove a secret"),
	Long: i18n.G(`This command removes a secret from an app environment.

Arbitrary secret removal is not supported. Secrets that are removed must
match those configured in the recipe beforehand.`),
	Example: i18n.G("  abra app secret rm 1312.net oauth_key"),
	Args:    cobra.RangeArgs(1, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string,
	) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			if !rmAllSecrets {
				app, err := appPkg.Get(args[0])
				if err != nil {
					return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
				}
				return autocomplete.SecretComplete(app.Recipe.Name)
			}
			return nil, cobra.ShellCompDirectiveDefault
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if len(args) == 2 && rmAllSecrets {
			log.Fatal(i18n.G("cannot use [secret] and --all/-a together"))
		}

		if len(args) != 2 && !rmAllSecrets {
			log.Fatal(i18n.G("no secret(s) specified?"))
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		filters, err := app.Filters(false, false)
		if err != nil {
			log.Fatal(err)
		}

		secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filters})
		if err != nil {
			log.Fatal(err)
		}

		remoteSecretNames := make(map[string]bool)
		for _, cont := range secretList {
			remoteSecretNames[cont.Spec.Annotations.Name] = true
		}

		var secretToRm string
		if len(args) == 2 {
			secretToRm = args[1]
		}

		match := false
		for secretName, val := range secrets {
			secretRemoteName := fmt.Sprintf("%s_%s_%s", app.StackName(), secretName, val.Version)
			if _, ok := remoteSecretNames[secretRemoteName]; ok {
				if secretToRm != "" {
					if secretName == secretToRm {
						if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
							log.Fatal(err)
						}

						return
					}
				} else {
					match = true

					if err := secretRm(cl, app, secretRemoteName, secretName); err != nil {
						log.Fatal(err)
					}
				}
			}
		}

		if !match && secretToRm != "" {
			log.Fatal(i18n.G("%s doesn't exist on server?", secretToRm))
		}

		if !match {
			log.Fatal(i18n.G("no secrets to remove?"))
		}
	},
}
View Source
var AppServicesCommand = &cobra.Command{

	Use:     i18n.G("services <domain> [flags]"),
	Aliases: strings.Split(appServicesAliases, ","),

	Short: i18n.G("Display all services of an app"),
	Args:  cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if !deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s is not deployed?", app.Name))
		}

		filters, err := app.Filters(true, true)
		if err != nil {
			log.Fatal(err)
		}

		containers, err := cl.ContainerList(context.Background(), containerTypes.ListOptions{Filters: filters})
		if err != nil {
			log.Fatal(err)
		}

		table, err := formatter.CreateTable()
		if err != nil {
			log.Fatal(err)
		}

		headers := []string{i18n.G("SERVICE (SHORT)"), i18n.G("SERVICE (LONG)")}
		table.Headers(headers...)

		var rows [][]string
		for _, container := range containers {
			var containerNames []string
			for _, containerName := range container.Names {
				trimmed := strings.TrimPrefix(containerName, "/")
				containerNames = append(containerNames, trimmed)
			}

			serviceShortName := service.ContainerToServiceName(container.Names, app.StackName())
			serviceLongName := fmt.Sprintf("%s_%s", app.StackName(), serviceShortName)

			row := []string{
				serviceShortName,
				serviceLongName,
			}

			rows = append(rows, row)
		}

		table.Rows(rows...)

		if len(rows) > 0 {
			if err := formatter.PrintTable(table); err != nil {
				log.Fatal(err)
			}
		}
	},
}
View Source
var AppUndeployCommand = &cobra.Command{

	Use: i18n.G("undeploy <domain> [flags]"),

	Aliases: strings.Split(appUndeployAliases, ","),
	Short:   i18n.G("Undeploy a deployed app"),
	Long: i18n.G(`This does not destroy any application data.

However, you should remain vigilant, as your swarm installation will consider
any previously attached volumes as eligible for pruning once undeployed.

Passing "--prune/-p" does not remove those volumes.`),
	Args: cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)
		stackName := app.StackName()

		if err := app.Recipe.EnsureExists(); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		log.Debug(i18n.G("checking whether %s is already deployed", stackName))

		deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
		if err != nil {
			log.Fatal(err)
		}

		if !deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s is not deployed?", app.Name))
		}

		version := deployMeta.Version
		if deployMeta.IsChaos {
			version = deployMeta.ChaosVersion
		}

		if err := internal.DeployOverview(
			app,
			version,
			config.MISSING_DEFAULT,
			"",
			nil,
			nil,
			nil,
			nil,
		); err != nil {
			log.Fatal(err)
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
		compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
		if err != nil {
			log.Fatal(err)
		}

		stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
		if err != nil {
			log.Fatal(err)
		}

		rmOpts := stack.Remove{
			Namespaces: []string{stackName},
			Detach:     false,
		}
		if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
			log.Fatal(err)
		}

		if prune {
			if err := pruneApp(cl, app); err != nil {
				log.Fatal(err)
			}
		}

		log.Info(i18n.G("undeploy succeeded 🟢"))

		if err := app.WriteRecipeVersion(version, false); err != nil {
			log.Fatal(i18n.G("writing recipe version failed: %s", err))
		}
	},
}
View Source
var AppUpgradeCommand = &cobra.Command{

	Use:     i18n.G("upgrade <domain> [version] [flags]"),
	Aliases: strings.Split(appUpgradeAliases, ","),

	Short: i18n.G("Upgrade an app"),
	Long: i18n.G(`Upgrade an app.

Unlike "abra app deploy", chaos operations are not supported here. Only recipe
versions are supported values for "[version]".

It is possible to "--force/-f" an upgrade if you want to re-deploy a specific
version.

Only the deployed version is consulted when trying to determine what upgrades
are available. The live deployment version is the "source of truth" in this
case. The stored .env version is not consulted.

An upgrade can be destructive, please ensure you have a copy of your app data
beforehand. See "abra app backup" for more.`),
	Args: cobra.RangeArgs(1, 2),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string,
	) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			app, err := appPkg.Get(args[0])
			if err != nil {
				return []string{i18n.G("autocomplete failed: %s", err)}, cobra.ShellCompDirectiveError
			}
			return autocomplete.RecipeVersionComplete(app.Recipe.Name)
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		var (
			upgradeWarnMessages []string
			chosenUpgrade       string
			availableUpgrades   []string
			upgradeReleaseNotes string
		)

		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(recipe.EnsureContext{
			Chaos:   internal.Chaos,
			Offline: internal.Offline,

			IgnoreEnvVersion: true,
		}); err != nil {
			log.Fatal(err)
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := ensureDeployed(cl, app)
		if err != nil {
			log.Fatal(err)
		}

		if err := lint.LintForErrors(app.Recipe); err != nil {
			log.Fatal(err)
		}

		versions, err := app.Recipe.Tags()
		if err != nil {
			log.Fatal(err)
		}

		if deployMeta.Version == config.UNKNOWN_DEFAULT {
			availableUpgrades = versions
		}

		if len(args) == 2 && args[1] != "" {
			chosenUpgrade = args[1]

			if err := validateUpgradeVersionArg(chosenUpgrade, app, deployMeta); err != nil {
				log.Fatal(err)
			}

			availableUpgrades = append(availableUpgrades, chosenUpgrade)
		}

		if deployMeta.Version != config.UNKNOWN_DEFAULT && chosenUpgrade == "" {
			upgradeAvailable, err := ensureUpgradesAvailable(app, versions, &availableUpgrades, deployMeta)
			if err != nil {
				log.Fatal(err)
			}

			if !upgradeAvailable {
				log.Info(i18n.G("no available upgrades"))
				return
			}
		}

		if internal.Force || internal.NoInput || chosenUpgrade != "" {
			if len(availableUpgrades) > 0 {
				chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
			}
		} else {
			if err := chooseUpgrade(availableUpgrades, deployMeta, &chosenUpgrade); err != nil {
				log.Fatal(err)
			}
		}

		if internal.Force &&
			chosenUpgrade == "" &&
			deployMeta.Version != config.UNKNOWN_DEFAULT {
			chosenUpgrade = deployMeta.Version
		}

		if chosenUpgrade == "" {
			log.Fatal(i18n.G("unknown deployed version, unable to upgrade"))
		}

		log.Debug(i18n.G("choosing %s as version to upgrade", chosenUpgrade))

		if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil {
			log.Fatal(err)
		}

		if _, err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
			log.Fatal(err)
		}

		if err := deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
			log.Fatal(err)
		}

		composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
		if err != nil {
			log.Fatal(err)
		}

		stackName := app.StackName()
		deployOpts := stack.Deploy{
			Composefiles: composeFiles,
			Namespace:    stackName,
			Prune:        false,
			ResolveImage: stack.ResolveImageAlways,
			Detach:       false,
		}

		compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
		if err != nil {
			log.Fatal(err)
		}

		newRecipeWithUpgradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenUpgrade)
		appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithUpgradeVersion)

		appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
		appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
		if internal.Chaos {
			appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
		}

		envVars, err := appPkg.CheckEnv(app)
		if err != nil {
			log.Fatal(err)
		}

		for _, envVar := range envVars {
			if !envVar.Present {
				upgradeWarnMessages = append(upgradeWarnMessages,
					i18n.G("%s missing from %s.env", envVar.Name, app.Domain),
				)
			}
		}

		secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		configInfo, err := deploy.GatherConfigsForDeploy(cl, app, compose, app.Env, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		imageInfo, err := deploy.GatherImagesForDeploy(cl, app, compose, internal.ShowUnchanged)
		if err != nil {
			log.Fatal(err)
		}

		if showReleaseNotes {
			fmt.Print(upgradeReleaseNotes)
			return
		}

		if upgradeReleaseNotes == "" {
			upgradeWarnMessages = append(
				upgradeWarnMessages,
				fmt.Sprintf("no release notes available for %s", chosenUpgrade),
			)
		}

		deployedVersion := deployMeta.Version
		if deployMeta.IsChaos {
			deployedVersion = deployMeta.ChaosVersion
		}

		if err := internal.DeployOverview(
			app,
			deployedVersion,
			chosenUpgrade,
			upgradeReleaseNotes,
			upgradeWarnMessages,
			secretInfo,
			configInfo,
			imageInfo,
		); err != nil {
			log.Fatal(err)
		}

		stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
		if err != nil {
			log.Fatal(err)
		}

		serviceNames, err := appPkg.GetAppServiceNames(app.Name)
		if err != nil {
			log.Fatal(err)
		}

		f, err := app.Filters(true, false, serviceNames...)
		if err != nil {
			log.Fatal(err)
		}

		if err := stack.RunDeploy(
			cl,
			deployOpts,
			compose,
			stackName,
			app.Server,
			internal.DontWaitConverge,
			internal.NoInput,
			f,
		); err != nil {
			log.Fatal(err)
		}

		postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"]
		if ok && !internal.DontWaitConverge {
			log.Debug(i18n.G("run the following post-deploy commands: %s", postDeployCmds))

			if err := internal.PostCmds(cl, app, postDeployCmds); err != nil {
				log.Fatal(i18n.G("attempting to run post deploy commands, saw: %s", err))
			}
		}

		if err := app.WriteRecipeVersion(chosenUpgrade, false); err != nil {
			log.Fatal(i18n.G("writing recipe version failed: %s", err))
		}
	},
}
View Source
var AppVolumeCommand = &cobra.Command{

	Use:     i18n.G("volume [cmd] [args] [flags]"),
	Aliases: strings.Split(appVolumeAliases, ","),
	Short:   i18n.G("Manage app volumes"),
}
View Source
var AppVolumeListCommand = &cobra.Command{

	Use:     i18n.G("list <domain> [flags]"),
	Aliases: strings.Split(appVolumeListAliases, ","),

	Short: i18n.G("List volumes associated with an app"),
	Args:  cobra.ExactArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		filters, err := app.Filters(false, true)
		if err != nil {
			log.Fatal(err)
		}

		volumes, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
		if err != nil {
			log.Fatal(err)
		}

		headers := []string{i18n.G("NAME"), i18n.G("ON SERVER")}

		table, err := formatter.CreateTable()
		if err != nil {
			log.Fatal(err)
		}

		table.Headers(headers...)

		var rows [][]string
		for _, volume := range volumes {
			row := []string{volume.Name, volume.Mountpoint}
			rows = append(rows, row)
		}

		table.Rows(rows...)

		if len(rows) > 0 {
			if err := formatter.PrintTable(table); err != nil {
				log.Fatal(err)
			}
			return
		}

		log.Warn(i18n.G("no volumes created for %s", app.Name))
	},
}
View Source
var AppVolumeRemoveCommand = &cobra.Command{

	Use: i18n.G("remove <domain> [volume] [flags]"),

	Short: i18n.G("Remove volume(s) associated with an app"),
	Long: i18n.G(`Remove volumes associated with an app.

The app in question must be undeployed before you try to remove volumes. See
"abra app undeploy <domain>" for more.

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.`),
	Example: i18n.G(`  # delete volumes interactively
  abra app volume rm 1312.net

  # delete specific volume
  abra app volume rm 1312.net my_volume`),
	Aliases: strings.Split(appVolumeRemoveAliases, ","),
	Args:    cobra.MinimumNArgs(1),
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		return autocomplete.AppNameComplete()
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		var volumeToDelete string
		if len(args) == 2 {
			volumeToDelete = args[1]
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		deployMeta, err := stack.IsDeployed(context.Background(), cl, app.StackName())
		if err != nil {
			log.Fatal(err)
		}

		if deployMeta.IsDeployed {
			log.Fatal(i18n.G("%s is still deployed. Run \"abra app undeploy %s\"", app.Name, app.Name))
		}

		filters, err := app.Filters(false, true)
		if err != nil {
			log.Fatal(err)
		}

		volumeList, err := client.GetVolumes(cl, context.Background(), app.Server, filters)
		if err != nil {
			log.Fatal(err)
		}
		volumeNames := client.GetVolumeNames(volumeList)

		if volumeToDelete != "" {
			var exactMatch bool

			fullVolumeToDeleteName := fmt.Sprintf("%s_%s", app.StackName(), volumeToDelete)
			for _, volName := range volumeNames {
				if volName == fullVolumeToDeleteName {
					exactMatch = true
				}
			}

			if !exactMatch {
				log.Fatal(i18n.G("unable to remove volume: no volume with name '%s'?", volumeToDelete))
			}

			err := client.RemoveVolumes(cl, context.Background(), []string{fullVolumeToDeleteName}, internal.Force, 5)
			if err != nil {
				log.Fatal(i18n.G("removing volume %s failed: %s", volumeToDelete, err))
			}

			log.Info(i18n.G("volume %s removed successfully", volumeToDelete))

			return
		}

		var volumesToRemove []string
		if !internal.Force && !internal.NoInput {
			volumesPrompt := &survey.MultiSelect{
				Message: i18n.G("which volumes do you want to remove?"),
				Help:    i18n.G("'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled"),
				VimMode: true,
				Options: volumeNames,
				Default: volumeNames,
			}
			if err := survey.AskOne(volumesPrompt, &volumesToRemove); err != nil {
				log.Fatal(err)
			}
		}

		if internal.Force || internal.NoInput {
			volumesToRemove = volumeNames
		}

		if len(volumesToRemove) > 0 {
			err := client.RemoveVolumes(cl, context.Background(), volumesToRemove, internal.Force, 5)
			if err != nil {
				log.Fatal(i18n.G("removing volumes failed: %s", err))
			}

			log.Info(i18n.G("%d volumes removed successfully", len(volumesToRemove)))
		} else {
			log.Info(i18n.G("no volumes removed"))
		}
	},
}

Functions

func CopyFromContainer

func CopyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error

CopyFromContainer copies a file or directory from the given container to the local file system. See the possible copy modes and their documentation.

func CopyToContainer

func CopyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error

CopyToContainer copies a file or directory from the local file system to the container. See the possible copy modes and their documentation.

Types

type AppResources

type AppResources struct {
	Secrets    map[string]string
	SecretList []swarm.Secret
	Volumes    map[string]containertypes.MountPoint
}

func (*AppResources) SecretNames

func (a *AppResources) SecretNames() []string

func (*AppResources) VolumeNames

func (a *AppResources) VolumeNames() []string

type AppSecrets

type AppSecrets map[string]string

AppSecrets represents all app secrest

type CopyMode

type CopyMode int

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL