forked from mirrors/cert-manager-webhook-variomedia
created webhook for Variomedia AG API (2019+)
This commit is contained in:
387
variomediaclient_api2019.go
Normal file
387
variomediaclient_api2019.go
Normal file
@@ -0,0 +1,387 @@
|
||||
// client implementation for Variomedia API, 2019+ version (https://api.variomedia.de/docs/)
|
||||
//
|
||||
// Written by Jens-Uwe Mozdzen <jmozdzen@nde.ag>
|
||||
//
|
||||
// Licensed under LGPL v3
|
||||
//
|
||||
// Entry points:
|
||||
// client = NewvariomediaClient( apikey)
|
||||
// - create new instance of API client
|
||||
// in:
|
||||
// apikey - customer-specific API key issued by Variomedia
|
||||
// returns:
|
||||
// client object
|
||||
//
|
||||
//
|
||||
// client.UpdateTxtRecord(&domain, &entry, Key, ttl)
|
||||
// - update TXT record
|
||||
// in:
|
||||
// domain - DNS domain
|
||||
// entry - host label
|
||||
// key - valus of TXT record
|
||||
// ttl - TTL of record
|
||||
// returns:
|
||||
// variomediaDNSEntryURL - the URL of the resulting DNS entry
|
||||
//
|
||||
// client.DeleteTxtRecord(&domain, &entry)
|
||||
// - delete TXT record for entry/domain
|
||||
// in:
|
||||
// url - DNS entry's URL
|
||||
// ttl - TTL of record
|
||||
// returns:
|
||||
// -
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
variomediaLiveDnsBaseUrl = "https://api.variomedia.de/dns-records"
|
||||
statusLookupDelay = 2 * time.Second
|
||||
)
|
||||
|
||||
type variomediaClient struct {
|
||||
apiKey string
|
||||
}
|
||||
|
||||
type variomediaDnsAttributes struct {
|
||||
RecordType string `json:"record_type"`
|
||||
Name string `json:"name"`
|
||||
Domain string `json:"domain"`
|
||||
Data string `json:"data"`
|
||||
Ttl int `json:"ttl"`
|
||||
} // variomediaDnsAttributes
|
||||
|
||||
type variomediaData struct {
|
||||
RequestType string `json:"type"`
|
||||
RequestAttr variomediaDnsAttributes `json:"attributes"`
|
||||
} // variomediaData
|
||||
|
||||
type variomediaRequest struct {
|
||||
variomediaData `json:"data"`
|
||||
} // variomediaRequest
|
||||
|
||||
type variomediaJobData struct {
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Attributes map[string]string `json:"attributes"`
|
||||
Links map[string]string `json:"links"`
|
||||
} // variomediaJobData
|
||||
|
||||
type variomediaResponse struct {
|
||||
Data variomediaJobData `json:"data"`
|
||||
Links map[string]string `json:"links"`
|
||||
} // variomediaRequest
|
||||
|
||||
// NewvariomediaClient()
|
||||
// create new instance of Variomedia client
|
||||
func NewvariomediaClient(apiKey string) *variomediaClient {
|
||||
klog.V(4).InfoS("NewvariomediaClient() called")
|
||||
klog.V(5).InfoS("parameters", "API key", apiKey)
|
||||
|
||||
klog.V(4).InfoS("NewvariomediaClient() finished")
|
||||
return &variomediaClient{
|
||||
apiKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
// client.UpdateTxtRecord(&domain, &entry, Key, ttl)
|
||||
// - create or update TXT record
|
||||
// in:
|
||||
// domain - DNS domain
|
||||
// entry - host label
|
||||
// key - valus of TXT record
|
||||
// ttl - TTL of record
|
||||
// returns:
|
||||
// variomediaDNSEntryURL - the URL of the resulting DNS entry
|
||||
func (c *variomediaClient) UpdateTxtRecord(domain *string, name *string, value *string, ttl int) (string, error) {
|
||||
klog.V(4).InfoS("UpdateTxtRecord() called")
|
||||
klog.V(5).InfoS("parameters", "domain", *domain, "name", *name, "value", *value, "TTL", ttl)
|
||||
|
||||
var reqData variomediaRequest
|
||||
|
||||
reqData = variomediaRequest{ variomediaData{
|
||||
RequestType: "dns-record",
|
||||
RequestAttr: variomediaDnsAttributes{
|
||||
RecordType: "TXT",
|
||||
Name: *name,
|
||||
Domain: *domain,
|
||||
Data: *value,
|
||||
Ttl: ttl,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// the actual request is encoded in JSON
|
||||
body, err := json.Marshal( reqData)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
|
||||
return "", fmt.Errorf("cannot marshall to json: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", variomediaLiveDnsBaseUrl, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
|
||||
return "", err
|
||||
}
|
||||
|
||||
// contact Variomedia and check the results
|
||||
status, respData, err := c.doRequest(req, true)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
|
||||
return "", err
|
||||
}
|
||||
|
||||
// have we hit the rate limit?
|
||||
if status == http.StatusTooManyRequests {
|
||||
klog.V(2).ErrorS( fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests), "UpdateTxtRecord() finished with error")
|
||||
return "", fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
|
||||
}
|
||||
|
||||
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted {
|
||||
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
|
||||
return "", fmt.Errorf("failed creating TXT record: %v", err)
|
||||
}
|
||||
|
||||
// the request has succeeded - but is the job already finished?
|
||||
// check the response for an according '' element
|
||||
var reply variomediaResponse
|
||||
err = json.Unmarshal( respData, &reply)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
|
||||
return "", fmt.Errorf("cannot unmarshall response to json: %v", err)
|
||||
}
|
||||
klog.V(5).InfoS( "HTTP finished", "JSON reply", reply)
|
||||
|
||||
// we give it 5 iterations to finish
|
||||
loopcount := 5
|
||||
for {
|
||||
if reply.Data.Attributes[ "status"] == "pending" {
|
||||
klog.V(2).InfoS( "DNS job still pending")
|
||||
|
||||
// inter-loop delay
|
||||
time.Sleep( statusLookupDelay)
|
||||
|
||||
// re-fetch the job status
|
||||
req, err := http.NewRequest("GET", reply.Data.Links[ "queue-job"], nil)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
|
||||
return "", err
|
||||
}
|
||||
|
||||
// contact Variomedia and check the results
|
||||
status, respData, err := c.doRequest(req, true)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
|
||||
return "", err
|
||||
}
|
||||
|
||||
// have we hit the rate limit?
|
||||
if status == http.StatusTooManyRequests {
|
||||
klog.V(2).ErrorS(fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests), "UpdateTxtRecord() finished with error")
|
||||
return "", fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
|
||||
}
|
||||
|
||||
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted {
|
||||
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
|
||||
return "", fmt.Errorf("failed creating TXT record: %v", err)
|
||||
}
|
||||
|
||||
// the request has succeeded - but is the job already finished?
|
||||
// check the response for an according '' element
|
||||
err = json.Unmarshal( respData, &reply)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "UpdateTxtRecord() finished with error")
|
||||
return "", fmt.Errorf("cannot unmarshall response to json: %v", err)
|
||||
}
|
||||
klog.V(5).InfoS( "HTTP finished", "JSON reply", reply)
|
||||
|
||||
}
|
||||
loopcount -= 1
|
||||
|
||||
if (reply.Data.Attributes[ "status"] == "done") {
|
||||
klog.V(2).InfoS( "DNS job finished", "retries left", loopcount)
|
||||
break;
|
||||
}
|
||||
if (loopcount == 0) {
|
||||
klog.V(2).ErrorS(fmt.Errorf("DNS update job timed out with most recent status '%s'", reply.Data.Attributes[ "status"]), "UpdateTxtRecord() finished with error")
|
||||
return "", fmt.Errorf("DNS update job timed out with most recent status '%s'", reply.Data.Attributes[ "status"])
|
||||
}
|
||||
} // emulated do until
|
||||
|
||||
klog.V(4).InfoS("UpdateTxtRecord() finished")
|
||||
klog.V(5).InfoS("return values", "url", reply.Data.Links[ "dns-record"])
|
||||
return reply.Data.Links[ "dns-record"], nil
|
||||
} //func UpdateTxtRecord()
|
||||
|
||||
// client.DeleteTxtRecord(&domain, &entry, Key, ttl)
|
||||
// - create or update TXT record
|
||||
// in:
|
||||
// url - DNS entry's URL
|
||||
// ttl - TTL of record
|
||||
// returns:
|
||||
// -
|
||||
func (c *variomediaClient) DeleteTxtRecord(url string, ttl int) error {
|
||||
klog.V(4).InfoS("DeleteTxtRecord() called")
|
||||
klog.V(5).InfoS("parameters", "url", url, "TTL", ttl)
|
||||
|
||||
// deleting a record happens by sending a HTTP "DELETE" request to the DNS entry's URL
|
||||
req, err := http.NewRequest("DELETE", url, nil)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
|
||||
return err
|
||||
}
|
||||
|
||||
// contact Variomedia and check the results
|
||||
status, respData, err := c.doRequest(req, true)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
|
||||
return err
|
||||
}
|
||||
|
||||
// have we hit the rate limit?
|
||||
if status == http.StatusTooManyRequests {
|
||||
klog.V(2).ErrorS(fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests), "DeleteTxtRecord() finished with error")
|
||||
return fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
|
||||
}
|
||||
|
||||
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted {
|
||||
klog.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
|
||||
return fmt.Errorf("failed deleting TXT record: %v", err)
|
||||
}
|
||||
|
||||
// the request has succeeded - but is the job already finished?
|
||||
// check the response for an according '' element
|
||||
var reply variomediaResponse
|
||||
err = json.Unmarshal( respData, &reply)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
|
||||
return fmt.Errorf("cannot unmarshall response to json: %v", err)
|
||||
}
|
||||
klog.V(5).InfoS( "HTTP finished", "JSON reply", reply)
|
||||
|
||||
// we give it 5 iterations to finish
|
||||
loopcount := 5
|
||||
for {
|
||||
if reply.Data.Attributes[ "status"] == "pending" && status != http.StatusNotFound {
|
||||
klog.V(2).InfoS( "DNS job still pending")
|
||||
|
||||
// inter-loop delay: two seconds
|
||||
time.Sleep( statusLookupDelay)
|
||||
|
||||
// re-fetch the job status
|
||||
req, err := http.NewRequest("GET", reply.Data.Links[ "queue-job"], nil)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
|
||||
return err
|
||||
}
|
||||
|
||||
// contact Variomedia and check the results
|
||||
status, respData, err := c.doRequest(req, true)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
|
||||
return err
|
||||
}
|
||||
|
||||
// have we hit the rate limit?
|
||||
if status == http.StatusTooManyRequests {
|
||||
klog.V(2).ErrorS(fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests), "DeleteTxtRecord() finished with error")
|
||||
return fmt.Errorf("Variomedia rate limit reached (HTTP code %d)", http.StatusTooManyRequests)
|
||||
}
|
||||
|
||||
if status != http.StatusCreated && status != http.StatusOK && status != http.StatusAccepted && status != http.StatusNotFound {
|
||||
klog.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
|
||||
return fmt.Errorf("failed creating TXT record: %v", err)
|
||||
}
|
||||
|
||||
// the request has succeeded - but is the job already finished?
|
||||
// check the response for an according '' element
|
||||
err = json.Unmarshal( respData, &reply)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "DeleteTxtRecord() finished with error")
|
||||
return fmt.Errorf("cannot unmarshall response to json: %v", err)
|
||||
}
|
||||
klog.V(5).InfoS( "HTTP finished", "JSON reply", reply)
|
||||
|
||||
}
|
||||
|
||||
// 404 means "DNS record not found" (anymore) - we're fine with that, the record is gone
|
||||
if status == http.StatusNotFound {
|
||||
klog.V(4).InfoS("DeleteTxtRecord() finished because DNS record is gone", "retries left", loopcount)
|
||||
return nil
|
||||
}
|
||||
|
||||
loopcount -= 1
|
||||
|
||||
if (reply.Data.Attributes[ "status"] == "done") {
|
||||
klog.V(2).InfoS( "DNS job finished", "retries left", loopcount)
|
||||
break;
|
||||
}
|
||||
if (loopcount == 0) {
|
||||
klog.V(2).ErrorS(fmt.Errorf("DNS update job timed out with most recent status '%s'", reply.Data.Attributes[ "status"]), "DeleteTxtRecord() finished with error")
|
||||
return fmt.Errorf("DNS update job timed out with most recent status '%s'", reply.Data.Attributes[ "status"])
|
||||
}
|
||||
} // emulated do until
|
||||
|
||||
klog.V(4).InfoS("DeleteTxtRecord() finished")
|
||||
return nil
|
||||
} // func DeleteTxtRecord()
|
||||
|
||||
|
||||
func (c *variomediaClient) variomediaRecordsUrl(domain string) string {
|
||||
klog.V(4).InfoS("variomediaRecordsUrl() called")
|
||||
klog.V(5).InfoS("parameters", "domain", domain)
|
||||
|
||||
urlpart := fmt.Sprintf("%s/dns-records", domain)
|
||||
|
||||
klog.V(4).InfoS("variomediaRecordsUrl() finished")
|
||||
klog.V(5).InfoS("return values", "url part", urlpart)
|
||||
return urlpart
|
||||
}
|
||||
|
||||
func (c *variomediaClient) doRequest(req *http.Request, readResponseBody bool) (int, []byte, error) {
|
||||
klog.V(4).InfoS("doRequest() called")
|
||||
klog.V(5).InfoS("parameters", "request", req, "readResponseBody", readResponseBody)
|
||||
|
||||
// Variomedia uses headers for auth, request content type and to signal accepted API versions
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.apiKey))
|
||||
req.Header.Set("Content-Type", "application/vnd.api+json")
|
||||
req.Header.Set("Accept", "application/vnd.variomedia.v1+json")
|
||||
|
||||
client := http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "doRequest() finished with error")
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
klog.V(5).InfoS( "HTTP request", "response", res)
|
||||
|
||||
// check for proper returns
|
||||
if (res.StatusCode == http.StatusOK || res.StatusCode == http.StatusAccepted) && readResponseBody {
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
klog.V(2).ErrorS(err, "HTTP request finished with error")
|
||||
return 0, nil, err
|
||||
}
|
||||
klog.V(4).InfoS( "HTTP request succeeded", "status code", res.StatusCode)
|
||||
klog.V(5).InfoS( "HTTP request result", "data", data)
|
||||
return res.StatusCode, data, nil
|
||||
}
|
||||
|
||||
klog.V(4).InfoS("doRequest() finished")
|
||||
klog.V(5).InfoS("return values", "status code", res.StatusCode)
|
||||
return res.StatusCode, nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user