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 }