github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/configs/experiments.go (about)

     1  package configs
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/iaas-resource-provision/iaas-rpc/internal/experiments"
     8  	"github.com/iaas-resource-provision/iaas-rpc/version"
     9  	"github.com/zclconf/go-cty/cty"
    10  )
    11  
    12  // sniffActiveExperiments does minimal parsing of the given body for
    13  // "terraform" blocks with "experiments" attributes, returning the
    14  // experiments found.
    15  //
    16  // This is separate from other processing so that we can be sure that all of
    17  // the experiments are known before we process the result of the module config,
    18  // and thus we can take into account which experiments are active when deciding
    19  // how to decode.
    20  func sniffActiveExperiments(body hcl.Body) (experiments.Set, hcl.Diagnostics) {
    21  	rootContent, _, diags := body.PartialContent(configFileTerraformBlockSniffRootSchema)
    22  
    23  	ret := experiments.NewSet()
    24  
    25  	for _, block := range rootContent.Blocks {
    26  		content, _, blockDiags := block.Body.PartialContent(configFileExperimentsSniffBlockSchema)
    27  		diags = append(diags, blockDiags...)
    28  
    29  		if attr, exists := content.Attributes["language"]; exists {
    30  			// We don't yet have a sense of selecting an edition of the
    31  			// language, but we're reserving this syntax for now so that
    32  			// if and when we do this later older versions of Terraform
    33  			// will emit a more helpful error message than just saying
    34  			// this attribute doesn't exist. Handling this as part of
    35  			// experiments is a bit odd for now but justified by the
    36  			// fact that a future fuller implementation of switchable
    37  			// languages would be likely use a similar implementation
    38  			// strategy as experiments, and thus would lead to this
    39  			// function being refactored to deal with both concerns at
    40  			// once. We'll see, though!
    41  			kw := hcl.ExprAsKeyword(attr.Expr)
    42  			currentVersion := version.SemVer.String()
    43  			const firstEdition = "TF2021"
    44  			switch {
    45  			case kw == "": // (the expression wasn't a keyword at all)
    46  				diags = diags.Append(&hcl.Diagnostic{
    47  					Severity: hcl.DiagError,
    48  					Summary:  "Invalid language edition",
    49  					Detail: fmt.Sprintf(
    50  						"The language argument expects a bare language edition keyword. Terraform %s supports only language edition %s, which is the default.",
    51  						currentVersion, firstEdition,
    52  					),
    53  					Subject: attr.Expr.Range().Ptr(),
    54  				})
    55  			case kw != firstEdition:
    56  				rel := "different"
    57  				if kw > firstEdition { // would be weird for this not to be true, but it's user input so anything goes
    58  					rel = "newer"
    59  				}
    60  				diags = diags.Append(&hcl.Diagnostic{
    61  					Severity: hcl.DiagError,
    62  					Summary:  "Unsupported language edition",
    63  					Detail: fmt.Sprintf(
    64  						"Terraform v%s only supports language edition %s. This module requires a %s version of Terraform CLI.",
    65  						currentVersion, firstEdition, rel,
    66  					),
    67  					Subject: attr.Expr.Range().Ptr(),
    68  				})
    69  			}
    70  		}
    71  
    72  		attr, exists := content.Attributes["experiments"]
    73  		if !exists {
    74  			continue
    75  		}
    76  
    77  		exps, expDiags := decodeExperimentsAttr(attr)
    78  		diags = append(diags, expDiags...)
    79  		if !expDiags.HasErrors() {
    80  			ret = experiments.SetUnion(ret, exps)
    81  		}
    82  	}
    83  
    84  	return ret, diags
    85  }
    86  
    87  func decodeExperimentsAttr(attr *hcl.Attribute) (experiments.Set, hcl.Diagnostics) {
    88  	var diags hcl.Diagnostics
    89  
    90  	exprs, moreDiags := hcl.ExprList(attr.Expr)
    91  	diags = append(diags, moreDiags...)
    92  	if moreDiags.HasErrors() {
    93  		return nil, diags
    94  	}
    95  
    96  	var ret = experiments.NewSet()
    97  	for _, expr := range exprs {
    98  		kw := hcl.ExprAsKeyword(expr)
    99  		if kw == "" {
   100  			diags = diags.Append(&hcl.Diagnostic{
   101  				Severity: hcl.DiagError,
   102  				Summary:  "Invalid experiment keyword",
   103  				Detail:   "Elements of \"experiments\" must all be keywords representing active experiments.",
   104  				Subject:  expr.Range().Ptr(),
   105  			})
   106  			continue
   107  		}
   108  
   109  		exp, err := experiments.GetCurrent(kw)
   110  		switch err := err.(type) {
   111  		case experiments.UnavailableError:
   112  			diags = diags.Append(&hcl.Diagnostic{
   113  				Severity: hcl.DiagError,
   114  				Summary:  "Unknown experiment keyword",
   115  				Detail:   fmt.Sprintf("There is no current experiment with the keyword %q.", kw),
   116  				Subject:  expr.Range().Ptr(),
   117  			})
   118  		case experiments.ConcludedError:
   119  			diags = diags.Append(&hcl.Diagnostic{
   120  				Severity: hcl.DiagError,
   121  				Summary:  "Experiment has concluded",
   122  				Detail:   fmt.Sprintf("Experiment %q is no longer available. %s", kw, err.Message),
   123  				Subject:  expr.Range().Ptr(),
   124  			})
   125  		case nil:
   126  			// No error at all means it's valid and current.
   127  			ret.Add(exp)
   128  
   129  			// However, experimental features are subject to breaking changes
   130  			// in future releases, so we'll warn about them to help make sure
   131  			// folks aren't inadvertently using them in places where that'd be
   132  			// inappropriate, particularly if the experiment is active in a
   133  			// shared module they depend on.
   134  			diags = diags.Append(&hcl.Diagnostic{
   135  				Severity: hcl.DiagWarning,
   136  				Summary:  fmt.Sprintf("Experimental feature %q is active", exp.Keyword()),
   137  				Detail:   "Experimental features are subject to breaking changes in future minor or patch releases, based on feedback.\n\nIf you have feedback on the design of this feature, please open a GitHub issue to discuss it.",
   138  				Subject:  expr.Range().Ptr(),
   139  			})
   140  
   141  		default:
   142  			// This should never happen, because GetCurrent is not documented
   143  			// to return any other error type, but we'll handle it to be robust.
   144  			diags = diags.Append(&hcl.Diagnostic{
   145  				Severity: hcl.DiagError,
   146  				Summary:  "Invalid experiment keyword",
   147  				Detail:   fmt.Sprintf("Could not parse %q as an experiment keyword: %s.", kw, err.Error()),
   148  				Subject:  expr.Range().Ptr(),
   149  			})
   150  		}
   151  	}
   152  	return ret, diags
   153  }
   154  
   155  func checkModuleExperiments(m *Module) hcl.Diagnostics {
   156  	var diags hcl.Diagnostics
   157  
   158  	// When we have current experiments, this is a good place to check that
   159  	// the features in question can only be used when the experiments are
   160  	// active. Return error diagnostics if a feature is being used without
   161  	// opting in to the feature. For example:
   162  	/*
   163  		if !m.ActiveExperiments.Has(experiments.ResourceForEach) {
   164  			for _, rc := range m.ManagedResources {
   165  				if rc.ForEach != nil {
   166  					diags = append(diags, &hcl.Diagnostic{
   167  						Severity: hcl.DiagError,
   168  						Summary:  "Resource for_each is experimental",
   169  						Detail:   "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding resource_for_each to the list of active experiments.",
   170  						Subject:  rc.ForEach.Range().Ptr(),
   171  					})
   172  				}
   173  			}
   174  			for _, rc := range m.DataResources {
   175  				if rc.ForEach != nil {
   176  					diags = append(diags, &hcl.Diagnostic{
   177  						Severity: hcl.DiagError,
   178  						Summary:  "Resource for_each is experimental",
   179  						Detail:   "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding resource_for_each to the list of active experiments.",
   180  						Subject:  rc.ForEach.Range().Ptr(),
   181  					})
   182  				}
   183  			}
   184  		}
   185  	*/
   186  
   187  	if !m.ActiveExperiments.Has(experiments.ModuleVariableOptionalAttrs) {
   188  		for _, v := range m.Variables {
   189  			if typeConstraintHasOptionalAttrs(v.Type) {
   190  				diags = diags.Append(&hcl.Diagnostic{
   191  					Severity: hcl.DiagError,
   192  					Summary:  "Optional object type attributes are experimental",
   193  					Detail:   "This feature is currently an opt-in experiment, subject to change in future releases based on feedback.\n\nActivate the feature for this module by adding module_variable_optional_attrs to the list of active experiments.",
   194  					Subject:  v.DeclRange.Ptr(),
   195  				})
   196  			}
   197  		}
   198  	}
   199  
   200  	return diags
   201  }
   202  
   203  func typeConstraintHasOptionalAttrs(ty cty.Type) bool {
   204  	if ty == cty.NilType {
   205  		// Weird, but we'll just ignore it to avoid crashing.
   206  		return false
   207  	}
   208  
   209  	switch {
   210  	case ty.IsPrimitiveType():
   211  		return false
   212  	case ty.IsCollectionType():
   213  		return typeConstraintHasOptionalAttrs(ty.ElementType())
   214  	case ty.IsObjectType():
   215  		if len(ty.OptionalAttributes()) != 0 {
   216  			return true
   217  		}
   218  		for _, aty := range ty.AttributeTypes() {
   219  			if typeConstraintHasOptionalAttrs(aty) {
   220  				return true
   221  			}
   222  		}
   223  		return false
   224  	case ty.IsTupleType():
   225  		for _, ety := range ty.TupleElementTypes() {
   226  			if typeConstraintHasOptionalAttrs(ety) {
   227  				return true
   228  			}
   229  		}
   230  		return false
   231  	default:
   232  		return false
   233  	}
   234  }