github.com/opentofu/opentofu@v1.7.1/internal/tofu/context_plugins.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package tofu
     7  
     8  import (
     9  	"fmt"
    10  	"log"
    11  
    12  	"github.com/opentofu/opentofu/internal/addrs"
    13  	"github.com/opentofu/opentofu/internal/configs/configschema"
    14  	"github.com/opentofu/opentofu/internal/providers"
    15  	"github.com/opentofu/opentofu/internal/provisioners"
    16  )
    17  
    18  // contextPlugins represents a library of available plugins (providers and
    19  // provisioners) which we assume will all be used with the same
    20  // tofu.Context, and thus it'll be safe to cache certain information
    21  // about the providers for performance reasons.
    22  type contextPlugins struct {
    23  	providerFactories    map[addrs.Provider]providers.Factory
    24  	provisionerFactories map[string]provisioners.Factory
    25  }
    26  
    27  func newContextPlugins(providerFactories map[addrs.Provider]providers.Factory, provisionerFactories map[string]provisioners.Factory) (*contextPlugins, error) {
    28  	return &contextPlugins{
    29  		providerFactories:    providerFactories,
    30  		provisionerFactories: provisionerFactories,
    31  	}, nil // TODO remove error from this function call!
    32  }
    33  
    34  func (cp *contextPlugins) HasProvider(addr addrs.Provider) bool {
    35  	_, ok := cp.providerFactories[addr]
    36  	return ok
    37  }
    38  
    39  func (cp *contextPlugins) NewProviderInstance(addr addrs.Provider) (providers.Interface, error) {
    40  	f, ok := cp.providerFactories[addr]
    41  	if !ok {
    42  		return nil, fmt.Errorf("unavailable provider %q", addr.String())
    43  	}
    44  
    45  	return f()
    46  
    47  }
    48  
    49  func (cp *contextPlugins) HasProvisioner(typ string) bool {
    50  	_, ok := cp.provisionerFactories[typ]
    51  	return ok
    52  }
    53  
    54  func (cp *contextPlugins) NewProvisionerInstance(typ string) (provisioners.Interface, error) {
    55  	f, ok := cp.provisionerFactories[typ]
    56  	if !ok {
    57  		return nil, fmt.Errorf("unavailable provisioner %q", typ)
    58  	}
    59  
    60  	return f()
    61  }
    62  
    63  // ProviderSchema uses a temporary instance of the provider with the given
    64  // address to obtain the full schema for all aspects of that provider.
    65  //
    66  // ProviderSchema memoizes results by unique provider address, so it's fine
    67  // to repeatedly call this method with the same address if various different
    68  // parts of OpenTofu all need the same schema information.
    69  func (cp *contextPlugins) ProviderSchema(addr addrs.Provider) (providers.ProviderSchema, error) {
    70  	// Check the global schema cache first.
    71  	// This cache is only written by the provider client, and transparently
    72  	// used by GetProviderSchema, but we check it here because at this point we
    73  	// may be able to avoid spinning up the provider instance at all.
    74  	//
    75  	// It's worth noting that ServerCapabilities.GetProviderSchemaOptional is ignored here.
    76  	// That is because we're checking *prior* to the provider's instantiation.
    77  	// GetProviderSchemaOptional only says that *if we instantiate a provider*,
    78  	// then we need to run the get schema call at least once.
    79  	// BUG This SHORT CIRCUITS the logic below and is not the only code which inserts provider schemas into the cache!!
    80  	schemas, ok := providers.SchemaCache.Get(addr)
    81  	if ok {
    82  		log.Printf("[TRACE] tofu.contextPlugins: Serving provider %q schema from global schema cache", addr)
    83  		return schemas, nil
    84  	}
    85  
    86  	log.Printf("[TRACE] tofu.contextPlugins: Initializing provider %q to read its schema", addr)
    87  	provider, err := cp.NewProviderInstance(addr)
    88  	if err != nil {
    89  		return schemas, fmt.Errorf("failed to instantiate provider %q to obtain schema: %w", addr, err)
    90  	}
    91  	defer provider.Close()
    92  
    93  	resp := provider.GetProviderSchema()
    94  	if resp.Diagnostics.HasErrors() {
    95  		return resp, fmt.Errorf("failed to retrieve schema from provider %q: %w", addr, resp.Diagnostics.Err())
    96  	}
    97  
    98  	if resp.Provider.Version < 0 {
    99  		// We're not using the version numbers here yet, but we'll check
   100  		// for validity anyway in case we start using them in future.
   101  		return resp, fmt.Errorf("provider %s has invalid negative schema version for its configuration blocks,which is a bug in the provider ", addr)
   102  	}
   103  
   104  	for t, r := range resp.ResourceTypes {
   105  		if err := r.Block.InternalValidate(); err != nil {
   106  			return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, which is a bug in the provider: %w", addr, t, err)
   107  		}
   108  		if r.Version < 0 {
   109  			return resp, fmt.Errorf("provider %s has invalid negative schema version for managed resource type %q, which is a bug in the provider", addr, t)
   110  		}
   111  	}
   112  
   113  	for t, d := range resp.DataSources {
   114  		if err := d.Block.InternalValidate(); err != nil {
   115  			return resp, fmt.Errorf("provider %s has invalid schema for data resource type %q, which is a bug in the provider: %w", addr, t, err)
   116  		}
   117  		if d.Version < 0 {
   118  			// We're not using the version numbers here yet, but we'll check
   119  			// for validity anyway in case we start using them in future.
   120  			return resp, fmt.Errorf("provider %s has invalid negative schema version for data resource type %q, which is a bug in the provider", addr, t)
   121  		}
   122  	}
   123  
   124  	return resp, nil
   125  }
   126  
   127  // ProviderConfigSchema is a helper wrapper around ProviderSchema which first
   128  // reads the full schema of the given provider and then extracts just the
   129  // provider's configuration schema, which defines what's expected in a
   130  // "provider" block in the configuration when configuring this provider.
   131  func (cp *contextPlugins) ProviderConfigSchema(providerAddr addrs.Provider) (*configschema.Block, error) {
   132  	providerSchema, err := cp.ProviderSchema(providerAddr)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  
   137  	return providerSchema.Provider.Block, nil
   138  }
   139  
   140  // ResourceTypeSchema is a helper wrapper around ProviderSchema which first
   141  // reads the schema of the given provider and then tries to find the schema
   142  // for the resource type of the given resource mode in that provider.
   143  //
   144  // ResourceTypeSchema will return an error if the provider schema lookup
   145  // fails, but will return nil if the provider schema lookup succeeds but then
   146  // the provider doesn't have a resource of the requested type.
   147  //
   148  // Managed resource types have versioned schemas, so the second return value
   149  // is the current schema version number for the requested resource. The version
   150  // is irrelevant for other resource modes.
   151  func (cp *contextPlugins) ResourceTypeSchema(providerAddr addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (*configschema.Block, uint64, error) {
   152  	providerSchema, err := cp.ProviderSchema(providerAddr)
   153  	if err != nil {
   154  		return nil, 0, err
   155  	}
   156  
   157  	schema, version := providerSchema.SchemaForResourceType(resourceMode, resourceType)
   158  	return schema, version, nil
   159  }
   160  
   161  // ProvisionerSchema uses a temporary instance of the provisioner with the
   162  // given type name to obtain the schema for that provisioner's configuration.
   163  //
   164  // ProvisionerSchema memoizes results by provisioner type name, so it's fine
   165  // to repeatedly call this method with the same name if various different
   166  // parts of OpenTofu all need the same schema information.
   167  func (cp *contextPlugins) ProvisionerSchema(typ string) (*configschema.Block, error) {
   168  	log.Printf("[TRACE] tofu.contextPlugins: Initializing provisioner %q to read its schema", typ)
   169  	provisioner, err := cp.NewProvisionerInstance(typ)
   170  	if err != nil {
   171  		return nil, fmt.Errorf("failed to instantiate provisioner %q to obtain schema: %w", typ, err)
   172  	}
   173  	defer provisioner.Close()
   174  
   175  	resp := provisioner.GetSchema()
   176  	if resp.Diagnostics.HasErrors() {
   177  		return nil, fmt.Errorf("failed to retrieve schema from provisioner %q: %w", typ, resp.Diagnostics.Err())
   178  	}
   179  
   180  	return resp.Provisioner, nil
   181  }