mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
ffe056bacb
This is part of a general effort to move all of Terraform's non-library package surface under internal in order to reinforce that these are for internal use within Terraform only. If you were previously importing packages under this prefix into an external codebase, you could pin to an earlier release tag as an interim solution until you've make a plan to achieve the same functionality some other way.
338 lines
12 KiB
Go
338 lines
12 KiB
Go
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.", 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)
|
|
}
|