github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/powerdns/client.go (about)

     1  package powerdns
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/hashicorp/go-cleanhttp"
    14  )
    15  
    16  type Client struct {
    17  	ServerUrl  string // Location of PowerDNS server to use
    18  	ApiKey     string // REST API Static authentication key
    19  	ApiVersion int    // API version to use
    20  	Http       *http.Client
    21  }
    22  
    23  // NewClient returns a new PowerDNS client
    24  func NewClient(serverUrl string, apiKey string) (*Client, error) {
    25  	client := Client{
    26  		ServerUrl: serverUrl,
    27  		ApiKey:    apiKey,
    28  		Http:      cleanhttp.DefaultClient(),
    29  	}
    30  	var err error
    31  	client.ApiVersion, err = client.detectApiVersion()
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  	return &client, nil
    36  }
    37  
    38  // Creates a new request with necessary headers
    39  func (c *Client) newRequest(method string, endpoint string, body []byte) (*http.Request, error) {
    40  
    41  	var urlStr string
    42  	if c.ApiVersion > 0 {
    43  		urlStr = c.ServerUrl + "/api/v" + strconv.Itoa(c.ApiVersion) + endpoint
    44  	} else {
    45  		urlStr = c.ServerUrl + endpoint
    46  	}
    47  	url, err := url.Parse(urlStr)
    48  	if err != nil {
    49  		return nil, fmt.Errorf("Error during parsing request URL: %s", err)
    50  	}
    51  
    52  	var bodyReader io.Reader
    53  	if body != nil {
    54  		bodyReader = bytes.NewReader(body)
    55  	}
    56  
    57  	req, err := http.NewRequest(method, url.String(), bodyReader)
    58  	if err != nil {
    59  		return nil, fmt.Errorf("Error during creation of request: %s", err)
    60  	}
    61  
    62  	req.Header.Add("X-API-Key", c.ApiKey)
    63  	req.Header.Add("Accept", "application/json")
    64  
    65  	if method != "GET" {
    66  		req.Header.Add("Content-Type", "application/json")
    67  	}
    68  
    69  	return req, nil
    70  }
    71  
    72  type ZoneInfo struct {
    73  	Id                 string              `json:"id"`
    74  	Name               string              `json:"name"`
    75  	URL                string              `json:"url"`
    76  	Kind               string              `json:"kind"`
    77  	DnsSec             bool                `json:"dnsssec"`
    78  	Serial             int64               `json:"serial"`
    79  	Records            []Record            `json:"records,omitempty"`
    80  	ResourceRecordSets []ResourceRecordSet `json:"rrsets,omitempty"`
    81  }
    82  
    83  type Record struct {
    84  	Name     string `json:"name"`
    85  	Type     string `json:"type"`
    86  	Content  string `json:"content"`
    87  	TTL      int    `json:"ttl"` // For API v0
    88  	Disabled bool   `json:"disabled"`
    89  }
    90  
    91  type ResourceRecordSet struct {
    92  	Name       string   `json:"name"`
    93  	Type       string   `json:"type"`
    94  	ChangeType string   `json:"changetype"`
    95  	TTL        int      `json:"ttl"` // For API v1
    96  	Records    []Record `json:"records,omitempty"`
    97  }
    98  
    99  type zonePatchRequest struct {
   100  	RecordSets []ResourceRecordSet `json:"rrsets"`
   101  }
   102  
   103  type errorResponse struct {
   104  	ErrorMsg string `json:"error"`
   105  }
   106  
   107  const idSeparator string = ":::"
   108  
   109  func (record *Record) Id() string {
   110  	return record.Name + idSeparator + record.Type
   111  }
   112  
   113  func (rrSet *ResourceRecordSet) Id() string {
   114  	return rrSet.Name + idSeparator + rrSet.Type
   115  }
   116  
   117  // Returns name and type of record or record set based on it's ID
   118  func parseId(recId string) (string, string, error) {
   119  	s := strings.Split(recId, idSeparator)
   120  	if len(s) == 2 {
   121  		return s[0], s[1], nil
   122  	} else {
   123  		return "", "", fmt.Errorf("Unknown record ID format")
   124  	}
   125  }
   126  
   127  // Detects the API version in use on the server
   128  // Uses int to represent the API version: 0 is the legacy AKA version 3.4 API
   129  // Any other integer correlates with the same API version
   130  func (client *Client) detectApiVersion() (int, error) {
   131  	req, err := client.newRequest("GET", "/api/v1/servers", nil)
   132  	if err != nil {
   133  		return -1, err
   134  	}
   135  	resp, err := client.Http.Do(req)
   136  	if err != nil {
   137  		return -1, err
   138  	}
   139  	defer resp.Body.Close()
   140  	if resp.StatusCode == 200 {
   141  		return 1, nil
   142  	} else {
   143  		return 0, nil
   144  	}
   145  }
   146  
   147  // Returns all Zones of server, without records
   148  func (client *Client) ListZones() ([]ZoneInfo, error) {
   149  
   150  	req, err := client.newRequest("GET", "/servers/localhost/zones", nil)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	resp, err := client.Http.Do(req)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	defer resp.Body.Close()
   160  
   161  	var zoneInfos []ZoneInfo
   162  
   163  	err = json.NewDecoder(resp.Body).Decode(&zoneInfos)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	return zoneInfos, nil
   169  }
   170  
   171  // Returns all records in Zone
   172  func (client *Client) ListRecords(zone string) ([]Record, error) {
   173  	req, err := client.newRequest("GET", fmt.Sprintf("/servers/localhost/zones/%s", zone), nil)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	resp, err := client.Http.Do(req)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	defer resp.Body.Close()
   183  
   184  	zoneInfo := new(ZoneInfo)
   185  	err = json.NewDecoder(resp.Body).Decode(zoneInfo)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	records := zoneInfo.Records
   191  	// Convert the API v1 response to v0 record structure
   192  	for _, rrs := range zoneInfo.ResourceRecordSets {
   193  		for _, record := range rrs.Records {
   194  			records = append(records, Record{
   195  				Name:    rrs.Name,
   196  				Type:    rrs.Type,
   197  				Content: record.Content,
   198  				TTL:     rrs.TTL,
   199  			})
   200  		}
   201  	}
   202  
   203  	return records, nil
   204  }
   205  
   206  // Returns only records of specified name and type
   207  func (client *Client) ListRecordsInRRSet(zone string, name string, tpe string) ([]Record, error) {
   208  	allRecords, err := client.ListRecords(zone)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	records := make([]Record, 0, 10)
   214  	for _, r := range allRecords {
   215  		if r.Name == name && r.Type == tpe {
   216  			records = append(records, r)
   217  		}
   218  	}
   219  
   220  	return records, nil
   221  }
   222  
   223  func (client *Client) ListRecordsByID(zone string, recId string) ([]Record, error) {
   224  	name, tpe, err := parseId(recId)
   225  	if err != nil {
   226  		return nil, err
   227  	} else {
   228  		return client.ListRecordsInRRSet(zone, name, tpe)
   229  	}
   230  }
   231  
   232  // Checks if requested record exists in Zone
   233  func (client *Client) RecordExists(zone string, name string, tpe string) (bool, error) {
   234  	allRecords, err := client.ListRecords(zone)
   235  	if err != nil {
   236  		return false, err
   237  	}
   238  
   239  	for _, record := range allRecords {
   240  		if record.Name == name && record.Type == tpe {
   241  			return true, nil
   242  		}
   243  	}
   244  	return false, nil
   245  }
   246  
   247  // Checks if requested record exists in Zone by it's ID
   248  func (client *Client) RecordExistsByID(zone string, recId string) (bool, error) {
   249  	name, tpe, err := parseId(recId)
   250  	if err != nil {
   251  		return false, err
   252  	} else {
   253  		return client.RecordExists(zone, name, tpe)
   254  	}
   255  }
   256  
   257  // Creates new record with single content entry
   258  func (client *Client) CreateRecord(zone string, record Record) (string, error) {
   259  	reqBody, _ := json.Marshal(zonePatchRequest{
   260  		RecordSets: []ResourceRecordSet{
   261  			{
   262  				Name:       record.Name,
   263  				Type:       record.Type,
   264  				ChangeType: "REPLACE",
   265  				Records:    []Record{record},
   266  			},
   267  		},
   268  	})
   269  
   270  	req, err := client.newRequest("PATCH", fmt.Sprintf("/servers/localhost/zones/%s", zone), reqBody)
   271  	if err != nil {
   272  		return "", err
   273  	}
   274  
   275  	resp, err := client.Http.Do(req)
   276  	if err != nil {
   277  		return "", err
   278  	}
   279  	defer resp.Body.Close()
   280  
   281  	if resp.StatusCode != 200 && resp.StatusCode != 204 {
   282  		errorResp := new(errorResponse)
   283  		if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
   284  			return "", fmt.Errorf("Error creating record: %s", record.Id())
   285  		} else {
   286  			return "", fmt.Errorf("Error creating record: %s, reason: %q", record.Id(), errorResp.ErrorMsg)
   287  		}
   288  	} else {
   289  		return record.Id(), nil
   290  	}
   291  }
   292  
   293  // Creates new record set in Zone
   294  func (client *Client) ReplaceRecordSet(zone string, rrSet ResourceRecordSet) (string, error) {
   295  	rrSet.ChangeType = "REPLACE"
   296  
   297  	reqBody, _ := json.Marshal(zonePatchRequest{
   298  		RecordSets: []ResourceRecordSet{rrSet},
   299  	})
   300  
   301  	req, err := client.newRequest("PATCH", fmt.Sprintf("/servers/localhost/zones/%s", zone), reqBody)
   302  	if err != nil {
   303  		return "", err
   304  	}
   305  
   306  	resp, err := client.Http.Do(req)
   307  	if err != nil {
   308  		return "", err
   309  	}
   310  	defer resp.Body.Close()
   311  
   312  	if resp.StatusCode != 200 && resp.StatusCode != 204 {
   313  		errorResp := new(errorResponse)
   314  		if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
   315  			return "", fmt.Errorf("Error creating record set: %s", rrSet.Id())
   316  		} else {
   317  			return "", fmt.Errorf("Error creating record set: %s, reason: %q", rrSet.Id(), errorResp.ErrorMsg)
   318  		}
   319  	} else {
   320  		return rrSet.Id(), nil
   321  	}
   322  }
   323  
   324  // Deletes record set from Zone
   325  func (client *Client) DeleteRecordSet(zone string, name string, tpe string) error {
   326  	reqBody, _ := json.Marshal(zonePatchRequest{
   327  		RecordSets: []ResourceRecordSet{
   328  			{
   329  				Name:       name,
   330  				Type:       tpe,
   331  				ChangeType: "DELETE",
   332  			},
   333  		},
   334  	})
   335  
   336  	req, err := client.newRequest("PATCH", fmt.Sprintf("/servers/localhost/zones/%s", zone), reqBody)
   337  	if err != nil {
   338  		return err
   339  	}
   340  
   341  	resp, err := client.Http.Do(req)
   342  	if err != nil {
   343  		return err
   344  	}
   345  	defer resp.Body.Close()
   346  
   347  	if resp.StatusCode != 200 && resp.StatusCode != 204 {
   348  		errorResp := new(errorResponse)
   349  		if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
   350  			return fmt.Errorf("Error deleting record: %s %s", name, tpe)
   351  		} else {
   352  			return fmt.Errorf("Error deleting record: %s %s, reason: %q", name, tpe, errorResp.ErrorMsg)
   353  		}
   354  	} else {
   355  		return nil
   356  	}
   357  }
   358  
   359  // Deletes record from Zone by it's ID
   360  func (client *Client) DeleteRecordSetByID(zone string, recId string) error {
   361  	name, tpe, err := parseId(recId)
   362  	if err != nil {
   363  		return err
   364  	} else {
   365  		return client.DeleteRecordSet(zone, name, tpe)
   366  	}
   367  }