github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/meta_providers.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package command 5 6 import ( 7 "bytes" 8 "errors" 9 "fmt" 10 "log" 11 "os" 12 "os/exec" 13 "strings" 14 15 plugin "github.com/hashicorp/go-plugin" 16 17 "github.com/terramate-io/tf/addrs" 18 terraformProvider "github.com/terramate-io/tf/builtin/providers/terraform" 19 "github.com/terramate-io/tf/getproviders" 20 "github.com/terramate-io/tf/logging" 21 tfplugin "github.com/terramate-io/tf/plugin" 22 tfplugin6 "github.com/terramate-io/tf/plugin6" 23 "github.com/terramate-io/tf/providercache" 24 "github.com/terramate-io/tf/providers" 25 "github.com/terramate-io/tf/tfdiags" 26 ) 27 28 // The TF_DISABLE_PLUGIN_TLS environment variable is intended only for use by 29 // the plugin SDK test framework, to reduce startup overhead when rapidly 30 // launching and killing lots of instances of the same provider. 31 // 32 // This is not intended to be set by end-users. 33 var enableProviderAutoMTLS = os.Getenv("TF_DISABLE_PLUGIN_TLS") == "" 34 35 // providerInstaller returns an object that knows how to install providers and 36 // how to recover the selections from a prior installation process. 37 // 38 // The resulting provider installer is constructed from the results of 39 // the other methods providerLocalCacheDir, providerGlobalCacheDir, and 40 // providerInstallSource. 41 // 42 // Only one object returned from this method should be live at any time, 43 // because objects inside contain caches that must be maintained properly. 44 // Because this method wraps a result from providerLocalCacheDir, that 45 // limitation applies also to results from that method. 46 func (m *Meta) providerInstaller() *providercache.Installer { 47 return m.providerInstallerCustomSource(m.providerInstallSource()) 48 } 49 50 // providerInstallerCustomSource is a variant of providerInstaller that 51 // allows the caller to specify a different installation source than the one 52 // that would naturally be selected. 53 // 54 // The result of this method has the same dependencies and constraints as 55 // providerInstaller. 56 // 57 // The result of providerInstallerCustomSource differs from 58 // providerInstaller only in how it determines package installation locations 59 // during EnsureProviderVersions. A caller that doesn't call 60 // EnsureProviderVersions (anything other than "terraform init") can safely 61 // just use the providerInstaller method unconditionally. 62 func (m *Meta) providerInstallerCustomSource(source getproviders.Source) *providercache.Installer { 63 targetDir := m.providerLocalCacheDir() 64 globalCacheDir := m.providerGlobalCacheDir() 65 inst := providercache.NewInstaller(targetDir, source) 66 if globalCacheDir != nil { 67 inst.SetGlobalCacheDir(globalCacheDir) 68 inst.SetGlobalCacheDirMayBreakDependencyLockFile(m.PluginCacheMayBreakDependencyLockFile) 69 } 70 var builtinProviderTypes []string 71 for ty := range m.internalProviders() { 72 builtinProviderTypes = append(builtinProviderTypes, ty) 73 } 74 inst.SetBuiltInProviderTypes(builtinProviderTypes) 75 unmanagedProviderTypes := make(map[addrs.Provider]struct{}, len(m.UnmanagedProviders)) 76 for ty := range m.UnmanagedProviders { 77 unmanagedProviderTypes[ty] = struct{}{} 78 } 79 inst.SetUnmanagedProviderTypes(unmanagedProviderTypes) 80 return inst 81 } 82 83 // providerCustomLocalDirectorySource produces a provider source that consults 84 // only the given local filesystem directories for plugins to install. 85 // 86 // This is used to implement the -plugin-dir option for "terraform init", where 87 // the result of this method is used instead of what would've been returned 88 // from m.providerInstallSource. 89 // 90 // If the given list of directories is empty then the resulting source will 91 // have no providers available for installation at all. 92 func (m *Meta) providerCustomLocalDirectorySource(dirs []string) getproviders.Source { 93 var ret getproviders.MultiSource 94 for _, dir := range dirs { 95 ret = append(ret, getproviders.MultiSourceSelector{ 96 Source: getproviders.NewFilesystemMirrorSource(dir), 97 }) 98 } 99 return ret 100 } 101 102 // providerLocalCacheDir returns an object representing the 103 // configuration-specific local cache directory. This is the 104 // only location consulted for provider plugin packages for Terraform 105 // operations other than provider installation. 106 // 107 // Only the provider installer (in "terraform init") is permitted to make 108 // modifications to this cache directory. All other commands must treat it 109 // as read-only. 110 // 111 // Only one object returned from this method should be live at any time, 112 // because objects inside contain caches that must be maintained properly. 113 func (m *Meta) providerLocalCacheDir() *providercache.Dir { 114 m.fixupMissingWorkingDir() 115 dir := m.WorkingDir.ProviderLocalCacheDir() 116 return providercache.NewDir(dir) 117 } 118 119 // providerGlobalCacheDir returns an object representing the shared global 120 // provider cache directory, used as a read-through cache when installing 121 // new provider plugin packages. 122 // 123 // This function may return nil, in which case there is no global cache 124 // configured and new packages should be downloaded directly into individual 125 // configuration-specific cache directories. 126 // 127 // Only one object returned from this method should be live at any time, 128 // because objects inside contain caches that must be maintained properly. 129 func (m *Meta) providerGlobalCacheDir() *providercache.Dir { 130 dir := m.PluginCacheDir 131 if dir == "" { 132 return nil // cache disabled 133 } 134 return providercache.NewDir(dir) 135 } 136 137 // providerInstallSource returns an object that knows how to consult one or 138 // more external sources to determine the availability of and package 139 // locations for versions of Terraform providers that are available for 140 // automatic installation. 141 // 142 // This returns the standard provider install source that consults a number 143 // of directories selected either automatically or via the CLI configuration. 144 // Users may choose to override this during a "terraform init" command by 145 // specifying one or more -plugin-dir options, in which case the installation 146 // process will construct its own source consulting only those directories 147 // and use that instead. 148 func (m *Meta) providerInstallSource() getproviders.Source { 149 // A provider source should always be provided in normal use, but our 150 // unit tests might not always populate Meta fully and so we'll be robust 151 // by returning a non-nil source that just always answers that no plugins 152 // are available. 153 if m.ProviderSource == nil { 154 // A multi-source with no underlying sources is effectively an 155 // always-empty source. 156 return getproviders.MultiSource(nil) 157 } 158 return m.ProviderSource 159 } 160 161 // providerDevOverrideInitWarnings returns a diagnostics that contains at 162 // least one warning if and only if there is at least one provider development 163 // override in effect. If not, the result is always empty. The result never 164 // contains error diagnostics. 165 // 166 // The init command can use this to include a warning that the results 167 // may differ from what's expected due to the development overrides. For 168 // other commands, providerDevOverrideRuntimeWarnings should be used. 169 func (m *Meta) providerDevOverrideInitWarnings() tfdiags.Diagnostics { 170 if len(m.ProviderDevOverrides) == 0 { 171 return nil 172 } 173 var detailMsg strings.Builder 174 detailMsg.WriteString("The following provider development overrides are set in the CLI configuration:\n") 175 for addr, path := range m.ProviderDevOverrides { 176 detailMsg.WriteString(fmt.Sprintf(" - %s in %s\n", addr.ForDisplay(), path)) 177 } 178 detailMsg.WriteString("\nSkip terraform init when using provider development overrides. It is not necessary and may error unexpectedly.") 179 return tfdiags.Diagnostics{ 180 tfdiags.Sourceless( 181 tfdiags.Warning, 182 "Provider development overrides are in effect", 183 detailMsg.String(), 184 ), 185 } 186 } 187 188 // providerDevOverrideRuntimeWarnings returns a diagnostics that contains at 189 // least one warning if and only if there is at least one provider development 190 // override in effect. If not, the result is always empty. The result never 191 // contains error diagnostics. 192 // 193 // Certain commands can use this to include a warning that their results 194 // may differ from what's expected due to the development overrides. It's 195 // not necessary to bother the user with this warning on every command, but 196 // it's helpful to return it on commands that have externally-visible side 197 // effects and on commands that are used to verify conformance to schemas. 198 // 199 // See providerDevOverrideInitWarnings for warnings specific to the init 200 // command. 201 func (m *Meta) providerDevOverrideRuntimeWarnings() tfdiags.Diagnostics { 202 if len(m.ProviderDevOverrides) == 0 { 203 return nil 204 } 205 var detailMsg strings.Builder 206 detailMsg.WriteString("The following provider development overrides are set in the CLI configuration:\n") 207 for addr, path := range m.ProviderDevOverrides { 208 detailMsg.WriteString(fmt.Sprintf(" - %s in %s\n", addr.ForDisplay(), path)) 209 } 210 detailMsg.WriteString("\nThe behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases.") 211 return tfdiags.Diagnostics{ 212 tfdiags.Sourceless( 213 tfdiags.Warning, 214 "Provider development overrides are in effect", 215 detailMsg.String(), 216 ), 217 } 218 } 219 220 // providerFactories uses the selections made previously by an installer in 221 // the local cache directory (m.providerLocalCacheDir) to produce a map 222 // from provider addresses to factory functions to create instances of 223 // those providers. 224 // 225 // providerFactories will return an error if the installer's selections cannot 226 // be honored with what is currently in the cache, such as if a selected 227 // package has been removed from the cache or if the contents of a selected 228 // package have been modified outside of the installer. If it returns an error, 229 // the returned map may be incomplete or invalid, but will be as complete 230 // as possible given the cause of the error. 231 func (m *Meta) providerFactories() (map[addrs.Provider]providers.Factory, error) { 232 locks, diags := m.lockedDependencies() 233 if diags.HasErrors() { 234 return nil, fmt.Errorf("failed to read dependency lock file: %s", diags.Err()) 235 } 236 237 // We'll always run through all of our providers, even if one of them 238 // encounters an error, so that we can potentially report multiple errors 239 // where appropriate and so that callers can potentially make use of the 240 // partial result we return if e.g. they want to enumerate which providers 241 // are available, or call into one of the providers that didn't fail. 242 errs := make(map[addrs.Provider]error) 243 244 // For the providers from the lock file, we expect them to be already 245 // available in the provider cache because "terraform init" should already 246 // have put them there. 247 providerLocks := locks.AllProviders() 248 cacheDir := m.providerLocalCacheDir() 249 250 // The internal providers are _always_ available, even if the configuration 251 // doesn't request them, because they don't need any special installation 252 // and they'll just be ignored if not used. 253 internalFactories := m.internalProviders() 254 255 // We have two different special cases aimed at provider development 256 // use-cases, which are not for "production" use: 257 // - The CLI config can specify that a particular provider should always 258 // use a plugin from a particular local directory, ignoring anything the 259 // lock file or cache directory might have to say about it. This is useful 260 // for manual testing of local development builds. 261 // - The Terraform SDK test harness (and possibly other callers in future) 262 // can ask that we use its own already-started provider servers, which we 263 // call "unmanaged" because Terraform isn't responsible for starting 264 // and stopping them. This is intended for automated testing where a 265 // calling harness is responsible both for starting the provider server 266 // and orchestrating one or more non-interactive Terraform runs that then 267 // exercise it. 268 // Unmanaged providers take precedence over overridden providers because 269 // overrides are typically a "session-level" setting while unmanaged 270 // providers are typically scoped to a single unattended command. 271 devOverrideProviders := m.ProviderDevOverrides 272 unmanagedProviders := m.UnmanagedProviders 273 274 factories := make(map[addrs.Provider]providers.Factory, len(providerLocks)+len(internalFactories)+len(unmanagedProviders)) 275 for name, factory := range internalFactories { 276 factories[addrs.NewBuiltInProvider(name)] = factory 277 } 278 for provider, lock := range providerLocks { 279 reportError := func(thisErr error) { 280 errs[provider] = thisErr 281 // We'll populate a provider factory that just echoes our error 282 // again if called, which allows us to still report a helpful 283 // error even if it gets detected downstream somewhere from the 284 // caller using our partial result. 285 factories[provider] = providerFactoryError(thisErr) 286 } 287 288 if locks.ProviderIsOverridden(provider) { 289 // Overridden providers we'll handle with the other separate 290 // loops below, for dev overrides etc. 291 continue 292 } 293 294 version := lock.Version() 295 cached := cacheDir.ProviderVersion(provider, version) 296 if cached == nil { 297 reportError(fmt.Errorf( 298 "there is no package for %s %s cached in %s", 299 provider, version, cacheDir.BasePath(), 300 )) 301 continue 302 } 303 // The cached package must match one of the checksums recorded in 304 // the lock file, if any. 305 if allowedHashes := lock.PreferredHashes(); len(allowedHashes) != 0 { 306 matched, err := cached.MatchesAnyHash(allowedHashes) 307 if err != nil { 308 reportError(fmt.Errorf( 309 "failed to verify checksum of %s %s package cached in in %s: %s", 310 provider, version, cacheDir.BasePath(), err, 311 )) 312 continue 313 } 314 if !matched { 315 reportError(fmt.Errorf( 316 "the cached package for %s %s (in %s) does not match any of the checksums recorded in the dependency lock file", 317 provider, version, cacheDir.BasePath(), 318 )) 319 continue 320 } 321 } 322 factories[provider] = providerFactory(cached) 323 } 324 for provider, localDir := range devOverrideProviders { 325 factories[provider] = devOverrideProviderFactory(provider, localDir) 326 } 327 for provider, reattach := range unmanagedProviders { 328 factories[provider] = unmanagedProviderFactory(provider, reattach) 329 } 330 331 var err error 332 if len(errs) > 0 { 333 err = providerPluginErrors(errs) 334 } 335 return factories, err 336 } 337 338 func (m *Meta) internalProviders() map[string]providers.Factory { 339 return map[string]providers.Factory{ 340 "terraform": func() (providers.Interface, error) { 341 return terraformProvider.NewProvider(), nil 342 }, 343 } 344 } 345 346 // providerFactory produces a provider factory that runs up the executable 347 // file in the given cache package and uses go-plugin to implement 348 // providers.Interface against it. 349 func providerFactory(meta *providercache.CachedProvider) providers.Factory { 350 return func() (providers.Interface, error) { 351 execFile, err := meta.ExecutableFile() 352 if err != nil { 353 return nil, err 354 } 355 356 config := &plugin.ClientConfig{ 357 HandshakeConfig: tfplugin.Handshake, 358 Logger: logging.NewProviderLogger(""), 359 AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 360 Managed: true, 361 Cmd: exec.Command(execFile), 362 AutoMTLS: enableProviderAutoMTLS, 363 VersionedPlugins: tfplugin.VersionedPlugins, 364 SyncStdout: logging.PluginOutputMonitor(fmt.Sprintf("%s:stdout", meta.Provider)), 365 SyncStderr: logging.PluginOutputMonitor(fmt.Sprintf("%s:stderr", meta.Provider)), 366 } 367 368 client := plugin.NewClient(config) 369 rpcClient, err := client.Client() 370 if err != nil { 371 return nil, err 372 } 373 374 raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName) 375 if err != nil { 376 return nil, err 377 } 378 379 // store the client so that the plugin can kill the child process 380 protoVer := client.NegotiatedVersion() 381 switch protoVer { 382 case 5: 383 p := raw.(*tfplugin.GRPCProvider) 384 p.PluginClient = client 385 p.Addr = meta.Provider 386 return p, nil 387 case 6: 388 p := raw.(*tfplugin6.GRPCProvider) 389 p.PluginClient = client 390 p.Addr = meta.Provider 391 return p, nil 392 default: 393 panic("unsupported protocol version") 394 } 395 } 396 } 397 398 func devOverrideProviderFactory(provider addrs.Provider, localDir getproviders.PackageLocalDir) providers.Factory { 399 // A dev override is essentially a synthetic cache entry for our purposes 400 // here, so that's how we'll construct it. The providerFactory function 401 // doesn't actually care about the version, so we can leave it 402 // unspecified: overridden providers are not explicitly versioned. 403 log.Printf("[DEBUG] Provider %s is overridden to load from %s", provider, localDir) 404 return providerFactory(&providercache.CachedProvider{ 405 Provider: provider, 406 Version: getproviders.UnspecifiedVersion, 407 PackageDir: string(localDir), 408 }) 409 } 410 411 // unmanagedProviderFactory produces a provider factory that uses the passed 412 // reattach information to connect to go-plugin processes that are already 413 // running, and implements providers.Interface against it. 414 func unmanagedProviderFactory(provider addrs.Provider, reattach *plugin.ReattachConfig) providers.Factory { 415 return func() (providers.Interface, error) { 416 config := &plugin.ClientConfig{ 417 HandshakeConfig: tfplugin.Handshake, 418 Logger: logging.NewProviderLogger("unmanaged."), 419 AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 420 Managed: false, 421 Reattach: reattach, 422 SyncStdout: logging.PluginOutputMonitor(fmt.Sprintf("%s:stdout", provider)), 423 SyncStderr: logging.PluginOutputMonitor(fmt.Sprintf("%s:stderr", provider)), 424 } 425 426 if reattach.ProtocolVersion == 0 { 427 // As of the 0.15 release, sdk.v2 doesn't include the protocol 428 // version in the ReattachConfig (only recently added to 429 // go-plugin), so client.NegotiatedVersion() always returns 0. We 430 // assume that an unmanaged provider reporting protocol version 0 is 431 // actually using proto v5 for backwards compatibility. 432 if defaultPlugins, ok := tfplugin.VersionedPlugins[5]; ok { 433 config.Plugins = defaultPlugins 434 } else { 435 return nil, errors.New("no supported plugins for protocol 0") 436 } 437 } else if plugins, ok := tfplugin.VersionedPlugins[reattach.ProtocolVersion]; !ok { 438 return nil, fmt.Errorf("no supported plugins for protocol %d", reattach.ProtocolVersion) 439 } else { 440 config.Plugins = plugins 441 } 442 443 client := plugin.NewClient(config) 444 rpcClient, err := client.Client() 445 if err != nil { 446 return nil, err 447 } 448 449 raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName) 450 if err != nil { 451 return nil, err 452 } 453 454 // store the client so that the plugin can kill the child process 455 protoVer := client.NegotiatedVersion() 456 switch protoVer { 457 case 0, 5: 458 // As of the 0.15 release, sdk.v2 doesn't include the protocol 459 // version in the ReattachConfig (only recently added to 460 // go-plugin), so client.NegotiatedVersion() always returns 0. We 461 // assume that an unmanaged provider reporting protocol version 0 is 462 // actually using proto v5 for backwards compatibility. 463 p := raw.(*tfplugin.GRPCProvider) 464 p.PluginClient = client 465 return p, nil 466 case 6: 467 p := raw.(*tfplugin6.GRPCProvider) 468 p.PluginClient = client 469 return p, nil 470 default: 471 return nil, fmt.Errorf("unsupported protocol version %d", protoVer) 472 } 473 } 474 } 475 476 // providerFactoryError is a stub providers.Factory that returns an error 477 // when called. It's used to allow providerFactories to still produce a 478 // factory for each available provider in an error case, for situations 479 // where the caller can do something useful with that partial result. 480 func providerFactoryError(err error) providers.Factory { 481 return func() (providers.Interface, error) { 482 return nil, err 483 } 484 } 485 486 // providerPluginErrors is an error implementation we can return from 487 // Meta.providerFactories to capture potentially multiple errors about the 488 // locally-cached plugins (or lack thereof) for particular external providers. 489 // 490 // Some functions closer to the UI layer can sniff for this error type in order 491 // to return a more helpful error message. 492 type providerPluginErrors map[addrs.Provider]error 493 494 func (errs providerPluginErrors) Error() string { 495 if len(errs) == 1 { 496 for addr, err := range errs { 497 return fmt.Sprintf("%s: %s", addr, err) 498 } 499 } 500 var buf bytes.Buffer 501 fmt.Fprintf(&buf, "missing or corrupted provider plugins:") 502 for addr, err := range errs { 503 fmt.Fprintf(&buf, "\n - %s: %s", addr, err) 504 } 505 return buf.String() 506 }