istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/cmd/mesh/profile-dump.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  	"encoding/json"
    19  	"fmt"
    20  	"sort"
    21  	"strings"
    22  
    23  	"github.com/spf13/cobra"
    24  	"sigs.k8s.io/yaml"
    25  
    26  	"istio.io/istio/operator/pkg/manifest"
    27  	"istio.io/istio/operator/pkg/tpath"
    28  	"istio.io/istio/operator/pkg/util"
    29  	"istio.io/istio/operator/pkg/util/clog"
    30  )
    31  
    32  type profileDumpArgs struct {
    33  	// inFilenames is an array of paths to the input IstioOperator CR files.
    34  	inFilenames []string
    35  	// configPath sets the root node for the subtree to display the config for.
    36  	configPath string
    37  	// outputFormat controls the format of profile dumps
    38  	outputFormat string
    39  	// manifestsPath is a path to a charts and profiles directory in the local filesystem with a release tgz.
    40  	manifestsPath string
    41  }
    42  
    43  const (
    44  	jsonOutput  = "json"
    45  	yamlOutput  = "yaml"
    46  	flagsOutput = "flags"
    47  )
    48  
    49  const (
    50  	istioOperatorTreeString = `
    51  apiVersion: install.istio.io/v1alpha1
    52  kind: IstioOperator
    53  `
    54  )
    55  
    56  func addProfileDumpFlags(cmd *cobra.Command, args *profileDumpArgs) {
    57  	cmd.PersistentFlags().StringSliceVarP(&args.inFilenames, "filename", "f", nil, filenameFlagHelpStr)
    58  	cmd.PersistentFlags().StringVarP(&args.configPath, "config-path", "p", "",
    59  		"The path the root of the configuration subtree to dump e.g. components.pilot. By default, dump whole tree")
    60  	cmd.PersistentFlags().StringVarP(&args.outputFormat, "output", "o", yamlOutput,
    61  		"Output format: one of json|yaml|flags")
    62  	cmd.PersistentFlags().StringVarP(&args.manifestsPath, "charts", "", "", ChartsDeprecatedStr)
    63  	cmd.PersistentFlags().StringVarP(&args.manifestsPath, "manifests", "d", "", ManifestsFlagHelpStr)
    64  }
    65  
    66  func profileDumpCmd(pdArgs *profileDumpArgs) *cobra.Command {
    67  	return &cobra.Command{
    68  		Use:   "dump [<profile>]",
    69  		Short: "Dumps an Istio configuration profile",
    70  		Long:  "The dump subcommand dumps the values in an Istio configuration profile.",
    71  		Args: func(cmd *cobra.Command, args []string) error {
    72  			if len(args) > 1 {
    73  				return fmt.Errorf("too many positional arguments")
    74  			}
    75  			return nil
    76  		},
    77  		RunE: func(cmd *cobra.Command, args []string) error {
    78  			l := clog.NewConsoleLogger(cmd.OutOrStdout(), cmd.ErrOrStderr(), installerScope)
    79  			return profileDump(args, pdArgs, l)
    80  		},
    81  	}
    82  }
    83  
    84  func prependHeader(yml string) (string, error) {
    85  	out, err := tpath.AddSpecRoot(yml)
    86  	if err != nil {
    87  		return "", err
    88  	}
    89  	out2, err := util.OverlayYAML(istioOperatorTreeString, out)
    90  	if err != nil {
    91  		return "", err
    92  	}
    93  	return out2, nil
    94  }
    95  
    96  // Convert the generated YAML to pretty JSON.
    97  func yamlToPrettyJSON(yml string) (string, error) {
    98  	// YAML objects are not completely compatible with JSON
    99  	// objects. Let yaml.YAMLToJSON handle the edge cases and
   100  	// we'll re-encode the result to pretty JSON.
   101  	uglyJSON, err := yaml.YAMLToJSON([]byte(yml))
   102  	if err != nil {
   103  		return "", err
   104  	}
   105  	var decoded any
   106  	if uglyJSON[0] == '[' {
   107  		decoded = make([]any, 0)
   108  	} else {
   109  		decoded = map[string]any{}
   110  	}
   111  	if err := json.Unmarshal(uglyJSON, &decoded); err != nil {
   112  		return "", err
   113  	}
   114  	prettyJSON, err := json.MarshalIndent(decoded, "", "    ")
   115  	if err != nil {
   116  		return "", err
   117  	}
   118  	return string(prettyJSON), nil
   119  }
   120  
   121  func profileDump(args []string, pdArgs *profileDumpArgs, l clog.Logger) error {
   122  	if len(args) == 1 && pdArgs.inFilenames != nil {
   123  		return fmt.Errorf("cannot specify both profile name and filename flag")
   124  	}
   125  
   126  	if err := validateProfileOutputFormatFlag(pdArgs.outputFormat); err != nil {
   127  		return err
   128  	}
   129  
   130  	setFlags := applyFlagAliases(make([]string, 0), pdArgs.manifestsPath, "")
   131  	if len(args) == 1 {
   132  		setFlags = append(setFlags, "profile="+args[0])
   133  	}
   134  
   135  	y, _, err := manifest.GenerateConfig(pdArgs.inFilenames, setFlags, true, nil, l)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	y, err = tpath.GetConfigSubtree(y, "spec")
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	if pdArgs.configPath == "" {
   145  		if y, err = prependHeader(y); err != nil {
   146  			return err
   147  		}
   148  	} else {
   149  		if y, err = tpath.GetConfigSubtree(y, pdArgs.configPath); err != nil {
   150  			return err
   151  		}
   152  	}
   153  
   154  	var output string
   155  	if output, err = yamlToFormat(y, pdArgs.outputFormat); err != nil {
   156  		return err
   157  	}
   158  	l.Print(output)
   159  	return nil
   160  }
   161  
   162  // validateOutputFormatFlag validates if the output format is valid.
   163  func validateProfileOutputFormatFlag(outputFormat string) error {
   164  	switch outputFormat {
   165  	case jsonOutput, yamlOutput, flagsOutput:
   166  	default:
   167  		return fmt.Errorf("unknown output format: %s", outputFormat)
   168  	}
   169  	return nil
   170  }
   171  
   172  // yamlToFormat converts the generated yaml config to the expected format
   173  func yamlToFormat(yaml, outputFormat string) (string, error) {
   174  	var output string
   175  	switch outputFormat {
   176  	case jsonOutput:
   177  		j, err := yamlToPrettyJSON(yaml)
   178  		if err != nil {
   179  			return "", err
   180  		}
   181  		output = fmt.Sprintf("%s\n", j)
   182  	case yamlOutput:
   183  		output = fmt.Sprintf("%s\n", yaml)
   184  	case flagsOutput:
   185  		f, err := yamlToFlags(yaml)
   186  		if err != nil {
   187  			return "", err
   188  		}
   189  		output = fmt.Sprintf("%s\n", strings.Join(f, "\n"))
   190  	}
   191  	return output, nil
   192  }
   193  
   194  // Convert the generated YAML to --set flags
   195  func yamlToFlags(yml string) ([]string, error) {
   196  	// YAML objects are not completely compatible with JSON
   197  	// objects. Let yaml.YAMLToJSON handle the edge cases and
   198  	// we'll re-encode the result to pretty JSON.
   199  	uglyJSON, err := yaml.YAMLToJSON([]byte(yml))
   200  	if err != nil {
   201  		return []string{}, err
   202  	}
   203  	var decoded any
   204  	if uglyJSON[0] == '[' {
   205  		decoded = make([]any, 0)
   206  	} else {
   207  		decoded = map[string]any{}
   208  	}
   209  	if err := json.Unmarshal(uglyJSON, &decoded); err != nil {
   210  		return []string{}, err
   211  	}
   212  	if d, ok := decoded.(map[string]any); ok {
   213  		if v, ok := d["spec"]; ok {
   214  			// Fall back to showing the entire spec.
   215  			// (When --config-path is used there will be no spec to remove)
   216  			decoded = v
   217  		}
   218  	}
   219  	setflags, err := walk("", "", decoded)
   220  	if err != nil {
   221  		return []string{}, err
   222  	}
   223  	sort.Strings(setflags)
   224  	return setflags, nil
   225  }
   226  
   227  func walk(path, separator string, obj any) ([]string, error) {
   228  	switch v := obj.(type) {
   229  	case map[string]any:
   230  		accum := make([]string, 0)
   231  		for key, vv := range v {
   232  			childwalk, err := walk(fmt.Sprintf("%s%s%s", path, separator, pathComponent(key)), ".", vv)
   233  			if err != nil {
   234  				return accum, err
   235  			}
   236  			accum = append(accum, childwalk...)
   237  		}
   238  		return accum, nil
   239  	case []any:
   240  		accum := make([]string, 0)
   241  		for idx, vv := range v {
   242  			indexwalk, err := walk(fmt.Sprintf("%s[%d]", path, idx), ".", vv)
   243  			if err != nil {
   244  				return accum, err
   245  			}
   246  			accum = append(accum, indexwalk...)
   247  		}
   248  		return accum, nil
   249  	case string:
   250  		return []string{fmt.Sprintf("%s=%q", path, v)}, nil
   251  	default:
   252  		return []string{fmt.Sprintf("%s=%v", path, v)}, nil
   253  	}
   254  }
   255  
   256  func pathComponent(component string) string {
   257  	if !strings.Contains(component, util.PathSeparator) {
   258  		return component
   259  	}
   260  	return strings.ReplaceAll(component, util.PathSeparator, util.EscapedPathSeparator)
   261  }