From 205d900ccd8e906ac4e2a454efaf8afa43119f22 Mon Sep 17 00:00:00 2001 From: Jieyu Tian Date: Fri, 13 Sep 2024 17:44:18 +0000 Subject: [PATCH] Automatically generate type info for output in metadata file --- cli/bpmetadata/cmd.go | 78 +++++++++++++++++-- cli/bpmetadata/parser/state_parser.go | 49 ++++++++++++ infra/blueprint-test/pkg/tft/terraform.go | 31 ++++++++ infra/blueprint-test/pkg/utils/mock_t.go | 49 ++++++++++++ infra/blueprint-test/pkg/utils/mock_t_test.go | 56 +++++++++++++ 5 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 cli/bpmetadata/parser/state_parser.go create mode 100644 infra/blueprint-test/pkg/utils/mock_t.go create mode 100644 infra/blueprint-test/pkg/utils/mock_t_test.go diff --git a/cli/bpmetadata/cmd.go b/cli/bpmetadata/cmd.go index 5430a2bf3a3f..8076d0639be2 100644 --- a/cli/bpmetadata/cmd.go +++ b/cli/bpmetadata/cmd.go @@ -2,27 +2,35 @@ 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" ) var mdFlags struct { - path string - nested bool - force bool - display bool - validate bool - quiet bool + path string + nested bool + force bool + display bool + validate bool + quiet bool + genOutputType bool } const ( @@ -49,6 +57,7 @@ func init() { Cmd.Flags().BoolVar(&mdFlags.nested, "nested", true, "Flag for generating metadata for nested blueprint, if any.") Cmd.Flags().BoolVarP(&mdFlags.validate, "validate", "v", false, "Validate metadata against the schema definition.") Cmd.Flags().BoolVarP(&mdFlags.quiet, "quiet", "q", false, "Run in quiet mode suppressing all prompts.") + Cmd.Flags().BoolVarP(&mdFlags.genOutputType, "generate-output-type", "g", false, "Automatically generate type field for outputs.") } var Cmd = &cobra.Command{ @@ -148,6 +157,21 @@ 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 mdFlags.genOutputType { + outputTypes, err := extractOutputTypesFromState(bpPath) + 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 + } + } + } + // write core metadata to disk err = WriteMetadata(bpMetaObj, bpPath, metadataFileName) if err != nil { @@ -180,6 +204,44 @@ 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.NewTFBlueprintTestForNonTest( + 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)) @@ -243,8 +305,8 @@ func CreateBlueprintMetadata(bpPath string, bpMetadataObj *BlueprintMetadata) (* return nil, fmt.Errorf("error creating blueprint interfaces: %w", err) } - // Merge existing connections (if any) into the newly generated interfaces - mergeExistingConnections(bpMetadataObj.Spec.Interfaces, existingInterfaces) + // Merge existing connections (if any) into the newly generated interfaces + mergeExistingConnections(bpMetadataObj.Spec.Interfaces, existingInterfaces) // get blueprint requirements rolesCfgPath := path.Join(repoDetails.Source.BlueprintRootPath, tfRolesFileName) diff --git a/cli/bpmetadata/parser/state_parser.go b/cli/bpmetadata/parser/state_parser.go new file mode 100644 index 000000000000..e3294238a426 --- /dev/null +++ b/cli/bpmetadata/parser/state_parser.go @@ -0,0 +1,49 @@ +package parser + +import ( + "encoding/json" + + "github.com/golang/protobuf/jsonpb" + "google.golang.org/protobuf/types/known/structpb" +) + +// ParseOutputTypesFromState parses the terraform.tfstate file and extracts the output types +func ParseOutputTypesFromState(stateData []byte) (map[string]*structpb.Value, error) { // Change return type + var state struct { + Outputs map[string]struct { + Type interface{} `json:"type"` + } `json:"outputs"` + } + + if err := json.Unmarshal(stateData, &state); err != nil { + return nil, err + } + + outputTypes := make(map[string]*structpb.Value) // Change map type + for outputName, outputData := range state.Outputs { + outputType, err := convertInterfaceToValue(outputData.Type) // Use convertInterfaceToValue + if err != nil { + return nil, err + } + outputTypes[outputName] = outputType + } + + return outputTypes, nil +} + +// convertInterfaceToValue converts an interface{} to a structpb.Value +func convertInterfaceToValue(v interface{}) (*structpb.Value, error) { + // Marshal the interface{} to JSON + jsonData, err := json.Marshal(v) + if err != nil { + return nil, err + } + + // Unmarshal the JSON into a structpb.Value + var val structpb.Value + if err := jsonpb.UnmarshalString(string(jsonData), &val); err != nil { + return nil, err + } + + return &val, nil +} diff --git a/infra/blueprint-test/pkg/tft/terraform.go b/infra/blueprint-test/pkg/tft/terraform.go index 89c20e44bbaa..30f238aa8ef7 100644 --- a/infra/blueprint-test/pkg/tft/terraform.go +++ b/infra/blueprint-test/pkg/tft/terraform.go @@ -272,6 +272,37 @@ func NewTFBlueprintTest(t testing.TB, opts ...tftOption) *TFBlueprintTest { return tft } +// TFBlueprintTestForNonTest encapsulates the necessary parts of TFBlueprintTest +// for use outside of a testing context. +type TFBlueprintTestForNonTest struct { + *TFBlueprintTest + Assertions *assert.Assertions // Add an Assertions field +} + +// NewTFBlueprintTestForNonTest creates a TFBlueprintTest instance suitable for +// non-testing scenarios. +func NewTFBlueprintTestForNonTest(opts ...tftOption) (*TFBlueprintTestForNonTest, error) { + // Create a mock testing.T implementation + mockT := &utils.MockT{T: &gotest.T{}} + + // Create a custom logger with minimal configuration + customLogger := logger.Discard + + // Append the custom logger option + opts = append(opts, WithLogger(customLogger)) + + // Create the underlying TFBlueprintTest + tft := NewTFBlueprintTest(mockT, opts...) + + // If you need assertions in your non-test context, initialize them here: + assertions := assert.New(mockT) + + return &TFBlueprintTestForNonTest{ + TFBlueprintTest: tft, + Assertions: assertions, + }, nil +} + // sensitiveOutputs returns a map of sensitive output keys for module in dir. func (b *TFBlueprintTest) sensitiveOutputs(dir string) map[string]bool { mod, err := tfconfig.LoadModule(dir) diff --git a/infra/blueprint-test/pkg/utils/mock_t.go b/infra/blueprint-test/pkg/utils/mock_t.go new file mode 100644 index 000000000000..4f4f01533a63 --- /dev/null +++ b/infra/blueprint-test/pkg/utils/mock_t.go @@ -0,0 +1,49 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "fmt" + "testing" +) + +// MockT implements the testing.TB interface with minimal functionality +type MockT struct { + *testing.T +} + +// Logf is required by testing.TB but does nothing in this mock +func (m *MockT) Logf(format string, args ...any) { + fmt.Printf("MockT.Logf called with format: %s, args: %v\n", format, args) +} + +// Errorf is required by testing.TB but does nothing in this mock +func (m *MockT) Errorf(format string, args ...any) { + fmt.Printf("MockT.Errorf called with format: %s, args: %v\n", format, args) +} + +// Fatal is required by testing.TB but does nothing in this mock +func (m *MockT) Fatal(args ...any) { + fmt.Printf("MockT.Fatal called with args: %v\n", args) + panic(fmt.Sprint(args...)) // Call panic to stop execution +} + +// Fatalf is required by testing.TB but does nothing in this mock +func (m *MockT) Fatalf(format string, args ...any) { + fmt.Printf("MockT.Fatalf called with format: %s, args: %v\n", format, args) + panic(fmt.Sprintf(format, args...)) // Call panic to stop execution +} diff --git a/infra/blueprint-test/pkg/utils/mock_t_test.go b/infra/blueprint-test/pkg/utils/mock_t_test.go new file mode 100644 index 000000000000..dfeeea28289c --- /dev/null +++ b/infra/blueprint-test/pkg/utils/mock_t_test.go @@ -0,0 +1,56 @@ +/** + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils_test + +import ( + "testing" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" +) + +func TestMockT(t *testing.T) { + mockT := &utils.MockT{T: t} + + // Test Logf + mockT.Logf("test logf with %s", "argument") + // Check the output (you might need to capture it for a more robust test) + + // Test Errorf + mockT.Errorf("test errorf with %d", 123) + // Check the output + + // Test Fatal + // Note: Fatal will stop execution, so use a subtest or defer to recover + t.Run("Test Fatal", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("mockT.Fatal did not panic") + } + }() + mockT.Fatal("test fatal") + }) + + // Test Fatalf + t.Run("Test Fatalf", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("mockT.Fatalf did not panic") + } + }() + mockT.Fatalf("test fatalf with %s", "argument") + }) +}