github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/terraform/schemas.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package terraform
     5  
     6  import (
     7  	"fmt"
     8  	"log"
     9  
    10  	"github.com/terramate-io/tf/addrs"
    11  	"github.com/terramate-io/tf/configs"
    12  	"github.com/terramate-io/tf/configs/configschema"
    13  	"github.com/terramate-io/tf/providers"
    14  	"github.com/terramate-io/tf/states"
    15  	"github.com/terramate-io/tf/tfdiags"
    16  )
    17  
    18  // Schemas is a container for various kinds of schema that Terraform needs
    19  // during processing.
    20  type Schemas struct {
    21  	Providers    map[addrs.Provider]*ProviderSchema
    22  	Provisioners map[string]*configschema.Block
    23  }
    24  
    25  // ProviderSchema returns the entire ProviderSchema object that was produced
    26  // by the plugin for the given provider, or nil if no such schema is available.
    27  //
    28  // It's usually better to go use the more precise methods offered by type
    29  // Schemas to handle this detail automatically.
    30  func (ss *Schemas) ProviderSchema(provider addrs.Provider) *ProviderSchema {
    31  	if ss.Providers == nil {
    32  		return nil
    33  	}
    34  	return ss.Providers[provider]
    35  }
    36  
    37  // ProviderConfig returns the schema for the provider configuration of the
    38  // given provider type, or nil if no such schema is available.
    39  func (ss *Schemas) ProviderConfig(provider addrs.Provider) *configschema.Block {
    40  	ps := ss.ProviderSchema(provider)
    41  	if ps == nil {
    42  		return nil
    43  	}
    44  	return ps.Provider
    45  }
    46  
    47  // ResourceTypeConfig returns the schema for the configuration of a given
    48  // resource type belonging to a given provider type, or nil of no such
    49  // schema is available.
    50  //
    51  // In many cases the provider type is inferrable from the resource type name,
    52  // but this is not always true because users can override the provider for
    53  // a resource using the "provider" meta-argument. Therefore it's important to
    54  // always pass the correct provider name, even though it many cases it feels
    55  // redundant.
    56  func (ss *Schemas) ResourceTypeConfig(provider addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (block *configschema.Block, schemaVersion uint64) {
    57  	ps := ss.ProviderSchema(provider)
    58  	if ps == nil || ps.ResourceTypes == nil {
    59  		return nil, 0
    60  	}
    61  	return ps.SchemaForResourceType(resourceMode, resourceType)
    62  }
    63  
    64  // ProvisionerConfig returns the schema for the configuration of a given
    65  // provisioner, or nil of no such schema is available.
    66  func (ss *Schemas) ProvisionerConfig(name string) *configschema.Block {
    67  	return ss.Provisioners[name]
    68  }
    69  
    70  // LoadSchemas searches the given configuration, state  and plan (any of which
    71  // may be nil) for constructs that have an associated schema, requests the
    72  // necessary schemas from the given component factory (which must _not_ be nil),
    73  // and returns a single object representing all of the necessary schemas.
    74  //
    75  // If an error is returned, it may be a wrapped tfdiags.Diagnostics describing
    76  // errors across multiple separate objects. Errors here will usually indicate
    77  // either misbehavior on the part of one of the providers or of the provider
    78  // protocol itself. When returned with errors, the returned schemas object is
    79  // still valid but may be incomplete.
    80  func LoadSchemas(config *configs.Config, state *states.State, components contextComponentFactory) (*Schemas, error) {
    81  	schemas := &Schemas{
    82  		Providers:    map[addrs.Provider]*ProviderSchema{},
    83  		Provisioners: map[string]*configschema.Block{},
    84  	}
    85  	var diags tfdiags.Diagnostics
    86  
    87  	newDiags := loadProviderSchemas(schemas.Providers, config, state, components)
    88  	diags = diags.Append(newDiags)
    89  	newDiags = loadProvisionerSchemas(schemas.Provisioners, config, components)
    90  	diags = diags.Append(newDiags)
    91  
    92  	return schemas, diags.Err()
    93  }
    94  
    95  func loadProviderSchemas(schemas map[addrs.Provider]*ProviderSchema, config *configs.Config, state *states.State, components contextComponentFactory) tfdiags.Diagnostics {
    96  	var diags tfdiags.Diagnostics
    97  
    98  	ensure := func(fqn addrs.Provider) {
    99  		name := fqn.String()
   100  
   101  		if _, exists := schemas[fqn]; exists {
   102  			return
   103  		}
   104  
   105  		log.Printf("[TRACE] LoadSchemas: retrieving schema for provider type %q", name)
   106  		provider, err := components.ResourceProvider(fqn)
   107  		if err != nil {
   108  			// We'll put a stub in the map so we won't re-attempt this on
   109  			// future calls.
   110  			schemas[fqn] = &ProviderSchema{}
   111  			diags = diags.Append(
   112  				fmt.Errorf("Failed to instantiate provider %q to obtain schema: %s", name, err),
   113  			)
   114  			return
   115  		}
   116  		defer func() {
   117  			provider.Close()
   118  		}()
   119  
   120  		resp := provider.GetProviderSchema()
   121  		if resp.Diagnostics.HasErrors() {
   122  			// We'll put a stub in the map so we won't re-attempt this on
   123  			// future calls.
   124  			schemas[fqn] = &ProviderSchema{}
   125  			diags = diags.Append(
   126  				fmt.Errorf("Failed to retrieve schema from provider %q: %s", name, resp.Diagnostics.Err()),
   127  			)
   128  			return
   129  		}
   130  
   131  		s := &ProviderSchema{
   132  			Provider:      resp.Provider.Block,
   133  			ResourceTypes: make(map[string]*configschema.Block),
   134  			DataSources:   make(map[string]*configschema.Block),
   135  
   136  			ResourceTypeSchemaVersions: make(map[string]uint64),
   137  		}
   138  
   139  		if resp.Provider.Version < 0 {
   140  			// We're not using the version numbers here yet, but we'll check
   141  			// for validity anyway in case we start using them in future.
   142  			diags = diags.Append(
   143  				fmt.Errorf("invalid negative schema version provider configuration for provider %q", name),
   144  			)
   145  		}
   146  
   147  		for t, r := range resp.ResourceTypes {
   148  			s.ResourceTypes[t] = r.Block
   149  			s.ResourceTypeSchemaVersions[t] = uint64(r.Version)
   150  			if r.Version < 0 {
   151  				diags = diags.Append(
   152  					fmt.Errorf("invalid negative schema version for resource type %s in provider %q", t, name),
   153  				)
   154  			}
   155  		}
   156  
   157  		for t, d := range resp.DataSources {
   158  			s.DataSources[t] = d.Block
   159  			if d.Version < 0 {
   160  				// We're not using the version numbers here yet, but we'll check
   161  				// for validity anyway in case we start using them in future.
   162  				diags = diags.Append(
   163  					fmt.Errorf("invalid negative schema version for data source %s in provider %q", t, name),
   164  				)
   165  			}
   166  		}
   167  
   168  		schemas[fqn] = s
   169  
   170  		if resp.ProviderMeta.Block != nil {
   171  			s.ProviderMeta = resp.ProviderMeta.Block
   172  		}
   173  	}
   174  
   175  	if config != nil {
   176  		for _, fqn := range config.ProviderTypes() {
   177  			ensure(fqn)
   178  		}
   179  	}
   180  
   181  	if state != nil {
   182  		needed := providers.AddressedTypesAbs(state.ProviderAddrs())
   183  		for _, typeAddr := range needed {
   184  			ensure(typeAddr)
   185  		}
   186  	}
   187  
   188  	return diags
   189  }
   190  
   191  func loadProvisionerSchemas(schemas map[string]*configschema.Block, config *configs.Config, components contextComponentFactory) tfdiags.Diagnostics {
   192  	var diags tfdiags.Diagnostics
   193  
   194  	ensure := func(name string) {
   195  		if _, exists := schemas[name]; exists {
   196  			return
   197  		}
   198  
   199  		log.Printf("[TRACE] LoadSchemas: retrieving schema for provisioner %q", name)
   200  		provisioner, err := components.ResourceProvisioner(name)
   201  		if err != nil {
   202  			// We'll put a stub in the map so we won't re-attempt this on
   203  			// future calls.
   204  			schemas[name] = &configschema.Block{}
   205  			diags = diags.Append(
   206  				fmt.Errorf("Failed to instantiate provisioner %q to obtain schema: %s", name, err),
   207  			)
   208  			return
   209  		}
   210  		defer func() {
   211  			if closer, ok := provisioner.(ResourceProvisionerCloser); ok {
   212  				closer.Close()
   213  			}
   214  		}()
   215  
   216  		resp := provisioner.GetSchema()
   217  		if resp.Diagnostics.HasErrors() {
   218  			// We'll put a stub in the map so we won't re-attempt this on
   219  			// future calls.
   220  			schemas[name] = &configschema.Block{}
   221  			diags = diags.Append(
   222  				fmt.Errorf("Failed to retrieve schema from provisioner %q: %s", name, resp.Diagnostics.Err()),
   223  			)
   224  			return
   225  		}
   226  
   227  		schemas[name] = resp.Provisioner
   228  	}
   229  
   230  	if config != nil {
   231  		for _, rc := range config.Module.ManagedResources {
   232  			for _, pc := range rc.Managed.Provisioners {
   233  				ensure(pc.Type)
   234  			}
   235  		}
   236  
   237  		// Must also visit our child modules, recursively.
   238  		for _, cc := range config.Children {
   239  			childDiags := loadProvisionerSchemas(schemas, cc, components)
   240  			diags = diags.Append(childDiags)
   241  		}
   242  	}
   243  
   244  	return diags
   245  }
   246  
   247  // ProviderSchema represents the schema for a provider's own configuration
   248  // and the configuration for some or all of its resources and data sources.
   249  //
   250  // The completeness of this structure depends on how it was constructed.
   251  // When constructed for a configuration, it will generally include only
   252  // resource types and data sources used by that configuration.
   253  type ProviderSchema struct {
   254  	Provider      *configschema.Block
   255  	ProviderMeta  *configschema.Block
   256  	ResourceTypes map[string]*configschema.Block
   257  	DataSources   map[string]*configschema.Block
   258  
   259  	ResourceTypeSchemaVersions map[string]uint64
   260  }
   261  
   262  // SchemaForResourceType attempts to find a schema for the given mode and type.
   263  // Returns nil if no such schema is available.
   264  func (ps *ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema *configschema.Block, version uint64) {
   265  	switch mode {
   266  	case addrs.ManagedResourceMode:
   267  		return ps.ResourceTypes[typeName], ps.ResourceTypeSchemaVersions[typeName]
   268  	case addrs.DataResourceMode:
   269  		// Data resources don't have schema versions right now, since state is discarded for each refresh
   270  		return ps.DataSources[typeName], 0
   271  	default:
   272  		// Shouldn't happen, because the above cases are comprehensive.
   273  		return nil, 0
   274  	}
   275  }
   276  
   277  // SchemaForResourceAddr attempts to find a schema for the mode and type from
   278  // the given resource address. Returns nil if no such schema is available.
   279  func (ps *ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema *configschema.Block, version uint64) {
   280  	return ps.SchemaForResourceType(addr.Mode, addr.Type)
   281  }
   282  
   283  // ProviderSchemaRequest is used to describe to a ResourceProvider which
   284  // aspects of schema are required, when calling the GetSchema method.
   285  type ProviderSchemaRequest struct {
   286  	ResourceTypes []string
   287  	DataSources   []string
   288  }