Skip to content

Commit 4373b2f

Browse files
committed
flux diff artifact: Print the differences in human readable form.
I was hoping to use `flux diff artifact` as part of a CI pipeline to show the difference between the merge request and the currently deployed artifact. The existing implementation doesn't work for us, since it only compares the checksums. This commit changes the output produced by `flux diff artifact` to a list of changes in human readable form. The code is using the `dyff` package to produce a semantic diff of the YAML files. That means, for example, that changes in the order of map fields are ignored, while changes in the order of lists are not. Example output: ``` $ ./bin/flux diff artifact "oci://${IMAGE}" --path=example-service/ spec.replicas (apps/v1/Deployment/example-service-t205j6/backend-production) ± value change - 1 + 7 ✗ "oci://registry.gitlab.com/${REDACTED}/example-service-t205j6/deploy:production" and "example-service/" differ ``` The new `--brief` / `-q` flag enables users to revert to the previous behavior of only printing a has changed/has not changed line. Signed-off-by: Florian Forster <[email protected]>
1 parent ec141c6 commit 4373b2f

File tree

1 file changed

+117
-4
lines changed

1 file changed

+117
-4
lines changed

cmd/flux/diff_artifact.go

+117-4
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ limitations under the License.
1717
package main
1818

1919
import (
20+
"bytes"
2021
"context"
22+
"errors"
2123
"fmt"
24+
"io"
2225
"os"
2326

2427
oci "github.com/fluxcd/pkg/oci/client"
2528
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
29+
"github.com/gonvenience/ytbx"
30+
"github.com/homeport/dyff/pkg/dyff"
2631
"github.com/spf13/cobra"
2732

2833
"github.com/fluxcd/flux2/v2/internal/flags"
@@ -42,6 +47,7 @@ type diffArtifactFlags struct {
4247
creds string
4348
provider flags.SourceOCIProvider
4449
ignorePaths []string
50+
brief bool
4551
}
4652

4753
var diffArtifactArgs = newDiffArtifactArgs()
@@ -57,6 +63,7 @@ func init() {
5763
diffArtifactCmd.Flags().StringVar(&diffArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format <username>[:<password>] if --provider is generic")
5864
diffArtifactCmd.Flags().Var(&diffArtifactArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description())
5965
diffArtifactCmd.Flags().StringSliceVar(&diffArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
66+
diffArtifactCmd.Flags().BoolVarP(&diffArtifactArgs.brief, "brief", "q", false, "Just print a line when the resources differ. Does not output a list of changes.")
6067
diffCmd.AddCommand(diffArtifactCmd)
6168
}
6269

@@ -67,7 +74,7 @@ func diffArtifactCmdRun(cmd *cobra.Command, args []string) error {
6774
ociURL := args[0]
6875

6976
if diffArtifactArgs.path == "" {
70-
return fmt.Errorf("invalid path %q", diffArtifactArgs.path)
77+
return errors.New("the '--path' flag is required")
7178
}
7279

7380
url, err := oci.ParseArtifactURL(ociURL)
@@ -103,10 +110,116 @@ func diffArtifactCmdRun(cmd *cobra.Command, args []string) error {
103110
}
104111
}
105112

106-
if err := ociClient.Diff(ctx, url, diffArtifactArgs.path, diffArtifactArgs.ignorePaths); err != nil {
113+
diff, err := diffArtifact(ctx, ociClient, url, diffArtifactArgs.path)
114+
if err != nil {
107115
return err
108116
}
109117

110-
logger.Successf("no changes detected")
111-
return nil
118+
if diff == "" {
119+
logger.Successf("no changes detected")
120+
return nil
121+
}
122+
123+
if !diffArtifactArgs.brief {
124+
fmt.Print(diff)
125+
}
126+
127+
return fmt.Errorf("%q and %q differ", ociURL, diffArtifactArgs.path)
128+
}
129+
130+
func diffArtifact(ctx context.Context, client *oci.Client, remoteURL, localPath string) (string, error) {
131+
localFile, err := loadLocal(localPath)
132+
if err != nil {
133+
return "", err
134+
}
135+
136+
remoteFile, cleanup, err := loadRemote(ctx, client, remoteURL)
137+
if err != nil {
138+
return "", err
139+
}
140+
defer cleanup()
141+
142+
report, err := dyff.CompareInputFiles(remoteFile, localFile,
143+
dyff.KubernetesEntityDetection(true),
144+
)
145+
if err != nil {
146+
return "", fmt.Errorf("dyff.CompareInputFiles(): %w", err)
147+
}
148+
149+
if len(report.Diffs) == 0 {
150+
return "", nil
151+
}
152+
153+
var buf bytes.Buffer
154+
155+
hr := &dyff.HumanReport{
156+
Report: report,
157+
OmitHeader: true,
158+
MultilineContextLines: 3,
159+
}
160+
if err := hr.WriteReport(&buf); err != nil {
161+
return "", fmt.Errorf("WriteReport(): %w", err)
162+
}
163+
164+
return buf.String(), nil
165+
}
166+
167+
func loadLocal(path string) (ytbx.InputFile, error) {
168+
if ytbx.IsStdin(path) {
169+
buf, err := io.ReadAll(os.Stdin)
170+
if err != nil {
171+
return ytbx.InputFile{}, fmt.Errorf("os.ReadAll(os.Stdin): %w", err)
172+
}
173+
174+
nodes, err := ytbx.LoadDocuments(buf)
175+
if err != nil {
176+
return ytbx.InputFile{}, fmt.Errorf("ytbx.LoadDocuments(): %w", err)
177+
}
178+
179+
return ytbx.InputFile{
180+
Location: "STDIN",
181+
Documents: nodes,
182+
}, nil
183+
}
184+
185+
sb, err := os.Stat(path)
186+
if err != nil {
187+
return ytbx.InputFile{}, fmt.Errorf("os.Stat(%q): %w", path, err)
188+
}
189+
190+
if sb.IsDir() {
191+
return ytbx.LoadDirectory(path)
192+
}
193+
194+
return ytbx.LoadFile(path)
195+
}
196+
197+
func loadRemote(ctx context.Context, client *oci.Client, url string) (ytbx.InputFile, func(), error) {
198+
noopCleanup := func() {}
199+
200+
tmpDir, err := os.MkdirTemp("", "flux-diff-artifact")
201+
if err != nil {
202+
return ytbx.InputFile{}, noopCleanup, fmt.Errorf("could not create temporary directory: %w", err)
203+
}
204+
205+
cleanup := func() {
206+
if err := os.RemoveAll(tmpDir); err != nil {
207+
fmt.Fprintf(os.Stderr, "os.RemoveAll(%q): %v\n", tmpDir, err)
208+
}
209+
}
210+
211+
if _, err := client.Pull(ctx, url, tmpDir); err != nil {
212+
cleanup()
213+
return ytbx.InputFile{}, noopCleanup, fmt.Errorf("Pull(%q): %w", url, err)
214+
}
215+
216+
inputFile, err := ytbx.LoadDirectory(tmpDir)
217+
if err != nil {
218+
cleanup()
219+
return ytbx.InputFile{}, noopCleanup, fmt.Errorf("ytbx.LoadDirectory(%q): %w", tmpDir, err)
220+
}
221+
222+
inputFile.Location = url
223+
224+
return inputFile, cleanup, nil
112225
}

0 commit comments

Comments
 (0)