github.com/evanlouie/fabrikate@v0.17.4/cmd/set.go (about)

     1  package cmd
     2  
     3  import (
     4  	"encoding/csv"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"strings"
     9  
    10  	"github.com/evanlouie/fabrikate/core"
    11  	"github.com/evanlouie/fabrikate/util"
    12  	"github.com/evanlouie/fabrikate/yaml"
    13  	"github.com/spf13/cobra"
    14  )
    15  
    16  // SplitPathValuePairs splits array of key/value pairs and returns array of path value pairs ([] PathValuePair) //
    17  func SplitPathValuePairs(pathValuePairStrings []string) (pathValuePairs []core.PathValuePair, err error) {
    18  	for _, pathValuePairString := range pathValuePairStrings {
    19  		pathValuePairParts := strings.Split(pathValuePairString, "=")
    20  
    21  		errMessage := "%s is not a properly formated configuration key/value pair"
    22  
    23  		if len(pathValuePairParts) != 2 {
    24  			return pathValuePairs, fmt.Errorf(errMessage, pathValuePairString)
    25  		}
    26  
    27  		pathParts, err := SplitPathParts(pathValuePairParts[0])
    28  
    29  		if err != nil {
    30  			return pathValuePairs, fmt.Errorf(errMessage, pathValuePairString)
    31  		}
    32  
    33  		pathValuePair := core.PathValuePair{
    34  			Path:  pathParts,
    35  			Value: pathValuePairParts[1],
    36  		}
    37  
    38  		pathValuePairs = append(pathValuePairs, pathValuePair)
    39  	}
    40  
    41  	return pathValuePairs, nil
    42  }
    43  
    44  // SplitPathParts splits path string at . while ignoring string literals enclosed in quotes (".") and returns an array //
    45  func SplitPathParts(path string) (pathParts []string, err error) {
    46  
    47  	csv := csv.NewReader(strings.NewReader(path))
    48  
    49  	// Comma is the field delimiter. Dot (.) will be the value for config key
    50  	csv.Comma = '.'
    51  
    52  	// setting it to true, a quote may appear in an unquoted field and a non-doubled quote may appear in a quoted field.
    53  	csv.LazyQuotes = true
    54  
    55  	// FieldsPerRecord is the number of expected fields per record.
    56  	// > 0: Read requires each record to have the given number of fields.
    57  	// == 0, Read sets it to the number of fields in the first record, so that future records must have the same field count.
    58  	// < 0, no check is made and config key may have a variable number of fields.
    59  	csv.FieldsPerRecord = -1
    60  
    61  	// Read parts and the error
    62  	parts, err := csv.Read()
    63  
    64  	// return err and empty parts
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	// return key parts
    70  	return parts, nil
    71  }
    72  
    73  // Set implements the 'set' command. It takes an environment, a set of config path / value strings (and a subcomponent if the config
    74  // should be set on a subcomponent versus the component itself) and sets the config in the appropriate config file,
    75  // writing the result out to disk at the end.
    76  func Set(environment string, subcomponent string, pathValuePairStrings []string, noNewConfigKeys bool, inputFile string) (err error) {
    77  
    78  	subcomponentPath := []string{}
    79  	if len(subcomponent) > 0 {
    80  		subcomponentPath = strings.Split(subcomponent, ".")
    81  	}
    82  
    83  	componentConfig := core.NewComponentConfig(".")
    84  
    85  	// Load input file if provided
    86  	inputFileValuePairList := []string{}
    87  	if inputFile != "" {
    88  		bytes, err := ioutil.ReadFile(inputFile)
    89  		if err != nil {
    90  			return err
    91  		}
    92  		yamlContent := map[string]interface{}{}
    93  		err = yaml.Unmarshal(bytes, &yamlContent)
    94  		if err != nil {
    95  			return err
    96  		}
    97  
    98  		// Flatten the map
    99  		flattenedInputFileContentMap := util.FlattenMap(yamlContent, ".", []string{})
   100  
   101  		// Append all key/value in map to the flattened list
   102  		for k, v := range flattenedInputFileContentMap {
   103  			// Join to PathValue strings with "="
   104  			valueAsString := fmt.Sprintf("%v", v)
   105  			joined := strings.Join([]string{k, valueAsString}, "=")
   106  			inputFileValuePairList = append(inputFileValuePairList, joined)
   107  		}
   108  	}
   109  
   110  	pathValuePairs, err := SplitPathValuePairs(append(inputFileValuePairList, pathValuePairStrings...))
   111  
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	if err := componentConfig.Load(environment); err != nil {
   117  		return err
   118  	}
   119  
   120  	newConfigError := errors.New("new configuration was specified and the --no-new-config-keys switch is on")
   121  
   122  	for _, pathValue := range pathValuePairs {
   123  		if noNewConfigKeys {
   124  			if !componentConfig.HasSubcomponentConfig(subcomponentPath) {
   125  				return newConfigError
   126  			}
   127  
   128  			sc := componentConfig.GetSubcomponentConfig(subcomponentPath)
   129  
   130  			if !sc.HasComponentConfig(pathValue.Path) {
   131  				return newConfigError
   132  			}
   133  		}
   134  
   135  		componentConfig.SetConfig(subcomponentPath, pathValue.Path, pathValue.Value)
   136  	}
   137  
   138  	return componentConfig.Write(environment)
   139  }
   140  
   141  var subcomponent string
   142  var environment string
   143  var noNewConfigKeys bool
   144  var inputFile string
   145  
   146  var setCmd = &cobra.Command{
   147  	Use:   "set <config> [--subcomponent subcomponent] [--file <my-yaml-file.yaml>] <path1>=<value1> <path2>=<value2> ...",
   148  	Short: "Sets a config value for a component for a particular config environment in the Fabrikate definition.",
   149  	Long: `Sets a config value for a component for a particular config environment in the Fabrikate definition.
   150  eg.
   151  $ fab set --environment prod data.replicas=4 username="ops"
   152  
   153  Sets the value of 'data.replicas' equal to 4 and 'username' equal to 'ops' in the 'prod' config for the current component.
   154  
   155  $ fab set --subcomponent "myapp" endpoint="east-db" 
   156  
   157  Sets the value of 'endpoint' equal to 'east-db' in the 'common' config (the default) for subcomponent 'myapp'.
   158  
   159  $ fab set --subcomponent "myapp.mysubcomponent" data.replicas=5 
   160  
   161  Sets the subkey "replicas" in the key 'data' equal to 5 in the 'common' config (the default) for the subcomponent 'mysubcomponent' of the subcomponent 'myapp'.
   162  
   163  $ fab set --subcomponent "myapp.mysubcomponent" data.replicas=5 --no-new-config-keys
   164  
   165  Use the --no-new-config-keys switch to prevent the creation of new config.
   166  `,
   167  	RunE: func(cmd *cobra.Command, args []string) error {
   168  		if len(args) < 1 && inputFile == "" {
   169  			return errors.New("'set' takes one or more key=value arguments and/or a --file")
   170  		}
   171  
   172  		return Set(environment, subcomponent, args, noNewConfigKeys, inputFile)
   173  	},
   174  }
   175  
   176  func init() {
   177  	setCmd.PersistentFlags().StringVar(&environment, "environment", "common", "Environment this configuration should apply to")
   178  	setCmd.PersistentFlags().StringVar(&subcomponent, "subcomponent", "", "Subcomponent this configuration should apply to")
   179  	setCmd.PersistentFlags().BoolVar(&noNewConfigKeys, "no-new-config-keys", false, "'Prevent creation of new config keys and only allow updating existing config values.")
   180  	setCmd.Flags().StringVarP(&inputFile, "file", "f", "", "Path to a single YAML file which can be read in and the values of which will be set; note '.' can not occur in keys and list values are not supported.")
   181  
   182  	rootCmd.AddCommand(setCmd)
   183  }