-
Notifications
You must be signed in to change notification settings - Fork 90
Expand file tree
/
Copy pathdiff.go
More file actions
197 lines (168 loc) · 5.93 KB
/
Copy pathdiff.go
File metadata and controls
197 lines (168 loc) · 5.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
stderrors "errors"
"fmt"
"log/slog"
"os"
"strings"
"github.com/urfave/cli/v3"
aicr "github.com/NVIDIA/aicr/pkg/client/v1"
"github.com/NVIDIA/aicr/pkg/defaults"
"github.com/NVIDIA/aicr/pkg/errors"
"github.com/NVIDIA/aicr/pkg/serializer"
)
// diffCmd creates the "diff" CLI command.
func diffCmd() *cli.Command {
return &cli.Command{
Name: "diff",
Category: functionalCategoryName,
Usage: "Compare two snapshots to detect configuration drift",
Description: `Compare two snapshots field-by-field to see what changed between
cluster states. Reports added, removed, and modified readings.
Examples:
# Compare two snapshots
aicr diff --baseline before.yaml --target after.yaml
# Human-readable table output
aicr diff --baseline before.yaml --target after.yaml --format table
# JSON output for CI/CD pipelines with non-zero exit on drift
aicr diff --baseline before.yaml --target after.yaml --format json --fail-on-drift
# Compare snapshots from ConfigMaps
aicr diff --baseline cm://default/baseline --target cm://default/current`,
Flags: diffCmdFlags(),
Action: runDiffCmd,
}
}
// diffCmdFlags returns the flags for the diff command.
func diffCmdFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "baseline",
Aliases: []string{"b"},
Usage: "baseline snapshot (file path or ConfigMap URI)",
},
&cli.StringFlag{
Name: "target",
Usage: "target snapshot (file path or ConfigMap URI)",
},
&cli.BoolFlag{
Name: "fail-on-drift",
Usage: "exit with non-zero status if drift is detected",
},
outputFlag(),
formatFlag(),
kubeconfigFlag(),
}
}
// runDiffCmd executes the diff command.
func runDiffCmd(ctx context.Context, cmd *cli.Command) error {
if err := validateSingleValueFlags(cmd, "baseline", "target", "output", "format", "kubeconfig"); err != nil {
return err
}
ctx, cancel := context.WithTimeout(ctx, defaults.CLISnapshotTimeout)
defer cancel()
outFormat, err := parseOutputFormat(cmd)
if err != nil {
return err
}
baselinePath := cmd.String("baseline")
targetPath := cmd.String("target")
if baselinePath == "" {
return errors.New(errors.ErrCodeInvalidRequest, "--baseline is required")
}
if targetPath == "" {
return errors.New(errors.ErrCodeInvalidRequest, "--target is required")
}
kubeconfig := cmd.String("kubeconfig")
slog.Debug("snapshot diff", slog.String("baseline", baselinePath), slog.String("target", targetPath))
client, err := embeddedClient(ctx)
if err != nil {
return err
}
defer func() { _ = client.Close() }()
baseline, err := client.LoadSnapshot(ctx, baselinePath, kubeconfig)
if err != nil {
return err
}
target, err := client.LoadSnapshot(ctx, targetPath, kubeconfig)
if err != nil {
return err
}
result, err := client.DiffSnapshots(ctx, baseline, target, aicr.SnapshotDiffOptions{
BaselineSource: baselinePath,
TargetSource: targetPath,
})
if err != nil {
return err
}
slog.Info("snapshot diff complete",
slog.Int("added", result.Summary.Added),
slog.Int("removed", result.Summary.Removed),
slog.Int("modified", result.Summary.Modified))
if err := writeDiffResult(ctx, cmd, outFormat, kubeconfig, result); err != nil {
return err
}
if cmd.Bool("fail-on-drift") && result.HasDrift() {
return errors.New(errors.ErrCodeConflict,
fmt.Sprintf("drift detected: %d change(s) found", result.Summary.Total))
}
return nil
}
// writeDiffResult serializes the diff result, using a custom table formatter
// when the output format is table. Uses a named return so Close() failures
// on writable handles are merged with any earlier error via errors.Join —
// data-loss on flush must surface even when the write itself also failed.
//
// kubeconfig is propagated through to ConfigMap writers so multi-cluster
// workflows write back to the same cluster the snapshots were read from.
func writeDiffResult(ctx context.Context, cmd *cli.Command, outFormat serializer.Format, kubeconfig string, result *aicr.SnapshotDiff) (err error) {
output := cmd.String("output")
// Use custom table writer for human-readable output
if outFormat == serializer.FormatTable {
output = strings.TrimSpace(output)
if strings.HasPrefix(output, serializer.ConfigMapURIScheme) {
return errors.New(errors.ErrCodeInvalidRequest, "table output does not support ConfigMap destinations")
}
w := cmd.Root().Writer
if output != "" && output != "-" && output != serializer.StdoutURI {
f, createErr := os.Create(output)
if createErr != nil {
return errors.Wrap(errors.ErrCodeInternal, "failed to create output file", createErr)
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
err = stderrors.Join(err, errors.Wrap(errors.ErrCodeInternal, "failed to close output file", closeErr))
}
}()
w = f
}
return aicr.WriteSnapshotDiffTable(w, result)
}
// JSON/YAML use standard serializer; thread kubeconfig so ConfigMap
// writes target the same cluster as the reads above.
ser, err := serializer.NewFileWriterOrStdoutWithKubeconfig(outFormat, output, kubeconfig)
if err != nil {
return err
}
defer func() {
if closer, ok := ser.(interface{ Close() error }); ok {
if closeErr := closer.Close(); closeErr != nil {
err = stderrors.Join(err, errors.Wrap(errors.ErrCodeInternal, "failed to close serializer", closeErr))
}
}
}()
return ser.Serialize(ctx, result)
}