template-solver/main.go
2019-12-01 21:33:24 +01:00

242 lines
5.7 KiB
Go

// 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
}