mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
Prototype custom functions (and some other hacks)
```hcl
function "hw" {
description = "who to say hello to"
parameter "name" {
type = string
}
parameter "info" {
type = string
variadic = true
}
scratch {
message = "welcome to the World!"
value = "Hello ${param.name}, ${scratch.message}${join(".", param.info)}"
}
return {
value = scratch.value
}
}
output "callfn" {
value = module::hw("Jeoff", " This is a custom function", " It supports varargs")
}
```
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
@@ -39,11 +39,13 @@ type Function struct {
|
||||
const (
|
||||
FunctionNamespaceProvider = "provider"
|
||||
FunctionNamespaceCore = "core"
|
||||
FunctionNamespaceModule = "module"
|
||||
)
|
||||
|
||||
var FunctionNamespaces = []string{
|
||||
FunctionNamespaceProvider,
|
||||
FunctionNamespaceCore,
|
||||
FunctionNamespaceModule,
|
||||
}
|
||||
|
||||
func ParseFunction(input string) Function {
|
||||
|
||||
387
internal/configs/function_def.go
Normal file
387
internal/configs/function_def.go
Normal file
@@ -0,0 +1,387 @@
|
||||
// Copyright (c) The OpenTofu Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Copyright (c) 2023 HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/opentofu/opentofu/internal/lang/marks"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/function"
|
||||
)
|
||||
|
||||
type Function struct {
|
||||
Name string
|
||||
|
||||
Description string
|
||||
// Deprecated?
|
||||
|
||||
Parameters []*FunctionParameter
|
||||
Scratch map[string]*FunctionScratch
|
||||
Return FunctionReturn
|
||||
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
type FunctionParameter struct {
|
||||
Name string
|
||||
DeclRange hcl.Range
|
||||
|
||||
Type cty.Type
|
||||
Validations []*CheckRule
|
||||
|
||||
// Unsure at this point
|
||||
Sensitive bool
|
||||
Ephemeral bool
|
||||
|
||||
Nullable bool
|
||||
Variadic bool
|
||||
}
|
||||
|
||||
type FunctionScratch struct {
|
||||
Name string
|
||||
Expr hcl.Expression
|
||||
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
type FunctionReturn struct {
|
||||
Expr hcl.Expression
|
||||
|
||||
Sensitive bool
|
||||
|
||||
Preconditions []*CheckRule
|
||||
|
||||
DeclRange hcl.Range
|
||||
}
|
||||
|
||||
func decodeFunctionBlock(block *hcl.Block) (*Function, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
fn := &Function{
|
||||
Name: block.Labels[0],
|
||||
DeclRange: block.DefRange,
|
||||
|
||||
Scratch: map[string]*FunctionScratch{},
|
||||
}
|
||||
|
||||
if !hclsyntax.ValidIdentifier(fn.Name) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid function name",
|
||||
Detail: badIdentifierDetail,
|
||||
Subject: &block.LabelRanges[0],
|
||||
})
|
||||
}
|
||||
|
||||
content, moreDiags := block.Body.Content(functionBlockSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
if attr, ok := content.Attributes["description"]; ok {
|
||||
decodeDiags := gohcl.DecodeExpression(attr.Expr, nil, &fn.Description)
|
||||
diags = append(diags, decodeDiags...)
|
||||
}
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
switch block.Type {
|
||||
case "parameter":
|
||||
param := &FunctionParameter{
|
||||
Name: block.Labels[0],
|
||||
DeclRange: block.DefRange,
|
||||
}
|
||||
|
||||
content, moreDiags := block.Body.Content(functionParameterBlockSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
if attr, exists := content.Attributes["type"]; exists {
|
||||
ty, _, _, tyDiags := decodeVariableType(attr.Expr)
|
||||
diags = append(diags, tyDiags...)
|
||||
//param.ConstraintType = ty
|
||||
//param.Typeaults = tyaults
|
||||
param.Type = ty.WithoutOptionalAttributesDeep()
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["sensitive"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, ¶m.Sensitive)
|
||||
diags = append(diags, valDiags...)
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["ephemeral"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, ¶m.Ephemeral)
|
||||
diags = append(diags, valDiags...)
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["nullable"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, ¶m.Nullable)
|
||||
diags = append(diags, valDiags...)
|
||||
} else {
|
||||
// The current default is true, which is subject to change in a future
|
||||
// language edition.
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["variadic"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, ¶m.Variadic)
|
||||
diags = append(diags, valDiags...)
|
||||
}
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
switch block.Type {
|
||||
|
||||
case "validation":
|
||||
vv, moreDiags := decodeVariableValidationBlock(param.Name, block, false)
|
||||
diags = append(diags, moreDiags...)
|
||||
param.Validations = append(param.Validations, vv)
|
||||
|
||||
default:
|
||||
// The above cases should be exhaustive for all block types
|
||||
// defined in functionReturnBlockSchema
|
||||
panic(fmt.Sprintf("unhandled block type %q", block.Type))
|
||||
}
|
||||
}
|
||||
|
||||
fn.Parameters = append(fn.Parameters, param)
|
||||
case "scratch":
|
||||
attrs, moreDiags := block.Body.JustAttributes()
|
||||
diags = append(diags, moreDiags...)
|
||||
for name, attr := range attrs {
|
||||
if !hclsyntax.ValidIdentifier(name) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid scratch value name",
|
||||
Detail: badIdentifierDetail,
|
||||
Subject: &attr.NameRange,
|
||||
})
|
||||
}
|
||||
|
||||
if _, ok := fn.Scratch[name]; ok {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate scratch entry",
|
||||
Detail: fmt.Sprintf("The function block already has a scratch entry named %s.", name),
|
||||
Subject: &block.DefRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
fn.Scratch[name] = &FunctionScratch{
|
||||
Name: name,
|
||||
Expr: attr.Expr,
|
||||
DeclRange: attr.Range,
|
||||
}
|
||||
}
|
||||
case "return":
|
||||
if !fn.Return.DeclRange.Empty() {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate return block",
|
||||
Detail: fmt.Sprintf("The function block already has a return block at %s.", fn.Return.DeclRange),
|
||||
Subject: &block.DefRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
content, moreDiags := block.Body.Content(functionReturnBlockSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
fn.Return.Expr = content.Attributes["value"].Expr
|
||||
fn.Return.DeclRange = block.DefRange
|
||||
|
||||
if attr, ok := content.Attributes["sensitive"]; ok {
|
||||
decodeDiags := gohcl.DecodeExpression(attr.Expr, nil, &fn.Return.Sensitive)
|
||||
diags = append(diags, decodeDiags...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fn.Return.DeclRange.Empty() {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Missing return block",
|
||||
Detail: "The function block is missing a return block",
|
||||
Subject: &block.DefRange,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO sort params by decl order
|
||||
|
||||
return fn, diags
|
||||
}
|
||||
|
||||
func (fn *Function) Implementation(parentCtx *hcl.EvalContext) (function.Function, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
spec := &function.Spec{
|
||||
Description: fn.Description,
|
||||
Type: function.StaticReturnType(cty.DynamicPseudoType), // I don't know if we care to try to make this smarter
|
||||
}
|
||||
|
||||
for i, param := range fn.Parameters {
|
||||
entry := function.Parameter{
|
||||
Name: param.Name,
|
||||
//Description: param.Description,
|
||||
Type: param.Type,
|
||||
AllowNull: param.Nullable,
|
||||
AllowUnknown: true,
|
||||
AllowDynamicType: true,
|
||||
AllowMarked: true,
|
||||
}
|
||||
|
||||
if param.Variadic {
|
||||
if i != len(fn.Parameters)-1 {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid parameter",
|
||||
Detail: "Variadic parameters must be the final parameter defined in a function",
|
||||
Subject: &fn.DeclRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
spec.VarParam = &entry
|
||||
} else {
|
||||
|
||||
spec.Params = append(spec.Params, entry)
|
||||
}
|
||||
}
|
||||
|
||||
spec.Impl = func(args []cty.Value, retType cty.Type) (cty.Value, error) {
|
||||
hclCtx := parentCtx.NewChild()
|
||||
hclCtx.Variables = map[string]cty.Value{}
|
||||
|
||||
paramMap := map[string]cty.Value{}
|
||||
for i, param := range spec.Params {
|
||||
paramMap[param.Name] = args[i]
|
||||
}
|
||||
if spec.VarParam != nil {
|
||||
// TODO check indexes
|
||||
paramMap[spec.VarParam.Name] = cty.TupleVal(args[len(spec.Params):])
|
||||
}
|
||||
hclCtx.Variables["param"] = cty.ObjectVal(paramMap)
|
||||
|
||||
// Mini Graph Time!
|
||||
var stack []string
|
||||
scratch := map[string]cty.Value{}
|
||||
|
||||
var addScratch func(*FunctionScratch)
|
||||
addScratch = func(entry *FunctionScratch) {
|
||||
if _, ok := scratch[entry.Name]; ok {
|
||||
// Already added
|
||||
return
|
||||
}
|
||||
if slices.Contains(stack, entry.Name) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Circular scratch dependency",
|
||||
Detail: strings.Join(append(stack, entry.Name), ", "),
|
||||
Subject: &entry.DeclRange,
|
||||
})
|
||||
scratch[entry.Name] = cty.NilVal
|
||||
return
|
||||
}
|
||||
// Push
|
||||
stack = append(stack, entry.Name)
|
||||
// Pop
|
||||
defer func() {
|
||||
stack = stack[:len(stack)-1]
|
||||
}()
|
||||
|
||||
for _, v := range entry.Expr.Variables() {
|
||||
if v.RootName() == "scratch" {
|
||||
if len(v) < 1 {
|
||||
panic("booo")
|
||||
}
|
||||
attr, ok := v[1].(hcl.TraverseAttr)
|
||||
if !ok {
|
||||
// Handle error elsewhere
|
||||
continue
|
||||
}
|
||||
dep, ok := fn.Scratch[attr.Name]
|
||||
if !ok {
|
||||
// Handle error elsewhere
|
||||
continue
|
||||
}
|
||||
addScratch(dep)
|
||||
}
|
||||
}
|
||||
|
||||
val, valDiags := entry.Expr.Value(hclCtx)
|
||||
diags = append(diags, valDiags...)
|
||||
|
||||
scratch[entry.Name] = val
|
||||
|
||||
hclCtx.Variables["scratch"] = cty.ObjectVal(scratch)
|
||||
}
|
||||
|
||||
for _, scratch := range fn.Scratch {
|
||||
addScratch(scratch)
|
||||
}
|
||||
|
||||
// TODO preconditions
|
||||
// TODO provider functions?
|
||||
val, valDiags := fn.Return.Expr.Value(hclCtx)
|
||||
diags = append(diags, valDiags...)
|
||||
if fn.Return.Sensitive {
|
||||
val = val.Mark(marks.Sensitive)
|
||||
}
|
||||
if diags.HasErrors() {
|
||||
return val, diags
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return function.New(spec), diags
|
||||
}
|
||||
|
||||
var functionBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{Name: "description"},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: "parameter", LabelNames: []string{"name"}},
|
||||
{Type: "scratch"},
|
||||
{Type: "return"},
|
||||
},
|
||||
}
|
||||
var functionParameterBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "type",
|
||||
},
|
||||
{
|
||||
Name: "sensitive",
|
||||
},
|
||||
{
|
||||
Name: "ephemeral",
|
||||
},
|
||||
{
|
||||
Name: "nullable",
|
||||
},
|
||||
{
|
||||
Name: "variadic",
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "validation",
|
||||
},
|
||||
},
|
||||
}
|
||||
var functionReturnBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "value",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Name: "sensitive",
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{Type: "precondition"},
|
||||
},
|
||||
}
|
||||
@@ -62,6 +62,8 @@ type Module struct {
|
||||
|
||||
Tests map[string]*TestFile
|
||||
|
||||
CustomFunctions map[string]*Function
|
||||
|
||||
// IsOverridden indicates if the module is being overridden. It's used in
|
||||
// testing framework to not call the underlying module.
|
||||
IsOverridden bool
|
||||
@@ -115,6 +117,9 @@ type File struct {
|
||||
Removed []*Removed
|
||||
|
||||
Checks []*Check
|
||||
|
||||
ModuleDefs []*ModuleDef
|
||||
Functions []*Function
|
||||
}
|
||||
|
||||
// SelectiveLoader allows the consumer to only load and validate the portions of files needed for the given operations/contexts
|
||||
@@ -182,6 +187,7 @@ func NewModuleUneval(primaryFiles, overrideFiles []*File, sourceDir string, load
|
||||
DataResources: map[string]*Resource{},
|
||||
EphemeralResources: map[string]*Resource{},
|
||||
Checks: map[string]*Check{},
|
||||
CustomFunctions: map[string]*Function{},
|
||||
ProviderMetas: map[addrs.Provider]*ProviderMeta{},
|
||||
Tests: map[string]*TestFile{},
|
||||
SourceDir: sourceDir,
|
||||
@@ -596,6 +602,19 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics {
|
||||
|
||||
m.Removed = append(m.Removed, file.Removed...)
|
||||
|
||||
for _, fn := range file.Functions {
|
||||
if existing, exists := m.CustomFunctions[fn.Name]; exists {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicate function",
|
||||
Detail: fmt.Sprintf("A function named %q was already defined at %s. Functions must have unique names within a module.", existing.Name, existing.DeclRange),
|
||||
Subject: &fn.DeclRange,
|
||||
})
|
||||
continue
|
||||
}
|
||||
m.CustomFunctions[fn.Name] = fn
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
|
||||
12
internal/configs/module_def.go
Normal file
12
internal/configs/module_def.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package configs
|
||||
|
||||
import "github.com/hashicorp/hcl/v2"
|
||||
|
||||
type ModuleDef struct {
|
||||
Name string
|
||||
Contents *Module
|
||||
}
|
||||
|
||||
func decodeModuleDefBlock(block *hcl.Block) (*ModuleDef, hcl.Diagnostics) {
|
||||
return nil, nil
|
||||
}
|
||||
18
internal/configs/module_meta.go
Normal file
18
internal/configs/module_meta.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package configs
|
||||
|
||||
type ModuleAccessSafety string
|
||||
|
||||
const (
|
||||
ModuleAccessSafeModule = ModuleAccessSafety("module")
|
||||
ModuleAccessSafeTree = ModuleAccessSafety("tree")
|
||||
ModuleAccessUnsafe = ModuleAccessSafety("unsafe")
|
||||
)
|
||||
|
||||
type ModuleMeta struct {
|
||||
Access *ModuleAccessSafety
|
||||
}
|
||||
|
||||
type ModulePackageMeta struct {
|
||||
DefaultAccess *ModuleAccessSafety
|
||||
OverrideAccess map[string]*ModuleAccessSafety
|
||||
}
|
||||
@@ -223,6 +223,26 @@ func loadConfigFileBody(body hcl.Body, filename string, override bool, allowExpe
|
||||
if cfg != nil {
|
||||
file.Removed = append(file.Removed, cfg)
|
||||
}
|
||||
case "define":
|
||||
if len(block.Labels) < 2 {
|
||||
panic("TODO diags")
|
||||
}
|
||||
switch block.Labels[0] {
|
||||
case "module":
|
||||
cfg, cfgDiags := decodeModuleDefBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.ModuleDefs = append(file.ModuleDefs, cfg)
|
||||
}
|
||||
default:
|
||||
panic("TODO diags")
|
||||
}
|
||||
case "function":
|
||||
cfg, cfgDiags := decodeFunctionBlock(block)
|
||||
diags = append(diags, cfgDiags...)
|
||||
if cfg != nil {
|
||||
file.Functions = append(file.Functions, cfg)
|
||||
}
|
||||
|
||||
default:
|
||||
// Should never happen because the above cases should be exhaustive
|
||||
@@ -328,6 +348,14 @@ var configFileSchema = &hcl.BodySchema{
|
||||
{
|
||||
Type: "removed",
|
||||
},
|
||||
{
|
||||
Type: "define",
|
||||
LabelNames: []string{"type", "name"},
|
||||
},
|
||||
{
|
||||
Type: "function",
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -348,6 +348,13 @@ func (s *Scope) evalContext(ctx context.Context, parent *hcl.EvalContext, refs [
|
||||
// provider-defined functions below.
|
||||
maps.Copy(hclCtx.Functions, s.Functions())
|
||||
|
||||
for name, fn := range s.CustomFunctions {
|
||||
// TODO recursion limitations
|
||||
impl, fnDiags := fn.Implementation(hclCtx)
|
||||
diags = diags.Append(fnDiags)
|
||||
hclCtx.Functions[addrs.FunctionNamespaceModule+"::"+name] = impl
|
||||
}
|
||||
|
||||
// Easy path for common case where there are no references at all.
|
||||
if len(refs) == 0 {
|
||||
return hclCtx, diags
|
||||
|
||||
@@ -74,9 +74,14 @@ type Scope struct {
|
||||
PlanTimestamp time.Time
|
||||
|
||||
ProviderFunctions ProviderFunction
|
||||
|
||||
CustomFunctions map[string]CustomFunction
|
||||
}
|
||||
|
||||
type ProviderFunction func(context.Context, addrs.ProviderFunction, tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics)
|
||||
type CustomFunction interface {
|
||||
Implementation(*hcl.EvalContext) (function.Function, hcl.Diagnostics)
|
||||
}
|
||||
|
||||
// SetActiveExperiments allows a caller to declare that a set of experiments
|
||||
// is active for the module that the receiving Scope belongs to, which might
|
||||
|
||||
@@ -454,10 +454,15 @@ func (c *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source ad
|
||||
mc := c.Evaluator.Config.DescendentForInstance(c.PathValue)
|
||||
|
||||
if mc == nil || mc.Module.ProviderRequirements == nil {
|
||||
return c.Evaluator.Scope(data, self, source, nil)
|
||||
return c.Evaluator.Scope(data, self, source, nil, nil)
|
||||
}
|
||||
|
||||
scope := c.Evaluator.Scope(data, self, source, func(ctx context.Context, pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) {
|
||||
crap := map[string]lang.CustomFunction{}
|
||||
for name, fn := range mc.Module.CustomFunctions {
|
||||
crap[name] = fn
|
||||
}
|
||||
|
||||
scope := c.Evaluator.Scope(data, self, source, crap, func(ctx context.Context, pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) {
|
||||
providedBy, ok := c.ProviderFunctionTracker.Lookup(c.PathValue.Module(), pf)
|
||||
if !ok {
|
||||
// This should not be possible if references are tracked correctly
|
||||
|
||||
@@ -77,7 +77,7 @@ type Evaluator struct {
|
||||
// If the "self" argument is nil then the "self" object is not available
|
||||
// in evaluated expressions. Otherwise, it behaves as an alias for the given
|
||||
// address.
|
||||
func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs.Referenceable, functions lang.ProviderFunction) *lang.Scope {
|
||||
func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs.Referenceable, customFunctions map[string]lang.CustomFunction, providerFuncs lang.ProviderFunction) *lang.Scope {
|
||||
return &lang.Scope{
|
||||
Data: data,
|
||||
ParseRef: addrs.ParseRef,
|
||||
@@ -86,7 +86,8 @@ func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs
|
||||
PureOnly: e.Operation != walkApply && e.Operation != walkDestroy && e.Operation != walkEval,
|
||||
BaseDir: ".", // Always current working directory for now.
|
||||
PlanTimestamp: e.PlanTimestamp,
|
||||
ProviderFunctions: functions,
|
||||
ProviderFunctions: providerFuncs,
|
||||
CustomFunctions: customFunctions,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user