From 99d2af76becde2a8269356abcb998b689bec762b Mon Sep 17 00:00:00 2001 From: Gun Store Date: Thu, 26 Nov 2020 00:23:05 +0000 Subject: [PATCH] WIP - tests working independently Refactored to be able to use credentials from yaml file Updated DynuClient updated customDNSProviderConfig --- dynuclient/dynuclient.go | 34 +++------- dynuclient/dynuclient_test.go | 10 +-- dynuclient/models.go | 10 ++- go.mod | 2 + go.sum | 9 +++ main.go | 95 ++++++++++++++++++++------- main_test.go | 16 ----- testdata/config.json | 3 - testdata/secret-dynu-credentials.yaml | 8 +-- 9 files changed, 108 insertions(+), 79 deletions(-) diff --git a/dynuclient/dynuclient.go b/dynuclient/dynuclient.go index 5ba087f..532ac71 100644 --- a/dynuclient/dynuclient.go +++ b/dynuclient/dynuclient.go @@ -18,14 +18,14 @@ var httpClient *http.Client // CreateDNSRecord ... Create a DNS Record and return it's ID // POST https://api.dynu.com/v2/dns/{DNSID}/record func (c *DynuClient) CreateDNSRecord(records DNSRecord) (int, error) { - dnsURL := fmt.Sprintf("%s/dns/%d/record", dynuAPI, c.DNSID) + dnsURL := fmt.Sprintf("%s/dns/%s/record", dynuAPI, c.DNSID) body, err := json.Marshal(records) if err != nil { return -1, err } var resp *http.Response - resp, err = c.MakeRequest(dnsURL, "POST", bytes.NewReader(body)) + resp, err = c.makeRequest(dnsURL, "POST", bytes.NewReader(body)) if err != nil { return -1, err } @@ -52,10 +52,10 @@ func (c *DynuClient) CreateDNSRecord(records DNSRecord) (int, error) { // RemoveDNSRecord ... Removes a DNS record based on dnsRecordID // DELETE https://api.dynu.com/v2/dns/{DNSID}/record/{DNSRecordID} func (c *DynuClient) RemoveDNSRecord(DNSRecordID int) error { - dnsURL := fmt.Sprintf("%s/dns/%d/record/%d", dynuAPI, c.DNSID, DNSRecordID) + dnsURL := fmt.Sprintf("%s/dns/%s/record/%d", dynuAPI, c.DNSID, DNSRecordID) var resp *http.Response - resp, err := c.MakeRequest(dnsURL, "DELETE", nil) + resp, err := c.makeRequest(dnsURL, "DELETE", nil) if err != nil { return err } @@ -67,29 +67,27 @@ func (c *DynuClient) RemoveDNSRecord(DNSRecordID int) error { return nil } -// MakeRequest ... -func (c *DynuClient) MakeRequest(URL string, method string, body io.Reader) (*http.Response, error) { +func (c *DynuClient) makeRequest(URL string, method string, body io.Reader) (*http.Response, error) { req, err := http.NewRequest(method, URL, body) if err != nil { return nil, err } req.Header["accept"] = []string{"application/json"} - req.Header["User-Agent"] = []string{"Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0"} //c.UserAgent) + req.Header["User-Agent"] = []string{c.UserAgent} req.Header["Content-Type"] = []string{"application/json"} - req.Header["API-Key"] = []string{c.APISecret} + req.Header["API-Key"] = []string{c.APIKey} if c.HTTPClient == nil { c.HTTPClient = &http.Client{} } - c.getHTTPClient().Timeout = 30 * time.Second + c.HTTPClient.Timeout = 30 * time.Second - return c.getHTTPClient().Do(req) + return c.HTTPClient.Do(req) } -// DecodeBytes ... -func (c *DynuClient) DecodeBytes(input []byte) (string, error) { +func (c *DynuClient) decodeBytes(input []byte) (string, error) { buf := new(strings.Builder) _, err := io.Copy(buf, bytes.NewBuffer(input)) @@ -99,15 +97,3 @@ func (c *DynuClient) DecodeBytes(input []byte) (string, error) { return buf.String(), nil } - -func (c *DynuClient) getHTTPClient() *http.Client { - if httpClient != nil { - return httpClient - } - if c.HTTPClient != nil { - httpClient = c.HTTPClient - } else { - httpClient = &http.Client{} - } - return httpClient -} diff --git a/dynuclient/dynuclient_test.go b/dynuclient/dynuclient_test.go index 0194b07..e522d28 100644 --- a/dynuclient/dynuclient_test.go +++ b/dynuclient/dynuclient_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" "testing" guntest "github.com/gstore/cert-manager-webhook-dynu/test" @@ -30,8 +31,8 @@ var i int // } func TestRemoveDNSRecord(t *testing.T) { expectedMethod := "DELETE" - dnsID := 1 - expectedURL := fmt.Sprintf("/v2/dns/%d/record/12345", dnsID) + dnsID := "1" + expectedURL := fmt.Sprintf("/v2/dns/%s/record/12345", dnsID) testHandlerFunc := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { assert.Equal(t, expectedURL, req.URL.String(), "Should call %s but called %s", expectedURL, req.URL.String()) @@ -85,8 +86,7 @@ func TestCreateDNSRecord(t *testing.T) { client := &guntest.Testclient{} httpClient, teardown := client.TestingHTTPClient(testHandlerFunc) defer teardown() - - dynu := DynuClient{HTTPClient: httpClient, DNSID: domainID} + dynu := DynuClient{HTTPClient: httpClient, DNSID: strconv.Itoa(domainID)} recordID, err := dynu.CreateDNSRecord(rec) if err != nil { fmt.Println("an error occured: ", err.Error()) @@ -109,7 +109,7 @@ func TestAddAndRemoveRecord(t *testing.T) { State: true, } - d := &DynuClient{DNSID: dnsID, APISecret: apiKey} + d := &DynuClient{DNSID: strconv.Itoa(dnsID), APIKey: apiKey} dnsrecordid, err := d.CreateDNSRecord(rec) assert.NoError(t, err) diff --git a/dynuclient/models.go b/dynuclient/models.go index cb17fa0..f1cb422 100644 --- a/dynuclient/models.go +++ b/dynuclient/models.go @@ -31,8 +31,14 @@ type DNSResponse struct { // DynuClient ... options for DynuClient type DynuClient struct { HTTPClient *http.Client - DNSID int + DNSID string DNSRecordID int UserAgent string - APISecret string + APIKey string +} + +// DynuCreds - Details required to access API +type DynuCreds struct { + APIKey string + DNSID string } diff --git a/go.mod b/go.mod index e281e00..8666587 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( github.com/jetstack/cert-manager v1.0.4 github.com/stretchr/testify v1.6.1 gitlab.com/smueller18/cert-manager-webhook-inwx v0.3.0 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect k8s.io/apiextensions-apiserver v0.19.0 + k8s.io/apimachinery v0.19.0 k8s.io/client-go v0.19.0 ) diff --git a/go.sum b/go.sum index 009cfa9..2002740 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blang/semver v3.5.0+incompatible h1:CGxCgetQ64DKk7rdZ++Vfnb1+ogGNnB17OJKJXD2Cfs= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bobappleyard/readline v0.0.0-20150707195538-7e300e02d38e h1:4G8AYOOwZdDWOiJR6D6JXaFmj5BDS7c5D5PyqsG/+Hg= +github.com/bobappleyard/readline v0.0.0-20150707195538-7e300e02d38e/go.mod h1:fmqtV+Wqx0uFYLN1F4VhjZdtT56Dr8c3yA7nALFsw/Q= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -352,6 +354,8 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGi github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/koyachi/go-term-ansicolor v0.0.0-20130114081603-6f81280f9360 h1:RAav20p/CzLXPs8DxPry4eM4YuTooJ1d6ctCmXjpaA8= +github.com/koyachi/go-term-ansicolor v0.0.0-20130114081603-6f81280f9360/go.mod h1:zUllqwUpvIS0pyapVJDiHwwEkmaV9qeCR9+sh5MPdRs= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -407,6 +411,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nathany/looper v0.3.3 h1:WRMKUgJt4jeU44DrMkeuyM/ueBrt/PfXuxx3lAGp7h8= +github.com/nathany/looper v0.3.3/go.mod h1:lAOncmeiijTmAX17hI41yptNBp880ShD1nObDNEaUm0= github.com/nrdcg/goinwx v0.6.1/go.mod h1:XPiut7enlbEdntAqalBIqcYcTEVhpv/dKWgDCX2SwKQ= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -677,6 +683,7 @@ golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002091554-b397fe3ad8ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -689,6 +696,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 281277a..049f895 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "fmt" "net/http" @@ -16,8 +17,12 @@ import ( // "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util" "github.com/gstore/cert-manager-webhook-dynu/dynuclient" + certmgrv1 "github.com/jetstack/cert-manager/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const acmeNode = "ACME node" + var ( dnsRecordID int ) @@ -77,12 +82,11 @@ type customDNSProviderConfig struct { //Email string `json:"email"` // APIKeySecretRef v1alpha1.SecretKeySelector `json:"apiKeySecretRef"` - APIKey string `json:"apiKey"` - DomainID string `json:"domainId"` - BaseURL string `json:"baseURL"` - EndPoint string `json:"endPoint"` - Production bool `json:"production"` - TTL int `json:"ttl"` + APIKey string `json:"apiKey"` + DomainID string `json:"domainId"` + TTL int `json:"ttl"` + APIKeySecretKeyRef certmgrv1.SecretKeySelector `json:"apikeySecretKeyRef"` + DomainIDKeyRef certmgrv1.SecretKeySelector `json:"domainidSecretKeyRef"` } // Name is used as the name for this DNS solver when referencing it on the ACME @@ -101,27 +105,23 @@ func (c *customDNSProviderSolver) Name() string { // cert-manager itself will later perform a self check to ensure that the // solver has correctly configured the DNS provider. func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { - fmt.Println("before config: ", ch) - cfg, err := loadConfig(ch.Config) + + dynu, cfg, err := c.NewDynuClient(ch) if err != nil { return err } - fmt.Println("cfg: ", cfg) rec := dynuclient.DNSRecord{ - NodeName: "asgard", + NodeName: acmeNode, RecordType: "TXT", - TextData: "some text", - TTL: "90", + TextData: ch.Key, + TTL: strconv.Itoa(cfg.TTL), } - domainID, err := strconv.Atoi(cfg.DomainID) - - dynu := &dynuclient.DynuClient{DNSID: domainID, APISecret: cfg.APIKey, HTTPClient: c.httpClient} + // &dynuclient.DynuClient{DNSID: cfg.DomainID, APIKey: cfg.APIKey, HTTPClient: c.httpClient} dnsRecordID, err = dynu.CreateDNSRecord(rec) if err != nil { return err } - return nil } @@ -132,11 +132,10 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error { // This is in order to facilitate multiple DNS validations for the same domain // concurrently. func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { - fmt.Println("before config: ", ch) - cfg, err := loadConfig(ch.Config) - fmt.Println("cfg: ", cfg) - domainID, err := strconv.Atoi(cfg.DomainID) - dynu := &dynuclient.DynuClient{DNSID: domainID, APISecret: cfg.APIKey, HTTPClient: c.httpClient} + dynu, _, err := c.NewDynuClient(ch) + if err != nil { + return err + } err = dynu.RemoveDNSRecord(dnsRecordID) if err != nil { return err @@ -156,12 +155,10 @@ func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { func (c *customDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { ///// UNCOMMENT THE BELOW CODE TO MAKE A KUBERNETES CLIENTSET AVAILABLE TO ///// YOUR CUSTOM DNS PROVIDER - fmt.Println("init") cl, err := kubernetes.NewForConfig(kubeClientConfig) if err != nil { return err } - fmt.Println("init:", cl) c.client = *cl ///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLEuri := cfg.BaseURL + cfg.DomainId + "/" + cfg.EndPoint @@ -182,3 +179,55 @@ func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) { return cfg, nil } + +func (c *customDNSProviderSolver) getCredentials(config *customDNSProviderConfig, ns string) (*dynuclient.DynuCreds, error) { + + creds := dynuclient.DynuCreds{} + + if config.APIKey != "" { + creds.APIKey = config.APIKey + } else { + secret, err := c.client.CoreV1().Secrets(ns).Get(context.Background(), config.APIKeySecretKeyRef.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to load secret %q", ns+"/"+config.APIKeySecretKeyRef.Name) + } + if apikey, ok := secret.Data[config.APIKeySecretKeyRef.Key]; ok { + creds.APIKey = string(apikey) + } else { + return nil, fmt.Errorf("no key %q in secret %q", config.APIKeySecretKeyRef, ns+"/"+config.APIKeySecretKeyRef.Name) + } + } + + if config.DomainID != "" { + creds.DNSID = config.DomainID + } else { + secret, err := c.client.CoreV1().Secrets(ns).Get(context.Background(), config.DomainIDKeyRef.Name, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to load secret %q", ns+"/"+config.DomainIDKeyRef.Name) + } + if password, ok := secret.Data[config.DomainIDKeyRef.Key]; ok { + creds.DNSID = string(password) + } else { + return nil, fmt.Errorf("no key %q in secret %q", config.DomainIDKeyRef, ns+"/"+config.DomainIDKeyRef.Name) + } + } + + return &creds, nil +} + +// NewDynuClient - Create a new DynuClient +func (c *customDNSProviderSolver) NewDynuClient(ch *v1alpha1.ChallengeRequest) (*dynuclient.DynuClient, *customDNSProviderConfig, error) { + cfg, err := loadConfig(ch.Config) + if err != nil { + return nil, &cfg, err + } + + creds, err := c.getCredentials(&cfg, ch.ResourceNamespace) + if err != nil { + return nil, &cfg, fmt.Errorf("error getting credentials: %v", err) + } + + client := *&dynuclient.DynuClient{DNSID: creds.DNSID, APIKey: creds.APIKey, HTTPClient: c.httpClient} + + return &client, &cfg, nil +} diff --git a/main_test.go b/main_test.go index eceeb38..7c5b8f6 100644 --- a/main_test.go +++ b/main_test.go @@ -28,23 +28,7 @@ var ( fqdn string ) -// RoundTripFunc . -type RoundTripFunc func(req *http.Request) *http.Response - -// RoundTrip . -func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { - return f(req), nil -} - -//NewTestClient returns *http.Client with Transport replaced to avoid making real calls -func NewTestClient(fn RoundTripFunc) *http.Client { - return &http.Client{ - Transport: RoundTripFunc(fn), - } -} - func TestRunsSuite(t *testing.T) { - t.Skip() // The manifest path should contain a file named config.json that is a // snippet of valid configuration that should be included on the // ChallengeRequest passed as part of the test cases. diff --git a/testdata/config.json b/testdata/config.json index a352e45..fe991bc 100644 --- a/testdata/config.json +++ b/testdata/config.json @@ -1,8 +1,5 @@ { "APIKey": "secretkey", "DomainID": "123456", - "BaseURL": "https://api.dynu.com/v2/dns/", - "Endpoint": "record", - "Production": false, "TTL": 600 } \ No newline at end of file diff --git a/testdata/secret-dynu-credentials.yaml b/testdata/secret-dynu-credentials.yaml index 1824d77..25e86ab 100644 --- a/testdata/secret-dynu-credentials.yaml +++ b/testdata/secret-dynu-credentials.yaml @@ -3,9 +3,5 @@ kind: Secret metadata: name: dynu-credentials data: - APIKey: "secretkey" - DomainID: "123456" - BaseURL: "https://api.dynu.com/v2/dns/" - Endpoint: "record" - Production: "false" - TTL: "600" \ No newline at end of file + APIKeySecretKeyRef: "" + DomainIDKeyRef: "" \ No newline at end of file