opentofu/configs/experiments.go
Martin Atkins b90fb25321 experiments: a mechanism for opt-in experimental language features
Traditionally we've preferred to release new language features in major
releases only, because we can then use the beta cycle to gather feedback
on the feature and learn about any usability challenges or other
situations we didn't consider during our design in time to make those
changes before inclusion in a stable release.

This "experiments" feature is intended to decouple the feedback cycle for
new features from the major release rhythm, and thus allow us to release
new features in minor releases by first releasing them as experimental for
a minor release or two, adjust for any feedback gathered during that
period, and then finally remove the experiment gate and enable the feature
for everyone.

The intended model here is that anything behind an experiment gate is
subject to breaking changes even in patch releases, and so any module
using these experimental features will be broken by a future Terraform
upgrade.

The behavior implemented here is:

- Recognize a new "experiments" setting in the "terraform" block which
  allows module authors to explicitly opt in to experimental features.

  terraform {
    experiments = [resource_for_each]
  }

- Generate a warning whenever loading a module that has experiments
  enabled, to avoid accidentally depending on experimental features and
  thus risking unexpected breakage on next Terraform upgrade.

- We check the enabled experiments against the configuration at module
  load time, which means that experiments are scoped to a particular
  module. Enabling an experiment in one module does not automatically
  enable it in any other module.

This experiments mechanism is itself an experiment, and so I'd like to
use the resource for_each feature to trial it. Because any configuration
using experiments is subject to breaking changes, we are free to adjust
this experiments feature in future releases as we see fit, but once
for_each is shipped without an experiment gate we'll be blocked from
making significant changes to it until the next major release at least.
2019-12-10 09:27:05 -08:00

144 lines
4.9 KiB
Go

package configs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/experiments"
)
// sniffActiveExperiments does minimal parsing of the given body for
// "terraform" blocks with "experiments" attributes, returning the
// experiments found.
//
// This is separate from other processing so that we can be sure that all of
// the experiments are known before we process the result of the module config,
// and thus we can take into account which experiments are active when deciding
// how to decode.
func sniffActiveExperiments(body hcl.Body) (experiments.Set, hcl.Diagnostics) {
rootContent, _, diags := body.PartialContent(configFileTerraformBlockSniffRootSchema)
ret := experiments.NewSet()
for _, block := range rootContent.Blocks {
content, _, blockDiags := block.Body.PartialContent(configFileExperimentsSniffBlockSchema)
diags = append(diags, blockDiags...)
attr, exists := content.Attributes["experiments"]
if !exists {
continue
}
exps, expDiags := decodeExperimentsAttr(attr)
diags = append(diags, expDiags...)
if !expDiags.HasErrors() {
ret = experiments.SetUnion(ret, exps)
}
}
return ret, diags
}
func decodeExperimentsAttr(attr *hcl.Attribute) (experiments.Set, hcl.Diagnostics) {
var diags hcl.Diagnostics
exprs, moreDiags := hcl.ExprList(attr.Expr)
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
return nil, diags
}
var ret = experiments.NewSet()
for _, expr := range exprs {
kw := hcl.ExprAsKeyword(expr)
if kw == "" {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid experiment keyword",
Detail: "Elements of \"experiments\" must all be keywords representing active experiments.",
Subject: expr.Range().Ptr(),
})
continue
}
exp, err := experiments.GetCurrent(kw)
switch err := err.(type) {
case experiments.UnavailableError:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unknown experiment keyword",
Detail: fmt.Sprintf("There is no current experiment with the keyword %q.", kw),
Subject: expr.Range().Ptr(),
})
case experiments.ConcludedError:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Experiment has concluded",
Detail: fmt.Sprintf("Experiment %q is no longer available. %s", kw, err.Message),
Subject: expr.Range().Ptr(),
})
case nil:
// No error at all means it's valid and current.
ret.Add(exp)
// However, experimental features are subject to breaking changes
// in future releases, so we'll warn about them to help make sure
// folks aren't inadvertently using them in places where that'd be
// inappropriate, particularly if the experiment is active in a
// shared module they depend on.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: fmt.Sprintf("Experimental feature %q is active", exp.Keyword()),
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.",
Subject: expr.Range().Ptr(),
})
default:
// This should never happen, because GetCurrent is not documented
// to return any other error type, but we'll handle it to be robust.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid experiment keyword",
Detail: fmt.Sprintf("Could not parse %q as an experiment keyword: %s.", kw, err.Error()),
Subject: expr.Range().Ptr(),
})
}
}
return ret, diags
}
func checkModuleExperiments(m *Module) hcl.Diagnostics {
var diags hcl.Diagnostics
// When we have current experiments, this is a good place to check that
// the features in question can only be used when the experiments are
// active. Return error diagnostics if a feature is being used without
// opting in to the feature. For example:
/*
if !m.ActiveExperiments.Has(experiments.ResourceForEach) {
for _, rc := range m.ManagedResources {
if rc.ForEach != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Resource for_each is experimental",
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.",
Subject: rc.ForEach.Range().Ptr(),
})
}
}
for _, rc := range m.DataResources {
if rc.ForEach != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Resource for_each is experimental",
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.",
Subject: rc.ForEach.Range().Ptr(),
})
}
}
}
*/
return diags
}