Skip to content

Commit f923368

Browse files
committed
Merge pull request hashicorp#672 from plan3-labs/heroku_cert_support
Heroku SSL certificate support
2 parents 9f0c887 + 765cd1f commit f923368

6 files changed

Lines changed: 348 additions & 0 deletions

File tree

builtin/providers/heroku/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func Provider() terraform.ResourceProvider {
3030
"heroku_addon": resourceHerokuAddon(),
3131
"heroku_domain": resourceHerokuDomain(),
3232
"heroku_drain": resourceHerokuDrain(),
33+
"heroku_cert": resourceHerokuCert(),
3334
},
3435

3536
ConfigureFunc: providerConfigure,
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package heroku
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
"github.com/cyberdelia/heroku-go/v3"
8+
"github.com/hashicorp/terraform/helper/schema"
9+
)
10+
11+
func resourceHerokuCert() *schema.Resource {
12+
return &schema.Resource{
13+
Create: resourceHerokuCertCreate,
14+
Read: resourceHerokuCertRead,
15+
Update: resourceHerokuCertUpdate,
16+
Delete: resourceHerokuCertDelete,
17+
18+
Schema: map[string]*schema.Schema{
19+
"app": &schema.Schema{
20+
Type: schema.TypeString,
21+
Required: true,
22+
ForceNew: true,
23+
},
24+
25+
"certificate_chain": &schema.Schema{
26+
Type: schema.TypeString,
27+
Required: true,
28+
},
29+
30+
"private_key": &schema.Schema{
31+
Type: schema.TypeString,
32+
Required: true,
33+
},
34+
35+
"cname": &schema.Schema{
36+
Type: schema.TypeString,
37+
Computed: true,
38+
},
39+
40+
"name": &schema.Schema{
41+
Type: schema.TypeString,
42+
Computed: true,
43+
},
44+
},
45+
}
46+
}
47+
48+
func resourceHerokuCertCreate(d *schema.ResourceData, meta interface{}) error {
49+
client := meta.(*heroku.Service)
50+
51+
app := d.Get("app").(string)
52+
preprocess := true
53+
opts := heroku.SSLEndpointCreateOpts{
54+
CertificateChain: d.Get("certificate_chain").(string),
55+
Preprocess: &preprocess,
56+
PrivateKey: d.Get("private_key").(string)}
57+
58+
log.Printf("[DEBUG] SSL Certificate create configuration: %#v, %#v", app, opts)
59+
a, err := client.SSLEndpointCreate(app, opts)
60+
if err != nil {
61+
panic(err)
62+
}
63+
64+
d.SetId(a.ID)
65+
log.Printf("[INFO] SSL Certificate ID: %s", d.Id())
66+
67+
return resourceHerokuCertRead(d, meta)
68+
}
69+
70+
func resourceHerokuCertRead(d *schema.ResourceData, meta interface{}) error {
71+
client := meta.(*heroku.Service)
72+
73+
cert, err := resourceHerokuSSLCertRetrieve(
74+
d.Get("app").(string), d.Id(), client)
75+
if err != nil {
76+
return err
77+
}
78+
79+
d.Set("certificate_chain", cert.CertificateChain)
80+
d.Set("name", cert.Name)
81+
d.Set("cname", cert.CName)
82+
83+
return nil
84+
}
85+
86+
func resourceHerokuCertUpdate(d *schema.ResourceData, meta interface{}) error {
87+
client := meta.(*heroku.Service)
88+
89+
app := d.Get("app").(string)
90+
91+
if d.HasChange("certificate_chain") {
92+
preprocess := true
93+
rollback := false
94+
ad, err := client.SSLEndpointUpdate(
95+
app, d.Id(), heroku.SSLEndpointUpdateOpts{
96+
CertificateChain: d.Get("certificate_chain").(*string),
97+
Preprocess: &preprocess,
98+
PrivateKey: d.Get("private_key").(*string),
99+
Rollback: &rollback})
100+
if err != nil {
101+
return err
102+
}
103+
104+
// Store the new ID
105+
d.SetId(ad.ID)
106+
}
107+
108+
return resourceHerokuCertRead(d, meta)
109+
}
110+
111+
func resourceHerokuCertDelete(d *schema.ResourceData, meta interface{}) error {
112+
client := meta.(*heroku.Service)
113+
114+
log.Printf("[INFO] Deleting SSL Cert: %s", d.Id())
115+
116+
// Destroy the app
117+
err := client.SSLEndpointDelete(d.Get("app").(string), d.Id())
118+
if err != nil {
119+
return fmt.Errorf("Error deleting SSL Cert: %s", err)
120+
}
121+
122+
d.SetId("")
123+
return nil
124+
}
125+
126+
func resourceHerokuSSLCertRetrieve(app string, id string, client *heroku.Service) (*heroku.SSLEndpoint, error) {
127+
addon, err := client.SSLEndpointInfo(app, id)
128+
129+
if err != nil {
130+
return nil, fmt.Errorf("Error retrieving SSL Cert: %s", err)
131+
}
132+
133+
return addon, nil
134+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package heroku
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"os"
7+
"io/ioutil"
8+
9+
"github.com/cyberdelia/heroku-go/v3"
10+
"github.com/hashicorp/terraform/helper/resource"
11+
"github.com/hashicorp/terraform/terraform"
12+
)
13+
14+
func TestAccHerokuCert_Basic(t *testing.T) {
15+
var endpoint heroku.SSLEndpoint
16+
wd, _ := os.Getwd()
17+
certificateChainFile := wd + "/test-fixtures/terraform.cert"
18+
certificateChainBytes, _ := ioutil.ReadFile(certificateChainFile)
19+
certificateChain := string(certificateChainBytes)
20+
testAccCheckHerokuCertConfig_basic := `
21+
resource "heroku_app" "foobar" {
22+
name = "terraform-test-cert-app"
23+
region = "eu"
24+
}
25+
26+
resource "heroku_addon" "ssl" {
27+
app = "${heroku_app.foobar.name}"
28+
plan = "ssl:endpoint"
29+
}
30+
31+
resource "heroku_cert" "ssl_certificate" {
32+
app = "${heroku_app.foobar.name}"
33+
depends_on = "heroku_addon.ssl"
34+
certificate_chain="${file("` + certificateChainFile + `")}"
35+
private_key="${file("` + wd + `/test-fixtures/terraform.key")}"
36+
}
37+
`
38+
39+
resource.Test(t, resource.TestCase{
40+
PreCheck: func() { testAccPreCheck(t) },
41+
Providers: testAccProviders,
42+
CheckDestroy: testAccCheckHerokuCertDestroy,
43+
Steps: []resource.TestStep{
44+
resource.TestStep{
45+
Config: testAccCheckHerokuCertConfig_basic,
46+
Check: resource.ComposeTestCheckFunc(
47+
testAccCheckHerokuCertExists("heroku_cert.ssl_certificate", &endpoint),
48+
testAccCheckHerokuCertificateChain(&endpoint, certificateChain),
49+
resource.TestCheckResourceAttr(
50+
"heroku_cert.ssl_certificate", "cname", "terraform-test-cert-app.herokuapp.com"),
51+
),
52+
},
53+
},
54+
})
55+
}
56+
57+
func testAccCheckHerokuCertDestroy(s *terraform.State) error {
58+
client := testAccProvider.Meta().(*heroku.Service)
59+
60+
for _, rs := range s.RootModule().Resources {
61+
if rs.Type != "heroku_cert" {
62+
continue
63+
}
64+
65+
_, err := client.SSLEndpointInfo(rs.Primary.Attributes["app"], rs.Primary.ID)
66+
67+
if err == nil {
68+
return fmt.Errorf("Cerfificate still exists")
69+
}
70+
}
71+
72+
return nil
73+
}
74+
75+
func testAccCheckHerokuCertificateChain(endpoint *heroku.SSLEndpoint, chain string) resource.TestCheckFunc {
76+
return func(s *terraform.State) error {
77+
78+
if endpoint.CertificateChain != chain {
79+
return fmt.Errorf("Bad certificate chain: %s", endpoint.CertificateChain)
80+
}
81+
82+
return nil
83+
}
84+
}
85+
86+
func testAccCheckHerokuCertExists(n string, endpoint *heroku.SSLEndpoint) resource.TestCheckFunc {
87+
return func(s *terraform.State) error {
88+
rs, ok := s.RootModule().Resources[n]
89+
90+
if !ok {
91+
return fmt.Errorf("Not found: %s", n)
92+
}
93+
94+
if rs.Primary.ID == "" {
95+
return fmt.Errorf("No SSL endpoint ID is set")
96+
}
97+
98+
client := testAccProvider.Meta().(*heroku.Service)
99+
100+
foundEndpoint, err := client.SSLEndpointInfo(rs.Primary.Attributes["app"], rs.Primary.ID)
101+
102+
if err != nil {
103+
return err
104+
}
105+
106+
if foundEndpoint.ID != rs.Primary.ID {
107+
return fmt.Errorf("SSL endpoint not found")
108+
}
109+
110+
*endpoint = *foundEndpoint
111+
112+
return nil
113+
}
114+
}
115+
116+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDCzCCAfOgAwIBAgIJAIUuu5XX/tCRMA0GCSqGSIb3DQEBBQUAMBwxGjAYBgNV
3+
BAMMEXd3dy50ZXJyYWZvcm0ub3JnMB4XDTE0MTIxNDIwMzA0MFoXDTI0MTIxMTIw
4+
MzA0MFowHDEaMBgGA1UEAwwRd3d3LnRlcnJhZm9ybS5vcmcwggEiMA0GCSqGSIb3
5+
DQEBAQUAA4IBDwAwggEKAoIBAQC7sp6oJ6czdRpl5azB7jaLCwQ38eqV2TRFPVVj
6+
PD7cWyhV2REFtqd7vEF/AUrp3+ACvc6mLdTjDuaGVVga4oA42Qgqz5Wzkl3tnBSB
7+
DlxFXXg+p4UjJWZPLUiOMbvHWNGthO9G1dp5h9rhqV7wJhyAlTqlnV7aaeSWcJgY
8+
fh7xMe50BlAmh6ywpnnlZzsy4eJiJwgbglG8OU0JK+1OxdOUDe/1eUOhFPPx1U/p
9+
25t8Z6qaI8FDLPwTVZzrvOZ0vTQSKyeA0ZhBTH1GhUqroogDlPETgkne6YqvZoxl
10+
o8+9Wdjln2bjYe/nRWYKR5BxC46PnNJMPPJFI/VNLPYanAu5AgMBAAGjUDBOMB0G
11+
A1UdDgQWBBQaJeROghiSQGVn30ARllECycYsczAfBgNVHSMEGDAWgBQaJeROghiS
12+
QGVn30ARllECycYsczAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQCa
13+
iRpZ0b4KVqDT3bqc9UZV491UdBVF9BN0CV4BLvg9KPyRcftujZu0lKFu+wGlAlYr
14+
bV6DjqHgFXltBzIFM/y790EivkgePcFv+0HKy1O8ELLduQcigYT5AC7h34xxWBy7
15+
96VW6qD7/OOjvexVdKmTfXO/njdmot38/uO9TdfJPQzCHrgpzjBCcI+eBFnvQwzb
16+
gOpMlh04U4nDeITOTbraLur1zWQjzSA9DjaGGA+IQ556MUPAS85YmJ4Jf+f8UW3o
17+
sZmzFojLFd8EhVRZDE4tZzqo/vN0plGv/Kh/oob5Cnp6EJ3BbG6y9tzATMWz/hRo
18+
oeTtQe3gt6gPKBS+UeBf
19+
-----END CERTIFICATE-----
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpAIBAAKCAQEAu7KeqCenM3UaZeWswe42iwsEN/Hqldk0RT1VYzw+3FsoVdkR
3+
Bbane7xBfwFK6d/gAr3Opi3U4w7mhlVYGuKAONkIKs+Vs5Jd7ZwUgQ5cRV14PqeF
4+
IyVmTy1IjjG7x1jRrYTvRtXaeYfa4ale8CYcgJU6pZ1e2mnklnCYGH4e8THudAZQ
5+
JoessKZ55Wc7MuHiYicIG4JRvDlNCSvtTsXTlA3v9XlDoRTz8dVP6dubfGeqmiPB
6+
Qyz8E1Wc67zmdL00EisngNGYQUx9RoVKq6KIA5TxE4JJ3umKr2aMZaPPvVnY5Z9m
7+
42Hv50VmCkeQcQuOj5zSTDzyRSP1TSz2GpwLuQIDAQABAoIBAQCethLiLWV8ZXDE
8+
6MiD02HbgJ04kR7DRr6kLZCeMLsWqR4aOUnjgudsAWuAcR9fUyagKs8qRWbV+CuF
9+
O3UchpnVd+8oBA+ZoBI8cNYFqpbrMHYUxKIXbfBs0uWfFv6pOblS+C07wGjUisPS
10+
PN1CQ3emYokMsV0bYp8fdmWlkD+pwhHH3vsPm4sYwbabaURRxZYLNyYd/4Czafqz
11+
UBWbAJ+xap6t/WLJCR7goHCVX0DNtpNfzoYK5/rKpQzw0H+L1pB5yghx5GbslthB
12+
xtTb1LjMMl4AUuU5Bv1XLzDZlS/HUYQyefhljlqJPC678KNlegRLjn2YZeZF07X1
13+
b/KSKDrhAoGBAPVST4JYGS9OoW4Fvu4SV7AJGW1ewHhAmzO1XFrYD8DvSjf3Cinn
14+
ylTYwkQK9ayLLM405Sof2J55NbEakDs5sahN93mqGrk9bWPZrFHgDf0NiMoyI/0N
15+
/ZBXhkBeg9LitBvmEWiBlGK55At0zePWVDcUtXg+d0tSJC4o0y1DoGo7AoGBAMPe
16+
ML95QKabWsCRpGKVwhOFrEp68rZlugwJEjzubC8EXHX8dNy3IURl1j8tSM0kDGay
17+
CDMpxOjqzVyLmLqfIghiG1nkQU7EnJdx86k3AaesHoJff3Ywi+9DwC5r/T3zg20U
18+
Qkr1c4Yxv0Lk3IluHwjPaT/uMd4utlPB6EpAvY6bAoGAdcNBb6yiylbQn2Qat2YO
19+
ue5kSmBFvHQnDLdu0h0N0uwLkLoCIwOl2P0EpG0uadmVdJdnusT203wUDiRWQFf9
20+
tHFY7wp9MZcPP/NqCROpI2Sv2YAgToW8xuF9DMFSPpWdKBdVG/m4JXxewDEd9NUa
21+
MCa8xjAWTA3uWEo4tW3VP6kCgYAKAYvT/EnFOSKFu+r97lCf1rBajbVghAnhG4WG
22+
/1cff8WJcYA21lQovlsXlySk9jZ7+JRaqMOacoRTOf5vajm+2+Qxz2tWrsyhH/0m
23+
o9y9yBk259IHI6vCaV+j/3hMdeg85lAMrEVekaQHstFhY/LJ7G6gCXcatqAx3zIS
24+
uQP2CQKBgQDGx31nzcrYcJz0EtZPJ5n4JMKrx9S4ZQ2+C7p6oDYXJl2Woh0f+c3K
25+
X1oW1MLFChq+5Or9IVh11cePtV3a6X10Q9xSLQnXdPC9X4QsZoWOUBh5fzw301g1
26+
WB9ncXqqew3EPHm1G5j4hZ+Gjz/P53TLRYTEYW8fm1Rv9ga5wiJpiw==
27+
-----END RSA PRIVATE KEY-----
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
layout: "heroku"
3+
page_title: "Heroku: heroku_cert"
4+
sidebar_current: "docs-heroku-resource-cert"
5+
description: |-
6+
Provides a Heroku SSL certificate resource. It allows to set a given certificate for a Heroku app.
7+
---
8+
9+
# heroku\_cert
10+
11+
Provides a Heroku SSL certificate resource. It allows to set a given certificate for a Heroku app.
12+
13+
## Example Usage
14+
15+
```
16+
# Create a new heroku app
17+
resource "heroku_app" "default" {
18+
name = "test-app"
19+
}
20+
21+
# Add-on SSL to application
22+
resource "heroku_addon" "ssl" {
23+
app = "${heroku_app.service.name}"
24+
plan = "ssl"
25+
}
26+
27+
# Establish certificate for a given application
28+
resource "heroku_cert" "ssl_certificate" {
29+
app = "${heroku_app.service.name}"
30+
certificate_chain = "${file("server.crt")}"
31+
private_key = "${file("server.key")}"
32+
depends_on = "heroku_addon.ssl"
33+
}
34+
```
35+
36+
## Argument Reference
37+
38+
The following arguments are supported:
39+
40+
* `app` - (Required) The Heroku app to add to.
41+
* `certificate_chain` - (Required) The certificate chain to add
42+
* `private_key` - (Required) The private key for a given certificate chain
43+
44+
## Attributes Reference
45+
46+
The following attributes are exported:
47+
48+
* `id` - The ID of the add-on
49+
* `cname` - The CNAME of ssl endpoint
50+
* `name` - The name of the SSL certificate
51+

0 commit comments

Comments
 (0)