package cmd import ( "fmt" "github.com/jensbecker/homelab/knecht/portainer" "github.com/jensbecker/homelab/knecht/stack" "github.com/jensbecker/homelab/knecht/tui" "github.com/spf13/cobra" ) var pruneEnvFlag bool var updateCmd = &cobra.Command{ Use: "update ", Short: "Update an existing stack, preserving env vars", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { _, client, svcPath, err := setup() if err != nil { return err } name := args[0] local, err := stack.Get(svcPath, name) if err != nil { return err } remote, err := client.GetStackByName(name) if err != nil { return err } if remote == nil { return fmt.Errorf("stack %q not found — use `knecht deploy %s` to create it", name, name) } compose, err := local.ReadCompose() if err != nil { return err } exampleKeys, err := local.EnvExampleKeys() if err != nil { return err } env, err := mergeEnvVars(name, remote.Env, exampleKeys, pruneEnvFlag) if err != nil { return err } if env == nil { fmt.Println("Cancelled.") return nil } s, err := client.UpdateStack(remote.ID, compose, env) if err != nil { return err } fmt.Printf("Updated stack %q (id %d)\n", s.Name, s.ID) return nil }, } // mergeEnvVars preserves existing Portainer env vars and prompts via TUI for // any keys present in .env.example but missing from Portainer. // Returns nil if the user cancelled the form. func mergeEnvVars(stackName string, existing []portainer.EnvVar, exampleKeys []string, prune bool) ([]portainer.EnvVar, error) { envMap := make(map[string]string, len(existing)) for _, e := range existing { envMap[e.Name] = e.Value } // Remove keys not in .env.example when pruning if prune && exampleKeys != nil { allowed := make(map[string]bool, len(exampleKeys)) for _, k := range exampleKeys { allowed[k] = true } for k := range envMap { if !allowed[k] { fmt.Printf("Removing unknown env var %q\n", k) delete(envMap, k) } } } var missing []string for _, key := range exampleKeys { if _, ok := envMap[key]; !ok { missing = append(missing, key) } } if len(missing) > 0 { values, err := tui.PromptMissingEnv(stackName, missing) if err != nil { return nil, err } if values == nil { return nil, nil // cancelled } for k, v := range values { envMap[k] = v } } result := make([]portainer.EnvVar, 0, len(envMap)) for k, v := range envMap { result = append(result, portainer.EnvVar{Name: k, Value: v}) } return result, nil } func init() { updateCmd.Flags().BoolVar(&pruneEnvFlag, "prune-env", false, "remove env vars not defined in .env.example") rootCmd.AddCommand(updateCmd) }