From bf3d8c2c692747b96c58f990aedae8c94785ddd9 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Wed, 31 Jan 2024 11:41:36 +0000 Subject: [PATCH 01/19] Add new redirect APIs --- ns1/examples/redirect.tf | 16 ++ ns1/provider.go | 32 ++- ns1/resource_redirect.go | 399 ++++++++++++++++++++++++++ ns1/resource_redirect_test.go | 174 +++++++++++ website/docs/r/redirect.html.markdown | 89 ++++++ website/ns1.erb | 3 + 6 files changed, 698 insertions(+), 15 deletions(-) create mode 100644 ns1/examples/redirect.tf create mode 100644 ns1/resource_redirect.go create mode 100644 ns1/resource_redirect_test.go create mode 100644 website/docs/r/redirect.html.markdown diff --git a/ns1/examples/redirect.tf b/ns1/examples/redirect.tf new file mode 100644 index 00000000..d1c6e66d --- /dev/null +++ b/ns1/examples/redirect.tf @@ -0,0 +1,16 @@ + +resource "ns1_redirect_certificate" "example" { + domain = "www.example.com" +} + +resource "ns1_redirect" "example" { + domain = "www.example.com" + path = "/from/path" + target = "https://url.com/target/path" + forwarding_mode = "all" + forwarding_type = "permanent" + ssl_enabled = true + force_redirect = true + query_forwarding = true + tags = [] +} diff --git a/ns1/provider.go b/ns1/provider.go index 1edf1879..c53cdde0 100644 --- a/ns1/provider.go +++ b/ns1/provider.go @@ -62,21 +62,23 @@ func Provider() *schema.Provider { "ns1_networks": dataSourceNetworks(), }, ResourcesMap: map[string]*schema.Resource{ - "ns1_zone": resourceZone(), - "ns1_record": recordResource(), - "ns1_datasource": dataSourceResource(), - "ns1_datafeed": dataFeedResource(), - "ns1_monitoringjob": monitoringJobResource(), - "ns1_notifylist": notifyListResource(), - "ns1_user": userResource(), - "ns1_apikey": apikeyResource(), - "ns1_team": teamResource(), - "ns1_application": resourceApplication(), - "ns1_pulsarjob": pulsarJobResource(), - "ns1_tsigkey": tsigKeyResource(), - "ns1_subnet": resourceSubnet(), - "ns1_dnsview": dnsView(), - "ns1_account_whitelist": accountWhitelistResource(), + "ns1_zone": resourceZone(), + "ns1_record": recordResource(), + "ns1_datasource": dataSourceResource(), + "ns1_datafeed": dataFeedResource(), + "ns1_monitoringjob": monitoringJobResource(), + "ns1_notifylist": notifyListResource(), + "ns1_user": userResource(), + "ns1_apikey": apikeyResource(), + "ns1_team": teamResource(), + "ns1_application": resourceApplication(), + "ns1_pulsarjob": pulsarJobResource(), + "ns1_tsigkey": tsigKeyResource(), + "ns1_subnet": resourceSubnet(), + "ns1_dnsview": dnsView(), + "ns1_account_whitelist": accountWhitelistResource(), + "ns1_redirect": redirectConfigResource(), + "ns1_redirect_certificate": redirectCertificateResource(), }, ConfigureFunc: ns1Configure, } diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go new file mode 100644 index 00000000..2ad46a5c --- /dev/null +++ b/ns1/resource_redirect.go @@ -0,0 +1,399 @@ +package ns1 + +import ( + "fmt" + "log" + "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + ns1 "gopkg.in/ns1/ns1-go.v2/rest" + "gopkg.in/ns1/ns1-go.v2/rest/model/redirect" +) + +var forwardingTypeStringEnum = NewStringEnum([]string{ + "permanent", + "temporary", + "masking", +}) + +var forwardingModeStringEnum = NewStringEnum([]string{ + "all", + "capture", + "none", +}) + +func redirectConfigResource() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + // Required + "domain": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateDomain, + DiffSuppressFunc: caseSensitivityDiffSuppress, + }, + "path": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validatePath, + DiffSuppressFunc: caseSensitivityDiffSuppress, + }, + "target": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateURL, + DiffSuppressFunc: caseSensitivityDiffSuppress, + }, + // Read-only + "id": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_id": { + Type: schema.TypeString, + Computed: true, + }, + // Optional + "forwarding_mode": { + Type: schema.TypeString, + Optional: true, + Default: "all", + ValidateFunc: forwardingModeStringEnum.ValidateFunc, + }, + "forwarding_type": { + Type: schema.TypeString, + Optional: true, + Default: "permanent", + ValidateFunc: forwardingTypeStringEnum.ValidateFunc, + }, + "ssl_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "force_redirect": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "query_forwarding": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "tags": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + }, + Create: RedirectConfigCreate, + Read: RedirectConfigRead, + Update: RedirectConfigUpdate, + Delete: RedirectConfigDelete, + } +} + +func redirectCertificateResource() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + // Required + "domain": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateDomain, + DiffSuppressFunc: caseSensitivityDiffSuppress, + }, + // Read-only + "id": { + Type: schema.TypeString, + Computed: true, + }, + "certificate": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "errors": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "processing": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + }, + Create: RedirectCertCreate, + Read: RedirectCertRead, + Update: RedirectCertUpdate, + Delete: RedirectCertDelete, + } +} + +// RedirectConfigCreate creates a redirect configuration +func RedirectConfigCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ns1.Client) + + var tags []string + terraformTags := d.Get("tags").([]interface{}) + for _, t := range terraformTags { + tags = append(tags, t.(string)) + } + + r := redirect.NewConfiguration( + d.Get("domain").(string), + d.Get("path").(string), + d.Get("target").(string), + tags, + getFwModep(d, "forwarding_mode"), + getFwTypep(d, "forwarding_type"), + getBoolp(d, "ssl_enabled"), + getBoolp(d, "force_redirect"), + getBoolp(d, "query_forwarding"), + ) + + cfg, resp, err := client.Redirects.Create(r) + if err != nil { + return ConvertToNs1Error(resp, err) + } + + return redirectConfigToResourceData(d, cfg) +} + +// RedirectConfigRead reads the redirect config from ns1 +func RedirectConfigRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ns1.Client) + + cfg, resp, err := client.Redirects.Get(d.Get("id").(string)) + if err != nil { + if err == ns1.ErrRedirectNotFound { + log.Printf("[DEBUG] NS1 redirect config (%s) not found", d.Id()) + d.SetId("") + return nil + } + + return ConvertToNs1Error(resp, err) + } + + return redirectConfigToResourceData(d, cfg) +} + +// RedirectConfigDelete deletes the redirect config from ns1 +func RedirectConfigDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ns1.Client) + resp, err := client.Redirects.Delete(d.Get("id").(string)) + d.SetId("") + return ConvertToNs1Error(resp, err) +} + +// RedirectConfigUpdate updates the given redirect config in ns1 +func RedirectConfigUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ns1.Client) + + var tags []string + terraformTags := d.Get("tags").([]interface{}) + for _, t := range terraformTags { + tags = append(tags, t.(string)) + } + + r := redirect.NewConfiguration( + d.Get("domain").(string), + d.Get("path").(string), + d.Get("target").(string), + tags, + getFwModep(d, "forwarding_mode"), + getFwTypep(d, "forwarding_type"), + getBoolp(d, "ssl_enabled"), + getBoolp(d, "force_redirect"), + getBoolp(d, "query_forwarding"), + ) + id := d.Id() + r.ID = &id + certId := d.Get("certificate_id").(string) + r.CertificateID = &certId + + cfg, resp, err := client.Redirects.Update(r) + if err != nil { + return ConvertToNs1Error(resp, err) + } + return redirectConfigToResourceData(d, cfg) +} + +// RedirectCertCreate creates a redirect certificate +func RedirectCertCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ns1.Client) + + cert, resp, err := client.RedirectCertificates.Create(d.Get("domain").(string)) + if err != nil { + return ConvertToNs1Error(resp, err) + } + + return redirectCertToResourceData(d, cert) +} + +// RedirectCertRead reads the redirect certificate from ns1 +func RedirectCertRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ns1.Client) + + cert, resp, err := client.RedirectCertificates.Get(d.Get("id").(string)) + if err != nil { + if err == ns1.ErrRedirectNotFound { + log.Printf("[DEBUG] NS1 redirect certificate (%s) not found", d.Id()) + d.SetId("") + return nil + } + + return ConvertToNs1Error(resp, err) + } + + return redirectCertToResourceData(d, cert) +} + +// RedirectCertDelete deletes the redirect certificate from ns1 +func RedirectCertDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ns1.Client) + resp, err := client.RedirectCertificates.Delete(d.Get("id").(string)) + d.SetId("") + return ConvertToNs1Error(resp, err) +} + +// RedirectCertUpdate updates the given redirect certificate in ns1 +func RedirectCertUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ns1.Client) + resp, err := client.RedirectCertificates.Update(d.Id()) + return ConvertToNs1Error(resp, err) +} + +// validateDomain verifies that the string matches a valid FQDN. +func validateDomain(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + + match, err := regexp.MatchString("^(\\*\\.)?([\\w-]+\\.)*[\\w-]+$", v) + if err != nil { + errs = append(errs, fmt.Errorf("%s is invalid, got: %s, error: %e", key, v, err)) + } + + if !match { + errs = append(errs, fmt.Errorf("%s is not a valid FQDN, got: %s", key, v)) + } + + return warns, errs +} + +// validatePath verifies that the pstringath matches a valid URL path. +func validatePath(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + + match, err := regexp.MatchString("^[*]?[a-zA-Z0-9\\.\\-/$!+(_)' ]+[*]?$", v) + if err != nil { + errs = append(errs, fmt.Errorf("%s is invalid, got: %s, error: %e", key, v, err)) + } + + if !match { + errs = append(errs, fmt.Errorf("%s is not a valid FQDN, got: %s", key, v)) + } + + return warns, errs +} + +// validateURL verifies that the string is a valid URL. +func validateURL(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + + match, err := regexp.MatchString("^(http://|https://)?[a-zA-Z0-9\\.\\-/$!+(_)' ]+$", v) + if err != nil { + errs = append(errs, fmt.Errorf("%s is invalid, got: %s, error: %e", key, v, err)) + } + + if !match { + errs = append(errs, fmt.Errorf("%s is not a valid FQDN, got: %s", key, v)) + } + + return warns, errs +} + +// return nil if the value is not set, a valid pointer if it is +func getBoolp(d *schema.ResourceData, key string) *bool { + val, exists := d.GetOk(key) + if exists { + ret := val.(bool) + return &ret + } else { + return nil + } +} + +// return nil if the value is not set, a valid pointer if it is +func getFwTypep(d *schema.ResourceData, key string) *redirect.ForwardingType { + val := d.Get(key).(string) + ret, found := redirect.ParseForwardingType(val) + if found { + return &ret + } else { + return nil + } +} + +// return nil if the value is not set, a valid pointer if it is +func getFwModep(d *schema.ResourceData, key string) *redirect.ForwardingMode { + val := d.Get(key).(string) + ret, found := redirect.ParseForwardingMode(val) + if found { + return &ret + } else { + return nil + } +} + +func redirectConfigToResourceData(d *schema.ResourceData, r *redirect.Configuration) error { + d.Set("domain", r.Domain) + d.Set("path", r.Path) + d.Set("target", r.Target) + if r.ID != nil { + d.SetId(*r.ID) + } + if r.CertificateID != nil { + d.Set("certificate_id", *r.CertificateID) + } + if r.ForwardingMode != nil { + d.Set("forwarding_mode", r.ForwardingMode.String()) + } + if r.ForwardingType != nil { + d.Set("forwarding_type", r.ForwardingType.String()) + } + if r.SslEnabled != nil { + d.Set("ssl_enabled", *r.SslEnabled) + } + if r.ForceRedirect != nil { + d.Set("force_redirect", *r.ForceRedirect) + } + if r.QueryForwarding != nil { + d.Set("query_forwarding", *r.QueryForwarding) + } + if r.Tags != nil { + d.Set("tags", r.Tags) + } + return nil +} + +func redirectCertToResourceData(d *schema.ResourceData, r *redirect.Certificate) error { + d.Set("domain", r.Domain) + if r.ID != nil { + d.SetId(*r.ID) + } + if r.Certificate != nil { + d.Set("certificate", *r.Certificate) + } + if r.Errors != nil { + d.Set("errors", *r.Errors) + } + if r.Processing != nil { + d.Set("processing", *r.Processing) + } + return nil +} diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go new file mode 100644 index 00000000..fd60a975 --- /dev/null +++ b/ns1/resource_redirect_test.go @@ -0,0 +1,174 @@ +package ns1 + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + ns1 "gopkg.in/ns1/ns1-go.v2/rest" + "gopkg.in/ns1/ns1-go.v2/rest/model/redirect" +) + +func TestAccRedirectConfig_basic(t *testing.T) { + var redirect redirect.Configuration + rString := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + domainName := fmt.Sprintf("terraform-test-%s.io", rString) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRedirectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRedirectBasic(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "masking"), + ), + }, + { + Config: testAccRedirectUpdated(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "permanent"), + ), + }, + }, + }) +} + +func testAccRedirectBasic(rString string) string { + return fmt.Sprintf(` +resource "ns1_redirect" "it" { + domain = "test.${ns1_zone.test.zone}" + path = "/from/path/*" + target = "https://url.com/target/path" + forwarding_mode = "capture" + forwarding_type = "masking" + ssl_enabled = true + force_redirect = true + query_forwarding = true + tags = [ "test", "it" ] +} + +resource "ns1_redirect_certificate" "example" { + domain = "*.${ns1_zone.test.zone}" +} + +resource "ns1_zone" "test" { + zone = "terraform-test-%s.io" +} +`, rString) +} + +func testAccRedirectUpdated(rString string) string { + return fmt.Sprintf(` +resource "ns1_redirect" "it" { + domain = "test.${ns1_zone.test.zone}" + path = "/from/path/*" + target = "https://url.com/target/path" + forwarding_mode = "capture" + forwarding_type = "permanent" + ssl_enabled = true + force_redirect = true + query_forwarding = true + tags = [ "test", "it" ] +} + +resource "ns1_redirect_certificate" "example" { + domain = "*.${ns1_zone.test.zone}" +} + +resource "ns1_zone" "test" { + zone = "terraform-test-%s.io" +} +`, rString) +} + +func testAccCheckRedirectDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ns1.Client) + + var id string + var certId string + + for _, rs := range s.RootModule().Resources { + if rs.Type != "ns1_redirect" && rs.Type != "ns1_redirect_certificate" { + continue + } + + if rs.Type == "ns1_redirect" { + id = rs.Primary.ID + } + + if rs.Type == "ns1_redirect_certificate" { + certId = rs.Primary.ID + } + } + + if id != "" { + foundRecord, _, err := client.Redirects.Get(id) + if err != ns1.ErrRedirectNotFound { + return fmt.Errorf("redirect still exists: %#v %#v", foundRecord, err) + } + } + if certId != "" { + foundRecord, _, err := client.RedirectCertificates.Get(certId) + if err != ns1.ErrRedirectCertificateNotFound { + return fmt.Errorf("certificate still exists: %#v %#v", foundRecord, err) + } + } + + return nil +} + +func testAccCheckRedirectConfigExists(n string, cfg *redirect.Configuration) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %v", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("noID is set") + } + + client := testAccProvider.Meta().(*ns1.Client) + + p := rs.Primary + + foundCfg, _, err := client.Redirects.Get(p.Attributes["id"]) + if err != nil { + return fmt.Errorf("redirect not found") + } + + if foundCfg.ID == nil || *foundCfg.ID != p.Attributes["id"] { + return fmt.Errorf("redirect not found") + } + + *cfg = *foundCfg + + return nil + } +} + +func testAccCheckRedirectConfigDomain(cfg *redirect.Configuration, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if cfg.Domain != expected { + return fmt.Errorf("Name: got: %s want: %s", cfg.Domain, expected) + } + return nil + } +} + +func testAccCheckRedirectConfigFwType(cfg *redirect.Configuration, expected string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if cfg.ForwardingType.String() != expected { + return fmt.Errorf("Name: got: %s want: %s", cfg.ForwardingType.String(), expected) + } + return nil + } +} diff --git a/website/docs/r/redirect.html.markdown b/website/docs/r/redirect.html.markdown new file mode 100644 index 00000000..8a176221 --- /dev/null +++ b/website/docs/r/redirect.html.markdown @@ -0,0 +1,89 @@ +--- +layout: "ns1" +page_title: "NS1: ns1_redirect" +sidebar_current: "docs-ns1-resource-redirect" +description: |- + Provides a NS1 Redirect resource. +--- + +# ns1\_redirect + +Provides a NS1 Redirect resource. This can be used to create, modify, and delete redirects. + +## Example Usage + +```hcl +resource "ns1_redirect" "example" { + domain = "www.example.com" + path = "/from/path" + target = "https://url.com/target/path" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `domain` - (Required) The domain name to redirect from. +* `path` - (Required) The path on the domain to redirect from. +* `target` - (Required) The URL to redirect to. +* `id` - (Optional) The redirect id, if already created. +* `certificate_id` - (Optional) The certificate redirect id, if already created. +* `forwarding_mode` - (Optional - defaults to "all") How the target is interpreted: + * __all__ appends the entire incoming path to the target destination; + * __capture__ appends only the part of the incoming path corresponding to the wildcard (*); + * __none__ does not append any part of the incoming path. +* `forwarding_type` - (Optional - defaults to "permanent") How the redirect is executed: + * __permanent__ (HTTP 301) indicates to search engines that they should remove the old page from + their database and replace it with the new target page (this is recommended for SEO); + * __temporary__ (HTTP 302) less common, indicates that search engines should keep the old domain or + page indexed as the redirect is only temporary (while both pages might appear in the + search results, a temporary redirect suggests to the search engine that it should + prefer the new target page); + * __masking__ preserves the redirected domain in the browser's address bar (this lets users see the + address they entered, even though the displayed content comes from a different web page). +* `ssl_enabled` - (Optional - defaults to true) Enables HTTPS support on the source domain by using Let's Encrypt certificates. +* `force_redirect` - (Optional - defaults to true) Forces redirect for users that try to visit HTTP domain to HTTPS instead. +* `query_forwarding` - (Optional - defaults to false) Enables the query string of a URL to be applied directly to the new target URL. +* `tags` - (Optional - array) Tags associated with the configuration. + +## Attributes Reference + +All of the arguments listed above are exported as attributes, with no +additions. + +## NS1 Documentation + +[Redirect Api Doc](https://ns1.com/api#redirect) + + +# ns1\_redirect\_certificate + +Provides a NS1 Redirect Certificate resource. This can be used to create, modify, and delete redirect certificates. + +## Example Usage + +```hcl +resource "ns1_redirect_certificate" "example" { + domain = "www.example.com" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `domain` - (Required) The domain the redirect refers to. +* `id` - (Optional) The certificate id, if already created. +* `certificate` - (Read Only) The certificate value. +* `processing` - (Read Only) Whether the certificate is active. +* `errors` - (Read Only) Any error encountered when applying the certificate. + +## Attributes Reference + +All of the arguments listed above are exported as attributes, with no +additions. + +## NS1 Documentation + +[Redirect Api Doc](https://ns1.com/api#redirect) diff --git a/website/ns1.erb b/website/ns1.erb index a9d6d9f4..5933bbd0 100644 --- a/website/ns1.erb +++ b/website/ns1.erb @@ -58,6 +58,9 @@ > ns1_user + > + ns1_user + From c4195d41a648993ad86bf2de3b043a62b8c94f5a Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Thu, 7 Mar 2024 16:36:55 +0000 Subject: [PATCH 02/19] Update objects --- ns1/examples/redirect.tf | 4 +-- ns1/provider.go | 2 +- ns1/resource_redirect.go | 48 +++++++++++++++++++++------ ns1/resource_redirect_test.go | 8 ++--- website/docs/r/redirect.html.markdown | 8 +++-- 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/ns1/examples/redirect.tf b/ns1/examples/redirect.tf index d1c6e66d..b0e9af26 100644 --- a/ns1/examples/redirect.tf +++ b/ns1/examples/redirect.tf @@ -9,8 +9,8 @@ resource "ns1_redirect" "example" { target = "https://url.com/target/path" forwarding_mode = "all" forwarding_type = "permanent" - ssl_enabled = true - force_redirect = true + https_enabled = true + https_forced = true query_forwarding = true tags = [] } diff --git a/ns1/provider.go b/ns1/provider.go index 4b734ca1..f611472a 100644 --- a/ns1/provider.go +++ b/ns1/provider.go @@ -77,7 +77,7 @@ func Provider() *schema.Provider { "ns1_subnet": resourceSubnet(), "ns1_dnsview": dnsView(), "ns1_account_whitelist": accountWhitelistResource(), - "ns1_dataset": datasetResource(), + "ns1_dataset": datasetResource(), "ns1_redirect": redirectConfigResource(), "ns1_redirect_certificate": redirectCertificateResource(), }, diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index 2ad46a5c..de1d0e66 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -55,6 +55,10 @@ func redirectConfigResource() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "last_updated": { + Type: schema.TypeInt, + Computed: true, + }, // Optional "forwarding_mode": { Type: schema.TypeString, @@ -68,12 +72,12 @@ func redirectConfigResource() *schema.Resource { Default: "permanent", ValidateFunc: forwardingTypeStringEnum.ValidateFunc, }, - "ssl_enabled": { + "https_enabled": { Type: schema.TypeBool, Optional: true, Default: true, }, - "force_redirect": { + "https_forced": { Type: schema.TypeBool, Optional: true, Default: true, @@ -117,6 +121,14 @@ func redirectCertificateResource() *schema.Resource { Optional: true, Computed: true, }, + "valid_from": { + Type: schema.TypeInt, + Computed: true, + }, + "valid_until": { + Type: schema.TypeInt, + Computed: true, + }, "errors": { Type: schema.TypeString, Optional: true, @@ -127,6 +139,10 @@ func redirectCertificateResource() *schema.Resource { Optional: true, Computed: true, }, + "last_updated": { + Type: schema.TypeInt, + Computed: true, + }, }, Create: RedirectCertCreate, Read: RedirectCertRead, @@ -152,8 +168,8 @@ func RedirectConfigCreate(d *schema.ResourceData, meta interface{}) error { tags, getFwModep(d, "forwarding_mode"), getFwTypep(d, "forwarding_type"), - getBoolp(d, "ssl_enabled"), - getBoolp(d, "force_redirect"), + getBoolp(d, "https_enabled"), + getBoolp(d, "https_forced"), getBoolp(d, "query_forwarding"), ) @@ -208,8 +224,8 @@ func RedirectConfigUpdate(d *schema.ResourceData, meta interface{}) error { tags, getFwModep(d, "forwarding_mode"), getFwTypep(d, "forwarding_type"), - getBoolp(d, "ssl_enabled"), - getBoolp(d, "force_redirect"), + getBoolp(d, "https_enabled"), + getBoolp(d, "https_forced"), getBoolp(d, "query_forwarding"), ) id := d.Id() @@ -366,11 +382,11 @@ func redirectConfigToResourceData(d *schema.ResourceData, r *redirect.Configurat if r.ForwardingType != nil { d.Set("forwarding_type", r.ForwardingType.String()) } - if r.SslEnabled != nil { - d.Set("ssl_enabled", *r.SslEnabled) + if r.HttpsEnabled != nil { + d.Set("https_enabled", *r.HttpsEnabled) } - if r.ForceRedirect != nil { - d.Set("force_redirect", *r.ForceRedirect) + if r.HttpsForced != nil { + d.Set("https_forced", *r.HttpsForced) } if r.QueryForwarding != nil { d.Set("query_forwarding", *r.QueryForwarding) @@ -378,6 +394,9 @@ func redirectConfigToResourceData(d *schema.ResourceData, r *redirect.Configurat if r.Tags != nil { d.Set("tags", r.Tags) } + if r.LastUpdated != nil { + d.Set("last_updated", *r.LastUpdated) + } return nil } @@ -389,11 +408,20 @@ func redirectCertToResourceData(d *schema.ResourceData, r *redirect.Certificate) if r.Certificate != nil { d.Set("certificate", *r.Certificate) } + if r.ValidFrom != nil { + d.Set("valid_from", *r.ValidFrom) + } + if r.ValidUntil != nil { + d.Set("valid_until", *r.ValidUntil) + } if r.Errors != nil { d.Set("errors", *r.Errors) } if r.Processing != nil { d.Set("processing", *r.Processing) } + if r.LastUpdated != nil { + d.Set("last_updated", *r.LastUpdated) + } return nil } diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index fd60a975..f2500f26 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -49,8 +49,8 @@ resource "ns1_redirect" "it" { target = "https://url.com/target/path" forwarding_mode = "capture" forwarding_type = "masking" - ssl_enabled = true - force_redirect = true + https_enabled = true + https_forced = true query_forwarding = true tags = [ "test", "it" ] } @@ -73,8 +73,8 @@ resource "ns1_redirect" "it" { target = "https://url.com/target/path" forwarding_mode = "capture" forwarding_type = "permanent" - ssl_enabled = true - force_redirect = true + https_enabled = true + https_forced = true query_forwarding = true tags = [ "test", "it" ] } diff --git a/website/docs/r/redirect.html.markdown b/website/docs/r/redirect.html.markdown index 8a176221..ca693ab8 100644 --- a/website/docs/r/redirect.html.markdown +++ b/website/docs/r/redirect.html.markdown @@ -42,10 +42,11 @@ The following arguments are supported: prefer the new target page); * __masking__ preserves the redirected domain in the browser's address bar (this lets users see the address they entered, even though the displayed content comes from a different web page). -* `ssl_enabled` - (Optional - defaults to true) Enables HTTPS support on the source domain by using Let's Encrypt certificates. -* `force_redirect` - (Optional - defaults to true) Forces redirect for users that try to visit HTTP domain to HTTPS instead. +* `https_enabled` - (Optional - defaults to true) Enables HTTPS support on the source domain by using Let's Encrypt certificates. +* `https_forced` - (Optional - defaults to true) Forces redirect for users that try to visit HTTP domain to HTTPS instead. * `query_forwarding` - (Optional - defaults to false) Enables the query string of a URL to be applied directly to the new target URL. * `tags` - (Optional - array) Tags associated with the configuration. +* `last_updated` - (Read Only) The Unix timestamp representing when the redirect configuration was last updated. ## Attributes Reference @@ -76,8 +77,11 @@ The following arguments are supported: * `domain` - (Required) The domain the redirect refers to. * `id` - (Optional) The certificate id, if already created. * `certificate` - (Read Only) The certificate value. +* `valid_from` - (Read Only) The Unix timestamp representing when the certificate first started being valid. +* `valid_until` - (Read Only) The Unix timestamp representing when the certificate will stop being valid. * `processing` - (Read Only) Whether the certificate is active. * `errors` - (Read Only) Any error encountered when applying the certificate. +* `last_updated` - (Read Only) The Unix timestamp representing when the certificate was last signed. ## Attributes Reference From 18e4121aa880ebbe8a8ef69f8105a4dce16b0a27 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Fri, 31 May 2024 11:40:15 +0100 Subject: [PATCH 03/19] Update ns1-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 953bc745..fe006202 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.2 github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 github.com/stretchr/testify v1.8.1 - gopkg.in/ns1/ns1-go.v2 v2.9.1 + gopkg.in/ns1/ns1-go.v2 v2.11.0 ) require ( diff --git a/go.sum b/go.sum index 7dcb54c0..3cc721e3 100644 --- a/go.sum +++ b/go.sum @@ -250,8 +250,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ns1/ns1-go.v2 v2.9.1 h1:3/QYzUazRCSE49d3sh1Q+X7IrDp/I7OqR/M7dKA0Oks= -gopkg.in/ns1/ns1-go.v2 v2.9.1/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= +gopkg.in/ns1/ns1-go.v2 v2.11.0 h1:T+rMHhQsQ58bSgGZwX8INxU0sjDO7cWieX9xPr/UEY4= +gopkg.in/ns1/ns1-go.v2 v2.11.0/go.mod h1:pfaU0vECVP7DIOr453z03HXS6dFJpXdNRwOyRzwmPSc= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 8fea570d3716c2f9bd131849e818c48adc40c0d3 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Wed, 5 Jun 2024 15:28:49 +0100 Subject: [PATCH 04/19] Add changelog and new version --- CHANGELOG.md | 9 +++++++++ ns1/config.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c5a596..d323358d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.3.0 (June 6, 2024) +ENHANCEMENTS +* Adds support for redirect endpoints + +## 2.2.2 (May 21, 2024) +BUGFIX +* Lifting ordering constraint on ns1_notifylist notifications objects +* Documentation fix for `tags` + ## 2.2.1 (April 3, 2024) BUGFIX * `ns1-go` client version bump to fix omitting tags diff --git a/ns1/config.go b/ns1/config.go index de0d54c0..babacd30 100644 --- a/ns1/config.go +++ b/ns1/config.go @@ -19,7 +19,7 @@ import ( ) var ( - clientVersion = "2.2.2" + clientVersion = "2.3.0" providerUserAgent = "tf-ns1" + "/" + clientVersion defaultRetryMax = 3 ) From 0dab8a17518c1121ff05f7cdfcb8282ca0f32ae8 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Wed, 5 Jun 2024 15:35:28 +0100 Subject: [PATCH 05/19] Add note in doc --- website/docs/r/redirect.html.markdown | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/docs/r/redirect.html.markdown b/website/docs/r/redirect.html.markdown index ca693ab8..bdca1c89 100644 --- a/website/docs/r/redirect.html.markdown +++ b/website/docs/r/redirect.html.markdown @@ -53,6 +53,9 @@ The following arguments are supported: All of the arguments listed above are exported as attributes, with no additions. +Note that https_enabled=true causes a certificate to be requested if none exist: the resulting certificate object +would not be tracked by the state, so it's recommended to create the certificate object manually in order to track it in terraform state. + ## NS1 Documentation [Redirect Api Doc](https://ns1.com/api#redirect) From f8be1e2187a256f1ed13fe584b4200ddff280ac3 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Thu, 6 Jun 2024 11:57:44 +0100 Subject: [PATCH 06/19] Force to use the wildcard cert in test --- ns1/resource_redirect.go | 8 ++++---- ns1/resource_redirect_test.go | 26 +++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index de1d0e66..5d08a7b5 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -51,15 +51,15 @@ func redirectConfigResource() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "certificate_id": { - Type: schema.TypeString, - Computed: true, - }, "last_updated": { Type: schema.TypeInt, Computed: true, }, // Optional + "certificate_id": { + Type: schema.TypeString, + Optional: true, + }, "forwarding_mode": { Type: schema.TypeString, Optional: true, diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index f2500f26..bf453dcc 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -2,6 +2,7 @@ package ns1 import ( "fmt" + "strings" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" @@ -27,6 +28,7 @@ func TestAccRedirectConfig_basic(t *testing.T) { testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), testAccCheckRedirectConfigFwType(&redirect, "masking"), + testAccCheckRedirectConfigTags(&redirect, []string{"test", "it"}), ), }, { @@ -35,6 +37,7 @@ func TestAccRedirectConfig_basic(t *testing.T) { testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), testAccCheckRedirectConfigFwType(&redirect, "permanent"), + testAccCheckRedirectConfigTags(&redirect, []string{"test"}), ), }, }, @@ -44,6 +47,7 @@ func TestAccRedirectConfig_basic(t *testing.T) { func testAccRedirectBasic(rString string) string { return fmt.Sprintf(` resource "ns1_redirect" "it" { + certificate_id = "${ns1_redirect_certificate.example.id}" domain = "test.${ns1_zone.test.zone}" path = "/from/path/*" target = "https://url.com/target/path" @@ -68,6 +72,7 @@ resource "ns1_zone" "test" { func testAccRedirectUpdated(rString string) string { return fmt.Sprintf(` resource "ns1_redirect" "it" { + certificate_id = "${ns1_redirect_certificate.example.id}" domain = "test.${ns1_zone.test.zone}" path = "/from/path/*" target = "https://url.com/target/path" @@ -76,7 +81,7 @@ resource "ns1_redirect" "it" { https_enabled = true https_forced = true query_forwarding = true - tags = [ "test", "it" ] + tags = [ "test" ] } resource "ns1_redirect_certificate" "example" { @@ -172,3 +177,22 @@ func testAccCheckRedirectConfigFwType(cfg *redirect.Configuration, expected stri return nil } } + +func testAccCheckRedirectConfigTags(cfg *redirect.Configuration, expected []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + diff := false + if len(cfg.Tags) != len(expected) { + diff = true + } else { + for i, _ := range expected { + if cfg.Tags[i] != expected[i] { + diff = true + } + } + } + if diff { + return fmt.Errorf("Name: got: %s want: %s", strings.Join(cfg.Tags, ","), strings.Join(expected, ",")) + } + return nil + } +} From e7403a23cd71b2a5951400295cb3f449dced04db Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Thu, 6 Jun 2024 14:24:04 +0100 Subject: [PATCH 07/19] Fix tags update --- ns1/resource_redirect.go | 3 +++ ns1/resource_redirect_test.go | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index 5d08a7b5..1f96f391 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -213,6 +213,9 @@ func RedirectConfigUpdate(d *schema.ResourceData, meta interface{}) error { var tags []string terraformTags := d.Get("tags").([]interface{}) + if terraformTags != nil { + tags = []string{} + } for _, t := range terraformTags { tags = append(tags, t.(string)) } diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index bf453dcc..62adf977 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -37,7 +37,7 @@ func TestAccRedirectConfig_basic(t *testing.T) { testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), testAccCheckRedirectConfigFwType(&redirect, "permanent"), - testAccCheckRedirectConfigTags(&redirect, []string{"test"}), + testAccCheckRedirectConfigTags(&redirect, []string{}), ), }, }, @@ -81,7 +81,7 @@ resource "ns1_redirect" "it" { https_enabled = true https_forced = true query_forwarding = true - tags = [ "test" ] + tags = [ ] } resource "ns1_redirect_certificate" "example" { @@ -184,7 +184,7 @@ func testAccCheckRedirectConfigTags(cfg *redirect.Configuration, expected []stri if len(cfg.Tags) != len(expected) { diff = true } else { - for i, _ := range expected { + for i := range expected { if cfg.Tags[i] != expected[i] { diff = true } From 01b1b2b1c1b9c4e8ae57f2dde4c97c9d35c089ca Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Thu, 6 Jun 2024 14:40:37 +0100 Subject: [PATCH 08/19] Enabling import --- ns1/resource_redirect.go | 20 +++++++++++--------- ns1/resource_redirect_test.go | 10 ++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index 1f96f391..71db01c4 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -93,10 +93,11 @@ func redirectConfigResource() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, }, - Create: RedirectConfigCreate, - Read: RedirectConfigRead, - Update: RedirectConfigUpdate, - Delete: RedirectConfigDelete, + Create: RedirectConfigCreate, + Read: RedirectConfigRead, + Update: RedirectConfigUpdate, + Delete: RedirectConfigDelete, + Importer: &schema.ResourceImporter{}, } } @@ -144,10 +145,11 @@ func redirectCertificateResource() *schema.Resource { Computed: true, }, }, - Create: RedirectCertCreate, - Read: RedirectCertRead, - Update: RedirectCertUpdate, - Delete: RedirectCertDelete, + Create: RedirectCertCreate, + Read: RedirectCertRead, + Update: RedirectCertUpdate, + Delete: RedirectCertDelete, + Importer: &schema.ResourceImporter{}, } } @@ -304,7 +306,7 @@ func validateDomain(val interface{}, key string) (warns []string, errs []error) return warns, errs } -// validatePath verifies that the pstringath matches a valid URL path. +// validatePath verifies that the path matches a valid URL path. func validatePath(val interface{}, key string) (warns []string, errs []error) { v := val.(string) diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index 62adf977..d55d1f18 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -40,6 +40,16 @@ func TestAccRedirectConfig_basic(t *testing.T) { testAccCheckRedirectConfigTags(&redirect, []string{}), ), }, + { + ResourceName: "ns1_redirect.it", + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "ns1_redirect_certificate.example", + ImportState: true, + ImportStateVerify: true, + }, }, }) } From 413fa87a6f95fbb825e4ec63485b71080d1d5cfb Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Fri, 7 Jun 2024 16:09:23 +0100 Subject: [PATCH 09/19] Two more test scenarios and fixes for it --- ns1/examples/redirect.tf | 3 +- ns1/resource_redirect.go | 23 +++++- ns1/resource_redirect_test.go | 139 +++++++++++++++++++++++++++++++++- 3 files changed, 158 insertions(+), 7 deletions(-) diff --git a/ns1/examples/redirect.tf b/ns1/examples/redirect.tf index b0e9af26..6e3393db 100644 --- a/ns1/examples/redirect.tf +++ b/ns1/examples/redirect.tf @@ -1,9 +1,10 @@ resource "ns1_redirect_certificate" "example" { - domain = "www.example.com" + domain = "*.example.com" } resource "ns1_redirect" "example" { + certificate_id = "${ns1_redirect_certificate.example.id}" domain = "www.example.com" path = "/from/path" target = "https://url.com/target/path" diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index 71db01c4..edfc9689 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -59,6 +59,7 @@ func redirectConfigResource() *schema.Resource { "certificate_id": { Type: schema.TypeString, Optional: true, + Computed: true, }, "forwarding_mode": { Type: schema.TypeString, @@ -175,6 +176,8 @@ func RedirectConfigCreate(d *schema.ResourceData, meta interface{}) error { getBoolp(d, "query_forwarding"), ) + r.CertificateID = getStringp(d, "certificate_id") + cfg, resp, err := client.Redirects.Create(r) if err != nil { return ConvertToNs1Error(resp, err) @@ -235,8 +238,8 @@ func RedirectConfigUpdate(d *schema.ResourceData, meta interface{}) error { ) id := d.Id() r.ID = &id - certId := d.Get("certificate_id").(string) - r.CertificateID = &certId + + r.CertificateID = getStringp(d, "certificate_id") cfg, resp, err := client.Redirects.Update(r) if err != nil { @@ -340,8 +343,8 @@ func validateURL(val interface{}, key string) (warns []string, errs []error) { // return nil if the value is not set, a valid pointer if it is func getBoolp(d *schema.ResourceData, key string) *bool { - val, exists := d.GetOk(key) - if exists { + val := d.Get(key) + if val != nil { ret := val.(bool) return &ret } else { @@ -349,6 +352,18 @@ func getBoolp(d *schema.ResourceData, key string) *bool { } } +// return nil if the value is not set, a valid pointer if it is +func getStringp(d *schema.ResourceData, key string) *string { + val := d.Get(key) + if val != nil { + ret := val.(string) + if ret != "" { + return &ret + } + } + return nil +} + // return nil if the value is not set, a valid pointer if it is func getFwTypep(d *schema.ResourceData, key string) *redirect.ForwardingType { val := d.Get(key).(string) diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index d55d1f18..1abf18a9 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -54,6 +54,72 @@ func TestAccRedirectConfig_basic(t *testing.T) { }) } +func TestAccRedirectConfig_untracked_cert(t *testing.T) { + var redirect redirect.Configuration + rString := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + domainName := fmt.Sprintf("terraform-test-%s.io", rString) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRedirectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRedirectNoCert(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "masking"), + testAccCheckRedirectConfigTags(&redirect, []string{}), + ), + }, + { + Config: testAccRedirectNoCertUpdated(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "permanent"), + testAccCheckRedirectConfigTags(&redirect, []string{}), + ), + }, + }, + }) +} + +func TestAccRedirectConfig_http_to_https(t *testing.T) { + var redirect redirect.Configuration + rString := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + domainName := fmt.Sprintf("terraform-test-%s.io", rString) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRedirectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRedirectHTTP(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "permanent"), + testAccCheckRedirectConfigHTTPS(&redirect, false), + testAccCheckRedirectConfigCertIdPresent(&redirect, false), + ), + }, + { + Config: testAccRedirectNoCertUpdated(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "permanent"), + testAccCheckRedirectConfigHTTPS(&redirect, true), + testAccCheckRedirectConfigCertIdPresent(&redirect, true), + ), + }, + }, + }) +} + func testAccRedirectBasic(rString string) string { return fmt.Sprintf(` resource "ns1_redirect" "it" { @@ -104,6 +170,57 @@ resource "ns1_zone" "test" { `, rString) } +func testAccRedirectNoCert(rString string) string { + return fmt.Sprintf(` +resource "ns1_redirect" "it" { + domain = "test.${ns1_zone.test.zone}" + path = "/from/path/*" + target = "https://url.com/target/path" + forwarding_mode = "capture" + forwarding_type = "masking" +} + +resource "ns1_zone" "test" { + zone = "terraform-test-%s.io" +} +`, rString) +} + +func testAccRedirectNoCertUpdated(rString string) string { + return fmt.Sprintf(` +resource "ns1_redirect" "it" { + domain = "test.${ns1_zone.test.zone}" + path = "/from/path/*" + target = "https://url.com/target/path" + forwarding_mode = "capture" + forwarding_type = "permanent" + https_enabled = true + https_forced = true + tags = [] +} + +resource "ns1_zone" "test" { + zone = "terraform-test-%s.io" +} +`, rString) +} + +func testAccRedirectHTTP(rString string) string { + return fmt.Sprintf(` +resource "ns1_redirect" "it" { + domain = "test.${ns1_zone.test.zone}" + path = "/from/path/*" + target = "https://url.com/target/path" + https_enabled = false + https_forced = false +} + +resource "ns1_zone" "test" { + zone = "terraform-test-%s.io" +} +`, rString) +} + func testAccCheckRedirectDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*ns1.Client) @@ -173,7 +290,7 @@ func testAccCheckRedirectConfigExists(n string, cfg *redirect.Configuration) res func testAccCheckRedirectConfigDomain(cfg *redirect.Configuration, expected string) resource.TestCheckFunc { return func(s *terraform.State) error { if cfg.Domain != expected { - return fmt.Errorf("Name: got: %s want: %s", cfg.Domain, expected) + return fmt.Errorf("Domain: got: %s want: %s", cfg.Domain, expected) } return nil } @@ -182,7 +299,25 @@ func testAccCheckRedirectConfigDomain(cfg *redirect.Configuration, expected stri func testAccCheckRedirectConfigFwType(cfg *redirect.Configuration, expected string) resource.TestCheckFunc { return func(s *terraform.State) error { if cfg.ForwardingType.String() != expected { - return fmt.Errorf("Name: got: %s want: %s", cfg.ForwardingType.String(), expected) + return fmt.Errorf("ForwardingType: got: %s want: %s", cfg.ForwardingType.String(), expected) + } + return nil + } +} + +func testAccCheckRedirectConfigHTTPS(cfg *redirect.Configuration, expected bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if *cfg.HttpsEnabled != expected { + return fmt.Errorf("HttpsEnabled: got: %t want: %t", *cfg.HttpsEnabled, expected) + } + return nil + } +} + +func testAccCheckRedirectConfigCertIdPresent(cfg *redirect.Configuration, expected bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if (cfg.CertificateID != nil) != expected { + return fmt.Errorf("CertificateID present: got: %t want: %t", cfg.CertificateID != nil, expected) } return nil } From aa04657bc887eac31ba5d141207263a69b1a6a3b Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Fri, 7 Jun 2024 17:04:40 +0100 Subject: [PATCH 10/19] Making cert mandatory for HTTPS and removing no-cert test scenario --- ns1/examples/redirect.tf | 2 +- ns1/resource_redirect.go | 7 +++ ns1/resource_redirect_test.go | 69 +-------------------------- website/docs/r/redirect.html.markdown | 6 +-- 4 files changed, 11 insertions(+), 73 deletions(-) diff --git a/ns1/examples/redirect.tf b/ns1/examples/redirect.tf index 6e3393db..cbd85025 100644 --- a/ns1/examples/redirect.tf +++ b/ns1/examples/redirect.tf @@ -4,7 +4,7 @@ resource "ns1_redirect_certificate" "example" { } resource "ns1_redirect" "example" { - certificate_id = "${ns1_redirect_certificate.example.id}" + certificate_id = "${ns1_redirect_certificate.example.id}" domain = "www.example.com" path = "/from/path" target = "https://url.com/target/path" diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index edfc9689..09f76463 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -1,6 +1,7 @@ package ns1 import ( + "errors" "fmt" "log" "regexp" @@ -177,6 +178,9 @@ func RedirectConfigCreate(d *schema.ResourceData, meta interface{}) error { ) r.CertificateID = getStringp(d, "certificate_id") + if r.CertificateID == nil && (r.HttpsEnabled == nil || *r.HttpsEnabled) { + return errors.New("a certificate is required when https_enabled is not false") + } cfg, resp, err := client.Redirects.Create(r) if err != nil { @@ -240,6 +244,9 @@ func RedirectConfigUpdate(d *schema.ResourceData, meta interface{}) error { r.ID = &id r.CertificateID = getStringp(d, "certificate_id") + if r.CertificateID == nil && (r.HttpsEnabled == nil || *r.HttpsEnabled) { + return errors.New("a certificate is required when https_enabled is not false") + } cfg, resp, err := client.Redirects.Update(r) if err != nil { diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index 1abf18a9..84cb5935 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -54,38 +54,6 @@ func TestAccRedirectConfig_basic(t *testing.T) { }) } -func TestAccRedirectConfig_untracked_cert(t *testing.T) { - var redirect redirect.Configuration - rString := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) - domainName := fmt.Sprintf("terraform-test-%s.io", rString) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckRedirectDestroy, - Steps: []resource.TestStep{ - { - Config: testAccRedirectNoCert(rString), - Check: resource.ComposeTestCheckFunc( - testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), - testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), - testAccCheckRedirectConfigFwType(&redirect, "masking"), - testAccCheckRedirectConfigTags(&redirect, []string{}), - ), - }, - { - Config: testAccRedirectNoCertUpdated(rString), - Check: resource.ComposeTestCheckFunc( - testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), - testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), - testAccCheckRedirectConfigFwType(&redirect, "permanent"), - testAccCheckRedirectConfigTags(&redirect, []string{}), - ), - }, - }, - }) -} - func TestAccRedirectConfig_http_to_https(t *testing.T) { var redirect redirect.Configuration rString := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) @@ -107,7 +75,7 @@ func TestAccRedirectConfig_http_to_https(t *testing.T) { ), }, { - Config: testAccRedirectNoCertUpdated(rString), + Config: testAccRedirectUpdated(rString), Check: resource.ComposeTestCheckFunc( testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), @@ -170,41 +138,6 @@ resource "ns1_zone" "test" { `, rString) } -func testAccRedirectNoCert(rString string) string { - return fmt.Sprintf(` -resource "ns1_redirect" "it" { - domain = "test.${ns1_zone.test.zone}" - path = "/from/path/*" - target = "https://url.com/target/path" - forwarding_mode = "capture" - forwarding_type = "masking" -} - -resource "ns1_zone" "test" { - zone = "terraform-test-%s.io" -} -`, rString) -} - -func testAccRedirectNoCertUpdated(rString string) string { - return fmt.Sprintf(` -resource "ns1_redirect" "it" { - domain = "test.${ns1_zone.test.zone}" - path = "/from/path/*" - target = "https://url.com/target/path" - forwarding_mode = "capture" - forwarding_type = "permanent" - https_enabled = true - https_forced = true - tags = [] -} - -resource "ns1_zone" "test" { - zone = "terraform-test-%s.io" -} -`, rString) -} - func testAccRedirectHTTP(rString string) string { return fmt.Sprintf(` resource "ns1_redirect" "it" { diff --git a/website/docs/r/redirect.html.markdown b/website/docs/r/redirect.html.markdown index bdca1c89..9b8501fc 100644 --- a/website/docs/r/redirect.html.markdown +++ b/website/docs/r/redirect.html.markdown @@ -28,7 +28,8 @@ The following arguments are supported: * `path` - (Required) The path on the domain to redirect from. * `target` - (Required) The URL to redirect to. * `id` - (Optional) The redirect id, if already created. -* `certificate_id` - (Optional) The certificate redirect id, if already created. +* `certificate_id` - (Optional) The certificate redirect id. + This field is mandatory if https_enabled is not false. * `forwarding_mode` - (Optional - defaults to "all") How the target is interpreted: * __all__ appends the entire incoming path to the target destination; * __capture__ appends only the part of the incoming path corresponding to the wildcard (*); @@ -53,9 +54,6 @@ The following arguments are supported: All of the arguments listed above are exported as attributes, with no additions. -Note that https_enabled=true causes a certificate to be requested if none exist: the resulting certificate object -would not be tracked by the state, so it's recommended to create the certificate object manually in order to track it in terraform state. - ## NS1 Documentation [Redirect Api Doc](https://ns1.com/api#redirect) From 787cde03c4a302a872cac568014bd10998ffd198 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Mon, 10 Jun 2024 14:20:49 +0100 Subject: [PATCH 11/19] Make cert create/delete sync by active wait, and https_enabled read-only --- ns1/resource_redirect.go | 59 ++++++++++++++++++++------- ns1/resource_redirect_test.go | 54 +++++++++++++++++++++--- website/docs/r/redirect.html.markdown | 3 +- 3 files changed, 95 insertions(+), 21 deletions(-) diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index 09f76463..e1b59c65 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -1,10 +1,10 @@ package ns1 import ( - "errors" "fmt" "log" "regexp" + "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -52,6 +52,10 @@ func redirectConfigResource() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "https_enabled": { + Type: schema.TypeBool, + Computed: true, + }, "last_updated": { Type: schema.TypeInt, Computed: true, @@ -74,15 +78,10 @@ func redirectConfigResource() *schema.Resource { Default: "permanent", ValidateFunc: forwardingTypeStringEnum.ValidateFunc, }, - "https_enabled": { - Type: schema.TypeBool, - Optional: true, - Default: true, - }, "https_forced": { Type: schema.TypeBool, Optional: true, - Default: true, + Computed: true, }, "query_forwarding": { Type: schema.TypeBool, @@ -177,9 +176,14 @@ func RedirectConfigCreate(d *schema.ResourceData, meta interface{}) error { getBoolp(d, "query_forwarding"), ) - r.CertificateID = getStringp(d, "certificate_id") - if r.CertificateID == nil && (r.HttpsEnabled == nil || *r.HttpsEnabled) { - return errors.New("a certificate is required when https_enabled is not false") + cert := getStringp(d, "certificate_id") + if cert != nil { + r.CertificateID = cert + t := true + r.HttpsEnabled = &t + } else { + f := false + r.HttpsEnabled = &f } cfg, resp, err := client.Redirects.Create(r) @@ -243,9 +247,14 @@ func RedirectConfigUpdate(d *schema.ResourceData, meta interface{}) error { id := d.Id() r.ID = &id - r.CertificateID = getStringp(d, "certificate_id") - if r.CertificateID == nil && (r.HttpsEnabled == nil || *r.HttpsEnabled) { - return errors.New("a certificate is required when https_enabled is not false") + cert := getStringp(d, "certificate_id") + if cert != nil { + r.CertificateID = cert + t := true + r.HttpsEnabled = &t + } else { + f := false + r.HttpsEnabled = &f } cfg, resp, err := client.Redirects.Update(r) @@ -260,6 +269,16 @@ func RedirectCertCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ns1.Client) cert, resp, err := client.RedirectCertificates.Create(d.Get("domain").(string)) + if err == nil && cert.ID != nil { + for i := 0; i < 20; i++ { + // 20 x 500 milliseconds = max 10 seconds plus network delay + time.Sleep(500 * time.Millisecond) + cert, _, err = client.RedirectCertificates.Get(*cert.ID) + if cert.Certificate != nil || cert.Errors != nil { + break + } + } + } if err != nil { return ConvertToNs1Error(resp, err) } @@ -288,7 +307,19 @@ func RedirectCertRead(d *schema.ResourceData, meta interface{}) error { // RedirectCertDelete deletes the redirect certificate from ns1 func RedirectCertDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*ns1.Client) - resp, err := client.RedirectCertificates.Delete(d.Get("id").(string)) + id := d.Get("id").(string) + resp, err := client.RedirectCertificates.Delete(id) + if err == nil { + for i := 0; i < 20; i++ { + // 20 x 500 milliseconds = max 10 seconds plus network delay + time.Sleep(500 * time.Millisecond) + _, _, err := client.RedirectCertificates.Get(id) + if err != nil { + break + } + } + } + d.SetId("") return ConvertToNs1Error(resp, err) } diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index 84cb5935..f8bc1719 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -29,6 +29,8 @@ func TestAccRedirectConfig_basic(t *testing.T) { testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), testAccCheckRedirectConfigFwType(&redirect, "masking"), testAccCheckRedirectConfigTags(&redirect, []string{"test", "it"}), + testAccCheckRedirectConfigHTTPS(&redirect, true), + testAccCheckRedirectConfigCertIdPresent(&redirect, true), ), }, { @@ -38,6 +40,8 @@ func TestAccRedirectConfig_basic(t *testing.T) { testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), testAccCheckRedirectConfigFwType(&redirect, "permanent"), testAccCheckRedirectConfigTags(&redirect, []string{}), + testAccCheckRedirectConfigHTTPS(&redirect, true), + testAccCheckRedirectConfigCertIdPresent(&redirect, true), ), }, { @@ -70,6 +74,7 @@ func TestAccRedirectConfig_http_to_https(t *testing.T) { testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), testAccCheckRedirectConfigFwType(&redirect, "permanent"), + testAccCheckRedirectConfigTags(&redirect, []string{}), testAccCheckRedirectConfigHTTPS(&redirect, false), testAccCheckRedirectConfigCertIdPresent(&redirect, false), ), @@ -80,10 +85,33 @@ func TestAccRedirectConfig_http_to_https(t *testing.T) { testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), testAccCheckRedirectConfigFwType(&redirect, "permanent"), + testAccCheckRedirectConfigTags(&redirect, []string{}), + testAccCheckRedirectConfigHTTPS(&redirect, true), + testAccCheckRedirectConfigCertIdPresent(&redirect, true), + ), + }, + { + Config: testAccRedirectHTTPwithCert(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "permanent"), + testAccCheckRedirectConfigTags(&redirect, []string{}), testAccCheckRedirectConfigHTTPS(&redirect, true), testAccCheckRedirectConfigCertIdPresent(&redirect, true), ), }, + { + Config: testAccRedirectHTTP(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "permanent"), + testAccCheckRedirectConfigTags(&redirect, []string{}), + testAccCheckRedirectConfigHTTPS(&redirect, false), + testAccCheckRedirectConfigCertIdPresent(&redirect, false), + ), + }, }, }) } @@ -91,13 +119,12 @@ func TestAccRedirectConfig_http_to_https(t *testing.T) { func testAccRedirectBasic(rString string) string { return fmt.Sprintf(` resource "ns1_redirect" "it" { - certificate_id = "${ns1_redirect_certificate.example.id}" + certificate_id = "${ns1_redirect_certificate.example.id}" domain = "test.${ns1_zone.test.zone}" path = "/from/path/*" target = "https://url.com/target/path" forwarding_mode = "capture" forwarding_type = "masking" - https_enabled = true https_forced = true query_forwarding = true tags = [ "test", "it" ] @@ -116,13 +143,12 @@ resource "ns1_zone" "test" { func testAccRedirectUpdated(rString string) string { return fmt.Sprintf(` resource "ns1_redirect" "it" { - certificate_id = "${ns1_redirect_certificate.example.id}" + certificate_id = "${ns1_redirect_certificate.example.id}" domain = "test.${ns1_zone.test.zone}" path = "/from/path/*" target = "https://url.com/target/path" forwarding_mode = "capture" forwarding_type = "permanent" - https_enabled = true https_forced = true query_forwarding = true tags = [ ] @@ -144,7 +170,6 @@ resource "ns1_redirect" "it" { domain = "test.${ns1_zone.test.zone}" path = "/from/path/*" target = "https://url.com/target/path" - https_enabled = false https_forced = false } @@ -154,6 +179,25 @@ resource "ns1_zone" "test" { `, rString) } +func testAccRedirectHTTPwithCert(rString string) string { + return fmt.Sprintf(` +resource "ns1_redirect" "it" { + certificate_id = "" + domain = "test.${ns1_zone.test.zone}" + path = "/from/path/*" + target = "https://url.com/target/path" +} + +resource "ns1_redirect_certificate" "example" { + domain = "*.${ns1_zone.test.zone}" +} + +resource "ns1_zone" "test" { + zone = "terraform-test-%s.io" +} +`, rString) +} + func testAccCheckRedirectDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*ns1.Client) diff --git a/website/docs/r/redirect.html.markdown b/website/docs/r/redirect.html.markdown index 9b8501fc..139c4cb4 100644 --- a/website/docs/r/redirect.html.markdown +++ b/website/docs/r/redirect.html.markdown @@ -29,7 +29,6 @@ The following arguments are supported: * `target` - (Required) The URL to redirect to. * `id` - (Optional) The redirect id, if already created. * `certificate_id` - (Optional) The certificate redirect id. - This field is mandatory if https_enabled is not false. * `forwarding_mode` - (Optional - defaults to "all") How the target is interpreted: * __all__ appends the entire incoming path to the target destination; * __capture__ appends only the part of the incoming path corresponding to the wildcard (*); @@ -43,7 +42,7 @@ The following arguments are supported: prefer the new target page); * __masking__ preserves the redirected domain in the browser's address bar (this lets users see the address they entered, even though the displayed content comes from a different web page). -* `https_enabled` - (Optional - defaults to true) Enables HTTPS support on the source domain by using Let's Encrypt certificates. +* `https_enabled` - (Read Only) True if HTTPS is supported on the source domain by using Let's Encrypt certificates. * `https_forced` - (Optional - defaults to true) Forces redirect for users that try to visit HTTP domain to HTTPS instead. * `query_forwarding` - (Optional - defaults to false) Enables the query string of a URL to be applied directly to the new target URL. * `tags` - (Optional - array) Tags associated with the configuration. From 4cae82ad631bfe3a4093900852a51f31dfe01e72 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Fri, 14 Jun 2024 13:47:42 +0100 Subject: [PATCH 12/19] Add further active waits to align to remote changes --- CHANGELOG.md | 2 + ns1/resource_record_test.go | 10 ++--- ns1/resource_redirect.go | 17 ++++++-- ns1/resource_redirect_test.go | 80 +++++++++++++++++++++++++++++++++-- 4 files changed, 98 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d323358d..8d7541e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 2.3.0 (June 6, 2024) ENHANCEMENTS * Adds support for redirect endpoints +BUGFIX +* Lifting ordering constraint on regions in record resource ## 2.2.2 (May 21, 2024) BUGFIX diff --git a/ns1/resource_record_test.go b/ns1/resource_record_test.go index 3d6b70aa..eb800e73 100644 --- a/ns1/resource_record_test.go +++ b/ns1/resource_record_test.go @@ -1311,17 +1311,17 @@ resource "ns1_record" "it" { domain = "test.${ns1_zone.test.zone}" type = "CNAME" ttl = 60 - + answers { answer = "test1.${ns1_zone.test.zone}" region = "cal" } - + regions { name = "cal" } } - + resource "ns1_zone" "test" { zone = "terraform-test-%s.io" } @@ -1365,7 +1365,7 @@ resource "ns1_record" "it" { type = "A" answers { answer = "1.2.3.4" - + meta = { up = true subdivisions = jsonencode({ @@ -1397,7 +1397,7 @@ resource "ns1_monitoringjob" "test" { name = "terraform-test-%s" active = true regions = [ - "ams" + "nrt" ] job_type = "http" frequency = 60 diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index e1b59c65..5467d136 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -289,15 +289,26 @@ func RedirectCertCreate(d *schema.ResourceData, meta interface{}) error { // RedirectCertRead reads the redirect certificate from ns1 func RedirectCertRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*ns1.Client) + id := d.Get("id").(string) - cert, resp, err := client.RedirectCertificates.Get(d.Get("id").(string)) + cert, resp, err := client.RedirectCertificates.Get(id) + if err == nil && cert.Errors != nil && *cert.Errors == "Revoking" { + // wait for delete + for i := 0; i < 20; i++ { + // 20 x 500 milliseconds = max 10 seconds plus network delay + time.Sleep(500 * time.Millisecond) + _, _, err = client.RedirectCertificates.Get(id) + if err != nil { + break + } + } + } if err != nil { - if err == ns1.ErrRedirectNotFound { + if err == ns1.ErrRedirectCertificateNotFound { log.Printf("[DEBUG] NS1 redirect certificate (%s) not found", d.Id()) d.SetId("") return nil } - return ConvertToNs1Error(resp, err) } diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index f8bc1719..615cb832 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -116,6 +116,43 @@ func TestAccRedirectConfig_http_to_https(t *testing.T) { }) } +func TestAccRedirectConfig_remoteChanges(t *testing.T) { + var redirect redirect.Configuration + rString := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + domainName := fmt.Sprintf("terraform-test-%s.io", rString) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRedirectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRedirectBasic(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "masking"), + testAccCheckRedirectConfigTags(&redirect, []string{"test", "it"}), + testAccCheckRedirectConfigHTTPS(&redirect, true), + testAccCheckRedirectConfigCertIdPresent(&redirect, true), + ), + }, + { + PreConfig: eraseAll(t, domainName), + Config: testAccRedirectBasic(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "masking"), + testAccCheckRedirectConfigTags(&redirect, []string{"test", "it"}), + testAccCheckRedirectConfigHTTPS(&redirect, true), + testAccCheckRedirectConfigCertIdPresent(&redirect, true), + ), + }, + }, + }) +} + func testAccRedirectBasic(rString string) string { return fmt.Sprintf(` resource "ns1_redirect" "it" { @@ -131,7 +168,7 @@ resource "ns1_redirect" "it" { } resource "ns1_redirect_certificate" "example" { - domain = "*.${ns1_zone.test.zone}" + domain = "*.${ns1_zone.test.zone}" } resource "ns1_zone" "test" { @@ -155,7 +192,7 @@ resource "ns1_redirect" "it" { } resource "ns1_redirect_certificate" "example" { - domain = "*.${ns1_zone.test.zone}" + domain = "*.${ns1_zone.test.zone}" } resource "ns1_zone" "test" { @@ -189,7 +226,7 @@ resource "ns1_redirect" "it" { } resource "ns1_redirect_certificate" "example" { - domain = "*.${ns1_zone.test.zone}" + domain = "*.${ns1_zone.test.zone}" } resource "ns1_zone" "test" { @@ -318,3 +355,40 @@ func testAccCheckRedirectConfigTags(cfg *redirect.Configuration, expected []stri return nil } } + +func eraseAll(t *testing.T, domain string) func() { + client, err := sharedClient() + if err != nil { + t.Fatalf("failed to get shared client: %e", err) + } + return func() { + // delete all configs + redirects := client.Redirects + cfgs, _, err := redirects.List() + if err != nil { + t.Fatalf("failed to get list of redirects: %e", err) + } + for _, v := range cfgs { + if v.ID != nil && strings.HasSuffix(v.Domain, domain) { + _, err = redirects.Delete(*v.ID) + if err != nil { + t.Fatalf("failed to delete redirect %s: %e", v.Domain+"/"+v.Path, err) + } + } + } + // delete all certs + certificates := client.RedirectCertificates + certs, _, err := certificates.List() + if err != nil { + t.Fatalf("failed to get list of certificates: %e", err) + } + for _, v := range certs { + if v.ID != nil && strings.HasSuffix(v.Domain, domain) { + _, err = certificates.Delete(*v.ID) + if err != nil { + t.Fatalf("failed to delete cert %s: %e", v.Domain, err) + } + } + } + } +} From 5d76c91e5674ff9233f34349a5b63ebe561892a1 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Fri, 14 Jun 2024 15:37:14 +0100 Subject: [PATCH 13/19] Unit test fixes --- ns1/resource_redirect.go | 25 +++++++++--------- ns1/resource_redirect_test.go | 50 ++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index 5467d136..18af81d8 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -178,9 +178,12 @@ func RedirectConfigCreate(d *schema.ResourceData, meta interface{}) error { cert := getStringp(d, "certificate_id") if cert != nil { - r.CertificateID = cert - t := true - r.HttpsEnabled = &t + _, _, err := client.RedirectCertificates.Get(*cert) + if err == nil || err != ns1.ErrRedirectCertificateNotFound { + r.CertificateID = cert + t := true + r.HttpsEnabled = &t + } } else { f := false r.HttpsEnabled = &f @@ -248,6 +251,12 @@ func RedirectConfigUpdate(d *schema.ResourceData, meta interface{}) error { r.ID = &id cert := getStringp(d, "certificate_id") + if cert != nil { + _, _, err := client.RedirectCertificates.Get(*cert) + if err == ns1.ErrRedirectCertificateNotFound { + cert = nil + } + } if cert != nil { r.CertificateID = cert t := true @@ -269,16 +278,6 @@ func RedirectCertCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ns1.Client) cert, resp, err := client.RedirectCertificates.Create(d.Get("domain").(string)) - if err == nil && cert.ID != nil { - for i := 0; i < 20; i++ { - // 20 x 500 milliseconds = max 10 seconds plus network delay - time.Sleep(500 * time.Millisecond) - cert, _, err = client.RedirectCertificates.Get(*cert.ID) - if cert.Certificate != nil || cert.Errors != nil { - break - } - } - } if err != nil { return ConvertToNs1Error(resp, err) } diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index 615cb832..f65206e3 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -2,6 +2,7 @@ package ns1 import ( "fmt" + "log" "strings" "testing" @@ -18,7 +19,7 @@ func TestAccRedirectConfig_basic(t *testing.T) { domainName := fmt.Sprintf("terraform-test-%s.io", rString) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccRedirectPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckRedirectDestroy, Steps: []resource.TestStep{ @@ -64,7 +65,7 @@ func TestAccRedirectConfig_http_to_https(t *testing.T) { domainName := fmt.Sprintf("terraform-test-%s.io", rString) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccRedirectPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckRedirectDestroy, Steps: []resource.TestStep{ @@ -101,6 +102,31 @@ func TestAccRedirectConfig_http_to_https(t *testing.T) { testAccCheckRedirectConfigCertIdPresent(&redirect, true), ), }, + }, + }) +} + +func TestAccRedirectConfig_https_to_http(t *testing.T) { + var redirect redirect.Configuration + rString := acctest.RandStringFromCharSet(15, acctest.CharSetAlphaNum) + domainName := fmt.Sprintf("terraform-test-%s.io", rString) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t); testAccRedirectPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRedirectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRedirectBasic(rString), + Check: resource.ComposeTestCheckFunc( + testAccCheckRedirectConfigExists("ns1_redirect.it", &redirect), + testAccCheckRedirectConfigDomain(&redirect, "test."+domainName), + testAccCheckRedirectConfigFwType(&redirect, "masking"), + testAccCheckRedirectConfigTags(&redirect, []string{"test", "it"}), + testAccCheckRedirectConfigHTTPS(&redirect, true), + testAccCheckRedirectConfigCertIdPresent(&redirect, true), + ), + }, { Config: testAccRedirectHTTP(rString), Check: resource.ComposeTestCheckFunc( @@ -122,7 +148,7 @@ func TestAccRedirectConfig_remoteChanges(t *testing.T) { domainName := fmt.Sprintf("terraform-test-%s.io", rString) resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t); testAccRedirectPreCheck(t) }, Providers: testAccProviders, CheckDestroy: testAccCheckRedirectDestroy, Steps: []resource.TestStep{ @@ -207,7 +233,10 @@ resource "ns1_redirect" "it" { domain = "test.${ns1_zone.test.zone}" path = "/from/path/*" target = "https://url.com/target/path" + forwarding_mode = "all" + forwarding_type = "permanent" https_forced = false + tags = [ ] } resource "ns1_zone" "test" { @@ -223,6 +252,9 @@ resource "ns1_redirect" "it" { domain = "test.${ns1_zone.test.zone}" path = "/from/path/*" target = "https://url.com/target/path" + forwarding_mode = "all" + forwarding_type = "permanent" + tags = [ ] } resource "ns1_redirect_certificate" "example" { @@ -392,3 +424,15 @@ func eraseAll(t *testing.T, domain string) func() { } } } + +// See if we have redirect permissions by trying to list redirects +func testAccRedirectPreCheck(t *testing.T) { + client, err := sharedClient() + if err != nil { + log.Fatalf("failed to get shared client: %s", err) + } + _, _, err = client.Redirects.List() + if err != nil { + t.Skipf("account not authorized for redirects, skipping test") + } +} From 4f99db3e09e831d60ce13946d395325c74cb5f2a Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Fri, 14 Jun 2024 15:41:06 +0100 Subject: [PATCH 14/19] Move client to runtime --- ns1/resource_redirect_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index f65206e3..1ed4bf11 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -389,11 +389,11 @@ func testAccCheckRedirectConfigTags(cfg *redirect.Configuration, expected []stri } func eraseAll(t *testing.T, domain string) func() { - client, err := sharedClient() - if err != nil { - t.Fatalf("failed to get shared client: %e", err) - } return func() { + client, err := sharedClient() + if err != nil { + t.Fatalf("failed to get shared client: %e", err) + } // delete all configs redirects := client.Redirects cfgs, _, err := redirects.List() From 774f077a3d402e83e67084a3bc943aa741f480a3 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Fri, 14 Jun 2024 16:06:21 +0100 Subject: [PATCH 15/19] Aligning create just in case --- ns1/resource_redirect.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index 18af81d8..6799b5a6 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -179,11 +179,14 @@ func RedirectConfigCreate(d *schema.ResourceData, meta interface{}) error { cert := getStringp(d, "certificate_id") if cert != nil { _, _, err := client.RedirectCertificates.Get(*cert) - if err == nil || err != ns1.ErrRedirectCertificateNotFound { - r.CertificateID = cert - t := true - r.HttpsEnabled = &t + if err == ns1.ErrRedirectCertificateNotFound { + cert = nil } + } + if cert != nil { + r.CertificateID = cert + t := true + r.HttpsEnabled = &t } else { f := false r.HttpsEnabled = &f From 26dffd4df5a47538f7ecdab1adfc34789c6c968c Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Fri, 14 Jun 2024 16:07:59 +0100 Subject: [PATCH 16/19] Update release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d7541e3..a8395130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.3.0 (June 6, 2024) +## 2.3.0 (June 14, 2024) ENHANCEMENTS * Adds support for redirect endpoints BUGFIX From 3bb969dd8bae0e4a950ec7df81cded8d6bd9b5be Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Mon, 17 Jun 2024 11:48:12 +0100 Subject: [PATCH 17/19] Change tags into a set, make more fields in the cert read only --- CHANGELOG.md | 2 +- ns1/resource_redirect.go | 52 ++++++++++++++--------------------- ns1/resource_redirect_test.go | 3 ++ 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8395130..1f7160c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.3.0 (June 14, 2024) +## 2.3.0 (June 17, 2024) ENHANCEMENTS * Adds support for redirect endpoints BUGFIX diff --git a/ns1/resource_redirect.go b/ns1/resource_redirect.go index 6799b5a6..9f97a4f9 100644 --- a/ns1/resource_redirect.go +++ b/ns1/resource_redirect.go @@ -89,7 +89,7 @@ func redirectConfigResource() *schema.Resource { Default: false, }, "tags": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, @@ -120,7 +120,6 @@ func redirectCertificateResource() *schema.Resource { }, "certificate": { Type: schema.TypeString, - Optional: true, Computed: true, }, "valid_from": { @@ -133,22 +132,20 @@ func redirectCertificateResource() *schema.Resource { }, "errors": { Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "processing": { - Type: schema.TypeBool, - Optional: true, Computed: true, }, + // "processing": { + // Type: schema.TypeBool, + // Computed: true, + // }, "last_updated": { Type: schema.TypeInt, Computed: true, }, }, - Create: RedirectCertCreate, - Read: RedirectCertRead, - Update: RedirectCertUpdate, + Create: RedirectCertCreate, + Read: RedirectCertRead, + // Update: RedirectCertUpdate, Delete: RedirectCertDelete, Importer: &schema.ResourceImporter{}, } @@ -158,17 +155,11 @@ func redirectCertificateResource() *schema.Resource { func RedirectConfigCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ns1.Client) - var tags []string - terraformTags := d.Get("tags").([]interface{}) - for _, t := range terraformTags { - tags = append(tags, t.(string)) - } - r := redirect.NewConfiguration( d.Get("domain").(string), d.Get("path").(string), d.Get("target").(string), - tags, + []string{}, getFwModep(d, "forwarding_mode"), getFwTypep(d, "forwarding_type"), getBoolp(d, "https_enabled"), @@ -176,6 +167,10 @@ func RedirectConfigCreate(d *schema.ResourceData, meta interface{}) error { getBoolp(d, "query_forwarding"), ) + for _, t := range d.Get("tags").(*schema.Set).List() { + r.Tags = append(r.Tags, t.(string)) + } + cert := getStringp(d, "certificate_id") if cert != nil { _, _, err := client.RedirectCertificates.Get(*cert) @@ -230,20 +225,11 @@ func RedirectConfigDelete(d *schema.ResourceData, meta interface{}) error { func RedirectConfigUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*ns1.Client) - var tags []string - terraformTags := d.Get("tags").([]interface{}) - if terraformTags != nil { - tags = []string{} - } - for _, t := range terraformTags { - tags = append(tags, t.(string)) - } - r := redirect.NewConfiguration( d.Get("domain").(string), d.Get("path").(string), d.Get("target").(string), - tags, + []string{}, getFwModep(d, "forwarding_mode"), getFwTypep(d, "forwarding_type"), getBoolp(d, "https_enabled"), @@ -253,6 +239,10 @@ func RedirectConfigUpdate(d *schema.ResourceData, meta interface{}) error { id := d.Id() r.ID = &id + for _, t := range d.Get("tags").(*schema.Set).List() { + r.Tags = append(r.Tags, t.(string)) + } + cert := getStringp(d, "certificate_id") if cert != nil { _, _, err := client.RedirectCertificates.Get(*cert) @@ -488,9 +478,9 @@ func redirectCertToResourceData(d *schema.ResourceData, r *redirect.Certificate) if r.Errors != nil { d.Set("errors", *r.Errors) } - if r.Processing != nil { - d.Set("processing", *r.Processing) - } + // if r.Processing != nil { + // d.Set("processing", *r.Processing) + // } if r.LastUpdated != nil { d.Set("last_updated", *r.LastUpdated) } diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index 1ed4bf11..b8d28740 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -3,6 +3,7 @@ package ns1 import ( "fmt" "log" + "slices" "strings" "testing" @@ -375,6 +376,8 @@ func testAccCheckRedirectConfigTags(cfg *redirect.Configuration, expected []stri if len(cfg.Tags) != len(expected) { diff = true } else { + slices.Sort(expected) + slices.Sort(cfg.Tags) for i := range expected { if cfg.Tags[i] != expected[i] { diff = true From c17f502008e71f9e2f4c399c86e8d74895e29f76 Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Mon, 17 Jun 2024 12:02:39 +0100 Subject: [PATCH 18/19] Remove slices package that isn't supported on older go --- ns1/resource_redirect_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ns1/resource_redirect_test.go b/ns1/resource_redirect_test.go index b8d28740..0ebb0955 100644 --- a/ns1/resource_redirect_test.go +++ b/ns1/resource_redirect_test.go @@ -3,7 +3,7 @@ package ns1 import ( "fmt" "log" - "slices" + "sort" "strings" "testing" @@ -376,8 +376,8 @@ func testAccCheckRedirectConfigTags(cfg *redirect.Configuration, expected []stri if len(cfg.Tags) != len(expected) { diff = true } else { - slices.Sort(expected) - slices.Sort(cfg.Tags) + sort.Strings(expected) + sort.Strings(cfg.Tags) for i := range expected { if cfg.Tags[i] != expected[i] { diff = true From 1ae5400efb5277bde25e23524086939d9ed52a9d Mon Sep 17 00:00:00 2001 From: Ferdinando Formica Date: Tue, 18 Jun 2024 10:11:49 +0100 Subject: [PATCH 19/19] Remove processing flag from docs as we're no longer exposing it on the cert --- website/docs/r/redirect.html.markdown | 1 - 1 file changed, 1 deletion(-) diff --git a/website/docs/r/redirect.html.markdown b/website/docs/r/redirect.html.markdown index 139c4cb4..838f694d 100644 --- a/website/docs/r/redirect.html.markdown +++ b/website/docs/r/redirect.html.markdown @@ -79,7 +79,6 @@ The following arguments are supported: * `certificate` - (Read Only) The certificate value. * `valid_from` - (Read Only) The Unix timestamp representing when the certificate first started being valid. * `valid_until` - (Read Only) The Unix timestamp representing when the certificate will stop being valid. -* `processing` - (Read Only) Whether the certificate is active. * `errors` - (Read Only) Any error encountered when applying the certificate. * `last_updated` - (Read Only) The Unix timestamp representing when the certificate was last signed.