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