Resource references in depdency mods must be fully qualified. Locals in dependency mods cannot be referenced. Support transitive dependencies referencing variables from different versions of same mod. Closes #3335. Closes #3336. Closes #3337

This commit is contained in:
kaidaguerre
2023-04-14 12:37:32 +01:00
committed by GitHub
parent dca9f594c2
commit 137e438bdc
10 changed files with 155 additions and 101 deletions

View File

@@ -57,8 +57,8 @@ func NewInitData(ctx context.Context) *InitData {
return i
}
if len(w.GetResourceMaps().Controls) == 0 {
i.Result.AddWarnings("no controls found in current workspace")
if len(w.GetResourceMaps().Controls)+len(w.GetResourceMaps().Benchmarks) == 0 {
i.Result.AddWarnings("no controls or benchmarks found in current workspace")
}
if err := controldisplay.EnsureTemplates(); err != nil {

View File

@@ -9,7 +9,7 @@ import (
)
// GetReferencedVariables builds map of variables values containing only those mod variables which are referenced
// NOTE: we refer to variables in depdencyt mods in the format which is valid for an SPVARS filer, i.e.
// NOTE: we refer to variables in dependency mods in the format which is valid for an SPVARS filer, i.e.
// <mod>.<var-name>
// the VariableValues map will contain these variables with the name format <mod>.var.<var-name>,
// so we must convert the name
@@ -22,7 +22,8 @@ func GetReferencedVariables(root dashboardtypes.DashboardTreeRun, w *workspace.W
if len(parts) == 2 && parts[0] == "var" {
varName := parts[1]
varValueName := varName
// NOTE: if the ref is NOT for the workspace mod, then use the fully qualifed name
// NOTE: if the ref is NOT for the workspace mod, then use the qualified variable name
// (e.g. aws_insights.var.v1)
if refMod := ref.GetMetadata().ModName; refMod != w.Mod.ShortName {
varValueName = fmt.Sprintf("%s.var.%s", refMod, varName)
varName = fmt.Sprintf("%s.%s", refMod, varName)

View File

@@ -38,12 +38,14 @@ func LoadMod(modPath string, parseCtx *parse.ModParseContext, opts ...LoadModOpt
o(mod)
}
// set the current mod on the run context
parseCtx.SetCurrentMod(mod)
// load the mod dependencies
if err := loadModDependencies(mod, parseCtx); err != nil {
return nil, modconfig.NewErrorsAndWarning(err)
}
// now we have loaded dependencies, set the current mod on the run context
parseCtx.CurrentMod = mod
// populate the resource maps of the current mod using the dependency mods
mod.ResourceMaps = parseCtx.GetResourceMaps()
// now load the mod resource hcl
@@ -64,12 +66,6 @@ func loadModDefinition(modPath string, parseCtx *parse.ModParseContext) (*modcon
if err != nil {
return nil, err
}
// now we have loaded the mod, if this is a dependency mod, add in any variables we have loaded
if parseCtx.ParentParseCtx != nil {
parseCtx.Variables = parseCtx.ParentParseCtx.DependencyVariables[mod.ShortName]
parseCtx.SetVariablesForDependencyMod(mod, parseCtx.ParentParseCtx.DependencyVariables)
}
} else {
// so there is no mod file - should we create a default?
if !parseCtx.ShouldCreateDefaultMod() {
@@ -124,7 +120,7 @@ func loadModDependency(modDependency *modconfig.ModVersionConstraint, parseCtx *
parentFolder := filepath.Dir(filepath.Join(parseCtx.WorkspaceLock.ModInstallationPath, modDependency.Name))
// search the parent folder for a mod installation which satisfied the given mod dependency
dependencyPath, version, err := findInstalledDependency(modDependency, parentFolder)
dependencyDir, version, err := findInstalledDependency(modDependency, parentFolder)
if err != nil {
return err
}
@@ -134,22 +130,9 @@ func loadModDependency(modDependency *modconfig.ModVersionConstraint, parseCtx *
parseCtx.ListOptions.Exclude = nil
defer func() { parseCtx.ListOptions.Exclude = prevExclusions }()
// create a child run context
childRunCtx := parse.NewModParseContext(
parseCtx.WorkspaceLock,
dependencyPath,
parse.CreatePseudoResources,
&filehelpers.ListOptions{
// listFlag specifies whether to load files recursively
Flags: filehelpers.FilesRecursive,
// only load .sp files
Include: filehelpers.InclusionsFromExtensions([]string{constants.ModDataExtension}),
})
childRunCtx.BlockTypes = parseCtx.BlockTypes
childRunCtx.ParentParseCtx = parseCtx
childParseCtx := parse.NewChildModParseContext(parseCtx, dependencyDir)
// NOTE: pass in the version and dependency path of the mod - these must be set before it loads its depdencies
mod, errAndWarnings := LoadMod(dependencyPath, childRunCtx, WithDependencyConfig(modDependency.Name, version))
mod, errAndWarnings := LoadMod(dependencyDir, childParseCtx, WithDependencyConfig(modDependency.Name, version))
if errAndWarnings.GetError() != nil {
return errAndWarnings.GetError()
}
@@ -158,6 +141,8 @@ func loadModDependency(modDependency *modconfig.ModVersionConstraint, parseCtx *
parseCtx.AddLoadedDependencyMod(mod)
if parseCtx.ParentParseCtx != nil {
parseCtx.ParentParseCtx.AddLoadedDependencyMod(mod)
// add mod resources to parent parse context
parseCtx.ParentParseCtx.AddModResources(mod)
}
return nil
@@ -191,13 +176,6 @@ func loadModResources(modPath string, parseCtx *parse.ModParseContext) (*modconf
// parse all hcl files (NOTE - this reads the CurrentMod out of ParseContext and adds to it)
mod, errAndWarnings := parse.ParseMod(fileData, pseudoResources, parseCtx)
if errAndWarnings.GetError() == nil {
// now add fully populated mod to the parent run context
if parseCtx.ParentParseCtx != nil {
parseCtx.ParentParseCtx.CurrentMod = mod
parseCtx.ParentParseCtx.AddMod(mod)
}
}
return mod, errAndWarnings
}

View File

@@ -23,7 +23,7 @@ func LoadVariableDefinitions(variablePath string, parseCtx *parse.ModParseContex
return nil, errAndWarnings.GetError()
}
variableMap := modconfig.NewModVariableMap(mod, parseCtx.GetTopLevelDependencyMods())
variableMap := modconfig.NewModVariableMap(mod, parseCtx.LoadedDependencyMods)
return variableMap, nil
}
@@ -56,9 +56,6 @@ func GetVariableValues(ctx context.Context, parseCtx *parse.ModParseContext, var
}
}
// add workspace mod variables to runContext
parseCtx.AddInputVariables(variableMap)
return variableMap, nil
}

View File

@@ -29,11 +29,11 @@ func NewModVariableMap(mod *Mod, dependencyMods ModMap) *ModVariableMap {
m.RootVariables[buildVariableMapKey(k)] = v
}
// now add variables from dependency mods
for _, mod := range dependencyMods {
for dependencyPath, mod := range dependencyMods {
// add variables into map, modifying the key to be the variable short name
m.DependencyVariables[mod.ShortName] = make(map[string]*Variable)
m.DependencyVariables[dependencyPath] = make(map[string]*Variable)
for k, v := range mod.ResourceMaps.Variables {
m.DependencyVariables[mod.ShortName][buildVariableMapKey(k)] = v
m.DependencyVariables[dependencyPath][buildVariableMapKey(k)] = v
}
}
// build map of all variables

View File

@@ -43,7 +43,7 @@ type ModParseContext struct {
ListOptions *filehelpers.ListOptions
// map of loaded dependency mods, keyed by DependencyPath (including version)
// there may be multiple versions of same mod in this map
loadedDependencyMods modconfig.ModMap
LoadedDependencyMods modconfig.ModMap
// Variables are populated in an initial parse pass top we store them on the run context
// so we can set them on the mod when we do the main parse
@@ -53,7 +53,8 @@ type ModParseContext struct {
Variables map[string]*modconfig.Variable
// DependencyVariables is a map of the variables in the dependency mods of the current mod
// it is used to populate the variables property on the dependency
// it is used to populate the variables values on child parseContexts when parsing dependencies
// (keyed by mod DependencyPath)
DependencyVariables map[string]map[string]*modconfig.Variable
ParentParseCtx *ModParseContext
@@ -68,9 +69,10 @@ type ModParseContext struct {
topLevelBlocks map[*hcl.Block]struct{}
// map of block names, keyed by a hash of the blopck
blockNameMap map[string]string
// map of ReferenceTypeValueMaps keyed by mod
// map of ReferenceTypeValueMaps keyed by mod name
// NOTE: all values from root mod are keyed with "local"
referenceValues map[string]ReferenceTypeValueMap
// a map of just the top level dependencies of the CurrentMod, keyed my full mod DepdencyName (with no version)
topLevelDependencyMods modconfig.ModMap
}
@@ -82,11 +84,11 @@ func NewModParseContext(workspaceLock *versionmap.WorkspaceLock, rootEvalPath st
Flags: flags,
WorkspaceLock: workspaceLock,
ListOptions: listOptions,
loadedDependencyMods: make(modconfig.ModMap),
LoadedDependencyMods: make(modconfig.ModMap),
blockChildMap: make(map[string][]string),
blockNameMap: make(map[string]string),
// initialise variable maps - even though we later overwrite them
// initialise reference maps - even though we later overwrite them
Variables: make(map[string]*modconfig.Variable),
referenceValues: map[string]ReferenceTypeValueMap{
"local": make(ReferenceTypeValueMap),
@@ -99,6 +101,23 @@ func NewModParseContext(workspaceLock *versionmap.WorkspaceLock, rootEvalPath st
return c
}
func NewChildModParseContext(parent *ModParseContext, rootEvalPath string) *ModParseContext {
// create a child run context
child := NewModParseContext(
parent.WorkspaceLock,
rootEvalPath,
parent.Flags,
parent.ListOptions)
// copy our block tpyes
child.BlockTypes = parent.BlockTypes
// set the child's parent
child.ParentParseCtx = parent
// copy DependencyVariables
child.DependencyVariables = parent.DependencyVariables
return child
}
func (m *ModParseContext) EnsureWorkspaceLock(mod *modconfig.Mod) error {
// if the mod has dependencies, there must a workspace lock object in the run context
// (mod MUST be the workspace mod, not a dependency, as we would hit this error as soon as we parse it)
@@ -127,8 +146,8 @@ func (m *ModParseContext) PeekParent() string {
return m.parents[len(m.parents)-1]
}
// VariableValueMap converts a map of variables to a map of the underlying cty value
func VariableValueMap(variables map[string]*modconfig.Variable) map[string]cty.Value {
// VariableValueCtyMap converts a map of variables to a map of the underlying cty value
func VariableValueCtyMap(variables map[string]*modconfig.Variable) map[string]cty.Value {
ret := make(map[string]cty.Value, len(variables))
for k, v := range variables {
ret[k] = v.Value
@@ -139,57 +158,77 @@ func VariableValueMap(variables map[string]*modconfig.Variable) map[string]cty.V
// AddInputVariables adds variables to the run context.
// This function is called for the root run context after loading all input variables
func (m *ModParseContext) AddInputVariables(inputVariables *modconfig.ModVariableMap) {
m.setRootVariables(inputVariables.RootVariables)
m.setDependencyVariables(inputVariables.DependencyVariables)
// store the variables
m.Variables = inputVariables.RootVariables
// store the depdency variables sop we can pass them down to our children
m.DependencyVariables = inputVariables.DependencyVariables
}
// SetVariablesForDependencyMod adds variables to the run context.
// This function is called for dependent mod run context
func (m *ModParseContext) SetVariablesForDependencyMod(mod *modconfig.Mod, dependencyVariablesMap map[string]map[string]*modconfig.Variable) {
m.setRootVariables(dependencyVariablesMap[mod.ShortName])
m.setDependencyVariables(dependencyVariablesMap)
func (m *ModParseContext) AddVariablesToReferenceMap() {
m.addRootVariablesToReferenceMap(m.Variables)
m.addDependencyVariablesToReferenceMap()
// NOTE: we do not rebuild the eval context here as in practice, buildEvalContext will be called after the
// mod definition is parsed
}
// setRootVariables sets the Variables property
// addRootVariablesToReferenceMap sets the Variables property
// and adds the variables to the referenceValues map (used to build the eval context)
func (m *ModParseContext) setRootVariables(variables map[string]*modconfig.Variable) {
m.Variables = variables
func (m *ModParseContext) addRootVariablesToReferenceMap(variables map[string]*modconfig.Variable) {
// write local variables directly into referenceValues map
// NOTE: we add with the name "var" not "variable" as that is how variables are referenced
m.referenceValues["local"]["var"] = VariableValueMap(variables)
m.referenceValues["local"]["var"] = VariableValueCtyMap(variables)
}
// setDependencyVariables sets the DependencyVariables property
// and adds the dependency variables to the referenceValues map (used to build the eval context
func (m *ModParseContext) setDependencyVariables(dependencyVariables map[string]map[string]*modconfig.Variable) {
m.DependencyVariables = dependencyVariables
// addDependencyVariablesToReferenceMap sets the DependencyVariables property
// and adds the dependency variables to the referenceValues map (used to build the eval context)
func (m *ModParseContext) addDependencyVariablesToReferenceMap() {
currentModKey := m.CurrentMod.GetInstallCacheKey()
topLevelDependencies := m.WorkspaceLock.InstallCache[currentModKey]
// convert topLevelDependencies into as map keyed by depdency path
topLevelDependencyPathMap := topLevelDependencies.ToDependencyPathMap()
// NOTE: we add with the name "var" not "variable" as that is how variables are referenced
// add top level variables
// add dependency mod variables, scoped by mod name
// add dependency mod variables to dependencyVariableValues, scoped by DependencyPath
for depModName, depVars := range m.DependencyVariables {
// create map for this dependency if needed
if m.referenceValues[depModName] == nil {
m.referenceValues[depModName] = make(ReferenceTypeValueMap)
// only add variables from top level dependencies
if _, ok := topLevelDependencyPathMap[depModName]; ok {
// create map for this dependency if needed
alias := topLevelDependencyPathMap[depModName]
if m.referenceValues[alias] == nil {
m.referenceValues[alias] = make(ReferenceTypeValueMap)
}
m.referenceValues[alias]["var"] = VariableValueCtyMap(depVars)
}
m.referenceValues[depModName]["var"] = VariableValueMap(depVars)
}
}
// AddMod is used to add a mod to the eval context
func (m *ModParseContext) AddMod(mod *modconfig.Mod) hcl.Diagnostics {
// AddModResources is used to add mod resources to the eval context
func (m *ModParseContext) AddModResources(mod *modconfig.Mod) hcl.Diagnostics {
if len(m.UnresolvedBlocks) > 0 {
// should never happen
panic("calling SetContent on runContext but there are unresolved blocks from a previous parse")
panic("calling AddModResources on ModParseContext but there are unresolved blocks from a previous parse")
}
var diags hcl.Diagnostics
moreDiags := m.storeResourceInCtyMap(mod)
moreDiags := m.storeResourceInReferenceValueMap(mod)
diags = append(diags, moreDiags...)
// do not add variables (as they have already been added)
// if the resource is for a dependency mod, do not add locals
shouldAdd := func(item modconfig.HclResource) bool {
if item.BlockType() == modconfig.BlockTypeVariable ||
item.BlockType() == modconfig.BlockTypeLocals && item.(modconfig.ModTreeItem).GetMod().ShortName != m.CurrentMod.ShortName {
return false
}
return true
}
resourceFunc := func(item modconfig.HclResource) (bool, error) {
// add all mod resources except variables into cty map
if _, ok := item.(*modconfig.Variable); !ok {
moreDiags := m.storeResourceInCtyMap(item)
// add all mod resources (except those excluded) into cty map
if shouldAdd(item) {
moreDiags := m.storeResourceInReferenceValueMap(item)
diags = append(diags, moreDiags...)
}
// continue walking
@@ -234,9 +273,9 @@ func (m *ModParseContext) CreatePseudoResources() bool {
return m.Flags&CreatePseudoResources == CreatePseudoResources
}
// AddResource stores this resource as a variable to be added to the eval context. It alse
// AddResource stores this resource as a variable to be added to the eval context.
func (m *ModParseContext) AddResource(resource modconfig.HclResource) hcl.Diagnostics {
diagnostics := m.storeResourceInCtyMap(resource)
diagnostics := m.storeResourceInReferenceValueMap(resource)
if diagnostics.HasErrors() {
return diagnostics
}
@@ -258,7 +297,7 @@ func (m *ModParseContext) GetMod(modShortName string) *modconfig.Mod {
key := m.CurrentMod.GetInstallCacheKey()
deps := m.WorkspaceLock.InstallCache[key]
for _, dep := range deps {
depMod, ok := m.loadedDependencyMods[dep.DependencyPath()]
depMod, ok := m.LoadedDependencyMods[dep.DependencyPath()]
if ok && depMod.ShortName == modShortName {
return depMod
}
@@ -287,16 +326,16 @@ func (m *ModParseContext) GetResource(parsedName *modconfig.ParsedResourceName)
return m.GetResourceMaps().GetResource(parsedName)
}
// eval functions
// build the eval context from the cached reference values
func (m *ModParseContext) buildEvalContext() {
// convert variables to cty values
variables := make(map[string]cty.Value)
// convert reference values to cty objects
referenceValues := make(map[string]cty.Value)
// now for each mod add all the values
for mod, modMap := range m.referenceValues {
if mod == "local" {
for k, v := range modMap {
variables[k] = cty.ObjectVal(v)
referenceValues[k] = cty.ObjectVal(v)
}
continue
}
@@ -304,18 +343,25 @@ func (m *ModParseContext) buildEvalContext() {
// mod map is map[string]map[string]cty.Value
// for each element (i.e. map[string]cty.Value) convert to cty object
refTypeMap := make(map[string]cty.Value)
for refType, typeValueMap := range modMap {
refTypeMap[refType] = cty.ObjectVal(typeValueMap)
if mod == "local" {
for k, v := range modMap {
referenceValues[k] = cty.ObjectVal(v)
}
} else {
for refType, typeValueMap := range modMap {
refTypeMap[refType] = cty.ObjectVal(typeValueMap)
}
}
// now convert the cty map to a cty object
variables[mod] = cty.ObjectVal(refTypeMap)
// now convert the referenceValues itself to a cty object
referenceValues[mod] = cty.ObjectVal(refTypeMap)
}
m.ParseContext.buildEvalContext(variables)
// rebuild the eval context
m.ParseContext.buildEvalContext(referenceValues)
}
// update the cached cty value for the given resource, as long as itr does not already exist
func (m *ModParseContext) storeResourceInCtyMap(resource modconfig.HclResource) hcl.Diagnostics {
// store the resource as a cty value in the reference valuemap
func (m *ModParseContext) storeResourceInReferenceValueMap(resource modconfig.HclResource) hcl.Diagnostics {
// add resource to variable map
ctyValue, diags := m.getResourceCtyValue(resource)
if diags.HasErrors() {
@@ -333,6 +379,7 @@ func (m *ModParseContext) storeResourceInCtyMap(resource modconfig.HclResource)
return nil
}
// convert a HclResource into a cty value, taking into account nested structs
func (m *ModParseContext) getResourceCtyValue(resource modconfig.HclResource) (cty.Value, hcl.Diagnostics) {
ctyValue, err := resource.(modconfig.CtyValueProvider).CtyValue()
if err != nil {
@@ -419,8 +466,16 @@ func (m *ModParseContext) addReferenceValue(resource modconfig.HclResource, valu
key := parsedName.Name
typeString := parsedName.ItemType
// the resource name will not have a mod - but the run context knows which mod we are parsing
mod := m.CurrentMod
// most resources will have a mod property - use this if available
var mod *modconfig.Mod
if modTreeItem, ok := resource.(modconfig.ModTreeItem); ok {
mod = modTreeItem.GetMod()
}
// fall back to current mod
if mod == nil {
mod = m.CurrentMod
}
modName := mod.ShortName
if mod.ModPath == m.RootEvalPath {
modName = "local"
@@ -466,7 +521,7 @@ func (m *ModParseContext) GetLoadedDependencyMod(requiredModVersion *modconfig.M
return nil, fmt.Errorf("not all dependencies are installed - run 'steampipe mod install'")
}
// use the full name of the locked version as key
d, _ := m.loadedDependencyMods[lockedVersion.FullName()]
d, _ := m.LoadedDependencyMods[lockedVersion.FullName()]
return d, nil
}
@@ -475,7 +530,7 @@ func (m *ModParseContext) AddLoadedDependencyMod(mod *modconfig.Mod) {
if mod.DependencyPath == nil {
return
}
m.loadedDependencyMods[*mod.DependencyPath] = mod
m.LoadedDependencyMods[*mod.DependencyPath] = mod
}
// GetTopLevelDependencyMods build a mod map of top level loaded dependencies, keyed by mod name
@@ -492,7 +547,7 @@ func (m *ModParseContext) GetTopLevelDependencyMods() modconfig.ModMap {
// merge in the dependency mods
for _, dep := range deps {
key := dep.DependencyPath()
loadedDepMod := m.loadedDependencyMods[key]
loadedDepMod := m.LoadedDependencyMods[key]
if loadedDepMod != nil {
// as key use the ModDependencyPath _without_ the version
m.topLevelDependencyMods[loadedDepMod.DependencyName] = loadedDepMod
@@ -500,3 +555,17 @@ func (m *ModParseContext) GetTopLevelDependencyMods() modconfig.ModMap {
}
return m.topLevelDependencyMods
}
func (m *ModParseContext) SetCurrentMod(mod *modconfig.Mod) {
m.CurrentMod = mod
// if the current mod is a dependency mod (i.e. has a DependencyPath property set), update the Variables property
if dependencyVariables, ok := m.DependencyVariables[mod.GetInstallCacheKey()]; ok {
m.Variables = dependencyVariables
}
// set the root variables from the parent
// now the mod is set we can add variables to the reference map
// ( we cannot do this until mod as set as we need to identify which variables to use if we are a dependency
m.AddVariablesToReferenceMap()
m.buildEvalContext()
}

View File

@@ -177,7 +177,7 @@ func ParseMod(fileData map[string][]byte, pseudoResources []modconfig.MappableRe
return nil, modconfig.NewErrorsAndWarning(err)
}
// if variables were passed in runcontext, add to the mod
// if variables were passed in parsecontext, add to the mod
for _, v := range parseCtx.Variables {
if diags = mod.AddResource(v); diags.HasErrors() {
return nil, modconfig.NewErrorsAndWarning(plugin.DiagsToError("Failed to add resource to mod", diags))
@@ -192,7 +192,7 @@ func ParseMod(fileData map[string][]byte, pseudoResources []modconfig.MappableRe
// add the mod to the run context
// - this it to ensure all pseudo resources get added and build the eval context with the variables we just added
if diags = parseCtx.AddMod(mod); diags.HasErrors() {
if diags = parseCtx.AddModResources(mod); diags.HasErrors() {
return nil, modconfig.NewErrorsAndWarning(plugin.DiagsToError("Failed to add mod to run context", diags))
}
@@ -219,7 +219,7 @@ func ParseMod(fileData map[string][]byte, pseudoResources []modconfig.MappableRe
// if the number of unresolved blocks has NOT reduced, fail
if prevUnresolvedBlocks != 0 && unresolvedBlocks >= prevUnresolvedBlocks {
str := parseCtx.FormatDependencies()
return nil, modconfig.NewErrorsAndWarning(fmt.Errorf("failed to resolve mod dependencies after %d attempts\nDependencies:\n%s", attempts+1, str))
return nil, modconfig.NewErrorsAndWarning(fmt.Errorf("failed to resolve dependencies for mod '%s' after %d attempts\nDependencies:\n%s", mod.FullName, attempts+1, str))
}
// update prevUnresolvedBlocks
prevUnresolvedBlocks = unresolvedBlocks

View File

@@ -19,3 +19,12 @@ func (m ResolvedVersionMap) ToVersionListMap() ResolvedVersionListMap {
}
return res
}
// ToDependencyPathMap converts to a map of mod aliases, keyed by mod dependency path
func (m ResolvedVersionMap) ToDependencyPathMap() map[string]string {
res := make(map[string]string, len(m))
for _, c := range m {
res[c.DependencyPath()] = c.Alias
}
return res
}

View File

@@ -23,7 +23,7 @@ var steampipeVersion = "0.20.0"
// A pre-release marker for the version. If this is "" (empty string)
// then it means that it is a final release. Otherwise, this is a pre-release
// such as "dev" (in development), "beta", "rc1", etc.
var prerelease = "alpha.3"
var prerelease = "alpha.4"
// SteampipeVersion is an instance of semver.Version. This has the secondary
// benefit of verifying during tests and init time that our version is a

View File

@@ -259,7 +259,7 @@ func (w *Workspace) loadWorkspaceMod(ctx context.Context) *modconfig.ErrorAndWar
if err != nil {
return modconfig.NewErrorsAndWarning(err)
}
// add variables to runContext
// add variables
parseCtx.AddInputVariables(inputVariables)
// do not reload variables as we already have them
parseCtx.BlockTypeExclusions = []string{modconfig.BlockTypeVariable}