github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/configs/experiments.go (about)

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