From 7dbdcb144b42d9eaae23296247605261a1850e07 Mon Sep 17 00:00:00 2001 From: Jieyu Tian Date: Thu, 26 Sep 2024 14:27:17 +0000 Subject: [PATCH] Move the outputs type updating logic to tfconfig.go and add test cases --- cli/bpmetadata/cmd.go | 57 +--------- cli/bpmetadata/tfconfig.go | 64 +++++++++++ cli/bpmetadata/tfconfig_test.go | 101 +++++++++++++++++- .../interfaces_without_types_metadata.yaml | 14 +++ .../tf/sample-module/terraform.tfstate | 26 +++++ 5 files changed, 207 insertions(+), 55 deletions(-) create mode 100644 cli/testdata/bpmetadata/metadata/interfaces_without_types_metadata.yaml create mode 100644 cli/testdata/bpmetadata/tf/sample-module/terraform.tfstate diff --git a/cli/bpmetadata/cmd.go b/cli/bpmetadata/cmd.go index c2e3eafdf45..a288a03f264 100644 --- a/cli/bpmetadata/cmd.go +++ b/cli/bpmetadata/cmd.go @@ -2,24 +2,17 @@ package bpmetadata import ( "errors" - "flag" "fmt" "os" "path" - "path/filepath" "strings" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/cli/bpmetadata/parser" "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/cli/util" - "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" "github.com/itchyny/json2yaml" - testingiface "github.com/mitchellh/go-testing-interface" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/stretchr/testify/assert" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" - structpb "google.golang.org/protobuf/types/known/structpb" "sigs.k8s.io/yaml" ) @@ -157,18 +150,11 @@ func generateMetadataForBpPath(bpPath string) error { return fmt.Errorf("error creating metadata for blueprint at path: %s. Details: %w", bpPath, err) } - // If the flag is set, extract output types + // If the flag is set, update output types if mdFlags.genOutputType { - outputTypes, err := extractOutputTypesFromState(bpPath) + err = updateOutputTypes(bpPath, bpMetaObj.Spec.Interfaces) if err != nil { - return fmt.Errorf("error extracting output types for blueprint at path: %s. Details: %w", bpPath, err) - } - - // Update the bpMetadata with the extracted output types - for i, output := range bpMetaObj.Spec.Interfaces.Outputs { - if outputType, ok := outputTypes[output.Name]; ok { - bpMetaObj.Spec.Interfaces.Outputs[i].Type = outputType - } + return fmt.Errorf("error updating output types: %w", err) } } @@ -204,43 +190,6 @@ func generateMetadataForBpPath(bpPath string) error { return nil } -// Extract output types from terraform.tfstate -func extractOutputTypesFromState(bpPath string) (map[string]*structpb.Value, error) { - outputTypes := make(map[string]*structpb.Value) - - // Construct the path to the test/setup directory - tfDir := filepath.Join(bpPath) - - // testing.T chekcs verbose flag to determine it's mode. Add this line as a flags initializer - // so the program doesn't panic. - flag.Parse() - runtimeT := testingiface.RuntimeT{} - - root := tft.NewTFBlueprintTest( - &runtimeT, - tft.WithTFDir(tfDir), // Setup test at the blueprint path - ) - - root.DefineVerify(func(assert *assert.Assertions) { - stateFilePath := path.Join(bpPath, "terraform.tfstate") - stateData, err := os.ReadFile(stateFilePath) - if err != nil { - assert.FailNowf("Failed to read terraform.tfstate", "Error reading state file: %v", err) - } - - // Parse the state file and extract output types - parsedOutputTypes, err := parser.ParseOutputTypesFromState(stateData) - if err != nil { - assert.FailNowf("Failed to parse output types", "Error parsing output types: %v", err) - } - outputTypes = parsedOutputTypes - }) - - root.Test() // This will run terraform init and apply, then the DefineVerify function, and finally destroy - - return outputTypes, nil -} - func CreateBlueprintMetadata(bpPath string, bpMetadataObj *BlueprintMetadata) (*BlueprintMetadata, error) { // Verify that readme is present. readmeContent, err := os.ReadFile(path.Join(bpPath, readmeFileName)) diff --git a/cli/bpmetadata/tfconfig.go b/cli/bpmetadata/tfconfig.go index bc5679ed444..647ca57b139 100644 --- a/cli/bpmetadata/tfconfig.go +++ b/cli/bpmetadata/tfconfig.go @@ -1,17 +1,23 @@ package bpmetadata import ( + "flag" "fmt" "os" + "path" "path/filepath" "regexp" "sort" "strings" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/cli/bpmetadata/parser" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" hcl "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclparse" "github.com/hashicorp/terraform-config-inspect/tfconfig" + testingiface "github.com/mitchellh/go-testing-interface" + "github.com/stretchr/testify/assert" "google.golang.org/protobuf/types/known/structpb" ) @@ -79,6 +85,9 @@ var moduleSchema = &hcl.BodySchema{ }, } +// Create alias for generateTFStateFile so we can mock it in unit test. +var tfStateFile = generateTFStateFile + // getBlueprintVersion gets both the required core version and the // version of the blueprint func getBlueprintVersion(configPath string) (*blueprintVersion, error) { @@ -513,3 +522,58 @@ func mergeExistingConnections(newInterfaces, existingInterfaces *BlueprintInterf } } } + +// UpdateOutputTypes generates the terraform.tfstate file, extracts output types from it, +// 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) + if err != nil { + return fmt.Errorf("error generating terraform.tfstate file: %w", err) + } + + // Parse the state file and extract output types + outputTypes, err := parser.ParseOutputTypesFromState(stateData) + if err != nil { + return fmt.Errorf("error parsing output types: %w", err) + } + + // Update the output types in the BlueprintInterface + for i, output := range bpInterfaces.Outputs { + if outputType, ok := outputTypes[output.Name]; ok { + bpInterfaces.Outputs[i].Type = outputType + } + } + return nil +} + +// generateTFStateFile generates the terraform.tfstate file by running terraform init and apply. +func generateTFStateFile(bpPath string) ([]byte, error) { + var stateData []byte + // Construct the path to the test/setup directory + tfDir := filepath.Join(bpPath) + + // testing.T checks verbose flag to determine its mode. Add this line as a flags initializer + // so the program doesn't panic + flag.Parse() + runtimeT := testingiface.RuntimeT{} + + root := tft.NewTFBlueprintTest( + &runtimeT, + tft.WithTFDir(tfDir), // Setup test at the blueprint path, + ) + + root.DefineVerify(func(assert *assert.Assertions) { + stateFilePath := path.Join(bpPath, "terraform.tfstate") + stateDataFromFile, err := os.ReadFile(stateFilePath) + if err != nil { + assert.FailNowf("Failed to read terraform.tfstate", "Error reading state file: %v", err) + } + + stateData = stateDataFromFile + }) + + root.Test() // This will run terraform init and apply, and then destroy + + return stateData, nil +} diff --git a/cli/bpmetadata/tfconfig_test.go b/cli/bpmetadata/tfconfig_test.go index 8752762c6c0..5c1c7bd0ebd 100644 --- a/cli/bpmetadata/tfconfig_test.go +++ b/cli/bpmetadata/tfconfig_test.go @@ -1,13 +1,16 @@ package bpmetadata import ( + "fmt" + "os" "path" + "slices" "testing" "github.com/hashicorp/hcl/v2/hclparse" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" + "google.golang.org/protobuf/types/known/structpb" ) const ( @@ -413,3 +416,99 @@ func TestTFVariableSortOrder(t *testing.T) { }) } } + +func TestUpdateOutputTypes(t *testing.T) { + tests := []struct { + name string + bpPath string + interfacesFile string + stateFile string + expectedOutputs []*BlueprintOutput + expectError bool + }{ + { + name: "Update output types from state", + bpPath: "sample-module", + interfacesFile: "interfaces_without_types_metadata.yaml", + stateFile: "terraform.tfstate", + expectedOutputs: []*BlueprintOutput{ + { + Name: "cluster_id", + Description: "Cluster ID", + Type: structpb.NewStringValue("string"), + }, + { + Name: "endpoint", + Description: "Cluster endpoint", + Type: &structpb.Value{ + Kind: &structpb.Value_ListValue{ + ListValue: &structpb.ListValue{ + Values: []*structpb.Value{ + { + Kind: &structpb.Value_StringValue{ + StringValue: "object", + }, + }, + { + Kind: &structpb.Value_StructValue{ + StructValue: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "host": { + Kind: &structpb.Value_StringValue{ + StringValue: "string", + }, + }, + "port": { + Kind: &structpb.Value_StringValue{ + StringValue: "number", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Load interfaces from file + bpInterfaces, err := UnmarshalMetadata(metadataTestdataPath, tt.interfacesFile) + require.NoError(t, err) + + // Override with a function that reads a hard-coded tfstate file. + tfStateFile = func(_ string) ([]byte, error) { + if tt.expectError { + return nil, fmt.Errorf("simulated error generating state file") + } + // Copy the test state file to the bpPath + stateFilePath := path.Join(tfTestdataPath, tt.bpPath, tt.stateFile) + stateData, err := os.ReadFile(stateFilePath) + if err != nil { + return nil, fmt.Errorf("error reading state file: %w", err) + } + return stateData, nil + } + + // Update output types + err = updateOutputTypes(path.Join(tfTestdataPath, tt.bpPath), bpInterfaces.Spec.Interfaces) + + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + // Assert that the output types are updated correctly + expectedOutputsStr := fmt.Sprintf("%v", tt.expectedOutputs) + actualOutputsStr := fmt.Sprintf("%v", bpInterfaces.Spec.Interfaces.Outputs) + assert.Equal(t, expectedOutputsStr, actualOutputsStr) + } + }) + } +} diff --git a/cli/testdata/bpmetadata/metadata/interfaces_without_types_metadata.yaml b/cli/testdata/bpmetadata/metadata/interfaces_without_types_metadata.yaml new file mode 100644 index 00000000000..50320f98b65 --- /dev/null +++ b/cli/testdata/bpmetadata/metadata/interfaces_without_types_metadata.yaml @@ -0,0 +1,14 @@ +# interfaces_without_types_metadata.yaml +apiVersion: blueprints.cloud.google.com/v1alpha1 +kind: BlueprintMetadata +metadata: + name: sample-module + annotations: + config.kubernetes.io/local-config: "true" +spec: + interfaces: + outputs: + - name: cluster_id + description: Cluster ID + - name: endpoint + description: Cluster endpoint \ No newline at end of file diff --git a/cli/testdata/bpmetadata/tf/sample-module/terraform.tfstate b/cli/testdata/bpmetadata/tf/sample-module/terraform.tfstate new file mode 100644 index 00000000000..49bf5e7d921 --- /dev/null +++ b/cli/testdata/bpmetadata/tf/sample-module/terraform.tfstate @@ -0,0 +1,26 @@ +{ + "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 + }, + "type": [ + "object", + { + "host": "string", + "port": "number" + } + ] + } + }, + "resources": [] +} \ No newline at end of file