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 }