github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/configs/provider.go (about) 1 package configs 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/hashicorp/hcl/v2/gohcl" 8 "github.com/hashicorp/hcl/v2/hclsyntax" 9 10 "github.com/eliastor/durgaform/internal/addrs" 11 "github.com/eliastor/durgaform/internal/tfdiags" 12 ) 13 14 // Provider represents a "provider" block in a module or file. A provider 15 // block is a provider configuration, and there can be zero or more 16 // configurations for each actual provider. 17 type Provider struct { 18 Name string 19 NameRange hcl.Range 20 Alias string 21 AliasRange *hcl.Range // nil if no alias set 22 23 Version VersionConstraint 24 25 Config hcl.Body 26 27 DeclRange hcl.Range 28 29 // TODO: this may not be set in some cases, so it is not yet suitable for 30 // use outside of this package. We currently only use it for internal 31 // validation, but once we verify that this can be set in all cases, we can 32 // export this so providers don't need to be re-resolved. 33 // This same field is also added to the ProviderConfigRef struct. 34 providerType addrs.Provider 35 } 36 37 func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { 38 var diags hcl.Diagnostics 39 40 content, config, moreDiags := block.Body.PartialContent(providerBlockSchema) 41 diags = append(diags, moreDiags...) 42 43 // Provider names must be localized. Produce an error with a message 44 // indicating the action the user can take to fix this message if the local 45 // name is not localized. 46 name := block.Labels[0] 47 nameDiags := checkProviderNameNormalized(name, block.DefRange) 48 diags = append(diags, nameDiags...) 49 if nameDiags.HasErrors() { 50 // If the name is invalid then we mustn't produce a result because 51 // downstreams could try to use it as a provider type and then crash. 52 return nil, diags 53 } 54 55 provider := &Provider{ 56 Name: name, 57 NameRange: block.LabelRanges[0], 58 Config: config, 59 DeclRange: block.DefRange, 60 } 61 62 if attr, exists := content.Attributes["alias"]; exists { 63 valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias) 64 diags = append(diags, valDiags...) 65 provider.AliasRange = attr.Expr.Range().Ptr() 66 67 if !hclsyntax.ValidIdentifier(provider.Alias) { 68 diags = append(diags, &hcl.Diagnostic{ 69 Severity: hcl.DiagError, 70 Summary: "Invalid provider configuration alias", 71 Detail: fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail), 72 }) 73 } 74 } 75 76 if attr, exists := content.Attributes["version"]; exists { 77 diags = append(diags, &hcl.Diagnostic{ 78 Severity: hcl.DiagWarning, 79 Summary: "Version constraints inside provider configuration blocks are deprecated", 80 Detail: "Durgaform 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is now deprecated and will be removed in a future version of Terraform. To silence this warning, move the provider version constraint into the required_providers block.", 81 Subject: attr.Expr.Range().Ptr(), 82 }) 83 var versionDiags hcl.Diagnostics 84 provider.Version, versionDiags = decodeVersionConstraint(attr) 85 diags = append(diags, versionDiags...) 86 } 87 88 // Reserved attribute names 89 for _, name := range []string{"count", "depends_on", "for_each", "source"} { 90 if attr, exists := content.Attributes[name]; exists { 91 diags = append(diags, &hcl.Diagnostic{ 92 Severity: hcl.DiagError, 93 Summary: "Reserved argument name in provider block", 94 Detail: fmt.Sprintf("The provider argument name %q is reserved for use by Durgaform in a future version.", name), 95 Subject: &attr.NameRange, 96 }) 97 } 98 } 99 100 var seenEscapeBlock *hcl.Block 101 for _, block := range content.Blocks { 102 switch block.Type { 103 case "_": 104 if seenEscapeBlock != nil { 105 diags = append(diags, &hcl.Diagnostic{ 106 Severity: hcl.DiagError, 107 Summary: "Duplicate escaping block", 108 Detail: fmt.Sprintf( 109 "The special block type \"_\" can be used to force particular arguments to be interpreted as provider-specific rather than as meta-arguments, but each provider block can have only one such block. The first escaping block was at %s.", 110 seenEscapeBlock.DefRange, 111 ), 112 Subject: &block.DefRange, 113 }) 114 continue 115 } 116 seenEscapeBlock = block 117 118 // When there's an escaping block its content merges with the 119 // existing config we extracted earlier, so later decoding 120 // will see a blend of both. 121 provider.Config = hcl.MergeBodies([]hcl.Body{provider.Config, block.Body}) 122 123 default: 124 // All of the other block types in our schema are reserved for 125 // future expansion. 126 diags = append(diags, &hcl.Diagnostic{ 127 Severity: hcl.DiagError, 128 Summary: "Reserved block type name in provider block", 129 Detail: fmt.Sprintf("The block type name %q is reserved for use by Durgaform in a future version.", block.Type), 130 Subject: &block.TypeRange, 131 }) 132 } 133 } 134 135 return provider, diags 136 } 137 138 // Addr returns the address of the receiving provider configuration, relative 139 // to its containing module. 140 func (p *Provider) Addr() addrs.LocalProviderConfig { 141 return addrs.LocalProviderConfig{ 142 LocalName: p.Name, 143 Alias: p.Alias, 144 } 145 } 146 147 func (p *Provider) moduleUniqueKey() string { 148 if p.Alias != "" { 149 return fmt.Sprintf("%s.%s", p.Name, p.Alias) 150 } 151 return p.Name 152 } 153 154 // ParseProviderConfigCompact parses the given absolute traversal as a relative 155 // provider address in compact form. The following are examples of traversals 156 // that can be successfully parsed as compact relative provider configuration 157 // addresses: 158 // 159 // aws 160 // aws.foo 161 // 162 // This function will panic if given a relative traversal. 163 // 164 // If the returned diagnostics contains errors then the result value is invalid 165 // and must not be used. 166 func ParseProviderConfigCompact(traversal hcl.Traversal) (addrs.LocalProviderConfig, tfdiags.Diagnostics) { 167 var diags tfdiags.Diagnostics 168 ret := addrs.LocalProviderConfig{ 169 LocalName: traversal.RootName(), 170 } 171 172 if len(traversal) < 2 { 173 // Just a type name, then. 174 return ret, diags 175 } 176 177 aliasStep := traversal[1] 178 switch ts := aliasStep.(type) { 179 case hcl.TraverseAttr: 180 ret.Alias = ts.Name 181 return ret, diags 182 default: 183 diags = diags.Append(&hcl.Diagnostic{ 184 Severity: hcl.DiagError, 185 Summary: "Invalid provider configuration address", 186 Detail: "The provider type name must either stand alone or be followed by an alias name separated with a dot.", 187 Subject: aliasStep.SourceRange().Ptr(), 188 }) 189 } 190 191 if len(traversal) > 2 { 192 diags = diags.Append(&hcl.Diagnostic{ 193 Severity: hcl.DiagError, 194 Summary: "Invalid provider configuration address", 195 Detail: "Extraneous extra operators after provider configuration address.", 196 Subject: traversal[2:].SourceRange().Ptr(), 197 }) 198 } 199 200 return ret, diags 201 } 202 203 // ParseProviderConfigCompactStr is a helper wrapper around ParseProviderConfigCompact 204 // that takes a string and parses it with the HCL native syntax traversal parser 205 // before interpreting it. 206 // 207 // This should be used only in specialized situations since it will cause the 208 // created references to not have any meaningful source location information. 209 // If a reference string is coming from a source that should be identified in 210 // error messages then the caller should instead parse it directly using a 211 // suitable function from the HCL API and pass the traversal itself to 212 // ParseProviderConfigCompact. 213 // 214 // Error diagnostics are returned if either the parsing fails or the analysis 215 // of the traversal fails. There is no way for the caller to distinguish the 216 // two kinds of diagnostics programmatically. If error diagnostics are returned 217 // then the returned address is invalid. 218 func ParseProviderConfigCompactStr(str string) (addrs.LocalProviderConfig, tfdiags.Diagnostics) { 219 var diags tfdiags.Diagnostics 220 221 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 222 diags = diags.Append(parseDiags) 223 if parseDiags.HasErrors() { 224 return addrs.LocalProviderConfig{}, diags 225 } 226 227 addr, addrDiags := ParseProviderConfigCompact(traversal) 228 diags = diags.Append(addrDiags) 229 return addr, diags 230 } 231 232 var providerBlockSchema = &hcl.BodySchema{ 233 Attributes: []hcl.AttributeSchema{ 234 { 235 Name: "alias", 236 }, 237 { 238 Name: "version", 239 }, 240 241 // Attribute names reserved for future expansion. 242 {Name: "count"}, 243 {Name: "depends_on"}, 244 {Name: "for_each"}, 245 {Name: "source"}, 246 }, 247 Blocks: []hcl.BlockHeaderSchema{ 248 {Type: "_"}, // meta-argument escaping block 249 250 // The rest of these are reserved for future expansion. 251 {Type: "lifecycle"}, 252 {Type: "locals"}, 253 }, 254 } 255 256 // checkProviderNameNormalized verifies that the given string is already 257 // normalized and returns an error if not. 258 func checkProviderNameNormalized(name string, declrange hcl.Range) hcl.Diagnostics { 259 var diags hcl.Diagnostics 260 // verify that the provider local name is normalized 261 normalized, err := addrs.IsProviderPartNormalized(name) 262 if err != nil { 263 diags = append(diags, &hcl.Diagnostic{ 264 Severity: hcl.DiagError, 265 Summary: "Invalid provider local name", 266 Detail: fmt.Sprintf("%s is an invalid provider local name: %s", name, err), 267 Subject: &declrange, 268 }) 269 return diags 270 } 271 if !normalized { 272 // we would have returned this error already 273 normalizedProvider, _ := addrs.ParseProviderPart(name) 274 diags = append(diags, &hcl.Diagnostic{ 275 Severity: hcl.DiagError, 276 Summary: "Invalid provider local name", 277 Detail: fmt.Sprintf("Provider names must be normalized. Replace %q with %q to fix this error.", name, normalizedProvider), 278 Subject: &declrange, 279 }) 280 } 281 return diags 282 }