From 56f1835d8d0a624b2a3ae39d6de79e242906a127 Mon Sep 17 00:00:00 2001 From: Dmytro Aleksandrov Date: Sun, 6 Dec 2015 20:20:43 +0200 Subject: [PATCH] provider/powerdns: Move provider implementation from personal repo --- builtin/bins/provider-powerdns/main.go | 12 + builtin/bins/provider-powerdns/main_test.go | 1 + builtin/providers/powerdns/client.go | 318 ++++++++++++++++++ builtin/providers/powerdns/config.go | 24 ++ builtin/providers/powerdns/provider.go | 53 +++ .../powerdns/resource_powerdns_record.go | 147 ++++++++ 6 files changed, 555 insertions(+) create mode 100644 builtin/bins/provider-powerdns/main.go create mode 100644 builtin/bins/provider-powerdns/main_test.go create mode 100644 builtin/providers/powerdns/client.go create mode 100644 builtin/providers/powerdns/config.go create mode 100644 builtin/providers/powerdns/provider.go create mode 100644 builtin/providers/powerdns/resource_powerdns_record.go diff --git a/builtin/bins/provider-powerdns/main.go b/builtin/bins/provider-powerdns/main.go new file mode 100644 index 000000000..10061a261 --- /dev/null +++ b/builtin/bins/provider-powerdns/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/powerdns" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: powerdns.Provider, + }) +} diff --git a/builtin/bins/provider-powerdns/main_test.go b/builtin/bins/provider-powerdns/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-powerdns/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/powerdns/client.go b/builtin/providers/powerdns/client.go new file mode 100644 index 000000000..cbbebc164 --- /dev/null +++ b/builtin/providers/powerdns/client.go @@ -0,0 +1,318 @@ +package powerdns + +import ( + "net/http" + "net/url" + "github.com/hashicorp/go-cleanhttp" + "fmt" + "encoding/json" + "bytes" + "io" + "strings" +) + +type Client struct { + // Location of PowerDNS server to use + ServerUrl string + // REST API Static authentication key + ApiKey string + Http *http.Client +} + +// NewClient returns a new PowerDNS client +func NewClient(serverUrl string, apiKey string) (*Client, error) { + client := Client{ + ServerUrl:serverUrl, + ApiKey:apiKey, + Http: cleanhttp.DefaultClient(), + } + return &client, nil +} + +// Creates a new request with necessary headers +func (c *Client) newRequest(method string, endpoint string, body []byte) (*http.Request, error) { + + url, err := url.Parse(c.ServerUrl + endpoint) + if err != nil { + return nil, fmt.Errorf("Error during parting request URL: %s", err) + } + + var bodyReader io.Reader + if body != nil { + bodyReader = bytes.NewReader(body) + } + + req, err := http.NewRequest(method, url.String(), bodyReader) + if err != nil { + return nil, fmt.Errorf("Error during creation of request: %s", err) + } + + req.Header.Add("X-API-Key", c.ApiKey) + req.Header.Add("Accept", "application/json") + + if method != "GET" { + req.Header.Add("Content-Type", "application/json") + } + + return req, nil +} + +type ZoneInfo struct { + Id string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Kind string `json:"kind"` + DnsSec bool `json:"dnsssec"` + Serial int64 `json:"serial"` + Records []Record `json:"records,omitempty"` +} + +type Record struct { + Name string `json:"name"` + Type string `json:"type"` + Content string `json:"content"` + TTL int `json:"ttl"` + Disabled bool `json:"disabled"` +} + +type ResourceRecordSet struct { + Name string `json:"name"` + Type string `json:"type"` + ChangeType string `json:"changetype"` + Records []Record `json:"records,omitempty"` +} + +type zonePatchRequest struct { + RecordSets []ResourceRecordSet `json:"rrsets"` +} + +type errorResponse struct { + ErrorMsg string `json:"error"` +} + +func (record *Record) Id() string { + return fmt.Sprintf("%s-%s", record.Name, record.Type) +} + +func (rrSet *ResourceRecordSet) Id() string { + return fmt.Sprintf("%s-%s", rrSet.Name, rrSet.Type) +} + +// Returns name and type of record or record set based on it's ID +func parseId(recId string) (string, string, error) { + s := strings.Split(recId, "-") + if (len(s) == 2) { + return s[0], s[1], nil + } else { + return "", "", fmt.Errorf("Unknown record ID format") + } +} + +// Returns all Zones of server, without records +func (client *Client) ListZones() ([]ZoneInfo, error) { + + req, err := client.newRequest("GET", "/servers/localhost/zones", nil) + if err != nil { + return nil, err + } + + resp, err := client.Http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var zoneInfos []ZoneInfo + + err = json.NewDecoder(resp.Body).Decode(&zoneInfos) + if err != nil { + return nil, err + } + + return zoneInfos, nil +} + +// Returns all records in Zone +func (client *Client) ListRecords(zone string) ([]Record, error) { + req, err := client.newRequest("GET", fmt.Sprintf("/servers/localhost/zones/%s", zone), nil) + if err != nil { + return nil, err + } + + resp, err := client.Http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + zoneInfo := new(ZoneInfo) + err = json.NewDecoder(resp.Body).Decode(zoneInfo) + if err != nil { + return nil, err + } + + return zoneInfo.Records, nil +} + +// Returns only records of specified name and type +func (client *Client) ListRecordsInRRSet(zone string, name string, tpe string) ([]Record, error) { + allRecords, err := client.ListRecords(zone) + if err != nil { + return nil, err + } + + records := make([]Record, 0, 10) + for _, r := range allRecords { + if r.Name == name && r.Type == tpe { + records = append(records, r) + } + } + + return records, nil +} + +func (client *Client) ListRecordsByID(zone string, recId string) ([]Record, error) { + name, tpe, err := parseId(recId) + if err != nil { + return nil, err + } else { + return client.ListRecordsInRRSet(zone, name, tpe) + } +} + +// Checks if requested record exists in Zone +func (client *Client) RecordExists(zone string, name string, tpe string) (bool, error) { + allRecords, err := client.ListRecords(zone) + if err != nil { + return false, err + } + + for _, record := range allRecords { + if record.Name == name && record.Type == tpe { + return true, nil + } + } + return false, nil +} + +// Checks if requested record exists in Zone by it's ID +func (client *Client) RecordExistsByID(zone string, recId string) (bool, error) { + name, tpe, err := parseId(recId) + if err != nil { + return false, err + } else { + return client.RecordExists(zone, name, tpe) + } +} + +// Creates new record with single content entry +func (client *Client) CreateRecord(zone string, record Record) (string, error) { + reqBody, _ := json.Marshal(zonePatchRequest{ + RecordSets: []ResourceRecordSet{ + ResourceRecordSet{ + Name: record.Name, + Type: record.Type, + ChangeType: "REPLACE", + Records: []Record{record}, + }, + }, + }) + + req, err := client.newRequest("PATCH", fmt.Sprintf("/servers/localhost/zones/%s", zone), reqBody) + if err != nil { + return "", err + } + + resp, err := client.Http.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + errorResp := new(errorResponse) + if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { + return "", fmt.Errorf("Error creating record: %s", record.Id()) + } else { + return "", fmt.Errorf("Error creating record: %s, reason: %q", record.Id(), errorResp.ErrorMsg) + } + } else { + return record.Id(), nil + } +} + +// Creates new record set in Zone +func (client *Client) ReplaceRecordSet(zone string, rrSet ResourceRecordSet) (string, error) { + rrSet.ChangeType = "REPLACE" + + reqBody, _ := json.Marshal(zonePatchRequest{ + RecordSets: []ResourceRecordSet{rrSet }, + }) + + req, err := client.newRequest("PATCH", fmt.Sprintf("/servers/localhost/zones/%s", zone), reqBody) + if err != nil { + return "", err + } + + resp, err := client.Http.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + errorResp := new(errorResponse) + if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { + return "", fmt.Errorf("Error creating record set: %s", rrSet.Id()) + } else { + return "", fmt.Errorf("Error creating record set: %s, reason: %q", rrSet.Id(), errorResp.ErrorMsg) + } + } else { + return rrSet.Id(), nil + } +} + +// Deletes record set from Zone +func (client *Client) DeleteRecordSet(zone string, name string, tpe string) error { + reqBody, _ := json.Marshal(zonePatchRequest{ + RecordSets: []ResourceRecordSet{ + ResourceRecordSet{ + Name: name, + Type: tpe, + ChangeType: "DELETE", + }, + }, + }) + + req, err := client.newRequest("PATCH", fmt.Sprintf("/servers/localhost/zones/%s", zone), reqBody) + if err != nil { + return err + } + + resp, err := client.Http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + errorResp := new(errorResponse) + if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { + return fmt.Errorf("Error deleting record: %s %s", name, tpe) + } else { + return fmt.Errorf("Error deleting record: %s %s, reason: %q", name, tpe, errorResp.ErrorMsg) + } + } else { + return nil + } +} + +// Deletes record from Zone by it's ID +func (client *Client) DeleteRecordSetByID(zone string, recId string) error { + name, tpe, err := parseId(recId) + if err != nil { + return err + } else { + return client.DeleteRecordSet(zone, name, tpe) + } +} \ No newline at end of file diff --git a/builtin/providers/powerdns/config.go b/builtin/providers/powerdns/config.go new file mode 100644 index 000000000..9919a10c0 --- /dev/null +++ b/builtin/providers/powerdns/config.go @@ -0,0 +1,24 @@ +package powerdns + +import ( + "log" + "fmt" +) + +type Config struct { + ServerUrl string + ApiKey string +} + +// Client returns a new client for accessing PowerDNS +func (c *Config) Client() (*Client, error) { + client, err := NewClient(c.ServerUrl, c.ApiKey) + + if err != nil { + return nil, fmt.Errorf("Error setting up PowerDNS client: %s", err) + } + + log.Printf("[INFO] PowerDNS Client configured for server %s", c.ServerUrl) + + return client, nil +} diff --git a/builtin/providers/powerdns/provider.go b/builtin/providers/powerdns/provider.go new file mode 100644 index 000000000..65c54ee50 --- /dev/null +++ b/builtin/providers/powerdns/provider.go @@ -0,0 +1,53 @@ +package powerdns + +import ( + "os" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "api_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: envDefaultFunc("PDNS_API_KEY"), + Description: "REST API authentication key", + }, + "server_url": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: envDefaultFunc("PDNS_SERVER_URL"), + Description: "Location of PowerDNS server", + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "powerdns_record": resourcePDNSRecord(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(data *schema.ResourceData) (interface{}, error) { + config := Config{ + ApiKey: data.Get("api_key").(string), + ServerUrl: data.Get("server_url").(string), + } + + return config.Client() +} + +func envDefaultFunc(k string) schema.SchemaDefaultFunc { + return func() (interface{}, error) { + if v := os.Getenv(k); v != "" { + return v, nil + } + + return nil, nil + } +} + diff --git a/builtin/providers/powerdns/resource_powerdns_record.go b/builtin/providers/powerdns/resource_powerdns_record.go new file mode 100644 index 000000000..660ba8ecc --- /dev/null +++ b/builtin/providers/powerdns/resource_powerdns_record.go @@ -0,0 +1,147 @@ +package powerdns + +import ( + "log" + + "github.com/hashicorp/terraform/helper/schema" + "fmt" +) + +func resourcePDNSRecord() *schema.Resource { + return &schema.Resource{ + Create: resourcePDNSRecordCreate, + Read: resourcePDNSRecordRead, + Delete: resourcePDNSRecordDelete, + Exists: resourcePDNSRecordExists, + + Schema: map[string]*schema.Schema{ + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "ttl": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "records": &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + ForceNew: true, + Set: schema.HashString, + }, + + }, + } +} + + +func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + + rrSet := ResourceRecordSet{ + Name: d.Get("name").(string), + Type: d.Get("type").(string), + } + + zone := d.Get("zone").(string) + ttl := d.Get("ttl").(int) + recs := d.Get("records").(*schema.Set).List() + + if (len(recs) > 0) { + records := make([]Record, 0, len(recs)) + for _, recContent := range recs { + records = append(records, Record{Name: rrSet.Name, Type:rrSet.Type, TTL: ttl, Content: recContent.(string)}) + } + rrSet.Records = records + + log.Printf("[DEBUG] Creating PowerDNS Record: %#v", rrSet) + + recId, err := client.ReplaceRecordSet(zone, rrSet) + if err != nil { + return fmt.Errorf("Failed to create PowerDNS Record: %s", err) + } + + d.SetId(recId) + log.Printf("[INFO] Created PowerDNS Record with ID: %s", d.Id()) + + } else { + log.Printf("[DEBUG] Deleting empty PowerDNS Record: %#v", rrSet) + err := client.DeleteRecordSet(zone, rrSet.Name, rrSet.Type) + if err != nil { + return fmt.Errorf("Failed to delete PowerDNS Record: %s", err) + } + + d.SetId(rrSet.Id()) + } + + return resourcePDNSRecordRead(d, meta) +} + +func resourcePDNSRecordRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + + log.Printf("[DEBUG] Reading PowerDNS Record: %s", d.Id()) + records, err := client.ListRecordsByID(d.Get("zone").(string), d.Id()) + if err != nil { + return fmt.Errorf("Couldn't fetch PowerDNS Record: %s", err) + } + + recs := make([]string, 0, len(records)) + for _, r := range records { + recs = append(recs, r.Content) + } + d.Set("records", recs) + + if (len(records) > 0) { + d.Set("ttl", records[0].TTL) + } + + return nil +} + +func resourcePDNSRecordDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client) + + log.Printf("[INFO] Deleting PowerDNS Record: %s", d.Id()) + err := client.DeleteRecordSetByID(d.Get("zone").(string), d.Id()) + + if err != nil { + return fmt.Errorf("Error deleting PowerDNS Record: %s", err) + } + + return nil +} + +func resourcePDNSRecordExists(d *schema.ResourceData, meta interface{}) (bool, error) { + zone := d.Get("zone").(string) + name := d.Get("name").(string) + tpe := d.Get("type").(string) + + log.Printf("[INFO] Checking existence of PowerDNS Record: %s, %s", name, tpe) + + client := meta.(*Client) + exists, err := client.RecordExists(zone, name, tpe) + + if err != nil { + return false, fmt.Errorf("Error checking PowerDNS Record: %s", err) + } else { + return exists, nil + } +} \ No newline at end of file