blob: a08b6ef4c735cb571d324e147859c4784d7d963e [file] [log] [blame]
Giorgi Lekveishviliae1a4a42023-12-07 13:23:17 +04001package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "strings"
10
11 extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
12 "k8s.io/client-go/kubernetes"
13 "k8s.io/client-go/rest"
14
15 "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
16 "github.com/cert-manager/cert-manager/pkg/acme/webhook/cmd"
17)
18
19const groupName = "dodo.cloud"
20
21func main() {
22 if groupName == "" {
23 panic("GROUP_NAME must be specified")
24 }
25 cmd.RunWebhookServer(groupName,
26 &pcloudDNSProviderSolver{},
27 )
28}
29
30type ZoneManager interface {
31 CreateTextRecord(domain, entry, txt string) error
32 DeleteTextRecord(domain, entry, txt string) error
33}
34
35type zoneControllerManager struct {
36 CreateAddr string
37 DeleteAddr string
38}
39
40type createTextRecordReq struct {
41 Domain string `json:"domain,omitempty"`
42 Entry string `json:"entry,omitempty"`
43 Text string `json:"text,omitempty"`
44}
45
46const contentTypeApplicationJSON = "application/json"
47
48func (m *zoneControllerManager) CreateTextRecord(domain, entry, txt string) error {
49 req := createTextRecordReq{domain, entry, txt}
50 var buf bytes.Buffer
51 if err := json.NewEncoder(&buf).Encode(req); err != nil {
52 return err
53 }
54 if resp, err := http.Post(m.CreateAddr, contentTypeApplicationJSON, &buf); err != nil {
55 return err
56 } else if resp.StatusCode != http.StatusOK {
57 var b strings.Builder
58 io.Copy(&b, resp.Body)
59 return fmt.Errorf("Create text record failed: %d %s", resp.StatusCode, b.String())
60 }
61 return nil
62}
63
64func (m *zoneControllerManager) DeleteTextRecord(domain, entry, txt string) error {
65 req := createTextRecordReq{domain, entry, txt}
66 var buf bytes.Buffer
67 if err := json.NewEncoder(&buf).Encode(req); err != nil {
68 return err
69 }
70 if resp, err := http.Post(m.DeleteAddr, contentTypeApplicationJSON, &buf); err != nil {
71 return err
72 } else if resp.StatusCode != http.StatusOK {
73 var b strings.Builder
74 io.Copy(&b, resp.Body)
75 return fmt.Errorf("Delete text record failed: %d %s", resp.StatusCode, b.String())
76 }
77 return nil
78}
79
80type pcloudDNSProviderSolver struct {
81 // If a Kubernetes 'clientset' is needed, you must:
82 // 1. uncomment the additional `client` field in this structure below
83 // 2. uncomment the "k8s.io/client-go/kubernetes" import at the top of the file
84 // 3. uncomment the relevant code in the Initialize method below
85 // 4. ensure your webhook's service account has the required RBAC role
86 // assigned to it for interacting with the Kubernetes APIs you need.
87 client *kubernetes.Clientset
88}
89
90// customDNSProviderConfig is a structure that is used to decode into when
91// solving a DNS01 challenge.
92// This information is provided by cert-manager, and may be a reference to
93// additional configuration that's needed to solve the challenge for this
94// particular certificate or issuer.
95// This typically includes references to Secret resources containing DNS
96// provider credentials, in cases where a 'multi-tenant' DNS solver is being
97// created.
98// If you do *not* require per-issuer or per-certificate configuration to be
99// provided to your webhook, you can skip decoding altogether in favour of
100// using CLI flags or similar to provide configuration.
101// You should not include sensitive information here. If credentials need to
102// be used by your provider here, you should reference a Kubernetes Secret
103// resource and fetch these credentials using a Kubernetes clientset.
104type pcloudDNSProviderConfig struct {
105 CreateAddress string `json:"createAddress,omitempty"`
106 DeleteAddress string `json:"deleteAddress,omitempty"`
107}
108
109// Name is used as the name for this DNS solver when referencing it on the ACME
110// Issuer resource.
111// This should be unique **within the group name**, i.e. you can have two
112// solvers configured with the same Name() **so long as they do not co-exist
113// within a single webhook deployment**.
114// For example, `cloudflare` may be used as the name of a solver.
115func (c *pcloudDNSProviderSolver) Name() string {
116 return "pcloud-dns-solver"
117}
118
119// Present is responsible for actually presenting the DNS record with the
120// DNS provider.
121// This method should tolerate being called multiple times with the same value.
122// cert-manager itself will later perform a self check to ensure that the
123// solver has correctly configured the DNS provider.
124func (c *pcloudDNSProviderSolver) Present(ch *v1alpha1.ChallengeRequest) error {
125 fmt.Printf("Received challenge %+v\n", ch)
126 cfg, err := loadConfig(ch.Config)
127 if err != nil {
128 fmt.Printf("")
129 return err
130 }
131 zm := &zoneControllerManager{cfg.CreateAddress, cfg.DeleteAddress}
132 domain, entry := getDomainAndEntry(ch)
133 return zm.CreateTextRecord(domain, entry, ch.Key)
134}
135
136// CleanUp should delete the relevant TXT record from the DNS provider console.
137// If multiple TXT records exist with the same record name (e.g.
138// _acme-challenge.example.com) then **only** the record with the same `key`
139// value provided on the ChallengeRequest should be cleaned up.
140// This is in order to facilitate multiple DNS validations for the same domain
141// concurrently.
142func (c *pcloudDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
143 cfg, err := loadConfig(ch.Config)
144 if err != nil {
145 return err
146 }
147 zm := &zoneControllerManager{cfg.CreateAddress, cfg.DeleteAddress}
148 domain, entry := getDomainAndEntry(ch)
149 return zm.DeleteTextRecord(domain, entry, ch.Key)
150}
151
152// Initialize will be called when the webhook first starts.
153// This method can be used to instantiate the webhook, i.e. initialising
154// connections or warming up caches.
155// Typically, the kubeClientConfig parameter is used to build a Kubernetes
156// client that can be used to fetch resources from the Kubernetes API, e.g.
157// Secret resources containing credentials used to authenticate with DNS
158// provider accounts.
159// The stopCh can be used to handle early termination of the webhook, in cases
160// where a SIGTERM or similar signal is sent to the webhook process.
161func (c *pcloudDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
162 fmt.Println("Initialization start")
163 client, err := kubernetes.NewForConfig(kubeClientConfig)
164 if err != nil {
165 return err
166 }
167 c.client = client
168 fmt.Println("Initialization done")
169 return nil
170}
171
172// loadConfig is a small helper function that decodes JSON configuration into
173// the typed config struct.
174func loadConfig(cfgJSON *extapi.JSON) (pcloudDNSProviderConfig, error) {
175 cfg := pcloudDNSProviderConfig{}
176 // handle the 'base case' where no configuration has been provided
177 if cfgJSON == nil {
178 return cfg, nil
179 }
180 if err := json.Unmarshal(cfgJSON.Raw, &cfg); err != nil {
181 return cfg, fmt.Errorf("error decoding solver config: %v", err)
182 }
183
184 return cfg, nil
185}
186
187func getDomainAndEntry(ch *v1alpha1.ChallengeRequest) (string, string) {
188 // Both ch.ResolvedZone and ch.ResolvedFQDN end with a dot: '.'
189 entry := strings.TrimSuffix(ch.ResolvedFQDN, ch.ResolvedZone)
190 entry = strings.TrimSuffix(entry, ".")
191 domain := strings.TrimSuffix(ch.ResolvedZone, ".")
192 return domain, entry
193}