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 }