Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/burnless/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func main() {
rootCmd.AddCommand(cli.NewInitCmd())
rootCmd.AddCommand(cli.NewValidateCmd())
rootCmd.AddCommand(cli.NewToilCmd())
rootCmd.AddCommand(cli.NewDiffCmd())

if err := rootCmd.Execute(); err != nil {
os.Exit(1)
Expand Down
175 changes: 175 additions & 0 deletions internal/cli/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package cli

import (
"fmt"

"github.com/burnless/burnless/internal/config"

"github.com/spf13/cobra"
)

// NewDiffCmd creates the "burnless diff" command.
func NewDiffCmd() *cobra.Command {
return &cobra.Command{
Use: "diff [file] [new-file]",
Short: "Show what burnless apply would create, or diff two sre.yaml files",
Args: cobra.MaximumNArgs(2),
SilenceUsage: true,
RunE: runDiff,
}
}

func runDiff(cmd *cobra.Command, args []string) error {
// Option A — two files provided: compare them
if len(args) == 2 {
return runFileDiff(args[0], args[1])
}

// Option C — zero or one file: show what apply would do
path := "sre.yaml"
if len(args) == 1 {
path = args[0]
}
return runApplyPreview(path)
}

// runApplyPreview shows what "burnless apply" would create from sre.yaml.
func runApplyPreview(path string) error {
cfg, err := config.Load(path)
if err != nil {
return fmt.Errorf("loading %s: %w", path, err)
}

fmt.Printf("Loaded: %s\n", path)
fmt.Printf("Service: %s\n", cfg.Service)
if cfg.Team != "" {
fmt.Printf("Team: %s\n", cfg.Team)
}
fmt.Println()
fmt.Println("Would create:")

// SLOs
for _, slo := range cfg.SLOs {
fmt.Printf(" + SLO: %s (target: %.1f%%, window: %s)\n",
slo.Name, slo.Target, slo.Window)
}

// Prometheus alerting rules
alertCount := len(cfg.ErrorBudget.BurnRateAlerts)
if alertCount > 0 {
fmt.Printf(" + %d Prometheus alerting rule(s)\n", alertCount)
}

// Runbooks
autoCount := 0
semiCount := 0
for _, rb := range cfg.Runbooks {
if rb.Mode == "auto" {
autoCount++
} else {
semiCount++
}
}
if len(cfg.Runbooks) > 0 {
fmt.Printf(" + %d runbook(s) (%d auto, %d semi-auto)\n",
len(cfg.Runbooks), autoCount, semiCount)
}

// Grafana dashboard
if cfg.Dashboards.AutoGenerate {
fmt.Printf(" + 1 Grafana dashboard (%d panel(s)) via %s\n",
len(cfg.Dashboards.Panels), cfg.Dashboards.Provider)
}

// On-call
if cfg.OnCall.Provider != "" {
fmt.Printf(" + 1 on-call policy via %s (escalation: %d min)\n",
cfg.OnCall.Provider, cfg.OnCall.EscalationMinutes)
}

fmt.Println()
fmt.Println("Run 'burnless apply' to deploy these changes.")
return nil
}

// runFileDiff compares two sre.yaml files and shows what changed.
func runFileDiff(oldPath, newPath string) error {
oldCfg, err := config.Load(oldPath)
if err != nil {
return fmt.Errorf("loading %s: %w", oldPath, err)
}

newCfg, err := config.Load(newPath)
if err != nil {
return fmt.Errorf("loading %s: %w", newPath, err)
}

fmt.Printf("Comparing %s → %s\n\n", oldPath, newPath)

changed := false

// service
if oldCfg.Service != newCfg.Service {
fmt.Printf(" ~ service: %s → %s\n", oldCfg.Service, newCfg.Service)
changed = true
} else {
fmt.Printf(" service: %s (unchanged)\n", newCfg.Service)
}

// team
if oldCfg.Team != newCfg.Team {
fmt.Printf(" ~ team: %s → %s\n", oldCfg.Team, newCfg.Team)
changed = true
}

// SLOs
oldSLOs := map[string]config.SLO{}
for _, s := range oldCfg.SLOs {
oldSLOs[s.Name] = s
}
newSLOs := map[string]config.SLO{}
for _, s := range newCfg.SLOs {
newSLOs[s.Name] = s
}

for name, ns := range newSLOs {
if os, exists := oldSLOs[name]; !exists {
fmt.Printf(" + SLO: %s added (target: %.1f%%)\n", name, ns.Target)
changed = true
} else if os.Target != ns.Target {
fmt.Printf(" ~ SLO: %s target: %.2f%% → %.2f%%\n", name, os.Target, ns.Target)
changed = true
} else {
fmt.Printf(" SLO: %s (unchanged)\n", name)
}
}
for name := range oldSLOs {
if _, exists := newSLOs[name]; !exists {
fmt.Printf(" - SLO: %s removed\n", name)
changed = true
}
}

// runbooks
for name := range newCfg.Runbooks {
if _, exists := oldCfg.Runbooks[name]; !exists {
fmt.Printf(" + runbook: %s added\n", name)
changed = true
}
}
for name := range oldCfg.Runbooks {
if _, exists := newCfg.Runbooks[name]; !exists {
fmt.Printf(" - runbook: %s removed\n", name)
changed = true
}
}

fmt.Println()
if !changed {
fmt.Println("No changes detected.")
} else {
fmt.Println("Changes detected. Review before running 'burnless apply'.")
}

return nil
}
112 changes: 112 additions & 0 deletions internal/cli/diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package cli

import (
"os"
"path/filepath"
"testing"
)

func TestDiffPreview_ValidFile(t *testing.T) {
tmp := t.TempDir()
sre := `
service: payments-api
team: platform-engineering
slos:
- name: availability
target: 99.9
window: 30d
error_budget:
burn_rate_alerts:
- severity: critical
rate: 14.4
window: 1h
remediate: scale-up
runbooks:
scale-up:
mode: auto
steps:
- kubectl scale deploy/payments --replicas=+2
oncall:
provider: pagerduty
escalation_minutes: 10
notify_slack: "#sre-incidents"
dashboards:
provider: grafana
auto_generate: true
panels:
- error_budget_remaining
`
path := filepath.Join(tmp, "sre.yaml")
if err := os.WriteFile(path, []byte(sre), 0o644); err != nil {
t.Fatalf("failed to write sre.yaml: %v", err)
}

cmd := NewDiffCmd()
cmd.SetArgs([]string{path})
if err := cmd.Execute(); err != nil {
t.Fatalf("expected no error, got: %v", err)
}
}

func TestDiffTwoFiles_DetectsChanges(t *testing.T) {
tmp := t.TempDir()

old := `
service: payments-api
slos:
- name: availability
target: 99.9
window: 30d
`
new := `
service: payments-api
slos:
- name: availability
target: 99.95
window: 30d
- name: latency_p99
target: 99.5
window: 30d
`
oldPath := filepath.Join(tmp, "old.yaml")
newPath := filepath.Join(tmp, "new.yaml")

if err := os.WriteFile(oldPath, []byte(old), 0o644); err != nil {
t.Fatalf("failed to write old.yaml: %v", err)
}
if err := os.WriteFile(newPath, []byte(new), 0o644); err != nil {
t.Fatalf("failed to write new.yaml: %v", err)
}

cmd := NewDiffCmd()
cmd.SetArgs([]string{oldPath, newPath})
if err := cmd.Execute(); err != nil {
t.Fatalf("expected no error, got: %v", err)
}
}

func TestDiffTwoFiles_NoChanges(t *testing.T) {
tmp := t.TempDir()
sre := `
service: payments-api
slos:
- name: availability
target: 99.9
window: 30d
`
path1 := filepath.Join(tmp, "a.yaml")
path2 := filepath.Join(tmp, "b.yaml")

if err := os.WriteFile(path1, []byte(sre), 0o644); err != nil {
t.Fatalf("failed to write a.yaml: %v", err)
}
if err := os.WriteFile(path2, []byte(sre), 0o644); err != nil {
t.Fatalf("failed to write b.yaml: %v", err)
}

cmd := NewDiffCmd()
cmd.SetArgs([]string{path1, path2})
if err := cmd.Execute(); err != nil {
t.Fatalf("expected no error, got: %v", err)
}
}
Loading