@ -1,5 +1,5 @@
/ *
/ *
Copyright 202 2 The Flux authors
Copyright 202 3 The Flux authors
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
you may not use this file except in compliance with the License .
@ -17,12 +17,18 @@ limitations under the License.
package main
package main
import (
import (
"archive/tar"
"bytes"
"context"
"context"
"fmt"
"fmt"
"io"
"os"
"os"
oci "github.com/fluxcd/pkg/oci/client"
oci "github.com/fluxcd/pkg/oci/client"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/spf13/cobra"
"github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/flags"
@ -42,6 +48,7 @@ type diffArtifactFlags struct {
creds string
creds string
provider flags . SourceOCIProvider
provider flags . SourceOCIProvider
ignorePaths [ ] string
ignorePaths [ ] string
compareTo string // New field to capture the URL or local path for comparison
}
}
var diffArtifactArgs = newDiffArtifactArgs ( )
var diffArtifactArgs = newDiffArtifactArgs ( )
@ -57,6 +64,7 @@ func init() {
diffArtifactCmd . Flags ( ) . StringVar ( & diffArtifactArgs . creds , "creds" , "" , "credentials for OCI registry in the format <username>[:<password>] if --provider is generic" )
diffArtifactCmd . Flags ( ) . StringVar ( & diffArtifactArgs . creds , "creds" , "" , "credentials for OCI registry in the format <username>[:<password>] if --provider is generic" )
diffArtifactCmd . Flags ( ) . Var ( & diffArtifactArgs . provider , "provider" , sourceOCIRepositoryArgs . provider . Description ( ) )
diffArtifactCmd . Flags ( ) . Var ( & diffArtifactArgs . provider , "provider" , sourceOCIRepositoryArgs . provider . Description ( ) )
diffArtifactCmd . Flags ( ) . StringSliceVar ( & diffArtifactArgs . ignorePaths , "ignore-paths" , excludeOCI , "set paths to ignore in .gitignore format" )
diffArtifactCmd . Flags ( ) . StringSliceVar ( & diffArtifactArgs . ignorePaths , "ignore-paths" , excludeOCI , "set paths to ignore in .gitignore format" )
diffArtifactCmd . Flags ( ) . BoolP ( "verbose" , "v" , false , "Show verbose output" ) // Add a --verbose flag specific to diff artifact.
diffCmd . AddCommand ( diffArtifactCmd )
diffCmd . AddCommand ( diffArtifactCmd )
}
}
@ -67,7 +75,7 @@ func diffArtifactCmdRun(cmd *cobra.Command, args []string) error {
ociURL := args [ 0 ]
ociURL := args [ 0 ]
if diffArtifactArgs . path == "" {
if diffArtifactArgs . path == "" {
return fmt . Errorf ( " invalid path %q", diffArtifactArgs . path )
return fmt . Errorf ( " path is required" )
}
}
url , err := oci . ParseArtifactURL ( ociURL )
url , err := oci . ParseArtifactURL ( ociURL )
@ -103,10 +111,112 @@ func diffArtifactCmdRun(cmd *cobra.Command, args []string) error {
}
}
}
}
if err := ociClient . Diff ( ctx , url , diffArtifactArgs . path , diffArtifactArgs . ignorePaths ) ; err != nil {
ociContents , err := fetchContentsFromURL ( ctx , url )
if err != nil {
return err
return err
}
}
logger . Successf ( "no changes detected" )
compareToContents , err := fetchContentsFromLocalPath ( diffArtifactArgs . path )
if err != nil {
return err
}
diffOutput , err := diffContents ( ociContents , compareToContents )
if err != nil {
return err
}
fmt . Printf ( "Diff between OCI artifact (%s) and local path (%s):\n" , ociURL , diffArtifactArgs . path )
// Show verbose output if the --verbose flag is set
if verbose , _ := cmd . Flags ( ) . GetBool ( "verbose" ) ; verbose {
fmt . Println ( "Printing diff..." )
}
fmt . Println ( diffOutput )
return nil
return nil
}
}
func fetchContentsFromURL ( ctx context . Context , url string ) ( [ ] byte , error ) {
img , err := crane . Pull ( url )
if err != nil {
return nil , fmt . Errorf ( "failed to pull OCI artifact: %w" , err )
}
layers , err := img . Layers ( )
if err != nil {
return nil , fmt . Errorf ( "failed to list layers: %w" , err )
}
if len ( layers ) < 1 {
return nil , fmt . Errorf ( "no layers found in OCI artifact" )
}
// Create a buffer to store the contents of the artifact
var contentsBuffer bytes . Buffer
// Extract the content of the first layer
layerContent , err := layers [ 0 ] . Compressed ( )
if err != nil {
return nil , fmt . Errorf ( "failed to extract layer content: %w" , err )
}
defer layerContent . Close ( )
// Use tar.NewReader to read the contents from the OCI artifact
tarReader := tar . NewReader ( layerContent )
for {
_ , err := tarReader . Next ( )
if err == io . EOF {
break
}
if err != nil {
return nil , fmt . Errorf ( "failed to read tar header: %w" , err )
}
// Read the file contents and write them to the buffer
if _ , err := io . Copy ( & contentsBuffer , tarReader ) ; err != nil {
return nil , fmt . Errorf ( "failed to read tar entry contents: %w" , err )
}
}
return contentsBuffer . Bytes ( ) , nil
}
func fetchContentsFromLocalPath ( path string ) ( [ ] byte , error ) {
// Open the local file or directory at the given path
file , err := os . Open ( path )
if err != nil {
return nil , fmt . Errorf ( "failed to open local file/directory: %w" , err )
}
defer file . Close ( )
// Create a buffer to store the contents
var contentsBuffer bytes . Buffer
// Use tar.NewReader to read the contents from the file/directory
tarReader := tar . NewReader ( file )
for {
_ , err := tarReader . Next ( )
if err == io . EOF {
break
}
if err != nil {
return nil , fmt . Errorf ( "failed to read tar header: %w" , err )
}
// Read the file contents and write them to the buffer
if _ , err := io . Copy ( & contentsBuffer , tarReader ) ; err != nil {
return nil , fmt . Errorf ( "failed to read tar entry contents: %w" , err )
}
}
return contentsBuffer . Bytes ( ) , nil
}
func diffContents ( contents1 , contents2 [ ] byte ) ( string , error ) {
dmp := diffmatchpatch . New ( )
diffs := dmp . DiffMain ( string ( contents1 ) , string ( contents2 ) , false )
return dmp . DiffPrettyText ( diffs ) , nil
}