From 3c0e9970e582d0977723bd980a353ca2549cd689 Mon Sep 17 00:00:00 2001 From: Jakub Janczak Date: Thu, 27 Nov 2014 12:12:34 +0100 Subject: [PATCH] Heroku SSL certificate support added --- builtin/providers/heroku/provider.go | 1 + .../providers/heroku/resource_heroku_cert.go | 136 ++++++++++++++++++ .../heroku/resource_heroku_cert_test.go | 121 ++++++++++++++++ .../heroku/test-fixtures/terraform.cert | 19 +++ .../heroku/test-fixtures/terraform.key | 27 ++++ .../providers/heroku/r/cert.html.markdown | 51 +++++++ 6 files changed, 355 insertions(+) create mode 100644 builtin/providers/heroku/resource_heroku_cert.go create mode 100644 builtin/providers/heroku/resource_heroku_cert_test.go create mode 100644 builtin/providers/heroku/test-fixtures/terraform.cert create mode 100644 builtin/providers/heroku/test-fixtures/terraform.key create mode 100644 website/source/docs/providers/heroku/r/cert.html.markdown diff --git a/builtin/providers/heroku/provider.go b/builtin/providers/heroku/provider.go index d8c6ced23..484c6c4e2 100644 --- a/builtin/providers/heroku/provider.go +++ b/builtin/providers/heroku/provider.go @@ -30,6 +30,7 @@ func Provider() terraform.ResourceProvider { "heroku_addon": resourceHerokuAddon(), "heroku_domain": resourceHerokuDomain(), "heroku_drain": resourceHerokuDrain(), + "heroku_cert": resourceHerokuCert(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/heroku/resource_heroku_cert.go b/builtin/providers/heroku/resource_heroku_cert.go new file mode 100644 index 000000000..9032df492 --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_cert.go @@ -0,0 +1,136 @@ +package heroku + +import ( + "fmt" + "log" + + "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceHerokuCert() *schema.Resource { + return &schema.Resource{ + Create: resourceHerokuCertCreate, + Read: resourceHerokuCertRead, + Update: resourceHerokuCertUpdate, + Delete: resourceHerokuCertDelete, + + Schema: map[string]*schema.Schema{ + "app": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "certificate_chain": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "private_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "cname": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceHerokuCertCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + app := d.Get("app").(string) + preprocess := true + opts := heroku.SSLEndpointCreateOpts{ + CertificateChain: d.Get("certificate_chain").(string), + Preprocess: &preprocess, + PrivateKey: d.Get("private_key").(string)} + + log.Printf("[DEBUG] SSL Certificate create configuration: %#v, %#v", app, opts) + a, err := client.SSLEndpointCreate(app, opts) + if err != nil { + panic(err) + } + + d.SetId(a.ID) + log.Printf("[INFO] SSL Certificate ID: %s", d.Id()) + + return resourceHerokuCertRead(d, meta) +} + +func resourceHerokuCertRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + cert, err := resource_heroku_ssl_cert_retrieve( + d.Get("app").(string), d.Id(), client) + if err != nil { + return err + } + + d.Set("certificate_chain", cert.CertificateChain) + d.Set("name", cert.Name) + d.Set("cname", cert.CName) + d.Set("created_at", cert.CreatedAt) + d.Set("updated_at", cert.UpdatedAt) + + return nil +} + +func resourceHerokuCertUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + app := d.Get("app").(string) + + if d.HasChange("certificate_chain") { + preprocess := true + rollback := false + ad, err := client.SSLEndpointUpdate( + app, d.Id(), heroku.SSLEndpointUpdateOpts{ + CertificateChain: d.Get("certificate_chain").(*string), + Preprocess: &preprocess, + PrivateKey: d.Get("private_key").(*string), + Rollback: &rollback}) + if err != nil { + return err + } + + // Store the new ID + d.SetId(ad.ID) + } + + return resourceHerokuCertRead(d, meta) +} + +func resourceHerokuCertDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*heroku.Service) + + log.Printf("[INFO] Deleting SSL Cert: %s", d.Id()) + + // Destroy the app + err := client.SSLEndpointDelete(d.Get("app").(string), d.Id()) + if err != nil { + return fmt.Errorf("Error deleting SSL Cert: %s", err) + } + + d.SetId("") + return nil +} + +func resource_heroku_ssl_cert_retrieve(app string, id string, client *heroku.Service) (*heroku.SSLEndpoint, error) { + addon, err := client.SSLEndpointInfo(app, id) + + if err != nil { + return nil, fmt.Errorf("Error retrieving SSL Cert: %s", err) + } + + return addon, nil +} diff --git a/builtin/providers/heroku/resource_heroku_cert_test.go b/builtin/providers/heroku/resource_heroku_cert_test.go new file mode 100644 index 000000000..0422e0e7d --- /dev/null +++ b/builtin/providers/heroku/resource_heroku_cert_test.go @@ -0,0 +1,121 @@ +package heroku + +import ( + "fmt" + "testing" + "os" + "io/ioutil" + + "github.com/cyberdelia/heroku-go/v3" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccHerokuCert_Basic(t *testing.T) { + var endpoint heroku.SSLEndpoint + wd, _ := os.Getwd() + certificateChainFile := wd + "/test-fixtures/terraform.cert" + certificateChainBytes, _ := ioutil.ReadFile(certificateChainFile) + certificateChain := string(certificateChainBytes) + testAccCheckHerokuCertConfig_basic := ` + resource "heroku_app" "foobar" { + name = "terraform-test-cert-app" + region = "eu" + organization = { + name = "plan3-labs" + locked = false + private = false + } + } + + resource "heroku_addon" "ssl" { + app = "${heroku_app.foobar.name}" + plan = "ssl:endpoint" + } + + resource "heroku_cert" "ssl_certificate" { + app = "${heroku_app.foobar.name}" + depends_on = "heroku_addon.ssl" + certificate_chain="${file("` + certificateChainFile + `")}" + private_key="${file("` + wd + `/test-fixtures/terraform.key")}" + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckHerokuCertDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckHerokuCertConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuCertExists("heroku_cert.ssl_certificate", &endpoint), + testAccCheckHerokuCertificateChain(&endpoint, certificateChain), + resource.TestCheckResourceAttr( + "heroku_cert.ssl_certificate", "cname", "terraform-test-cert-app.herokuapp.com"), + ), + }, + }, + }) +} + +func testAccCheckHerokuCertDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Service) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "heroku_cert" { + continue + } + + _, err := client.SSLEndpointInfo(rs.Primary.Attributes["app"], rs.Primary.ID) + + if err == nil { + return fmt.Errorf("Cerfificate still exists") + } + } + + return nil +} + +func testAccCheckHerokuCertificateChain(endpoint *heroku.SSLEndpoint, chain string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if endpoint.CertificateChain != chain { + return fmt.Errorf("Bad certificate chain: %s", endpoint.CertificateChain) + } + + return nil + } +} + +func testAccCheckHerokuCertExists(n string, endpoint *heroku.SSLEndpoint) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No SSL endpoint ID is set") + } + + client := testAccProvider.Meta().(*heroku.Service) + + foundEndpoint, err := client.SSLEndpointInfo(rs.Primary.Attributes["app"], rs.Primary.ID) + + if err != nil { + return err + } + + if foundEndpoint.ID != rs.Primary.ID { + return fmt.Errorf("SSL endpoint not found") + } + + *endpoint = *foundEndpoint + + return nil + } +} + + diff --git a/builtin/providers/heroku/test-fixtures/terraform.cert b/builtin/providers/heroku/test-fixtures/terraform.cert new file mode 100644 index 000000000..8f8b982f6 --- /dev/null +++ b/builtin/providers/heroku/test-fixtures/terraform.cert @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCzCCAfOgAwIBAgIJAIUuu5XX/tCRMA0GCSqGSIb3DQEBBQUAMBwxGjAYBgNV +BAMMEXd3dy50ZXJyYWZvcm0ub3JnMB4XDTE0MTIxNDIwMzA0MFoXDTI0MTIxMTIw +MzA0MFowHDEaMBgGA1UEAwwRd3d3LnRlcnJhZm9ybS5vcmcwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC7sp6oJ6czdRpl5azB7jaLCwQ38eqV2TRFPVVj +PD7cWyhV2REFtqd7vEF/AUrp3+ACvc6mLdTjDuaGVVga4oA42Qgqz5Wzkl3tnBSB +DlxFXXg+p4UjJWZPLUiOMbvHWNGthO9G1dp5h9rhqV7wJhyAlTqlnV7aaeSWcJgY +fh7xMe50BlAmh6ywpnnlZzsy4eJiJwgbglG8OU0JK+1OxdOUDe/1eUOhFPPx1U/p +25t8Z6qaI8FDLPwTVZzrvOZ0vTQSKyeA0ZhBTH1GhUqroogDlPETgkne6YqvZoxl +o8+9Wdjln2bjYe/nRWYKR5BxC46PnNJMPPJFI/VNLPYanAu5AgMBAAGjUDBOMB0G +A1UdDgQWBBQaJeROghiSQGVn30ARllECycYsczAfBgNVHSMEGDAWgBQaJeROghiS +QGVn30ARllECycYsczAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQCa +iRpZ0b4KVqDT3bqc9UZV491UdBVF9BN0CV4BLvg9KPyRcftujZu0lKFu+wGlAlYr +bV6DjqHgFXltBzIFM/y790EivkgePcFv+0HKy1O8ELLduQcigYT5AC7h34xxWBy7 +96VW6qD7/OOjvexVdKmTfXO/njdmot38/uO9TdfJPQzCHrgpzjBCcI+eBFnvQwzb +gOpMlh04U4nDeITOTbraLur1zWQjzSA9DjaGGA+IQ556MUPAS85YmJ4Jf+f8UW3o +sZmzFojLFd8EhVRZDE4tZzqo/vN0plGv/Kh/oob5Cnp6EJ3BbG6y9tzATMWz/hRo +oeTtQe3gt6gPKBS+UeBf +-----END CERTIFICATE----- diff --git a/builtin/providers/heroku/test-fixtures/terraform.key b/builtin/providers/heroku/test-fixtures/terraform.key new file mode 100644 index 000000000..590c2068d --- /dev/null +++ b/builtin/providers/heroku/test-fixtures/terraform.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAu7KeqCenM3UaZeWswe42iwsEN/Hqldk0RT1VYzw+3FsoVdkR +Bbane7xBfwFK6d/gAr3Opi3U4w7mhlVYGuKAONkIKs+Vs5Jd7ZwUgQ5cRV14PqeF +IyVmTy1IjjG7x1jRrYTvRtXaeYfa4ale8CYcgJU6pZ1e2mnklnCYGH4e8THudAZQ +JoessKZ55Wc7MuHiYicIG4JRvDlNCSvtTsXTlA3v9XlDoRTz8dVP6dubfGeqmiPB +Qyz8E1Wc67zmdL00EisngNGYQUx9RoVKq6KIA5TxE4JJ3umKr2aMZaPPvVnY5Z9m +42Hv50VmCkeQcQuOj5zSTDzyRSP1TSz2GpwLuQIDAQABAoIBAQCethLiLWV8ZXDE +6MiD02HbgJ04kR7DRr6kLZCeMLsWqR4aOUnjgudsAWuAcR9fUyagKs8qRWbV+CuF +O3UchpnVd+8oBA+ZoBI8cNYFqpbrMHYUxKIXbfBs0uWfFv6pOblS+C07wGjUisPS +PN1CQ3emYokMsV0bYp8fdmWlkD+pwhHH3vsPm4sYwbabaURRxZYLNyYd/4Czafqz +UBWbAJ+xap6t/WLJCR7goHCVX0DNtpNfzoYK5/rKpQzw0H+L1pB5yghx5GbslthB +xtTb1LjMMl4AUuU5Bv1XLzDZlS/HUYQyefhljlqJPC678KNlegRLjn2YZeZF07X1 +b/KSKDrhAoGBAPVST4JYGS9OoW4Fvu4SV7AJGW1ewHhAmzO1XFrYD8DvSjf3Cinn +ylTYwkQK9ayLLM405Sof2J55NbEakDs5sahN93mqGrk9bWPZrFHgDf0NiMoyI/0N +/ZBXhkBeg9LitBvmEWiBlGK55At0zePWVDcUtXg+d0tSJC4o0y1DoGo7AoGBAMPe +ML95QKabWsCRpGKVwhOFrEp68rZlugwJEjzubC8EXHX8dNy3IURl1j8tSM0kDGay +CDMpxOjqzVyLmLqfIghiG1nkQU7EnJdx86k3AaesHoJff3Ywi+9DwC5r/T3zg20U +Qkr1c4Yxv0Lk3IluHwjPaT/uMd4utlPB6EpAvY6bAoGAdcNBb6yiylbQn2Qat2YO +ue5kSmBFvHQnDLdu0h0N0uwLkLoCIwOl2P0EpG0uadmVdJdnusT203wUDiRWQFf9 +tHFY7wp9MZcPP/NqCROpI2Sv2YAgToW8xuF9DMFSPpWdKBdVG/m4JXxewDEd9NUa +MCa8xjAWTA3uWEo4tW3VP6kCgYAKAYvT/EnFOSKFu+r97lCf1rBajbVghAnhG4WG +/1cff8WJcYA21lQovlsXlySk9jZ7+JRaqMOacoRTOf5vajm+2+Qxz2tWrsyhH/0m +o9y9yBk259IHI6vCaV+j/3hMdeg85lAMrEVekaQHstFhY/LJ7G6gCXcatqAx3zIS +uQP2CQKBgQDGx31nzcrYcJz0EtZPJ5n4JMKrx9S4ZQ2+C7p6oDYXJl2Woh0f+c3K +X1oW1MLFChq+5Or9IVh11cePtV3a6X10Q9xSLQnXdPC9X4QsZoWOUBh5fzw301g1 +WB9ncXqqew3EPHm1G5j4hZ+Gjz/P53TLRYTEYW8fm1Rv9ga5wiJpiw== +-----END RSA PRIVATE KEY----- diff --git a/website/source/docs/providers/heroku/r/cert.html.markdown b/website/source/docs/providers/heroku/r/cert.html.markdown new file mode 100644 index 000000000..972a78ffb --- /dev/null +++ b/website/source/docs/providers/heroku/r/cert.html.markdown @@ -0,0 +1,51 @@ +--- +layout: "heroku" +page_title: "Heroku: heroku_cert" +sidebar_current: "docs-heroku-resource-cert" +description: |- + Provides a Heroku SSL certificate resource. It allows to set a given certificate for a Heroku app. +--- + +# heroku\_cert + +Provides a Heroku SSL certificate resource. It allows to set a given certificate for a Heroku app. + +## Example Usage + +``` +# Create a new heroku app +resource "heroku_app" "default" { + name = "test-app" +} + +# Add-on SSL to application +resource "heroku_addon" "ssl" { + app = "${heroku_app.service.name}" + plan = "ssl" +} + +# Establish certificate for a given application +resource "heroku_cert" "ssl_certificate" { + app = "${heroku_app.service.name}" + certificate_chain = "${file("server.crt")}" + private_key = "${file("server.key")}" + depends_on = "heroku_addon.ssl" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `app` - (Required) The Heroku app to add to. +* `certificate_chain` - (Required) The certificate chain to add +* `private_key` - (Required) The private key for a given certificate chain + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the add-on +* `cname` - The CNAME of ssl endpoint +* `name` - The name of the SSL certificate +