-
-
Notifications
You must be signed in to change notification settings - Fork 164
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
196 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,77 +1,220 @@ | ||
package azure | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/netip" | ||
"net/url" | ||
|
||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity" | ||
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" | ||
"github.com/qdm12/ddns-updater/internal/provider/constants" | ||
"github.com/qdm12/ddns-updater/internal/provider/errors" | ||
"github.com/qdm12/ddns-updater/internal/provider/headers" | ||
"github.com/qdm12/ddns-updater/internal/provider/utils" | ||
) | ||
|
||
func (p *Provider) createClient() (client *armdns.RecordSetsClient, err error) { | ||
credential, err := azidentity.NewClientSecretCredential(p.tenantID, p.clientID, p.clientSecret, nil) | ||
type rrSet struct { | ||
ID string `json:"id"` | ||
Etag string `json:"etag"` | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
Properties struct { | ||
Metadata map[string]string `json:"metadata"` | ||
TTL uint32 `json:"TTL"` | ||
FQDN string `json:"fqdn"` | ||
ARecords []arecord `json:"ARecords"` | ||
AAAARecords []aaaarecord `json:"AAAARecords"` | ||
} `json:"properties"` | ||
} | ||
|
||
type arecord struct { | ||
IPv4Address string `json:"ipv4Address"` | ||
} | ||
|
||
type aaaarecord struct { | ||
IPv6Address string `json:"ipv6Address"` | ||
} | ||
|
||
func makeURL(subscriptionID, resourceGroupName, domain, recordType, host string) string { | ||
path := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/dnsZones/%s/%s/%s", | ||
subscriptionID, resourceGroupName, domain, recordType, host) | ||
values := url.Values{} | ||
values.Set("api-version", "2018-05-01") | ||
u := url.URL{ | ||
Scheme: "https", | ||
Host: "management.azure.com", | ||
Path: path, | ||
RawQuery: values.Encode(), | ||
} | ||
return u.String() | ||
} | ||
|
||
func (p *Provider) getRecordSet(ctx context.Context, client *http.Client, | ||
recordType string) (data rrSet, err error) { | ||
url := makeURL(p.subscriptionID, p.resourceGroupName, p.domain, recordType, p.host) | ||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("creating client secret credential: %w", err) | ||
return data, err | ||
} | ||
headers.SetUserAgent(request) | ||
headers.SetAuthBearer(request, p.token) | ||
headers.SetAccept(request, "application/json") | ||
|
||
client, err = armdns.NewRecordSetsClient(p.subscriptionID, credential, nil) | ||
response, err := client.Do(request) | ||
if err != nil { | ||
return nil, fmt.Errorf("creating record sets client: %w", err) | ||
return data, err | ||
} | ||
|
||
return client, nil | ||
} | ||
switch response.StatusCode { | ||
case http.StatusOK: | ||
case http.StatusNotFound: | ||
return data, fmt.Errorf("%w: %s %s", | ||
errors.ErrRecordNotFound, p.host, recordType) | ||
default: | ||
message := decodeError(response.Body) | ||
_ = response.Body.Close() | ||
return data, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, | ||
response.StatusCode, message) | ||
} | ||
|
||
func (p *Provider) getRecordSet(ctx context.Context, client *armdns.RecordSetsClient, | ||
recordType armdns.RecordType) (response armdns.RecordSetsClientGetResponse, err error) { | ||
return client.Get(ctx, p.resourceGroupName, p.domain, p.host, recordType, nil) | ||
decoder := json.NewDecoder(response.Body) | ||
err = decoder.Decode(&data) | ||
_ = response.Body.Close() | ||
if err != nil { | ||
return data, fmt.Errorf("JSON decoding response: %w", err) | ||
} | ||
|
||
return data, nil | ||
} | ||
|
||
func (p *Provider) createRecordSet(ctx context.Context, client *armdns.RecordSetsClient, | ||
func (p *Provider) createRecordSet(ctx context.Context, client *http.Client, | ||
ip netip.Addr) (err error) { | ||
rrSet := armdns.RecordSet{Properties: &armdns.RecordSetProperties{}} | ||
recordType := armdns.RecordTypeA | ||
var data rrSet | ||
recordType := constants.A | ||
if ip.Is4() { | ||
rrSet.Properties.ARecords = []*armdns.ARecord{{IPv4Address: ptrTo(ip.String())}} | ||
data.Properties.ARecords = []arecord{{IPv4Address: ip.String()}} | ||
} else { | ||
recordType = armdns.RecordTypeAAAA | ||
rrSet.Properties.AaaaRecords = []*armdns.AaaaRecord{{IPv6Address: ptrTo(ip.String())}} | ||
recordType = constants.AAAA | ||
data.Properties.AAAARecords = []aaaarecord{{IPv6Address: ip.String()}} | ||
} | ||
|
||
buffer := bytes.NewBuffer(nil) | ||
encoder := json.NewEncoder(buffer) | ||
err = encoder.Encode(data) | ||
if err != nil { | ||
return fmt.Errorf("JSON encoding request body: %w", err) | ||
} | ||
|
||
url := makeURL(p.subscriptionID, p.resourceGroupName, p.domain, recordType, p.host) | ||
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodPatch, url, buffer) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = client.CreateOrUpdate(ctx, p.resourceGroupName, p.domain, | ||
p.host, recordType, rrSet, nil) | ||
headers.SetUserAgent(request) | ||
headers.SetAuthBearer(request, p.token) | ||
headers.SetContentType(request, "application/json") | ||
headers.SetAccept(request, "application/json") | ||
|
||
response, err := client.Do(request) | ||
if err != nil { | ||
return fmt.Errorf("creating record set: %w", err) | ||
return err | ||
} | ||
|
||
if response.StatusCode != http.StatusOK { | ||
message := decodeError(response.Body) | ||
_ = response.Body.Close() | ||
return fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, | ||
response.StatusCode, message) | ||
} | ||
|
||
err = response.Body.Close() | ||
if err != nil { | ||
return fmt.Errorf("closing response body: %w", err) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (p *Provider) updateRecordSet(ctx context.Context, client *armdns.RecordSetsClient, | ||
response armdns.RecordSetsClientGetResponse, ip netip.Addr) (err error) { | ||
properties := response.Properties | ||
recordType := armdns.RecordTypeA | ||
func (p *Provider) updateRecordSet(ctx context.Context, client *http.Client, | ||
data rrSet, ip netip.Addr) (err error) { | ||
recordType := constants.A | ||
if ip.Is4() { | ||
if len(properties.ARecords) == 0 { | ||
properties.ARecords = make([]*armdns.ARecord, 1) | ||
if len(data.Properties.ARecords) == 0 { | ||
data.Properties.ARecords = make([]arecord, 1) | ||
} | ||
for i := range properties.ARecords { | ||
properties.ARecords[i].IPv4Address = ptrTo(ip.String()) | ||
for i := range data.Properties.ARecords { | ||
data.Properties.ARecords[i].IPv4Address = ip.String() | ||
} | ||
data.Properties.ARecords = []arecord{{IPv4Address: ip.String()}} | ||
} else { | ||
recordType = armdns.RecordTypeAAAA | ||
if len(properties.AaaaRecords) == 0 { | ||
properties.AaaaRecords = make([]*armdns.AaaaRecord, 1) | ||
recordType = constants.AAAA | ||
if len(data.Properties.AAAARecords) == 0 { | ||
data.Properties.AAAARecords = make([]aaaarecord, 1) | ||
} | ||
for i := range properties.AaaaRecords { | ||
properties.AaaaRecords[i].IPv6Address = ptrTo(ip.String()) | ||
for i := range data.Properties.AAAARecords { | ||
data.Properties.AAAARecords[i].IPv6Address = ip.String() | ||
} | ||
} | ||
rrSet := armdns.RecordSet{ | ||
Etag: response.Etag, | ||
Properties: properties, | ||
|
||
buffer := bytes.NewBuffer(nil) | ||
encoder := json.NewEncoder(buffer) | ||
err = encoder.Encode(data) | ||
if err != nil { | ||
return fmt.Errorf("JSON encoding request body: %w", err) | ||
} | ||
url := makeURL(p.subscriptionID, p.resourceGroupName, p.domain, recordType, p.host) | ||
request, err := http.NewRequestWithContext(ctx, http.MethodPatch, url, buffer) | ||
if err != nil { | ||
return err | ||
} | ||
headers.SetUserAgent(request) | ||
headers.SetAuthBearer(request, p.token) | ||
headers.SetContentType(request, "application/json") | ||
headers.SetAccept(request, "application/json") | ||
|
||
response, err := client.Do(request) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if response.StatusCode != http.StatusOK { | ||
message := decodeError(response.Body) | ||
_ = response.Body.Close() | ||
return fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, | ||
response.StatusCode, message) | ||
} | ||
|
||
err = response.Body.Close() | ||
if err != nil { | ||
return fmt.Errorf("closing response body: %w", err) | ||
} | ||
|
||
_, err = client.CreateOrUpdate(ctx, p.resourceGroupName, p.domain, | ||
p.host, recordType, rrSet, nil) | ||
return err | ||
return nil | ||
} | ||
|
||
func decodeError(body io.ReadCloser) (message string) { | ||
type cloudErrorBody struct { | ||
Code string `json:"code"` | ||
Message string `json:"message"` | ||
Target string `json:"target"` | ||
Details []cloudErrorBody `json:"details"` | ||
} | ||
var errorBody struct { | ||
Error cloudErrorBody `json:"error"` | ||
} | ||
b, err := io.ReadAll(body) | ||
if err != nil { | ||
return err.Error() | ||
} | ||
err = json.Unmarshal(b, &errorBody) | ||
_ = body.Close() | ||
if err != nil { | ||
return utils.ToSingleLine(string(b)) | ||
} | ||
return fmt.Sprintf("%s: %s (target: %s)", | ||
errorBody.Error.Code, errorBody.Error.Message, errorBody.Error.Target) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters