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