From acc2afa40c3fd42388d4f0d62ec9d4946dd52aea Mon Sep 17 00:00:00 2001 From: Stoyan Rachev Date: Wed, 3 Nov 2021 19:16:14 +0200 Subject: [PATCH] Upgrade terraform to 0.13.7 --- build/alicloud/TF_VERSION | 2 +- build/alicloud/terraform-bundle.hcl | 12 +++- build/all/TF_VERSION | 2 +- build/all/terraform-bundle.hcl | 38 +++++++++--- build/aws/TF_VERSION | 2 +- build/aws/terraform-bundle.hcl | 12 +++- build/azure/TF_VERSION | 2 +- build/azure/terraform-bundle.hcl | 12 +++- build/equinixmetal/terraform-bundle.hcl | 2 +- build/fetch-providers.sh | 6 +- build/gcp/TF_VERSION | 2 +- build/gcp/terraform-bundle.hcl | 16 +++-- build/openstack/TF_VERSION | 2 +- build/openstack/terraform-bundle.hcl | 13 +++- build/slim/TF_VERSION | 2 +- hack/get-version.sh | 2 +- pkg/terraformer/terraformer.go | 81 +++++++++++++++++++++++-- pkg/terraformer/types.go | 2 + test/utils/objects.go | 2 +- 19 files changed, 172 insertions(+), 40 deletions(-) diff --git a/build/alicloud/TF_VERSION b/build/alicloud/TF_VERSION index f98d9c0d..5daaa7ba 100644 --- a/build/alicloud/TF_VERSION +++ b/build/alicloud/TF_VERSION @@ -1 +1 @@ -0.12.31 +0.13.7 diff --git a/build/alicloud/terraform-bundle.hcl b/build/alicloud/terraform-bundle.hcl index 14ba2e04..3ac05641 100644 --- a/build/alicloud/terraform-bundle.hcl +++ b/build/alicloud/terraform-bundle.hcl @@ -7,7 +7,13 @@ terraform { } providers { - alicloud = ["1.124.2"] - template = ["2.1.2"] - null = ["2.1.2"] + alicloud = { + versions = ["1.124.2"] + } + template = { + versions = ["2.1.2"] + } + null = { + versions = ["2.1.2"] + } } diff --git a/build/all/TF_VERSION b/build/all/TF_VERSION index f98d9c0d..5daaa7ba 100644 --- a/build/all/TF_VERSION +++ b/build/all/TF_VERSION @@ -1 +1 @@ -0.12.31 +0.13.7 diff --git a/build/all/terraform-bundle.hcl b/build/all/terraform-bundle.hcl index 3a9f55a0..f5ce4761 100644 --- a/build/all/terraform-bundle.hcl +++ b/build/all/terraform-bundle.hcl @@ -7,13 +7,33 @@ terraform { } providers { - aws = ["3.63.0"] - azurerm = ["2.68.0"] - google = ["3.62.0"] - google-beta = ["3.62.0"] - openstack = ["1.37.0"] - alicloud = ["1.124.2"] - packet = ["2.3.0"] - template = ["2.1.2"] - null = ["2.1.2"] + aws = { + versions = ["3.63.0"] + } + azurerm = { + versions = ["2.68.0"] + } + google = { + versions = ["3.62.0"] + } + google-beta = { + versions = ["3.62.0"] + } + openstack = { + versions = ["1.37.0"] + source = "terraform-provider-openstack/openstack" + } + alicloud = { + versions = ["1.124.2"] + } + metal = { + versions = ["3.1.0"] + source = "equinix/metal" + } + template = { + versions = ["2.1.2"] + } + null = { + versions = ["2.1.2"] + } } diff --git a/build/aws/TF_VERSION b/build/aws/TF_VERSION index f98d9c0d..5daaa7ba 100644 --- a/build/aws/TF_VERSION +++ b/build/aws/TF_VERSION @@ -1 +1 @@ -0.12.31 +0.13.7 diff --git a/build/aws/terraform-bundle.hcl b/build/aws/terraform-bundle.hcl index 279a3b60..d2ebb978 100644 --- a/build/aws/terraform-bundle.hcl +++ b/build/aws/terraform-bundle.hcl @@ -7,7 +7,13 @@ terraform { } providers { - aws = ["3.63.0"] - template = ["2.1.2"] - null = ["2.1.2"] + aws = { + versions = ["3.63.0"] + } + template = { + versions = ["2.1.2"] + } + null = { + versions = ["2.1.2"] + } } diff --git a/build/azure/TF_VERSION b/build/azure/TF_VERSION index f98d9c0d..5daaa7ba 100644 --- a/build/azure/TF_VERSION +++ b/build/azure/TF_VERSION @@ -1 +1 @@ -0.12.31 +0.13.7 diff --git a/build/azure/terraform-bundle.hcl b/build/azure/terraform-bundle.hcl index 8c8f45e8..cf7c281d 100644 --- a/build/azure/terraform-bundle.hcl +++ b/build/azure/terraform-bundle.hcl @@ -7,7 +7,13 @@ terraform { } providers { - azurerm = ["2.68.0"] - template = ["2.1.2"] - null = ["2.1.2"] + azurerm = { + versions = ["2.68.0"] + } + template = { + versions = ["2.1.2"] + } + null = { + versions = ["2.1.2"] + } } diff --git a/build/equinixmetal/terraform-bundle.hcl b/build/equinixmetal/terraform-bundle.hcl index fd0c036f..034194b9 100644 --- a/build/equinixmetal/terraform-bundle.hcl +++ b/build/equinixmetal/terraform-bundle.hcl @@ -7,7 +7,7 @@ terraform { } providers { - metal = { + metal = { versions = ["3.1.0"] source = "equinix/metal" } diff --git a/build/fetch-providers.sh b/build/fetch-providers.sh index 76e00aa4..68d247a4 100755 --- a/build/fetch-providers.sh +++ b/build/fetch-providers.sh @@ -25,5 +25,9 @@ else # Above `terraform-bundle` command already maintains such structure, hence, we keep it instead of copying # the `terraform-provider` binaries. find . -name "*.md" | xargs -I % rm -f % - mv ./plugins ./tfproviders/ + if [ -d ./plugins ]; then + mv ./plugins ./tfproviders/ + else + mkdir "tfproviders" + fi fi diff --git a/build/gcp/TF_VERSION b/build/gcp/TF_VERSION index f98d9c0d..5daaa7ba 100644 --- a/build/gcp/TF_VERSION +++ b/build/gcp/TF_VERSION @@ -1 +1 @@ -0.12.31 +0.13.7 diff --git a/build/gcp/terraform-bundle.hcl b/build/gcp/terraform-bundle.hcl index 92098681..6a5e914a 100644 --- a/build/gcp/terraform-bundle.hcl +++ b/build/gcp/terraform-bundle.hcl @@ -7,8 +7,16 @@ terraform { } providers { - google = ["3.62.0"] - google-beta = ["3.62.0"] - template = ["2.1.2"] - null = ["2.1.2"] + google = { + versions = ["3.62.0"] + } + google-beta = { + versions = ["3.62.0"] + } + template = { + versions = ["2.1.2"] + } + null = { + versions = ["2.1.2"] + } } diff --git a/build/openstack/TF_VERSION b/build/openstack/TF_VERSION index f98d9c0d..5daaa7ba 100644 --- a/build/openstack/TF_VERSION +++ b/build/openstack/TF_VERSION @@ -1 +1 @@ -0.12.31 +0.13.7 diff --git a/build/openstack/terraform-bundle.hcl b/build/openstack/terraform-bundle.hcl index f8c9ad59..a90f6cee 100644 --- a/build/openstack/terraform-bundle.hcl +++ b/build/openstack/terraform-bundle.hcl @@ -7,7 +7,14 @@ terraform { } providers { - openstack = ["1.37.0"] - template = ["2.1.2"] - null = ["2.1.2"] + openstack = { + versions = ["1.37.0"] + source = "terraform-provider-openstack/openstack" + } + template = { + versions = ["2.1.2"] + } + null = { + versions = ["2.1.2"] + } } diff --git a/build/slim/TF_VERSION b/build/slim/TF_VERSION index f98d9c0d..5daaa7ba 100644 --- a/build/slim/TF_VERSION +++ b/build/slim/TF_VERSION @@ -1 +1 @@ -0.12.31 +0.13.7 diff --git a/hack/get-version.sh b/hack/get-version.sh index f22ed3e6..78cf212d 100755 --- a/hack/get-version.sh +++ b/hack/get-version.sh @@ -32,7 +32,7 @@ fi # as a dirty work tree. # Additionally, it filters out changes to the `VERSION` file, as this is currently the only way to inject the # version-to-build in our pipelines (see https://github.com/gardener/cc-utils/issues/431). -TREE_STATE="$([ -z "$(git status --porcelain 2>/dev/null | grep -vf <(git ls-files --deleted --ignored --exclude-from=.dockerignore) -e 'VERSION')" ] && echo clean || echo dirty)" +TREE_STATE="$([ -z "$(git status --porcelain 2>/dev/null | grep -vf <(git ls-files -o --deleted --ignored --exclude-from=.dockerignore) -e 'VERSION')" ] && echo clean || echo dirty)" if [ "$TREE_STATE" = dirty ] ; then VERSION="$VERSION-dirty" diff --git a/pkg/terraformer/terraformer.go b/pkg/terraformer/terraformer.go index 3098ed52..899b4e0d 100644 --- a/pkg/terraformer/terraformer.go +++ b/pkg/terraformer/terraformer.go @@ -7,6 +7,7 @@ package terraformer import ( "bytes" "context" + "encoding/json" "fmt" "io" "os" @@ -34,6 +35,13 @@ import ( const ( // maxPatchRetries define the maximum number of attempts to patch a resource in case of conflict maxPatchRetries = 2 + + // terraformVersionKey is the terraform version key in the terraform state JSON + terraformVersionKey = "terraform_version" + // previousVersionPrefix is the previous version from which upgrades are supported + previousVersionPrefix = "0.12." + // registry is the terraform registry + registry = "registry.terraform.io" ) var ( @@ -50,6 +58,22 @@ var ( SignalNotify = signal.Notify ) +var ( + // providers is a map from the short provider names (used with terraform 0.12 or lower) + // to their longer names (used with terraform 0.13 or higher) + providers = map[string]string{ + "aws": "hashicorp/aws", + "azurerm": "hashicorp/azurerm", + "google": "hashicorp/google", + "google-beta": "hashicorp/google-beta", + "openstack": "terraform-provider-openstack/openstack", + "alicloud": "hashicorp/alicloud", + "packet": "equinix/metal", + "template": "hashicorp/template", + "null": "hashicorp/null", + } +) + // NewDefaultTerraformer creates a new Terraformer with the default PathSet and logger. func NewDefaultTerraformer(config *Config) (*Terraformer, error) { return NewTerraformer(config, runtimelog.Log, DefaultPaths().WithBaseDir(config.BaseDir), clock.RealClock{}) @@ -164,6 +188,20 @@ func (t *Terraformer) execute(command Command) (rErr error) { return fmt.Errorf("error executing terraform %s: %w", Init, err) } + // get terraform version from state and execute state replace-provider commands if needed + terraformVersion, err := t.getTerraformVersionFromState(ctx) + if err != nil { + return fmt.Errorf("error getting terraform version from state: %w", err) + } + if strings.HasPrefix(terraformVersion, previousVersionPrefix) { + for key, value := range providers { + fromProvider, toProvider := fmt.Sprintf("%s/-/%s", registry, key), fmt.Sprintf("%s/%s", registry, value) + if err := t.executeTerraform(ctx, StateReplaceProvider, fromProvider, toProvider); err != nil { + return fmt.Errorf("error executing terraform %s: %w", StateReplaceProvider, err) + } + } + } + // execute main terraform command if err := t.executeTerraform(ctx, command); err != nil { return fmt.Errorf("error executing terraform %s: %w", command, err) @@ -185,7 +223,7 @@ func (t *Terraformer) execute(command Command) (rErr error) { return nil } -func (t *Terraformer) executeTerraform(ctx context.Context, command Command) error { +func (t *Terraformer) executeTerraform(ctx context.Context, command Command, params ...string) error { log := t.stepLogger("executeTerraform") // open termination log file already to ensure we can write to it. If we can't write to it, we should exit early @@ -196,22 +234,32 @@ func (t *Terraformer) executeTerraform(ctx context.Context, command Command) err } defer terminationLogFile.Close() + args := []string{string(command)} + if command == StateReplaceProvider { + args = append(args, "replace-provider") + } + // disable colors, which will look weird in termination message, k8s status fields and so on - args := []string{string(command), "-no-color"} + args = append(args, "-no-color") switch command { case Init: args = append(args, "-plugin-dir="+t.paths.ProvidersDir) + args = append(args, t.paths.ConfigDir) case Plan: args = append(args, "-var-file="+t.paths.VarsPath, "-parallelism=4", "-detailed-exitcode", "-state="+t.paths.StatePath) + args = append(args, t.paths.ConfigDir) case Apply: args = append(args, "-var-file="+t.paths.VarsPath, "-parallelism=4", "-auto-approve", "-state="+t.paths.StatePath) + args = append(args, t.paths.ConfigDir) case Destroy: args = append(args, "-var-file="+t.paths.VarsPath, "-parallelism=4", "-auto-approve", "-state="+t.paths.StatePath) + args = append(args, t.paths.ConfigDir) + case StateReplaceProvider: + args = append(args, "-auto-approve", "-state="+t.paths.StatePath) + args = append(args, params...) } - args = append(args, t.paths.ConfigDir) - log.Info("executing terraform", "command", command, "args", strings.Join(args[1:], " ")) tfCmd := exec.Command(TerraformBinary, args...) @@ -293,6 +341,31 @@ func (t *Terraformer) isStateEmpty(ctx context.Context) (bool, error) { return !ok || len(data) == 0, nil } +func (t *Terraformer) getTerraformVersionFromState(ctx context.Context) (string, error) { + state := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: t.config.StateConfigMapName, + Namespace: t.config.Namespace, + }, + } + if err := t.client.Get(ctx, client.ObjectKeyFromObject(state), state); client.IgnoreNotFound(err) != nil { + return "", err + } + data, ok := state.Data[tfStateKey] + if !ok || len(data) == 0 { + return "", nil + } + var terraformState map[string]interface{} + if err := json.Unmarshal([]byte(data), &terraformState); err != nil { + return "", fmt.Errorf("could not unmarshal terraform state from JSON: %w", err) + } + terraformVersion, ok := terraformState[terraformVersionKey].(string) + if !ok { + return "", fmt.Errorf("terraform state key %s is not of type string", terraformVersionKey) + } + return terraformVersion, nil +} + func (t *Terraformer) terraformObjects() []client.Object { return []client.Object{ &corev1.Secret{ diff --git a/pkg/terraformer/types.go b/pkg/terraformer/types.go index 407b2196..689532cb 100644 --- a/pkg/terraformer/types.go +++ b/pkg/terraformer/types.go @@ -29,6 +29,8 @@ const ( Validate Command = "validate" // Plan is the terraform `plan` command. Plan Command = "plan" + // StateReplaceProvider is the terraform `state` command with the `replace-provider` subcommand. + StateReplaceProvider Command = "state" // TerraformerFinalizer is the finalizer used by the terraformer on the terraform configmaps and secrets TerraformerFinalizer = "gardener.cloud/terraformer" ) diff --git a/test/utils/objects.go b/test/utils/objects.go index 93087824..9b9bafa9 100644 --- a/test/utils/objects.go +++ b/test/utils/objects.go @@ -70,7 +70,7 @@ func PrepareTestObjects(ctx context.Context, c client.Client, namespacePrefix st o.StateConfigMap = &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Name: "tf-state", Namespace: o.Namespace}, Data: map[string]string{ - StateKey: `some state`, + StateKey: `{"terraform_version":"0.13.7"}`, }, } err = o.client.Create(ctx, o.StateConfigMap)