| package command |
| |
| import ( |
| "fmt" |
| "io/ioutil" |
| "net/url" |
| "os" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/depsfile" |
| "github.com/hashicorp/terraform/internal/getproviders" |
| "github.com/hashicorp/terraform/internal/providercache" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| ) |
| |
| type providersLockChangeType string |
| |
| const ( |
| providersLockChangeTypeNoChange providersLockChangeType = "providersLockChangeTypeNoChange" |
| providersLockChangeTypeNewProvider providersLockChangeType = "providersLockChangeTypeNewProvider" |
| providersLockChangeTypeNewHashes providersLockChangeType = "providersLockChangeTypeNewHashes" |
| ) |
| |
| // ProvidersLockCommand is a Command implementation that implements the |
| // "terraform providers lock" command, which creates or updates the current |
| // configuration's dependency lock file using information from upstream |
| // registries, regardless of the provider installation configuration that |
| // is configured for normal provider installation. |
| type ProvidersLockCommand struct { |
| Meta |
| } |
| |
| func (c *ProvidersLockCommand) Synopsis() string { |
| return "Write out dependency locks for the configured providers" |
| } |
| |
| func (c *ProvidersLockCommand) Run(args []string) int { |
| args = c.Meta.process(args) |
| cmdFlags := c.Meta.defaultFlagSet("providers lock") |
| var optPlatforms FlagStringSlice |
| var fsMirrorDir string |
| var netMirrorURL string |
| cmdFlags.Var(&optPlatforms, "platform", "target platform") |
| cmdFlags.StringVar(&fsMirrorDir, "fs-mirror", "", "filesystem mirror directory") |
| cmdFlags.StringVar(&netMirrorURL, "net-mirror", "", "network mirror base URL") |
| cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } |
| if err := cmdFlags.Parse(args); err != nil { |
| c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) |
| return 1 |
| } |
| |
| var diags tfdiags.Diagnostics |
| |
| if fsMirrorDir != "" && netMirrorURL != "" { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Invalid installation method options", |
| "The -fs-mirror and -net-mirror command line options are mutually-exclusive.", |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| providerStrs := cmdFlags.Args() |
| |
| var platforms []getproviders.Platform |
| if len(optPlatforms) == 0 { |
| platforms = []getproviders.Platform{getproviders.CurrentPlatform} |
| } else { |
| platforms = make([]getproviders.Platform, 0, len(optPlatforms)) |
| for _, platformStr := range optPlatforms { |
| platform, err := getproviders.ParsePlatform(platformStr) |
| if err != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Invalid target platform", |
| fmt.Sprintf("The string %q given in the -platform option is not a valid target platform: %s.", platformStr, err), |
| )) |
| continue |
| } |
| platforms = append(platforms, platform) |
| } |
| } |
| |
| // Unlike other commands, this command ignores the installation methods |
| // selected in the CLI configuration and instead chooses an installation |
| // method based on CLI options. |
| // |
| // This is so that folks who use a local mirror for everyday use can |
| // use this command to populate their lock files from upstream so |
| // subsequent "terraform init" calls can then verify the local mirror |
| // against the upstream checksums. |
| var source getproviders.Source |
| switch { |
| case fsMirrorDir != "": |
| source = getproviders.NewFilesystemMirrorSource(fsMirrorDir) |
| case netMirrorURL != "": |
| u, err := url.Parse(netMirrorURL) |
| if err != nil || u.Scheme != "https" { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Invalid network mirror URL", |
| "The -net-mirror option requires a valid https: URL as the mirror base URL.", |
| )) |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| source = getproviders.NewHTTPMirrorSource(u, c.Services.CredentialsSource()) |
| default: |
| // With no special options we consult upstream registries directly, |
| // because that gives us the most information to produce as complete |
| // and portable as possible a lock entry. |
| source = getproviders.NewRegistrySource(c.Services) |
| } |
| |
| config, confDiags := c.loadConfig(".") |
| diags = diags.Append(confDiags) |
| reqs, hclDiags := config.ProviderRequirements() |
| diags = diags.Append(hclDiags) |
| |
| // If we have explicit provider selections on the command line then |
| // we'll modify "reqs" to only include those. Modifying this is okay |
| // because config.ProviderRequirements generates a fresh map result |
| // for each call. |
| if len(providerStrs) != 0 { |
| providers := map[addrs.Provider]struct{}{} |
| for _, raw := range providerStrs { |
| addr, moreDiags := addrs.ParseProviderSourceString(raw) |
| diags = diags.Append(moreDiags) |
| if moreDiags.HasErrors() { |
| continue |
| } |
| providers[addr] = struct{}{} |
| if _, exists := reqs[addr]; !exists { |
| // Can't request a provider that isn't required by the |
| // current configuration. |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Invalid provider argument", |
| fmt.Sprintf("The provider %s is not required by the current configuration.", addr.String()), |
| )) |
| } |
| } |
| |
| for addr := range reqs { |
| if _, exists := providers[addr]; !exists { |
| delete(reqs, addr) |
| } |
| } |
| } |
| |
| // We'll also ignore any providers that don't participate in locking. |
| for addr := range reqs { |
| if !depsfile.ProviderIsLockable(addr) { |
| delete(reqs, addr) |
| } |
| } |
| |
| // We'll start our work with whatever locks we already have, so that |
| // we'll honor any existing version selections and just add additional |
| // hashes for them. |
| oldLocks, moreDiags := c.lockedDependencies() |
| diags = diags.Append(moreDiags) |
| |
| // If we have any error diagnostics already then we won't proceed further. |
| if diags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| // Our general strategy here is to install the requested providers into |
| // a separate temporary directory -- thus ensuring that the results won't |
| // ever be inadvertently executed by other Terraform commands -- and then |
| // use the results of that installation to update the lock file for the |
| // current working directory. Because we throwaway the packages we |
| // downloaded after completing our work, a subsequent "terraform init" will |
| // then respect the CLI configuration's provider installation strategies |
| // but will verify the packages against the hashes we found upstream. |
| |
| // Because our Installer abstraction is a per-platform idea, we'll |
| // instantiate one for each of the platforms the user requested, and then |
| // merge all of the generated locks together at the end. |
| updatedLocks := map[getproviders.Platform]*depsfile.Locks{} |
| selectedVersions := map[addrs.Provider]getproviders.Version{} |
| ctx, cancel := c.InterruptibleContext() |
| defer cancel() |
| for _, platform := range platforms { |
| tempDir, err := ioutil.TempDir("", "terraform-providers-lock") |
| if err != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Could not create temporary directory", |
| fmt.Sprintf("Failed to create a temporary directory for staging the requested provider packages: %s.", err), |
| )) |
| break |
| } |
| defer os.RemoveAll(tempDir) |
| |
| evts := &providercache.InstallerEvents{ |
| // Our output from this command is minimal just to show that |
| // we're making progress, rather than just silently hanging. |
| FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, loc getproviders.PackageLocation) { |
| c.Ui.Output(fmt.Sprintf("- Fetching %s %s for %s...", provider.ForDisplay(), version, platform)) |
| if prevVersion, exists := selectedVersions[provider]; exists && version != prevVersion { |
| // This indicates a weird situation where we ended up |
| // selecting a different version for one platform than |
| // for another. We won't be able to merge the result |
| // in that case, so we'll generate an error. |
| // |
| // This could potentially happen if there's a provider |
| // we've not previously recorded in the lock file and |
| // the available versions change while we're running. To |
| // avoid that would require pre-locking all of the |
| // providers, which is complicated to do with the building |
| // blocks we have here, and so we'll wait to do it only |
| // if this situation arises often in practice. |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Inconsistent provider versions", |
| fmt.Sprintf( |
| "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.", |
| provider, |
| ), |
| )) |
| } |
| selectedVersions[provider] = version |
| }, |
| FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, auth *getproviders.PackageAuthenticationResult) { |
| var keyID string |
| if auth != nil && auth.ThirdPartySigned() { |
| keyID = auth.KeyID |
| } |
| if keyID != "" { |
| keyID = c.Colorize().Color(fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID)) |
| } |
| c.Ui.Output(fmt.Sprintf("- Retrieved %s %s for %s (%s%s)", provider.ForDisplay(), version, platform, auth, keyID)) |
| }, |
| } |
| ctx := evts.OnContext(ctx) |
| |
| dir := providercache.NewDirWithPlatform(tempDir, platform) |
| installer := providercache.NewInstaller(dir, source) |
| |
| newLocks, err := installer.EnsureProviderVersions(ctx, oldLocks, reqs, providercache.InstallNewProvidersForce) |
| if err != nil { |
| diags = diags.Append(tfdiags.Sourceless( |
| tfdiags.Error, |
| "Could not retrieve providers for locking", |
| fmt.Sprintf("Terraform failed to fetch the requested providers for %s in order to calculate their checksums: %s.", platform, err), |
| )) |
| break |
| } |
| updatedLocks[platform] = newLocks |
| } |
| |
| // If we have any error diagnostics from installation then we won't |
| // proceed to merging and updating the lock file on disk. |
| if diags.HasErrors() { |
| c.showDiagnostics(diags) |
| return 1 |
| } |
| |
| // Track whether we've made any changes to the lock file as part of this |
| // operation. We can customise the final message based on our actions. |
| madeAnyChange := false |
| |
| // We now have a separate updated locks object for each platform. We need |
| // to merge those all together so that the final result has the union of |
| // all of the checksums we saw for each of the providers we've worked on. |
| // |
| // We'll copy the old locks first because we want to retain any existing |
| // locks for providers that we _didn't_ visit above. |
| newLocks := oldLocks.DeepCopy() |
| for provider := range reqs { |
| oldLock := oldLocks.Provider(provider) |
| |
| var version getproviders.Version |
| var constraints getproviders.VersionConstraints |
| var hashes []getproviders.Hash |
| if oldLock != nil { |
| version = oldLock.Version() |
| constraints = oldLock.VersionConstraints() |
| hashes = append(hashes, oldLock.AllHashes()...) |
| } |
| for platform, platformLocks := range updatedLocks { |
| platformLock := platformLocks.Provider(provider) |
| if platformLock == nil { |
| continue // weird, but we'll tolerate it to avoid crashing |
| } |
| version = platformLock.Version() |
| constraints = platformLock.VersionConstraints() |
| |
| // We don't make any effort to deduplicate hashes between different |
| // platforms here, because the SetProvider method we call below |
| // handles that automatically. |
| hashes = append(hashes, platformLock.AllHashes()...) |
| |
| // At this point, we've merged all the hashes for this (provider, platform) |
| // combo into the combined hashes for this provider. Let's take this |
| // opportunity to print out a summary for this particular combination. |
| switch providersLockCalculateChangeType(oldLock, platformLock) { |
| case providersLockChangeTypeNewProvider: |
| madeAnyChange = true |
| c.Ui.Output( |
| fmt.Sprintf( |
| "- Obtained %s checksums for %s; This was a new provider and the checksums for this platform are now tracked in the lock file", |
| provider.ForDisplay(), |
| platform)) |
| case providersLockChangeTypeNewHashes: |
| madeAnyChange = true |
| c.Ui.Output( |
| fmt.Sprintf( |
| "- Obtained %s checksums for %s; Additional checksums for this platform are now tracked in the lock file", |
| provider.ForDisplay(), |
| platform)) |
| case providersLockChangeTypeNoChange: |
| c.Ui.Output( |
| fmt.Sprintf( |
| "- Obtained %s checksums for %s; All checksums for this platform were already tracked in the lock file", |
| provider.ForDisplay(), |
| platform)) |
| } |
| } |
| newLocks.SetProvider(provider, version, constraints, hashes) |
| } |
| |
| moreDiags = c.replaceLockedDependencies(newLocks) |
| diags = diags.Append(moreDiags) |
| |
| c.showDiagnostics(diags) |
| if diags.HasErrors() { |
| return 1 |
| } |
| |
| if madeAnyChange { |
| c.Ui.Output(c.Colorize().Color("\n[bold][green]Success![reset] [bold]Terraform has updated the lock file.[reset]")) |
| c.Ui.Output("\nReview the changes in .terraform.lock.hcl and then commit to your\nversion control system to retain the new checksums.\n") |
| } else { |
| c.Ui.Output(c.Colorize().Color("\n[bold][green]Success![reset] [bold]Terraform has validated the lock file and found no need for changes.[reset]")) |
| } |
| return 0 |
| } |
| |
| func (c *ProvidersLockCommand) Help() string { |
| return ` |
| Usage: terraform [global options] providers lock [options] [providers...] |
| |
| Normally the dependency lock file (.terraform.lock.hcl) is updated |
| automatically by "terraform init", but the information available to the |
| normal provider installer can be constrained when you're installing providers |
| from filesystem or network mirrors, and so the generated lock file can end |
| up incomplete. |
| |
| The "providers lock" subcommand addresses that by updating the lock file |
| based on the official packages available in the origin registry, ignoring |
| the currently-configured installation strategy. |
| |
| After this command succeeds, the lock file will contain suitable checksums |
| to allow installation of the providers needed by the current configuration |
| on all of the selected platforms. |
| |
| By default this command updates the lock file for every provider declared |
| in the configuration. You can override that behavior by providing one or |
| more provider source addresses on the command line. |
| |
| Options: |
| |
| -fs-mirror=dir Consult the given filesystem mirror directory instead |
| of the origin registry for each of the given providers. |
| |
| This would be necessary to generate lock file entries for |
| a provider that is available only via a mirror, and not |
| published in an upstream registry. In this case, the set |
| of valid checksums will be limited only to what Terraform |
| can learn from the data in the mirror directory. |
| |
| -net-mirror=url Consult the given network mirror (given as a base URL) |
| instead of the origin registry for each of the given |
| providers. |
| |
| This would be necessary to generate lock file entries for |
| a provider that is available only via a mirror, and not |
| published in an upstream registry. In this case, the set |
| of valid checksums will be limited only to what Terraform |
| can learn from the data in the mirror indices. |
| |
| -platform=os_arch Choose a target platform to request package checksums |
| for. |
| |
| By default Terraform will request package checksums |
| suitable only for the platform where you run this |
| command. Use this option multiple times to include |
| checksums for multiple target systems. |
| |
| Target names consist of an operating system and a CPU |
| architecture. For example, "linux_amd64" selects the |
| Linux operating system running on an AMD64 or x86_64 |
| CPU. Each provider is available only for a limited |
| set of target platforms. |
| ` |
| } |
| |
| // providersLockCalculateChangeType works out whether there is any difference |
| // between oldLock and newLock and returns a variable the main function can use |
| // to decide on which message to print. |
| // |
| // One assumption made here that is not obvious without the context from the |
| // main function is that while platformLock contains the lock information for a |
| // single platform after the current run, oldLock contains the combined |
| // information of all platforms from when the versions were last checked. A |
| // simple equality check is not sufficient for deciding on change as we expect |
| // that oldLock will be a superset of platformLock if no new hashes have been |
| // found. |
| // |
| // We've separated this function out so we can write unit tests around the |
| // logic. This function assumes the platformLock is not nil, as the main |
| // function explicitly checks this before calling this function. |
| func providersLockCalculateChangeType(oldLock *depsfile.ProviderLock, platformLock *depsfile.ProviderLock) providersLockChangeType { |
| if oldLock == nil { |
| return providersLockChangeTypeNewProvider |
| } |
| if oldLock.ContainsAll(platformLock) { |
| return providersLockChangeTypeNoChange |
| } |
| return providersLockChangeTypeNewHashes |
| } |