diff --git a/.gitignore b/.gitignore index fa8bc858..eb807cd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -terraform-provider-dns +terraform-provider-powerdns *.dll *.exe diff --git a/docker-compose.yml b/docker-compose.yml index 7040d96b..79c23758 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3" +version: "3.7" services: nginx: image: nginx:1.17.2 diff --git a/powerdns/client.go b/powerdns/client.go index db3b16f5..ea6261f2 100644 --- a/powerdns/client.go +++ b/powerdns/client.go @@ -204,6 +204,12 @@ type ResourceRecordSet struct { Records []Record `json:"records,omitempty"` } +// ResourceZoneMetadata represents a PowerDNS Zone Metadata object +type ResourceZoneMetadata struct { + Kind string `json:"kind"` + Metadata []string `json:"metadata"` +} + type zonePatchRequest struct { RecordSets []ResourceRecordSet `json:"rrsets"` } @@ -234,6 +240,11 @@ func (rrSet *ResourceRecordSet) ID() string { return rrSet.Name + idSeparator + rrSet.Type } +// ID returns a zoneMetadata with the ID format +func (metadata *ResourceZoneMetadata) ID(zone string) string { + return zone + idSeparator + metadata.Kind +} + // Returns name and type of record or record set based on its ID func parseID(recID string) (string, string, error) { s := strings.Split(recID, idSeparator) @@ -269,7 +280,7 @@ func (client *Client) detectAPIVersion() (int, error) { } defer resp.Body.Close() - if resp.StatusCode == 200 { + if resp.StatusCode == http.StatusOK { return 1, nil } return 0, nil @@ -429,7 +440,7 @@ func (client *Client) DeleteZone(name string) error { } defer resp.Body.Close() - if resp.StatusCode != 204 { + if resp.StatusCode != http.StatusNoContent { errorResp := new(errorResponse) if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { return fmt.Errorf("Error deleting zone: %s", name) @@ -582,7 +593,7 @@ func (client *Client) ReplaceRecordSet(zone string, rrSet ResourceRecordSet) (st } defer resp.Body.Close() - if resp.StatusCode != 200 && resp.StatusCode != 204 { + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { errorResp := new(errorResponse) if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { return "", fmt.Errorf("Error creating record set: %s", rrSet.ID()) @@ -615,7 +626,7 @@ func (client *Client) DeleteRecordSet(zone string, name string, tpe string) erro } defer resp.Body.Close() - if resp.StatusCode != 200 && resp.StatusCode != 204 { + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { errorResp := new(errorResponse) if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { return fmt.Errorf("Error deleting record: %s %s", name, tpe) @@ -634,6 +645,136 @@ func (client *Client) DeleteRecordSetByID(zone string, recID string) error { return client.DeleteRecordSet(zone, name, tpe) } +// GetZoneMetadata get metadata from zone by its ID +func (client *Client) GetZoneMetadata(id string) (ResourceZoneMetadata, error) { + zone, kind, err := parseID(id) + if err != nil { + return ResourceZoneMetadata{}, err + } + + req, err := client.newRequest("GET", fmt.Sprintf("/servers/localhost/zones/%s/metadata/%s", zone, kind), nil) + if err != nil { + return ResourceZoneMetadata{}, err + } + + resp, err := client.HTTP.Do(req) + if err != nil { + return ResourceZoneMetadata{}, err + } + defer resp.Body.Close() + + var zoneMetadata ResourceZoneMetadata + err = json.NewDecoder(resp.Body).Decode(&zoneMetadata) + if err != nil { + return ResourceZoneMetadata{}, err + } + + return zoneMetadata, nil +} + +// UpdateZoneMetadata creates new record set in Zone +func (client *Client) UpdateZoneMetadata(zone string, zoneMetadata ResourceZoneMetadata) (string, error) { + body, err := json.Marshal(zoneMetadata) + if err != nil { + return "", err + } + + req, err := client.newRequest("PUT", fmt.Sprintf("/servers/localhost/zones/%s/metadata/%s", zone, zoneMetadata.Kind), body) + if err != nil { + return "", err + } + + resp, err := client.HTTP.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + errorResp := new(errorResponse) + if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { + return "", fmt.Errorf("Error updating zone metadata: %s", zoneMetadata.Kind) + } + return "", fmt.Errorf("Error updating zone metadata: %s, reason: %q", zoneMetadata.Kind, errorResp.ErrorMsg) + } + + var createdZoneMetadata ResourceZoneMetadata + err = json.NewDecoder(resp.Body).Decode(&createdZoneMetadata) + if err != nil { + return "", err + } + + return createdZoneMetadata.ID(zone), nil +} + +// DeleteZoneMetadata deletes zone metadata by its ID +func (client *Client) DeleteZoneMetadata(id string) error { + zone, kind, err := parseID(id) + if err != nil { + return err + } + + req, err := client.newRequest("DELETE", fmt.Sprintf("/servers/localhost/zones/%s/metadata/%s", zone, kind), nil) + if err != nil { + return err + } + + resp, err := client.HTTP.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + errorResp := new(errorResponse) + if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { + return fmt.Errorf("Error deleting Zone Metadata: %s", kind) + } + return fmt.Errorf("Error deleting Zone Metadata: %s, reason: %q", kind, errorResp.ErrorMsg) + } + return nil +} + +// ZoneMetadataExists checks if requested zone exists +func (client *Client) ZoneMetadataExists(id string) (bool, error) { + zone, kind, err := parseID(id) + if err != nil { + return false, err + } + + req, err := client.newRequest("GET", fmt.Sprintf("/servers/localhost/zones/%s/metadata/%s", zone, kind), nil) + if err != nil { + return false, err + } + + resp, err := client.HTTP.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound { + errorResp := new(errorResponse) + if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { + return false, fmt.Errorf("Error getting Zone Metadata: %s", kind) + } + return false, fmt.Errorf("Error getting Zone Metadata: %s, reason: %q", kind, errorResp.ErrorMsg) + } + + var ZoneMetadata ResourceZoneMetadata + err = json.NewDecoder(resp.Body).Decode(&ZoneMetadata) + + if err != nil { + return false, err + } + + if len(ZoneMetadata.Metadata) == 0 { + return false, err + } + + return resp.StatusCode == http.StatusOK, nil +} + func (client *Client) setServerVersion() error { req, err := client.newRequest("GET", "/servers/localhost", nil) if err != nil { @@ -645,7 +786,7 @@ func (client *Client) setServerVersion() error { return err } defer resp.Body.Close() - if resp.StatusCode != 200 { + if resp.StatusCode != http.StatusOK { return fmt.Errorf("Invalid response code from server: '%d'. Response body: %v", resp.StatusCode, resp.Body) } diff --git a/powerdns/provider.go b/powerdns/provider.go index d8d20402..53f29d7b 100644 --- a/powerdns/provider.go +++ b/powerdns/provider.go @@ -54,8 +54,9 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "powerdns_zone": resourcePDNSZone(), - "powerdns_record": resourcePDNSRecord(), + "powerdns_zone": resourcePDNSZone(), + "powerdns_zone_metadata": resourcePDNSZoneMetadata(), + "powerdns_record": resourcePDNSRecord(), }, ConfigureFunc: providerConfigure, diff --git a/powerdns/resource_powerdns_zone_metadata.go b/powerdns/resource_powerdns_zone_metadata.go new file mode 100644 index 00000000..da01c8c7 --- /dev/null +++ b/powerdns/resource_powerdns_zone_metadata.go @@ -0,0 +1,128 @@ +package powerdns + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +func resourcePDNSZoneMetadata() *schema.Resource { + return &schema.Resource{ + Create: resourcePDNSZoneMetadataCreate, + Read: resourcePDNSZoneMetadataRead, + Delete: resourcePDNSZoneMetadataDelete, + Exists: resourcePDNSZoneMetadataExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "zone": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "kind": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "metadata": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourcePDNSZoneMetadataCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + + zone := d.Get("zone").(string) + mtdata := d.Get("metadata").(*schema.Set).List() + + for _, mt := range mtdata { + if len(strings.TrimSpace(mt.(string))) == 0 { + log.Printf("[WARN] One or more values in 'metadata' contain empty value(s)") + break + } + } + if !(len(mtdata) > 0) { + return fmt.Errorf("'metadata' must not be empty") + } + + metadata := make([]string, 0, len(mtdata)) + for _, mt := range mtdata { + metadata = append(metadata, mt.(string)) + } + + zoneMetadata := ResourceZoneMetadata{ + Kind: d.Get("kind").(string), + Metadata: metadata, + } + + log.Printf("[DEBUG] Creating PowerDNS Zone Metadata: %#v", zoneMetadata) + + metaid, err := client.UpdateZoneMetadata(zone, zoneMetadata) + if err != nil { + return fmt.Errorf("Failed to create PowerDNS Zone Metadata: %s", err) + } + + d.SetId(metaid) + log.Printf("[INFO] Created PowerDNS Zone Metadata with ID: %s", d.Id()) + + return nil +} + +func resourcePDNSZoneMetadataRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + + log.Printf("[DEBUG] Reading PowerDNS Zone Metadata: %s", d.Id()) + record, err := client.GetZoneMetadata(d.Id()) + if err != nil { + return fmt.Errorf("Couldn't fetch PowerDNS Zone Metadata: %s", err) + } + + zone, _, err := parseID(d.Id()) + if err != nil { + return err + } + + d.SetId(d.Id()) + d.Set("kind", record.Kind) + d.Set("metadata", record.Metadata) + d.Set("zone", zone) + + return nil +} + +func resourcePDNSZoneMetadataDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + + log.Printf("[INFO] Deleting PowerDNS Zone Metadata: %s", d.Id()) + err := client.DeleteZoneMetadata(d.Id()) + + if err != nil { + return fmt.Errorf("Error deleting PowerDNS Zone Metadata: %s", err) + } + + return nil +} + +func resourcePDNSZoneMetadataExists(d *schema.ResourceData, meta interface{}) (bool, error) { + log.Printf("[INFO] Checking existence of PowerDNS Zone Metadata: %s", d.Id()) + + client := meta.(*Client) + exists, err := client.ZoneMetadataExists(d.Id()) + + if err != nil { + return false, fmt.Errorf("Error checking PowerDNS Zone Metadata: %s", err) + } + return exists, nil +} diff --git a/powerdns/resource_powerdns_zone_metadata_test.go b/powerdns/resource_powerdns_zone_metadata_test.go new file mode 100644 index 00000000..a4a5eb54 --- /dev/null +++ b/powerdns/resource_powerdns_zone_metadata_test.go @@ -0,0 +1,139 @@ +package powerdns + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +func TestAccPDNSZoneMetadata_Empty(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testPDNSZoneMetadata_Empty, + ExpectError: regexp.MustCompile("'metadata' must not be empty"), + }, + }, + }) +} + +func TestAccPDNSZoneMetadata_AxfrFrom(t *testing.T) { + resourceName := "powerdns_zone_metadata.test-axfr-from" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPDNSZoneMetadataDestroy, + Steps: []resource.TestStep{ + { + Config: testPDNSZoneMetadata_AxfrFrom, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "kind", "ALLOW-AXFR-FROM"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccPDNSZoneMetadata_AxfrSource(t *testing.T) { + resourceName := "powerdns_zone_metadata.test-axfr-source" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPDNSZoneMetadataDestroy, + Steps: []resource.TestStep{ + { + Config: testPDNSZoneMetadata_AxfrSource, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "kind", "AXFR-SOURCE"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccPDNSZoneMetadata_XTest(t *testing.T) { + resourceName := "powerdns_zone_metadata.test-x-test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPDNSZoneMetadataDestroy, + Steps: []resource.TestStep{ + { + Config: testPDNSZoneMetadata_XTest, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "kind", "X-TEST"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckPDNSZoneMetadataDestroy(s *terraform.State) error { + for _, resource := range s.RootModule().Resources { + if resource.Type != "powerdns_zone_metadata" { + continue + } + + client := testAccProvider.Meta().(*Client) + exists, err := client.ZoneMetadataExists(resource.Primary.ID) + if err != nil { + return fmt.Errorf("Error checking if zone metadata still exists: %#v", resource.Primary.ID) + } + if exists { + return fmt.Errorf("Zone still exists: %#v", resource.Primary.ID) + } + + } + return nil +} + +const testPDNSZoneMetadata_Empty = ` +resource "powerdns_zone_metadata" "empty" { + zone = "sysa.xyz." + kind = "ALLOW-AXFR-FROM" + metadata = [ ] +}` + +const testPDNSZoneMetadata_AxfrFrom = ` +resource "powerdns_zone_metadata" "test-axfr-from" { + zone = "sysa.xyz." + kind = "ALLOW-AXFR-FROM" + metadata = ["AUTO-NS"] +}` + +const testPDNSZoneMetadata_AxfrSource = ` +resource "powerdns_zone_metadata" "test-axfr-source" { + zone = "sysa.xyz." + kind = "AXFR-SOURCE" + metadata = ["10.0.0.1"] +}` + +const testPDNSZoneMetadata_XTest = ` +resource "powerdns_zone_metadata" "test-x-test" { + zone = "sysa.xyz." + kind = "X-TEST" + metadata = ["test1", "test2"] +}` diff --git a/website/docs/r/metadata.html.markdown b/website/docs/r/metadata.html.markdown new file mode 100644 index 00000000..5a67cc8d --- /dev/null +++ b/website/docs/r/metadata.html.markdown @@ -0,0 +1,55 @@ +--- +layout: "powerdns" +page_title: "PowerDNS: powerdns_zone_metadata" +sidebar_current: "docs-powerdns-resource-zone-metadata" +description: |- + Provides a PowerDNS zone metadata resource. +--- + +# powerdns\_zone\_metadata + +Provides a PowerDNS zone metadata resource. + +## Example Usage + +All possible zone metadata can be found [here](https://doc.powerdns.com/authoritative/domainmetadata.html#). + +### ALLOW-AXFR-FROM example +For the v1 API (PowerDNS version 4): + +```hcl +# Add ALLOW-AXFR-FROM metadata to the zone +resource "powerdns_zone_metadata" "foobar" { + zone = "example.com." + kind = "ALLOW-AXFR-FROM" + metadata = ["AUTO-NS", "10.0.0.0/24"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `zone` - (Required) The name of zone to contain this metadata. +* `kind` - (Required) The kind of the metadata. +* `metadata` - (Required) A string list of metadata. + +### Attribute Reference + +The id of the resource is a composite of the zone name and metadata kind, joined by a separator - `:::`. + +For example, metadata in zone `foo.test.com.` of kind `ALLOW-AXFR-FROM` will be represented with the following `id`: `foo.test.com.:::ALLOW-AXFR-FROM` + +### Importing + +An existing record can be imported into this resource by supplying both the zone name and metadata kind it belongs to. +If the kind or zone is not found, or if the record is of a different type or in a different zone, an error will be returned. + +For example: + +``` +$ terraform import powerdns_zone_metadata.test-a test.com.:::AXFR-SOURCE +``` + +For more information on how to use terraform's `import` command, please refer to terraform's [core documentation](https://www.terraform.io/docs/import/index.html#currently-state-only). +