blob: 905573592dd0ac262ee4048710d82292459fd5a9 [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 {
Giorgi Lekveishvili9e2fafa2023-12-18 14:12:34 +0400146 fmt.Printf("Failed to load API config: %s\n", err.Error())
Giorgi Lekveishvili5c2c0b92023-12-07 17:35:40 +0400147 return err
148 }
Giorgi Lekveishvili9e2fafa2023-12-18 14:12:34 +0400149 fmt.Printf("API config: %+v\n", apiCfg)
Giorgi Lekveishvili5c2c0b92023-12-07 17:35:40 +0400150 zm := &zoneControllerManager{apiCfg.CreateAddress, apiCfg.DeleteAddress}
Giorgi Lekveishviliae1a4a42023-12-07 13:23:17 +0400151 domain, entry := getDomainAndEntry(ch)
Giorgi Lekveishvili9e2fafa2023-12-18 14:12:34 +0400152 fmt.Printf("%s %s\n", domain, entry)
153 err = zm.CreateTextRecord(domain, entry, ch.Key)
154 if err != nil {
155 fmt.Printf("Failed to create TXT record: %s\n", err.Error())
156 return err
157 }
158 return nil
Giorgi Lekveishviliae1a4a42023-12-07 13:23:17 +0400159}
160
161// CleanUp should delete the relevant TXT record from the DNS provider console.
162// If multiple TXT records exist with the same record name (e.g.
163// _acme-challenge.example.com) then **only** the record with the same `key`
164// value provided on the ChallengeRequest should be cleaned up.
165// This is in order to facilitate multiple DNS validations for the same domain
166// concurrently.
167func (c *pcloudDNSProviderSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error {
168 cfg, err := loadConfig(ch.Config)
169 if err != nil {
170 return err
171 }
Giorgi Lekveishvili5c2c0b92023-12-07 17:35:40 +0400172 apiCfg, err := loadAPIConfig(c.client, cfg)
173 if err != nil {
Giorgi Lekveishvili9e2fafa2023-12-18 14:12:34 +0400174 fmt.Printf("Failed to load API config: %s\n", err.Error())
Giorgi Lekveishvili5c2c0b92023-12-07 17:35:40 +0400175 return err
176 }
Giorgi Lekveishvili9e2fafa2023-12-18 14:12:34 +0400177 fmt.Printf("API config: %+v\n", apiCfg)
Giorgi Lekveishvili5c2c0b92023-12-07 17:35:40 +0400178 zm := &zoneControllerManager{apiCfg.CreateAddress, apiCfg.DeleteAddress}
Giorgi Lekveishviliae1a4a42023-12-07 13:23:17 +0400179 domain, entry := getDomainAndEntry(ch)
Giorgi Lekveishvili9e2fafa2023-12-18 14:12:34 +0400180 err = zm.DeleteTextRecord(domain, entry, ch.Key)
181 if err != nil {
182 fmt.Printf("Failed to delete TXT record: %s\n", err.Error())
183 return err
184 }
185 return nil
Giorgi Lekveishviliae1a4a42023-12-07 13:23:17 +0400186}
187
188// Initialize will be called when the webhook first starts.
189// This method can be used to instantiate the webhook, i.e. initialising
190// connections or warming up caches.
191// Typically, the kubeClientConfig parameter is used to build a Kubernetes
192// client that can be used to fetch resources from the Kubernetes API, e.g.
193// Secret resources containing credentials used to authenticate with DNS
194// provider accounts.
195// The stopCh can be used to handle early termination of the webhook, in cases
196// where a SIGTERM or similar signal is sent to the webhook process.
197func (c *pcloudDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
198 fmt.Println("Initialization start")
199 client, err := kubernetes.NewForConfig(kubeClientConfig)
200 if err != nil {
201 return err
202 }
203 c.client = client
204 fmt.Println("Initialization done")
205 return nil
206}
207
208// loadConfig is a small helper function that decodes JSON configuration into
209// the typed config struct.
210func loadConfig(cfgJSON *extapi.JSON) (pcloudDNSProviderConfig, error) {
211 cfg := pcloudDNSProviderConfig{}
212 // handle the 'base case' where no configuration has been provided
213 if cfgJSON == nil {
214 return cfg, nil
215 }
216 if err := json.Unmarshal(cfgJSON.Raw, &cfg); err != nil {
217 return cfg, fmt.Errorf("error decoding solver config: %v", err)
218 }
219
220 return cfg, nil
221}
222
Giorgi Lekveishvili5c2c0b92023-12-07 17:35:40 +0400223func loadAPIConfig(client *kubernetes.Clientset, cfg pcloudDNSProviderConfig) (apiConfig, error) {
224 config, err := client.CoreV1().ConfigMaps(cfg.APIConfigMapNamespace).Get(context.Background(), cfg.APIConfigMapName, metav1.GetOptions{})
225 if err != nil {
226 return apiConfig{}, fmt.Errorf("unable to get api config map `%s` `%s`; %v", cfg.APIConfigMapName, cfg.APIConfigMapNamespace, err)
227 }
Giorgi Lekveishvili1d587042023-12-08 10:49:26 +0400228 create, ok := config.Data["createTXTAddr"]
Giorgi Lekveishvili5c2c0b92023-12-07 17:35:40 +0400229 if !ok {
230 return apiConfig{}, fmt.Errorf("create address missing")
231 }
Giorgi Lekveishvili1d587042023-12-08 10:49:26 +0400232 delete, ok := config.Data["deleteTXTAddr"]
Giorgi Lekveishvili5c2c0b92023-12-07 17:35:40 +0400233 if !ok {
234 return apiConfig{}, fmt.Errorf("delete address missing")
235 }
236 return apiConfig{create, delete}, nil
237}
238
Giorgi Lekveishviliae1a4a42023-12-07 13:23:17 +0400239func getDomainAndEntry(ch *v1alpha1.ChallengeRequest) (string, string) {
240 // Both ch.ResolvedZone and ch.ResolvedFQDN end with a dot: '.'
Giorgi Lekveishvili9e2fafa2023-12-18 14:12:34 +0400241 resolvedFQDN := strings.TrimSuffix(ch.ResolvedFQDN, ".")
242 domain := strings.Join(strings.Split(strings.TrimSuffix(ch.DNSName, "."), ".")[1:], ".")
243 entry := strings.TrimSuffix(resolvedFQDN, domain)
244 return strings.TrimSuffix(domain, "."), strings.TrimSuffix(entry, ".")
Giorgi Lekveishviliae1a4a42023-12-07 13:23:17 +0400245}