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  }