constellix: improve challenge. (#1115)
This commit is contained in:
parent
c0bc316a5f
commit
b58c9499ca
13 changed files with 261 additions and 91 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue