github.com/cilium/cilium@v1.16.2/pkg/command/output.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package command
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"os"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"github.com/spf13/cobra"
    14  	"gopkg.in/yaml.v3"
    15  	"k8s.io/client-go/util/jsonpath"
    16  )
    17  
    18  var (
    19  	outputOpt string
    20  	re        = regexp.MustCompile(`^jsonpath\=(.*)`)
    21  )
    22  
    23  // OutputOption returns true if an output option was specified.
    24  func OutputOption() bool {
    25  	return len(outputOpt) > 0
    26  }
    27  
    28  // OutputOptionString returns the output option as a string
    29  func OutputOptionString() string {
    30  	if outputOpt == "yaml" {
    31  		return "YAML"
    32  	}
    33  
    34  	if outputOpt == "json" || re.MatchString(outputOpt) {
    35  		return "JSON"
    36  	}
    37  
    38  	return "unknown"
    39  }
    40  
    41  // AddOutputOption adds the -o|--output option to any cmd to export to json or yaml.
    42  func AddOutputOption(cmd *cobra.Command) {
    43  	cmd.Flags().StringVarP(&outputOpt, "output", "o", "", "json| yaml| jsonpath='{}'")
    44  }
    45  
    46  // ForceJSON sets output mode to JSON (for unit tests)
    47  func ForceJSON() {
    48  	outputOpt = "json"
    49  }
    50  
    51  // PrintOutput receives an interface and dump the data using the --output flag.
    52  // ATM only json or jsonpath. In the future yaml
    53  func PrintOutput(data interface{}) error {
    54  	return PrintOutputWithType(data, outputOpt)
    55  }
    56  
    57  // PrintOutputWithPatch merges data with patch and dump the data using the --output flag.
    58  func PrintOutputWithPatch(data interface{}, patch interface{}) error {
    59  	mergedInterface, err := mergeInterfaces(data, patch)
    60  	if err != nil {
    61  		return fmt.Errorf("Unable to merge Interfaces: %w", err)
    62  	}
    63  	return PrintOutputWithType(mergedInterface, outputOpt)
    64  }
    65  
    66  func mergeInterfaces(data, patch interface{}) (interface{}, error) {
    67  	var i1, i2 interface{}
    68  
    69  	data1, err := json.Marshal(data)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	data2, err := json.Marshal(patch)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	err = json.Unmarshal(data1, &i1)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	err = json.Unmarshal(data2, &i2)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	return recursiveMerge(i1, i2), nil
    87  }
    88  
    89  func recursiveMerge(i1, i2 interface{}) interface{} {
    90  	switch i1 := i1.(type) {
    91  	case map[string]interface{}:
    92  		i2, ok := i2.(map[string]interface{})
    93  		if !ok {
    94  			return i1
    95  		}
    96  		for k, v2 := range i2 {
    97  			if v1, ok := i1[k]; ok {
    98  				i1[k] = recursiveMerge(v1, v2)
    99  			} else {
   100  				i1[k] = v2
   101  			}
   102  		}
   103  	case nil:
   104  		i2, ok := i2.(map[string]interface{})
   105  		if ok {
   106  			return i2
   107  		}
   108  	}
   109  	return i1
   110  }
   111  
   112  // PrintOutputWithType receives an interface and dump the data using the --output flag.
   113  // ATM only json, yaml, or jsonpath.
   114  func PrintOutputWithType(data interface{}, outputType string) error {
   115  	if outputType == "json" {
   116  		return dumpJSON(data, "")
   117  	}
   118  
   119  	if outputType == "yaml" {
   120  		return dumpYAML(data)
   121  	}
   122  
   123  	if re.MatchString(outputType) {
   124  		return dumpJSON(data, re.ReplaceAllString(outputType, "$1"))
   125  	}
   126  
   127  	return fmt.Errorf("couldn't find output printer")
   128  }
   129  
   130  // DumpJSONToString dumps the contents of data into a string. If jsonpath is
   131  // non-empty, will attempt to do jsonpath filtering using said string. Returns a
   132  // string containing the JSON in data, or an error if any JSON marshaling,
   133  // parsing operations fail.
   134  func DumpJSONToString(data interface{}, jsonPath string) (string, error) {
   135  	if len(jsonPath) == 0 {
   136  		result, err := json.MarshalIndent(data, "", "  ")
   137  		if err != nil {
   138  			fmt.Fprintf(os.Stderr, "Couldn't marshal to json: '%s'\n", err)
   139  			return "", err
   140  		}
   141  		fmt.Println(string(result))
   142  		return "", nil
   143  	}
   144  
   145  	parser := jsonpath.New("").AllowMissingKeys(true)
   146  	if err := parser.Parse(jsonPath); err != nil {
   147  		fmt.Fprintf(os.Stderr, "Couldn't parse jsonpath expression: '%s'\n", err)
   148  		return "", err
   149  	}
   150  
   151  	var sb strings.Builder
   152  	if err := parser.Execute(&sb, data); err != nil {
   153  		fmt.Fprintf(os.Stderr, "Couldn't parse jsonpath expression: '%s'\n", err)
   154  		return "", err
   155  
   156  	}
   157  	return sb.String(), nil
   158  }
   159  
   160  // dumpJSON dumps the data variable to the stdout as json.
   161  // If something fails, it returns an error
   162  // If jsonPath is passed, it runs the json query over data var.
   163  func dumpJSON(data interface{}, jsonPath string) error {
   164  	jsonStr, err := DumpJSONToString(data, jsonPath)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	fmt.Println(jsonStr)
   169  	return nil
   170  }
   171  
   172  // dumpYAML dumps the data variable to the stdout as yaml.
   173  // If something fails, it returns an error
   174  func dumpYAML(data interface{}) error {
   175  	result, err := yaml.Marshal(data)
   176  	if err != nil {
   177  		fmt.Fprintf(os.Stderr, "Couldn't marshal to yaml: '%s'\n", err)
   178  		return err
   179  	}
   180  	fmt.Println(string(result))
   181  	return nil
   182  }