From b3f4fde2d11e5e38dda562e97486936385e582d0 Mon Sep 17 00:00:00 2001 From: Ray Zhang <141791707+rzy-goog@users.noreply.github.com> Date: Tue, 29 Aug 2023 17:49:50 -0700 Subject: [PATCH] add mpdev tf overwrite for metadata display (#75) * add mpdev tf overwrite for metadata display * add OverwriteDisplay to overwritecmd * change yaml example format to standard array/object --- mpdev/cmd/tf/overwritecmd.go | 12 +- mpdev/internal/tf/overwrite.go | 76 +++++++++++ mpdev/internal/tf/overwrite_test.go | 198 +++++++++++++++++++++++++++- 3 files changed, 281 insertions(+), 5 deletions(-) diff --git a/mpdev/cmd/tf/overwritecmd.go b/mpdev/cmd/tf/overwritecmd.go index ee80512..88e4250 100644 --- a/mpdev/cmd/tf/overwritecmd.go +++ b/mpdev/cmd/tf/overwritecmd.go @@ -15,11 +15,12 @@ package tf import ( + "io" + "os" + "github.com/GoogleCloudPlatform/marketplace-tools/mpdev/internal/docs" "github.com/GoogleCloudPlatform/marketplace-tools/mpdev/internal/tf" "github.com/spf13/cobra" - "io" - "os" ) // GetOverwriteCommand returns `overwrite` command used to create mpdev resources. @@ -57,5 +58,10 @@ func overwriteRunE(_ *cobra.Command, _ []string) (err error) { return err } - return tf.OverwriteMetadata(config, dir) + err = tf.OverwriteMetadata(config, dir) + if err != nil { + return err + } + + return tf.OverwriteDisplay(config, dir) } diff --git a/mpdev/internal/tf/overwrite.go b/mpdev/internal/tf/overwrite.go index c1981b7..6804458 100644 --- a/mpdev/internal/tf/overwrite.go +++ b/mpdev/internal/tf/overwrite.go @@ -17,6 +17,7 @@ package tf import ( "encoding/json" "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclwrite" @@ -30,12 +31,18 @@ import ( ) const metadataFile = "metadata.yaml" +const metadataDisplayFile = "metadata.display.yaml" type overwriteConfig struct { Variables []string Replacements map[string]string } +type EnumValueLabel struct { + Label string `json:"label"` + Value string `json:"value"` +} + // OverwriteTf replaces default variable values in Terraform modules func OverwriteTf(config *overwriteConfig, dir string) error { fmt.Printf("Replacing the default values of the variables: %s\n", config.Variables) @@ -208,3 +215,72 @@ func OverwriteMetadata(config *overwriteConfig, dir string) error { fmt.Printf("Successfully replaced default values in %s\n", metadataFile) return nil } + +// OverwriteDisplay replaces variable values in Blueprint metadata display file. +func OverwriteDisplay(config *overwriteConfig, dir string) error { + fmt.Printf("Replacing the values of the display variables: %s in %s\n", + config.Variables, metadataDisplayFile) + + data, err := os.ReadFile(path.Join(dir, metadataDisplayFile)) + if err != nil { + // CLI only modules will not have a metadata display file. Ignore file not found errors + if os.IsNotExist(err) { + return nil + } + return err + } + + json, err := yaml.YAMLToJSON(data) + if err != nil { + return fmt.Errorf("failure parsing %s error: %w", metadataDisplayFile, err) + } + + for _, variable := range config.Variables { + variableQuery := fmt.Sprintf(`spec.ui.input.variables.%s`, variable) + variableInfo := gjson.GetBytes(json, variableQuery).String() + if variableInfo == "" { + return fmt.Errorf("missing valid display info for variable: %s in %s", + variable, metadataDisplayFile) + } + + enumValueLabels := gjson.Get(variableInfo, "enumValueLabels").Array() + if len(enumValueLabels) == 0 { + fmt.Printf("No enum value labels for display variable: %s in %s\n", + variable, metadataDisplayFile) + continue + } + + var replacementEnumValueLabels []EnumValueLabel + for _, enumValueLabel := range enumValueLabels { + currValue := enumValueLabel.Get("value").String() + currLabel := enumValueLabel.Get("label").String() + replaceVal, ok := config.Replacements[currValue] + if !ok { + return fmt.Errorf("enum value: %s of variable: %s in %s not found"+ + " in replacements", currValue, variable, metadataDisplayFile) + } + replacementEnumValueLabels = append(replacementEnumValueLabels, EnumValueLabel{Label: currLabel, Value: replaceVal}) + } + + enumQuery := fmt.Sprintf(`spec.ui.input.variables.%s.enumValueLabels`, variable) + json, err = sjson.SetBytes(json, enumQuery, replacementEnumValueLabels) + if err != nil { + return fmt.Errorf("error setting default value of variable: %s. error: %w", + variable, err) + } + + } + + modifiedYaml, err := yaml.JSONToYAML([]byte(json)) + if err != nil { + return err + } + + err = os.WriteFile(path.Join(dir, metadataDisplayFile), modifiedYaml, 0644) + if err != nil { + return err + } + + fmt.Printf("Successfully replaced display values in %s\n", metadataDisplayFile) + return nil +} diff --git a/mpdev/internal/tf/overwrite_test.go b/mpdev/internal/tf/overwrite_test.go index 9d1f72e..9d26bae 100644 --- a/mpdev/internal/tf/overwrite_test.go +++ b/mpdev/internal/tf/overwrite_test.go @@ -14,12 +14,13 @@ package tf import ( - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" "os" "path" "path/filepath" "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestOverwriteTf(t *testing.T) { @@ -308,6 +309,107 @@ func getDirContents(dir string) (map[string]string, error) { return fileContents, err } +func TestOverwriteDisplay(t *testing.T) { + testcases := []struct { + name string + originalMetadataDisplay string + expectedMetadataDisplay string + overwriteConfig overwriteConfig + errorContains string + }{ + { + name: "Overwrite single display variable enum values", + originalMetadataDisplay: metadataDisplayWithEnumsSingle, + expectedMetadataDisplay: metadataDisplayWithEnumsSingleReplaced, + overwriteConfig: overwriteConfig{ + Variables: []string{"source_image"}, + Replacements: map[string]string{ + "projects/click-to-deploy-images/global/images/wordpress-1": "projects/replacement/global/images/wordpress-1-new", + }, + }, + }, + { + name: "Overwrite multiple display variable enum values", + originalMetadataDisplay: metadataDisplayWithEnumsDouble, + expectedMetadataDisplay: metadataDisplayWithEnumsDoubleReplaced, + overwriteConfig: overwriteConfig{ + Variables: []string{"source_image", "another_image"}, + Replacements: map[string]string{ + "projects/click-to-deploy-images/global/images/wordpress-1": "projects/replacement/global/images/wordpress-1-new", + "projects/click-to-deploy-images/global/images/wordpress-2": "projects/replacement/global/images/wordpress-2-new", + "projects/click-to-deploy-images/global/images/wordpress-3": "projects/replacement/global/images/wordpress-3-new", + }, + }, + }, + { + name: "No changes if no display variable enum value labels", + originalMetadataDisplay: metadataDisplayNoEnums, + expectedMetadataDisplay: metadataDisplayNoEnums, + overwriteConfig: overwriteConfig{ + Variables: []string{"source_image"}, + Replacements: map[string]string{}, + }, + }, { + name: "Fail when metadata display is invalid yaml", + originalMetadataDisplay: "- not validyaml\ninvalid-", + errorContains: "failure parsing metadata.display.yaml", + }, { + name: "Fail when display variable not present in Metadata display", + originalMetadataDisplay: metadata, + overwriteConfig: overwriteConfig{ + Variables: []string{"missing_variable"}, + Replacements: map[string]string{ + "original-value": "new-value", + }, + }, + errorContains: "missing valid display info for variable: missing_variable", + }, { + name: "Fail when display variable enum value is not in replacements", + originalMetadataDisplay: metadataDisplayWithEnumsSingle, + overwriteConfig: overwriteConfig{ + Variables: []string{"source_image"}, + Replacements: map[string]string{ + "non-existent": "new-value", + }, + }, + errorContains: "enum value: projects/click-to-deploy-images/global/images/wordpress-1 of variable: source_image in metadata.display.yaml not found in replacements", + }} + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "tftest") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + err = os.WriteFile(path.Join(tmpDir, "metadata.display.yaml"), + []byte(tc.originalMetadataDisplay), 0600) + assert.NoError(t, err) + + err = OverwriteDisplay(&tc.overwriteConfig, tmpDir) + + if tc.errorContains == "" { + assert.NoError(t, err) + + metadataDisplayBytes, err := os.ReadFile(path.Join(tmpDir, "metadata.display.yaml")) + assert.NoError(t, err) + actualMetadataDisplay := make(map[interface{}]interface{}) + expectedMetadataDisplay := make(map[interface{}]interface{}) + + err = yaml.Unmarshal(metadataDisplayBytes, &actualMetadataDisplay) + assert.NoError(t, err) + + err = yaml.Unmarshal([]byte(tc.expectedMetadataDisplay), expectedMetadataDisplay) + assert.NoError(t, err) + + assert.Equal(t, expectedMetadataDisplay, actualMetadataDisplay) + } else { + assert.Error(t, err) + assert.ErrorContains(t, err, tc.errorContains) + } + }) + } +} + var mainTf string = ` resource "google_compute_instance_template" "template" { name = "template" @@ -406,3 +508,95 @@ spec: description: The image name for the disk for the VM instance. varType: string ` + +var metadataDisplayWithEnumsSingle string = ` +spec: + ui: + input: + variables: + source_image: + name: source_image + title: Source Image + enumValueLabels: + - label: wordpress-1 + value: projects/click-to-deploy-images/global/images/wordpress-1 + xGoogleProperty: + type: ET_GCE_DISK_IMAGE +` + +var metadataDisplayWithEnumsSingleReplaced string = ` +spec: + ui: + input: + variables: + source_image: + name: source_image + title: Source Image + enumValueLabels: + - label: wordpress-1 + value: projects/replacement/global/images/wordpress-1-new + xGoogleProperty: + type: ET_GCE_DISK_IMAGE +` + +var metadataDisplayWithEnumsDouble string = ` +spec: + ui: + input: + variables: + source_image: + name: source_image + title: Source Image + enumValueLabels: + - label: wordpress-1 + value: projects/click-to-deploy-images/global/images/wordpress-1 + - label: wordpress-2 + value: projects/click-to-deploy-images/global/images/wordpress-2 + xGoogleProperty: + type: ET_GCE_DISK_IMAGE + another_image: + name: another_image + title: Another Image + enumValueLabels: + - label: wordpress-3 + value: projects/click-to-deploy-images/global/images/wordpress-3 + xGoogleProperty: + type: ET_GCE_DISK_IMAGE +` + +var metadataDisplayWithEnumsDoubleReplaced string = ` +spec: + ui: + input: + variables: + source_image: + name: source_image + title: Source Image + enumValueLabels: + - label: wordpress-1 + value: projects/replacement/global/images/wordpress-1-new + - label: wordpress-2 + value: projects/replacement/global/images/wordpress-2-new + xGoogleProperty: + type: ET_GCE_DISK_IMAGE + another_image: + name: another_image + title: Another Image + enumValueLabels: + - label: wordpress-3 + value: projects/replacement/global/images/wordpress-3-new + xGoogleProperty: + type: ET_GCE_DISK_IMAGE +` + +var metadataDisplayNoEnums string = ` +spec: + ui: + input: + variables: + source_image: + name: source_image + title: Source Image + xGoogleProperty: + type: ET_GCE_DISK_IMAGE +`