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 }