github.com/erriapo/terraform@v0.6.12-0.20160203182612-0340ea72354f/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  		Update: resourceCloudinitConfigCreate,
    23  		Exists: resourceCloudinitConfigExists,
    24  		Read:   resourceCloudinitConfigRead,
    25  
    26  		Schema: map[string]*schema.Schema{
    27  			"part": &schema.Schema{
    28  				Type:     schema.TypeList,
    29  				Required: true,
    30  				Elem: &schema.Resource{
    31  					Schema: map[string]*schema.Schema{
    32  						"content_type": &schema.Schema{
    33  							Type:     schema.TypeString,
    34  							Optional: true,
    35  						},
    36  						"content": &schema.Schema{
    37  							Type:     schema.TypeString,
    38  							Required: true,
    39  						},
    40  						"filename": &schema.Schema{
    41  							Type:     schema.TypeString,
    42  							Optional: true,
    43  						},
    44  						"merge_type": &schema.Schema{
    45  							Type:     schema.TypeString,
    46  							Optional: true,
    47  						},
    48  					},
    49  				},
    50  			},
    51  			"gzip": &schema.Schema{
    52  				Type:     schema.TypeBool,
    53  				Optional: true,
    54  				Default:  true,
    55  				ForceNew: true,
    56  			},
    57  			"base64_encode": &schema.Schema{
    58  				Type:     schema.TypeBool,
    59  				Optional: true,
    60  				Default:  true,
    61  				ForceNew: true,
    62  			},
    63  			"rendered": &schema.Schema{
    64  				Type:        schema.TypeString,
    65  				Computed:    true,
    66  				Description: "rendered cloudinit configuration",
    67  			},
    68  		},
    69  	}
    70  }
    71  
    72  func resourceCloudinitConfigCreate(d *schema.ResourceData, meta interface{}) error {
    73  	rendered, err := renderCloudinitConfig(d)
    74  	if err != nil {
    75  		return err
    76  	}
    77  
    78  	d.Set("rendered", rendered)
    79  	d.SetId(strconv.Itoa(hashcode.String(rendered)))
    80  	return nil
    81  }
    82  
    83  func resourceCloudinitConfigDelete(d *schema.ResourceData, meta interface{}) error {
    84  	d.SetId("")
    85  	return nil
    86  }
    87  
    88  func resourceCloudinitConfigExists(d *schema.ResourceData, meta interface{}) (bool, error) {
    89  	rendered, err := renderCloudinitConfig(d)
    90  	if err != nil {
    91  		return false, err
    92  	}
    93  
    94  	return strconv.Itoa(hashcode.String(rendered)) == d.Id(), nil
    95  }
    96  
    97  func resourceCloudinitConfigRead(d *schema.ResourceData, meta interface{}) error {
    98  	return nil
    99  }
   100  
   101  func renderCloudinitConfig(d *schema.ResourceData) (string, error) {
   102  	gzipOutput := d.Get("gzip").(bool)
   103  	base64Output := d.Get("base64_encode").(bool)
   104  
   105  	partsValue, hasParts := d.GetOk("part")
   106  	if !hasParts {
   107  		return "", fmt.Errorf("No parts found in the cloudinit resource declaration")
   108  	}
   109  
   110  	cloudInitParts := make(cloudInitParts, len(partsValue.([]interface{})))
   111  	for i, v := range partsValue.([]interface{}) {
   112  		p := v.(map[string]interface{})
   113  
   114  		part := cloudInitPart{}
   115  		if p, ok := p["content_type"]; ok {
   116  			part.ContentType = p.(string)
   117  		}
   118  		if p, ok := p["content"]; ok {
   119  			part.Content = p.(string)
   120  		}
   121  		if p, ok := p["merge_type"]; ok {
   122  			part.MergeType = p.(string)
   123  		}
   124  		if p, ok := p["filename"]; ok {
   125  			part.Filename = p.(string)
   126  		}
   127  		cloudInitParts[i] = part
   128  	}
   129  
   130  	var buffer bytes.Buffer
   131  
   132  	var err error
   133  	if gzipOutput {
   134  		gzipWriter := gzip.NewWriter(&buffer)
   135  		err = renderPartsToWriter(cloudInitParts, gzipWriter)
   136  		gzipWriter.Close()
   137  	} else {
   138  		err = renderPartsToWriter(cloudInitParts, &buffer)
   139  	}
   140  	if err != nil {
   141  		return "", err
   142  	}
   143  
   144  	output := ""
   145  	if base64Output {
   146  		output = base64.StdEncoding.EncodeToString(buffer.Bytes())
   147  	} else {
   148  		output = buffer.String()
   149  	}
   150  
   151  	return output, nil
   152  }
   153  
   154  func renderPartsToWriter(parts cloudInitParts, writer io.Writer) error {
   155  	mimeWriter := multipart.NewWriter(writer)
   156  	defer mimeWriter.Close()
   157  
   158  	// we need to set the boundary explictly, otherwise the boundary is random
   159  	// and this causes terraform to complain about the resource being different
   160  	if err := mimeWriter.SetBoundary("MIMEBOUNDARY"); err != nil {
   161  		return err
   162  	}
   163  
   164  	writer.Write([]byte(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\n", mimeWriter.Boundary())))
   165  	writer.Write([]byte("MIME-Version: 1.0\r\n"))
   166  
   167  	for _, part := range parts {
   168  		header := textproto.MIMEHeader{}
   169  		if part.ContentType == "" {
   170  			header.Set("Content-Type", "text/plain")
   171  		} else {
   172  			header.Set("Content-Type", part.ContentType)
   173  		}
   174  
   175  		header.Set("MIME-Version", "1.0")
   176  		header.Set("Content-Transfer-Encoding", "7bit")
   177  
   178  		if part.Filename != "" {
   179  			header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, part.Filename))
   180  		}
   181  
   182  		if part.MergeType != "" {
   183  			header.Set("X-Merge-Type", part.MergeType)
   184  		}
   185  
   186  		partWriter, err := mimeWriter.CreatePart(header)
   187  		if err != nil {
   188  			return err
   189  		}
   190  
   191  		_, err = partWriter.Write([]byte(part.Content))
   192  		if err != nil {
   193  			return err
   194  		}
   195  	}
   196  
   197  	return nil
   198  }
   199  
   200  type cloudInitPart struct {
   201  	ContentType string
   202  	MergeType   string
   203  	Filename    string
   204  	Content     string
   205  }
   206  
   207  type cloudInitParts []cloudInitPart