kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/configs/provisioner.go (about)

     1  package configs
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/hashicorp/hcl/v2"
     7  )
     8  
     9  // Provisioner represents a "provisioner" block when used within a
    10  // "resource" block in a module or file.
    11  type Provisioner struct {
    12  	Type       string
    13  	Config     hcl.Body
    14  	Connection *Connection
    15  	When       ProvisionerWhen
    16  	OnFailure  ProvisionerOnFailure
    17  
    18  	DeclRange hcl.Range
    19  	TypeRange hcl.Range
    20  }
    21  
    22  func decodeProvisionerBlock(block *hcl.Block) (*Provisioner, hcl.Diagnostics) {
    23  	pv := &Provisioner{
    24  		Type:      block.Labels[0],
    25  		TypeRange: block.LabelRanges[0],
    26  		DeclRange: block.DefRange,
    27  		When:      ProvisionerWhenCreate,
    28  		OnFailure: ProvisionerOnFailureFail,
    29  	}
    30  
    31  	content, config, diags := block.Body.PartialContent(provisionerBlockSchema)
    32  	pv.Config = config
    33  
    34  	switch pv.Type {
    35  	case "chef", "habitat", "puppet", "salt-masterless":
    36  		diags = append(diags, &hcl.Diagnostic{
    37  			Severity: hcl.DiagError,
    38  			Summary:  fmt.Sprintf("The \"%s\" provisioner has been removed", pv.Type),
    39  			Detail:   fmt.Sprintf("The \"%s\" provisioner was deprecated in Terraform 0.13.4 has been removed from Terraform. Visit https://learn.hashicorp.com/collections/terraform/provision for alternatives to using provisioners that are a better fit for the Terraform workflow.", pv.Type),
    40  			Subject:  &pv.TypeRange,
    41  		})
    42  		return nil, diags
    43  	}
    44  
    45  	if attr, exists := content.Attributes["when"]; exists {
    46  		expr, shimDiags := shimTraversalInString(attr.Expr, true)
    47  		diags = append(diags, shimDiags...)
    48  
    49  		switch hcl.ExprAsKeyword(expr) {
    50  		case "create":
    51  			pv.When = ProvisionerWhenCreate
    52  		case "destroy":
    53  			pv.When = ProvisionerWhenDestroy
    54  		default:
    55  			diags = append(diags, &hcl.Diagnostic{
    56  				Severity: hcl.DiagError,
    57  				Summary:  "Invalid \"when\" keyword",
    58  				Detail:   "The \"when\" argument requires one of the following keywords: create or destroy.",
    59  				Subject:  expr.Range().Ptr(),
    60  			})
    61  		}
    62  	}
    63  
    64  	// destroy provisioners can only refer to self
    65  	if pv.When == ProvisionerWhenDestroy {
    66  		diags = append(diags, onlySelfRefs(config)...)
    67  	}
    68  
    69  	if attr, exists := content.Attributes["on_failure"]; exists {
    70  		expr, shimDiags := shimTraversalInString(attr.Expr, true)
    71  		diags = append(diags, shimDiags...)
    72  
    73  		switch hcl.ExprAsKeyword(expr) {
    74  		case "continue":
    75  			pv.OnFailure = ProvisionerOnFailureContinue
    76  		case "fail":
    77  			pv.OnFailure = ProvisionerOnFailureFail
    78  		default:
    79  			diags = append(diags, &hcl.Diagnostic{
    80  				Severity: hcl.DiagError,
    81  				Summary:  "Invalid \"on_failure\" keyword",
    82  				Detail:   "The \"on_failure\" argument requires one of the following keywords: continue or fail.",
    83  				Subject:  attr.Expr.Range().Ptr(),
    84  			})
    85  		}
    86  	}
    87  
    88  	var seenConnection *hcl.Block
    89  	var seenEscapeBlock *hcl.Block
    90  	for _, block := range content.Blocks {
    91  		switch block.Type {
    92  		case "_":
    93  			if seenEscapeBlock != nil {
    94  				diags = append(diags, &hcl.Diagnostic{
    95  					Severity: hcl.DiagError,
    96  					Summary:  "Duplicate escaping block",
    97  					Detail: fmt.Sprintf(
    98  						"The special block type \"_\" can be used to force particular arguments to be interpreted as provisioner-typpe-specific rather than as meta-arguments, but each provisioner block can have only one such block. The first escaping block was at %s.",
    99  						seenEscapeBlock.DefRange,
   100  					),
   101  					Subject: &block.DefRange,
   102  				})
   103  				continue
   104  			}
   105  			seenEscapeBlock = block
   106  
   107  			// When there's an escaping block its content merges with the
   108  			// existing config we extracted earlier, so later decoding
   109  			// will see a blend of both.
   110  			pv.Config = hcl.MergeBodies([]hcl.Body{pv.Config, block.Body})
   111  
   112  		case "connection":
   113  			if seenConnection != nil {
   114  				diags = append(diags, &hcl.Diagnostic{
   115  					Severity: hcl.DiagError,
   116  					Summary:  "Duplicate connection block",
   117  					Detail:   fmt.Sprintf("This provisioner already has a connection block at %s.", seenConnection.DefRange),
   118  					Subject:  &block.DefRange,
   119  				})
   120  				continue
   121  			}
   122  			seenConnection = block
   123  
   124  			// destroy provisioners can only refer to self
   125  			if pv.When == ProvisionerWhenDestroy {
   126  				diags = append(diags, onlySelfRefs(block.Body)...)
   127  			}
   128  
   129  			pv.Connection = &Connection{
   130  				Config:    block.Body,
   131  				DeclRange: block.DefRange,
   132  			}
   133  
   134  		default:
   135  			// Any other block types are ones we've reserved for future use,
   136  			// so they get a generic message.
   137  			diags = append(diags, &hcl.Diagnostic{
   138  				Severity: hcl.DiagError,
   139  				Summary:  "Reserved block type name in provisioner block",
   140  				Detail:   fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type),
   141  				Subject:  &block.TypeRange,
   142  			})
   143  		}
   144  	}
   145  
   146  	return pv, diags
   147  }
   148  
   149  func onlySelfRefs(body hcl.Body) hcl.Diagnostics {
   150  	var diags hcl.Diagnostics
   151  
   152  	// Provisioners currently do not use any blocks in their configuration.
   153  	// Blocks are likely to remain solely for meta parameters, but in the case
   154  	// that blocks are supported for provisioners, we will want to extend this
   155  	// to find variables in nested blocks.
   156  	attrs, _ := body.JustAttributes()
   157  	for _, attr := range attrs {
   158  		for _, v := range attr.Expr.Variables() {
   159  			valid := false
   160  			switch v.RootName() {
   161  			case "self", "path", "terraform":
   162  				valid = true
   163  			case "count":
   164  				// count must use "index"
   165  				if len(v) == 2 {
   166  					if t, ok := v[1].(hcl.TraverseAttr); ok && t.Name == "index" {
   167  						valid = true
   168  					}
   169  				}
   170  
   171  			case "each":
   172  				if len(v) == 2 {
   173  					if t, ok := v[1].(hcl.TraverseAttr); ok && t.Name == "key" {
   174  						valid = true
   175  					}
   176  				}
   177  			}
   178  
   179  			if !valid {
   180  				diags = append(diags, &hcl.Diagnostic{
   181  					Severity: hcl.DiagError,
   182  					Summary:  "Invalid reference from destroy provisioner",
   183  					Detail: "Destroy-time provisioners and their connection configurations may only " +
   184  						"reference attributes of the related resource, via 'self', 'count.index', " +
   185  						"or 'each.key'.\n\nReferences to other resources during the destroy phase " +
   186  						"can cause dependency cycles and interact poorly with create_before_destroy.",
   187  					Subject: attr.Expr.Range().Ptr(),
   188  				})
   189  			}
   190  		}
   191  	}
   192  	return diags
   193  }
   194  
   195  // Connection represents a "connection" block when used within either a
   196  // "resource" or "provisioner" block in a module or file.
   197  type Connection struct {
   198  	Config hcl.Body
   199  
   200  	DeclRange hcl.Range
   201  }
   202  
   203  // ProvisionerWhen is an enum for valid values for when to run provisioners.
   204  type ProvisionerWhen int
   205  
   206  //go:generate go run golang.org/x/tools/cmd/stringer -type ProvisionerWhen
   207  
   208  const (
   209  	ProvisionerWhenInvalid ProvisionerWhen = iota
   210  	ProvisionerWhenCreate
   211  	ProvisionerWhenDestroy
   212  )
   213  
   214  // ProvisionerOnFailure is an enum for valid values for on_failure options
   215  // for provisioners.
   216  type ProvisionerOnFailure int
   217  
   218  //go:generate go run golang.org/x/tools/cmd/stringer -type ProvisionerOnFailure
   219  
   220  const (
   221  	ProvisionerOnFailureInvalid ProvisionerOnFailure = iota
   222  	ProvisionerOnFailureContinue
   223  	ProvisionerOnFailureFail
   224  )
   225  
   226  var provisionerBlockSchema = &hcl.BodySchema{
   227  	Attributes: []hcl.AttributeSchema{
   228  		{Name: "when"},
   229  		{Name: "on_failure"},
   230  	},
   231  	Blocks: []hcl.BlockHeaderSchema{
   232  		{Type: "_"}, // meta-argument escaping block
   233  
   234  		{Type: "connection"},
   235  		{Type: "lifecycle"}, // reserved for future use
   236  	},
   237  }