From 866f9171c8ecb704f2312131fc96b31b63dd0c2f Mon Sep 17 00:00:00 2001 From: Philippe Vienne Date: Wed, 26 Jun 2024 15:46:27 +0200 Subject: [PATCH 1/2] feat: implement name server resource for domains Using the /nameServers/update endpoint, we allow to update the NS of a domain registered at OVHCloud Note that this resource require a new testing resource of a Domain where you can change the NS on it. --- README.md | 1 + ovh/provider.go | 1 + ovh/provider_test.go | 1 + ovh/resource_domain_name_servers.go | 220 +++++++++++++++++++++++ ovh/resource_domain_name_servers_test.go | 84 +++++++++ 5 files changed, 307 insertions(+) create mode 100644 ovh/resource_domain_name_servers.go create mode 100644 ovh/resource_domain_name_servers_test.go diff --git a/README.md b/README.md index 542a52e00..7a24190fe 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ export OVH_CLOUD_PROJECT_FAILOVER_IP_ROUTED_TO_1_TEST="..." export OVH_CLOUD_PROJECT_FAILOVER_IP_ROUTED_TO_2_TEST="..." export OVH_VRACK_SERVICE_TEST="..." export OVH_ZONE_TEST="..." +export OVH_DOMAIN_TEST="..." $ make testacc ``` diff --git a/ovh/provider.go b/ovh/provider.go index 728b1043d..5c058e9f9 100644 --- a/ovh/provider.go +++ b/ovh/provider.go @@ -226,6 +226,7 @@ func Provider() *schema.Provider { "ovh_dedicated_server_reboot_task": resourceDedicatedServerRebootTask(), "ovh_dedicated_server_update": resourceDedicatedServerUpdate(), "ovh_dedicated_server_networking": resourceDedicatedServerNetworking(), + "ovh_domain_name_servers": resourceOvhDomainNameServers(), "ovh_domain_zone": resourceDomainZone(), "ovh_domain_zone_record": resourceOvhDomainZoneRecord(), "ovh_domain_zone_redirection": resourceOvhDomainZoneRedirection(), diff --git a/ovh/provider_test.go b/ovh/provider_test.go index 9fc6daf4f..0cc23998f 100644 --- a/ovh/provider_test.go +++ b/ovh/provider_test.go @@ -138,6 +138,7 @@ func testAccPreCheckOrderCloudProject(t *testing.T) { func testAccPreCheckDomain(t *testing.T) { testAccPreCheckCredentials(t) checkEnvOrSkip(t, "OVH_ZONE_TEST") + checkEnvOrSkip(t, "OVH_DOMAIN_TEST") } // Checks that the environment variables needed to order /domain for acceptance tests diff --git a/ovh/resource_domain_name_servers.go b/ovh/resource_domain_name_servers.go new file mode 100644 index 000000000..a2ba62e7e --- /dev/null +++ b/ovh/resource_domain_name_servers.go @@ -0,0 +1,220 @@ +package ovh + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/ovh/go-ovh/ovh" + "log" + "strings" +) + +type OvhDomainNameServerUpdate struct { + NameServers []OvhDomainNameServer `json:"nameServers,omitempty"` +} + +type OvhDomainNameServerUpdateResult struct { + CanAccelerate bool `json:"canAccelerate,omitempty"` + CanCancel bool `json:"canCancel,omitempty"` + CanRelaunch bool `json:"canRelaunch,omitempty"` + Comment string `json:"comment,omitempty"` + CreationDate string `json:"creationDate,omitempty"` + Domain string `json:"domain,omitempty"` + DoneDate string `json:"doneDate,omitempty"` + Function string `json:"function,omitempty"` + Id int64 `json:"id,omitempty"` + LastUpdate string `json:"lastUpdate,omitempty"` + Status string `json:"status,omitempty"` + TodoDate string `json:"todoDate,omitempty"` +} + +type OvhDomainNameServer struct { + Id int64 `json:"id,omitempty"` + Host string `json:"host,omitempty"` + Ip string `json:"ip,omitempty"` + IsUsed bool `json:"isUsed,omitempty"` + ToDelete bool `json:"toDelete,omitempty"` +} + +type OvhDomainNameServers struct { + Id string `json:"id,omitempty"` + Domain string `json:"domain,omitempty"` + Servers []OvhDomainNameServer `json:"servers,omitempty"` +} + +func (r *OvhDomainNameServers) String() string { + domains := make([]string, 0) + for _, server := range r.Servers { + domains = append(domains, fmt.Sprintf( + "server[id: %v, host: %s, ip: %s, isUsed: %v, toDelete: %v]", + server.Id, + server.Host, + server.Ip, + server.IsUsed, + server.ToDelete, + )) + } + return fmt.Sprintf( + "nameservers[id: %v, domain: %s, servers: [%s]]", + r.Id, + r.Domain, + strings.Join(domains, ", "), + ) +} + +func resourceOvhDomainNameServersImportState( + d *schema.ResourceData, + meta interface{}) ([]*schema.ResourceData, error) { + givenId := d.Id() + d.SetId(givenId) + d.Set("domain", d.Id()) + results := make([]*schema.ResourceData, 1) + results[0] = d + return results, nil +} + +func resourceOvhDomainNameServers() *schema.Resource { + return &schema.Resource{ + Create: resourceOvhDomainNameServersCreate, + Read: resourceOvhDomainNameServersRead, + Update: resourceOvhDomainNameServersCreate, // Update is the same as create + Delete: resourceOvhDomainNameServersDelete, + Importer: &schema.ResourceImporter{ + State: resourceOvhDomainNameServersImportState, + }, + + Schema: map[string]*schema.Schema{ + "domain": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "servers": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "host": { + Type: schema.TypeString, + Required: true, + }, + "ip": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, + }, + }, + MinItems: 1, + }, + }, + } +} + +func resourceOvhDomainNameServersCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + serviceName := d.Get("domain").(string) + + // Servers to put + servers := &OvhDomainNameServerUpdate{ + NameServers: make([]OvhDomainNameServer, 0), + } + // Loop due to rename of the field in the API + for _, server := range d.Get("servers").([]interface{}) { + s := server.(map[string]interface{}) + servers.NameServers = append(servers.NameServers, OvhDomainNameServer{ + Host: s["host"].(string), + Ip: s["ip"].(string), + }) + } + log.Printf("[DEBUG] OVH Record create configuration: %#v", servers) + err := config.OVHClient.Post(fmt.Sprintf("/domain/%s/nameServers/update", serviceName), servers, nil) + + if err != nil { + return fmt.Errorf("failed to register OVH Nameservers: %s", err) + } + + d.SetId(serviceName) + + return resourceOvhDomainNameServersRead(d, meta) +} + +func resourceOvhDomainNameServersRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + record, err := ovhDomainNameServers(config.OVHClient, d) + + if err != nil { + return err + } + + if record == nil { + return fmt.Errorf("domain %v has been deleted", d.Id()) + } + + d.SetId(record.Id) + d.Set("domain", record.Domain) + d.Set("servers", flattenOvhDomainNameServers(record.Servers)) + + return nil +} + +func flattenOvhDomainNameServers(servers []OvhDomainNameServer) []interface{} { + result := make([]interface{}, 0) + for _, server := range servers { + result = append(result, map[string]interface{}{ + "host": server.Host, + "ip": server.Ip, + }) + } + return result +} + +func resourceOvhDomainNameServersDelete(d *schema.ResourceData, meta interface{}) error { + // Note that NameServers can not be deleted, only updated + d.SetId("") + return nil +} + +func ovhDomainNameServers(client *ovh.Client, d *schema.ResourceData) (*OvhDomainNameServers, error) { + domain := d.Get("domain").(string) + nameServers := &OvhDomainNameServers{ + Servers: make([]OvhDomainNameServer, 0), + Domain: domain, + Id: domain, + } + rec := &([]int{}) + + endpoint := fmt.Sprintf("/domain/%s/nameServer", domain) + + err := client.Get(endpoint, rec) + + if err != nil { + if err.(*ovh.APIError).Code == 404 { + return nil, nil + } + return nil, err + } + + // Read each name server + for _, id := range *rec { + server, err := ovhDomainNameServer(client, domain, id) + if err != nil { + return nil, err + } + nameServers.Servers = append(nameServers.Servers, *server) + } + + return nameServers, nil +} + +func ovhDomainNameServer(client *ovh.Client, domain string, id int) (*OvhDomainNameServer, error) { + rec := &OvhDomainNameServer{} + + endpoint := fmt.Sprintf("/domain/%s/nameServer/%d", domain, id) + err := client.Get(endpoint, rec) + if err != nil { + return nil, err + } + + return rec, nil +} diff --git a/ovh/resource_domain_name_servers_test.go b/ovh/resource_domain_name_servers_test.go new file mode 100644 index 000000000..d2237faf4 --- /dev/null +++ b/ovh/resource_domain_name_servers_test.go @@ -0,0 +1,84 @@ +package ovh + +import ( + "fmt" + "github.com/ovh/go-ovh/ovh" + "log" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func init() { + resource.AddTestSweepers("ovh_domain_name_servers", &resource.Sweeper{ + Name: "ovh_domain_name_servers", + F: testSweepOvhDomainNameServers, + }) +} + +func testSweepOvhDomainNameServers(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + + domain := os.Getenv("OVH_DOMAIN_TEST") + if domain == "" { + log.Print("[DEBUG] OVH_DOMAIN_TEST is not set. No domain to sweep") + return nil + } + + // Check we have access to the domain + err = client.Get(fmt.Sprintf("/domain/%s", domain), nil) + + if err != nil { + if err.(*ovh.APIError).Code == 404 { + log.Printf("[DEBUG] OVH domain %s does not exist. No domain to sweep", domain) + return nil + } + return fmt.Errorf("error getting domain: %s", err) + } + + return nil +} + +func TestAccDomainNameServers_Basic(t *testing.T) { + domain := os.Getenv("OVH_DOMAIN_TEST") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckDomain(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // provider shall send an error if the TTL is less than 60 + { + Config: testAccCheckOvhDomainNameServersConfig(domain, []string{"dns104.ovh.net", "ns104.ovh.net"}), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "ovh_domain_name_servers.foobar", "domain", domain), + ), + }, + }, + }) +} + +func testAccCheckOvhDomainNameServersConfig(domain string, nameServers []string) string { + return ` +resource "ovh_domain_name_servers" "foobar" { + domain = "` + domain + `" + ` + testAccCheckOvhDomainNameServersConfigNameServers(nameServers) + ` +} +` +} + +func testAccCheckOvhDomainNameServersConfigNameServers(nameServers []string) string { + var config string + for _, nameServer := range nameServers { + config += ` + servers { + host = "` + nameServer + `" + } + ` + } + return config +} From a8f17b48b244562d78754d1fcc437137c815a1b1 Mon Sep 17 00:00:00 2001 From: Philippe Vienne Date: Fri, 28 Jun 2024 13:52:22 +0200 Subject: [PATCH 2/2] fix: update path escape for service name Co-authored-by: Romain Beuque <556072+rbeuque74@users.noreply.github.com> --- ovh/resource_domain_name_servers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ovh/resource_domain_name_servers.go b/ovh/resource_domain_name_servers.go index a2ba62e7e..1c947a2b3 100644 --- a/ovh/resource_domain_name_servers.go +++ b/ovh/resource_domain_name_servers.go @@ -127,7 +127,7 @@ func resourceOvhDomainNameServersCreate(d *schema.ResourceData, meta interface{} }) } log.Printf("[DEBUG] OVH Record create configuration: %#v", servers) - err := config.OVHClient.Post(fmt.Sprintf("/domain/%s/nameServers/update", serviceName), servers, nil) + err := config.OVHClient.Post(fmt.Sprintf("/domain/%s/nameServers/update", url.PathEscape(serviceName)), servers, nil) if err != nil { return fmt.Errorf("failed to register OVH Nameservers: %s", err)