pkg/validators/validators.go (177 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 validators import ( "errors" "fmt" "hpc-toolkit/pkg/config" "strings" "github.com/zclconf/go-cty/cty" ) func projectError(p string) error { return config.HintError{ Err: fmt.Errorf("project %q does not exist or your credentials do not have permission to access it", p), Hint: "It is possible the machine you are working on has not been authenticated.\n" + "Try to run `gcloud auth application-default login`", } } const regionError = "region %s is not available in project ID %s or your credentials do not have permission to access it" const zoneError = "zone %s is not available in project ID %s or your credentials do not have permission to access it" const zoneInRegionError = "zone %s is not in region %s in project ID %s or your credentials do not have permissions to access it" const unusedModuleMsg = "module %q uses module %q, but matching setting and outputs were not found. This may be because the value is set explicitly or set by a prior used module" const credentialsHint = "load application default credentials following instructions at https://github.com/GoogleCloudPlatform/hpc-toolkit/blob/main/README.md#supplying-cloud-credentials-to-terraform" var ErrNoDefaultCredentials = errors.New("could not find application default credentials") func handleClientError(e error) error { if strings.Contains(e.Error(), "could not find default credentials") { return config.HintError{Hint: credentialsHint, Err: ErrNoDefaultCredentials} } return e } const ( testApisEnabledName = "test_apis_enabled" testProjectExistsName = "test_project_exists" testRegionExistsName = "test_region_exists" testZoneExistsName = "test_zone_exists" testZoneInRegionName = "test_zone_in_region" testModuleNotUsedName = "test_module_not_used" testDeploymentVariableNotUsedName = "test_deployment_variable_not_used" ) func implementations() map[string]func(config.Blueprint, config.Dict) error { return map[string]func(config.Blueprint, config.Dict) error{ testApisEnabledName: testApisEnabled, testProjectExistsName: testProjectExists, testRegionExistsName: testRegionExists, testZoneExistsName: testZoneExists, testZoneInRegionName: testZoneInRegion, testModuleNotUsedName: testModuleNotUsed, testDeploymentVariableNotUsedName: testDeploymentVariableNotUsed, } } // ValidatorError is an error wrapper for errors that occurred during validation type ValidatorError struct { Validator string Err error } func (e ValidatorError) Unwrap() error { return e.Err } func (e ValidatorError) Error() string { return fmt.Sprintf("validator %q failed:\n%v", e.Validator, e.Err) } // Execute runs all validators on the blueprint func Execute(bp config.Blueprint) error { if bp.ValidationLevel == config.ValidationIgnore { return nil } impl := implementations() errs := config.Errors{} for iv, v := range validators(bp) { p := config.Root.Validators.At(iv) if v.Skip { continue } f, ok := impl[v.Validator] if !ok { errs.At(p.Validator, fmt.Errorf("unknown validator %q", v.Validator)) continue } inp, err := bp.EvalDict(v.Inputs) if err != nil { errs.At(p.Inputs, err) continue } if err := f(bp, inp); err != nil { errs.Add(ValidatorError{v.Validator, err}) // do not bother running further validators if project ID could not be found if v.Validator == "test_project_exists" { break } } } return errs.OrNil() } func checkInputs(inputs config.Dict, required []string) error { errs := config.Errors{} for _, inp := range required { if !inputs.Has(inp) { errs.Add(fmt.Errorf("a required input %q was not provided", inp)) } } if errs.Any() { return errs } // ensure that no extra inputs were provided by comparing length if len(required) != len(inputs.Items()) { errStr := "only %v inputs %s should be provided" return fmt.Errorf(errStr, len(required), required) } return nil } // Helper function to sure that all input values are strings. func inputsAsStrings(inputs config.Dict) (map[string]string, error) { ms := map[string]string{} for k, v := range inputs.Items() { if v.Type() != cty.String { return nil, fmt.Errorf("validator inputs must be strings, %s is a %s", k, v.Type()) } ms[k] = v.AsString() } return ms, nil } // Creates a list of default validators for the given blueprint, // inspect the blueprint for global variables that exist and add an appropriate validators. func defaults(bp config.Blueprint) []config.Validator { projectIDExists := bp.Vars.Has("project_id") projectRef := config.GlobalRef("project_id").AsValue() regionExists := bp.Vars.Has("region") regionRef := config.GlobalRef("region").AsValue() zoneExists := bp.Vars.Has("zone") zoneRef := config.GlobalRef("zone").AsValue() defaults := []config.Validator{ {Validator: testModuleNotUsedName}, {Validator: testDeploymentVariableNotUsedName}} // always add the project ID validator before subsequent validators that can // only succeed if credentials can access the project. If the project ID // validator fails, all remaining validators are not executed. if projectIDExists { inputs := config.Dict{}.With("project_id", projectRef) defaults = append(defaults, config.Validator{ Validator: testProjectExistsName, Inputs: inputs, }, config.Validator{ Validator: testApisEnabledName, Inputs: inputs, }, ) } if projectIDExists && regionExists { defaults = append(defaults, config.Validator{ Validator: testRegionExistsName, Inputs: config.NewDict(map[string]cty.Value{ "project_id": projectRef, "region": regionRef, }, )}) } if projectIDExists && zoneExists { defaults = append(defaults, config.Validator{ Validator: testZoneExistsName, Inputs: config.NewDict(map[string]cty.Value{ "project_id": projectRef, "zone": zoneRef, }), }) } if projectIDExists && regionExists && zoneExists { defaults = append(defaults, config.Validator{ Validator: testZoneInRegionName, Inputs: config.NewDict(map[string]cty.Value{ "project_id": projectRef, "region": regionRef, "zone": zoneRef, }), }) } return defaults } // Returns a list of validators for the given blueprint with any default validators appended. func validators(bp config.Blueprint) []config.Validator { used := map[string]bool{} for _, v := range bp.Validators { used[v.Validator] = true } vs := append([]config.Validator{}, bp.Validators...) // clone for _, v := range defaults(bp) { if !used[v.Validator] { vs = append(vs, v) } } return vs }