constellix: improve challenge. (#1115)

This commit is contained in:
Ludovic Fernandez 2020-04-18 20:39:10 +02:00 committed by GitHub
parent c0bc316a5f
commit b58c9499ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 261 additions and 91 deletions

View file

@ -104,22 +104,26 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return fmt.Errorf("constellix: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err)
}
domainID, err := d.client.Domains.GetID(dns01.UnFqdn(authZone))
dom, err := d.client.Domains.GetByName(dns01.UnFqdn(authZone))
if err != nil {
return fmt.Errorf("constellix: failed to get domain ID: %w", err)
}
records, err := d.client.TxtRecords.GetAll(domainID)
if err != nil {
return fmt.Errorf("constellix: failed to get TXT records: %w", err)
return fmt.Errorf("constellix: failed to get domain: %w", err)
}
recordName := getRecordName(fqdn, authZone)
record := findRecords(records, recordName)
records, err := d.client.TxtRecords.Search(dom.ID, internal.Exact, recordName)
if err != nil {
return fmt.Errorf("constellix: failed to get TXT records: %w", err)
}
if len(records) > 1 {
return errors.New("constellix: failed to get TXT records")
}
// TXT record entry already existing
if record != nil {
if len(records) == 1 {
record := records[0]
if containsValue(record, value) {
return nil
}
@ -130,7 +134,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
RoundRobin: append(record.RoundRobin, internal.RecordValue{Value: fmt.Sprintf(`"%s"`, value)}),
}
_, err = d.client.TxtRecords.Update(domainID, record.ID, request)
_, err = d.client.TxtRecords.Update(dom.ID, record.ID, request)
if err != nil {
return fmt.Errorf("constellix: failed to update TXT records: %w", err)
}
@ -145,7 +149,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error {
},
}
_, err = d.client.TxtRecords.Create(domainID, request)
_, err = d.client.TxtRecords.Create(dom.ID, request)
if err != nil {
return fmt.Errorf("constellix: failed to create TXT record %s: %w", fqdn, err)
}
@ -162,30 +166,35 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return fmt.Errorf("constellix: could not find zone for domain %q and fqdn %q : %w", domain, fqdn, err)
}
domainID, err := d.client.Domains.GetID(dns01.UnFqdn(authZone))
dom, err := d.client.Domains.GetByName(dns01.UnFqdn(authZone))
if err != nil {
return fmt.Errorf("constellix: failed to get domain ID: %w", err)
}
records, err := d.client.TxtRecords.GetAll(domainID)
if err != nil {
return fmt.Errorf("constellix: failed to get TXT records: %w", err)
return fmt.Errorf("constellix: failed to get domain: %w", err)
}
recordName := getRecordName(fqdn, authZone)
record := findRecords(records, recordName)
if record == nil {
records, err := d.client.TxtRecords.Search(dom.ID, internal.Exact, recordName)
if err != nil {
return fmt.Errorf("constellix: failed to get TXT records: %w", err)
}
if len(records) > 1 {
return errors.New("constellix: failed to get TXT records")
}
if len(records) == 0 {
return nil
}
record := records[0]
if !containsValue(record, value) {
return nil
}
// only 1 record value, the whole record must be deleted.
if len(record.Value) == 1 {
_, err = d.client.TxtRecords.Delete(domainID, record.ID)
_, err = d.client.TxtRecords.Delete(dom.ID, record.ID)
if err != nil {
return fmt.Errorf("constellix: failed to delete TXT records: %w", err)
}
@ -203,7 +212,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
}
}
_, err = d.client.TxtRecords.Update(domainID, record.ID, request)
_, err = d.client.TxtRecords.Update(dom.ID, record.ID, request)
if err != nil {
return fmt.Errorf("constellix: failed to update TXT records: %w", err)
}
@ -211,17 +220,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return nil
}
func findRecords(records []internal.Record, name string) *internal.Record {
for _, r := range records {
if r.Name == name {
return &r
}
}
return nil
}
func containsValue(record *internal.Record, value string) bool {
func containsValue(record internal.Record, value string) bool {
for _, val := range record.Value {
if val.Value == fmt.Sprintf(`"%s"`, value) {
return true

View file

@ -96,11 +96,20 @@ func checkResponse(resp *http.Response) error {
data, err := ioutil.ReadAll(resp.Body)
if err == nil && data != nil {
msg := APIError{}
if json.Unmarshal(data, &msg) != nil {
msg := &APIError{StatusCode: resp.StatusCode}
if json.Unmarshal(data, msg) != nil {
return fmt.Errorf("API error: status code: %d: %v", resp.StatusCode, string(data))
}
return msg
switch resp.StatusCode {
case http.StatusNotFound:
return &NotFound{APIError: msg}
case http.StatusBadRequest:
return &BadRequest{APIError: msg}
default:
return msg
}
}
return fmt.Errorf("API error, status code: %d", resp.StatusCode)

View file

@ -1,6 +1,7 @@
package internal
import (
"errors"
"fmt"
"net/http"
@ -10,48 +11,8 @@ import (
// DomainService API access to Domain.
type DomainService service
// GetID for a domain name.
func (s *DomainService) GetID(domainName string) (int64, error) {
params := &PaginationParameters{
Offset: 0,
Max: 100,
Sort: "name",
Order: "asc",
}
domains, err := s.GetAll(params)
if err != nil {
return 0, err
}
for len(domains) > 0 {
for _, domain := range domains {
if domain.Name == domainName {
return domain.ID, nil
}
}
if params.Max > len(domains) {
break
}
params = &PaginationParameters{
Offset: params.Max,
Max: 100,
Sort: "name",
Order: "asc",
}
domains, err = s.GetAll(params)
if err != nil {
return 0, err
}
}
return 0, fmt.Errorf("domain not found: %s", domainName)
}
// GetAll domains.
// https://api-docs.constellix.com/?version=latest#484c3f21-d724-4ee4-a6fa-ab22c8eb9e9b
func (s *DomainService) GetAll(params *PaginationParameters) ([]Domain, error) {
endpoint, err := s.client.createEndpoint(defaultVersion, "domains")
if err != nil {
@ -79,3 +40,50 @@ func (s *DomainService) GetAll(params *PaginationParameters) ([]Domain, error) {
return domains, nil
}
// GetByName Gets domain by name.
func (s *DomainService) GetByName(domainName string) (Domain, error) {
domains, err := s.Search(Exact, domainName)
if err != nil {
return Domain{}, err
}
if len(domains) == 0 {
return Domain{}, fmt.Errorf("domain not found: %s", domainName)
}
if len(domains) > 1 {
return Domain{}, fmt.Errorf("multiple domains found: %v", domains)
}
return domains[0], nil
}
// Search searches for a domain by name.
// https://api-docs.constellix.com/?version=latest#3d7b2679-2209-49f3-b011-b7d24e512008
func (s *DomainService) Search(filter searchFilter, value string) ([]Domain, error) {
endpoint, err := s.client.createEndpoint(defaultVersion, "domains", "search")
if err != nil {
return nil, fmt.Errorf("failed to create request endpoint: %w", err)
}
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
query := req.URL.Query()
query.Set(string(filter), value)
req.URL.RawQuery = query.Encode()
var domains []Domain
err = s.client.do(req, &domains)
if err != nil {
var nf *NotFound
if !errors.As(err, &nf) {
return nil, err
}
}
return domains, nil
}

View file

@ -31,7 +31,7 @@ func TestDomainService_GetAll(t *testing.T) {
return
}
file, err := os.Open("./fixtures/domains-01.json")
file, err := os.Open("./fixtures/domains-GetAll.json")
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
@ -49,23 +49,26 @@ func TestDomainService_GetAll(t *testing.T) {
require.NoError(t, err)
expected := []Domain{
{ID: 273302, Name: "lego.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"},
{ID: 273301, Name: "aaa.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"},
{ID: 273302, Name: "bbb.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"},
{ID: 273303, Name: "ccc.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"},
{ID: 273304, Name: "ddd.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"},
}
assert.Equal(t, expected, data)
}
func TestDomainService_GetID(t *testing.T) {
func TestDomainService_Search(t *testing.T) {
client, handler, tearDown := setupAPIMock()
defer tearDown()
handler.HandleFunc("/v1/domains", func(rw http.ResponseWriter, req *http.Request) {
handler.HandleFunc("/v1/domains/search", func(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
return
}
file, err := os.Open("./fixtures/domains-02.json")
file, err := os.Open("./fixtures/domains-Search.json")
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
@ -79,8 +82,12 @@ func TestDomainService_GetID(t *testing.T) {
}
})
data, err := client.Domains.GetID("ddd.wtf")
data, err := client.Domains.Search(Exact, "lego.wtf")
require.NoError(t, err)
assert.EqualValues(t, 273304, data)
expected := []Domain{
{ID: 273302, Name: "lego.wtf", TypeID: 1, Version: 9, Status: "ACTIVE"},
}
assert.Equal(t, expected, data)
}

View file

@ -0,0 +1,25 @@
[
{
"id": 3557066,
"type": "TXT",
"recordType": "txt",
"name": "test",
"recordOption": "roundRobin",
"ttl": 300,
"gtdRegion": 1,
"parentId": 273302,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1580908547865,
"value": [
{
"value": "\"test\""
}
],
"roundRobin": [
{
"value": "\"test\""
}
]
}
]

View file

@ -0,0 +1,25 @@
[
{
"id": 3557066,
"type": "TXT",
"recordType": "txt",
"name": "test",
"recordOption": "roundRobin",
"ttl": 300,
"gtdRegion": 1,
"parentId": 273302,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1580908547865,
"value": [
{
"value": "\"test\""
}
],
"roundRobin": [
{
"value": "\"test\""
}
]
}
]

View file

@ -1,16 +1,46 @@
package internal
import (
"fmt"
"strings"
)
// Search filters
const (
StartsWith searchFilter = "startswith"
Exact searchFilter = "exact"
EndsWith searchFilter = "endswith"
Contains searchFilter = "contains"
)
type searchFilter string
// NotFound Not found error.
type NotFound struct {
*APIError
}
func (e *NotFound) Unwrap() error {
return e.APIError
}
// BadRequest Bad request error.
type BadRequest struct {
*APIError
}
func (e *BadRequest) Unwrap() error {
return e.APIError
}
// APIError is the representation of an API error.
type APIError struct {
Errors []string `json:"errors"`
StatusCode int `json:"statusCode"`
Errors []string `json:"errors"`
}
func (a APIError) Error() string {
return strings.Join(a.Errors, ": ")
return fmt.Sprintf("%d: %s", a.StatusCode, strings.Join(a.Errors, ": "))
}
// SuccessMessage is the representation of a success message.

View file

@ -3,6 +3,7 @@ package internal
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
@ -130,3 +131,33 @@ func (s *TxtRecordService) Delete(domainID, recordID int64) (*SuccessMessage, er
return msg, nil
}
// Search searches for a TXT record by name.
// https://api-docs.constellix.com/?version=latest#81003e4f-bd3f-413f-a18d-6d9d18f10201
func (s *TxtRecordService) Search(domainID int64, filter searchFilter, value string) ([]Record, error) {
endpoint, err := s.client.createEndpoint(defaultVersion, "domains", strconv.FormatInt(domainID, 10), "records", "txt", "search")
if err != nil {
return nil, fmt.Errorf("failed to create request endpoint: %w", err)
}
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
query := req.URL.Query()
query.Set(string(filter), value)
req.URL.RawQuery = query.Encode()
var records []Record
err = s.client.do(req, &records)
if err != nil {
var nf *NotFound
if !errors.As(err, &nf) {
return nil, err
}
}
return records, nil
}

View file

@ -22,7 +22,7 @@ func TestTxtRecordService_Create(t *testing.T) {
return
}
file, err := os.Open("./fixtures/records-01.json")
file, err := os.Open("./fixtures/records-Create.json")
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
@ -42,7 +42,7 @@ func TestTxtRecordService_Create(t *testing.T) {
recordsJSON, err := json.Marshal(records)
require.NoError(t, err)
expectedContent, err := ioutil.ReadFile("./fixtures/records-01.json")
expectedContent, err := ioutil.ReadFile("./fixtures/records-Create.json")
require.NoError(t, err)
assert.JSONEq(t, string(expectedContent), string(recordsJSON))
@ -58,7 +58,7 @@ func TestTxtRecordService_GetAll(t *testing.T) {
return
}
file, err := os.Open("./fixtures/records-01.json")
file, err := os.Open("./fixtures/records-GetAll.json")
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
@ -78,7 +78,7 @@ func TestTxtRecordService_GetAll(t *testing.T) {
recordsJSON, err := json.Marshal(records)
require.NoError(t, err)
expectedContent, err := ioutil.ReadFile("./fixtures/records-01.json")
expectedContent, err := ioutil.ReadFile("./fixtures/records-GetAll.json")
require.NoError(t, err)
assert.JSONEq(t, string(expectedContent), string(recordsJSON))
@ -94,7 +94,7 @@ func TestTxtRecordService_Get(t *testing.T) {
return
}
file, err := os.Open("./fixtures/records-02.json")
file, err := os.Open("./fixtures/records-Get.json")
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
@ -180,3 +180,39 @@ func TestTxtRecordService_Delete(t *testing.T) {
expected := &SuccessMessage{Success: "Record deleted successfully"}
assert.Equal(t, expected, msg)
}
func TestTxtRecordService_Search(t *testing.T) {
client, handler, tearDown := setupAPIMock()
defer tearDown()
handler.HandleFunc("/v1/domains/12345/records/txt/search", func(rw http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodGet {
http.Error(rw, "invalid method: "+req.Method, http.StatusBadRequest)
return
}
file, err := os.Open("./fixtures/records-Search.json")
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
defer func() { _ = file.Close() }()
_, err = io.Copy(rw, file)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
})
records, err := client.TxtRecords.Search(12345, Exact, "test")
require.NoError(t, err)
recordsJSON, err := json.Marshal(records)
require.NoError(t, err)
expectedContent, err := ioutil.ReadFile("./fixtures/records-Search.json")
require.NoError(t, err)
assert.JSONEq(t, string(expectedContent), string(recordsJSON))
}