github.com/pmcatominey/terraform@v0.7.0-rc2.0.20160708105029-1401a52a5cc5/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