package cmd import ( "fmt" "github.com/jensbecker/homelab/knecht/drift" "github.com/jensbecker/homelab/knecht/portainer" "github.com/jensbecker/homelab/knecht/stack" "github.com/spf13/cobra" ) var listCmd = &cobra.Command{ Use: "list", Short: "List all stacks with status and drift", RunE: func(cmd *cobra.Command, args []string) error { _, client, svcPath, err := setup() if err != nil { return err } stacks, err := client.ListStacks() if err != nil { return err } locals, err := stack.Discover(svcPath) if err != nil { return err } localByName := make(map[string]stack.Local, len(locals)) for _, l := range locals { localByName[l.Name] = l } fmt.Printf("%-20s %-10s %s\n", "STACK", "STATUS", "DRIFT") fmt.Println(repeat("-", 60)) for _, s := range stacks { status := statusLabel(s.Status) driftSummary := "no local compose" local, ok := localByName[s.Name] if ok { driftSummary, err = computeDriftSummary(client, &s, &local) if err != nil { driftSummary = "error: " + err.Error() } } fmt.Printf("%-20s %-10s %s\n", s.Name, status, driftSummary) } return nil }, } func init() { rootCmd.AddCommand(listCmd) } func statusLabel(status int) string { if status == 1 { return "running" } return "stopped" } func computeDriftSummary(client *portainer.Client, s *portainer.Stack, local *stack.Local) (string, error) { remoteCompose, err := client.GetStackFile(s.ID) if err != nil { return "", err } localCompose, err := local.ReadCompose() if err != nil { return "", err } exampleKeys, err := local.EnvExampleKeys() if err != nil { return "", err } portainerKeys := make([]string, len(s.Env)) for i, e := range s.Env { portainerKeys[i] = e.Name } composeDiff := drift.Compose(localCompose, remoteCompose) missingKeys, unknownKeys := drift.EnvKeys(exampleKeys, portainerKeys) r := drift.Result{ ComposeDiff: composeDiff, MissingKeys: missingKeys, UnknownKeys: unknownKeys, } return r.Summary(), nil } func repeat(s string, n int) string { out := "" for i := 0; i < n; i++ { out += s } return out }