istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/cmd/mesh/manifest-diff.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package mesh
    16  
    17  import (
    18  	"fmt"
    19  	"os"
    20  	"path/filepath"
    21  
    22  	"github.com/spf13/cobra"
    23  
    24  	"istio.io/istio/operator/pkg/compare"
    25  	"istio.io/istio/operator/pkg/util"
    26  )
    27  
    28  // YAMLSuffix is the suffix of a YAML file.
    29  const YAMLSuffix = ".yaml"
    30  
    31  type manifestDiffArgs struct {
    32  	// compareDir indicates comparison between directory.
    33  	compareDir bool
    34  	// verbose generates verbose output.
    35  	verbose bool
    36  	// selectResources constrains the list of resources to compare to only the ones in this list, ignoring all others.
    37  	// The format of each list item is :: and the items are comma separated. The * character represents wildcard selection.
    38  	// e.g.
    39  	// Deployment:istio-system:* - compare all deployments in istio-system namespace
    40  	// Service:*:istio-pilot - compare Services called "istio-pilot" in all namespaces.
    41  	selectResources string
    42  	// ignoreResources ignores all listed items during comparison. It uses the same list format as selectResources.
    43  	ignoreResources string
    44  	// renameResources identifies renamed resources before comparison.
    45  	// The format of each renaming pair is A->B, all renaming pairs are comma separated.
    46  	// e.g. Service:*:istio-pilot->Service:*:istio-control - rename istio-pilot service into istio-control
    47  	renameResources string
    48  }
    49  
    50  func addManifestDiffFlags(cmd *cobra.Command, diffArgs *manifestDiffArgs) {
    51  	cmd.PersistentFlags().BoolVarP(&diffArgs.compareDir, "directory", "r",
    52  		false, "Compare directory.")
    53  	cmd.PersistentFlags().BoolVarP(&diffArgs.verbose, "verbose", "v",
    54  		false, "Verbose output.")
    55  	cmd.PersistentFlags().StringVar(&diffArgs.selectResources, "select", "::",
    56  		"Constrain the list of resources to compare to only the ones in this list, ignoring all others.\n"+
    57  			"The format of each list item is \"::\" and the items are comma separated. The \"*\" character represents wildcard selection.\n"+
    58  			"e.g.\n"+
    59  			"    Deployment:istio-system:* - compare all deployments in istio-system namespace\n"+
    60  			"    Service:*:istiod - compare Services called \"istiod\" in all namespaces")
    61  	cmd.PersistentFlags().StringVar(&diffArgs.ignoreResources, "ignore", "",
    62  		"Ignore all listed items during comparison, using the same list format as selectResources.")
    63  	cmd.PersistentFlags().StringVar(&diffArgs.renameResources, "rename", "",
    64  		"Rename resources before comparison.\n"+
    65  			"The format of each renaming pair is A->B, all renaming pairs are comma separated.\n"+
    66  			"e.g. Service:*:istiod->Service:*:istio-control - rename istiod service into istio-control")
    67  }
    68  
    69  func manifestDiffCmd(diffArgs *manifestDiffArgs) *cobra.Command {
    70  	cmd := &cobra.Command{
    71  		Use:   "diff <file|dir> <file|dir>",
    72  		Short: "Compare manifests and generate diff",
    73  		Long: "The diff subcommand compares manifests from two files or directories. The output is a list of\n" +
    74  			"changed paths with the value changes shown as OLD-VALUE -> NEW-VALUE.\n" +
    75  			"List order changes are shown as [OLD-INDEX->NEW-INDEX], with ? used where a list item is added or\n" +
    76  			"removed.",
    77  		Args: func(cmd *cobra.Command, args []string) error {
    78  			if len(args) != 2 {
    79  				return fmt.Errorf("diff requires two files or directories")
    80  			}
    81  			return nil
    82  		},
    83  		RunE: func(cmd *cobra.Command, args []string) error {
    84  			var err error
    85  			var equal bool
    86  			if diffArgs.compareDir {
    87  				equal, err = compareManifestsFromDirs(diffArgs.verbose, args[0], args[1],
    88  					diffArgs.renameResources, diffArgs.selectResources, diffArgs.ignoreResources)
    89  				if err != nil {
    90  					return err
    91  				}
    92  				if !equal {
    93  					os.Exit(1)
    94  				}
    95  				return nil
    96  			}
    97  
    98  			equal, err = compareManifestsFromFiles(args, diffArgs.verbose,
    99  				diffArgs.renameResources, diffArgs.selectResources, diffArgs.ignoreResources)
   100  			if err != nil {
   101  				return err
   102  			}
   103  			if !equal {
   104  				os.Exit(1)
   105  			}
   106  			return nil
   107  		},
   108  	}
   109  	return cmd
   110  }
   111  
   112  // compareManifestsFromFiles compares two manifest files
   113  func compareManifestsFromFiles(args []string, verbose bool,
   114  	renameResources, selectResources, ignoreResources string,
   115  ) (bool, error) {
   116  	a, err := os.ReadFile(args[0])
   117  	if err != nil {
   118  		return false, fmt.Errorf("could not read %q: %v", args[0], err)
   119  	}
   120  	b, err := os.ReadFile(args[1])
   121  	if err != nil {
   122  		return false, fmt.Errorf("could not read %q: %v", args[1], err)
   123  	}
   124  
   125  	diff, err := compare.ManifestDiffWithRenameSelectIgnore(string(a), string(b), renameResources, selectResources,
   126  		ignoreResources, verbose)
   127  	if err != nil {
   128  		return false, err
   129  	}
   130  	if diff != "" {
   131  		fmt.Printf("Differences in manifests are:\n%s\n", diff)
   132  		return false, nil
   133  	}
   134  
   135  	fmt.Println("Manifests are identical")
   136  	return true, nil
   137  }
   138  
   139  func yamlFileFilter(path string) bool {
   140  	return filepath.Ext(path) == YAMLSuffix
   141  }
   142  
   143  // compareManifestsFromDirs compares manifests from two directories
   144  func compareManifestsFromDirs(verbose bool, dirName1, dirName2,
   145  	renameResources, selectResources, ignoreResources string,
   146  ) (bool, error) {
   147  	mf1, err := util.ReadFilesWithFilter(dirName1, yamlFileFilter)
   148  	if err != nil {
   149  		return false, err
   150  	}
   151  	mf2, err := util.ReadFilesWithFilter(dirName2, yamlFileFilter)
   152  	if err != nil {
   153  		return false, err
   154  	}
   155  
   156  	diff, err := compare.ManifestDiffWithRenameSelectIgnore(mf1, mf2, renameResources, selectResources,
   157  		ignoreResources, verbose)
   158  	if err != nil {
   159  		return false, err
   160  	}
   161  	if diff != "" {
   162  		fmt.Printf("Differences in manifests are:\n%s\n", diff)
   163  		return false, nil
   164  	}
   165  
   166  	fmt.Println("Manifests are identical")
   167  	return true, nil
   168  }