github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/provisioner.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package schema
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"sync"
    11  
    12  	"github.com/hashicorp/go-multierror"
    13  	"github.com/terramate-io/tf/configs/configschema"
    14  	"github.com/terramate-io/tf/legacy/terraform"
    15  )
    16  
    17  // Provisioner represents a resource provisioner in Terraform and properly
    18  // implements all of the ResourceProvisioner API.
    19  //
    20  // This higher level structure makes it much easier to implement a new or
    21  // custom provisioner for Terraform.
    22  //
    23  // The function callbacks for this structure are all passed a context object.
    24  // This context object has a number of pre-defined values that can be accessed
    25  // via the global functions defined in context.go.
    26  type Provisioner struct {
    27  	// ConnSchema is the schema for the connection settings for this
    28  	// provisioner.
    29  	//
    30  	// The keys of this map are the configuration keys, and the value is
    31  	// the schema describing the value of the configuration.
    32  	//
    33  	// NOTE: The value of connection keys can only be strings for now.
    34  	ConnSchema map[string]*Schema
    35  
    36  	// Schema is the schema for the usage of this provisioner.
    37  	//
    38  	// The keys of this map are the configuration keys, and the value is
    39  	// the schema describing the value of the configuration.
    40  	Schema map[string]*Schema
    41  
    42  	// ApplyFunc is the function for executing the provisioner. This is required.
    43  	// It is given a context. See the Provisioner struct docs for more
    44  	// information.
    45  	ApplyFunc func(ctx context.Context) error
    46  
    47  	// ValidateFunc is a function for extended validation. This is optional
    48  	// and should be used when individual field validation is not enough.
    49  	ValidateFunc func(*terraform.ResourceConfig) ([]string, []error)
    50  
    51  	stopCtx       context.Context
    52  	stopCtxCancel context.CancelFunc
    53  	stopOnce      sync.Once
    54  }
    55  
    56  // Keys that can be used to access data in the context parameters for
    57  // Provisioners.
    58  var (
    59  	connDataInvalid = contextKey("data invalid")
    60  
    61  	// This returns a *ResourceData for the connection information.
    62  	// Guaranteed to never be nil.
    63  	ProvConnDataKey = contextKey("provider conn data")
    64  
    65  	// This returns a *ResourceData for the config information.
    66  	// Guaranteed to never be nil.
    67  	ProvConfigDataKey = contextKey("provider config data")
    68  
    69  	// This returns a terraform.UIOutput. Guaranteed to never be nil.
    70  	ProvOutputKey = contextKey("provider output")
    71  
    72  	// This returns the raw InstanceState passed to Apply. Guaranteed to
    73  	// be set, but may be nil.
    74  	ProvRawStateKey = contextKey("provider raw state")
    75  )
    76  
    77  // InternalValidate should be called to validate the structure
    78  // of the provisioner.
    79  //
    80  // This should be called in a unit test to verify before release that this
    81  // structure is properly configured for use.
    82  func (p *Provisioner) InternalValidate() error {
    83  	if p == nil {
    84  		return errors.New("provisioner is nil")
    85  	}
    86  
    87  	var validationErrors error
    88  	{
    89  		sm := schemaMap(p.ConnSchema)
    90  		if err := sm.InternalValidate(sm); err != nil {
    91  			validationErrors = multierror.Append(validationErrors, err)
    92  		}
    93  	}
    94  
    95  	{
    96  		sm := schemaMap(p.Schema)
    97  		if err := sm.InternalValidate(sm); err != nil {
    98  			validationErrors = multierror.Append(validationErrors, err)
    99  		}
   100  	}
   101  
   102  	if p.ApplyFunc == nil {
   103  		validationErrors = multierror.Append(validationErrors, fmt.Errorf(
   104  			"ApplyFunc must not be nil"))
   105  	}
   106  
   107  	return validationErrors
   108  }
   109  
   110  // StopContext returns a context that checks whether a provisioner is stopped.
   111  func (p *Provisioner) StopContext() context.Context {
   112  	p.stopOnce.Do(p.stopInit)
   113  	return p.stopCtx
   114  }
   115  
   116  func (p *Provisioner) stopInit() {
   117  	p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background())
   118  }
   119  
   120  // Stop implementation of terraform.ResourceProvisioner interface.
   121  func (p *Provisioner) Stop() error {
   122  	p.stopOnce.Do(p.stopInit)
   123  	p.stopCtxCancel()
   124  	return nil
   125  }
   126  
   127  // GetConfigSchema implementation of terraform.ResourceProvisioner interface.
   128  func (p *Provisioner) GetConfigSchema() (*configschema.Block, error) {
   129  	return schemaMap(p.Schema).CoreConfigSchema(), nil
   130  }
   131  
   132  // Apply implementation of terraform.ResourceProvisioner interface.
   133  func (p *Provisioner) Apply(
   134  	o terraform.UIOutput,
   135  	s *terraform.InstanceState,
   136  	c *terraform.ResourceConfig) error {
   137  	var connData, configData *ResourceData
   138  
   139  	{
   140  		// We first need to turn the connection information into a
   141  		// terraform.ResourceConfig so that we can use that type to more
   142  		// easily build a ResourceData structure. We do this by simply treating
   143  		// the conn info as configuration input.
   144  		raw := make(map[string]interface{})
   145  		if s != nil {
   146  			for k, v := range s.Ephemeral.ConnInfo {
   147  				raw[k] = v
   148  			}
   149  		}
   150  
   151  		c := terraform.NewResourceConfigRaw(raw)
   152  		sm := schemaMap(p.ConnSchema)
   153  		diff, err := sm.Diff(nil, c, nil, nil, true)
   154  		if err != nil {
   155  			return err
   156  		}
   157  		connData, err = sm.Data(nil, diff)
   158  		if err != nil {
   159  			return err
   160  		}
   161  	}
   162  
   163  	{
   164  		// Build the configuration data. Doing this requires making a "diff"
   165  		// even though that's never used. We use that just to get the correct types.
   166  		configMap := schemaMap(p.Schema)
   167  		diff, err := configMap.Diff(nil, c, nil, nil, true)
   168  		if err != nil {
   169  			return err
   170  		}
   171  		configData, err = configMap.Data(nil, diff)
   172  		if err != nil {
   173  			return err
   174  		}
   175  	}
   176  
   177  	// Build the context and call the function
   178  	ctx := p.StopContext()
   179  	ctx = context.WithValue(ctx, ProvConnDataKey, connData)
   180  	ctx = context.WithValue(ctx, ProvConfigDataKey, configData)
   181  	ctx = context.WithValue(ctx, ProvOutputKey, o)
   182  	ctx = context.WithValue(ctx, ProvRawStateKey, s)
   183  	return p.ApplyFunc(ctx)
   184  }
   185  
   186  // Validate implements the terraform.ResourceProvisioner interface.
   187  func (p *Provisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
   188  	if err := p.InternalValidate(); err != nil {
   189  		return nil, []error{fmt.Errorf(
   190  			"Internal validation of the provisioner failed! This is always a bug\n"+
   191  				"with the provisioner itself, and not a user issue. Please report\n"+
   192  				"this bug:\n\n%s", err)}
   193  	}
   194  
   195  	if p.Schema != nil {
   196  		w, e := schemaMap(p.Schema).Validate(c)
   197  		ws = append(ws, w...)
   198  		es = append(es, e...)
   199  	}
   200  
   201  	if p.ValidateFunc != nil {
   202  		w, e := p.ValidateFunc(c)
   203  		ws = append(ws, w...)
   204  		es = append(es, e...)
   205  	}
   206  
   207  	return ws, es
   208  }