From c3547fbaa7e813e4f16882acea819ce8d3568696 Mon Sep 17 00:00:00 2001 From: ravilr Date: Mon, 7 Oct 2024 22:28:11 -0700 Subject: [PATCH] ignore drift in rds.Cluster and rds.Instance due to engine_version diff arising out of automanaged upgrades Signed-off-by: ravilr --- config/rds/config.go | 32 +++- config/rds/utils/engine_version.go | 89 ++++++++++++ config/rds/utils/engine_version_test.go | 186 ++++++++++++++++++++++++ 3 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 config/rds/utils/engine_version.go create mode 100644 config/rds/utils/engine_version_test.go diff --git a/config/rds/config.go b/config/rds/config.go index 03927b2094..be027ca373 100644 --- a/config/rds/config.go +++ b/config/rds/config.go @@ -8,15 +8,17 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/crossplane/upjet/pkg/config" "github.com/crossplane/upjet/pkg/types/comments" "github.com/upbound/provider-aws/config/common" + "github.com/upbound/provider-aws/config/rds/utils" ) // Configure adds configurations for the rds group. -func Configure(p *config.Provider) { +func Configure(p *config.Provider) { //nolint:gocyclo p.AddResourceConfigurator("aws_rds_cluster", func(r *config.Resource) { // Mutually exclusive with aws_rds_cluster_role_association config.MoveToStatus(r.TerraformResource, "iam_roles") @@ -80,6 +82,20 @@ func Configure(p *config.Provider) { "master DB user. If you set autoGeneratePassword to true, the Secret" + " referenced here will be created or updated with generated password" + " if it does not already contain one." + r.TerraformCustomDiff = func(diff *terraform.InstanceDiff, _ *terraform.InstanceState, _ *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { + if diff == nil || diff.Destroy { + return diff, nil + } + // Ignore the engine version diff, if the desired spec version is lower than the external's actual version. + // Downgrades are not allowed by AWS RDS. + if evDiff, ok := diff.Attributes["engine_version"]; ok && evDiff.Old != "" && evDiff.New != "" { + c := utils.CompareEngineVersions(evDiff.New, evDiff.Old) + if c <= 0 { + delete(diff.Attributes, "engine_version") + } + } + return diff, nil + } }) p.AddResourceConfigurator("aws_rds_cluster_instance", func(r *config.Resource) { @@ -167,6 +183,20 @@ func Configure(p *config.Provider) { " if it does not already contain one." r.MetaResource.ArgumentDocs["engine"] = "- (Required unless a `snapshotIdentifier` or `replicateSourceDb` is provided) The database engine to use. For supported values, see the Engine parameter in [API action CreateDBInstance](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBInstance.html). Note that for Amazon Aurora instances the engine must match the [DB Cluster](https://marketplace.upbound.io/providers/upbound/provider-aws/latest/resources/rds.aws.upbound.io/Cluster/v1beta1)'s engine'. For information on the difference between the available Aurora MySQL engines see Comparison in the [Amazon RDS Release Notes](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/Welcome.html)." r.MetaResource.ArgumentDocs["engine_version"] = "- (Optional) The engine version to use. If `autoMinorVersionUpgrade` is enabled, you can provide a prefix of the version such as 5.7 (for 5.7.10). The actual engine version used is returned in the attribute `status.atProvider.engineVersionActual`. For supported values, see the EngineVersion parameter in [API action CreateDBInstance](https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBInstance.html). Note that for Amazon Aurora instances the engine version must match the [DB Cluster](https://marketplace.upbound.io/providers/upbound/provider-aws/latest/resources/rds.aws.upbound.io/Cluster/v1beta1)'s engine version'." + r.TerraformCustomDiff = func(diff *terraform.InstanceDiff, _ *terraform.InstanceState, _ *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { + if diff == nil || diff.Destroy { + return diff, nil + } + // Ignore the engine version diff, if the desired spec version is lower than the external's actual version. + // Downgrades are not allowed by AWS RDS. + if evDiff, ok := diff.Attributes["engine_version"]; ok && evDiff.Old != "" && evDiff.New != "" { + c := utils.CompareEngineVersions(evDiff.New, evDiff.Old) + if c <= 0 { + delete(diff.Attributes, "engine_version") + } + } + return diff, nil + } }) p.AddResourceConfigurator("aws_db_proxy", func(r *config.Resource) { diff --git a/config/rds/utils/engine_version.go b/config/rds/utils/engine_version.go new file mode 100644 index 0000000000..57b27ec3d2 --- /dev/null +++ b/config/rds/utils/engine_version.go @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2024 The Crossplane Authors +// +// SPDX-License-Identifier: CC0-1.0 + +package utils + +import ( + "strconv" + "strings" +) + +// EngineVersion represents an AWS RDS engine version. +type EngineVersion []any + +// ParseEngineVersion from a raw string. +func ParseEngineVersion(raw string) EngineVersion { + split := strings.Split(raw, ".") + + v := make(EngineVersion, len(split)) + for i, s := range split { + d, err := strconv.Atoi(s) + if err != nil { + v[i] = s + } else { + v[i] = d + } + } + return v +} + +const ( + compareIsHigher = 1 + compareIsEqual = 0 + compareIsLower = -1 +) + +// Compare returns a positive value if v is represents a higher version number +// than other. A negative value is returned if other is higher than v. +// It returns 0 if both are considered equal. +func (v EngineVersion) Compare(other EngineVersion) int { + if other == nil { + return compareIsHigher + } + + for i := 0; i < len(v); i++ { + a := v.get(i) + b := other.get(i) + c := compareVersionComponents(a, b) + if c != 0 { + return c + } + } + return compareIsEqual +} + +func compareVersionComponents(a, b any) int { + if a == b { + return compareIsEqual + } + if b == nil { + return compareIsHigher + } + aI, aIsInt := a.(int) + bI, bIsInt := b.(int) + if aIsInt { + if bIsInt { + return aI - bI + } + return compareIsHigher + } + if bIsInt { + return compareIsLower + } + return compareIsEqual // We cannot decide if both are strings. +} + +func (v EngineVersion) get(i int) any { + if i >= 0 && i < len(v) { + return v[i] + } + return nil +} + +// CompareEngineVersions is a shortcut to compare two engine versions. +func CompareEngineVersions(a, b string) int { + av := ParseEngineVersion(a) + bv := ParseEngineVersion(b) + return av.Compare(bv) +} diff --git a/config/rds/utils/engine_version_test.go b/config/rds/utils/engine_version_test.go new file mode 100644 index 0000000000..707e30988d --- /dev/null +++ b/config/rds/utils/engine_version_test.go @@ -0,0 +1,186 @@ +// SPDX-FileCopyrightText: 2024 The Crossplane Authors +// +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestEngineVersionIsHigherOrEqual(t *testing.T) { + type args struct { + spec string + current string + } + + type want struct { + result int + } + + cases := map[string]struct { + args + want + }{ + "AuroraMySQLIsEqual": { + args: args{ + spec: "5", + current: "5.7.mysql_aurora.2.07.0", + }, + want: want{ + result: 0, + }, + }, + "AuroraMySQLIsEqual2": { + args: args{ + spec: "5.7", + current: "5.7.mysql_aurora.2.07.0", + }, + want: want{ + result: 0, + }, + }, + "AuroraMySQLIsHigher": { + args: args{ + spec: "8", + current: "5.7.mysql_aurora.2.07.0", + }, + want: want{ + result: 1, + }, + }, + "AuroraPostgresIsEqual": { + args: args{ + spec: "14", + current: "14.4", + }, + want: want{ + result: 0, + }, + }, + "AuroraPostgresIsLower": { + args: args{ + spec: "14", + current: "15.2", + }, + want: want{ + result: -1, + }, + }, + "MariaDBIsEqual": { + args: args{ + spec: "10", + current: "10.3.35", + }, + want: want{ + result: 0, + }, + }, + "MariaDBNotIsLower": { + args: args{ + spec: "10", + current: "11.0", + }, + want: want{ + result: -1, + }, + }, + "MySQLIsEqual": { + args: args{ + spec: "5.7", + current: "5.7.38", + }, + want: want{ + result: 0, + }, + }, + "MySQLIsHigher": { + args: args{ + spec: "8", + current: "5.7.38", + }, + want: want{ + result: 1, + }, + }, + "OracleIsEqual": { + args: args{ + spec: "19", + current: "19.0.0.0.ru-2023-01.rur-2023-01.r1", + }, + want: want{ + result: 0, + }, + }, + "OracleIsHigher": { + args: args{ + spec: "20", + current: "19.0.0.0.ru-2023-01.rur-2023-01.r1", + }, + want: want{ + result: 1, + }, + }, + "PostgresIsEqual": { + args: args{ + spec: "10", + current: "10.17", + }, + want: want{ + result: 0, + }, + }, + "PostgresIsHigher": { + args: args{ + spec: "14", + current: "10.17", + }, + want: want{ + result: 1, + }, + }, + "SQLServerIsEqual": { + args: args{ + spec: "12", + current: "12.00.6293.0.v1", + }, + want: want{ + result: 0, + }, + }, + "SQLServerIsHigher": { + args: args{ + spec: "14", + current: "12.00.6293.0.v1", + }, + want: want{ + result: 1, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + specV := ParseEngineVersion(tc.args.spec) + curV := ParseEngineVersion(tc.args.current) + + res := specV.Compare(curV) + resSign := sign(res) + if diff := cmp.Diff(tc.want.result, resSign); diff != "" { + t.Errorf("r: -want, +got:\n%q\n%q\n%s", tc.args.spec, tc.args.current, diff) + } + }) + } +} + +func sign(x int) int { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +}