diff --git a/.changelog/2173.txt b/.changelog/2173.txt new file mode 100644 index 0000000000..5a8b7facc0 --- /dev/null +++ b/.changelog/2173.txt @@ -0,0 +1,3 @@ +```release-note:bug +`resource/kubernetes_manifest`: fix an issue with the `kubernetes_manifest` resource, where an object fails to update correctly when employing wait conditions and thus some attributes are not available for the reference after creation. +``` diff --git a/kubernetes/test-infra/gke/main.tf b/kubernetes/test-infra/gke/main.tf index 0bac22c786..69f5d0fda5 100644 --- a/kubernetes/test-infra/gke/main.tf +++ b/kubernetes/test-infra/gke/main.tf @@ -17,6 +17,10 @@ variable "enable_alpha" { default = false } +variable "cluster_name" { + default = "" +} + data "google_compute_zones" "available" { } @@ -36,7 +40,7 @@ resource "google_service_account" "default" { resource "google_container_cluster" "primary" { provider = google-beta - name = "tf-acc-test-${random_id.cluster_name.hex}" + name = var.cluster_name != "" ? var.cluster_name : "tf-acc-test-${random_id.cluster_name.hex}" location = data.google_compute_zones.available.names[0] node_version = data.google_container_engine_versions.supported.latest_node_version min_master_version = data.google_container_engine_versions.supported.latest_master_version diff --git a/manifest/provider/apply.go b/manifest/provider/apply.go index e432fc02f5..68495d8949 100644 --- a/manifest/provider/apply.go +++ b/manifest/provider/apply.go @@ -404,18 +404,6 @@ func (s *RawProviderServer) ApplyResourceChange(ctx context.Context, req *tfprot return resp, nil } - newResObject, err := payload.ToTFValue(RemoveServerSideFields(result.Object), tsch, th, tftypes.NewAttributePath()) - if err != nil { - resp.Diagnostics = append(resp.Diagnostics, - &tfprotov5.Diagnostic{ - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "Conversion from Unstructured to tftypes.Value failed", - Detail: err.Error(), - }) - return resp, nil - } - s.logger.Trace("[ApplyResourceChange][Apply]", "[payload.ToTFValue]", dump(newResObject)) - wt, _, err := s.TFTypeFromOpenAPI(ctx, gvk, true) if err != nil { return resp, fmt.Errorf("failed to determine resource type ID: %s", err) @@ -454,8 +442,34 @@ func (s *RawProviderServer) ApplyResourceChange(ctx context.Context, req *tfprot } return resp, nil } + + r, err := rs.Get(ctxDeadline, rname, metav1.GetOptions{}) + if err != nil { + s.logger.Error("[ApplyResourceChange][ReadAfterWait]", "API error", dump(err), "API response", dump(result)) + resp.Diagnostics = append(resp.Diagnostics, + &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: fmt.Sprintf(`Failed to read resource %q after wait conditions`, rname), + Detail: err.Error(), + }) + + return resp, nil + } + result = r } + newResObject, err := payload.ToTFValue(RemoveServerSideFields(result.Object), tsch, th, tftypes.NewAttributePath()) + if err != nil { + resp.Diagnostics = append(resp.Diagnostics, + &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Conversion from Unstructured to tftypes.Value failed", + Detail: err.Error(), + }) + return resp, nil + } + s.logger.Trace("[ApplyResourceChange][Apply]", "[payload.ToTFValue]", dump(newResObject)) + compObj, err := morph.DeepUnknown(tsch, newResObject, tftypes.NewAttributePath()) if err != nil { return resp, err diff --git a/manifest/test/acceptance/testdata/Wait/wait_for_fields_annotations.tf b/manifest/test/acceptance/testdata/Wait/wait_for_fields_annotations.tf new file mode 100644 index 0000000000..83a09d5771 --- /dev/null +++ b/manifest/test/acceptance/testdata/Wait/wait_for_fields_annotations.tf @@ -0,0 +1,31 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "kubernetes_manifest" "test" { + manifest = { + apiVersion = "v1" + kind = "Secret" + metadata = { + name = var.name + namespace = var.namespace + + annotations = { + "kubernetes.io/service-account.name" = "default" + } + } + type = "kubernetes.io/service-account-token" + } + wait { + fields = { + "metadata.annotations[\"kubernetes.io/service-account.uid\"]" = "^.*$", + } + } + + timeouts { + create = "10s" + } +} + +output "test" { + value = kubernetes_manifest.test.object.metadata.annotations["kubernetes.io/service-account.uid"] +} diff --git a/manifest/test/acceptance/wait_test.go b/manifest/test/acceptance/wait_test.go index 92a5cde63f..7c652aefdb 100644 --- a/manifest/test/acceptance/wait_test.go +++ b/manifest/test/acceptance/wait_test.go @@ -223,3 +223,51 @@ func TestKubernetesManifest_Wait_InvalidCondition(t *testing.T) { t.Fatalf("Waiter should have timed out") } } + +func TestKubernetesManifest_WaitFields_Annotations(t *testing.T) { + ctx := context.Background() + + name := randName() + namespace := randName() + + reattachInfo, err := provider.ServeTest(ctx, hclog.Default(), t) + if err != nil { + t.Errorf("Failed to create provider instance: %q", err) + } + + tf := tfhelper.RequireNewWorkingDir(ctx, t) + tf.SetReattachInfo(ctx, reattachInfo) + defer func() { + tf.Destroy(ctx) + tf.Close() + k8shelper.AssertNamespacedResourceDoesNotExist(t, "v1", "secrets", namespace, name) + }() + + k8shelper.CreateNamespace(t, namespace) + defer k8shelper.DeleteResource(t, namespace, kubernetes.NewGroupVersionResource("v1", "namespaces")) + + tfvars := TFVARS{ + "namespace": namespace, + "name": name, + } + tfconfig := loadTerraformConfig(t, "Wait/wait_for_fields_annotations.tf", tfvars) + tf.SetConfig(ctx, tfconfig) + tf.Init(ctx) + + tf.Apply(ctx) + + k8shelper.AssertNamespacedResourceExists(t, "v1", "secrets", namespace, name) + + st, err := tf.State(ctx) + if err != nil { + t.Fatalf("Failed to obtain state: %q", err) + } + tfstate := tfstatehelper.NewHelper(st) + tfstate.AssertAttributeValues(t, tfstatehelper.AttributeValues{ + "kubernetes_manifest.test.wait.0.fields": map[string]interface{}{ + "metadata.annotations[\"kubernetes.io/service-account.uid\"]": "^.*$", + }, + }) + + tfstate.AssertOutputExists(t, "test") +} diff --git a/manifest/test/helper/state/state_helper.go b/manifest/test/helper/state/state_helper.go index 7737ebc4c2..2d38d1c64d 100644 --- a/manifest/test/helper/state/state_helper.go +++ b/manifest/test/helper/state/state_helper.go @@ -37,6 +37,17 @@ func getAttributesValuesFromResource(state *Helper, address string) (interface{} return nil, fmt.Errorf("Could not find resource %q in state", address) } +// getOutputValues gets the given output name value from the state +func getOutputValues(state *Helper, name string) (interface{}, error) { + for n, v := range state.Values.Outputs { + if n == name { + return v.Value, nil + } + } + + return nil, fmt.Errorf("Could not find output %q in state", name) +} + var errFieldNotFound = fmt.Errorf("Field not found") // findAttributeValue will return the value of the attribute at the given address in a tree of arrays and maps @@ -107,6 +118,18 @@ func (s *Helper) GetAttributeValue(t *testing.T, address string) interface{} { return value } +// GetOutputValue gets the given output name value from the state +func (s *Helper) GetOutputValue(t *testing.T, name string) interface{} { + t.Helper() + + value, err := getOutputValues(s, name) + if err != nil { + t.Fatal(err) + } + + return value +} + // AttributeValues is a convenience type for supplying maps of attributes and values // to AssertAttributeValues type AttributeValues map[string]interface{} @@ -207,3 +230,10 @@ func (s *Helper) AssertAttributeFalse(t *testing.T, address string) { assert.False(t, v, fmt.Sprintf("Address: %q", address)) } } + +// AssertOutputExists will fail the test if the output does not exist +func (s *Helper) AssertOutputExists(t *testing.T, name string) { + t.Helper() + + s.GetOutputValue(t, name) +}