github.com/wtrep/tgf@v1.18.8/config.go (about) 1 package main 2 3 import ( 4 "crypto/md5" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "os/user" 12 "path" 13 "path/filepath" 14 "reflect" 15 "regexp" 16 "strings" 17 "time" 18 19 "github.com/aws/aws-sdk-go/service/ssm" 20 "github.com/blang/semver" 21 "github.com/coveo/gotemplate/collections" 22 "github.com/gruntwork-io/terragrunt/aws_helper" 23 "github.com/hashicorp/go-getter" 24 yaml "gopkg.in/yaml.v2" 25 ) 26 27 const ( 28 // ssm configuration 29 defaultSSMParameterFolder = "/default/tgf" 30 ssmParameterFolderEnvVariable = "TGF_SSM_PATH" 31 32 // ssm configuration used to fetch configs from a remote location 33 remoteDefaultConfigPath = "TGFConfig" 34 remoteConfigLocationParameter = "config-location" 35 remoteConfigPathsParameter = "config-paths" 36 37 // configuration files 38 configFile = ".tgf.config" 39 userConfigFile = "tgf.user.config" 40 41 tagSeparator = "-" 42 ) 43 44 // TGFConfig contains the resulting configuration that will be applied 45 type TGFConfig struct { 46 Image string `yaml:"docker-image,omitempty" json:"docker-image,omitempty" hcl:"docker-image,omitempty"` 47 ImageVersion *string `yaml:"docker-image-version,omitempty" json:"docker-image-version,omitempty" hcl:"docker-image-version,omitempty"` 48 ImageTag *string `yaml:"docker-image-tag,omitempty" json:"docker-image-tag,omitempty" hcl:"docker-image-tag,omitempty"` 49 ImageBuild string `yaml:"docker-image-build,omitempty" json:"docker-image-build,omitempty" hcl:"docker-image-build,omitempty"` 50 ImageBuildFolder string `yaml:"docker-image-build-folder,omitempty" json:"docker-image-build-folder,omitempty" hcl:"docker-image-build-folder,omitempty"` 51 ImageBuildTag string `yaml:"docker-image-build-tag,omitempty" json:"docker-image-build-tag,omitempty" hcl:"docker-image-build-tag,omitempty"` 52 LogLevel string `yaml:"logging-level,omitempty" json:"logging-level,omitempty" hcl:"logging-level,omitempty"` 53 EntryPoint string `yaml:"entry-point,omitempty" json:"entry-point,omitempty" hcl:"entry-point,omitempty"` 54 Refresh time.Duration `yaml:"docker-refresh,omitempty" json:"docker-refresh,omitempty" hcl:"docker-refresh,omitempty"` 55 DockerOptions []string `yaml:"docker-options,omitempty" json:"docker-options,omitempty" hcl:"docker-options,omitempty"` 56 RecommendedImageVersion string `yaml:"recommended-image-version,omitempty" json:"recommended-image-version,omitempty" hcl:"recommended-image-version,omitempty"` 57 RequiredVersionRange string `yaml:"required-image-version,omitempty" json:"required-image-version,omitempty" hcl:"required-image-version,omitempty"` 58 RecommendedTGFVersion string `yaml:"tgf-recommended-version,omitempty" json:"tgf-recommended-version,omitempty" hcl:"tgf-recommended-version,omitempty"` 59 Environment map[string]string `yaml:"environment,omitempty" json:"environment,omitempty" hcl:"environment,omitempty"` 60 RunBefore string `yaml:"run-before,omitempty" json:"run-before,omitempty" hcl:"run-before,omitempty"` 61 RunAfter string `yaml:"run-after,omitempty" json:"run-after,omitempty" hcl:"run-after,omitempty"` 62 Aliases map[string]string `yaml:"alias,omitempty" json:"alias,omitempty" hcl:"alias,omitempty"` 63 64 runBeforeCommands, runAfterCommands []string 65 imageBuildConfigs []TGFConfigBuild // List of config built from previous build configs 66 } 67 68 // TGFConfigBuild contains an entry specifying how to customize the current docker image 69 type TGFConfigBuild struct { 70 Instructions string 71 Folder string 72 Tag string 73 source string 74 } 75 76 func (cb TGFConfigBuild) hash() string { 77 h := md5.New() 78 io.WriteString(h, filepath.Base(filepath.Dir(cb.source))) 79 io.WriteString(h, cb.Instructions) 80 if cb.Folder != "" { 81 filepath.Walk(cb.Dir(), func(path string, info os.FileInfo, err error) error { 82 if info == nil || info.IsDir() || err != nil { 83 return nil 84 } 85 if !strings.Contains(path, dockerfilePattern) { 86 io.WriteString(h, fmt.Sprintf("%v", info.ModTime())) 87 } 88 return nil 89 }) 90 } 91 return fmt.Sprintf("%x", h.Sum(nil)) 92 } 93 94 // Dir returns the folder name relative to the source 95 func (cb TGFConfigBuild) Dir() string { 96 if cb.Folder == "" { 97 return filepath.Dir(cb.source) 98 } 99 if filepath.IsAbs(cb.Folder) { 100 return cb.Folder 101 } 102 return must(filepath.Abs(filepath.Join(filepath.Dir(cb.source), cb.Folder))).(string) 103 } 104 105 // GetTag returns the tag name that should be added to the image 106 func (cb TGFConfigBuild) GetTag() string { 107 tag := filepath.Base(filepath.Dir(cb.source)) 108 if cb.Tag != "" { 109 tag = cb.Tag 110 } 111 tagRegex := regexp.MustCompile(`[^a-zA-Z0-9\._-]`) 112 return tagRegex.ReplaceAllString(tag, "") 113 } 114 115 // InitConfig returns a properly initialized TGF configuration struct 116 func InitConfig() *TGFConfig { 117 return &TGFConfig{Image: "coveo/tgf", 118 Refresh: 1 * time.Hour, 119 EntryPoint: "terragrunt", 120 LogLevel: "notice", 121 Environment: make(map[string]string), 122 imageBuildConfigs: []TGFConfigBuild{}, 123 } 124 } 125 126 func (config TGFConfig) String() string { 127 bytes, err := yaml.Marshal(config) 128 if err != nil { 129 return fmt.Sprintf("Error parsing TGFConfig: %v", err) 130 } 131 return string(bytes) 132 } 133 134 // InitAWS tries to open an AWS session and init AWS environment variable on success 135 func (config *TGFConfig) InitAWS(profile string) error { 136 _, err := aws_helper.InitAwsSession(profile) 137 if err != nil { 138 return err 139 } 140 141 for _, s := range os.Environ() { 142 if strings.HasPrefix(s, "AWS_") { 143 split := strings.SplitN(s, "=", 2) 144 if len(split) < 2 { 145 continue 146 } 147 config.Environment[split[0]] = split[1] 148 } 149 } 150 return nil 151 } 152 153 // SetDefaultValues sets the uninitialized values from the config files and the parameter store 154 // Priorities (Higher overwrites lower values): 155 // 1. SSM Parameter Config 156 // 2. Secrets Manager Config (If exists, will not check SSM) 157 // 3. tgf.user.config 158 // 4. .tgf.config 159 func (config *TGFConfig) SetDefaultValues() { 160 config.setDefaultValues(getSSMParameterFolder()) 161 } 162 163 func (config *TGFConfig) setDefaultValues(ssmParameterFolder string) { 164 type configData struct { 165 Name string 166 Raw string 167 Config *TGFConfig 168 } 169 configsData := []configData{} 170 171 // Fetch SSM configs 172 if awsConfigExist() { 173 if err := config.InitAWS(""); err != nil { 174 printError("Unable to authentify to AWS: %v\nPararameter store is ignored\n", err) 175 } else { 176 parameters := must(aws_helper.GetSSMParametersByPath(ssmParameterFolder, "")).([]*ssm.Parameter) 177 parameterValues := extractMapFromParameters(ssmParameterFolder, parameters) 178 179 for _, configFile := range findRemoteConfigFiles(parameterValues) { 180 configsData = append(configsData, configData{Name: "RemoteConfigFile", Raw: configFile}) 181 } 182 183 // Only fetch SSM parameters if no ConfigFile was found 184 if len(configsData) == 0 { 185 ssmConfig := parseSsmConfig(parameterValues) 186 if ssmConfig != "" { 187 configsData = append(configsData, configData{Name: "AWS/ParametersStore", Raw: ssmConfig}) 188 } 189 } 190 } 191 } 192 193 // Fetch file configs 194 for _, configFile := range findConfigFiles(must(os.Getwd()).(string)) { 195 debugPrint("# Reading configuration from %s\n", configFile) 196 bytes, err := ioutil.ReadFile(configFile) 197 198 if err != nil { 199 fmt.Fprintln(os.Stderr, errorString("Error while loading configuration file %s\n%v", configFile, err)) 200 continue 201 } 202 configsData = append(configsData, configData{Name: configFile, Raw: string(bytes)}) 203 } 204 205 // Parse/Unmarshal configs 206 for i := range configsData { 207 configData := &configsData[i] 208 if err := collections.ConvertData(configData.Raw, config); err != nil { 209 fmt.Fprintln(os.Stderr, errorString("Error while loading configuration from %s\nConfiguration file must be valid YAML, JSON or HCL\n%v", configData.Name, err)) 210 } 211 collections.ConvertData(configData.Raw, &configData.Config) 212 } 213 214 // Special case for image build configs and run before/after, we must build a list of instructions from all configs 215 for i := range configsData { 216 configData := &configsData[i] 217 if configData.Config.ImageBuild != "" { 218 config.imageBuildConfigs = append([]TGFConfigBuild{TGFConfigBuild{ 219 Instructions: configData.Config.ImageBuild, 220 Folder: configData.Config.ImageBuildFolder, 221 Tag: configData.Config.ImageBuildTag, 222 source: configData.Name, 223 }}, config.imageBuildConfigs...) 224 } 225 if configData.Config.RunBefore != "" { 226 config.runBeforeCommands = append(config.runBeforeCommands, configData.Config.RunBefore) 227 } 228 if configData.Config.RunAfter != "" { 229 config.runAfterCommands = append(config.runAfterCommands, configData.Config.RunAfter) 230 } 231 } 232 // We reverse the execution of before scripts to ensure that more specific commands are executed last 233 config.runBeforeCommands = collections.AsList(config.runBeforeCommands).Reverse().Strings() 234 } 235 236 var reVersion = regexp.MustCompile(`(?P<version>\d+\.\d+(?:\.\d+){0,1})`) 237 238 // https://regex101.com/r/ZKt4OP/5 239 var reImage = regexp.MustCompile(`^(?P<image>.*?)(?::(?:` + reVersion.String() + `(?:(?P<sep>[\.-])(?P<spec>.+))?|(?P<fix>.+)))?$`) 240 241 // Validate ensure that the current version is compliant with the setting (mainly those in the parameter store1) 242 func (config *TGFConfig) Validate() (errors []error) { 243 if strings.Contains(config.Image, ":") { 244 errors = append(errors, ConfigWarning(fmt.Sprintf("Image should not contain the version: %s", config.Image))) 245 } 246 247 if config.ImageVersion != nil && strings.ContainsAny(*config.ImageVersion, ":-") { 248 errors = append(errors, ConfigWarning(fmt.Sprintf("Image version parameter should not contain the image name nor the specialized version: %s", *config.ImageVersion))) 249 } 250 251 if config.ImageTag != nil && strings.ContainsAny(*config.ImageTag, ":") { 252 errors = append(errors, ConfigWarning(fmt.Sprintf("Image tag parameter should not contain the image name: %s", *config.ImageTag))) 253 } 254 255 if config.RecommendedTGFVersion != "" { 256 if valid, err := CheckVersionRange(version, config.RecommendedTGFVersion); err != nil { 257 errors = append(errors, fmt.Errorf("Unable to check recommended tgf version %s vs %s: %v", version, config.RecommendedTGFVersion, err)) 258 } else if !valid { 259 errors = append(errors, ConfigWarning(fmt.Sprintf("TGF v%s does not meet the recommended version range %s", version, config.RecommendedTGFVersion))) 260 } 261 } 262 263 if config.RequiredVersionRange != "" && config.ImageVersion != nil && *config.ImageVersion != "" && reVersion.MatchString(*config.ImageVersion) { 264 if valid, err := CheckVersionRange(*config.ImageVersion, config.RequiredVersionRange); err != nil { 265 errors = append(errors, fmt.Errorf("Unable to check recommended image version %s vs %s: %v", *config.ImageVersion, config.RequiredVersionRange, err)) 266 return 267 } else if !valid { 268 errors = append(errors, VersionMistmatchError(fmt.Sprintf("Image %s does not meet the required version range %s", config.GetImageName(), config.RequiredVersionRange))) 269 return 270 } 271 } 272 273 if config.RecommendedImageVersion != "" && config.ImageVersion != nil && *config.ImageVersion != "" && reVersion.MatchString(*config.ImageVersion) { 274 if valid, err := CheckVersionRange(*config.ImageVersion, config.RecommendedImageVersion); err != nil { 275 errors = append(errors, fmt.Errorf("Unable to check recommended image version %s vs %s: %v", *config.ImageVersion, config.RecommendedImageVersion, err)) 276 } else if !valid { 277 errors = append(errors, ConfigWarning(fmt.Sprintf("Image %s does not meet the recommended version range %s", config.GetImageName(), config.RecommendedImageVersion))) 278 } 279 } 280 281 return 282 } 283 284 // GetImageName returns the actual image name 285 func (config *TGFConfig) GetImageName() string { 286 var suffix string 287 if config.ImageVersion != nil { 288 suffix += *config.ImageVersion 289 } 290 shouldAddTag := config.ImageVersion == nil || *config.ImageVersion == "" || reVersion.MatchString(*config.ImageVersion) 291 if config.ImageTag != nil && shouldAddTag { 292 if suffix != "" && *config.ImageTag != "" { 293 suffix += tagSeparator 294 } 295 suffix += *config.ImageTag 296 } 297 if len(suffix) > 1 { 298 return fmt.Sprintf("%s:%s", config.Image, suffix) 299 } 300 return config.Image 301 } 302 303 // ParseAliases will parse the original argument list and replace aliases only in the first argument. 304 func (config *TGFConfig) ParseAliases(args []string) []string { 305 if len(args) > 0 { 306 if replace := String(config.Aliases[args[0]]); replace != "" { 307 var result collections.StringArray 308 replace, quoted := replace.Protect() 309 result = replace.Fields() 310 if len(quoted) > 0 { 311 for i := range result { 312 result[i] = result[i].RestoreProtected(quoted).Trim(`"`) 313 } 314 } 315 return append(result.Strings(), args[1:]...) 316 } 317 } 318 return nil 319 } 320 321 func extractMapFromParameters(ssmParameterFolder string, parameters []*ssm.Parameter) map[string]string { 322 values := make(map[string]string) 323 for _, parameter := range parameters { 324 key := strings.TrimLeft(strings.Replace(*parameter.Name, ssmParameterFolder, "", 1), "/") 325 values[key] = *parameter.Value 326 } 327 return values 328 } 329 330 func findRemoteConfigFiles(parameterValues map[string]string) []string { 331 configLocation, configLocationOk := parameterValues[remoteConfigLocationParameter] 332 if !configLocationOk || configLocation == "" { 333 return []string{} 334 } 335 336 if !strings.HasSuffix(configLocation, "/") { 337 configLocation = configLocation + "/" 338 } 339 340 configPaths := []string{remoteDefaultConfigPath} 341 if configPathString, configPathsOk := parameterValues[remoteConfigPathsParameter]; configPathsOk && configPathString != "" { 342 configPaths = strings.Split(configPathString, ":") 343 } 344 345 tempDir := must(ioutil.TempDir("", "tgf-config-files")).(string) 346 defer os.RemoveAll(tempDir) 347 348 configs := []string{} 349 for _, configPath := range configPaths { 350 fullConfigPath := configLocation + configPath 351 destConfigPath := path.Join(tempDir, configPath) 352 source := must(getter.Detect(fullConfigPath, must(os.Getwd()).(string), getter.Detectors)).(string) 353 354 err := getter.Get(destConfigPath, source) 355 if err == nil { 356 _, err = os.Stat(destConfigPath) 357 if os.IsNotExist(err) { 358 err = errors.New("Config file was not found at the source") 359 } 360 } 361 362 if err != nil { 363 printWarning("Error fetching config at %s: %v", source, err) 364 continue 365 } 366 367 if content, err := ioutil.ReadFile(destConfigPath); err != nil { 368 printWarning("Error reading fetched config file %s: %v", configPath, err) 369 } else { 370 contentString := string(content) 371 if contentString != "" { 372 configs = append(configs, contentString) 373 } 374 } 375 } 376 377 return configs 378 } 379 380 func parseSsmConfig(parameterValues map[string]string) string { 381 ssmConfig := "" 382 for key, value := range parameterValues { 383 isDict := strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") 384 isList := strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") 385 if !isDict && !isList { 386 value = fmt.Sprintf("\"%s\"", value) 387 } 388 ssmConfig += fmt.Sprintf("%s: %s\n", key, value) 389 } 390 return ssmConfig 391 } 392 393 // Check if there is an AWS configuration available. 394 // 395 // We call this function before trying to init an AWS session. This avoid trying to init a session in a non AWS context 396 // and having to wait for metadata resolution or generating an error. 397 func awsConfigExist() bool { 398 if os.Getenv("AWS_PROFILE")+os.Getenv("AWS_ACCESS_KEY_ID")+os.Getenv("AWS_CONFIG_FILE") != "" { 399 // If any AWS identification variable is defined, we consider that we are in an AWS environment. 400 return true 401 } 402 403 if _, err := exec.LookPath("aws"); err == nil { 404 // If aws program is installed, we also consider that we are in an AWS environment. 405 return true 406 } 407 408 // Otherwise, we check if the current user has a folder named .aws defined under its home directory. 409 currentUser, err := user.Current() 410 if err != nil { 411 return false 412 } 413 awsFolder, err := os.Stat(filepath.Join(currentUser.HomeDir, ".aws")) 414 if err != nil { 415 return false 416 } 417 return awsFolder.IsDir() 418 } 419 420 // Return the list of configuration files found from the current working directory up to the root folder 421 func findConfigFiles(folder string) (result []string) { 422 configFiles := []string{userConfigFile, configFile} 423 if disableUserConfig { 424 configFiles = []string{configFile} 425 } 426 for _, file := range configFiles { 427 file = filepath.Join(folder, file) 428 if _, err := os.Stat(file); !os.IsNotExist(err) { 429 result = append(result, file) 430 } 431 } 432 433 if parent := filepath.Dir(folder); parent != folder { 434 result = append(findConfigFiles(parent), result...) 435 } 436 437 return 438 } 439 440 func getTgfConfigFields() []string { 441 fields := []string{} 442 classType := reflect.ValueOf(TGFConfig{}).Type() 443 for i := 0; i < classType.NumField(); i++ { 444 tagValue := classType.Field(i).Tag.Get("yaml") 445 if tagValue != "" { 446 fields = append(fields, strings.Replace(tagValue, ",omitempty", "", -1)) 447 } 448 } 449 return fields 450 } 451 452 func getSSMParameterFolder() string { 453 if value, ok := os.LookupEnv(ssmParameterFolderEnvVariable); ok { 454 return value 455 } 456 return defaultSSMParameterFolder 457 } 458 459 // CheckVersionRange compare a version with a range of values 460 // Check https://github.com/blang/semver/blob/master/README.md for more information 461 func CheckVersionRange(version, compare string) (bool, error) { 462 if strings.Count(version, ".") == 1 { 463 version = version + ".9999" // Patch is irrelevant if major and minor are OK 464 } 465 v, err := semver.Make(version) 466 if err != nil { 467 return false, err 468 } 469 470 comp, err := semver.ParseRange(compare) 471 if err != nil { 472 return false, err 473 } 474 475 return comp(v), nil 476 } 477 478 // ConfigWarning is used to represent messages that should not be considered as critical error 479 type ConfigWarning string 480 481 func (e ConfigWarning) Error() string { 482 return string(e) 483 } 484 485 // VersionMistmatchError is used to describe an out of range version 486 type VersionMistmatchError string 487 488 func (e VersionMistmatchError) Error() string { 489 return string(e) 490 }