github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/command/providers_lock.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/url"
     7  	"os"
     8  
     9  	"github.com/iaas-resource-provision/iaas-rpc/internal/addrs"
    10  	"github.com/iaas-resource-provision/iaas-rpc/internal/depsfile"
    11  	"github.com/iaas-resource-provision/iaas-rpc/internal/getproviders"
    12  	"github.com/iaas-resource-provision/iaas-rpc/internal/providercache"
    13  	"github.com/iaas-resource-provision/iaas-rpc/internal/tfdiags"
    14  )
    15  
    16  // ProvidersLockCommand is a Command implementation that implements the
    17  // "terraform providers lock" command, which creates or updates the current
    18  // configuration's dependency lock file using information from upstream
    19  // registries, regardless of the provider installation configuration that
    20  // is configured for normal provider installation.
    21  type ProvidersLockCommand struct {
    22  	Meta
    23  }
    24  
    25  func (c *ProvidersLockCommand) Synopsis() string {
    26  	return "Write out dependency locks for the configured providers"
    27  }
    28  
    29  func (c *ProvidersLockCommand) Run(args []string) int {
    30  	args = c.Meta.process(args)
    31  	cmdFlags := c.Meta.defaultFlagSet("providers lock")
    32  	var optPlatforms FlagStringSlice
    33  	var fsMirrorDir string
    34  	var netMirrorURL string
    35  	cmdFlags.Var(&optPlatforms, "platform", "target platform")
    36  	cmdFlags.StringVar(&fsMirrorDir, "fs-mirror", "", "filesystem mirror directory")
    37  	cmdFlags.StringVar(&netMirrorURL, "net-mirror", "", "network mirror base URL")
    38  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    39  	if err := cmdFlags.Parse(args); err != nil {
    40  		c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
    41  		return 1
    42  	}
    43  
    44  	var diags tfdiags.Diagnostics
    45  
    46  	if fsMirrorDir != "" && netMirrorURL != "" {
    47  		diags = diags.Append(tfdiags.Sourceless(
    48  			tfdiags.Error,
    49  			"Invalid installation method options",
    50  			"The -fs-mirror and -net-mirror command line options are mutually-exclusive.",
    51  		))
    52  		c.showDiagnostics(diags)
    53  		return 1
    54  	}
    55  
    56  	providerStrs := cmdFlags.Args()
    57  
    58  	var platforms []getproviders.Platform
    59  	if len(optPlatforms) == 0 {
    60  		platforms = []getproviders.Platform{getproviders.CurrentPlatform}
    61  	} else {
    62  		platforms = make([]getproviders.Platform, 0, len(optPlatforms))
    63  		for _, platformStr := range optPlatforms {
    64  			platform, err := getproviders.ParsePlatform(platformStr)
    65  			if err != nil {
    66  				diags = diags.Append(tfdiags.Sourceless(
    67  					tfdiags.Error,
    68  					"Invalid target platform",
    69  					fmt.Sprintf("The string %q given in the -platform option is not a valid target platform: %s.", platformStr, err),
    70  				))
    71  				continue
    72  			}
    73  			platforms = append(platforms, platform)
    74  		}
    75  	}
    76  
    77  	// Unlike other commands, this command ignores the installation methods
    78  	// selected in the CLI configuration and instead chooses an installation
    79  	// method based on CLI options.
    80  	//
    81  	// This is so that folks who use a local mirror for everyday use can
    82  	// use this command to populate their lock files from upstream so
    83  	// subsequent "terraform init" calls can then verify the local mirror
    84  	// against the upstream checksums.
    85  	var source getproviders.Source
    86  	switch {
    87  	case fsMirrorDir != "":
    88  		source = getproviders.NewFilesystemMirrorSource(fsMirrorDir)
    89  	case netMirrorURL != "":
    90  		u, err := url.Parse(netMirrorURL)
    91  		if err != nil || u.Scheme != "https" {
    92  			diags = diags.Append(tfdiags.Sourceless(
    93  				tfdiags.Error,
    94  				"Invalid network mirror URL",
    95  				"The -net-mirror option requires a valid https: URL as the mirror base URL.",
    96  			))
    97  			c.showDiagnostics(diags)
    98  			return 1
    99  		}
   100  		source = getproviders.NewHTTPMirrorSource(u, c.Services.CredentialsSource())
   101  	default:
   102  		// With no special options we consult upstream registries directly,
   103  		// because that gives us the most information to produce as complete
   104  		// and portable as possible a lock entry.
   105  		source = getproviders.NewRegistrySource(c.Services)
   106  	}
   107  
   108  	config, confDiags := c.loadConfig(".")
   109  	diags = diags.Append(confDiags)
   110  	reqs, hclDiags := config.ProviderRequirements()
   111  	diags = diags.Append(hclDiags)
   112  
   113  	// If we have explicit provider selections on the command line then
   114  	// we'll modify "reqs" to only include those. Modifying this is okay
   115  	// because config.ProviderRequirements generates a fresh map result
   116  	// for each call.
   117  	if len(providerStrs) != 0 {
   118  		providers := map[addrs.Provider]struct{}{}
   119  		for _, raw := range providerStrs {
   120  			addr, moreDiags := addrs.ParseProviderSourceString(raw)
   121  			diags = diags.Append(moreDiags)
   122  			if moreDiags.HasErrors() {
   123  				continue
   124  			}
   125  			providers[addr] = struct{}{}
   126  			if _, exists := reqs[addr]; !exists {
   127  				// Can't request a provider that isn't required by the
   128  				// current configuration.
   129  				diags = diags.Append(tfdiags.Sourceless(
   130  					tfdiags.Error,
   131  					"Invalid network mirror URL",
   132  					"The -net-mirror option requires a valid https: URL as the mirror base URL.",
   133  				))
   134  			}
   135  		}
   136  
   137  		for addr := range reqs {
   138  			if _, exists := providers[addr]; !exists {
   139  				delete(reqs, addr)
   140  			}
   141  		}
   142  	}
   143  
   144  	// We'll also ignore any providers that don't participate in locking.
   145  	for addr := range reqs {
   146  		if !depsfile.ProviderIsLockable(addr) {
   147  			delete(reqs, addr)
   148  		}
   149  	}
   150  
   151  	// We'll start our work with whatever locks we already have, so that
   152  	// we'll honor any existing version selections and just add additional
   153  	// hashes for them.
   154  	oldLocks, moreDiags := c.lockedDependencies()
   155  	diags = diags.Append(moreDiags)
   156  
   157  	// If we have any error diagnostics already then we won't proceed further.
   158  	if diags.HasErrors() {
   159  		c.showDiagnostics(diags)
   160  		return 1
   161  	}
   162  
   163  	// Our general strategy here is to install the requested providers into
   164  	// a separate temporary directory -- thus ensuring that the results won't
   165  	// ever be inadvertently executed by other Terraform commands -- and then
   166  	// use the results of that installation to update the lock file for the
   167  	// current working directory. Because we throwaway the packages we
   168  	// downloaded after completing our work, a subsequent "terraform init" will
   169  	// then respect the CLI configuration's provider installation strategies
   170  	// but will verify the packages against the hashes we found upstream.
   171  
   172  	// Because our Installer abstraction is a per-platform idea, we'll
   173  	// instantiate one for each of the platforms the user requested, and then
   174  	// merge all of the generated locks together at the end.
   175  	updatedLocks := map[getproviders.Platform]*depsfile.Locks{}
   176  	selectedVersions := map[addrs.Provider]getproviders.Version{}
   177  	ctx, cancel := c.InterruptibleContext()
   178  	defer cancel()
   179  	for _, platform := range platforms {
   180  		tempDir, err := ioutil.TempDir("", "terraform-providers-lock")
   181  		if err != nil {
   182  			diags = diags.Append(tfdiags.Sourceless(
   183  				tfdiags.Error,
   184  				"Could not create temporary directory",
   185  				fmt.Sprintf("Failed to create a temporary directory for staging the requested provider packages: %s.", err),
   186  			))
   187  			break
   188  		}
   189  		defer os.RemoveAll(tempDir)
   190  
   191  		evts := &providercache.InstallerEvents{
   192  			// Our output from this command is minimal just to show that
   193  			// we're making progress, rather than just silently hanging.
   194  			FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, loc getproviders.PackageLocation) {
   195  				c.Ui.Output(fmt.Sprintf("- Fetching %s %s for %s...", provider.ForDisplay(), version, platform))
   196  				if prevVersion, exists := selectedVersions[provider]; exists && version != prevVersion {
   197  					// This indicates a weird situation where we ended up
   198  					// selecting a different version for one platform than
   199  					// for another. We won't be able to merge the result
   200  					// in that case, so we'll generate an error.
   201  					//
   202  					// This could potentially happen if there's a provider
   203  					// we've not previously recorded in the lock file and
   204  					// the available versions change while we're running. To
   205  					// avoid that would require pre-locking all of the
   206  					// providers, which is complicated to do with the building
   207  					// blocks we have here, and so we'll wait to do it only
   208  					// if this situation arises often in practice.
   209  					diags = diags.Append(tfdiags.Sourceless(
   210  						tfdiags.Error,
   211  						"Inconsistent provider versions",
   212  						fmt.Sprintf(
   213  							"The version constraint for %s selected inconsistent versions for different platforms, which is unexpected.\n\nThe upstream registry may have changed its available versions during Terraform's work. If so, re-running this command may produce a successful result.",
   214  							provider,
   215  						),
   216  					))
   217  				}
   218  				selectedVersions[provider] = version
   219  			},
   220  			FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, auth *getproviders.PackageAuthenticationResult) {
   221  				var keyID string
   222  				if auth != nil && auth.ThirdPartySigned() {
   223  					keyID = auth.KeyID
   224  				}
   225  				if keyID != "" {
   226  					keyID = c.Colorize().Color(fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID))
   227  				}
   228  				c.Ui.Output(fmt.Sprintf("- Obtained %s checksums for %s (%s%s)", provider.ForDisplay(), platform, auth, keyID))
   229  			},
   230  		}
   231  		ctx := evts.OnContext(ctx)
   232  
   233  		dir := providercache.NewDirWithPlatform(tempDir, platform)
   234  		installer := providercache.NewInstaller(dir, source)
   235  
   236  		newLocks, err := installer.EnsureProviderVersions(ctx, oldLocks, reqs, providercache.InstallNewProvidersOnly)
   237  		if err != nil {
   238  			diags = diags.Append(tfdiags.Sourceless(
   239  				tfdiags.Error,
   240  				"Could not retrieve providers for locking",
   241  				fmt.Sprintf("Terraform failed to fetch the requested providers for %s in order to calculate their checksums: %s.", platform, err),
   242  			))
   243  			break
   244  		}
   245  		updatedLocks[platform] = newLocks
   246  	}
   247  
   248  	// If we have any error diagnostics from installation then we won't
   249  	// proceed to merging and updating the lock file on disk.
   250  	if diags.HasErrors() {
   251  		c.showDiagnostics(diags)
   252  		return 1
   253  	}
   254  
   255  	// We now have a separate updated locks object for each platform. We need
   256  	// to merge those all together so that the final result has the union of
   257  	// all of the checksums we saw for each of the providers we've worked on.
   258  	//
   259  	// We'll copy the old locks first because we want to retain any existing
   260  	// locks for providers that we _didn't_ visit above.
   261  	newLocks := oldLocks.DeepCopy()
   262  	for provider := range reqs {
   263  		oldLock := oldLocks.Provider(provider)
   264  
   265  		var version getproviders.Version
   266  		var constraints getproviders.VersionConstraints
   267  		var hashes []getproviders.Hash
   268  		if oldLock != nil {
   269  			version = oldLock.Version()
   270  			constraints = oldLock.VersionConstraints()
   271  			hashes = append(hashes, oldLock.AllHashes()...)
   272  		}
   273  		for _, platformLocks := range updatedLocks {
   274  			platformLock := platformLocks.Provider(provider)
   275  			if platformLock == nil {
   276  				continue // weird, but we'll tolerate it to avoid crashing
   277  			}
   278  			version = platformLock.Version()
   279  			constraints = platformLock.VersionConstraints()
   280  
   281  			// We don't make any effort to deduplicate hashes between different
   282  			// platforms here, because the SetProvider method we call below
   283  			// handles that automatically.
   284  			hashes = append(hashes, platformLock.AllHashes()...)
   285  		}
   286  		newLocks.SetProvider(provider, version, constraints, hashes)
   287  	}
   288  
   289  	moreDiags = c.replaceLockedDependencies(newLocks)
   290  	diags = diags.Append(moreDiags)
   291  
   292  	c.showDiagnostics(diags)
   293  	if diags.HasErrors() {
   294  		return 1
   295  	}
   296  
   297  	c.Ui.Output(c.Colorize().Color("\n[bold][green]Success![reset] [bold]Terraform has updated the lock file.[reset]"))
   298  	c.Ui.Output("\nReview the changes in .iaas-rpc.lock and then commit to your\nversion control system to retain the new checksums.\n")
   299  	return 0
   300  }
   301  
   302  func (c *ProvidersLockCommand) Help() string {
   303  	return `
   304  Usage: terraform [global options] providers lock [options] [providers...]
   305  
   306    Normally the dependency lock file (.iaas-rpc.lock) is updated
   307    automatically by "terraform init", but the information available to the
   308    normal provider installer can be constrained when you're installing providers
   309    from filesystem or network mirrors, and so the generated lock file can end
   310    up incomplete.
   311  
   312    The "providers lock" subcommand addresses that by updating the lock file
   313    based on the official packages available in the origin registry, ignoring
   314    the currently-configured installation strategy.
   315  
   316    After this command succeeds, the lock file will contain suitable checksums
   317    to allow installation of the providers needed by the current configuration
   318    on all of the selected platforms.
   319  
   320    By default this command updates the lock file for every provider declared
   321    in the configuration. You can override that behavior by providing one or
   322    more provider source addresses on the command line.
   323  
   324  Options:
   325  
   326    -fs-mirror=dir     Consult the given filesystem mirror directory instead
   327                       of the origin registry for each of the given providers.
   328  
   329                       This would be necessary to generate lock file entries for
   330                       a provider that is available only via a mirror, and not
   331                       published in an upstream registry. In this case, the set
   332                       of valid checksums will be limited only to what Terraform
   333                       can learn from the data in the mirror directory.
   334  
   335    -net-mirror=url    Consult the given network mirror (given as a base URL)
   336                       instead of the origin registry for each of the given
   337                       providers.
   338  
   339                       This would be necessary to generate lock file entries for
   340                       a provider that is available only via a mirror, and not
   341                       published in an upstream registry. In this case, the set
   342                       of valid checksums will be limited only to what Terraform
   343                       can learn from the data in the mirror indices.
   344  
   345    -platform=os_arch  Choose a target platform to request package checksums
   346                       for.
   347  
   348                       By default Terraform will request package checksums
   349                       suitable only for the platform where you run this
   350                       command. Use this option multiple times to include
   351                       checksums for multiple target systems.
   352  
   353                       Target names consist of an operating system and a CPU
   354                       architecture. For example, "linux_amd64" selects the
   355                       Linux operating system running on an AMD64 or x86_64
   356                       CPU. Each provider is available only for a limited
   357                       set of target platforms.
   358  `
   359  }