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 [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 [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() }, ) }