From b867723ff6a4304acb4fe49a05cd70522ba9a1b9 Mon Sep 17 00:00:00 2001 From: Jieyu Tian Date: Tue, 1 Oct 2024 13:59:22 +0000 Subject: [PATCH] Use No state. to get well-structured state instead of reading directly from .tfstate file. --- cli/bpmetadata/parser/state_parser.go | 30 +--- cli/bpmetadata/parser/state_parser_test.go | 155 +++++++++--------- cli/bpmetadata/tfconfig.go | 17 +- cli/bpmetadata/tfconfig_test.go | 2 +- .../tf/sample-module/terraform.tfstate | 41 +++-- 5 files changed, 107 insertions(+), 138 deletions(-) diff --git a/cli/bpmetadata/parser/state_parser.go b/cli/bpmetadata/parser/state_parser.go index 33314042e98..42d4ad76565 100644 --- a/cli/bpmetadata/parser/state_parser.go +++ b/cli/bpmetadata/parser/state_parser.go @@ -12,37 +12,11 @@ import ( ) func ParseOutputTypesFromState(stateData []byte) (map[string]*structpb.Value, error) { - // Unmarshal the state data into a map[string]interface{} first - var rawState map[string]interface{} - err := json.Unmarshal(stateData, &rawState) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal state data: %w", err) - } - - // Check if "format_version" key exists - if _, ok := rawState["format_version"]; !ok { - // If not present, add it with a default value - rawState["format_version"] = "1.0" - } - - // Create a "values" field with "outputs" inside - rawState["values"] = map[string]interface{}{ - "outputs": rawState["outputs"], - } - - // Remove the top-level "outputs" field - delete(rawState, "outputs") - - // Now marshal the updated map back to JSON - updatedStateData, err := json.MarshalIndent(rawState, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal updated state data: %w", err) - } var state tfjson.State - // Unmarshal the updated JSON into tfjson.State - err = json.Unmarshal(updatedStateData, &state) + // Unmarshal the state data into tfjson.State + err := json.Unmarshal(stateData, &state) if err != nil { return nil, fmt.Errorf("failed to unmarshal state data: %w", err) } diff --git a/cli/bpmetadata/parser/state_parser_test.go b/cli/bpmetadata/parser/state_parser_test.go index c52efbd8465..a97417509bc 100644 --- a/cli/bpmetadata/parser/state_parser_test.go +++ b/cli/bpmetadata/parser/state_parser_test.go @@ -11,25 +11,24 @@ func TestParseOutputTypesFromState_WithSimpleTypes(t *testing.T) { t.Parallel() stateData := []byte(` { - "version": 4, - "terraform_version": "0.12.0", - "serial": 8, - "lineage": "491fd8f4-81b5-9890-520c-8a173c36e483", - "outputs": { - "boolean_output": { - "value": true, - "type": "bool" - }, - "number_output": { - "value": 42, - "type": "number" - }, - "string_output": { - "value": "foo", - "type": "string" + "format_version": "1.0", + "terraform_version": "1.2.0", + "values": { + "outputs": { + "boolean_output": { + "type": "bool", + "value": true + }, + "number_output": { + "type": "number", + "value": 42 + }, + "string_output": { + "type": "string", + "value": "foo" + } } - }, - "resources": [] + } } `) want := map[string]*structpb.Value{ @@ -51,63 +50,62 @@ func TestParseOutputTypesFromState_WithComplexTypes(t *testing.T) { t.Parallel() stateData := []byte(` { - "version": 4, - "terraform_version": "0.12.0", - "serial": 8, - "lineage": "491fd8f4-81b5-9890-520c-8a173c36e483", - "outputs": { - "interpolated_deep": { - "value": { - "foo": "bar", - "map": { - "bar": "baz", - "id": "424881806176056736" - }, - "number": 42 - }, - "type": [ - "object", - { - "foo": "string", - "map": [ - "object", - { - "bar": "string", - "id": "string" - } - ], - "number": "number" + "format_version": "1.0", + "terraform_version": "1.2.0", + "values": { + "outputs": { + "interpolated_deep": { + "type": [ + "object", + { + "foo": "string", + "map": [ + "object", + { + "bar": "string", + "id": "string" + } + ], + "number": "number" + } + ], + "value": { + "foo": "bar", + "map": { + "bar": "baz", + "id": "424881806176056736" + }, + "number": 42 } - ] - }, - "list_output": { - "value": [ - "foo", - "bar" - ], - "type": [ - "tuple", - [ - "string", - "string" + }, + "list_output": { + "type": [ + "tuple", + [ + "string", + "string" + ] + ], + "value": [ + "foo", + "bar" ] - ] - }, - "map_output": { - "value": { - "foo": "bar", - "number": 42 }, - "type": [ - "object", - { - "foo": "string", - "number": "number" + "map_output": { + "type": [ + "object", + { + "foo": "string", + "number": "number" + } + ], + "value": { + "foo": "bar", + "number": 42 } - ] + } } - }, - "resources": [] + } } `) want := map[string]*structpb.Value{ @@ -145,16 +143,15 @@ func TestParseOutputTypesFromState_WithoutTypes(t *testing.T) { t.Parallel() stateData := []byte(` { - "version": 4, - "terraform_version": "0.12.0", - "serial": 8, - "lineage": "491fd8f4-81b5-9890-520c-8a173c36e483", - "outputs": { - "no_type_output": { - "value": "some_value" + "format_version": "1.0", + "terraform_version": "1.2.0", + "values": { + "outputs": { + "no_type_output": { + "value": "some_value" + } } - }, - "resources": [] + } } `) want := map[string]*structpb.Value{ diff --git a/cli/bpmetadata/tfconfig.go b/cli/bpmetadata/tfconfig.go index e9ea2d79a1f..819feda47cd 100644 --- a/cli/bpmetadata/tfconfig.go +++ b/cli/bpmetadata/tfconfig.go @@ -4,7 +4,6 @@ import ( "flag" "fmt" "os" - "path" "path/filepath" "regexp" "sort" @@ -12,6 +11,7 @@ import ( "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/cli/bpmetadata/parser" "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/gruntwork-io/terratest/modules/terraform" hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclparse" @@ -86,7 +86,7 @@ var moduleSchema = &hcl.BodySchema{ } // Create alias for generateTFStateFile so we can mock it in unit test. -var tfStateFile = generateTFStateFile +var tfState = generateTFState // getBlueprintVersion gets both the required core version and the // version of the blueprint @@ -526,7 +526,7 @@ func mergeExistingConnections(newInterfaces, existingInterfaces *BlueprintInterf // and updates the output types in the provided BlueprintInterface. func updateOutputTypes(bpPath string, bpInterfaces *BlueprintInterface) error { // Generate the terraform.tfstate file - stateData, err := tfStateFile(bpPath) + stateData, err := tfState(bpPath) if err != nil { return fmt.Errorf("error generating terraform.tfstate file: %w", err) } @@ -546,8 +546,8 @@ func updateOutputTypes(bpPath string, bpInterfaces *BlueprintInterface) error { return nil } -// generateTFStateFile generates the terraform.tfstate file by running terraform init and apply. -func generateTFStateFile(bpPath string) ([]byte, error) { +// generateTFState generates the terraform.tfstate by running terraform init and apply, and terraform show to capture the state. +func generateTFState(bpPath string) ([]byte, error) { var stateData []byte // Construct the path to the test/setup directory tfDir := filepath.Join(bpPath) @@ -563,13 +563,12 @@ func generateTFStateFile(bpPath string) ([]byte, error) { ) root.DefineVerify(func(assert *assert.Assertions) { - stateFilePath := path.Join(bpPath, "terraform.tfstate") - stateDataFromFile, err := os.ReadFile(stateFilePath) + stateStr, err := terraform.ShowE(&runtimeT, root.GetTFOptions()) if err != nil { - assert.FailNowf("Failed to read terraform.tfstate", "Error reading state file: %v", err) + assert.FailNowf("Failed to generate terraform.tfstate", "Error calling `terraform show`: %v", err) } - stateData = stateDataFromFile + stateData = []byte(stateStr) }) root.Test() // This will run terraform init and apply, and then destroy diff --git a/cli/bpmetadata/tfconfig_test.go b/cli/bpmetadata/tfconfig_test.go index 5c1c7bd0ebd..123d86a95d3 100644 --- a/cli/bpmetadata/tfconfig_test.go +++ b/cli/bpmetadata/tfconfig_test.go @@ -484,7 +484,7 @@ func TestUpdateOutputTypes(t *testing.T) { require.NoError(t, err) // Override with a function that reads a hard-coded tfstate file. - tfStateFile = func(_ string) ([]byte, error) { + tfState = func(_ string) ([]byte, error) { if tt.expectError { return nil, fmt.Errorf("simulated error generating state file") } diff --git a/cli/testdata/bpmetadata/tf/sample-module/terraform.tfstate b/cli/testdata/bpmetadata/tf/sample-module/terraform.tfstate index 49bf5e7d921..1d19d377e55 100644 --- a/cli/testdata/bpmetadata/tf/sample-module/terraform.tfstate +++ b/cli/testdata/bpmetadata/tf/sample-module/terraform.tfstate @@ -1,26 +1,25 @@ { - "version": 4, - "terraform_version": "0.12.0", - "serial": 8, - "lineage": "491fd8f4-81b5-9890-520c-8a173c36e483", - "outputs": { - "cluster_id": { - "value": "sample-cluster-id", - "type": "string" - }, - "endpoint": { - "value": { - "host": "127.0.0.1", - "port": 443 + "format_version": "1.0", + "terraform_version": "1.2.0", + "values": { + "outputs": { + "cluster_id": { + "type": "string", + "value": "sample-cluster-id" }, - "type": [ - "object", - { - "host": "string", - "port": "number" + "endpoint": { + "type": [ + "object", + { + "host": "string", + "port": "number" + } + ], + "value": { + "host": "127.0.0.1", + "port": 443 } - ] + } } - }, - "resources": [] + } } \ No newline at end of file