diff --git a/internal/api/primary_server.go b/internal/api/primary_server.go index 2f74c55..6a35972 100644 --- a/internal/api/primary_server.go +++ b/internal/api/primary_server.go @@ -35,7 +35,7 @@ func (c *Client) GetPrimaryServer(ctx context.Context, id string) (*PrimaryServe switch resp.StatusCode { case http.StatusNotFound: - return nil, nil + return nil, fmt.Errorf("primary server %s not found", id) case http.StatusOK: var response *PrimaryServerResponse @@ -50,6 +50,27 @@ func (c *Client) GetPrimaryServer(ctx context.Context, id string) (*PrimaryServe } } +func (c *Client) GetPrimaryServers(ctx context.Context, zoneID string) ([]PrimaryServer, error) { + resp, err := c.request(ctx, http.MethodGet, "/api/v1/primary_servers?zone_id="+zoneID, nil) + if err != nil { + return nil, fmt.Errorf("error getting primary servers for zone %s: %w", zoneID, err) + } + + switch resp.StatusCode { + case http.StatusOK: + var response PrimaryServersResponse + + err = readAndParseJSONBody(resp, &response) + if err != nil { + return nil, err + } + + return response.PrimaryServers, nil + default: + return nil, fmt.Errorf("http status %d unhandled", resp.StatusCode) + } +} + func (c *Client) CreatePrimaryServer(ctx context.Context, server CreatePrimaryServerRequest) (*PrimaryServer, error) { resp, err := c.request(ctx, http.MethodPost, "/api/v1/primary_servers", server) if err != nil { diff --git a/internal/provider/primary_server_resource.go b/internal/provider/primary_server_resource.go index 5d0c199..20f04dd 100644 --- a/internal/provider/primary_server_resource.go +++ b/internal/provider/primary_server_resource.go @@ -228,14 +228,14 @@ func (r *primaryServerResource) Read(ctx context.Context, req resource.ReadReque return nil }) - if err != nil { + if err != nil && fmt.Sprint(err) != fmt.Sprintf("primary server %s not found", state.ID.ValueString()) { resp.Diagnostics.AddError("API Error", fmt.Sprintf("read primary server: %s", err)) return } if server == nil { - resp.Diagnostics.AddWarning("Resource Not Found", fmt.Sprintf("Primary server with id %s doesn't exist, removing it from state", state.ID)) + resp.State.RemoveResource(ctx) return } diff --git a/internal/provider/primary_server_resource_test.go b/internal/provider/primary_server_resource_test.go index 53a9544..6e5f410 100644 --- a/internal/provider/primary_server_resource_test.go +++ b/internal/provider/primary_server_resource_test.go @@ -1,12 +1,17 @@ package provider import ( + "context" "fmt" + "net/http" "regexp" "strconv" "strings" "testing" + "github.com/germanbrew/terraform-provider-hetznerdns/internal/api" + "github.com/germanbrew/terraform-provider-hetznerdns/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -118,6 +123,97 @@ func TestAccPrimaryServer_TwoPrimaryServersResources(t *testing.T) { }) } +func TestAccPrimaryServer_StalePrimaryServersResources(t *testing.T) { + aZoneName := acctest.RandString(10) + ".online" + aZoneTTL := 3600 + + psAddress := "1.1.0.0" + psPort := 53 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: strings.Join( + []string{ + testAccZoneResourceConfig("test", aZoneName, aZoneTTL), + testAccPrimaryServerResourceConfigCreate("test", psAddress, psPort), + }, "\n", + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("hetznerdns_primary_server.test", "id"), + resource.TestCheckResourceAttr("hetznerdns_primary_server.test", "address", psAddress), + resource.TestCheckResourceAttr("hetznerdns_primary_server.test", "port", strconv.Itoa(psPort)), + ), + }, + // ImportState testing + { + ResourceName: "hetznerdns_primary_server.test", + ImportState: true, + ImportStateVerify: true, + }, + // Update and Read testing + { + Config: strings.Join( + []string{ + testAccZoneResourceConfig("test", aZoneName, aZoneTTL), + testAccPrimaryServerResourceConfigCreate("test", psAddress, psPort*2), + }, "\n", + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("hetznerdns_primary_server.test", "port", strconv.Itoa(psPort*2)), + ), + }, + // Remove primary server from Hetzner DNS and check if it will be recreated by Terraform + { + PreConfig: func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var ( + data hetznerDNSProviderModel + apiToken string + apiClient *api.Client + err error + ) + + apiToken = utils.ConfigureStringAttribute(data.ApiToken, "HETZNER_DNS_API_TOKEN", "") + httpClient := logging.NewLoggingHTTPTransport(http.DefaultTransport) + apiClient, err = api.New("https://dns.hetzner.com", apiToken, httpClient) + if err != nil { + t.Fatalf("Error while creating API apiClient: %s", err) + } + zone, err := apiClient.GetZoneByName(ctx, aZoneName) + if err != nil { + t.Fatalf("Error while fetching zone: %s", err) + } else if zone == nil { + t.Fatalf("Zone %s not found", aZoneName) + } + + primaryServer, err := apiClient.GetPrimaryServers(ctx, zone.ID) + if err != nil { + t.Fatalf("Error while fetching primary server: %s", err) + } else if primaryServer == nil { + t.Fatalf("Primary server %s not found", zone.ID) + } + + err = apiClient.DeletePrimaryServer(ctx, primaryServer[0].ID) + if err != nil { + t.Fatalf("Error while deleting primary server: %s", err) + } + }, + // Check if the record is recreated + // ExpectNonEmptyPlan: true, + RefreshState: true, + ExpectError: regexp.MustCompile("hetznerdns_primary_server.test will be created"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + func testAccPrimaryServerResourceConfigCreate(resourceName, psAddress string, psPort int) string { return fmt.Sprintf(` resource "hetznerdns_primary_server" "%s" { diff --git a/internal/provider/record_resource.go b/internal/provider/record_resource.go index 5c09b53..8842ce1 100644 --- a/internal/provider/record_resource.go +++ b/internal/provider/record_resource.go @@ -265,7 +265,7 @@ func (r *recordResource) Read(ctx context.Context, req resource.ReadRequest, res } if record == nil { - resp.Diagnostics.AddWarning("Resource Not Found", fmt.Sprintf("DNS record with id %s doesn't exist, removing it from state", state.ID)) + resp.State.RemoveResource(ctx) return } diff --git a/internal/provider/record_resource_test.go b/internal/provider/record_resource_test.go index cc6272e..d146b8e 100644 --- a/internal/provider/record_resource_test.go +++ b/internal/provider/record_resource_test.go @@ -1,12 +1,17 @@ package provider import ( + "context" "fmt" + "net/http" "regexp" "strconv" "strings" "testing" + "github.com/germanbrew/terraform-provider-hetznerdns/internal/api" + "github.com/germanbrew/terraform-provider-hetznerdns/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -234,6 +239,101 @@ func TestAccRecord_ResourcesDKIM(t *testing.T) { }) } +func TestAccRecord_StaleResources(t *testing.T) { + zoneName := acctest.RandString(10) + ".online" + aZoneTTL := 60 + + value := "192.168.1.1" + aName := acctest.RandString(10) + aType := "A" + ttl := aZoneTTL * 2 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: strings.Join( + []string{ + testAccZoneResourceConfig("test", zoneName, aZoneTTL), + testAccRecordResourceConfigWithTTL("record1", aName, aType, value, ttl), + }, "\n", + ), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet( + "hetznerdns_record.record1", "id"), + resource.TestCheckResourceAttr( + "hetznerdns_record.record1", "type", aType), + resource.TestCheckResourceAttr( + "hetznerdns_record.record1", "name", aName), + resource.TestCheckResourceAttr( + "hetznerdns_record.record1", "value", value), + resource.TestCheckResourceAttr( + "hetznerdns_record.record1", "ttl", strconv.Itoa(ttl)), + ), + }, + // ImportState testing + { + ResourceName: "hetznerdns_record.record1", + ImportState: true, + ImportStateVerify: true, + }, + // Update and Read testing + { + Config: strings.Join( + []string{ + testAccZoneResourceConfig("test", zoneName, aZoneTTL), + testAccRecordResourceConfigWithTTL("record1", aName, aType, value, ttl*2), + }, "\n", + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "hetznerdns_record.record1", "ttl", strconv.Itoa(ttl*2)), + ), + }, + // Remove record from Hetzner DNS and check if it will be recreated by Terraform + { + PreConfig: func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var ( + data hetznerDNSProviderModel + apiToken string + apiClient *api.Client + err error + ) + + apiToken = utils.ConfigureStringAttribute(data.ApiToken, "HETZNER_DNS_API_TOKEN", "") + httpClient := logging.NewLoggingHTTPTransport(http.DefaultTransport) + apiClient, err = api.New("https://dns.hetzner.com", apiToken, httpClient) + if err != nil { + t.Fatalf("Error while creating API apiClient: %s", err) + } + zone, err := apiClient.GetZoneByName(ctx, zoneName) + if err != nil { + t.Fatalf("Error while fetching zone: %s", err) + } + record, err := apiClient.GetRecordByName(ctx, zone.ID, aName) + if err != nil { + t.Fatalf("Error while fetching record: %s", err) + } + err = apiClient.DeleteRecord(ctx, record.ID) + if err != nil { + t.Fatalf("Error while deleting record: %s", err) + } + }, + // Check if the record is recreated + // ExpectNonEmptyPlan: true, + RefreshState: true, + ExpectError: regexp.MustCompile("hetznerdns_record.record1 will be created"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + func testAccRecordResourceConfigWithTTL(resourceName, name, recordType, value string, ttl int) string { return fmt.Sprintf(` resource "hetznerdns_record" "%s" { diff --git a/internal/provider/zone_resource.go b/internal/provider/zone_resource.go index cd0f63a..f2bff0a 100644 --- a/internal/provider/zone_resource.go +++ b/internal/provider/zone_resource.go @@ -260,7 +260,7 @@ func (r *zoneResource) Read(ctx context.Context, req resource.ReadRequest, resp } if zone == nil { - resp.Diagnostics.AddWarning("Resource Not Found", fmt.Sprintf("DNS zone with id %s doesn't exist, removing it from state", state.ID)) + resp.State.RemoveResource(ctx) return } diff --git a/internal/provider/zone_resource_test.go b/internal/provider/zone_resource_test.go index c0d950b..9522367 100644 --- a/internal/provider/zone_resource_test.go +++ b/internal/provider/zone_resource_test.go @@ -1,11 +1,16 @@ package provider import ( + "context" "fmt" + "net/http" "regexp" "strconv" "testing" + "github.com/germanbrew/terraform-provider-hetznerdns/internal/api" + "github.com/germanbrew/terraform-provider-hetznerdns/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -117,6 +122,68 @@ func TestAccZone_ZoneExists(t *testing.T) { }) } +func TestAccZone_StaleZone(t *testing.T) { + aZoneName := acctest.RandString(10) + ".online" + aZoneTTL := 60 + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccZoneResourceConfig("test", aZoneName, aZoneTTL), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("hetznerdns_zone.test", "id"), + resource.TestCheckResourceAttr("hetznerdns_zone.test", "name", aZoneName), + resource.TestCheckResourceAttr("hetznerdns_zone.test", "ttl", strconv.Itoa(aZoneTTL)), + resource.TestCheckResourceAttrSet("hetznerdns_zone.test", "ns.#"), + ), + }, + // ImportState testing + { + ResourceName: "hetznerdns_zone.test", + ImportState: true, + ImportStateVerify: true, + }, + // Remove zone from Hetzner DNS and check if it will be recreated by Terraform + { + PreConfig: func() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var ( + data hetznerDNSProviderModel + apiToken string + apiClient *api.Client + err error + ) + + apiToken = utils.ConfigureStringAttribute(data.ApiToken, "HETZNER_DNS_API_TOKEN", "") + httpClient := logging.NewLoggingHTTPTransport(http.DefaultTransport) + apiClient, err = api.New("https://dns.hetzner.com", apiToken, httpClient) + if err != nil { + t.Fatalf("Error while creating API apiClient: %s", err) + } + zone, err := apiClient.GetZoneByName(ctx, aZoneName) + if err != nil { + t.Fatalf("Error while fetching zone: %s", err) + } + err = apiClient.DeleteZone(ctx, zone.ID) + if err != nil { + t.Fatalf("Error while deleting zone: %s", err) + } + }, + // Check if the zone is recreated + // ExpectNonEmptyPlan: true, + RefreshState: true, + ExpectError: regexp.MustCompile("hetznerdns_zone.test will be created"), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + func testAccZoneResourceConfig(resourceName string, name string, ttl int) string { return fmt.Sprintf(` resource "hetznerdns_zone" "%[1]s" {