pkg/config/validate.go (137 lines of code) (raw):

/** * Copyright 2022 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 config import ( "fmt" "regexp" "strings" "hpc-toolkit/pkg/modulereader" "github.com/pkg/errors" "github.com/zclconf/go-cty/cty" "golang.org/x/exp/maps" ) const maxLabels = 64 func validateGlobalLabels(bp Blueprint) error { if !bp.Vars.Has("labels") { return nil } p := Root.Vars.Dot("labels") labels := bp.Vars.Get("labels") if _, is := IsExpressionValue(labels); is { return nil // do not inspect expressions } ty := labels.Type() if !ty.IsObjectType() && !ty.IsMapType() { return BpError{ p, errors.New("vars.labels must be a map of strings")} // skip further validation } errs := Errors{} if labels.LengthInt() > maxLabels { // GCP resources cannot have more than 64 labels, so enforce this upper bound here // to do some early validation. Modules may add more labels, leading to potential // deployment failures. errs.At(p, errors.New("vars.labels cannot have more than 64 labels")) } for k, v := range labels.AsValueMap() { vp := p.Cty(cty.Path{}.IndexString(k)) // Check that label names are valid if !isValidLabelName(k) { errs.At(vp, HintError{ Err: fmt.Errorf("invalid label name %q", k), Hint: "name must begin with a lowercase letter, can only contain lowercase letters, numeric characters, underscores and dashes, and must be between 1 and 63 characters long"}) } if _, is := IsExpressionValue(v); is { continue // do not inspect expressions } if v.Type() != cty.String { errs.At(vp, errors.New("vars.labels must be a map of strings")) continue } s := v.AsString() // Check that label values are valid if !isValidLabelValue(s) { errs.At(vp, errors.Errorf("%s: '%s: %s'", errMsgLabelValueReqs, k, s)) } } return errs.OrNil() } // validateVars checks the global variables for viable types func validateVars(bp Blueprint) error { if _, err := varsTopologicalOrder(bp.Vars); err != nil { return err } errs := (&Errors{}). Add(validateDeploymentName(bp)). Add(validateGlobalLabels(bp)) // Check for any nil values // Iterator over non evaluated variables, it's Ok if evaluated value is null for key, val := range bp.Vars.Items() { if val.IsNull() { errs.At(Root.Vars.Dot(key), fmt.Errorf("deployment variable %q was not set", key)) } } return errs.OrNil() } func validateModule(p ModulePath, m Module, bp Blueprint) error { // Source/Kind validations are required to pass to perform other validations if m.Source == "" { return BpError{p.Source, EmptyModuleSource} } if err := checkMovedModule(m.Source); err != nil { return BpError{p.Source, err} } if !IsValidModuleKind(m.Kind.String()) { return BpError{p.Kind, InvalidModuleKind} } info, err := modulereader.GetModuleInfo(m.Source, m.Kind.kind) if err != nil { return BpError{p.Source, err} } errs := Errors{} if m.ID == "" { errs.At(p.ID, EmptyModuleID) } if m.ID == "vars" { // invalid module ID errs.At(p.ID, errors.New("module id cannot be 'vars'")) } return errs. Add(validateSettings(p, m, info)). Add(validateOutputs(p, m, info)). Add(validateModuleUseReferences(p, m, bp)). Add(validateModuleSettingReferences(p, m, bp)). OrNil() } func validateOutputs(p ModulePath, mod Module, info modulereader.ModuleInfo) error { errs := Errors{} outputs := info.GetOutputsAsMap() // Ensure output exists in the underlying modules for io, output := range mod.Outputs { if _, ok := outputs[output.Name]; !ok { err := fmt.Errorf("requested output %q was not found in the module %q", output.Name, mod.ID) errs.At(p.Outputs.At(io), err) } } return errs.OrNil() } type moduleVariables struct { Inputs map[string]bool Outputs map[string]bool } func validateSettings( p ModulePath, mod Module, info modulereader.ModuleInfo) error { var cVars = moduleVariables{ Inputs: map[string]bool{}, Outputs: map[string]bool{}, } for _, input := range info.Inputs { cVars.Inputs[input.Name] = input.Required } errs := Errors{} for k := range mod.Settings.Items() { sp := p.Settings.Dot(k) // Setting name included a period // The user was likely trying to set a subfield which is not supported. // HCL does not support periods in variables names either: // https://hcl.readthedocs.io/en/latest/language_design.html#language-keywords-and-identifiers if strings.Contains(k, ".") { errs.At(sp, ModuleSettingWithPeriod) continue // do not perform other validations } // Setting includes invalid characters if !regexp.MustCompile(`^[a-zA-Z-_][a-zA-Z0-9-_]*$`).MatchString(k) { errs.At(sp, ModuleSettingInvalidChar) continue // do not perform other validations } // Setting not found if _, ok := cVars.Inputs[k]; !ok { err := HintSpelling(k, maps.Keys(cVars.Inputs), UnknownModuleSetting) errs.At(sp, err) continue // do not perform other validations } } return errs.OrNil() }