mpdev/internal/tf/overwrite.go (333 lines of code) (raw):

// Copyright 2023 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 tf import ( "encoding/json" "fmt" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/hashicorp/terraform-config-inspect/tfconfig" "github.com/tidwall/gjson" "github.com/tidwall/sjson" "sigs.k8s.io/yaml" "os" "path" ) const mainTfFile = "main.tf" const metadataFile = "metadata.yaml" const metadataDisplayFile = "metadata.display.yaml" const defaultLabelsConst = "default_labels" const consumerLabelConst = "goog-partner-solution" type overwriteConfig struct { ConsumerLabel string NewValues map[string]string // Deprecated. If NewValues is specified, the following have no effect. 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 { upsertErr := upsertConsumerLabel(dir, config.ConsumerLabel) if upsertErr != nil { return upsertErr } if config.NewValues != nil { fmt.Printf("Replacing the default values of the variables: %s\n", config.NewValues) for varName, newValue := range config.NewValues { varInfo, err := getVarInfo(varName, dir) if err != nil { return err } if varInfo.Type != "string" { return fmt.Errorf("image variable: %s must be type string", varName) } err = overwriteFile(varInfo.Pos.Filename, varName, newValue) if err != nil { return err } } } else { fmt.Printf("Replacing the default values of the variables: %s\n", config.Variables) fmt.Printf("Mapping of values to replace: %s\n", config.Replacements) for _, varname := range config.Variables { varInfo, err := getVarInfo(varname, dir) if err != nil { return err } if varInfo.Default == nil { return fmt.Errorf("image variable: %s must have default value", varname) } defaultVal, ok := varInfo.Default.(string) if !ok { return fmt.Errorf("image variable: %s must be type string", varname) } replaceVal, ok := config.Replacements[defaultVal] if !ok { return fmt.Errorf("default value: %s of variable: %s not found in replacements", defaultVal, varname) } err = overwriteFile(varInfo.Pos.Filename, varname, replaceVal) if err != nil { return err } } } fmt.Println("Successfully replaced default values in tf files") return nil } // GetOverwriteConfig parses overwriteConfig from a byte array func GetOverwriteConfig(b []byte) (*overwriteConfig, error) { var config overwriteConfig err := json.Unmarshal(b, &config) if err != nil { return nil, fmt.Errorf("failure parsing overwrite config: %s error: %w", string(b), err) } return &config, nil } func getVarInfo(varname string, dir string) (*tfconfig.Variable, error) { module, diag := tfconfig.LoadModule(dir) if diag.HasErrors() { return nil, fmt.Errorf("failure parsing terraform module: %w", diag) } variable, ok := module.Variables[varname] if !ok { return nil, fmt.Errorf("variable: %s not found in module", varname) } return variable, nil } func getAttributeValueTokens(value string) hclwrite.Tokens { // Use logic similar to https://github.com/hashicorp/hcl/blob/4679383728fe331fc8a6b46036a27b8f818d9bc0/hclwrite/generate.go#L217-L234 // for writing string values return hclwrite.Tokens{ { Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`), SpacesBefore: 1, }, { Type: hclsyntax.TokenQuotedLit, Bytes: []byte(value), }, { Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`), }, } } func overwriteFile(filename string, varname string, value string) error { b, err := os.ReadFile(filename) if err != nil { return err } file, diag := hclwrite.ParseConfig(b, "", hcl.Pos{Line: 1, Column: 1}) if diag.HasErrors() { return diag } block := file.Body().FirstMatchingBlock("variable", []string{varname}) if block == nil { return fmt.Errorf("did not find block with variable: %s", varname) } // SetAttributeValue() is cleaner to overwrite values, however SetAttributeRaw gives more // control over formatting. SetAttributeValue() and File.WriteTo() would overwrite all // formatting. See: https://github.com/hashicorp/hcl/issues/316 block.Body().SetAttributeRaw("default", getAttributeValueTokens(value)) f, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC, 0000) if err != nil { return err } defer f.Close() rawBytes := file.BuildTokens(nil).Bytes() formattedBytes := hclwrite.Format(rawBytes) _, err = f.Write(formattedBytes) return err } // Inserts a consumer label under the `provider "google"` block if it does // not exist. // The `dir` parameter is the path to the TF main file. // The `label` parameter is the label value. func upsertConsumerLabel(dir string, label string) error { // If the parameter is not provided, do nothing. // This is for backward-compatibility purpose. if len(label) == 0 { fmt.Printf("No consumber label was passed as a parameter.\n") return nil } fmt.Printf("Inserting the '%s' consumber label.\n", label) mainTfFullPath := path.Join(dir, mainTfFile) b, err := os.ReadFile(mainTfFullPath) if err != nil { return err } mainTfParsedFile, diag := hclwrite.ParseConfig(b, "", hcl.Pos{Line: 1, Column: 1}) if diag.HasErrors() { return diag } // the `provider` section is expected in the main config file providerBlock := mainTfParsedFile.Body().FirstMatchingBlock("provider", []string{"google"}) if providerBlock == nil { // We always expect a `provider` block in the main config return fmt.Errorf("'provider \"google\"' block not found in %s", mainTfFullPath) } else { fmt.Printf("'provider \"google\"' block detected in %s\n", mainTfFullPath) defaultLabelsBlock := providerBlock.Body().FirstMatchingBlock(defaultLabelsConst, []string{}) if defaultLabelsBlock == nil { fmt.Printf("'%s' block not found in %s. Appending.\n", defaultLabelsConst, mainTfFullPath) defaultLabelsBlock = providerBlock.Body().AppendBlock( hclwrite.NewBlock(defaultLabelsConst, []string{})) } else { fmt.Printf("'%s' block detected in %s\n", defaultLabelsConst, mainTfFullPath) } labelAttribute := defaultLabelsBlock.Body().GetAttribute(consumerLabelConst) if labelAttribute == nil { // SetAttributeValue() is cleaner to overwrite values, however SetAttributeRaw gives more // control over formatting. SetAttributeValue() and File.WriteTo() would overwrite all // formatting. See: https://github.com/hashicorp/hcl/issues/316 defaultLabelsBlock.Body().SetAttributeRaw(consumerLabelConst, getAttributeValueTokens(label)) f, err := os.OpenFile(mainTfFullPath, os.O_WRONLY|os.O_TRUNC, 0000) if err != nil { return err } defer f.Close() rawBytes := mainTfParsedFile.BuildTokens(nil).Bytes() formattedBytes := hclwrite.Format(rawBytes) _, err = f.Write(formattedBytes) fmt.Printf("Successfully upserted consumber label in %s\n", mainTfFullPath) return err } else { fmt.Printf("Consumer label already exists in %s. Not overwriting.\n", mainTfFullPath) } } return nil } // OverwriteMetadata replaces default values for variables in Blueprints Metadata // // This overwrite rearranges the order of keys in the YAML, since we are not using the // definition in // https://github.com/GoogleCloudPlatform/cloud-foundation-toolkit/blob/master/cli/bpmetadata/types.go // // We are not using this definition because // 1. There are version compatiblilty issues with Kpt and cloud-foundation-toolkit to resolve // 2. We will avoid dropping fields if mpdev is using an out-of-date version of cloud-foundation-toolkit func OverwriteMetadata(config *overwriteConfig, dir string) error { data, err := os.ReadFile(path.Join(dir, metadataFile)) if err != nil { // CLI only modules will not have a metadata 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", metadataFile, err) } if config.NewValues != nil { fmt.Printf("Replacing the default values of the variables: %s in %s\n", config.NewValues, metadataFile) for varName, newValue := range config.NewValues { varQuery := fmt.Sprintf(`spec.interfaces.variables.#(name=="%s")`, varName) varEntry := gjson.GetBytes(json, varQuery) if varEntry.Raw == "" { return fmt.Errorf("missing variable entry for variable: %s in %s", varName, metadataFile) } // sjson.SetBytes doesn't work when spec.interfaces.variables.#(name=="%s").defaultValue // doesn't already exist. Retrieving and setting the whole variable entry // as a workaround. varEntryMap := varEntry.Value().(map[string]interface{}) varEntryMap["defaultValue"] = newValue json, err = sjson.SetBytes(json, varQuery, varEntryMap) if err != nil { return fmt.Errorf("error setting the updated entry for variable: %s. error: %w", varName, err) } } } else { fmt.Printf("Replacing the default values of the variables: %s in %s\n", config.Variables, metadataFile) for _, variable := range config.Variables { query := fmt.Sprintf(`spec.interfaces.variables.#(name=="%s").defaultValue`, variable) defaultVal := gjson.GetBytes(json, query).String() if defaultVal == "" { return fmt.Errorf("Missing valid default value for variable: %s in %s", variable, metadataFile) } replaceVal, ok := config.Replacements[defaultVal] if !ok { return fmt.Errorf("default value: %s of variable: %s in %s not found"+ " in replacements", defaultVal, variable, metadataFile) } json, err = sjson.SetBytes(json, query, replaceVal) 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, metadataFile), modifiedYaml, 0644) if err != nil { return err } 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) } if config.NewValues != nil { for varName, newValue := range config.NewValues { variableQuery := fmt.Sprintf(`spec.ui.input.variables.%s`, varName) variableInfo := gjson.GetBytes(json, variableQuery).String() if variableInfo == "" { return fmt.Errorf("missing valid display info for variable: %s in %s", varName, metadataDisplayFile) } enumValueLabels := gjson.Get(variableInfo, "enumValueLabels").Array() if len(enumValueLabels) == 0 { fmt.Printf("No enum value labels for display variable: %s in %s\n", varName, metadataDisplayFile) continue } var replacementEnumValueLabels []EnumValueLabel for _, enumValueLabel := range enumValueLabels { currLabel := enumValueLabel.Get("label").String() replacementEnumValueLabels = append(replacementEnumValueLabels, EnumValueLabel{Label: currLabel, Value: newValue}) } enumQuery := fmt.Sprintf(`spec.ui.input.variables.%s.enumValueLabels`, varName) json, err = sjson.SetBytes(json, enumQuery, replacementEnumValueLabels) if err != nil { return fmt.Errorf("error setting default value of variable: %s. error: %w", varName, err) } } } else { 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 }