diff --git a/.prerelease.yml b/.prerelease.yml new file mode 100644 index 0000000..6068b29 --- /dev/null +++ b/.prerelease.yml @@ -0,0 +1,2 @@ +release: + prerelease: true diff --git a/.travis.yml b/.travis.yml index 5cc5194..694b054 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,11 @@ script: fi - if [ ${TRAVIS_TAG::1} = v ]; then - curl -sL https://git.io/goreleaser | bash; + if [[ $TRAVIS_TAG =~ .*-.* ]]; + then + cat .prerelease.yml >> .goreleaser.yml && set -- --skip-validate; + fi; + curl -sL https://git.io/goreleaser | bash /dev/stdin $@; fi notifications: diff --git a/Dockerfile.2.K8S b/Dockerfile.2.K8S index 74649bd..1240fab 100644 --- a/Dockerfile.2.K8S +++ b/Dockerfile.2.K8S @@ -6,7 +6,7 @@ LABEL vendor="Coveo" LABEL maintainer "jgiroux@coveo.com" # Update version here (do not move at the beginning of the file since it would slow down the docker build) -ARG KUBE_VERSION=1.7.9 +ARG KUBE_VERSION=1.8.2 ARG KOPS_VERSION=1.7.1 ARG HELM_VERSION=2.7.0 diff --git a/README.md b/README.md index eca8d65..8bbd03e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A **T**erra**g**runt **f**rontend that allow execution of Terragrunt/Terraform t Table of content: -* [Description](#desciption) +* [Description](#description) * [Configuration](#configuration) * [Invocation arguments](#tgf-invocation) * [Images](#default-docker-images) @@ -94,9 +94,9 @@ Key | Description | Default value --- | --- | --- | docker-image | Identify the docker image to use | coveo/tgf | docker-image-version | Identify the image version | -| docker-image-tag | Identify the image tag (could specify specilized version such as aws, mysql, k8s, full) | latest +| docker-image-tag | Identify the image tag (could specify specialized version such as aws, mysql, k8s, full) | latest | docker-refresh | Delay before checking if a newer version of the docker image is available | 1h (1 hour) -| logging-level | Terragrunt logging level (only apply to Terragrunt entry point).
*Critical (0), Error (1), Warning (2), Notice (3), Info (4), Debug (5)* | Notice +| logging-level | Terragrunt logging level (only apply to Terragrunt entry point).
*Critical (0), Error (1), Warning (2), Notice (3), Info (4), Debug (5), Full (6)* | Notice | entry-point | The program that will be automatically launched when the docker starts | terragrunt | tgf-recommended-version | The minimal tgf version recommended in your context (should not be placed in `.tgf.config file`) | *no default* | recommended-image | The tgf image recommended in your context (should not be placed in `.tgf.config file`) | *no default* @@ -109,66 +109,59 @@ Note: *The key names are not case sensitive* > tgf usage: tgf [] -DESCRIPTION: TGF (terragrunt frontend) is a Docker frontend for terragrunt/terraform. It automatically maps your current folder, -your HOME folder, your TEMP folder as well of most environment variables to the docker process. You can add -D to your command to -get the exact docker command that is generated. +DESCRIPTION: -It then looks in your current folder and all its parents to find a file named '.tgf.config' to retrieve the default configuration. -If not all configurable values are satisfied and you have an AWS configuration, it will then try to retrieve the missing elements -from the AWS Parameter Store under the key '/default/tgf'. +TGF (terragrunt frontend) is a Docker frontend for terragrunt/terraform. It automatically maps your current folder, your HOME folder, your TEMP folder as well of most +environment variables to the docker process. You can add -D to your command to get the exact docker command that is generated. -Configurable values are: docker-image, docker-image-version, docker-image-tag, docker-refresh, logging-level, entry-point, tgf-recommended-version, recommended-image. +It then looks in your current folder and all its parents to find a file named '.tgf.config' to retrieve the default configuration. If not all configurable values are satisfied and you have an AWS configuration, +it will then try to retrieve the missing elements from the AWS Parameter Store under the key '/default/tgf'. + +Configurable values are: docker-image, docker-image-version, docker-image-tag, docker-refresh, recommended-image-version, required-image-version, logging-level, entry-point, tgf-recommended-version. You can get the full documentation at https://github.com/coveo/tgf/blob/master/README.md and check for new version at https://github.com/coveo/tgf/releases/latest. Any docker image could be used, but TGF specialized images could be found at: https://hub.docker.com/r/coveo/tgf/tags. -Terragrunt documentation could be found at https://github.com/coveo/terragrunt/blob/master/README.md (Coveo fork) or https://github.com/gruntwork-io/terragrunt/blob/master/README.md (Gruntwork.io original) +Terragrunt documentation could be found at https://github.com/coveo/terragrunt/blob/master/README.md (Coveo fork) or https://github.com/gruntwork-io/terragrunt/blob/master/README.md +(Gruntwork.io original) Terraform documentation could be found at https://www.terraform.io/docs/index.html. -IMPORTANT: Most of the tgf command line arguments are in uppercase to avoid potential conflict with the underlying command. If you -must supply parameters to your command and they are unwillingly catched by tgf, you have to put them after '--' such as in the following example: +IMPORTANT: + +Most of the tgf command line arguments are in uppercase to avoid potential conflict with the underlying command. If any of the tgf arguments conflicts with an argument of the desired entry point, you must place that argument after -- to ensure that they are not interpreted by tgf and are passed to the entry point. Any non conflicting argument will be passed to the entry point wherever it is located on the invocation arguments. tgf ls -- -D # Avoid -D to be interpretated by tgf as --debug-docker -VERSION: master +VERSION: 1.15.0 AUTHOR: Coveo 🇲🇶 🇨🇦 Flags: - -H, --tgf-help Show context-sensitive help (also try - --help-man). +Flags: + -H, --tgf-help Show context-sensitive help (also try --help-man). -E, --entrypoint=terragrunt Override the entry point for docker - --image=coveo/tgf Use the specified image instead of the default - one - --image-version=version Use a different version of docker image instead - of the default one (alias --iv) - -T, --tag=latest Use a different tag of docker image instead of - the default one + --image=coveo/tgf Use the specified image instead of the default one + --image-version=version Use a different version of docker image instead of the default one (alias --iv) + -T, --tag=latest Use a different tag of docker image instead of the default one --profile="" Set the AWS profile configuration to use -D, --debug-docker Print the docker command issued --refresh-image Force a refresh of the docker image (alias --ri) - -V, --tgf-version Get the current version of tgf - -L, --logging-level= Set the logging level (critical=0, error=1, - warning=2, notice=3, info=4, debug=5) - -F, --flush-cache Invoke terragrunt with --terragrunt-update-source - to flush the cache - --no-home Disable the mapping of the home directory (alias - --nh) + -L, --logging-level= Set the logging level (critical=0, error=1, warning=2, notice=3, info=4, debug=5, full=6) + -F, --flush-cache Invoke terragrunt with --terragrunt-update-source to flush the cache + --no-home Disable the mapping of the home directory (alias --nh) --get-image-name Just return the resulting image name (alias --gi) --docker-arg= ... Supply extra argument to Docker (alias --da) + --all-versions Get versions of TGF & all others underlying utilities (alias --av) + --current-version Get current version information (alias --cv) ``` -If any of the tgf arguments conflicts with an argument of the desired entry point, you must place that argument after -- to ensure that they are -not interpreted by tgf and are passed to the entry point. Any non conflicting argument will be passed to the entry point wherever it is located on -the invocation arguments. - Example: ```bash -> tgf --version -v1.14.0 +> tgf --current-version +tgf v1.15.0 ``` Returns the current version of the tgf tool @@ -195,12 +188,13 @@ Returns the version of `Terraform` since we specified the entry point to be terr * [Terraform](https://www.terraform.io/) * [Terragrunt](https://github.com/coveo/terragrunt) * [Go Template](https://github.com/coveo/gotemplate) -* Shells +* Shells & tools * `sh` + * `openssl` ### Default image: coveo/tgf (based on Alpine) -All tools included in `coveo/tgf.base` plus: +All tools included in `coveo/tgf:base` plus: * [Python](https://www.python.org/) (2 and 3) * [Ruby](https://www.ruby-lang.org/en/) @@ -208,17 +202,43 @@ All tools included in `coveo/tgf.base` plus: * [jq](https://stedolan.github.io/jq/) * [Terraforming](https://github.com/dtan4/terraforming) * [Tflint](https://github.com/wata727/tflint) +* [Terraform-docs](https://github.com/segmentio/terraform-docs) * [Terraform Quantum Provider](https://github.com/coveo/terraform-provider-quantum) * Shells * `bash` * `zsh` * `fish` +* Tools & editors + * `vim` + * `nano` + * `zip` + * `git` -### Full image: coveo/tgf.full (based on Ubuntu) +### AWS provider specialized image: coveo/tgf:aws (based on Alpine) All tools included in `coveo/tgf` plus: -* [AWS Tools for Powershell](https://aws.amazon.com/powershell/) +* [terraform-provider-aws (fork)](https://github.com/coveo/terraform-provider-aws) + +### Kubernetes tools (based on Alpine) + +All tools included in `coveo/tgf:aws` plus: + +* `kubectl` +* `kops` +* `helm` + +### MySQL image: coveo/tgf:mysql (based on Alpine) + +All tools included in `coveo/tgf:aws` plus: + +* `mysql-client` + +### Full image: coveo/tgf:full (based on Ubuntu) + +All tools included in the other images plus: + +* [AWS Tools for Powershell](https://aws.amazon.com/powershell) * [Oh My ZSH](http://ohmyz.sh/) * Shells * `powershell` diff --git a/config.go b/config.go index 6c24a31..ee0dcf7 100644 --- a/config.go +++ b/config.go @@ -6,42 +6,57 @@ import ( "os" "os/user" "path/filepath" + "regexp" "strings" "time" "github.com/aws/aws-sdk-go/service/ssm" - "gopkg.in/yaml.v2" - + "github.com/blang/semver" "github.com/gruntwork-io/terragrunt/aws_helper" "github.com/hashicorp/hcl" + "gopkg.in/yaml.v2" ) const ( - parameterFolder = "/default/tgf" - configFile = ".tgf.config" - dockerImage = "docker-image" - dockerImageVersion = "docker-image-version" - dockerImageTag = "docker-image-tag" - dockerRefresh = "docker-refresh" - loggingLevel = "logging-level" - entryPoint = "entry-point" - tgfVersion = "tgf-recommended-version" - recommendedImage = "recommended-image" + parameterFolder = "/default/tgf" + configFile = ".tgf.config" + dockerImage = "docker-image" + dockerImageVersion = "docker-image-version" + dockerImageTag = "docker-image-tag" + dockerRefresh = "docker-refresh" + loggingLevel = "logging-level" + entryPoint = "entry-point" + tgfVersion = "tgf-recommended-version" + recommendedImageVersion = "recommended-image-version" + requiredImageVersion = "required-image-version" + deprecatedRecommendedImage = "recommended-image" ) type tgfConfig struct { - Image string - ImageVersion string - ImageTag string - LogLevel string - EntryPoint string - Refresh time.Duration - RecommendedMinimalVersion string - RecommendedImage string + Image string + ImageVersion string + ImageTag string + LogLevel string + EntryPoint string + Refresh time.Duration + RecommendedImageVersion string + RequiredVersionRange string + RecommendedTGFVersion string + recommendedImage string + separator string } -func (config *tgfConfig) complete() bool { - return config.Image != "" && config.LogLevel != "" && config.Refresh != 0 && config.EntryPoint != "" && config.RecommendedMinimalVersion != "" +func (config *tgfConfig) String() (result string) { + result += fmt.Sprintln(dockerImage, "=", config.Image) + result += fmt.Sprintln(" ", dockerImageVersion, "=", config.ImageVersion) + result += fmt.Sprintln(" ", dockerImageTag, "=", config.ImageTag) + result += fmt.Sprintln(" ", recommendedImageVersion, "=", config.RecommendedImageVersion) + result += fmt.Sprintln(" ", requiredImageVersion, "=", config.RequiredVersionRange) + result += fmt.Sprintln(" ", dockerRefresh, "=", config.Refresh) + result += fmt.Sprintln(loggingLevel, "=", config.LogLevel) + result += fmt.Sprintln(entryPoint, "=", config.EntryPoint) + result += fmt.Sprintln(tgfVersion, config.RecommendedTGFVersion) + return } // SetDefaultValues sets the uninitialized values from the config files and the parameter store @@ -62,7 +77,7 @@ func (config *tgfConfig) SetDefaultValues() { } } - if !config.complete() && awsConfigExist() { + if awsConfigExist() { // If we need to read the parameter store, we must init the session first to ensure that // the credentials are only initialized once (avoiding asking multiple time the MFA) if _, err := aws_helper.InitAwsSession(""); err != nil { @@ -82,18 +97,32 @@ func (config *tgfConfig) SetDefaultValues() { // SetValue sets value of the key in the configuration only if it does not already have a value func (config *tgfConfig) SetValue(key, value string) { + if value == "" { + return + } switch strings.ToLower(key) { case dockerImage: - if config.Image == "" { - config.Image = value + if strings.Contains(value, ":") && config.Image == "" { + fmt.Fprintf(os.Stderr, warningString("Parameter %s should not contains the version: %s\n", key, value)) } + config.apply(value) case dockerImageVersion: - if config.ImageVersion == "" { - config.ImageVersion = value + if strings.ContainsAny(value, ":-") && config.ImageVersion == "" { + fmt.Fprintf(os.Stderr, warningString("Parameter %s should not contains the image name nor the specialized version: %s\n", key, value)) } + config.apply(":" + value) case dockerImageTag: - if config.ImageTag == "" { - config.ImageTag = value + if strings.ContainsAny(value, ":") && config.ImageTag == "" { + fmt.Fprintf(os.Stderr, warningString("Parameter %s should not contains the image name: %s\n", key, value)) + } + config.apply(":" + value) + case recommendedImageVersion: + if config.RecommendedImageVersion == "" { + config.RecommendedImageVersion = value + } + case requiredImageVersion: + if config.RequiredVersionRange == "" { + config.RequiredVersionRange = value } case dockerRefresh: if config.Refresh == 0 { @@ -108,15 +137,88 @@ func (config *tgfConfig) SetValue(key, value string) { config.EntryPoint = value } case tgfVersion: - if config.RecommendedMinimalVersion == "" { - config.RecommendedMinimalVersion = value - } - case recommendedImage: - if config.RecommendedImage == "" { - config.RecommendedImage = value + if config.RecommendedTGFVersion == "" { + config.RecommendedTGFVersion = value } + case deprecatedRecommendedImage: + fmt.Fprintf(os.Stderr, warningString("Config key %s is deprecated (%s ignored)\n", key, value)) default: - fmt.Fprintf(os.Stderr, "Unknown parameter %s = %s\n", key, value) + fmt.Fprintf(os.Stderr, errorString("Unknown parameter %s = %s\n", key, value)) + } +} + +func (config *tgfConfig) Validate() (errors []error) { + if config.RecommendedImageVersion != "" { + if valid, err := CheckVersionRange(config.ImageVersion, config.RecommendedImageVersion); err != nil { + errors = append(errors, fmt.Errorf("Unable to check recommended image version %s vs %s: %v", config.ImageVersion, config.RecommendedImageVersion, err)) + } else if !valid { + errors = append(errors, ConfigWarning(fmt.Sprintf("Image %s does not meet the recommended version range %s", config.GetImageName(), config.RecommendedImageVersion))) + } + } + + if config.RequiredVersionRange != "" { + if valid, err := CheckVersionRange(config.ImageVersion, config.RequiredVersionRange); err != nil { + errors = append(errors, fmt.Errorf("Unable to check recommended image version %s vs %s: %v", config.ImageVersion, config.RequiredVersionRange, err)) + } else if !valid { + errors = append(errors, VersionMistmatchError(fmt.Sprintf("Image %s does not meet the required version range %s", config.GetImageName(), config.RequiredVersionRange))) + } + } + + if config.RecommendedTGFVersion != "" { + if valid, err := CheckVersionRange(version, config.RecommendedTGFVersion); err != nil { + errors = append(errors, fmt.Errorf("Unable to check recommended tgf version %s vs %s: %v", version, config.RecommendedTGFVersion, err)) + } else if !valid { + errors = append(errors, ConfigWarning(fmt.Sprintf("TGF v%s does not meet the recommended version range %s", version, config.RecommendedTGFVersion))) + } + } + return +} + +func (config *tgfConfig) GetImageName() string { + image := config.Image + if config.separator == "" { + config.separator = "-" + } + suffix := fmt.Sprintf("%s%s%s", config.ImageVersion, config.separator, config.ImageTag) + if len(suffix) > 1 { + return fmt.Sprintf("%s:%s", image, suffix) + } + return image +} + +// https://regex101.com/r/ZKt4OP/2/ +var reVersion = regexp.MustCompile(`^(?P.*?)(:((?P\d+\.\d+\.\d+)((?P[\.-])(?P.+))?|(?P.+)))?$`) + +func (config *tgfConfig) apply(value string) { + matches := reVersion.FindStringSubmatch(value) + var valueUsed bool + for i, name := range reVersion.SubexpNames() { + switch name { + case "image": + if config.Image == "" { + config.Image = matches[i] + valueUsed = true + } + if matches[i] != "" { + config.recommendedImage = matches[i] + } + case "version": + if config.ImageVersion == "" && config.Image == config.recommendedImage { + config.ImageVersion = matches[i] + valueUsed = true + } + case "spec": + fallthrough + case "fix": + if config.ImageTag == "" && config.Image == config.recommendedImage { + config.ImageTag = matches[i] + valueUsed = true + } + case "sep": + if config.separator == "" && config.Image == config.recommendedImage && valueUsed { + config.separator = matches[i] + } + } } } @@ -151,3 +253,33 @@ func awsConfigExist() bool { return awsFolder.IsDir() } + +// CheckVersionRange compare a version with a range of values +// Check https://github.com/blang/semver/blob/master/README.md for more information +func CheckVersionRange(version, compare string) (bool, error) { + v, err := semver.Make(version) + if err != nil { + return false, err + } + + comp, err := semver.ParseRange(compare) + if err != nil { + return false, err + } + + return comp(v), nil +} + +// ConfigWarning is used to represent messages that should not be considered as critical error +type ConfigWarning string + +func (e ConfigWarning) Error() string { + return string(e) +} + +// VersionMistmatchError is used to describe an out of range version +type VersionMistmatchError string + +func (e VersionMistmatchError) Error() string { + return string(e) +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..dc8783a --- /dev/null +++ b/config_test.go @@ -0,0 +1,37 @@ +package main + +import "testing" + +func TestCheckVersionRange(t *testing.T) { + type args struct { + version string + compare string + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + {"Invalid version", args{"x", "y"}, false, true}, + {"Valid", args{"1.20.0", ">=1.19.x"}, true, false}, + {"Out of range", args{"1.15.9-Beta.1", ">=1.19.x"}, false, false}, + {"Same", args{"1.22.1", "=1.22.1"}, true, false}, + {"Not same", args{"1.22.1", "=1.22.2"}, false, false}, + {"Same minor", args{"1.22.1", "=1.22.x"}, true, false}, + {"Same major", args{"1.22.1", "=1.x"}, true, false}, + {"Not same major", args{"2.22.1", "=1.x"}, false, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CheckVersionRange(tt.args.version, tt.args.compare) + if (err != nil) != tt.wantErr { + t.Errorf("CheckVersionRange() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("CheckVersionRange() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/docker.go b/docker.go index 5dcfa01..8c4db81 100644 --- a/docker.go +++ b/docker.go @@ -21,6 +21,12 @@ func callDocker(config tgfConfig, mapHome bool, flushCache bool, debug bool, doc // Change the default log level for terragrunt const logLevelArg = "--terragrunt-logging-level" if !util.ListContainsElement(command, logLevelArg) && config.EntryPoint == "terragrunt" { + if config.LogLevel == "6" || strings.ToLower(config.LogLevel) == "full" { + config.LogLevel = "debug" + os.Setenv("TF_LOG", "DEBUG") + os.Setenv("TERRAGRUNT_DEBUG", "1") + } + // The log level option should not be supplied if there is no actual command for _, arg := range args { if !strings.HasPrefix(arg, "-") { @@ -37,18 +43,19 @@ func callDocker(config tgfConfig, mapHome bool, flushCache bool, debug bool, doc currentUser := Must(user.Current()).(*user.User) home := filepath.ToSlash(currentUser.HomeDir) homeWithoutVolume := strings.TrimPrefix(home, filepath.VolumeName(home)) - cwd := filepath.ToSlash(Must(os.Getwd()).(string)) + cwd := filepath.ToSlash(Must(os.Getwd()).(string)) currentDrive := fmt.Sprintf("%s/", filepath.VolumeName(cwd)) rootFolder := strings.Split(strings.TrimPrefix(cwd, currentDrive), "/")[0] - tempDrive := fmt.Sprintf("%s/", filepath.VolumeName(os.TempDir())) - tempFolder := strings.Split(strings.TrimPrefix(os.TempDir(), tempDrive), "/")[0] + temp := filepath.ToSlash(filepath.Join(os.TempDir(), "tgf-cache")) + tempDrive := fmt.Sprintf("%s/", filepath.VolumeName(temp)) + tempFolder := strings.TrimPrefix(temp, tempDrive) dockerArgs := []string{ "run", "-it", "-v", fmt.Sprintf("%s%s:/%[2]s", convertDrive(currentDrive), rootFolder), - "-v", fmt.Sprintf("%s%s:/%[2]s", convertDrive(tempDrive), tempFolder), + "-v", fmt.Sprintf("%s%s:/var/tgf", convertDrive(tempDrive), tempFolder), "-w", strings.TrimPrefix(cwd, filepath.VolumeName(cwd)), "--rm", } @@ -59,21 +66,19 @@ func callDocker(config tgfConfig, mapHome bool, flushCache bool, debug bool, doc }...) } + os.Setenv("TERRAGRUNT_CACHE", "/var/tgf") os.Setenv("TGF_COMMAND", config.EntryPoint) os.Setenv("TGF_VERSION", version) - image := strings.Split(config.Image, ":") - os.Setenv("TGF_IMAGE", image[0]) - if len(image) > 1 { - os.Setenv("TGF_IMAGE_TAG", image[1]) - } else { - os.Setenv("TGF_IMAGE_TAG", "latest") - } + os.Setenv("TGF_IMAGE", config.Image) + os.Setenv("TGF_IMAGE_VERSION", config.ImageVersion) + os.Setenv("TGF_IMAGE_TAG", config.ImageTag) + os.Setenv("TGF_IMAGE_NAME", config.GetImageName()) for _, do := range dockerOptions { dockerArgs = append(dockerArgs, strings.Split(do, " ")...) } dockerArgs = append(dockerArgs, getEnviron(mapHome)...) - dockerArgs = append(dockerArgs, config.Image) + dockerArgs = append(dockerArgs, config.GetImageName()) dockerArgs = append(dockerArgs, command...) dockerCmd := exec.Command("docker", dockerArgs...) dockerCmd.Stdin, dockerCmd.Stdout = os.Stdin, os.Stdout diff --git a/main.go b/main.go index 89ba576..db2bbed 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,6 @@ import ( "fmt" "html/template" "os" - "path/filepath" "strings" "github.com/fatih/color" @@ -18,12 +17,13 @@ var version = "master" var description = ` DESCRIPTION: -TGF ({{ .terragrunt }}) is a Docker frontend for terragrunt/terraform. It automatically maps your current folder, your HOME folder, your TEMP folder -as well of most environment variables to the docker process. You can add -D to your command to get the exact docker command that is generated. +TGF ({{ .terragrunt }}) is a Docker frontend for terragrunt/terraform. It automatically maps your current folder, +your HOME folder, your TEMP folder as well of most environment variables to the docker process. You can add -D to +your command to get the exact docker command that is generated. -It then looks in your current folder and all its parents to find a file named '{{ .config }}' to retrieve the default configuration. If not all -configurable values are satisfied and you have an AWS configuration, it will then try to retrieve the missing elements from the AWS Parameter -Store under the key '{{ .parameterStoreKey }}'. +It then looks in your current folder and all its parents to find a file named '{{ .config }}' to retrieve the +default configuration. If not all configurable values are satisfied and you have an AWS configuration, it will +then try to retrieve the missing elements from the AWS Parameter Store under the key '{{ .parameterStoreKey }}'. Configurable values are: {{ .options }}. @@ -36,8 +36,11 @@ Terragrunt documentation could be found at {{ .terragruntCoveo }} (Coveo fork) o Terraform documentation could be found at {{ .terraform }}. IMPORTANT: -Most of the tgf command line arguments are in uppercase to avoid potential conflict with the underlying command. If you must -supply parameters to your command and they are unwillingly catched by tgf, you have to put them after '--' such as in the following example: +Most of the tgf command line arguments are in uppercase to avoid potential conflict with the underlying command. +If any of the tgf arguments conflicts with an argument of the desired entry point, you must place that argument +after -- to ensure that they are not interpreted by tgf and are passed to the entry point. Any non conflicting +argument will be passed to the entry point wherever it is located on the invocation arguments. + tgf ls -- -D # Avoid -D to be interpretated by tgf as --debug-docker VERSION: {{ .version }} @@ -49,7 +52,7 @@ func main() { // Handle eventual panic message defer func() { if err := recover(); err != nil { - fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, errorString("%[1]v (%[1]T)", err)) os.Exit(1) } }() @@ -62,15 +65,18 @@ func main() { descriptionTemplate.Execute(&descriptionBuffer, map[string]interface{}{ "parameterStoreKey": parameterFolder, "config": configFile, - "options": color.GreenString(strings.Join([]string{dockerImage, dockerImageVersion, dockerImageTag, dockerRefresh, loggingLevel, entryPoint, tgfVersion, recommendedImage}, ", ")), - "readme": link(gitSource + "/blob/master/README.md"), - "latest": link(gitSource + "/releases/latest"), - "terragruntCoveo": link("https://github.com/coveo/terragrunt/blob/master/README.md"), - "terragruntGW": link("https://github.com/gruntwork-io/terragrunt/blob/master/README.md"), - "terraform": link("https://www.terraform.io/docs/index.html"), - "tgfImages": link("https://hub.docker.com/r/coveo/tgf/tags"), - "terragrunt": bold("t") + "erra" + bold("g") + "runt " + bold("f") + "rontend", - "version": version, + "options": color.GreenString(strings.Join([]string{ + dockerImage, dockerImageVersion, dockerImageTag, dockerRefresh, recommendedImageVersion, requiredImageVersion, + loggingLevel, entryPoint, tgfVersion, + }, ", ")), + "readme": link(gitSource + "/blob/master/README.md"), + "latest": link(gitSource + "/releases/latest"), + "terragruntCoveo": link("https://github.com/coveo/terragrunt/blob/master/README.md"), + "terragruntGW": link("https://github.com/gruntwork-io/terragrunt/blob/master/README.md"), + "terraform": link("https://www.terraform.io/docs/index.html"), + "tgfImages": link("https://hub.docker.com/r/coveo/tgf/tags"), + "terragrunt": bold("t") + "erra" + bold("g") + "runt " + bold("f") + "rontend", + "version": version, }) var app = NewApplication(kingpin.New(os.Args[0], descriptionBuffer.String())) @@ -88,19 +94,22 @@ func main() { awsProfile = app.Argument("profile", "Set the AWS profile configuration to use").Default("").String() debug = app.Switch("debug-docker", "Print the docker command issued", 'D').Bool() refresh = app.Switch("refresh-image", "Force a refresh of the docker image (alias --ri)").Bool() - getVersion = app.Switch("tgf-version", "Get the current version of tgf", 'V').Bool() - loggingLevel = app.Argument("logging-level", "Set the logging level (critical=0, error=1, warning=2, notice=3, info=4, debug=5)", 'L').PlaceHolder("").String() + loggingLevel = app.Argument("logging-level", "Set the logging level (critical=0, error=1, warning=2, notice=3, info=4, debug=5, full=6)", 'L').PlaceHolder("").String() flushCache = app.Switch("flush-cache", "Invoke terragrunt with --terragrunt-update-source to flush the cache", 'F').Bool() noHome = app.Switch("no-home", "Disable the mapping of the home directory (alias --nh)").Bool() getImageName = app.Switch("get-image-name", "Just return the resulting image name (alias --gi)").Bool() dockerOptions = app.Argument("docker-arg", "Supply extra argument to Docker (alias --da)").PlaceHolder("").Strings() + getAllVersions = app.Switch("all-versions", "Get versions of TGF & all others underlying utilities (alias --av)").Bool() + getCurrentVersion = app.Switch("current-version", "Get current version infomation (alias --cv)").Bool() // Shorten version of the tags - refresh2 = app.Switch("ri", "alias for refresh-image)").Hidden().Bool() - getImageName2 = app.Switch("gi", "alias for get-image-name").Hidden().Bool() - noHome2 = app.Switch("nh", "alias for no-home-mapping").Hidden().Bool() - dockerOptions2 = app.Argument("da", "alias for docker-arg").Hidden().Strings() - imageVersion2 = app.Argument("iv", "alias for image-version").Hidden().String() + refresh2 = app.Switch("ri", "alias for refresh-image)").Hidden().Bool() + getImageName2 = app.Switch("gi", "alias for get-image-name").Hidden().Bool() + noHome2 = app.Switch("nh", "alias for no-home-mapping").Hidden().Bool() + getCurrentVersion2 = app.Switch("cv", "alias for current-version").Hidden().Bool() + getAllVersions2 = app.Switch("av", "alias for all-versions").Hidden().Bool() + dockerOptions2 = app.Argument("da", "alias for docker-arg").Hidden().Strings() + imageVersion2 = app.Argument("iv", "alias for image-version").Hidden().String() ) // Split up the managed parameters from the unmanaged ones @@ -110,54 +119,71 @@ func main() { // We combine the tags that have multiple definitions *refresh = *refresh || *refresh2 *noHome = *noHome || *noHome2 + *getCurrentVersion = *getCurrentVersion || *getCurrentVersion2 + *getAllVersions = *getAllVersions || *getAllVersions2 *getImageName = *getImageName || *getImageName2 *dockerOptions = append(*dockerOptions, *dockerOptions2...) *imageVersion += *imageVersion2 - if *getVersion { - fmt.Println(version) - os.Exit(0) - } - if *awsProfile != "" { Must(aws_helper.InitAwsSession(*awsProfile)) } config := tgfConfig{} config.SetValue(dockerImage, *image) + config.SetValue(dockerImageVersion, *imageVersion) + config.SetValue(dockerImageTag, *imageTag) config.SetValue(entryPoint, *defaultEntryPoint) config.SetDefaultValues() - _ = *imageVersion - if *imageTag != "" { - split := strings.Split(config.Image, ":") - config.Image = strings.Join([]string{split[0], *imageTag}, ":") + var fatalError bool + for _, err := range config.Validate() { + switch err := err.(type) { + case ConfigWarning: + fmt.Fprintln(os.Stderr, warningString("%v", err)) + case VersionMistmatchError: + fmt.Fprintln(os.Stderr, errorString("%v", err)) + if *imageVersion == "" { + // We consider this as a fatal error only if the version has not been explicitly specified on the command line + fatalError = true + } + default: + fmt.Fprintln(os.Stderr, errorString("%v", err)) + fatalError = true + } + } + if fatalError { + os.Exit(1) } if *getImageName { - fmt.Println(config.Image) + fmt.Println(config.GetImageName()) os.Exit(0) } - if !isVersionedImage(config.Image) && lastRefresh(config.Image) > config.Refresh || !checkImage(config.Image) || *refresh { - refreshImage(config.Image) + if *getCurrentVersion { + fmt.Printf("tgf v%s\n", version) + os.Exit(0) } - os.Setenv("TERRAGRUNT_CACHE", filepath.Join(os.TempDir(), "tgf-cache")) - - if *loggingLevel != "" { - config.LogLevel = *loggingLevel + if *getAllVersions { + if config.EntryPoint != "terragrunt" { + fmt.Fprintln(os.Stderr, errorString("--all-version works only with terragrunt as the entrypoint")) + os.Exit(1) + } + fmt.Println("TGF version", version) + unmanaged = []string{"get-versions"} } - if config.RecommendedMinimalVersion != "" && version < config.RecommendedMinimalVersion { - fmt.Fprintf(os.Stderr, "Your version of tgf is outdated, you have %s. The recommended minimal version is %s\n\n", version, config.RecommendedMinimalVersion) + if config.ImageVersion == "" && lastRefresh(config.GetImageName()) > config.Refresh || !checkImage(config.GetImageName()) || *refresh { + refreshImage(config.GetImageName()) } - if config.RecommendedImage != "" && config.Image != config.RecommendedImage && image == nil && imageTag == nil { - fmt.Fprintf(os.Stderr, "A new version of tgf image is available, you use %s. The recommended image is %s\n\n", config.Image, config.RecommendedImage) + if *loggingLevel != "" { + config.LogLevel = *loggingLevel } - if unmanaged == nil && !*debug { + if unmanaged == nil && !*debug && config.EntryPoint == "terragrunt" { title := color.New(color.FgYellow, color.Underline).SprintFunc() fmt.Println(title("\nTGF Usage\n")) app.Usage(nil) @@ -165,3 +191,6 @@ func main() { os.Exit(callDocker(config, !*noHome, *flushCache, *debug, *dockerOptions, unmanaged...)) } + +var warningString = color.New(color.FgYellow).SprintfFunc() +var errorString = color.New(color.FgRed).SprintfFunc()