mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-09 05:36:27 +08:00
The PR follows https://github.com/StackExchange/dnscontrol/pull/3542 Found some bugs when running intergration tests locally again, and the PR is an attempt to fix them: - When updating/creating HTTPS/SRV records, Vercel API only reads from the corresponding struct (either `srv` or `https`). If we provide a `value`, the Vercel API will reject with an error. - The PR makes `Value` "nil-able", and sets `Value` to nil when dealing with `SRV` or `HTTPS` records. - When updating a record, currently, we treat the empty SVC param as omitting the field. But with Vercel's API, omitting a field means not updating the field. We need to explicitly make the field an empty string to create/update an empty SVC param, and the PR does that. - Vercel implements an unknown `ech=` parameter validation process for HTTPS records. The validation process is unknown, undocumented, thus I can't implement a `rejectif` for `AuditRecord`. - Let's make this a known caveat, describe it in the provider docs, skip these intergration tests, and move on. Please tag this PR w/ `provider-VERCEL`.
172 lines
4.6 KiB
Go
172 lines
4.6 KiB
Go
package vercel
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
vercelClient "github.com/vercel/terraform-provider-vercel/client"
|
|
)
|
|
|
|
// DNSRecord is a helper struct to unmarshal the JSON response.
|
|
// It embeds vercelClient.DNSRecord to reuse the upstream type,
|
|
// but adds fields to handle API inconsistencies (type vs recordType, mxPriority).
|
|
type DNSRecord struct {
|
|
vercelClient.DNSRecord
|
|
Type string `json:"type"`
|
|
// Normally MXPriority would be uint16 type, but since vercelClient.DNSRecord uses int64, we'd better be consistent here
|
|
// Later in GetZoneRecords we do a `uint16OrZero` to ensure the type is correct
|
|
MXPriority int64 `json:"mxPriority"`
|
|
}
|
|
|
|
// pagination represents the pagination object in Vercel API responses.
|
|
type pagination struct {
|
|
Count int64 `json:"count"`
|
|
Next *int64 `json:"next"`
|
|
Prev *int64 `json:"prev"`
|
|
}
|
|
|
|
// listResponse represents the response from the Vercel List DNS Records API.
|
|
type listResponse struct {
|
|
Records []DNSRecord `json:"records"`
|
|
Pagination pagination `json:"pagination"`
|
|
}
|
|
|
|
// Vercel API limit is max 100
|
|
const vercelAPIPaginationLimit = 100
|
|
|
|
// ListDNSRecords retrieves all DNS records for a domain, handling pagination.
|
|
func (c *vercelProvider) ListDNSRecords(ctx context.Context, domain string) ([]DNSRecord, error) {
|
|
var allRecords []DNSRecord
|
|
var nextTimestamp int64
|
|
|
|
for {
|
|
url := fmt.Sprintf("https://api.vercel.com/v4/domains/%s/records?limit=%d", domain, vercelAPIPaginationLimit)
|
|
if c.teamID != "" {
|
|
url += fmt.Sprintf("&teamId=%s", c.teamID)
|
|
}
|
|
if nextTimestamp != 0 {
|
|
url += fmt.Sprintf("&until=%d", nextTimestamp)
|
|
}
|
|
|
|
var result listResponse
|
|
err := c.doRequest(clientRequest{
|
|
ctx: ctx,
|
|
method: http.MethodGet,
|
|
url: url,
|
|
}, &result, c.listLimiter)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list DNS records: %w", err)
|
|
}
|
|
|
|
for _, r := range result.Records {
|
|
// The official SDK expects 'recordType' but the API returns 'type'.
|
|
// We explicitly map it here to fix the discrepancy.
|
|
r.RecordType = r.Type
|
|
// Ensure Domain field is set (it might not be in the record object itself)
|
|
if r.Domain == "" {
|
|
r.Domain = domain
|
|
}
|
|
if r.TeamID == "" {
|
|
r.TeamID = c.teamID
|
|
}
|
|
|
|
allRecords = append(allRecords, r)
|
|
}
|
|
|
|
if result.Pagination.Next == nil {
|
|
break
|
|
}
|
|
nextTimestamp = *result.Pagination.Next
|
|
}
|
|
|
|
return allRecords, nil
|
|
}
|
|
|
|
// httpsRecord structure for Vercel API
|
|
type httpsRecord struct {
|
|
Priority int64 `json:"priority"`
|
|
Target string `json:"target"`
|
|
Params string `json:"params"`
|
|
}
|
|
|
|
// createDNSRecordRequest embeds the official SDK request but adds HTTPS support
|
|
type createDNSRecordRequest struct {
|
|
vercelClient.CreateDNSRecordRequest
|
|
Value *string `json:"value,omitempty"`
|
|
HTTPS *httpsRecord `json:"https,omitempty"`
|
|
}
|
|
|
|
// CreateDNSRecord creates a DNS record.
|
|
func (c *vercelProvider) CreateDNSRecord(ctx context.Context, req createDNSRecordRequest) (*vercelClient.DNSRecord, error) {
|
|
url := fmt.Sprintf("https://api.vercel.com/v4/domains/%s/records", req.Domain)
|
|
if c.teamID != "" {
|
|
url += "?teamId=" + c.teamID
|
|
}
|
|
|
|
var response struct {
|
|
RecordID string `json:"uid"`
|
|
}
|
|
|
|
payloadJSON, err := json.Marshal(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = c.doRequest(clientRequest{
|
|
ctx: ctx,
|
|
method: http.MethodPost,
|
|
url: url,
|
|
body: string(payloadJSON),
|
|
}, &response, c.createLimiter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &vercelClient.DNSRecord{ID: response.RecordID}, nil
|
|
}
|
|
|
|
// updateDNSRecordRequest embeds the official SDK request but adds HTTPS support
|
|
type updateDNSRecordRequest struct {
|
|
vercelClient.UpdateDNSRecordRequest
|
|
HTTPS *httpsRecord `json:"https,omitempty"`
|
|
}
|
|
|
|
// UpdateDNSRecord updates a DNS record.
|
|
func (c *vercelProvider) UpdateDNSRecord(ctx context.Context, recordID string, req updateDNSRecordRequest) (*vercelClient.DNSRecord, error) {
|
|
url := fmt.Sprintf("https://api.vercel.com/v4/domains/records/%s", recordID)
|
|
if c.teamID != "" {
|
|
url += "?teamId=" + c.teamID
|
|
}
|
|
|
|
payloadJSON, err := json.Marshal(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result vercelClient.DNSRecord
|
|
err = c.doRequest(clientRequest{
|
|
ctx: ctx,
|
|
method: http.MethodPatch,
|
|
url: url,
|
|
body: string(payloadJSON),
|
|
}, &result, c.updateLimiter)
|
|
|
|
return &result, err
|
|
}
|
|
|
|
// DeleteDNSRecord deletes a DNS record.
|
|
func (c *vercelProvider) DeleteDNSRecord(ctx context.Context, domain string, recordID string) error {
|
|
url := fmt.Sprintf("https://api.vercel.com/v2/domains/%s/records/%s", domain, recordID)
|
|
if c.teamID != "" {
|
|
url += "?teamId=" + c.teamID
|
|
}
|
|
|
|
return c.doRequest(clientRequest{
|
|
ctx: ctx,
|
|
method: http.MethodDelete,
|
|
url: url,
|
|
}, nil, c.deleteLimiter)
|
|
}
|