github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/lang/eval.go (about)

     1  package lang
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/eliastor/durgaform/internal/addrs"
     7  	"github.com/eliastor/durgaform/internal/configs/configschema"
     8  	"github.com/eliastor/durgaform/internal/instances"
     9  	"github.com/eliastor/durgaform/internal/lang/blocktoattr"
    10  	"github.com/eliastor/durgaform/internal/tfdiags"
    11  	"github.com/hashicorp/hcl/v2"
    12  	"github.com/hashicorp/hcl/v2/ext/dynblock"
    13  	"github.com/hashicorp/hcl/v2/hcldec"
    14  	"github.com/zclconf/go-cty/cty"
    15  	"github.com/zclconf/go-cty/cty/convert"
    16  )
    17  
    18  // ExpandBlock expands any "dynamic" blocks present in the given body. The
    19  // result is a body with those blocks expanded, ready to be evaluated with
    20  // EvalBlock.
    21  //
    22  // If the returned diagnostics contains errors then the result may be
    23  // incomplete or invalid.
    24  func (s *Scope) ExpandBlock(body hcl.Body, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) {
    25  	spec := schema.DecoderSpec()
    26  
    27  	traversals := dynblock.ExpandVariablesHCLDec(body, spec)
    28  	refs, diags := References(traversals)
    29  
    30  	ctx, ctxDiags := s.EvalContext(refs)
    31  	diags = diags.Append(ctxDiags)
    32  
    33  	return dynblock.Expand(body, ctx), diags
    34  }
    35  
    36  // EvalBlock evaluates the given body using the given block schema and returns
    37  // a cty object value representing its contents. The type of the result conforms
    38  // to the implied type of the given schema.
    39  //
    40  // This function does not automatically expand "dynamic" blocks within the
    41  // body. If that is desired, first call the ExpandBlock method to obtain
    42  // an expanded body to pass to this method.
    43  //
    44  // If the returned diagnostics contains errors then the result may be
    45  // incomplete or invalid.
    46  func (s *Scope) EvalBlock(body hcl.Body, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) {
    47  	spec := schema.DecoderSpec()
    48  
    49  	refs, diags := ReferencesInBlock(body, schema)
    50  
    51  	ctx, ctxDiags := s.EvalContext(refs)
    52  	diags = diags.Append(ctxDiags)
    53  	if diags.HasErrors() {
    54  		// We'll stop early if we found problems in the references, because
    55  		// it's likely evaluation will produce redundant copies of the same errors.
    56  		return cty.UnknownVal(schema.ImpliedType()), diags
    57  	}
    58  
    59  	// HACK: In order to remain compatible with some assumptions made in
    60  	// Durgaform v0.11 and earlier about the approximate equivalence of
    61  	// attribute vs. block syntax, we do a just-in-time fixup here to allow
    62  	// any attribute in the schema that has a list-of-objects or set-of-objects
    63  	// kind to potentially be populated instead by one or more nested blocks
    64  	// whose type is the attribute name.
    65  	body = blocktoattr.FixUpBlockAttrs(body, schema)
    66  
    67  	val, evalDiags := hcldec.Decode(body, spec, ctx)
    68  	diags = diags.Append(evalDiags)
    69  
    70  	return val, diags
    71  }
    72  
    73  // EvalSelfBlock evaluates the given body only within the scope of the provided
    74  // object and instance key data. References to the object must use self, and the
    75  // key data will only contain count.index or each.key. The static values for
    76  // durgaform and path will also be available in this context.
    77  func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschema.Block, keyData instances.RepetitionData) (cty.Value, tfdiags.Diagnostics) {
    78  	var diags tfdiags.Diagnostics
    79  
    80  	spec := schema.DecoderSpec()
    81  
    82  	vals := make(map[string]cty.Value)
    83  	vals["self"] = self
    84  
    85  	if !keyData.CountIndex.IsNull() {
    86  		vals["count"] = cty.ObjectVal(map[string]cty.Value{
    87  			"index": keyData.CountIndex,
    88  		})
    89  	}
    90  	if !keyData.EachKey.IsNull() {
    91  		vals["each"] = cty.ObjectVal(map[string]cty.Value{
    92  			"key": keyData.EachKey,
    93  		})
    94  	}
    95  
    96  	refs, refDiags := References(hcldec.Variables(body, spec))
    97  	diags = diags.Append(refDiags)
    98  
    99  	durgaformAttrs := map[string]cty.Value{}
   100  	pathAttrs := map[string]cty.Value{}
   101  
   102  	// We could always load the static values for Path and Durgaform values,
   103  	// but we want to parse the references so that we can get source ranges for
   104  	// user diagnostics.
   105  	for _, ref := range refs {
   106  		// we already loaded the self value
   107  		if ref.Subject == addrs.Self {
   108  			continue
   109  		}
   110  
   111  		switch subj := ref.Subject.(type) {
   112  		case addrs.PathAttr:
   113  			val, valDiags := normalizeRefValue(s.Data.GetPathAttr(subj, ref.SourceRange))
   114  			diags = diags.Append(valDiags)
   115  			pathAttrs[subj.Name] = val
   116  
   117  		case addrs.DurgaformAttr:
   118  			val, valDiags := normalizeRefValue(s.Data.GetDurgaformAttr(subj, ref.SourceRange))
   119  			diags = diags.Append(valDiags)
   120  			durgaformAttrs[subj.Name] = val
   121  
   122  		case addrs.CountAttr, addrs.ForEachAttr:
   123  			// each and count have already been handled.
   124  
   125  		default:
   126  			// This should have been caught in validation, but point the user
   127  			// to the correct location in case something slipped through.
   128  			diags = diags.Append(&hcl.Diagnostic{
   129  				Severity: hcl.DiagError,
   130  				Summary:  `Invalid reference`,
   131  				Detail:   fmt.Sprintf("The reference to %q is not valid in this context", ref.Subject),
   132  				Subject:  ref.SourceRange.ToHCL().Ptr(),
   133  			})
   134  		}
   135  	}
   136  
   137  	vals["path"] = cty.ObjectVal(pathAttrs)
   138  	vals["durgaform"] = cty.ObjectVal(durgaformAttrs)
   139  
   140  	ctx := &hcl.EvalContext{
   141  		Variables: vals,
   142  		Functions: s.Functions(),
   143  	}
   144  
   145  	val, decDiags := hcldec.Decode(body, schema.DecoderSpec(), ctx)
   146  	diags = diags.Append(decDiags)
   147  	return val, diags
   148  }
   149  
   150  // EvalExpr evaluates a single expression in the receiving context and returns
   151  // the resulting value. The value will be converted to the given type before
   152  // it is returned if possible, or else an error diagnostic will be produced
   153  // describing the conversion error.
   154  //
   155  // Pass an expected type of cty.DynamicPseudoType to skip automatic conversion
   156  // and just obtain the returned value directly.
   157  //
   158  // If the returned diagnostics contains errors then the result may be
   159  // incomplete, but will always be of the requested type.
   160  func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) {
   161  	refs, diags := ReferencesInExpr(expr)
   162  
   163  	ctx, ctxDiags := s.EvalContext(refs)
   164  	diags = diags.Append(ctxDiags)
   165  	if diags.HasErrors() {
   166  		// We'll stop early if we found problems in the references, because
   167  		// it's likely evaluation will produce redundant copies of the same errors.
   168  		return cty.UnknownVal(wantType), diags
   169  	}
   170  
   171  	val, evalDiags := expr.Value(ctx)
   172  	diags = diags.Append(evalDiags)
   173  
   174  	if wantType != cty.DynamicPseudoType {
   175  		var convErr error
   176  		val, convErr = convert.Convert(val, wantType)
   177  		if convErr != nil {
   178  			val = cty.UnknownVal(wantType)
   179  			diags = diags.Append(&hcl.Diagnostic{
   180  				Severity:    hcl.DiagError,
   181  				Summary:     "Incorrect value type",
   182  				Detail:      fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)),
   183  				Subject:     expr.Range().Ptr(),
   184  				Expression:  expr,
   185  				EvalContext: ctx,
   186  			})
   187  		}
   188  	}
   189  
   190  	return val, diags
   191  }
   192  
   193  // EvalReference evaluates the given reference in the receiving scope and
   194  // returns the resulting value. The value will be converted to the given type before
   195  // it is returned if possible, or else an error diagnostic will be produced
   196  // describing the conversion error.
   197  //
   198  // Pass an expected type of cty.DynamicPseudoType to skip automatic conversion
   199  // and just obtain the returned value directly.
   200  //
   201  // If the returned diagnostics contains errors then the result may be
   202  // incomplete, but will always be of the requested type.
   203  func (s *Scope) EvalReference(ref *addrs.Reference, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) {
   204  	var diags tfdiags.Diagnostics
   205  
   206  	// We cheat a bit here and just build an EvalContext for our requested
   207  	// reference with the "self" address overridden, and then pull the "self"
   208  	// result out of it to return.
   209  	ctx, ctxDiags := s.evalContext([]*addrs.Reference{ref}, ref.Subject)
   210  	diags = diags.Append(ctxDiags)
   211  	val := ctx.Variables["self"]
   212  	if val == cty.NilVal {
   213  		val = cty.DynamicVal
   214  	}
   215  
   216  	var convErr error
   217  	val, convErr = convert.Convert(val, wantType)
   218  	if convErr != nil {
   219  		val = cty.UnknownVal(wantType)
   220  		diags = diags.Append(&hcl.Diagnostic{
   221  			Severity: hcl.DiagError,
   222  			Summary:  "Incorrect value type",
   223  			Detail:   fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)),
   224  			Subject:  ref.SourceRange.ToHCL().Ptr(),
   225  		})
   226  	}
   227  
   228  	return val, diags
   229  }
   230  
   231  // EvalContext constructs a HCL expression evaluation context whose variable
   232  // scope contains sufficient values to satisfy the given set of references.
   233  //
   234  // Most callers should prefer to use the evaluation helper methods that
   235  // this type offers, but this is here for less common situations where the
   236  // caller will handle the evaluation calls itself.
   237  func (s *Scope) EvalContext(refs []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) {
   238  	return s.evalContext(refs, s.SelfAddr)
   239  }
   240  
   241  func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceable) (*hcl.EvalContext, tfdiags.Diagnostics) {
   242  	if s == nil {
   243  		panic("attempt to construct EvalContext for nil Scope")
   244  	}
   245  
   246  	var diags tfdiags.Diagnostics
   247  	vals := make(map[string]cty.Value)
   248  	funcs := s.Functions()
   249  	ctx := &hcl.EvalContext{
   250  		Variables: vals,
   251  		Functions: funcs,
   252  	}
   253  
   254  	if len(refs) == 0 {
   255  		// Easy path for common case where there are no references at all.
   256  		return ctx, diags
   257  	}
   258  
   259  	// First we'll do static validation of the references. This catches things
   260  	// early that might otherwise not get caught due to unknown values being
   261  	// present in the scope during planning.
   262  	staticDiags := s.Data.StaticValidateReferences(refs, selfAddr)
   263  	diags = diags.Append(staticDiags)
   264  	if staticDiags.HasErrors() {
   265  		return ctx, diags
   266  	}
   267  
   268  	// The reference set we are given has not been de-duped, and so there can
   269  	// be redundant requests in it for two reasons:
   270  	//  - The same item is referenced multiple times
   271  	//  - Both an item and that item's container are separately referenced.
   272  	// We will still visit every reference here and ask our data source for
   273  	// it, since that allows us to gather a full set of any errors and
   274  	// warnings, but once we've gathered all the data we'll then skip anything
   275  	// that's redundant in the process of populating our values map.
   276  	dataResources := map[string]map[string]cty.Value{}
   277  	managedResources := map[string]map[string]cty.Value{}
   278  	wholeModules := map[string]cty.Value{}
   279  	inputVariables := map[string]cty.Value{}
   280  	localValues := map[string]cty.Value{}
   281  	pathAttrs := map[string]cty.Value{}
   282  	durgaformAttrs := map[string]cty.Value{}
   283  	countAttrs := map[string]cty.Value{}
   284  	forEachAttrs := map[string]cty.Value{}
   285  	var self cty.Value
   286  
   287  	for _, ref := range refs {
   288  		rng := ref.SourceRange
   289  
   290  		rawSubj := ref.Subject
   291  		if rawSubj == addrs.Self {
   292  			if selfAddr == nil {
   293  				diags = diags.Append(&hcl.Diagnostic{
   294  					Severity: hcl.DiagError,
   295  					Summary:  `Invalid "self" reference`,
   296  					// This detail message mentions some current practice that
   297  					// this codepath doesn't really "know about". If the "self"
   298  					// object starts being supported in more contexts later then
   299  					// we'll need to adjust this message.
   300  					Detail:  `The "self" object is not available in this context. This object can be used only in resource provisioner, connection, and postcondition blocks.`,
   301  					Subject: ref.SourceRange.ToHCL().Ptr(),
   302  				})
   303  				continue
   304  			}
   305  
   306  			if selfAddr == addrs.Self {
   307  				// Programming error: the self address cannot alias itself.
   308  				panic("scope SelfAddr attempting to alias itself")
   309  			}
   310  
   311  			// self can only be used within a resource instance
   312  			subj := selfAddr.(addrs.ResourceInstance)
   313  
   314  			val, valDiags := normalizeRefValue(s.Data.GetResource(subj.ContainingResource(), rng))
   315  
   316  			diags = diags.Append(valDiags)
   317  
   318  			// Self is an exception in that it must always resolve to a
   319  			// particular instance. We will still insert the full resource into
   320  			// the context below.
   321  			var hclDiags hcl.Diagnostics
   322  			// We should always have a valid self index by this point, but in
   323  			// the case of an error, self may end up as a cty.DynamicValue.
   324  			switch k := subj.Key.(type) {
   325  			case addrs.IntKey:
   326  				self, hclDiags = hcl.Index(val, cty.NumberIntVal(int64(k)), ref.SourceRange.ToHCL().Ptr())
   327  				diags = diags.Append(hclDiags)
   328  			case addrs.StringKey:
   329  				self, hclDiags = hcl.Index(val, cty.StringVal(string(k)), ref.SourceRange.ToHCL().Ptr())
   330  				diags = diags.Append(hclDiags)
   331  			default:
   332  				self = val
   333  			}
   334  			continue
   335  		}
   336  
   337  		// This type switch must cover all of the "Referenceable" implementations
   338  		// in package addrs, however we are removing the possibility of
   339  		// Instances beforehand.
   340  		switch addr := rawSubj.(type) {
   341  		case addrs.ResourceInstance:
   342  			rawSubj = addr.ContainingResource()
   343  		case addrs.ModuleCallInstance:
   344  			rawSubj = addr.Call
   345  		case addrs.ModuleCallInstanceOutput:
   346  			rawSubj = addr.Call.Call
   347  		}
   348  
   349  		switch subj := rawSubj.(type) {
   350  		case addrs.Resource:
   351  			var into map[string]map[string]cty.Value
   352  			switch subj.Mode {
   353  			case addrs.ManagedResourceMode:
   354  				into = managedResources
   355  			case addrs.DataResourceMode:
   356  				into = dataResources
   357  			default:
   358  				panic(fmt.Errorf("unsupported ResourceMode %s", subj.Mode))
   359  			}
   360  
   361  			val, valDiags := normalizeRefValue(s.Data.GetResource(subj, rng))
   362  			diags = diags.Append(valDiags)
   363  
   364  			r := subj
   365  			if into[r.Type] == nil {
   366  				into[r.Type] = make(map[string]cty.Value)
   367  			}
   368  			into[r.Type][r.Name] = val
   369  
   370  		case addrs.ModuleCall:
   371  			val, valDiags := normalizeRefValue(s.Data.GetModule(subj, rng))
   372  			diags = diags.Append(valDiags)
   373  			wholeModules[subj.Name] = val
   374  
   375  		case addrs.InputVariable:
   376  			val, valDiags := normalizeRefValue(s.Data.GetInputVariable(subj, rng))
   377  			diags = diags.Append(valDiags)
   378  			inputVariables[subj.Name] = val
   379  
   380  		case addrs.LocalValue:
   381  			val, valDiags := normalizeRefValue(s.Data.GetLocalValue(subj, rng))
   382  			diags = diags.Append(valDiags)
   383  			localValues[subj.Name] = val
   384  
   385  		case addrs.PathAttr:
   386  			val, valDiags := normalizeRefValue(s.Data.GetPathAttr(subj, rng))
   387  			diags = diags.Append(valDiags)
   388  			pathAttrs[subj.Name] = val
   389  
   390  		case addrs.DurgaformAttr:
   391  			val, valDiags := normalizeRefValue(s.Data.GetDurgaformAttr(subj, rng))
   392  			diags = diags.Append(valDiags)
   393  			durgaformAttrs[subj.Name] = val
   394  
   395  		case addrs.CountAttr:
   396  			val, valDiags := normalizeRefValue(s.Data.GetCountAttr(subj, rng))
   397  			diags = diags.Append(valDiags)
   398  			countAttrs[subj.Name] = val
   399  
   400  		case addrs.ForEachAttr:
   401  			val, valDiags := normalizeRefValue(s.Data.GetForEachAttr(subj, rng))
   402  			diags = diags.Append(valDiags)
   403  			forEachAttrs[subj.Name] = val
   404  
   405  		default:
   406  			// Should never happen
   407  			panic(fmt.Errorf("Scope.buildEvalContext cannot handle address type %T", rawSubj))
   408  		}
   409  	}
   410  
   411  	// Managed resources are exposed in two different locations. The primary
   412  	// is at the top level where the resource type name is the root of the
   413  	// traversal, but we also expose them under "resource" as an escaping
   414  	// technique if we add a reserved name in a future language edition which
   415  	// conflicts with someone's existing provider.
   416  	for k, v := range buildResourceObjects(managedResources) {
   417  		vals[k] = v
   418  	}
   419  	vals["resource"] = cty.ObjectVal(buildResourceObjects(managedResources))
   420  
   421  	vals["data"] = cty.ObjectVal(buildResourceObjects(dataResources))
   422  	vals["module"] = cty.ObjectVal(wholeModules)
   423  	vals["var"] = cty.ObjectVal(inputVariables)
   424  	vals["local"] = cty.ObjectVal(localValues)
   425  	vals["path"] = cty.ObjectVal(pathAttrs)
   426  	vals["durgaform"] = cty.ObjectVal(durgaformAttrs)
   427  	vals["count"] = cty.ObjectVal(countAttrs)
   428  	vals["each"] = cty.ObjectVal(forEachAttrs)
   429  	if self != cty.NilVal {
   430  		vals["self"] = self
   431  	}
   432  
   433  	return ctx, diags
   434  }
   435  
   436  func buildResourceObjects(resources map[string]map[string]cty.Value) map[string]cty.Value {
   437  	vals := make(map[string]cty.Value)
   438  	for typeName, nameVals := range resources {
   439  		vals[typeName] = cty.ObjectVal(nameVals)
   440  	}
   441  	return vals
   442  }
   443  
   444  func normalizeRefValue(val cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics) {
   445  	if diags.HasErrors() {
   446  		// If there are errors then we will force an unknown result so that
   447  		// we can still evaluate and catch type errors but we'll avoid
   448  		// producing redundant re-statements of the same errors we've already
   449  		// dealt with here.
   450  		return cty.UnknownVal(val.Type()), diags
   451  	}
   452  	return val, diags
   453  }