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