| package cliconfig |
| |
| import ( |
| "fmt" |
| "path/filepath" |
| |
| "github.com/hashicorp/hcl" |
| hclast "github.com/hashicorp/hcl/hcl/ast" |
| "github.com/hashicorp/terraform/internal/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.\n\n%s", rawAddr, block.Pos(), moreDiags.Err().Error()), |
| )) |
| 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) |
| } |