Files
homelab/knecht/drift/drift.go
2026-04-04 14:34:22 +02:00

103 lines
2.6 KiB
Go

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
}