Skip to content

Commit

Permalink
--output=flags option for 'istioctl profile dump' (#24403)
Browse files Browse the repository at this point in the history
* --output=flags option for 'istioctl profile dump'

* Escape path elements with '.'

* Blacklist 'installPackagePath' to ensure output is stable

* Instead of blacklisting, fix test to correctly suppress installPackagePath differences
  • Loading branch information
esnible authored Jun 8, 2020
1 parent 039ac94 commit 0df3734
Show file tree
Hide file tree
Showing 5 changed files with 578 additions and 10 deletions.
84 changes: 78 additions & 6 deletions operator/cmd/mesh/profile-dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package mesh
import (
"encoding/json"
"fmt"
"sort"
"strings"

"github.com/ghodss/yaml"
"github.com/spf13/cobra"
Expand All @@ -38,12 +40,13 @@ type profileDumpArgs struct {
}

const (
jsonOutput = "json"
yamlOutput = "yaml"
jsonOutput = "json"
yamlOutput = "yaml"
flagsOutput = "flags"
)

const (
IstioOperatorTreeString = `
istioOperatorTreeString = `
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
`
Expand All @@ -54,7 +57,7 @@ func addProfileDumpFlags(cmd *cobra.Command, args *profileDumpArgs) {
cmd.PersistentFlags().StringVarP(&args.configPath, "config-path", "p", "",
"The path the root of the configuration subtree to dump e.g. components.pilot. By default, dump whole tree")
cmd.PersistentFlags().StringVarP(&args.outputFormat, "output", "o", yamlOutput,
"Output format: one of json|yaml")
"Output format: one of json|yaml|flags")
cmd.PersistentFlags().StringVarP(&args.charts, "charts", "d", "", ChartsFlagHelpStr)
}

Expand All @@ -81,7 +84,7 @@ func prependHeader(yml string) (string, error) {
if err != nil {
return "", err
}
out2, err := util.OverlayYAML(IstioOperatorTreeString, out)
out2, err := util.OverlayYAML(istioOperatorTreeString, out)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -116,7 +119,7 @@ func profileDump(args []string, rootArgs *rootArgs, pdArgs *profileDumpArgs, l c
}

switch pdArgs.outputFormat {
case jsonOutput, yamlOutput:
case jsonOutput, yamlOutput, flagsOutput:
default:
return fmt.Errorf("unknown output format: %v", pdArgs.outputFormat)
}
Expand Down Expand Up @@ -150,7 +153,76 @@ func profileDump(args []string, rootArgs *rootArgs, pdArgs *profileDumpArgs, l c
l.Print(j + "\n")
case yamlOutput:
l.Print(y + "\n")
case flagsOutput:
f, err := yamlToFlags(y)
if err != nil {
return err
}
l.Print(strings.Join(f, "\n") + "\n")
}

return nil
}

// Convert the generated YAML to --set flags
func yamlToFlags(yml string) ([]string, error) {
// YAML objects are not completely compatible with JSON
// objects. Let yaml.YAMLToJSON handle the edge cases and
// we'll re-encode the result to pretty JSON.
uglyJSON, err := yaml.YAMLToJSON([]byte(yml))
if err != nil {
return []string{}, err
}
var decoded map[string]interface{}
if err := json.Unmarshal(uglyJSON, &decoded); err != nil {
return []string{}, err
}
spec, ok := decoded["spec"]
if !ok {
// Fall back to showing the entire spec.
// (When --config-path is used there will be no spec to remove)
spec = decoded
}
setflags, err := walk("", "", spec)
if err != nil {
return []string{}, err
}
sort.Strings(setflags)
return setflags, nil
}

func walk(path, separator string, obj interface{}) ([]string, error) {
switch v := obj.(type) {
case map[string]interface{}:
accum := make([]string, 0)
for key, vv := range v {
childwalk, err := walk(fmt.Sprintf("%s%s%s", path, separator, pathComponent(key)), ".", vv)
if err != nil {
return accum, err
}
accum = append(accum, childwalk...)
}
return accum, nil
case []interface{}:
accum := make([]string, 0)
for idx, vv := range v {
indexwalk, err := walk(fmt.Sprintf("%s[%d]", path, idx), ".", vv)
if err != nil {
return accum, err
}
accum = append(accum, indexwalk...)
}
return accum, nil
case string:
return []string{fmt.Sprintf("%s=%q", path, v)}, nil
default:
return []string{fmt.Sprintf("%s=%v", path, v)}, nil
}
}

func pathComponent(component string) string {
if !strings.Contains(component, util.PathSeparator) {
return component
}
return strings.ReplaceAll(component, util.PathSeparator, util.EscapedPathSeparator)
}
54 changes: 52 additions & 2 deletions operator/cmd/mesh/profile-dump_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"regexp"
"testing"

"github.com/kylelemons/godebug/diff"

"istio.io/istio/operator/pkg/util"
)

Expand All @@ -43,7 +45,7 @@ func TestProfileDump(t *testing.T) {
inPath := filepath.Join(testDataDir, "input", tt.desc+".yaml")
outPath := filepath.Join(testDataDir, "output", tt.desc+".yaml")

got, err := runProfileDump(inPath, tt.configPath, snapshotCharts)
got, err := runProfileDump(inPath, tt.configPath, snapshotCharts, "")
if err != nil {
t.Fatal(err)
}
Expand All @@ -68,13 +70,61 @@ func TestProfileDump(t *testing.T) {
}
}

func runProfileDump(profilePath, configPath string, chartSource chartSourceType) (string, error) {
func runProfileDump(profilePath, configPath string, chartSource chartSourceType, outfmt string) (string, error) {
cmd := "profile dump -f " + profilePath
if configPath != "" {
cmd += " --config-path " + configPath
}
if len(chartSource) > 0 {
cmd += " --charts=" + string(chartSource)
}
if outfmt != "" {
cmd += " --output=" + outfmt
}
return runCommand(cmd)
}

func TestProfileDumpFlags(t *testing.T) {
testDataDir = filepath.Join(operatorRootDir, "cmd/mesh/testdata/profile-dump")
tests := []struct {
desc string
configPath string
}{
{
desc: "all_off",
},
{
desc: "config_path",
configPath: "components",
},
}
installPackagePathRegex := regexp.MustCompile("(?m)^installPackagePath=\".*\"\n")
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
inPath := filepath.Join(testDataDir, "input", tt.desc+".yaml")
outPath := filepath.Join(testDataDir, "output", tt.desc+".txt")

got, err := runProfileDump(inPath, tt.configPath, snapshotCharts, "flags")
if err != nil {
t.Fatal(err)
}
// installPackagePath may change, we will remove it for consistent output
got = installPackagePathRegex.ReplaceAllString(got, "")

if refreshGoldenFiles() {
t.Logf("Refreshing golden file for %s", outPath)
if err := ioutil.WriteFile(outPath, []byte(got), 0644); err != nil {
t.Error(err)
}
}

want, err := readFile(outPath)
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("profile-dump command(%s): got:\n%s\n\nwant:\n%s\nDiff:\n%s\n", tt.desc, got, want, diff.Diff(got, want))
}
})
}
}
Loading

0 comments on commit 0df3734

Please sign in to comment.