github.com/leeprovoost/terraform@v0.6.10-0.20160119085442-96f3f76118e7/builtin/providers/template/resource_cloudinit_config.go (about)

     1  package template
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io"
     9  	"net/textproto"
    10  	"strconv"
    11  
    12  	"github.com/hashicorp/terraform/helper/hashcode"
    13  	"github.com/hashicorp/terraform/helper/schema"
    14  
    15  	"github.com/sthulb/mime/multipart"
    16  )
    17  
    18  func resourceCloudinitConfig() *schema.Resource {
    19  	return &schema.Resource{
    20  		Create: resourceCloudinitConfigCreate,
    21  		Delete: resourceCloudinitConfigDelete,
    22  		Exists: resourceCloudinitConfigExists,
    23  		Read:   resourceCloudinitConfigRead,
    24  
    25  		Schema: map[string]*schema.Schema{
    26  			"part": &schema.Schema{
    27  				Type:     schema.TypeList,
    28  				Required: true,
    29  				ForceNew: true,
    30  				Elem: &schema.Resource{
    31  					Schema: map[string]*schema.Schema{
    32  						"content_type": &schema.Schema{
    33  							Type:     schema.TypeString,
    34  							Optional: true,
    35  							ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
    36  								value := v.(string)
    37  
    38  								if _, supported := supportedContentTypes[value]; !supported {
    39  									errors = append(errors, fmt.Errorf("Part has an unsupported content type: %s", v))
    40  								}
    41  
    42  								return
    43  							},
    44  						},
    45  						"content": &schema.Schema{
    46  							Type:     schema.TypeString,
    47  							Required: true,
    48  						},
    49  						"filename": &schema.Schema{
    50  							Type:     schema.TypeString,
    51  							Optional: true,
    52  						},
    53  						"merge_type": &schema.Schema{
    54  							Type:     schema.TypeString,
    55  							Optional: true,
    56  						},
    57  					},
    58  				},
    59  			},
    60  			"gzip": &schema.Schema{
    61  				Type:     schema.TypeBool,
    62  				Optional: true,
    63  				Default:  true,
    64  				ForceNew: true,
    65  			},
    66  			"base64_encode": &schema.Schema{
    67  				Type:     schema.TypeBool,
    68  				Optional: true,
    69  				Default:  true,
    70  				ForceNew: true,
    71  			},
    72  			"rendered": &schema.Schema{
    73  				Type:        schema.TypeString,
    74  				Computed:    true,
    75  				Description: "rendered cloudinit configuration",
    76  			},
    77  		},
    78  	}
    79  }
    80  
    81  func resourceCloudinitConfigCreate(d *schema.ResourceData, meta interface{}) error {
    82  	rendered, err := renderCloudinitConfig(d)
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	d.Set("rendered", rendered)
    88  	d.SetId(strconv.Itoa(hashcode.String(rendered)))
    89  	return nil
    90  }
    91  
    92  func resourceCloudinitConfigDelete(d *schema.ResourceData, meta interface{}) error {
    93  	d.SetId("")
    94  	return nil
    95  }
    96  
    97  func resourceCloudinitConfigExists(d *schema.ResourceData, meta interface{}) (bool, error) {
    98  	rendered, err := renderCloudinitConfig(d)
    99  	if err != nil {
   100  		return false, err
   101  	}
   102  
   103  	return strconv.Itoa(hashcode.String(rendered)) == d.Id(), nil
   104  }
   105  
   106  func resourceCloudinitConfigRead(d *schema.ResourceData, meta interface{}) error {
   107  	return nil
   108  }
   109  
   110  func renderCloudinitConfig(d *schema.ResourceData) (string, error) {
   111  	gzipOutput := d.Get("gzip").(bool)
   112  	base64Output := d.Get("base64_encode").(bool)
   113  
   114  	partsValue, hasParts := d.GetOk("part")
   115  	if !hasParts {
   116  		return "", fmt.Errorf("No parts found in the cloudinit resource declaration")
   117  	}
   118  
   119  	cloudInitParts := make(cloudInitParts, len(partsValue.([]interface{})))
   120  	for i, v := range partsValue.([]interface{}) {
   121  		p := v.(map[string]interface{})
   122  
   123  		part := cloudInitPart{}
   124  		if p, ok := p["content_type"]; ok {
   125  			part.ContentType = p.(string)
   126  		}
   127  		if p, ok := p["content"]; ok {
   128  			part.Content = p.(string)
   129  		}
   130  		if p, ok := p["merge_type"]; ok {
   131  			part.MergeType = p.(string)
   132  		}
   133  		if p, ok := p["filename"]; ok {
   134  			part.Filename = p.(string)
   135  		}
   136  		cloudInitParts[i] = part
   137  	}
   138  
   139  	var buffer bytes.Buffer
   140  
   141  	var err error
   142  	if gzipOutput {
   143  		gzipWriter := gzip.NewWriter(&buffer)
   144  		err = renderPartsToWriter(cloudInitParts, gzipWriter)
   145  		gzipWriter.Close()
   146  	} else {
   147  		err = renderPartsToWriter(cloudInitParts, &buffer)
   148  	}
   149  	if err != nil {
   150  		return "", err
   151  	}
   152  
   153  	output := ""
   154  	if base64Output {
   155  		output = base64.StdEncoding.EncodeToString(buffer.Bytes())
   156  	} else {
   157  		output = buffer.String()
   158  	}
   159  
   160  	return output, nil
   161  }
   162  
   163  func renderPartsToWriter(parts cloudInitParts, writer io.Writer) error {
   164  	mimeWriter := multipart.NewWriter(writer)
   165  	defer mimeWriter.Close()
   166  
   167  	// we need to set the boundary explictly, otherwise the boundary is random
   168  	// and this causes terraform to complain about the resource being different
   169  	if err := mimeWriter.SetBoundary("MIMEBOUNDRY"); err != nil {
   170  		return err
   171  	}
   172  
   173  	writer.Write([]byte(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\n", mimeWriter.Boundary())))
   174  	writer.Write([]byte("MIME-Version: 1.0\r\n"))
   175  
   176  	for _, part := range parts {
   177  		header := textproto.MIMEHeader{}
   178  		if part.ContentType == "" {
   179  			header.Set("Content-Type", "text/plain")
   180  		} else {
   181  			header.Set("Content-Type", part.ContentType)
   182  		}
   183  
   184  		header.Set("MIME-Version", "1.0")
   185  		header.Set("Content-Transfer-Encoding", "7bit")
   186  
   187  		if part.Filename != "" {
   188  			header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, part.Filename))
   189  		}
   190  
   191  		if part.MergeType != "" {
   192  			header.Set("X-Merge-Type", part.MergeType)
   193  		}
   194  
   195  		partWriter, err := mimeWriter.CreatePart(header)
   196  		if err != nil {
   197  			return err
   198  		}
   199  
   200  		_, err = partWriter.Write([]byte(part.Content))
   201  		if err != nil {
   202  			return err
   203  		}
   204  	}
   205  
   206  	return nil
   207  }
   208  
   209  type cloudInitPart struct {
   210  	ContentType string
   211  	MergeType   string
   212  	Filename    string
   213  	Content     string
   214  }
   215  
   216  type cloudInitParts []cloudInitPart
   217  
   218  // Support content types as specified by http://cloudinit.readthedocs.org/en/latest/topics/format.html
   219  var supportedContentTypes = map[string]bool{
   220  	"text/x-include-once-url":   true,
   221  	"text/x-include-url":        true,
   222  	"text/cloud-config-archive": true,
   223  	"text/upstart-job":          true,
   224  	"text/cloud-config":         true,
   225  	"text/part-handler":         true,
   226  	"text/x-shellscript":        true,
   227  	"text/cloud-boothook":       true,
   228  }