mirror of
https://github.com/cert-manager/webhook-example.git
synced 2025-07-01 22:35:49 +02:00
feat: Add Sotoon solver
This commit is contained in:
parent
e57412778d
commit
03ea58ac3d
9 changed files with 214 additions and 299 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -17,3 +17,4 @@ cert-manager-webhook-example
|
|||
# Make artifacts
|
||||
_out
|
||||
_test
|
||||
sotoon-api-key.yaml
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
package example
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func (e *exampleSolver) handleDNSRequest(w dns.ResponseWriter, req *dns.Msg) {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetReply(req)
|
||||
switch req.Opcode {
|
||||
case dns.OpcodeQuery:
|
||||
for _, q := range msg.Question {
|
||||
if err := e.addDNSAnswer(q, msg, req); err != nil {
|
||||
msg.SetRcode(req, dns.RcodeServerFailure)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteMsg(msg)
|
||||
}
|
||||
|
||||
func (e *exampleSolver) addDNSAnswer(q dns.Question, msg *dns.Msg, req *dns.Msg) error {
|
||||
switch q.Qtype {
|
||||
// Always return loopback for any A query
|
||||
case dns.TypeA:
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN A 127.0.0.1", q.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Answer = append(msg.Answer, rr)
|
||||
return nil
|
||||
|
||||
// TXT records are the only important record for ACME dns-01 challenges
|
||||
case dns.TypeTXT:
|
||||
e.RLock()
|
||||
record, found := e.txtRecords[q.Name]
|
||||
e.RUnlock()
|
||||
if !found {
|
||||
msg.SetRcode(req, dns.RcodeNameError)
|
||||
return nil
|
||||
}
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN TXT %s", q.Name, record))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Answer = append(msg.Answer, rr)
|
||||
return nil
|
||||
|
||||
// NS and SOA are for authoritative lookups, return obviously invalid data
|
||||
case dns.TypeNS:
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN NS ns.example-acme-webook.invalid.", q.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Answer = append(msg.Answer, rr)
|
||||
return nil
|
||||
case dns.TypeSOA:
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s 5 IN SOA %s 20 5 5 5 5", "ns.example-acme-webook.invalid.", "ns.example-acme-webook.invalid."))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Answer = append(msg.Answer, rr)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unimplemented record type %v", q.Qtype)
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
// package example contains a self-contained example of a webhook that passes the cert-manager
|
||||
// DNS conformance tests
|
||||
package example
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/cert-manager/cert-manager/pkg/acme/webhook"
|
||||
acme "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
|
||||
"github.com/miekg/dns"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
type exampleSolver struct {
|
||||
name string
|
||||
server *dns.Server
|
||||
txtRecords map[string]string
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (e *exampleSolver) Name() string {
|
||||
return e.name
|
||||
}
|
||||
|
||||
func (e *exampleSolver) Present(ch *acme.ChallengeRequest) error {
|
||||
e.Lock()
|
||||
e.txtRecords[ch.ResolvedFQDN] = ch.Key
|
||||
e.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *exampleSolver) CleanUp(ch *acme.ChallengeRequest) error {
|
||||
e.Lock()
|
||||
delete(e.txtRecords, ch.ResolvedFQDN)
|
||||
e.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *exampleSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
|
||||
go func(done <-chan struct{}) {
|
||||
<-done
|
||||
if err := e.server.Shutdown(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
}
|
||||
}(stopCh)
|
||||
go func() {
|
||||
if err := e.server.ListenAndServe(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(port string) webhook.Solver {
|
||||
e := &exampleSolver{
|
||||
name: "example",
|
||||
txtRecords: make(map[string]string),
|
||||
}
|
||||
e.server = &dns.Server{
|
||||
Addr: ":" + port,
|
||||
Net: "udp",
|
||||
Handler: dns.HandlerFunc(e.handleDNSRequest),
|
||||
}
|
||||
return e
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
package example
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
acme "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExampleSolver_Name(t *testing.T) {
|
||||
port, _ := rand.Int(rand.Reader, big.NewInt(50000))
|
||||
port = port.Add(port, big.NewInt(15534))
|
||||
solver := New(port.String())
|
||||
assert.Equal(t, "example", solver.Name())
|
||||
}
|
||||
|
||||
func TestExampleSolver_Initialize(t *testing.T) {
|
||||
port, _ := rand.Int(rand.Reader, big.NewInt(50000))
|
||||
port = port.Add(port, big.NewInt(15534))
|
||||
solver := New(port.String())
|
||||
done := make(chan struct{})
|
||||
err := solver.Initialize(nil, done)
|
||||
assert.NoError(t, err, "Expected Initialize not to error")
|
||||
close(done)
|
||||
}
|
||||
|
||||
func TestExampleSolver_Present_Cleanup(t *testing.T) {
|
||||
port, _ := rand.Int(rand.Reader, big.NewInt(50000))
|
||||
port = port.Add(port, big.NewInt(15534))
|
||||
solver := New(port.String())
|
||||
done := make(chan struct{})
|
||||
err := solver.Initialize(nil, done)
|
||||
assert.NoError(t, err, "Expected Initialize not to error")
|
||||
|
||||
validTestData := []struct {
|
||||
hostname string
|
||||
record string
|
||||
}{
|
||||
{"test1.example.com.", "testkey1"},
|
||||
{"test2.example.com.", "testkey2"},
|
||||
{"test3.example.com.", "testkey3"},
|
||||
}
|
||||
for _, test := range validTestData {
|
||||
err := solver.Present(&acme.ChallengeRequest{
|
||||
Action: acme.ChallengeActionPresent,
|
||||
Type: "dns-01",
|
||||
ResolvedFQDN: test.hostname,
|
||||
Key: test.record,
|
||||
})
|
||||
assert.NoError(t, err, "Unexpected error while presenting %v", t)
|
||||
}
|
||||
|
||||
// Resolve test data
|
||||
for _, test := range validTestData {
|
||||
msg := new(dns.Msg)
|
||||
msg.Id = dns.Id()
|
||||
msg.RecursionDesired = true
|
||||
msg.Question = make([]dns.Question, 1)
|
||||
msg.Question[0] = dns.Question{dns.Fqdn(test.hostname), dns.TypeTXT, dns.ClassINET}
|
||||
in, err := dns.Exchange(msg, "127.0.0.1:"+port.String())
|
||||
|
||||
assert.NoError(t, err, "Presented record %s not resolvable", test.hostname)
|
||||
assert.Len(t, in.Answer, 1, "RR response is of incorrect length")
|
||||
assert.Equal(t, []string{test.record}, in.Answer[0].(*dns.TXT).Txt, "TXT record returned did not match presented record")
|
||||
}
|
||||
|
||||
// Cleanup test data
|
||||
for _, test := range validTestData {
|
||||
err := solver.CleanUp(&acme.ChallengeRequest{
|
||||
Action: acme.ChallengeActionCleanUp,
|
||||
Type: "dns-01",
|
||||
ResolvedFQDN: test.hostname,
|
||||
Key: test.record,
|
||||
})
|
||||
assert.NoError(t, err, "Unexpected error while cleaning up %v", t)
|
||||
}
|
||||
|
||||
// Resolve test data
|
||||
for _, test := range validTestData {
|
||||
msg := new(dns.Msg)
|
||||
msg.Id = dns.Id()
|
||||
msg.RecursionDesired = true
|
||||
msg.Question = make([]dns.Question, 1)
|
||||
msg.Question[0] = dns.Question{dns.Fqdn(test.hostname), dns.TypeTXT, dns.ClassINET}
|
||||
in, err := dns.Exchange(msg, "127.0.0.1:"+port.String())
|
||||
|
||||
assert.NoError(t, err, "Presented record %s not resolvable", test.hostname)
|
||||
assert.Len(t, in.Answer, 0, "RR response is of incorrect length")
|
||||
assert.Equal(t, dns.RcodeNameError, in.Rcode, "Expexted NXDOMAIN")
|
||||
}
|
||||
|
||||
close(done)
|
||||
}
|
249
main.go
249
main.go
|
@ -1,15 +1,21 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
|
||||
"github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd"
|
||||
v1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
|
||||
extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
var GroupName = os.Getenv("GROUP_NAME")
|
||||
|
@ -25,46 +31,37 @@ func main() {
|
|||
// webhook, where the Name() method will be used to disambiguate between
|
||||
// the different implementations.
|
||||
cmd.RunWebhookServer(GroupName,
|
||||
&customDNSProviderSolver{},
|
||||
&sotoonDNSProviderSolver{},
|
||||
)
|
||||
}
|
||||
|
||||
// customDNSProviderSolver implements the provider-specific logic needed to
|
||||
// 'present' an ACME challenge TXT record for your own DNS provider.
|
||||
// To do so, it must implement the `github.com/cert-manager/cert-manager/pkg/acme/webhook.Solver`
|
||||
// sotoonDNSProviderSolver implements Sotoon DNS logic needed to
|
||||
// 'present' an ACME challenge TXT record. To do so, it must implement
|
||||
// the `github.com/cert-manager/cert-manager/pkg/acme/webhook.Solver`
|
||||
// interface.
|
||||
type customDNSProviderSolver struct {
|
||||
type sotoonDNSProviderSolver struct {
|
||||
// If a Kubernetes 'clientset' is needed, you must:
|
||||
// 1. uncomment the additional `client` field in this structure below
|
||||
// 2. uncomment the "k8s.io/client-go/kubernetes" import at the top of the file
|
||||
// 3. uncomment the relevant code in the Initialize method below
|
||||
// 4. ensure your webhook's service account has the required RBAC role
|
||||
// assigned to it for interacting with the Kubernetes APIs you need.
|
||||
//client kubernetes.Clientset
|
||||
client *kubernetes.Clientset
|
||||
}
|
||||
|
||||
// customDNSProviderConfig is a structure that is used to decode into when
|
||||
// sootonNSProviderConfig is a structure that is used to decode into when
|
||||
// solving a DNS01 challenge.
|
||||
// This information is provided by cert-manager, and may be a reference to
|
||||
// additional configuration that's needed to solve the challenge for this
|
||||
// particular certificate or issuer.
|
||||
// This typically includes references to Secret resources containing DNS
|
||||
// provider credentials, in cases where a 'multi-tenant' DNS solver is being
|
||||
// created.
|
||||
// If you do *not* require per-issuer or per-certificate configuration to be
|
||||
// provided to your webhook, you can skip decoding altogether in favour of
|
||||
// using CLI flags or similar to provide configuration.
|
||||
// You should not include sensitive information here. If credentials need to
|
||||
// be used by your provider here, you should reference a Kubernetes Secret
|
||||
// resource and fetch these credentials using a Kubernetes clientset.
|
||||
type customDNSProviderConfig struct {
|
||||
// Change the two fields below according to the format of the configuration
|
||||
// to be decoded.
|
||||
// These fields will be set by users in the
|
||||
// This information is provided by cert-manager, and a reference to credentials
|
||||
// that's needed to add TXT record in Sotoon to solve the challenge for this
|
||||
// particular certificate. If credentials need to be used by your provider here,
|
||||
// you should reference a Kubernetes Secret resource and fetch these credentials
|
||||
// using a Kubernetes clientset.
|
||||
type sotoonDNSProviderConfig struct {
|
||||
// This field will be set by users in the
|
||||
// `issuer.spec.acme.dns01.providers.webhook.config` field.
|
||||
|
||||
//Email string `json:"email"`
|
||||
//APIKeySecretRef v1alpha1.SecretKeySelector `json:"apiKeySecretRef"`
|
||||
APIKeySecretRef v1.SecretKeySelector `json:"apiKeySecretRef"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
}
|
||||
|
||||
// Name is used as the name for this DNS solver when referencing it on the ACME
|
||||
|
@ -73,8 +70,105 @@ type customDNSProviderConfig struct {
|
|||
// solvers configured with the same Name() **so long as they do not co-exist
|
||||
// within a single webhook deployment**.
|
||||
// For example, `cloudflare` may be used as the name of a solver.
|
||||
func (c *customDNSProviderSolver) Name() string {
|
||||
return "my-custom-solver"
|
||||
func (s *sotoonDNSProviderSolver) Name() string {
|
||||
return "sotoon"
|
||||
}
|
||||
|
||||
// removeDotFromEnd will remove . at the end of resolvedZone
|
||||
// Sotoon doesn't recognize dot at the end of zone names.
|
||||
func removeDotFromEnd(resolvedZone string) string {
|
||||
if resolvedZone[len(resolvedZone)-1:] == "." {
|
||||
return resolvedZone[0 : len(resolvedZone)-1]
|
||||
}
|
||||
return resolvedZone
|
||||
}
|
||||
|
||||
// normalizeResolvedFQDN will remove resolvedZone from resolvedFQDN
|
||||
// Sotoon doesn't recognize full names that include zone in them and will consider them part
|
||||
// of the name. For example if `ResolvedFQDN` is "_acme-challenge.example.com" in "example.com"
|
||||
// zone the record will be "_acme-challenge.example.com.example.com", so we need to remove
|
||||
// the added ".example.com" from it.
|
||||
func normalizeResolvedFQDN(resolvedFQDN, resolvedZone string) string {
|
||||
if resolvedFQDN[len(resolvedFQDN)-len(resolvedZone):] == resolvedZone {
|
||||
// Example: resolvedFQDN = "_acme-challenge.example.com.", resolvedZone = "example.com."
|
||||
// resolvedFQDN[:len(resolvedFQDN)-len(resolvedZone)-1] = _acme-challenge
|
||||
return resolvedFQDN[:len(resolvedFQDN)-len(resolvedZone)-1]
|
||||
}
|
||||
return removeDotFromEnd(resolvedFQDN)
|
||||
}
|
||||
|
||||
// getApiKey retrieves apiKey from secret provided to solver in `APIKeySecretRef`
|
||||
func (s *sotoonDNSProviderSolver) getApiKey(secretNamespace string, secretRef v1.SecretKeySelector) (string, error) {
|
||||
secret, err := s.client.CoreV1().Secrets(secretNamespace).Get(
|
||||
context.Background(), secretRef.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("couldn't retrieve secret %s from namespace %s", secretRef.Name, secretNamespace)
|
||||
}
|
||||
|
||||
secretByteString, ok := secret.Data[secretRef.Key]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("couldn't fetch apikey from key %q of secret %s in namespace %s",
|
||||
secretRef.Key, secretRef.Name, secretNamespace)
|
||||
}
|
||||
|
||||
return string(secretByteString), nil
|
||||
}
|
||||
|
||||
// getAllRecords retrieves all records from Sotoon DNS and returns them in a map of string to interface{}
|
||||
func (s *sotoonDNSProviderSolver) getAllRecords(baseUrl, resolvedZone, apiKey string) (map[string]interface{}, error) {
|
||||
httpClient := &http.Client{}
|
||||
dnsUrl := fmt.Sprintf("%s/%s", baseUrl, resolvedZone)
|
||||
req, err := http.NewRequest("GET", dnsUrl, nil)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't create http request object with error %v", err)
|
||||
}
|
||||
|
||||
response, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request to Sotoon API failed with error %v", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't read Sotoon dns response's body with error %v", err)
|
||||
}
|
||||
|
||||
var dnsData map[string]interface{}
|
||||
err = json.Unmarshal(body, &dnsData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't parse Sotoon dns response's body %s", string(body))
|
||||
}
|
||||
|
||||
return dnsData, nil
|
||||
}
|
||||
|
||||
// dnsChangeQuery receives a payload in query in the format of `[{"op":"<op>","path":"<path>"}]` and
|
||||
// updates DNS records based on it. For more information look at the requests that ocean panel sends
|
||||
// to Sotoon's API.
|
||||
func (s *sotoonDNSProviderSolver) dnsChangeQuery(baseUrl, resolvedZone, apiKey, query string) error {
|
||||
httpClient := &http.Client{}
|
||||
dnsUrl := fmt.Sprintf("%s/%s", baseUrl, resolvedZone)
|
||||
patchPayload := []byte(query)
|
||||
req, err := http.NewRequest("PATCH", dnsUrl, bytes.NewBuffer(patchPayload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||||
req.Header.Set("Content-type", "application/json-patch+json")
|
||||
|
||||
response, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request to Sotoon API failed with error %v", err)
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return fmt.Errorf("couldn't change record in zone %s with response status code of: %d",
|
||||
resolvedZone, response.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Present is responsible for actually presenting the DNS record with the
|
||||
|
@ -82,16 +176,43 @@ func (c *customDNSProviderSolver) Name() string {
|
|||
// This method should tolerate being called multiple times with the same value.
|
||||
// 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 {
|
||||
func (s *sotoonDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
|
||||
cfg, err := loadConfig(ch.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: do something more useful with the decoded configuration
|
||||
fmt.Printf("Decoded configuration %v", cfg)
|
||||
apiKey, err := s.getApiKey(ch.ResourceNamespace, cfg.APIKeySecretRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dnsData, err := s.getAllRecords(cfg.BaseUrl, removeDotFromEnd(ch.ResolvedZone), apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
normalizedResolvedFQDN := normalizeResolvedFQDN(ch.ResolvedFQDN, ch.ResolvedZone)
|
||||
// Check whether the key is already present in DNS
|
||||
for recordName, records := range dnsData["spec"].(map[string]interface{})["records"].(map[string]interface{}) {
|
||||
if recordName == normalizedResolvedFQDN {
|
||||
for _, record := range records.([]interface{}) {
|
||||
val, ok := record.(map[string]interface{})["TXT"]
|
||||
if ok && val.(string) == ch.Key {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If code reaches here the key is not present and we should add a record containing that key
|
||||
patchPayload := fmt.Sprintf(`[{"op":"add","path":"/spec/records/%s","value":[{"TXT":"%s","ttl":300}]}]`,
|
||||
normalizedResolvedFQDN, ch.Key)
|
||||
err = s.dnsChangeQuery(cfg.BaseUrl, removeDotFromEnd(ch.ResolvedZone), apiKey, patchPayload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: add code that sets a record in the DNS provider's console
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -101,8 +222,43 @@ func (c *customDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
|
|||
// value provided on the ChallengeRequest should be cleaned up.
|
||||
// This is in order to facilitate multiple DNS validations for the same domain
|
||||
// concurrently.
|
||||
func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
|
||||
// TODO: add code that deletes a record from the DNS provider's console
|
||||
func (s *sotoonDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
|
||||
cfg, err := loadConfig(ch.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiKey, err := s.getApiKey(ch.ResourceNamespace, cfg.APIKeySecretRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dnsData, err := s.getAllRecords(cfg.BaseUrl, removeDotFromEnd(ch.ResolvedZone), apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find record and delete it
|
||||
normalizedResolvedFQDN := normalizeResolvedFQDN(ch.ResolvedFQDN, ch.ResolvedZone)
|
||||
for recordName, records := range dnsData["spec"].(map[string]interface{})["records"].(map[string]interface{}) {
|
||||
if recordName == normalizedResolvedFQDN {
|
||||
for recordIndex, record := range records.([]interface{}) {
|
||||
val, ok := record.(map[string]interface{})["TXT"]
|
||||
if ok && val.(string) == ch.Key {
|
||||
// Record found
|
||||
patchPayload := fmt.Sprintf(`[{"op":"remove","path":"/spec/records/%s/%d"}]`,
|
||||
normalizedResolvedFQDN, recordIndex)
|
||||
err := s.dnsChangeQuery(cfg.BaseUrl, removeDotFromEnd(ch.ResolvedZone), apiKey, patchPayload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("an error occurred while deleting DNS Record %s with value of %s",
|
||||
normalizedResolvedFQDN, ch.Key)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -115,25 +271,22 @@ func (c *customDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
|
|||
// provider accounts.
|
||||
// The stopCh can be used to handle early termination of the webhook, in cases
|
||||
// where a SIGTERM or similar signal is sent to the webhook process.
|
||||
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
|
||||
func (s *sotoonDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
|
||||
// Initializing kubernetes client to access credentials secret
|
||||
cl, err := kubernetes.NewForConfig(kubeClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//cl, err := kubernetes.NewForConfig(kubeClientConfig)
|
||||
//if err != nil {
|
||||
// return err
|
||||
//}
|
||||
//
|
||||
//c.client = cl
|
||||
s.client = cl
|
||||
|
||||
///// END OF CODE TO MAKE KUBERNETES CLIENTSET AVAILABLE
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadConfig is a small helper function that decodes JSON configuration into
|
||||
// the typed config struct.
|
||||
func loadConfig(cfgJSON *extapi.JSON) (customDNSProviderConfig, error) {
|
||||
cfg := customDNSProviderConfig{}
|
||||
func loadConfig(cfgJSON *extapi.JSON) (sotoonDNSProviderConfig, error) {
|
||||
cfg := sotoonDNSProviderConfig{}
|
||||
// handle the 'base case' where no configuration has been provided
|
||||
if cfgJSON == nil {
|
||||
return cfg, nil
|
||||
|
|
22
main_test.go
22
main_test.go
|
@ -5,8 +5,6 @@ import (
|
|||
"testing"
|
||||
|
||||
acmetest "github.com/cert-manager/cert-manager/test/acme"
|
||||
|
||||
"github.com/cert-manager/webhook-example/example"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -17,22 +15,12 @@ func TestRunsSuite(t *testing.T) {
|
|||
// 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.
|
||||
//
|
||||
|
||||
// Uncomment the below fixture when implementing your custom DNS provider
|
||||
//fixture := acmetest.NewFixture(&customDNSProviderSolver{},
|
||||
// acmetest.SetResolvedZone(zone),
|
||||
// acmetest.SetAllowAmbientCredentials(false),
|
||||
// acmetest.SetManifestPath("testdata/my-custom-solver"),
|
||||
// acmetest.SetBinariesPath("_test/kubebuilder/bin"),
|
||||
//)
|
||||
solver := example.New("59351")
|
||||
fixture := acmetest.NewFixture(solver,
|
||||
acmetest.SetResolvedZone("example.com."),
|
||||
acmetest.SetManifestPath("testdata/my-custom-solver"),
|
||||
acmetest.SetDNSServer("127.0.0.1:59351"),
|
||||
acmetest.SetUseAuthoritative(false),
|
||||
fixture := acmetest.NewFixture(&sotoonDNSProviderSolver{},
|
||||
acmetest.SetResolvedZone(zone),
|
||||
acmetest.SetAllowAmbientCredentials(false),
|
||||
acmetest.SetManifestPath("testdata/sotoon-solver"),
|
||||
)
|
||||
|
||||
//need to uncomment and RunConformance delete runBasic and runExtended once https://github.com/cert-manager/cert-manager/pull/4835 is merged
|
||||
//fixture.RunConformance(t)
|
||||
fixture.RunBasic(t)
|
||||
|
|
1
testdata/my-custom-solver/config.json
vendored
1
testdata/my-custom-solver/config.json
vendored
|
@ -1 +0,0 @@
|
|||
{}
|
7
testdata/sotoon-solver/config.json
vendored
Normal file
7
testdata/sotoon-solver/config.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"apiKeySecretRef": {
|
||||
"name": "sotoon-api-key",
|
||||
"key": "key"
|
||||
},
|
||||
"baseUrl": "https://api.delivery.sotoon.ir/apis/delivery.cafebazaar.cloud%2Fv1beta1/namespaces/yektanet/domainzones"
|
||||
}
|
Loading…
Reference in a new issue