kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/command/providers_lock.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "net/url" 7 "os" 8 9 "kubeform.dev/terraform-backend-sdk/addrs" 10 "kubeform.dev/terraform-backend-sdk/depsfile" 11 "kubeform.dev/terraform-backend-sdk/getproviders" 12 "kubeform.dev/terraform-backend-sdk/providercache" 13 "kubeform.dev/terraform-backend-sdk/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 provider argument", 132 fmt.Sprintf("The provider %s is not required by the current configuration.", addr.String()), 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 .terraform.lock.hcl 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 (.terraform.lock.hcl) 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 }