github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/cmd/orbctl/patch.go (about)

     1  // Inspired by https://samrapdev.com/capturing-sensitive-input-with-editor-in-golang-from-the-cli/
     2  
     3  package main
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/AlecAivazis/survey/v2"
    12  	"github.com/spf13/cobra"
    13  	"gopkg.in/yaml.v3"
    14  
    15  	"github.com/caos/orbos/internal/operator/common"
    16  	"github.com/caos/orbos/pkg/git"
    17  )
    18  
    19  func PatchCommand(getRv GetRootValues) *cobra.Command {
    20  
    21  	cmd := &cobra.Command{
    22  		Use:   "patch <filepath> [yamlpath]",
    23  		Short: "Patch a yaml property",
    24  		Args:  cobra.MinimumNArgs(1),
    25  		Example: `Overwiting a file: orbctl file patch orbiter.yml --exact
    26  Patching an edge property interactively: orbctl file patch orbiter.yml
    27  Patching a node property non-interactively: orbctl file path orbiter.yml clusters.k8s --exact --file /path/to/my/cluster/definition.yml`,
    28  	}
    29  	flags := cmd.Flags()
    30  	var (
    31  		value string
    32  		file  string
    33  		stdin bool
    34  		exact bool
    35  	)
    36  	flags.StringVar(&value, "value", "", "Content value")
    37  	flags.StringVar(&file, "file", "", "File containing the content value")
    38  	flags.BoolVar(&stdin, "stdin", false, "Read content value by stdin")
    39  	flags.BoolVar(&exact, "exact", false, "Write the content exactly at the path given without further prompting")
    40  
    41  	cmd.RunE = func(cmd *cobra.Command, args []string) (err error) {
    42  
    43  		var path []string
    44  		if len(args) > 1 {
    45  			path = strings.Split(args[1], ".")
    46  		}
    47  
    48  		filePath := args[0]
    49  
    50  		rv := getRv("patch", "", map[string]interface{}{"value": value, "filePath": filePath, "valuePath": file, "stdin": stdin, "exact": exact})
    51  		defer rv.ErrFunc(err)
    52  
    53  		if !rv.Gitops {
    54  			return errors.New("patch command is only supported with the --gitops flag")
    55  		}
    56  
    57  		if err := initRepo(rv.OrbConfig, rv.GitClient); err != nil {
    58  			return err
    59  		}
    60  
    61  		contentStr, err := content(value, file, stdin)
    62  		if err != nil {
    63  			return err
    64  		}
    65  
    66  		contentYaml := yamlTypedValue(contentStr)
    67  
    68  		var result interface{}
    69  		if len(path) == 0 && exact {
    70  			result = contentYaml
    71  		} else {
    72  			structure := map[string]interface{}{}
    73  			if err := yaml.Unmarshal(rv.GitClient.Read(filePath), structure); err != nil {
    74  				return err
    75  			}
    76  			if err := updateMap(structure, path, contentYaml, exact); err != nil {
    77  				return err
    78  			}
    79  			result = structure
    80  		}
    81  
    82  		return rv.GitClient.UpdateRemote(fmt.Sprintf("Overwrite %s", filePath), func() []git.File {
    83  			return []git.File{{
    84  				Path:    filePath,
    85  				Content: common.MarshalYAML(result),
    86  			}}
    87  		})
    88  	}
    89  
    90  	return cmd
    91  }
    92  
    93  func updateMap(structure map[string]interface{}, path []string, value interface{}, exact bool) error {
    94  
    95  	if len(path) == 1 && exact {
    96  		structure[path[0]] = value
    97  		return nil
    98  	}
    99  
   100  	path, nextStep := drillPath(path)
   101  
   102  	if nextStep == "" {
   103  
   104  		var keys []string
   105  		for key := range structure {
   106  			keys = append(keys, key)
   107  		}
   108  
   109  		var err error
   110  		nextStep, err = prompt(keys)
   111  		if err != nil {
   112  			return err
   113  		}
   114  	}
   115  
   116  	child, ok := structure[nextStep]
   117  	if !ok {
   118  		return fmt.Errorf("path element %s not found", nextStep)
   119  	}
   120  
   121  	drilled, err := drillContent(child, path, value, exact)
   122  	if err != nil {
   123  		return err
   124  	}
   125  	if !drilled {
   126  		structure[nextStep] = value
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  func prompt(keys []string) (string, error) {
   133  	var key string
   134  	return key, survey.AskOne(&survey.Select{
   135  		Message: "Select key:",
   136  		Options: keys,
   137  	}, &key, survey.WithValidator(survey.Required))
   138  }
   139  
   140  func updateSlice(slice []interface{}, path []string, value interface{}, exact bool) error {
   141  
   142  	pos := func(pathNode string) (int, error) {
   143  		idx, err := strconv.Atoi(pathNode)
   144  		if err != nil {
   145  			return -1, err
   146  		}
   147  
   148  		length := len(slice)
   149  		if length < idx {
   150  			return -1, fmt.Errorf("property has only %d elements", length)
   151  		}
   152  		return idx, nil
   153  	}
   154  
   155  	if len(path) == 1 && exact {
   156  		idx, err := pos(path[0])
   157  		if err != nil {
   158  			return err
   159  		}
   160  
   161  		slice[idx] = value
   162  		return nil
   163  	}
   164  
   165  	path, nextStep := drillPath(path)
   166  
   167  	if nextStep == "" {
   168  		var keys []string
   169  		for key := range slice {
   170  			keys = append(keys, strconv.Itoa(key))
   171  		}
   172  
   173  		var err error
   174  		nextStep, err = prompt(keys)
   175  		if err != nil {
   176  			return err
   177  		}
   178  	}
   179  
   180  	idx, err := pos(nextStep)
   181  
   182  	drilled, err := drillContent(slice[idx], path, value, exact)
   183  	if err != nil {
   184  		return err
   185  	}
   186  	if !drilled {
   187  		slice[idx] = value
   188  	}
   189  
   190  	return nil
   191  }
   192  
   193  func drillPath(path []string) ([]string, string) {
   194  	var next string
   195  
   196  	if len(path) > 0 {
   197  		next = path[0]
   198  		path = path[1:]
   199  	}
   200  	return path, next
   201  }
   202  
   203  func drillContent(child interface{}, path []string, value interface{}, exact bool) (bool, error) {
   204  
   205  	switch typedNext := child.(type) {
   206  	case map[string]interface{}:
   207  		return true, updateMap(typedNext, path, value, exact)
   208  	case []interface{}:
   209  		return true, updateSlice(typedNext, path, value, exact)
   210  	}
   211  
   212  	if len(path) > 0 {
   213  		return false, fmt.Errorf("invalid path %s", strings.Join(path, "."))
   214  	}
   215  
   216  	return false, nil
   217  }
   218  
   219  func yamlTypedValue(value string) interface{} {
   220  
   221  	var out interface{}
   222  	if err := yaml.Unmarshal([]byte(value), &out); err != nil {
   223  		panic(err)
   224  	}
   225  	return out
   226  }