forked from toolshed/abra
341 lines
9.5 KiB
Go
341 lines
9.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
"coopcloud.tech/abra/pkg/app"
|
|
appPkg "coopcloud.tech/abra/pkg/app"
|
|
"coopcloud.tech/abra/pkg/autocomplete"
|
|
"coopcloud.tech/abra/pkg/client"
|
|
"coopcloud.tech/abra/pkg/config"
|
|
containerPkg "coopcloud.tech/abra/pkg/container"
|
|
contextPkg "coopcloud.tech/abra/pkg/context"
|
|
"coopcloud.tech/abra/pkg/formatter"
|
|
"coopcloud.tech/abra/pkg/i18n"
|
|
"coopcloud.tech/abra/pkg/log"
|
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// translators: `abra app env` aliases. use a comma separated list of aliases
|
|
// with no spaces in between
|
|
var appEnvAliases = i18n.G("e")
|
|
|
|
// translators: `abra app env list` aliases. use a comma separated list of
|
|
// aliases with no spaces in between
|
|
var appEnvListAliases = i18n.G("l,ls")
|
|
|
|
// translators: `abra app env pull` aliases. use a comma separated list of
|
|
// aliases with no spaces in between
|
|
var appEnvPullAliases = i18n.G("pl,p")
|
|
|
|
var AppEnvListCommand = &cobra.Command{
|
|
// translators: `app env list` command
|
|
Use: i18n.G("list <domain> [flags]"),
|
|
Aliases: strings.Split(appEnvListAliases, ","),
|
|
// translators: Short description for `app env list` command
|
|
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)
|
|
},
|
|
}
|
|
|
|
var AppEnvPullCommand = &cobra.Command{
|
|
// translators: `app pull` command
|
|
Use: i18n.G("pull <domain> [flags]"),
|
|
Aliases: strings.Split(appEnvPullAliases, ","),
|
|
// translators: Short description for `app env pull` command
|
|
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
|
|
for _, context := range contexts {
|
|
if context.Name == server || server == "default" {
|
|
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"]))
|
|
}
|
|
},
|
|
}
|
|
|
|
var AppEnvCommand = &cobra.Command{
|
|
// translators: `app env` command group
|
|
Use: i18n.G("env [cmd] [args] [flags]"),
|
|
Aliases: strings.Split(appEnvAliases, ","),
|
|
// translators: Short description for `app env` command group
|
|
Short: i18n.G("Manage app environment values"),
|
|
}
|
|
|
|
var (
|
|
server string
|
|
)
|
|
|
|
func init() {
|
|
AppEnvPullCommand.Flags().BoolVarP(
|
|
&internal.Force,
|
|
i18n.G("force"),
|
|
i18n.G("f"),
|
|
false,
|
|
i18n.G("perform action without further prompt"),
|
|
)
|
|
|
|
AppEnvPullCommand.Flags().StringVarP(
|
|
&server,
|
|
i18n.G("server"),
|
|
i18n.G("s"),
|
|
"",
|
|
i18n.G("server associated with deployed app"),
|
|
)
|
|
|
|
AppEnvPullCommand.RegisterFlagCompletionFunc(
|
|
i18n.G("server"),
|
|
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
return autocomplete.ServerNameComplete()
|
|
},
|
|
)
|
|
}
|