242 lines
5.7 KiB
Go
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
|
|
}
|