package cliconfig import ( "fmt" "path/filepath" "github.com/hashicorp/hcl" hclast "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/tfdiags" ) // ProviderInstallation is the structure of the "provider_installation" // nested block within the CLI configuration. type ProviderInstallation struct { Methods []*ProviderInstallationMethod // DevOverrides allows overriding the normal selection process for // a particular subset of providers to force using a particular // local directory and disregard version numbering altogether. // This is here to allow provider developers to conveniently test // local builds of their plugins in a development environment, without // having to fuss with version constraints, dependency lock files, and // so forth. // // This is _not_ intended for "production" use because it bypasses the // usual version selection and checksum verification mechanisms for // the providers in question. To make that intent/effect clearer, some // Terraform commands emit warnings when overrides are present. Local // mirror directories are a better way to distribute "released" // providers, because they are still subject to version constraints and // checksum verification. DevOverrides map[addrs.Provider]getproviders.PackageLocalDir } // decodeProviderInstallationFromConfig uses the HCL AST API directly to // decode "provider_installation" blocks from the given file. // // This uses the HCL AST directly, rather than HCL's decoder, because the // intended configuration structure can't be represented using the HCL // decoder's struct tags. This structure is intended as something that would // be relatively easier to deal with in HCL 2 once we eventually migrate // CLI config over to that, and so this function is stricter than HCL 1's // decoder would be in terms of exactly what configuration shape it is // expecting. // // Note that this function wants the top-level file object which might or // might not contain provider_installation blocks, not a provider_installation // block directly itself. func decodeProviderInstallationFromConfig(hclFile *hclast.File) ([]*ProviderInstallation, tfdiags.Diagnostics) { var ret []*ProviderInstallation var diags tfdiags.Diagnostics root := hclFile.Node.(*hclast.ObjectList) // This is a rather odd hybrid: it's a HCL 2-like decode implemented using // the HCL 1 AST API. That makes it a bit awkward in places, but it allows // us to mimick the strictness of HCL 2 (making a later migration easier) // and to support a block structure that the HCL 1 decoder can't represent. for _, block := range root.Items { if block.Keys[0].Token.Value() != "provider_installation" { continue } // HCL only tracks whether the input was JSON or native syntax inside // individual tokens, so we'll use our block type token to decide // and assume that the rest of the block must be written in the same // syntax, because syntax is a whole-file idea. isJSON := block.Keys[0].Token.JSON if block.Assign.Line != 0 && !isJSON { // Seems to be an attribute rather than a block diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation block", fmt.Sprintf("The provider_installation block at %s must not be introduced with an equals sign.", block.Pos()), )) continue } if len(block.Keys) > 1 && !isJSON { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation block", fmt.Sprintf("The provider_installation block at %s must not have any labels.", block.Pos()), )) } pi := &ProviderInstallation{} devOverrides := make(map[addrs.Provider]getproviders.PackageLocalDir) body, ok := block.Val.(*hclast.ObjectType) if !ok { // We can't get in here with native HCL syntax because we // already checked above that we're using block syntax, but // if we're reading JSON then our value could potentially be // anything. diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation block", fmt.Sprintf("The provider_installation block at %s must not be introduced with an equals sign.", block.Pos()), )) continue } for _, methodBlock := range body.List.Items { if methodBlock.Assign.Line != 0 && !isJSON { // Seems to be an attribute rather than a block diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("The items inside the provider_installation block at %s must all be blocks.", block.Pos()), )) continue } if len(methodBlock.Keys) > 1 && !isJSON { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("The blocks inside the provider_installation block at %s may not have any labels.", block.Pos()), )) } methodBody, ok := methodBlock.Val.(*hclast.ObjectType) if !ok { // We can't get in here with native HCL syntax because we // already checked above that we're using block syntax, but // if we're reading JSON then our value could potentially be // anything. diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("The items inside the provider_installation block at %s must all be blocks.", block.Pos()), )) continue } methodTypeStr := methodBlock.Keys[0].Token.Value().(string) var location ProviderInstallationLocation var include, exclude []string switch methodTypeStr { case "direct": type BodyContent struct { Include []string `hcl:"include"` Exclude []string `hcl:"exclude"` } var bodyContent BodyContent err := hcl.DecodeObject(&bodyContent, methodBody) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("Invalid %s block at %s: %s.", methodTypeStr, block.Pos(), err), )) continue } location = ProviderInstallationDirect include = bodyContent.Include exclude = bodyContent.Exclude case "filesystem_mirror": type BodyContent struct { Path string `hcl:"path"` Include []string `hcl:"include"` Exclude []string `hcl:"exclude"` } var bodyContent BodyContent err := hcl.DecodeObject(&bodyContent, methodBody) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("Invalid %s block at %s: %s.", methodTypeStr, block.Pos(), err), )) continue } if bodyContent.Path == "" { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("Invalid %s block at %s: \"path\" argument is required.", methodTypeStr, block.Pos()), )) continue } location = ProviderInstallationFilesystemMirror(bodyContent.Path) include = bodyContent.Include exclude = bodyContent.Exclude case "network_mirror": type BodyContent struct { URL string `hcl:"url"` Include []string `hcl:"include"` Exclude []string `hcl:"exclude"` } var bodyContent BodyContent err := hcl.DecodeObject(&bodyContent, methodBody) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("Invalid %s block at %s: %s.", methodTypeStr, block.Pos(), err), )) continue } if bodyContent.URL == "" { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("Invalid %s block at %s: \"url\" argument is required.", methodTypeStr, block.Pos()), )) continue } location = ProviderInstallationNetworkMirror(bodyContent.URL) include = bodyContent.Include exclude = bodyContent.Exclude case "dev_overrides": if len(pi.Methods) > 0 { // We require dev_overrides to appear first if it's present, // because dev_overrides effectively bypass the normal // selection process for a particular provider altogether, // and so they don't participate in the usual // include/exclude arguments and priority ordering. diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", 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()), )) continue } // The content of a dev_overrides block is a mapping from // provider source addresses to local filesystem paths. To get // our decoding started, we'll use the normal HCL decoder to // populate a map of strings and then decode further from // that. var rawItems map[string]string err := hcl.DecodeObject(&rawItems, methodBody) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("Invalid %s block at %s: %s.", methodTypeStr, block.Pos(), err), )) continue } for rawAddr, rawPath := range rawItems { addr, moreDiags := addrs.ParseProviderSourceString(rawAddr) if moreDiags.HasErrors() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider installation dev overrides", fmt.Sprintf("The entry %q in %s is not a valid provider source string.", rawAddr, block.Pos()), )) continue } dirPath := filepath.Clean(rawPath) devOverrides[addr] = getproviders.PackageLocalDir(dirPath) } continue // We won't add anything to pi.Methods for this one default: diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", fmt.Sprintf("Unknown provider installation method %q at %s.", methodTypeStr, methodBlock.Pos()), )) continue } pi.Methods = append(pi.Methods, &ProviderInstallationMethod{ Location: location, Include: include, Exclude: exclude, }) } if len(devOverrides) > 0 { pi.DevOverrides = devOverrides } ret = append(ret, pi) } return ret, diags } // ProviderInstallationMethod represents an installation method block inside // a provider_installation block. type ProviderInstallationMethod struct { Location ProviderInstallationLocation Include []string `hcl:"include"` Exclude []string `hcl:"exclude"` } // ProviderInstallationLocation is an interface type representing the // different installation location types. The concrete implementations of // this interface are: // // ProviderInstallationDirect: install from the provider's origin registry // ProviderInstallationFilesystemMirror(dir): install from a local filesystem mirror // ProviderInstallationNetworkMirror(host): install from a network mirror type ProviderInstallationLocation interface { providerInstallationLocation() } type providerInstallationDirect [0]byte func (i providerInstallationDirect) providerInstallationLocation() {} // ProviderInstallationDirect is a ProviderInstallationSourceLocation // representing installation from a provider's origin registry. var ProviderInstallationDirect ProviderInstallationLocation = providerInstallationDirect{} func (i providerInstallationDirect) GoString() string { return "cliconfig.ProviderInstallationDirect" } // ProviderInstallationFilesystemMirror is a ProviderInstallationSourceLocation // representing installation from a particular local filesystem mirror. The // string value is the filesystem path to the mirror directory. type ProviderInstallationFilesystemMirror string func (i ProviderInstallationFilesystemMirror) providerInstallationLocation() {} func (i ProviderInstallationFilesystemMirror) GoString() string { return fmt.Sprintf("cliconfig.ProviderInstallationFilesystemMirror(%q)", i) } // ProviderInstallationNetworkMirror is a ProviderInstallationSourceLocation // representing installation from a particular local network mirror. The // string value is the HTTP base URL exactly as written in the configuration, // without any normalization. type ProviderInstallationNetworkMirror string func (i ProviderInstallationNetworkMirror) providerInstallationLocation() {} func (i ProviderInstallationNetworkMirror) GoString() string { return fmt.Sprintf("cliconfig.ProviderInstallationNetworkMirror(%q)", i) }