From 5b311ef8a3255d8b56b192023f243cdac94dea6e Mon Sep 17 00:00:00 2001 From: Alexander Chernov Date: Wed, 4 Mar 2020 10:52:12 +0000 Subject: [PATCH] feat: added certificate to load-balancer (#396) --- scaleway/helpers_lb.go | 29 +++ scaleway/provider.go | 1 + scaleway/resource_lb_certificate_beta.go | 201 ++++++++++++++++++ scaleway/resource_lb_certificate_beta_test.go | 141 ++++++++++++ .../docs/r/lb_certificate_beta.html.markdown | 86 ++++++++ website/scaleway.erb | 3 + 6 files changed, 461 insertions(+) create mode 100644 scaleway/resource_lb_certificate_beta.go create mode 100644 scaleway/resource_lb_certificate_beta_test.go create mode 100644 website/docs/r/lb_certificate_beta.html.markdown diff --git a/scaleway/helpers_lb.go b/scaleway/helpers_lb.go index b59d2d090..67d755994 100644 --- a/scaleway/helpers_lb.go +++ b/scaleway/helpers_lb.go @@ -200,6 +200,7 @@ func expandLbHCHTTPS(raw interface{}) *lb.HealthCheckHTTPSConfig { if raw == nil || len(raw.([]interface{})) != 1 { return nil } + rawMap := raw.([]interface{})[0].(map[string]interface{}) return &lb.HealthCheckHTTPSConfig{ URI: rawMap["uri"].(string), @@ -207,3 +208,31 @@ func expandLbHCHTTPS(raw interface{}) *lb.HealthCheckHTTPSConfig { Code: expandInt32Ptr(rawMap["code"]), } } + +func expandLbLetsEncrypt(raw interface{}) *lb.CreateCertificateRequestLetsencryptConfig { + if raw == nil || len(raw.([]interface{})) != 1 { + return nil + } + + rawMap := raw.([]interface{})[0].(map[string]interface{}) + alternativeNames := rawMap["subject_alternative_name"].([]interface{}) + config := &lb.CreateCertificateRequestLetsencryptConfig{ + CommonName: rawMap["common_name"].(string), + } + for _, alternativeName := range alternativeNames { + config.SubjectAlternativeName = append(config.SubjectAlternativeName, alternativeName.(string)) + } + return config +} + +func expandLbCustomCertificate(raw interface{}) *lb.CreateCertificateRequestCustomCertificate { + if raw == nil || len(raw.([]interface{})) != 1 { + return nil + } + + rawMap := raw.([]interface{})[0].(map[string]interface{}) + config := &lb.CreateCertificateRequestCustomCertificate{ + CertificateChain: rawMap["certificate_chain"].(string), + } + return config +} diff --git a/scaleway/provider.go b/scaleway/provider.go index bf669b49b..e43259e33 100644 --- a/scaleway/provider.go +++ b/scaleway/provider.go @@ -201,6 +201,7 @@ func Provider() terraform.ResourceProvider { "scaleway_k8s_pool_beta": resourceScalewayK8SPoolBeta(), "scaleway_lb_beta": resourceScalewayLbBeta(), "scaleway_lb_backend_beta": resourceScalewayLbBackendBeta(), + "scaleway_lb_certificate_beta": resourceScalewayLbCertificateBeta(), "scaleway_lb_frontend_beta": resourceScalewayLbFrontendBeta(), "scaleway_registry_namespace_beta": resourceScalewayRegistryNamespaceBeta(), "scaleway_rdb_instance_beta": resourceScalewayRdbInstanceBeta(), diff --git a/scaleway/resource_lb_certificate_beta.go b/scaleway/resource_lb_certificate_beta.go new file mode 100644 index 000000000..2ae15eb13 --- /dev/null +++ b/scaleway/resource_lb_certificate_beta.go @@ -0,0 +1,201 @@ +package scaleway + +import ( + "errors" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/scaleway/scaleway-sdk-go/api/lb/v1" +) + +func resourceScalewayLbCertificateBeta() *schema.Resource { + return &schema.Resource{ + Create: resourceScalewayLbCertificateBetaCreate, + Read: resourceScalewayLbCertificateBetaRead, + Update: resourceScalewayLbCertificateBetaUpdate, + Delete: resourceScalewayLbCertificateBetaDelete, + SchemaVersion: 0, + Schema: map[string]*schema.Schema{ + "lb_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The load-balancer ID", + }, + "name": { + Type: schema.TypeString, + Description: "The name of the load-balancer certificate", + Optional: true, + Computed: true, + }, + "letsencrypt": { + ConflictsWith: []string{"custom_certificate"}, + MaxItems: 1, + Description: "The Let's Encrypt type certificate configuration", + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "common_name": { + Type: schema.TypeString, + Required: true, + Description: "The main domain name of the certificate", + }, + "subject_alternative_name": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "The alternative domain names of the certificate", + }, + }, + }, + }, + "custom_certificate": { + ConflictsWith: []string{"letsencrypt"}, + MaxItems: 1, + Type: schema.TypeList, + Description: "The custom type certificate type configuration", + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "certificate_chain": { + Type: schema.TypeString, + Required: true, + Description: "The full PEM-formatted certificate chain", + }, + }, + }, + }, + + // Readonly attributes + "common_name": { + Type: schema.TypeString, + Computed: true, + Description: "The main domain name of the certificate", + }, + "subject_alternative_name": { + Type: schema.TypeString, + Computed: true, + Description: "The alternative domain names of the certificate", + }, + "fingerprint": { + Type: schema.TypeString, + Computed: true, + Description: "The identifier (SHA-1) of the certificate", + }, + "not_valid_before": { + Type: schema.TypeString, + Computed: true, + Description: "The not valid before validity bound timestamp", + }, + "not_valid_after": { + Type: schema.TypeString, + Computed: true, + Description: "The not valid after validity bound timestamp", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The status of certificate", + }, + }, + } +} + +func resourceScalewayLbCertificateBetaCreate(d *schema.ResourceData, m interface{}) error { + region, lbID, err := parseRegionalID(d.Get("lb_id").(string)) + if err != nil { + return err + } + + createReq := &lb.CreateCertificateRequest{ + Region: region, + LbID: lbID, + Name: expandOrGenerateString(d.Get("name"), "lb-cert"), + Letsencrypt: expandLbLetsEncrypt(d.Get("letsencrypt")), + CustomCertificate: expandLbCustomCertificate(d.Get("custom_certificate")), + } + if createReq.Letsencrypt == nil && createReq.CustomCertificate == nil { + return errors.New("you need to define either letsencrypt or custom_certificate configuration") + } + + lbAPI := lbAPI(m) + res, err := lbAPI.CreateCertificate(createReq) + if err != nil { + return err + } + + d.SetId(newRegionalId(region, res.ID)) + + return resourceScalewayLbCertificateBetaRead(d, m) +} + +func resourceScalewayLbCertificateBetaRead(d *schema.ResourceData, m interface{}) error { + lbAPI, region, ID, err := lbAPIWithRegionAndID(m, d.Id()) + if err != nil { + return err + } + + res, err := lbAPI.GetCertificate(&lb.GetCertificateRequest{ + CertificateID: ID, + Region: region, + }) + + if err != nil { + if is404Error(err) { + d.SetId("") + return nil + } + return err + } + + _ = d.Set("name", res.Name) + _ = d.Set("common_name", res.CommonName) + _ = d.Set("subject_alternative_name", res.SubjectAlternativeName) + _ = d.Set("fingerprint", res.Fingerprint) + _ = d.Set("not_valid_before", flattenTime(&res.NotValidBefore)) + _ = d.Set("not_valid_after", flattenTime(&res.NotValidAfter)) + _ = d.Set("status", res.Status) + return nil +} + +func resourceScalewayLbCertificateBetaUpdate(d *schema.ResourceData, m interface{}) error { + lbAPI, region, ID, err := lbAPIWithRegionAndID(m, d.Id()) + if err != nil { + return err + } + + req := &lb.UpdateCertificateRequest{ + CertificateID: ID, + Region: region, + Name: d.Get("name").(string), + } + + _, err = lbAPI.UpdateCertificate(req) + if err != nil { + return err + } + + return resourceScalewayLbCertificateBetaRead(d, m) +} + +func resourceScalewayLbCertificateBetaDelete(d *schema.ResourceData, m interface{}) error { + lbAPI, region, ID, err := lbAPIWithRegionAndID(m, d.Id()) + if err != nil { + return err + } + + err = lbAPI.DeleteCertificate(&lb.DeleteCertificateRequest{ + Region: region, + CertificateID: ID, + }) + + if err != nil && !is404Error(err) { + return err + } + + return nil +} diff --git a/scaleway/resource_lb_certificate_beta_test.go b/scaleway/resource_lb_certificate_beta_test.go new file mode 100644 index 000000000..b79d2d42d --- /dev/null +++ b/scaleway/resource_lb_certificate_beta_test.go @@ -0,0 +1,141 @@ +package scaleway + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccScalewayLbCertificateBeta(t *testing.T) { + /** + * Note regarding the usage of xip.io + * See the discussion on https://github.com/terraform-providers/terraform-provider-scaleway/pull/396 + * Long story short, scaleway API will not permit you to request a certificate in case common name is not pointed + * to the load balancer IP (which is unknown before creating it). In production, this can be overcome by introducing + * an additional step which creates a DNS record and depending on it, but for test purposes, xip.io is an ideal solution. + */ + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckScalewayLbBetaDestroy, + Steps: []resource.TestStep{ + { + Config: ` + resource scaleway_lb_beta lb01 { + name = "test-lb" + type = "lb-s" + } + resource scaleway_lb_certificate_beta cert01 { + lb_id = scaleway_lb_beta.lb01.id + name = "test-cert" + letsencrypt { + common_name = "${scaleway_lb_beta.lb01.ip_address}.xip.io" + } + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("scaleway_lb_certificate_beta.cert01", "name", "test-cert"), + resource.TestCheckResourceAttr("scaleway_lb_certificate_beta.cert01", "letsencrypt.#", "1"), + ), + }, + { + Config: ` + resource scaleway_lb_beta lb01 { + name = "test-lb" + type = "lb-s" + } + resource scaleway_lb_certificate_beta cert01 { + lb_id = scaleway_lb_beta.lb01.id + name = "test-cert-new" + letsencrypt { + common_name = "${scaleway_lb_beta.lb01.ip_address}.xip.io" + } + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("scaleway_lb_certificate_beta.cert01", "name", "test-cert-new"), + ), + }, + { + Config: ` + resource scaleway_lb_beta lb01 { + name = "test-lb" + type = "lb-s" + } + resource scaleway_lb_certificate_beta cert01 { + lb_id = scaleway_lb_beta.lb01.id + name = "test-cert" + letsencrypt { + common_name = "${scaleway_lb_beta.lb01.ip_address}.xip.io" + subject_alternative_name = [ + "sub1.${scaleway_lb_beta.lb01.ip_address}.xip.io", + "sub2.${scaleway_lb_beta.lb01.ip_address}.xip.io" + ] + } + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("scaleway_lb_certificate_beta.cert01", "name", "test-cert"), + resource.TestCheckResourceAttr("scaleway_lb_certificate_beta.cert01", "letsencrypt.#", "1"), + resource.TestCheckResourceAttr("scaleway_lb_certificate_beta.cert01", "letsencrypt.0.subject_alternative_name.#", "2"), + ), + }, + { + Config: ` + resource scaleway_lb_beta lb01 { + name = "test-lb" + type = "lb-s" + } + resource scaleway_lb_certificate_beta cert01 { + lb_id = scaleway_lb_beta.lb01.id + name = "test-custom-cert" + custom_certificate { + certificate_chain = < **Note:** This terraform resource is flagged beta and might include breaking change in future releases. + +Creates and manages Scaleway Load-Balancer Certificates. For more information, see [the documentation](https://developers.scaleway.com/en/products/lb/api). + +## Examples + +#### Let's Encrypt +```hcl +resource scaleway_lb_certificate_beta cert01 { + lb_id = scaleway_lb_beta.lb01.id + name = "cert1" + letsencrypt { + common_name = "example.org" + subject_alternative_name = [ + "sub1.example.com", + "sub2.example.com" + ] + } +} +``` + +#### Custom Certificate +```hcl +resource scaleway_lb_certificate_beta cert01 { + lb_id = scaleway_lb_beta.lb01.id + name = "custom-cert" + custom_certificate { + certificate_chain = < **Important:** Updates to `lb_id` will recreate the load-balancer certificate. + +- `name` - (Optional) The name of the certificate backend. + +- `letsencrypt` - (Optional) Configuration block for Let's Encrypt configuration. Only one of `letsencrypt` and `custom_certificate` should be specified. + + - `common_name` - (Required) Main domain of the certificate. + + - `subject_alternative_name` - (Optional) Array of alternative domain names. + +~> **Important:** Updates to `letsencrypt` will recreate the load-balancer certificate. + +- `custom_certificate` - (Optional) Configuration block for custom certificate chain. Only one of `letsencrypt` and `custom_certificate` should be specified. + + - `certificate_chain` - (Required) Full PEM-formatted certificate chain. + +~> **Important:** Updates to `custom_certificate` will recreate the load-balancer certificate. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +- `id` - The ID of the loadbalancer certificate. +- `common_name` - Main domain of the certificate +- `subject_alternative_name` - The alternative domain names of the certificate +- `fingerprint` - The identifier (SHA-1) of the certificate +- `not_valid_before` - The not valid before validity bound timestamp +- `not_valid_after` - The not valid after validity bound timestamp +- `status` - Certificate status + +## Additional notes + +* Ensure that all domain names used in configuration are pointing to the load balancer IP. You can achieve this by creating a DNS record through terraform pointing to `ip_adress` property of `lb_beta` entity +* In case there are any issues with the certificate, you will receive a `400` error from the `apply` operation. Use `export TF_LOG=DEBUG` to view exact problem returned by the api. +* Wildcards are not supported with Let's Encrypt yet. diff --git a/website/scaleway.erb b/website/scaleway.erb index 8d5e5aca8..ef0f60d91 100644 --- a/website/scaleway.erb +++ b/website/scaleway.erb @@ -142,6 +142,9 @@
  • scaleway_lb_backend_beta
  • +
  • + scaleway_lb_certificate_beta +
  • scaleway_lb_frontend_beta