pkg/config/config.go (695 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 manages and updates the gcluster input config package config import ( "bytes" "fmt" "os" "path/filepath" "regexp" "slices" "sort" "strings" "github.com/agext/levenshtein" "github.com/hashicorp/hcl/v2" "github.com/pkg/errors" "github.com/zclconf/go-cty/cty" "golang.org/x/exp/maps" "gopkg.in/yaml.v3" "hpc-toolkit/pkg/modulereader" ) const ( maxHintDist int = 3 // Maximum Levenshtein distance where we suggest a hint ) // map[moved module path]replacing module path var movedModules = map[string]string{ "community/modules/scheduler/cloud-batch-job": "modules/scheduler/batch-job-template", "community/modules/scheduler/cloud-batch-login-node": "modules/scheduler/batch-login-node", "community/modules/scheduler/htcondor-configure": "community/modules/scheduler/htcondor-setup", "community/modules/scripts/spack-install": "community/modules/scripts/spack-setup", } // GroupName is the name of a deployment group type GroupName string // Validate checks that the group name is valid func (n GroupName) Validate() error { if n == "" { return EmptyGroupName } if !regexp.MustCompile(`^\w(-*\w)*$`).MatchString(string(n)) { return fmt.Errorf("invalid character(s) found in group name %q.\n"+ "Allowed : alphanumeric, '_', and '-'; can not start/end with '-'", n) } return nil } // Group defines a group of Modules that are all executed together type Group struct { Name GroupName `yaml:"group"` TerraformBackend TerraformBackend `yaml:"terraform_backend,omitempty"` TerraformProviders map[string]TerraformProvider `yaml:"terraform_providers,omitempty"` Modules []Module `yaml:"modules"` // DEPRECATED fields deprecatedKind interface{} `yaml:"kind,omitempty"` //lint:ignore U1000 keep in the struct for backwards compatibility } func (g *Group) Clone() Group { c := *g // copy immutable fields // modules require deep copy c.Modules = make([]Module, len(g.Modules)) for i, m := range g.Modules { c.Modules[i] = m.Clone() } return c } // Kind returns the kind of all the modules in the group. // If the group contains modules of different kinds, it returns UnknownKind func (g Group) Kind() ModuleKind { if len(g.Modules) == 0 { return UnknownKind } k := g.Modules[0].Kind for _, m := range g.Modules { if m.Kind != k { return UnknownKind } } return k } // Module return the module with the given ID func (bp *Blueprint) Module(id ModuleID) (*Module, error) { var mod *Module bp.WalkModulesSafe(func(_ ModulePath, m *Module) { if m.ID == id { mod = m } }) if mod == nil { return nil, UnknownModuleError{id} } return mod, nil } func HintSpelling(s string, dict []string, err error) error { best, minDist := "", maxHintDist+1 for _, w := range dict { d := levenshtein.Distance(s, w, nil) if d < minDist { best, minDist = w, d } } if minDist <= maxHintDist { return HintError{fmt.Sprintf("did you mean %q?", best), err} } return err } // ModuleGroup returns the group containing the module func (bp Blueprint) ModuleGroup(mod ModuleID) (Group, error) { for _, g := range bp.Groups { for _, m := range g.Modules { if m.ID == mod { return g, nil } } } return Group{}, UnknownModuleError{mod} } // ModuleGroupOrDie returns the group containing the module; panics if unfound func (bp Blueprint) ModuleGroupOrDie(mod ModuleID) Group { g, err := bp.ModuleGroup(mod) if err != nil { panic(err) } return g } // GroupIndex returns the index of the input group in the blueprint // return -1 if not found func (bp Blueprint) GroupIndex(n GroupName) int { for i, g := range bp.Groups { if g.Name == n { return i } } return -1 } // Group returns the deployment group with a given name func (bp Blueprint) Group(n GroupName) (Group, error) { idx := bp.GroupIndex(n) if idx == -1 { return Group{}, fmt.Errorf("could not find group %s in blueprint", n) } return bp.Groups[idx], nil } // TerraformBackend defines the configuration for the terraform state backend type TerraformBackend struct { Type string Configuration Dict } // TerraformProvider defines the configuration for the terraform providers type TerraformProvider struct { Source string Version string Configuration Dict } // ModuleKind abstracts Toolkit module kinds (presently: packer/terraform) type ModuleKind struct { kind string } // UnknownKind is the default value when the user has not specified module kind var UnknownKind = ModuleKind{kind: ""} // TerraformKind is the kind for Terraform modules (should be treated as const) var TerraformKind = ModuleKind{kind: "terraform"} // PackerKind is the kind for Packer modules (should be treated as const) var PackerKind = ModuleKind{kind: "packer"} // IsValidModuleKind ensures that the user has specified a supported kind func IsValidModuleKind(kind string) bool { return kind == TerraformKind.String() || kind == PackerKind.String() || kind == UnknownKind.String() } func (mk ModuleKind) String() string { return mk.kind } // this enum will be used to control how fatal validator failures will be // treated during blueprint creation const ( ValidationError int = iota ValidationWarning ValidationIgnore ) // Validator defines a validation step to be run on a blueprint type Validator struct { Validator string Inputs Dict `yaml:"inputs,omitempty"` Skip bool `yaml:"skip,omitempty"` } // ModuleID is a unique identifier for a module in a blueprint type ModuleID string // ModuleIDs is a list of ModuleID type ModuleIDs []ModuleID // Module stores YAML definition of an HPC cluster component defined in a blueprint type Module struct { Source string Kind ModuleKind ID ModuleID Use ModuleIDs `yaml:"use,omitempty"` Outputs []modulereader.OutputInfo `yaml:"outputs,omitempty"` Settings Dict `yaml:"settings,omitempty"` // DEPRECATED fields, keep in the struct for backwards compatibility RequiredApis interface{} `yaml:"required_apis,omitempty"` WrapSettingsWith interface{} `yaml:"wrapsettingswith,omitempty"` } func (m *Module) Clone() Module { c := *m // copy immutable fields // copy slices c.Use = slices.Clone(m.Use) c.Outputs = slices.Clone(m.Outputs) return c } // InfoOrDie returns the ModuleInfo for the module or panics func (m Module) InfoOrDie() modulereader.ModuleInfo { mi, err := modulereader.GetModuleInfo(m.Source, m.Kind.String()) if err != nil { panic(err) } return mi } // Blueprint stores the contents on the User YAML // omitempty on validation_level ensures that expand will not expose the setting // unless it has been set to a non-default value; the implementation as an // integer is primarily for internal purposes even if it can be set in blueprint type Blueprint struct { BlueprintName string `yaml:"blueprint_name"` GhpcVersion string `yaml:"ghpc_version,omitempty"` Validators []Validator `yaml:"validators,omitempty"` ValidationLevel int `yaml:"validation_level,omitempty"` Vars Dict Groups []Group `yaml:"deployment_groups"` TerraformBackendDefaults TerraformBackend `yaml:"terraform_backend_defaults,omitempty"` TerraformProviders map[string]TerraformProvider `yaml:"terraform_providers,omitempty"` ToolkitModulesURL string `yaml:"toolkit_modules_url,omitempty"` ToolkitModulesVersion string `yaml:"toolkit_modules_version,omitempty"` // internal & non-serializable fields // absolute path to the blueprint file path string // records of intentions to stage file (populated by ghpc_stage function) stagedFiles map[string]string } func (bp *Blueprint) Clone() Blueprint { c := *bp // copy immutable fields // copy slices & maps of immutable types c.Validators = slices.Clone(bp.Validators) c.stagedFiles = maps.Clone(bp.stagedFiles) // groups require deep copy c.Groups = make([]Group, len(bp.Groups)) for i, g := range bp.Groups { c.Groups[i] = g.Clone() } return c } func (bp *Blueprint) mutateDicts(cb func(dictPath, *Dict) Dict) { bp.Vars = cb(Root.Vars, &bp.Vars) bp.TerraformBackendDefaults.Configuration = cb(Root.Backend.Configuration, &bp.TerraformBackendDefaults.Configuration) for k, p := range bp.TerraformProviders { p.Configuration = cb(Root.Provider.Dot(k).Configuration, &p.Configuration) bp.TerraformProviders[k] = p } for ig := range bp.Groups { g := &bp.Groups[ig] gp := Root.Groups.At(ig) g.TerraformBackend.Configuration = cb(gp.Backend.Configuration, &g.TerraformBackend.Configuration) for k, p := range g.TerraformProviders { p.Configuration = cb(gp.Provider.Dot(k).Configuration, &p.Configuration) g.TerraformProviders[k] = p } for im := range g.Modules { m := &g.Modules[im] m.Settings = cb(gp.Modules.At(im).Settings, &m.Settings) } } for i := range bp.Validators { v := &bp.Validators[i] v.Inputs = cb(Root.Validators.At(i).Inputs, &v.Inputs) } } func (bp *Blueprint) visitDicts(cb func(dictPath, *Dict)) { bp.mutateDicts(func(p dictPath, d *Dict) Dict { cb(p, d) return *d }) } // DeploymentSettings are deployment-specific override settings type DeploymentSettings struct { TerraformBackendDefaults TerraformBackend `yaml:"terraform_backend_defaults,omitempty"` Vars Dict } // Expand expands the config in place func (bp *Blueprint) Expand() error { // expand the blueprint in dependency order: // BlueprintName -> DefaultBackend -> Vars -> Groups errs := (&Errors{}). Add(checkStringLiterals(bp)). Add(bp.checkBlueprintName()). Add(bp.checkToolkitModulesUrlAndVersion()). Add(checkProviders(Root.Provider, bp.TerraformProviders)) if errs.Any() { return *errs } if err := bp.expandVars(); err != nil { return err } if err := bp.checkReferences(); err != nil { return err } return bp.expandGroups() } // ListUnusedModules provides a list modules that are in the // "use" field, but not actually used. func (m Module) ListUnusedModules() ModuleIDs { used := map[ModuleID]bool{} // Recurse through objects/maps/lists checking each element for having `ProductOfModuleUse` mark. cty.Walk(m.Settings.AsObject(), func(p cty.Path, v cty.Value) (bool, error) { for _, mod := range IsProductOfModuleUse(v) { used[mod] = true } return true, nil }) unused := ModuleIDs{} for _, w := range m.Use { if !used[w] { unused = append(unused, w) } } return unused } // GetUsedDeploymentVars returns a list of deployment vars used in the given value func GetUsedDeploymentVars(val cty.Value) []string { res := []string{} for ref := range valueReferences(val) { if ref.GlobalVar { res = append(res, ref.Name) } } return res } // ListUnusedVariables returns a list of variables that are defined but not used func (bp Blueprint) ListUnusedVariables() []string { // Gather all scopes where variables are used ns := map[string]cty.Value{ "vars": bp.Vars.AsObject(), } bp.WalkModulesSafe(func(_ ModulePath, m *Module) { ns["module_"+string(m.ID)] = m.Settings.AsObject() }) for _, v := range bp.Validators { ns["validator_"+v.Validator] = v.Inputs.AsObject() } for k, v := range bp.TerraformProviders { ns["bp_provider_"+k] = v.Configuration.AsObject() } for _, grp := range bp.Groups { for k, v := range grp.TerraformProviders { ns["grp_"+string(grp.Name)+"_provider_"+k] = v.Configuration.AsObject() } } var used = map[string]bool{ "labels": true, // automatically added "deployment_name": true, // required "project_id": true, // by google provider "region": true, // by google provider "zone": true, // by google provider } for _, v := range GetUsedDeploymentVars(cty.ObjectVal(ns)) { used[v] = true } unused := []string{} for _, k := range bp.Vars.Keys() { if _, ok := used[k]; !ok { unused = append(unused, k) } } return unused } func checkMovedModule(source string) error { if replacement, ok := movedModules[strings.Trim(source, "./")]; ok { return fmt.Errorf( "a module has moved. %s has been replaced with %s. Please update the source in your blueprint and try again", source, replacement) } return nil } // NewBlueprint is a constructor for Blueprint func NewBlueprint(path string) (Blueprint, *YamlCtx, error) { absPath, err := filepath.Abs(path) if err != nil { return Blueprint{}, &YamlCtx{}, err } bp, ctx, err := parseYamlFile[Blueprint](absPath) if err != nil { return Blueprint{}, &ctx, err } bp.path = absPath return bp, &ctx, nil } func NewDeploymentSettings(deploymentFilename string) (DeploymentSettings, YamlCtx, error) { return parseYamlFile[DeploymentSettings](deploymentFilename) } // Export exports the internal representation of a blueprint config func (bp Blueprint) Export(outputFilename string) error { var buf bytes.Buffer buf.WriteString(YamlLicense) buf.WriteString("\n") encoder := yaml.NewEncoder(&buf) encoder.SetIndent(2) err := encoder.Encode(&bp) encoder.Close() d := buf.Bytes() if err != nil { return fmt.Errorf("failed to export the configuration to a blueprint yaml file: %w", err) } err = os.WriteFile(outputFilename, d, 0644) if err != nil { return fmt.Errorf("failed to write the expanded yaml %s: %w", outputFilename, err) } return nil } // addKindToModules sets the kind to 'terraform' when empty. func (bp *Blueprint) addKindToModules() { bp.WalkModulesSafe(func(_ ModulePath, m *Module) { if m.Kind == UnknownKind { m.Kind = TerraformKind } }) } func checkModulesAndGroups(bp Blueprint) error { seenMod := map[ModuleID]bool{} seenGrp := map[GroupName]bool{} errs := Errors{} for ig, grp := range bp.Groups { pg := Root.Groups.At(ig) errs.At(pg.Name, grp.Name.Validate()) if seenGrp[grp.Name] { errs.At(pg.Name, fmt.Errorf("group names must be unique, %q used more than once", grp.Name)) } seenGrp[grp.Name] = true if len(grp.Modules) == 0 { errs.At(pg.Modules, errors.New("deployment group must have at least one module")) } else if grp.Kind() == UnknownKind { errs.At(pg.Modules, errors.New("mixing modules of differing kinds in a deployment group is not supported")) } else if grp.Kind() == PackerKind && len(grp.Modules) > 1 { errs.At(pg, HintError{ Err: fmt.Errorf("packer group %q has more than 1 module", grp.Name), Hint: "separate each packer module into its own deployment group"}) } for im, mod := range grp.Modules { pm := pg.Modules.At(im) if seenMod[mod.ID] { errs.At(pm.ID, fmt.Errorf("module IDs must be unique, %q used more than once", mod.ID)) } seenMod[mod.ID] = true errs.Add(validateModule(pm, mod, bp)) } errs.Add(checkProviders(pg.Provider, grp.TerraformProviders)) } return errs.OrNil() } // validateModuleUseReferences verifies that any used modules exist and // are in the correct group func validateModuleUseReferences(p ModulePath, mod Module, bp Blueprint) error { errs := Errors{} for iu, used := range mod.Use { errs.At(p.Use.At(iu), validateModuleReference(bp, mod, used)) } return errs.OrNil() } func checkStringLiterals(bp *Blueprint) error { errs := Errors{} bp.visitStringLiterals(func(p Path, s string) { errs.Add(checkStringLiteral(p, s)) }) return errs.OrNil() } func checkStringLiteral(p Path, s string) error { val, perr := parseYamlString(s) if _, is := IsExpressionValue(val); is || perr != nil { return BpError{p, errors.New("can not use expression here")} } return nil } func checkProviders(pp mapPath[providerPath], tp map[string]TerraformProvider) error { for k, v := range tp { if v.Source == "" { return BpError{pp.Dot(k).Source, errors.New(fmt.Sprintf("provider %q is missing source", k))} } if v.Version == "" { return BpError{pp.Dot(k).Version, errors.New(fmt.Sprintf("provider %q is missing version", k))} } } return nil } // SkipValidator marks validator(s) as skipped, // if no validator is present, adds one, marked as skipped. func (bp *Blueprint) SkipValidator(name string) { if bp.Validators == nil { bp.Validators = []Validator{} } skipped := false for i, v := range bp.Validators { if v.Validator == name { bp.Validators[i].Skip = true skipped = true } } if !skipped { bp.Validators = append(bp.Validators, Validator{Validator: name, Skip: true}) } } // InputValueError signifies a problem with the blueprint name. type InputValueError struct { inputKey string cause string } func (err InputValueError) Error() string { return fmt.Sprintf("%v input error, cause: %v", err.inputKey, err.cause) } var matchLabelNameExp *regexp.Regexp = regexp.MustCompile(`^[\p{Ll}\p{Lo}][\p{Ll}\p{Lo}\p{N}_-]{0,62}$`) var matchLabelValueExp *regexp.Regexp = regexp.MustCompile(`^[\p{Ll}\p{Lo}\p{N}_-]{0,63}$`) // isValidLabelName checks if a string is a valid name for a GCP label. // For more information on valid label names, see the docs at: // https://cloud.google.com/resource-manager/docs/creating-managing-labels#requirements func isValidLabelName(name string) bool { return matchLabelNameExp.MatchString(name) } // isValidLabelValue checks if a string is a valid value for a GCP label. // For more information on valid label values, see the docs at: // https://cloud.google.com/resource-manager/docs/creating-managing-labels#requirements func isValidLabelValue(value string) bool { return matchLabelValueExp.MatchString(value) } func (bp *Blueprint) DeploymentName() string { v, _ := bp.Eval(GlobalRef("deployment_name").AsValue()) // ignore errors as we already validated the blueprint return v.AsString() } func validateDeploymentName(bp Blueprint) error { path := Root.Vars.Dot("deployment_name") if !bp.Vars.Has("deployment_name") { return BpError{path, InputValueError{ inputKey: "deployment_name", cause: "could not find source of variable", }} } v, err := bp.Eval(GlobalRef("deployment_name").AsValue()) if err != nil { return BpError{path, err} } if v.Type() != cty.String || v.IsNull() || !v.IsKnown() { return BpError{path, InputValueError{ inputKey: "deployment_name", cause: errMsgValueNotString, }} } s := v.AsString() if len(s) == 0 { return BpError{path, InputValueError{ inputKey: "deployment_name", cause: errMsgValueEmptyString, }} } // Check that deployment_name is a valid label if !isValidLabelValue(s) { return BpError{path, InputValueError{ inputKey: "deployment_name", cause: errMsgLabelValueReqs, }} } return nil } // checkBlueprintName returns an error if blueprint_name does not comply with // requirements for correct GCP label values. func (bp *Blueprint) checkBlueprintName() error { if len(bp.BlueprintName) == 0 { return BpError{Root.BlueprintName, InputValueError{ inputKey: "blueprint_name", cause: errMsgValueEmptyString, }} } if !isValidLabelValue(bp.BlueprintName) { return BpError{Root.BlueprintName, InputValueError{ inputKey: "blueprint_name", cause: errMsgLabelValueReqs, }} } return nil } // checkToolkitModulesUrlAndVersion returns an error if either // toolkit_modules_url or toolkit_modules_version is // exclsuively supplied (i.e., one is present, but the other is missing). func (bp *Blueprint) checkToolkitModulesUrlAndVersion() error { if bp.ToolkitModulesURL == "" && bp.ToolkitModulesVersion != "" { return BpError{Root.ToolkitModulesVersion, HintError{ Err: errors.New("toolkit_modules_url must be provided when toolkit_modules_version is specified"), Hint: "Specify toolkit_modules_url"}} } if bp.ToolkitModulesURL != "" && bp.ToolkitModulesVersion == "" { return BpError{Root.ToolkitModulesURL, HintError{ Err: errors.New("toolkit_modules_version must be provided when toolkit_modules_url is specified"), Hint: "Specify toolkit_modules_version"}} } return nil } // Check that all references in expressions are valid func (bp *Blueprint) checkReferences() error { errs := Errors{} bp.visitDicts(func(dp dictPath, d *Dict) { isModSettings := IsModuleSettingsPath(dp) for k, v := range d.Items() { for ref, rp := range valueReferences(v) { path := dp.Dot(k).Cty(rp) if !ref.GlobalVar { if !isModSettings { errs.At(path, fmt.Errorf("module output %q can only be referenced in other module settings", ref)) } // module to module references are checked by validateModuleSettingReferences later return } if !bp.Vars.Has(ref.Name) { errs.At(path, fmt.Errorf("variable %q not found", ref.Name)) } } } }) return errs.OrNil() } // productOfModuleUseMark is a "mark" applied to values that are result of `use`. // Should not be used directly, use AsProductOfModuleUse and IsProductOfModuleUse instead. type productOfModuleUseMark struct { mods string } // AsProductOfModuleUse marks a value as a result of `use` of given modules. func AsProductOfModuleUse(v cty.Value, mods ...ModuleID) cty.Value { s := make([]string, len(mods)) for i, m := range mods { s[i] = string(m) } sort.Strings(s) return v.Mark(productOfModuleUseMark{strings.Join(s, ",")}) } // IsProductOfModuleUse returns list of modules that contributed (by `use`) to this value. func IsProductOfModuleUse(v cty.Value) []ModuleID { mark, marked := HasMark[productOfModuleUseMark](v) if !marked { return []ModuleID{} } s := strings.Split(mark.mods, ",") mods := make([]ModuleID, len(s)) for i, m := range s { mods[i] = ModuleID(m) } return mods } // WalkModules walks all modules in the blueprint and calls the walker function func (bp *Blueprint) WalkModules(walker func(ModulePath, *Module) error) error { for ig := range bp.Groups { g := &bp.Groups[ig] for im := range g.Modules { p := Root.Groups.At(ig).Modules.At(im) m := &g.Modules[im] if err := walker(p, m); err != nil { return err } } } return nil } func (bp *Blueprint) WalkModulesSafe(walker func(ModulePath, *Module)) { bp.WalkModules(func(p ModulePath, m *Module) error { walker(p, m) return nil }) } func (bp *Blueprint) visitStringLiterals(cb func(Path, string)) { cb(Root.BlueprintName, bp.BlueprintName) cb(Root.GhpcVersion, bp.GhpcVersion) for iv, v := range bp.Validators { cb(Root.Validators.At(iv).Validator, v.Validator) } vBackend := func(pbe backendPath, be *TerraformBackend) { cb(pbe.Type, be.Type) } vProviders := func(pps mapPath[providerPath], ps map[string]TerraformProvider) { for k, p := range ps { pp := pps.Dot(k) cb(pp, k) cb(pp.Source, p.Source) cb(pp.Version, p.Version) } } vBackend(Root.Backend, &bp.TerraformBackendDefaults) vProviders(Root.Provider, bp.TerraformProviders) for ig, g := range bp.Groups { pg := Root.Groups.At(ig) cb(pg.Name, string(g.Name)) vBackend(pg.Backend, &g.TerraformBackend) vProviders(pg.Provider, g.TerraformProviders) for im, m := range g.Modules { pm := pg.Modules.At(im) cb(pm.Source, m.Source) cb(pm.Kind, m.Kind.String()) cb(pm.ID, string(m.ID)) for iu, u := range m.Use { cb(pm.Use.At(iu), string(u)) } for io, o := range m.Outputs { po := pm.Outputs.At(io) cb(po.Name, o.Name) cb(po.Description, o.Description) } } } bp.visitDicts(func(dp dictPath, d *Dict) { for _, k := range d.Keys() { cb(dp.Dot(k), k) } }) } // validate every module setting in the blueprint containing a reference func validateModuleSettingReferences(p ModulePath, m Module, bp Blueprint) error { errs := Errors{} for k, v := range m.Settings.Items() { for r, rp := range valueReferences(v) { errs.At( p.Settings.Dot(k).Cty(rp), validateModuleSettingReference(bp, m, r)) } } return errs.OrNil() } func varsTopologicalOrder(vars Dict) ([]string, error) { // 0, 1, 2 - unvisited, on stack, exited used := map[string]int{} // default is 0 - unvisited res := []string{} // walk vars in reverse topological order var dfs func(string) error dfs = func(n string) error { used[n] = 1 // put on stack v := vars.Get(n) for ref, rp := range valueReferences(v) { p := Root.Vars.Dot(n).Cty(rp) if !ref.GlobalVar { continue } if used[ref.Name] == 1 { return BpError{p, fmt.Errorf("cyclic dependency detected: %q -> %q", v, ref)} } if used[ref.Name] == 0 { if err := dfs(ref.Name); err != nil { return err } } } used[n] = 2 // remove from stack and add to result res = append(res, n) return nil } for n := range vars.Items() { if used[n] == 0 { // unvisited if err := dfs(n); err != nil { return nil, err } } } return res, nil } func (bp *Blueprint) evalVars() (Dict, error) { order, err := varsTopologicalOrder(bp.Vars) if err != nil { return Dict{}, err } res := map[string]cty.Value{} ctx := hcl.EvalContext{ Variables: map[string]cty.Value{}, Functions: bp.functions()} for _, n := range order { ctx.Variables["var"] = cty.ObjectVal(res) ev, err := eval(bp.Vars.Get(n), &ctx) if err != nil { return Dict{}, BpError{Root.Vars.Dot(n), err} } res[n] = ev } return NewDict(res), nil }