// Copyright (C) 2019 Marius Schellenberger package main import ( "bytes" "encoding/json" "fmt" "net" "net/http" "os" "sync" "text/template" "time" "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" "github.com/jetstack/cert-manager/pkg/acme/webhook/cmd" corev1 "k8s.io/api/core/v1" extapiv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" extapiv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) var GroupName = os.Getenv("GROUP_NAME") func main() { if GroupName == "" { panic("GROUP_NAME must be specified") } cmd.RunWebhookServer(GroupName, &TemplateSolver{}, ) } type TemplateSolver struct { sync.Mutex cl *kubernetes.Clientset cache map[string]struct{} } type TemplateSolverConfig struct { // `issuer.spec.acme.dns01.providers.webhook.config` field. APIKeySecretRef corev1.SecretKeySelector `json:"apiKeySecretRef"` URLTemplate string `json:"urlTemplate"` URLData *extapiv1.JSON `json:"urlData,omitempty"` Headers *extapiv1.JSON `json:"headers,omitempty"` BodyTemplate string `json:"bodyTemplate,omitempty"` BodyData *extapiv1.JSON `json:"bodyData,omitempty"` } type TemplateConfig struct { SecretName string SecretKey string Headers map[string]string URLTemp *template.Template BodyTemp *template.Template Data TemplateData } type TemplateData struct { DNSKey string // DNS Key APIKey string // API Key URLData interface{} // URLData BodyData interface{} // BodyData } func (*TemplateSolver) Name() string { return "template-solver" } func loadConfig(cfg *extapiv1beta1.JSON) (*TemplateConfig, error) { if cfg == nil { return nil, fmt.Errorf("error missing solver config") } var c TemplateSolverConfig err := json.Unmarshal(cfg.Raw, &c) if err != nil { return nil, fmt.Errorf("error decoding solver config: %v", err) } if c.URLTemplate == "" { return nil, fmt.Errorf("error empty urlTemplate") } tc := &TemplateConfig{ SecretName: c.APIKeySecretRef.Name, SecretKey: c.APIKeySecretRef.Key, Headers: make(map[string]string), Data: TemplateData{}, } tc.URLTemp, err = template.New("url").Parse(c.URLTemplate) if err != nil { return tc, fmt.Errorf("error parsing urlTemplate: %v", err) } if c.URLData == nil { return tc, fmt.Errorf("error missing urlData field") } if err = json.Unmarshal(c.URLData.Raw, &tc.Data.URLData); err != nil { return tc, fmt.Errorf("error decoding urlData: %v", err) } if c.Headers != nil { if err = json.Unmarshal(c.Headers.Raw, &tc.Headers); err != nil { return tc, fmt.Errorf("error decoding headers: %v", err) } } if c.BodyTemplate != "" { tc.BodyTemp, err = template.New("body").Parse(c.BodyTemplate) if err != nil { return tc, fmt.Errorf("error parsing bodyTemplate: %v", err) } if c.BodyData == nil { return tc, fmt.Errorf("error missing bodyData field") } if err = json.Unmarshal(c.BodyData.Raw, &tc.Data.BodyData); err != nil { return tc, fmt.Errorf("error decoding bodyData: %v", err) } } return tc, nil } func request(tc *TemplateConfig) error { var ( body bytes.Buffer url bytes.Buffer err error ) if tc.BodyTemp != nil { if err = tc.BodyTemp.Execute(&body, &tc.Data); err != nil { return fmt.Errorf("error executing bodyTemplate: %v", err) } } if err = tc.URLTemp.Execute(&url, &tc.Data); err != nil { return fmt.Errorf("error executing urlTemplate: %v", err) } var req *http.Request if body.Len() == 0 { req, err = http.NewRequest("GET", url.String(), nil) if err != nil { return fmt.Errorf("error constructing GET request: %v", err) } } else { req, err = http.NewRequest("POST", url.String(), &body) if err != nil { return fmt.Errorf("error constructing POST request: %v", err) } } for k, v := range tc.Headers { req.Header.Set(k, v) } c := &http.Client{Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: 30 * time.Second, }).DialContext, }} _, err = c.Do(req) if err != nil { return fmt.Errorf("error response: %v", err) } return nil } func (ts *TemplateSolver) getSecret(tc *TemplateConfig, ns string) error { s, err := ts.cl.CoreV1().Secrets(ns).Get(tc.SecretName, metav1.GetOptions{}) if err != nil { return err } key, ok := s.Data[tc.SecretKey] if !ok { return fmt.Errorf("no api key for %q in secret '%s/%s'", tc.SecretKey, ns, tc.SecretName) } tc.Data.APIKey = string(key) return nil } func (ts *TemplateSolver) addKey(k string) bool { ts.Lock() _, ok := ts.cache[k] if ok { return ok } ts.cache[k] = struct{}{} ts.Unlock() return false } func (ts *TemplateSolver) removeKey(k string) { ts.Lock() delete(ts.cache, k) ts.Unlock() } func (ts *TemplateSolver) Present(ch *v1alpha1.ChallengeRequest) error { if ts.addKey(ch.Key) { return nil } tc, err := loadConfig(ch.Config) if err != nil { ts.removeKey(ch.Key) return err } tc.Data.DNSKey = ch.Key err = ts.getSecret(tc, ch.ResourceNamespace) if err != nil { ts.removeKey(ch.Key) return err } err = request(tc) if err != nil { ts.removeKey(ch.Key) } return err } func (ts *TemplateSolver) CleanUp(ch *v1alpha1.ChallengeRequest) error { ts.removeKey(ch.Key) tc, err := loadConfig(ch.Config) if err != nil { return err } err = ts.getSecret(tc, ch.ResourceNamespace) if err != nil { return err } return request(tc) } func (ts *TemplateSolver) Initialize(cfg *rest.Config, stopCh <-chan struct{}) error { cl, err := kubernetes.NewForConfig(cfg) if err != nil { return err } ts.cl = cl ts.cache = make(map[string]struct{}) return nil }