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
Click to show internal directories.
Click to hide internal directories.