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 }