github.com/opentofu/opentofu@v1.7.1/internal/command/cliconfig/provider_installation.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package cliconfig 7 8 import ( 9 "fmt" 10 "path/filepath" 11 12 "github.com/hashicorp/hcl" 13 hclast "github.com/hashicorp/hcl/hcl/ast" 14 15 "github.com/opentofu/opentofu/internal/addrs" 16 "github.com/opentofu/opentofu/internal/getproviders" 17 "github.com/opentofu/opentofu/internal/tfdiags" 18 ) 19 20 // ProviderInstallation is the structure of the "provider_installation" 21 // nested block within the CLI configuration. 22 type ProviderInstallation struct { 23 Methods []*ProviderInstallationMethod 24 25 // DevOverrides allows overriding the normal selection process for 26 // a particular subset of providers to force using a particular 27 // local directory and disregard version numbering altogether. 28 // This is here to allow provider developers to conveniently test 29 // local builds of their plugins in a development environment, without 30 // having to fuss with version constraints, dependency lock files, and 31 // so forth. 32 // 33 // This is _not_ intended for "production" use because it bypasses the 34 // usual version selection and checksum verification mechanisms for 35 // the providers in question. To make that intent/effect clearer, some 36 // OpenTofu commands emit warnings when overrides are present. Local 37 // mirror directories are a better way to distribute "released" 38 // providers, because they are still subject to version constraints and 39 // checksum verification. 40 DevOverrides map[addrs.Provider]getproviders.PackageLocalDir 41 } 42 43 // decodeProviderInstallationFromConfig uses the HCL AST API directly to 44 // decode "provider_installation" blocks from the given file. 45 // 46 // This uses the HCL AST directly, rather than HCL's decoder, because the 47 // intended configuration structure can't be represented using the HCL 48 // decoder's struct tags. This structure is intended as something that would 49 // be relatively easier to deal with in HCL 2 once we eventually migrate 50 // CLI config over to that, and so this function is stricter than HCL 1's 51 // decoder would be in terms of exactly what configuration shape it is 52 // expecting. 53 // 54 // Note that this function wants the top-level file object which might or 55 // might not contain provider_installation blocks, not a provider_installation 56 // block directly itself. 57 func decodeProviderInstallationFromConfig(hclFile *hclast.File) ([]*ProviderInstallation, tfdiags.Diagnostics) { 58 var ret []*ProviderInstallation 59 var diags tfdiags.Diagnostics 60 61 root := hclFile.Node.(*hclast.ObjectList) 62 63 // This is a rather odd hybrid: it's a HCL 2-like decode implemented using 64 // the HCL 1 AST API. That makes it a bit awkward in places, but it allows 65 // us to mimick the strictness of HCL 2 (making a later migration easier) 66 // and to support a block structure that the HCL 1 decoder can't represent. 67 for _, block := range root.Items { 68 if block.Keys[0].Token.Value() != "provider_installation" { 69 continue 70 } 71 // HCL only tracks whether the input was JSON or native syntax inside 72 // individual tokens, so we'll use our block type token to decide 73 // and assume that the rest of the block must be written in the same 74 // syntax, because syntax is a whole-file idea. 75 isJSON := block.Keys[0].Token.JSON 76 if block.Assign.Line != 0 && !isJSON { 77 // Seems to be an attribute rather than a block 78 diags = diags.Append(tfdiags.Sourceless( 79 tfdiags.Error, 80 "Invalid provider_installation block", 81 fmt.Sprintf("The provider_installation block at %s must not be introduced with an equals sign.", block.Pos()), 82 )) 83 continue 84 } 85 if len(block.Keys) > 1 && !isJSON { 86 diags = diags.Append(tfdiags.Sourceless( 87 tfdiags.Error, 88 "Invalid provider_installation block", 89 fmt.Sprintf("The provider_installation block at %s must not have any labels.", block.Pos()), 90 )) 91 } 92 93 pi := &ProviderInstallation{} 94 devOverrides := make(map[addrs.Provider]getproviders.PackageLocalDir) 95 96 body, ok := block.Val.(*hclast.ObjectType) 97 if !ok { 98 // We can't get in here with native HCL syntax because we 99 // already checked above that we're using block syntax, but 100 // if we're reading JSON then our value could potentially be 101 // anything. 102 diags = diags.Append(tfdiags.Sourceless( 103 tfdiags.Error, 104 "Invalid provider_installation block", 105 fmt.Sprintf("The provider_installation block at %s must not be introduced with an equals sign.", block.Pos()), 106 )) 107 continue 108 } 109 110 for _, methodBlock := range body.List.Items { 111 if methodBlock.Assign.Line != 0 && !isJSON { 112 // Seems to be an attribute rather than a block 113 diags = diags.Append(tfdiags.Sourceless( 114 tfdiags.Error, 115 "Invalid provider_installation method block", 116 fmt.Sprintf("The items inside the provider_installation block at %s must all be blocks.", block.Pos()), 117 )) 118 continue 119 } 120 if len(methodBlock.Keys) > 1 && !isJSON { 121 diags = diags.Append(tfdiags.Sourceless( 122 tfdiags.Error, 123 "Invalid provider_installation method block", 124 fmt.Sprintf("The blocks inside the provider_installation block at %s may not have any labels.", block.Pos()), 125 )) 126 } 127 128 methodBody, ok := methodBlock.Val.(*hclast.ObjectType) 129 if !ok { 130 // We can't get in here with native HCL syntax because we 131 // already checked above that we're using block syntax, but 132 // if we're reading JSON then our value could potentially be 133 // anything. 134 diags = diags.Append(tfdiags.Sourceless( 135 tfdiags.Error, 136 "Invalid provider_installation method block", 137 fmt.Sprintf("The items inside the provider_installation block at %s must all be blocks.", block.Pos()), 138 )) 139 continue 140 } 141 142 methodTypeStr := methodBlock.Keys[0].Token.Value().(string) 143 var location ProviderInstallationLocation 144 var include, exclude []string 145 switch methodTypeStr { 146 case "direct": 147 type BodyContent struct { 148 Include []string `hcl:"include"` 149 Exclude []string `hcl:"exclude"` 150 } 151 var bodyContent BodyContent 152 err := hcl.DecodeObject(&bodyContent, methodBody) 153 if err != nil { 154 diags = diags.Append(tfdiags.Sourceless( 155 tfdiags.Error, 156 "Invalid provider_installation method block", 157 fmt.Sprintf("Invalid %s block at %s: %s.", methodTypeStr, block.Pos(), err), 158 )) 159 continue 160 } 161 location = ProviderInstallationDirect 162 include = bodyContent.Include 163 exclude = bodyContent.Exclude 164 case "filesystem_mirror": 165 type BodyContent struct { 166 Path string `hcl:"path"` 167 Include []string `hcl:"include"` 168 Exclude []string `hcl:"exclude"` 169 } 170 var bodyContent BodyContent 171 err := hcl.DecodeObject(&bodyContent, methodBody) 172 if err != nil { 173 diags = diags.Append(tfdiags.Sourceless( 174 tfdiags.Error, 175 "Invalid provider_installation method block", 176 fmt.Sprintf("Invalid %s block at %s: %s.", methodTypeStr, block.Pos(), err), 177 )) 178 continue 179 } 180 if bodyContent.Path == "" { 181 diags = diags.Append(tfdiags.Sourceless( 182 tfdiags.Error, 183 "Invalid provider_installation method block", 184 fmt.Sprintf("Invalid %s block at %s: \"path\" argument is required.", methodTypeStr, block.Pos()), 185 )) 186 continue 187 } 188 location = ProviderInstallationFilesystemMirror(bodyContent.Path) 189 include = bodyContent.Include 190 exclude = bodyContent.Exclude 191 case "network_mirror": 192 type BodyContent struct { 193 URL string `hcl:"url"` 194 Include []string `hcl:"include"` 195 Exclude []string `hcl:"exclude"` 196 } 197 var bodyContent BodyContent 198 err := hcl.DecodeObject(&bodyContent, methodBody) 199 if err != nil { 200 diags = diags.Append(tfdiags.Sourceless( 201 tfdiags.Error, 202 "Invalid provider_installation method block", 203 fmt.Sprintf("Invalid %s block at %s: %s.", methodTypeStr, block.Pos(), err), 204 )) 205 continue 206 } 207 if bodyContent.URL == "" { 208 diags = diags.Append(tfdiags.Sourceless( 209 tfdiags.Error, 210 "Invalid provider_installation method block", 211 fmt.Sprintf("Invalid %s block at %s: \"url\" argument is required.", methodTypeStr, block.Pos()), 212 )) 213 continue 214 } 215 location = ProviderInstallationNetworkMirror(bodyContent.URL) 216 include = bodyContent.Include 217 exclude = bodyContent.Exclude 218 case "dev_overrides": 219 if len(pi.Methods) > 0 { 220 // We require dev_overrides to appear first if it's present, 221 // because dev_overrides effectively bypass the normal 222 // selection process for a particular provider altogether, 223 // and so they don't participate in the usual 224 // include/exclude arguments and priority ordering. 225 diags = diags.Append(tfdiags.Sourceless( 226 tfdiags.Error, 227 "Invalid provider_installation method block", 228 fmt.Sprintf("The dev_overrides block at at %s must appear before all other installation methods, because development overrides always have the highest priority.", methodBlock.Pos()), 229 )) 230 continue 231 } 232 233 // The content of a dev_overrides block is a mapping from 234 // provider source addresses to local filesystem paths. To get 235 // our decoding started, we'll use the normal HCL decoder to 236 // populate a map of strings and then decode further from 237 // that. 238 var rawItems map[string]string 239 err := hcl.DecodeObject(&rawItems, methodBody) 240 if err != nil { 241 diags = diags.Append(tfdiags.Sourceless( 242 tfdiags.Error, 243 "Invalid provider_installation method block", 244 fmt.Sprintf("Invalid %s block at %s: %s.", methodTypeStr, block.Pos(), err), 245 )) 246 continue 247 } 248 249 for rawAddr, rawPath := range rawItems { 250 addr, moreDiags := addrs.ParseProviderSourceString(rawAddr) 251 if moreDiags.HasErrors() { 252 diags = diags.Append(tfdiags.Sourceless( 253 tfdiags.Error, 254 "Invalid provider installation dev overrides", 255 fmt.Sprintf("The entry %q in %s is not a valid provider source string.\n\n%s", rawAddr, block.Pos(), moreDiags.Err().Error()), 256 )) 257 continue 258 } 259 dirPath := filepath.Clean(rawPath) 260 devOverrides[addr] = getproviders.PackageLocalDir(dirPath) 261 } 262 263 continue // We won't add anything to pi.MethodConfigs for this one 264 265 default: 266 diags = diags.Append(tfdiags.Sourceless( 267 tfdiags.Error, 268 "Invalid provider_installation method block", 269 fmt.Sprintf("Unknown provider installation method %q at %s.", methodTypeStr, methodBlock.Pos()), 270 )) 271 continue 272 } 273 274 pi.Methods = append(pi.Methods, &ProviderInstallationMethod{ 275 Location: location, 276 Include: include, 277 Exclude: exclude, 278 }) 279 } 280 281 if len(devOverrides) > 0 { 282 pi.DevOverrides = devOverrides 283 } 284 285 ret = append(ret, pi) 286 } 287 288 return ret, diags 289 } 290 291 // ProviderInstallationMethod represents an installation method block inside 292 // a provider_installation block. 293 type ProviderInstallationMethod struct { 294 Location ProviderInstallationLocation 295 Include []string `hcl:"include"` 296 Exclude []string `hcl:"exclude"` 297 } 298 299 // ProviderInstallationLocation is an interface type representing the 300 // different installation location types. The concrete implementations of 301 // this interface are: 302 // 303 // - [ProviderInstallationDirect]: install from the provider's origin registry 304 // - [ProviderInstallationFilesystemMirror] (dir): install from a local filesystem mirror 305 // - [ProviderInstallationNetworkMirror] (host): install from a network mirror 306 type ProviderInstallationLocation interface { 307 providerInstallationLocation() 308 } 309 310 type providerInstallationDirect [0]byte 311 312 func (i providerInstallationDirect) providerInstallationLocation() {} 313 314 // ProviderInstallationDirect is a ProviderInstallationSourceLocation 315 // representing installation from a provider's origin registry. 316 var ProviderInstallationDirect ProviderInstallationLocation = providerInstallationDirect{} 317 318 func (i providerInstallationDirect) GoString() string { 319 return "cliconfig.ProviderInstallationDirect" 320 } 321 322 // ProviderInstallationFilesystemMirror is a ProviderInstallationSourceLocation 323 // representing installation from a particular local filesystem mirror. The 324 // string value is the filesystem path to the mirror directory. 325 type ProviderInstallationFilesystemMirror string 326 327 func (i ProviderInstallationFilesystemMirror) providerInstallationLocation() {} 328 329 func (i ProviderInstallationFilesystemMirror) GoString() string { 330 return fmt.Sprintf("cliconfig.ProviderInstallationFilesystemMirror(%q)", i) 331 } 332 333 // ProviderInstallationNetworkMirror is a ProviderInstallationSourceLocation 334 // representing installation from a particular local network mirror. The 335 // string value is the HTTP base URL exactly as written in the configuration, 336 // without any normalization. 337 type ProviderInstallationNetworkMirror string 338 339 func (i ProviderInstallationNetworkMirror) providerInstallationLocation() {} 340 341 func (i ProviderInstallationNetworkMirror) GoString() string { 342 return fmt.Sprintf("cliconfig.ProviderInstallationNetworkMirror(%q)", i) 343 }