github.com/kevinklinger/open_terraform@v1.3.6/noninternal/configs/experiments.go (about)

     1  package configs
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  	"github.com/kevinklinger/open_terraform/noninternal/experiments"
     8  	"github.com/kevinklinger/open_terraform/version"
     9  )
    10  
    11  // When developing UI for experimental features, you can temporarily disable
    12  // the experiment warning by setting this package-level variable to a non-empty
    13  // value using a link-time flag:
    14  //
    15  // go install -ldflags="-X 'github.com/kevinklinger/open_terraform/noninternal/configs.disableExperimentWarnings=yes'"
    16  //
    17  // This functionality is for development purposes only and is not a feature we
    18  // are committing to supporting for end users.
    19  var disableExperimentWarnings = ""
    20  
    21  // sniffActiveExperiments does minimal parsing of the given body for
    22  // "terraform" blocks with "experiments" attributes, returning the
    23  // experiments found.
    24  //
    25  // This is separate from other processing so that we can be sure that all of
    26  // the experiments are known before we process the result of the module config,
    27  // and thus we can take into account which experiments are active when deciding
    28  // how to decode.
    29  func sniffActiveExperiments(body hcl.Body, allowed bool) (experiments.Set, hcl.Diagnostics) {
    30  	rootContent, _, diags := body.PartialContent(configFileTerraformBlockSniffRootSchema)
    31  
    32  	ret := experiments.NewSet()
    33  
    34  	for _, block := range rootContent.Blocks {
    35  		content, _, blockDiags := block.Body.PartialContent(configFileExperimentsSniffBlockSchema)
    36  		diags = append(diags, blockDiags...)
    37  
    38  		if attr, exists := content.Attributes["language"]; exists {
    39  			// We don't yet have a sense of selecting an edition of the
    40  			// language, but we're reserving this syntax for now so that
    41  			// if and when we do this later older versions of Terraform
    42  			// will emit a more helpful error message than just saying
    43  			// this attribute doesn't exist. Handling this as part of
    44  			// experiments is a bit odd for now but justified by the
    45  			// fact that a future fuller implementation of switchable
    46  			// languages would be likely use a similar implementation
    47  			// strategy as experiments, and thus would lead to this
    48  			// function being refactored to deal with both concerns at
    49  			// once. We'll see, though!
    50  			kw := hcl.ExprAsKeyword(attr.Expr)
    51  			currentVersion := version.SemVer.String()
    52  			const firstEdition = "TF2021"
    53  			switch {
    54  			case kw == "": // (the expression wasn't a keyword at all)
    55  				diags = diags.Append(&hcl.Diagnostic{
    56  					Severity: hcl.DiagError,
    57  					Summary:  "Invalid language edition",
    58  					Detail: fmt.Sprintf(
    59  						"The language argument expects a bare language edition keyword. Terraform %s supports only language edition %s, which is the default.",
    60  						currentVersion, firstEdition,
    61  					),
    62  					Subject: attr.Expr.Range().Ptr(),
    63  				})
    64  			case kw != firstEdition:
    65  				rel := "different"
    66  				if kw > firstEdition { // would be weird for this not to be true, but it's user input so anything goes
    67  					rel = "newer"
    68  				}
    69  				diags = diags.Append(&hcl.Diagnostic{
    70  					Severity: hcl.DiagError,
    71  					Summary:  "Unsupported language edition",
    72  					Detail: fmt.Sprintf(
    73  						"Terraform v%s only supports language edition %s. This module requires a %s version of Terraform CLI.",
    74  						currentVersion, firstEdition, rel,
    75  					),
    76  					Subject: attr.Expr.Range().Ptr(),
    77  				})
    78  			}
    79  		}
    80  
    81  		attr, exists := content.Attributes["experiments"]
    82  		if !exists {
    83  			continue
    84  		}
    85  
    86  		exps, expDiags := decodeExperimentsAttr(attr)
    87  
    88  		// Because we concluded this particular experiment in the same
    89  		// release as we made experiments alpha-releases-only, we need to
    90  		// treat it as special to avoid masking the "experiment has concluded"
    91  		// error with the more general "experiments are not available at all"
    92  		// error. Note that this experiment is marked as concluded so this
    93  		// only "allows" showing the different error message that it is
    94  		// concluded, and does not allow actually using the experiment outside
    95  		// of an alpha.
    96  		// NOTE: We should be able to remove this special exception a release
    97  		// or two after v1.3 when folks have had a chance to notice that the
    98  		// experiment has concluded and update their modules accordingly.
    99  		// When we do so, we might also consider changing decodeExperimentsAttr
   100  		// to _not_ include concluded experiments in the returned set, since
   101  		// we're doing that right now only to make this condition work.
   102  		if exps.Has(experiments.ModuleVariableOptionalAttrs) && len(exps) == 1 {
   103  			allowed = true
   104  		}
   105  
   106  		if allowed {
   107  			diags = append(diags, expDiags...)
   108  			if !expDiags.HasErrors() {
   109  				ret = experiments.SetUnion(ret, exps)
   110  			}
   111  		} else {
   112  			diags = diags.Append(&hcl.Diagnostic{
   113  				Severity: hcl.DiagError,
   114  				Summary:  "Module uses experimental features",
   115  				Detail:   "Experimental features are intended only for gathering early feedback on new language designs, and so are available only in alpha releases of Terraform.",
   116  				Subject:  attr.NameRange.Ptr(),
   117  			})
   118  		}
   119  	}
   120  
   121  	return ret, diags
   122  }
   123  
   124  func decodeExperimentsAttr(attr *hcl.Attribute) (experiments.Set, hcl.Diagnostics) {
   125  	var diags hcl.Diagnostics
   126  
   127  	exprs, moreDiags := hcl.ExprList(attr.Expr)
   128  	diags = append(diags, moreDiags...)
   129  	if moreDiags.HasErrors() {
   130  		return nil, diags
   131  	}
   132  
   133  	var ret = experiments.NewSet()
   134  	for _, expr := range exprs {
   135  		kw := hcl.ExprAsKeyword(expr)
   136  		if kw == "" {
   137  			diags = diags.Append(&hcl.Diagnostic{
   138  				Severity: hcl.DiagError,
   139  				Summary:  "Invalid experiment keyword",
   140  				Detail:   "Elements of \"experiments\" must all be keywords representing active experiments.",
   141  				Subject:  expr.Range().Ptr(),
   142  			})
   143  			continue
   144  		}
   145  
   146  		exp, err := experiments.GetCurrent(kw)
   147  		switch err := err.(type) {
   148  		case experiments.UnavailableError:
   149  			diags = diags.Append(&hcl.Diagnostic{
   150  				Severity: hcl.DiagError,
   151  				Summary:  "Unknown experiment keyword",
   152  				Detail:   fmt.Sprintf("There is no current experiment with the keyword %q.", kw),
   153  				Subject:  expr.Range().Ptr(),
   154  			})
   155  		case experiments.ConcludedError:
   156  			// As a special case we still include the optional attributes
   157  			// experiment if it's present, because our caller treats that
   158  			// as special. See the comment in sniffActiveExperiments for
   159  			// more information, and remove this special case here one the
   160  			// special case up there is also removed.
   161  			if kw == "module_variable_optional_attrs" {
   162  				ret.Add(experiments.ModuleVariableOptionalAttrs)
   163  			}
   164  
   165  			diags = diags.Append(&hcl.Diagnostic{
   166  				Severity: hcl.DiagError,
   167  				Summary:  "Experiment has concluded",
   168  				Detail:   fmt.Sprintf("Experiment %q is no longer available. %s", kw, err.Message),
   169  				Subject:  expr.Range().Ptr(),
   170  			})
   171  		case nil:
   172  			// No error at all means it's valid and current.
   173  			ret.Add(exp)
   174  
   175  			if disableExperimentWarnings == "" {
   176  				// However, experimental features are subject to breaking changes
   177  				// in future releases, so we'll warn about them to help make sure
   178  				// folks aren't inadvertently using them in places where that'd be
   179  				// inappropriate, particularly if the experiment is active in a
   180  				// shared module they depend on.
   181  				diags = diags.Append(&hcl.Diagnostic{
   182  					Severity: hcl.DiagWarning,
   183  					Summary:  fmt.Sprintf("Experimental feature %q is active", exp.Keyword()),
   184  					Detail:   "Experimental features are available only in alpha releases of Terraform and are subject to breaking changes or total removal in later versions, based on feedback. We recommend against using experimental features in production.\n\nIf you have feedback on the design of this feature, please open a GitHub issue to discuss it.",
   185  					Subject:  expr.Range().Ptr(),
   186  				})
   187  			}
   188  
   189  		default:
   190  			// This should never happen, because GetCurrent is not documented
   191  			// to return any other error type, but we'll handle it to be robust.
   192  			diags = diags.Append(&hcl.Diagnostic{
   193  				Severity: hcl.DiagError,
   194  				Summary:  "Invalid experiment keyword",
   195  				Detail:   fmt.Sprintf("Could not parse %q as an experiment keyword: %s.", kw, err.Error()),
   196  				Subject:  expr.Range().Ptr(),
   197  			})
   198  		}
   199  	}
   200  	return ret, diags
   201  }
   202  
   203  func checkModuleExperiments(m *Module) hcl.Diagnostics {
   204  	var diags hcl.Diagnostics
   205  
   206  	// When we have current experiments, this is a good place to check that
   207  	// the features in question can only be used when the experiments are
   208  	// active. Return error diagnostics if a feature is being used without
   209  	// opting in to the feature. For example:
   210  	/*
   211  		if !m.ActiveExperiments.Has(experiments.ResourceForEach) {
   212  			for _, rc := range m.ManagedResources {
   213  				if rc.ForEach != nil {
   214  					diags = append(diags, &hcl.Diagnostic{
   215  						Severity: hcl.DiagError,
   216  						Summary:  "Resource for_each is experimental",
   217  						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.",
   218  						Subject:  rc.ForEach.Range().Ptr(),
   219  					})
   220  				}
   221  			}
   222  			for _, rc := range m.DataResources {
   223  				if rc.ForEach != nil {
   224  					diags = append(diags, &hcl.Diagnostic{
   225  						Severity: hcl.DiagError,
   226  						Summary:  "Resource for_each is experimental",
   227  						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.",
   228  						Subject:  rc.ForEach.Range().Ptr(),
   229  					})
   230  				}
   231  			}
   232  		}
   233  	*/
   234  
   235  	return diags
   236  }