package drift import ( "fmt" "strings" ) type Result struct { ComposeDiff []string // unified-style diff lines MissingKeys []string // in .env.example but not in Portainer UnknownKeys []string // in Portainer but not in .env.example (file exists) PortainerOnlyKeys []string // Portainer has keys but no .env.example exists at all } func (r *Result) HasDrift() bool { return len(r.ComposeDiff) > 0 || len(r.MissingKeys) > 0 || len(r.UnknownKeys) > 0 || len(r.PortainerOnlyKeys) > 0 } func (r *Result) Summary() string { parts := []string{} if len(r.ComposeDiff) > 0 { parts = append(parts, fmt.Sprintf("compose ~%d lines", len(r.ComposeDiff))) } if len(r.MissingKeys) > 0 { parts = append(parts, fmt.Sprintf("%d env key(s) missing", len(r.MissingKeys))) } if len(r.UnknownKeys) > 0 { parts = append(parts, fmt.Sprintf("%d env key(s) unknown", len(r.UnknownKeys))) } if len(r.PortainerOnlyKeys) > 0 { parts = append(parts, fmt.Sprintf("no .env.example (%d key(s) in Portainer)", len(r.PortainerOnlyKeys))) } if len(parts) == 0 { return "in sync" } return strings.Join(parts, ", ") } // Compose diffs local vs remote compose content line by line. func Compose(local, remote string) []string { localLines := strings.Split(strings.TrimRight(local, "\n"), "\n") remoteLines := strings.Split(strings.TrimRight(remote, "\n"), "\n") if local == remote { return nil } // Simple line-by-line diff: mark additions and removals. remoteSet := toSet(remoteLines) localSet := toSet(localLines) var diff []string for _, l := range remoteLines { if !localSet[l] { diff = append(diff, "- "+l) } } for _, l := range localLines { if !remoteSet[l] { diff = append(diff, "+ "+l) } } return diff } // EnvKeys compares keys from .env.example against keys set in Portainer. // If exampleKeys is nil (no .env.example file), both slices are returned nil — // use PortainerOnlyKeys on the Result instead to handle that case. func EnvKeys(exampleKeys []string, portainerKeys []string) (missing, unknown []string) { if exampleKeys == nil { return nil, nil } portainerSet := make(map[string]bool, len(portainerKeys)) for _, k := range portainerKeys { portainerSet[k] = true } exampleSet := make(map[string]bool, len(exampleKeys)) for _, k := range exampleKeys { exampleSet[k] = true } for _, k := range exampleKeys { if !portainerSet[k] { missing = append(missing, k) } } for _, k := range portainerKeys { if !exampleSet[k] { unknown = append(unknown, k) } } return } func toSet(lines []string) map[string]bool { s := make(map[string]bool, len(lines)) for _, l := range lines { s[l] = true } return s }