Make HTTP challenge solver async

This commit is contained in:
James Munnelly 2018-03-12 14:29:01 +00:00
parent de59fc70ee
commit 7a44cb3e0e
7 changed files with 549 additions and 372 deletions

View File

@ -25,15 +25,15 @@ func (a *ACMEIssuerDNS01Config) Provider(name string) (*ACMEIssuerDNS01Provider,
return nil, fmt.Errorf("provider '%s' not found", name)
}
func (a *ACMECertificateConfig) ConfigForDomain(domain string) ACMECertificateDomainConfig {
func (a *ACMECertificateConfig) ConfigForDomain(domain string) *ACMECertificateDomainConfig {
for _, cfg := range a.Config {
for _, d := range cfg.Domains {
if d == domain {
return cfg
return &cfg
}
}
}
return ACMECertificateDomainConfig{}
return &ACMECertificateDomainConfig{}
}
func (c *CertificateStatus) ACMEStatus() *CertificateACMEStatus {

View File

@ -11,6 +11,7 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
corelisters "k8s.io/client-go/listers/core/v1"
extlisters "k8s.io/client-go/listers/extensions/v1beta1"
"k8s.io/client-go/tools/record"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
@ -32,7 +33,10 @@ type Acme struct {
cmClient clientset.Interface
recorder record.EventRecorder
secretsLister corelisters.SecretLister
secretsLister corelisters.SecretLister
podsLister corelisters.PodLister
servicesLister corelisters.ServiceLister
ingressLister extlisters.IngressLister
dnsSolver solver
httpSolver solver
@ -61,7 +65,10 @@ func New(issuer v1alpha1.GenericIssuer,
recorder record.EventRecorder,
resourceNamespace string,
acmeHTTP01SolverImage string,
secretsLister corelisters.SecretLister) (issuer.Interface, error) {
secretsLister corelisters.SecretLister,
podsLister corelisters.PodLister,
servicesLister corelisters.ServiceLister,
ingressLister extlisters.IngressLister) (issuer.Interface, error) {
if issuer.GetSpec().ACME == nil {
return nil, fmt.Errorf("acme config may not be empty")
}
@ -77,13 +84,17 @@ func New(issuer v1alpha1.GenericIssuer,
}
return &Acme{
issuer: issuer,
client: client,
cmClient: cmClient,
recorder: recorder,
secretsLister: secretsLister,
issuer: issuer,
client: client,
cmClient: cmClient,
recorder: recorder,
secretsLister: secretsLister,
podsLister: podsLister,
servicesLister: servicesLister,
ingressLister: ingressLister,
dnsSolver: dns.NewSolver(issuer, client, secretsLister, resourceNamespace),
httpSolver: http.NewSolver(issuer, client, secretsLister, acmeHTTP01SolverImage),
httpSolver: http.NewSolver(issuer, client, podsLister, servicesLister, ingressLister, acmeHTTP01SolverImage),
issuerResourcesNamespace: resourceNamespace,
}, nil
}
@ -147,6 +158,9 @@ func init() {
issuerResourcesNamespace,
ctx.ACMEHTTP01SolverImage,
ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister(),
ctx.KubeSharedInformerFactory.Core().V1().Pods().Lister(),
ctx.KubeSharedInformerFactory.Core().V1().Services().Lister(),
ctx.KubeSharedInformerFactory.Extensions().V1beta1().Ingresses().Lister(),
)
})
}

View File

@ -7,25 +7,16 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/golang/glog"
corev1 "k8s.io/api/core/v1"
extv1beta1 "k8s.io/api/extensions/v1beta1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/ingress/core/pkg/ingress/annotations/class"
extv1beta1listers "k8s.io/client-go/listers/extensions/v1beta1"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/issuer/acme/http/solver"
"github.com/jetstack/cert-manager/pkg/util"
"github.com/jetstack/cert-manager/pkg/util/kube"
)
const (
@ -34,357 +25,53 @@ const (
HTTP01Timeout = time.Minute * 15
// acmeSolverListenPort is the port acmesolver should listen on
acmeSolverListenPort = 8089
// orderURLLabelKey is the key used for the order URL label on resources
// created by the HTTP01 solver
orderURLLabelKey = "certmanager.k8s.io/acme-order-url"
domainLabelKey = "certmanager.k8s.io/acme-http-domain"
)
// svcNameFunc returns the name for the service to solve the challenge
func svcNameFunc(crtName, domain string) string {
return dns1035(fmt.Sprintf("cm-%s-%s", crtName, util.RandStringRunes(5)))
}
// ingNameFunc returns the name for the ingress to solve the challenge
func ingNameFunc(crtName, domain string) string {
return dns1035(fmt.Sprintf("cm-%s-%s", crtName, util.RandStringRunes(5)))
}
func podNameFunc(crtName, domain string) string {
return dns1035(fmt.Sprintf("cm-%s-%s", crtName, util.RandStringRunes(5)))
}
// Solver is an implementation of the acme http-01 challenge solver protocol
type Solver struct {
issuer v1alpha1.GenericIssuer
client kubernetes.Interface
secretLister corev1listers.SecretLister
solverImage string
issuer v1alpha1.GenericIssuer
client kubernetes.Interface
solverImage string
podLister corev1listers.PodLister
serviceLister corev1listers.ServiceLister
ingressLister extv1beta1listers.IngressLister
testReachability reachabilityTest
requiredPasses int
// This is a hack to record the randomly generated names of resources
// created by this Solver. This should be refactored out in future with a
// redesign of this package. It is used so resources can be cleaned up
// during the call to Cleanup()
svcNames map[string]string
ingNames map[string]string
podNames map[string]string
// lock is used to lock the solver
lock sync.Mutex
}
type reachabilityTest func(ctx context.Context, domain, path, key string) (bool, error)
// NewSolver returns a new ACME HTTP01 solver for the given Issuer and client.
func NewSolver(issuer v1alpha1.GenericIssuer, client kubernetes.Interface, secretLister corev1listers.SecretLister, solverImage string) *Solver {
// TODO: refactor this to have fewer args
func NewSolver(issuer v1alpha1.GenericIssuer, client kubernetes.Interface, podLister corev1listers.PodLister, serviceLister corev1listers.ServiceLister, ingressLister extv1beta1listers.IngressLister, solverImage string) *Solver {
return &Solver{
issuer: issuer,
client: client,
secretLister: secretLister,
podLister: podLister,
serviceLister: serviceLister,
ingressLister: ingressLister,
solverImage: solverImage,
testReachability: testReachability,
requiredPasses: 5,
svcNames: make(map[string]string),
ingNames: make(map[string]string),
podNames: make(map[string]string),
}
}
// labelsForCert returns some labels to add to resources related to the given
// Certificate.
// TODO: move this somewhere 'general', so that other control loops can filter
// their watches based on these labels and save watching *all* resource types.
func labelsForCert(crt *v1alpha1.Certificate, domain string) map[string]string {
return map[string]string{
"certmanager.k8s.io/managed": "true",
"certmanager.k8s.io/domain": domain,
"certmanager.k8s.io/certificate": crt.Name,
"certmanager.k8s.io/id": util.RandStringRunes(5),
}
}
func dns1035(s string) string {
return strings.Replace(s, ".", "-", -1)
}
// createService will create the service required to solve this challenge
// in the target API server.
func (s *Solver) createService(crt *v1alpha1.Certificate, domain string, labels map[string]string) (*corev1.Service, error) {
svcName := svcNameFunc(crt.Name, domain)
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: svcName,
Namespace: crt.Namespace,
Labels: labels,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeNodePort,
Ports: []corev1.ServicePort{
{
Name: "http",
Port: acmeSolverListenPort,
TargetPort: intstr.FromInt(acmeSolverListenPort),
},
},
Selector: labels,
},
}
svc, err := s.client.CoreV1().Services(crt.Namespace).Create(svc)
if err != nil {
return nil, err
}
s.svcNames[domain] = svcName
return svc, nil
}
// cleanupService will ensure the service created for this challenge request
// does not exist.
func (s *Solver) cleanupService(crt *v1alpha1.Certificate, domain string) error {
var svcName string
var ok bool
if svcName, ok = s.svcNames[domain]; !ok {
// no service to cleanup
return nil
}
err := s.client.CoreV1().Services(crt.Namespace).Delete(svcName, nil)
if err != nil && !k8sErrors.IsNotFound(err) {
return fmt.Errorf("error cleaning up service: %s", err.Error())
}
return nil
}
// ensureIngress will ensure the ingress required to solve this challenge
// exists.
func (s *Solver) ensureIngress(crt *v1alpha1.Certificate, svcName, domain, token string, labels map[string]string) (ing *extv1beta1.Ingress, err error) {
domainCfg := crt.Spec.ACME.ConfigForDomain(domain)
if existingIngressName := domainCfg.HTTP01.Ingress; existingIngressName != "" {
ing, err = s.ensureIngressHasRule(existingIngressName, crt, svcName, domain, token, nil)
} else {
ingName := ingNameFunc(crt.Name, domain)
s.ingNames[domain] = ingName
ing, err = s.ensureIngressHasRule(ingName, crt, svcName, domain, token, labels)
}
if err != nil {
return nil, err
}
return kube.EnsureIngress(s.client, ing)
}
// cleanupIngress will remove the rules added by cert-manager to an existing
// ingress, or delete the ingress if an existing ingress name is not specified
// on the certificate.
func (s *Solver) cleanupIngress(crt *v1alpha1.Certificate, svcName, domain, token string, labels map[string]string) error {
domainCfg := crt.Spec.ACME.ConfigForDomain(domain)
existingIngressName := domainCfg.HTTP01.Ingress
if existingIngressName == "" {
var ingName string
var ok bool
if ingName, ok = s.ingNames[domain]; !ok {
// no service to cleanup
return nil
}
err := s.client.ExtensionsV1beta1().Ingresses(crt.Namespace).Delete(ingName, nil)
if err != nil && !k8sErrors.IsNotFound(err) {
return fmt.Errorf("error cleaning up ingress: %s", err.Error())
}
return nil
}
ing, err := s.client.ExtensionsV1beta1().Ingresses(crt.Namespace).Get(existingIngressName, metav1.GetOptions{})
if err != nil && !k8sErrors.IsNotFound(err) {
return fmt.Errorf("error cleaning up ingress: %s", err.Error())
}
ingPathToDel := ingressPath(token, svcName)
Outer:
for _, rule := range ing.Spec.Rules {
if rule.Host == domain {
if rule.HTTP == nil {
return nil
}
for i, path := range rule.HTTP.Paths {
if path.Path == ingPathToDel.Path {
rule.HTTP.Paths = append(rule.HTTP.Paths[:i], rule.HTTP.Paths[i+1:]...)
break Outer
}
}
}
}
_, err = s.client.ExtensionsV1beta1().Ingresses(ing.Namespace).Update(ing)
if err != nil {
return fmt.Errorf("error cleaning up ingress: %s", err.Error())
}
return nil
}
// ensureIngressHasRule will return an Ingress resource that contains the rule
// required to solve the ACME challenge request for the given domain. If an
// ingress named `ingName` already exists, it will be updated to contain the
// required rule and returned. Otherwise, a new Ingress resource is returned.
func (s *Solver) ensureIngressHasRule(ingName string, crt *v1alpha1.Certificate, svcName, domain, token string, labels map[string]string) (ing *extv1beta1.Ingress, err error) {
domainCfg := crt.Spec.ACME.ConfigForDomain(domain)
ing, err = s.client.ExtensionsV1beta1().Ingresses(crt.Namespace).Get(ingName, metav1.GetOptions{})
if err != nil && !k8sErrors.IsNotFound(err) {
return nil, fmt.Errorf("error checking for existing ingress when ensuring ingress: %s", err.Error())
}
if ing == nil {
ing = &extv1beta1.Ingress{}
}
ing.Name = ingName
ing.Namespace = crt.Namespace
if ing.Annotations == nil {
ing.Annotations = make(map[string]string)
}
if domainCfg.HTTP01.IngressClass != nil {
ing.Annotations[class.IngressKey] = *domainCfg.HTTP01.IngressClass
}
if ing.Labels == nil {
ing.Labels = make(map[string]string)
}
for k, v := range labels {
ing.Labels[k] = v
}
ingPathToAdd := ingressPath(token, svcName)
for i, rule := range ing.Spec.Rules {
if rule.Host == domain {
http := rule.HTTP
if http == nil {
http = &extv1beta1.HTTPIngressRuleValue{}
ing.Spec.Rules[i].HTTP = http
}
http.Paths = append(http.Paths, ingPathToAdd)
return ing, nil
}
}
ing.Spec.Rules = append(ing.Spec.Rules, extv1beta1.IngressRule{
Host: domain,
IngressRuleValue: extv1beta1.IngressRuleValue{
HTTP: &extv1beta1.HTTPIngressRuleValue{
Paths: []extv1beta1.HTTPIngressPath{ingPathToAdd},
},
},
})
return ing, nil
}
// ingressPath returns the ingress HTTPIngressPath object needed to solve this
// challenge.
func ingressPath(token, serviceName string) extv1beta1.HTTPIngressPath {
return extv1beta1.HTTPIngressPath{
Path: fmt.Sprintf("%s/%s", solver.HTTPChallengePath, token),
Backend: extv1beta1.IngressBackend{
ServiceName: serviceName,
ServicePort: intstr.FromInt(acmeSolverListenPort),
},
}
}
// ensurePod will ensure the pod required to solve this challenge exists in the
// Kubernetes API server.
func (s *Solver) ensurePod(crt *v1alpha1.Certificate, domain, token, key string, labels map[string]string) (*corev1.Pod, error) {
podName := podNameFunc(crt.Name, domain)
err := s.client.CoreV1().Pods(crt.Namespace).Delete(podName, nil)
if err != nil && !k8sErrors.IsNotFound(err) {
return nil, fmt.Errorf("error removing old pod when creating new pod resource: %s", err.Error())
}
s.podNames[domain] = podName
return s.client.CoreV1().Pods(crt.Namespace).Create(&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: crt.Namespace,
Labels: labels,
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyOnFailure,
Containers: []corev1.Container{
{
Name: "acmesolver",
// TODO: use an image as specified as a config option
Image: s.solverImage,
ImagePullPolicy: corev1.PullIfNotPresent,
// TODO: replace this with some kind of cmdline generator
Args: []string{
fmt.Sprintf("--listen-port=%d", acmeSolverListenPort),
fmt.Sprintf("--domain=%s", domain),
fmt.Sprintf("--token=%s", token),
fmt.Sprintf("--key=%s", key),
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("10m"),
corev1.ResourceMemory: resource.MustParse("2Mi"),
},
},
Ports: []corev1.ContainerPort{
{
Name: "http",
ContainerPort: acmeSolverListenPort,
},
},
},
},
},
})
}
func (s *Solver) cleanupPod(crt *v1alpha1.Certificate, domain string) error {
var podName string
var ok bool
if podName, ok = s.podNames[domain]; !ok {
// no pod to cleanup
return nil
}
propPolicy := metav1.DeletePropagationBackground
err := s.client.CoreV1().Pods(crt.Namespace).Delete(podName, &metav1.DeleteOptions{
PropagationPolicy: &propPolicy,
})
if err != nil && !k8sErrors.IsNotFound(err) {
return fmt.Errorf("error cleaning up pod '%s': %s", podName, err.Error())
}
return nil
}
// Present will create the required service, update/create the required ingress
// and created a Kubernetes Pod to solve the HTTP01 challenge
// Present will realise the resources required to solve the given HTTP01
// challenge validation in the apiserver. If those resources already exist, it
// will return nil (i.e. this function is idempotent).
func (s *Solver) Present(ctx context.Context, crt *v1alpha1.Certificate, domain, token, key string) error {
s.lock.Lock()
defer s.lock.Unlock()
labels := labelsForCert(crt, domain)
svc, err := s.createService(crt, domain, labels)
if err != nil {
return fmt.Errorf("error ensuring http01 challenge service: %s", err.Error())
}
_, err = s.ensureIngress(crt, svc.Name, domain, token, labels)
if err != nil {
return fmt.Errorf("error ensuring http01 challenge ingress: %s", err.Error())
}
_, err = s.ensurePod(crt, domain, token, key, labels)
if err != nil {
return fmt.Errorf("error ensuring http01 challenge pod: %s", err.Error())
}
return nil
var errs []error
_, podErr := s.ensurePod(crt, domain, token, key)
_, svcErr := s.ensureService(crt, domain, token, key)
_, ingressErr := s.ensureIngress(crt, domain, token, key)
errs = append(errs, podErr, svcErr, ingressErr)
return utilerrors.NewAggregate(errs)
}
func (s *Solver) Check(domain, token, key string) (bool, error) {
@ -403,6 +90,20 @@ func (s *Solver) Check(domain, token, key string) (bool, error) {
return true, nil
}
// CleanUp will ensure the created service, ingress and pod are clean/deleted of any
// cert-manager created data.
func (s *Solver) CleanUp(ctx context.Context, crt *v1alpha1.Certificate, domain, token, key string) error {
var errs []error
errs = append(errs, s.cleanupPods(crt, domain))
errs = append(errs, s.cleanupServices(crt, domain))
errs = append(errs, s.cleanupIngresses(crt, domain, token))
return utilerrors.NewAggregate(errs)
}
func dns1035(s string) string {
return strings.Replace(s, ".", "-", -1)
}
// testReachability will attempt to connect to the 'domain' with 'path' and
// check if the returned body equals 'key'
func testReachability(ctx context.Context, domain, path, key string) (bool, error) {
@ -435,25 +136,3 @@ func testReachability(ctx context.Context, domain, path, key string) (bool, erro
return false, nil
}
// CleanUp will ensure the created service and ingress are clean/deleted of any
// cert-manager created data.
func (s *Solver) CleanUp(ctx context.Context, crt *v1alpha1.Certificate, domain, token, key string) error {
s.lock.Lock()
defer s.lock.Unlock()
var errs []error
if err := s.cleanupPod(crt, domain); err != nil {
errs = append(errs, fmt.Errorf("[%s] Error cleaning up pod: %s", domain, err.Error()))
}
if err := s.cleanupService(crt, domain); err != nil {
errs = append(errs, fmt.Errorf("[%s] Error cleaning up service: %s", domain, err.Error()))
}
if err := s.cleanupIngress(crt, svcNameFunc(crt.Name, domain), domain, token, labelsForCert(crt, domain)); err != nil {
errs = append(errs, fmt.Errorf("[%s] Error cleaning up ingress: %s", domain, err.Error()))
}
if errs != nil {
return utilerrors.NewAggregate(errs)
}
return nil
}

View File

@ -0,0 +1,227 @@
package http
import (
"fmt"
extv1beta1 "k8s.io/api/extensions/v1beta1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/ingress/core/pkg/ingress/annotations/class"
"github.com/golang/glog"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
"github.com/jetstack/cert-manager/pkg/issuer/acme/http/solver"
)
// getIngressesForCertificate returns a list of Ingresses that were created to solve
// http challenges for the given domain
func (s *Solver) getIngressesForCertificate(crt *v1alpha1.Certificate, domain string) ([]*extv1beta1.Ingress, error) {
if crt.Status.ACME.OrderURL == "" {
return []*extv1beta1.Ingress{}, nil
}
podLabels := podLabels(crt, domain)
selector := labels.NewSelector()
for key, val := range podLabels {
req, err := labels.NewRequirement(key, selection.Equals, []string{val})
if err != nil {
return nil, err
}
selector = selector.Add(*req)
}
ingressList, err := s.ingressLister.Ingresses(crt.Namespace).List(selector)
if err != nil {
return nil, err
}
var relevantIngresses []*extv1beta1.Ingress
for _, ingress := range ingressList {
if !metav1.IsControlledBy(ingress, crt) {
glog.Infof("Found ingress %q with acme-order-url annotation set to that of Certificate %q"+
"but it is not owned by the Certificate resource, so skipping it.", ingress.Name, crt.Name)
continue
}
if ingress.Labels == nil ||
ingress.Labels[domainLabelKey] != domain {
continue
}
relevantIngresses = append(relevantIngresses, ingress)
}
return relevantIngresses, nil
}
// ensureIngress will ensure the ingress required to solve this challenge
// exists, or if an existing ingress is specified on the secret will ensure
// that the ingress has an appropriate challenge path configured
func (s *Solver) ensureIngress(crt *v1alpha1.Certificate, svcName, domain, token string) (ing *extv1beta1.Ingress, err error) {
domainCfg := crt.Spec.ACME.ConfigForDomain(domain)
if domainCfg == nil {
return nil, fmt.Errorf("no ACME challenge configuration found for domain %q", domain)
}
httpDomainCfg := domainCfg.HTTP01
if httpDomainCfg == nil {
httpDomainCfg = &v1alpha1.ACMECertificateHTTP01Config{}
}
if httpDomainCfg != nil &&
httpDomainCfg.Ingress != "" {
return s.addChallengePathToIngress(crt, svcName, domain, token, *httpDomainCfg)
}
return s.createIngress(crt, svcName, domain, token, *httpDomainCfg)
}
// createIngress will create a challenge solving pod for the given certificate,
// domain, token and key.
func (s *Solver) createIngress(crt *v1alpha1.Certificate, svcName, domain, token string, domainCfg v1alpha1.ACMECertificateHTTP01Config) (*extv1beta1.Ingress, error) {
podLabels := podLabels(crt, domain)
// TODO: add additional annotations to help workaround problematic ingress controller behaviours
ingAnnotaions := make(map[string]string)
if ingClass := domainCfg.IngressClass; ingClass != nil {
ingAnnotaions[class.IngressKey] = *ingClass
}
ingPathToAdd := ingressPath(token, svcName)
return s.client.ExtensionsV1beta1().Ingresses(crt.Namespace).Create(&extv1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "cm-acme-http-solver-",
Namespace: crt.Namespace,
Labels: podLabels,
Annotations: ingAnnotaions,
},
Spec: extv1beta1.IngressSpec{
Rules: []extv1beta1.IngressRule{
{
Host: domain,
IngressRuleValue: extv1beta1.IngressRuleValue{
HTTP: &extv1beta1.HTTPIngressRuleValue{
Paths: []extv1beta1.HTTPIngressPath{ingPathToAdd},
},
},
},
},
},
})
}
func (s *Solver) addChallengePathToIngress(crt *v1alpha1.Certificate, svcName, domain, token string, domainCfg v1alpha1.ACMECertificateHTTP01Config) (*extv1beta1.Ingress, error) {
ing, err := s.ingressLister.Ingresses(crt.Namespace).Get(domainCfg.Ingress)
if err != nil {
return nil, err
}
ingPathToAdd := ingressPath(token, svcName)
// check for an existing Rule for the given domain on the ingress resource
for _, rule := range ing.Spec.Rules {
if rule.Host == domain {
if rule.HTTP == nil {
rule.HTTP = &extv1beta1.HTTPIngressRuleValue{}
}
for i, p := range rule.HTTP.Paths {
// if an existing path exists on this rule for the challenge path,
// we overwrite it else we'll confuse ingress controllers
if p.Path == ingPathToAdd.Path {
rule.HTTP.Paths[i] = ingPathToAdd
return s.client.ExtensionsV1beta1().Ingresses(ing.Namespace).Update(ing)
}
}
rule.HTTP.Paths = append(rule.HTTP.Paths, ingPathToAdd)
return s.client.ExtensionsV1beta1().Ingresses(ing.Namespace).Update(ing)
}
}
// if one doesn't exist, create a new IngressRule
ing.Spec.Rules = append(ing.Spec.Rules, extv1beta1.IngressRule{
Host: domain,
IngressRuleValue: extv1beta1.IngressRuleValue{
HTTP: &extv1beta1.HTTPIngressRuleValue{
Paths: []extv1beta1.HTTPIngressPath{ingPathToAdd},
},
},
})
return s.client.ExtensionsV1beta1().Ingresses(ing.Namespace).Update(ing)
}
// cleanupIngresses will remove the rules added by cert-manager to an existing
// ingress, or delete the ingress if an existing ingress name is not specified
// on the certificate.
func (s *Solver) cleanupIngresses(crt *v1alpha1.Certificate, domain, token string) error {
domainCfg := crt.Spec.ACME.ConfigForDomain(domain)
httpDomainCfg := domainCfg.HTTP01
if httpDomainCfg == nil {
httpDomainCfg = &v1alpha1.ACMECertificateHTTP01Config{}
}
existingIngressName := httpDomainCfg.Ingress
// if the 'ingress' field on the domain config is not set, we need to delete
// the ingress resources that cert-manager has created to solve the challenge
if existingIngressName == "" {
ingresses, err := s.getIngressesForCertificate(crt, domain)
if err != nil {
return err
}
var errs []error
for _, ingress := range ingresses {
// TODO: should we call DeleteCollection here? We'd need to somehow
// also ensure ownership as part of that request using a FieldSelector.
err := s.client.ExtensionsV1beta1().Ingresses(ingress.Namespace).Delete(ingress.Name, nil)
if err != nil {
errs = append(errs, err)
}
}
return utilerrors.NewAggregate(errs)
}
// otherwise, we need to remove any cert-manager added rules from the ingress resource
ing, err := s.client.ExtensionsV1beta1().Ingresses(crt.Namespace).Get(existingIngressName, metav1.GetOptions{})
if k8sErrors.IsNotFound(err) {
glog.Infof("attempt to cleanup Ingress %q of ACME challenge path failed: %v", existingIngressName, err)
return nil
}
if err != nil {
return err
}
ingPathToDel := solverPathFn(token)
Outer:
for _, rule := range ing.Spec.Rules {
if rule.Host == domain {
if rule.HTTP == nil {
return nil
}
for i, path := range rule.HTTP.Paths {
if path.Path == ingPathToDel {
rule.HTTP.Paths = append(rule.HTTP.Paths[:i], rule.HTTP.Paths[i+1:]...)
break Outer
}
}
}
}
_, err = s.client.ExtensionsV1beta1().Ingresses(ing.Namespace).Update(ing)
if err != nil {
return err
}
return nil
}
// ingressPath returns the ingress HTTPIngressPath object needed to solve this
// challenge.
func ingressPath(token, serviceName string) extv1beta1.HTTPIngressPath {
return extv1beta1.HTTPIngressPath{
Path: solverPathFn(token),
Backend: extv1beta1.IngressBackend{
ServiceName: serviceName,
ServicePort: intstr.FromInt(acmeSolverListenPort),
},
}
}
var solverPathFn = func(token string) string {
return fmt.Sprintf("%s/%s", solver.HTTPChallengePath, token)
}

138
pkg/issuer/acme/http/pod.go Normal file
View File

@ -0,0 +1,138 @@
package http
import (
"fmt"
"github.com/golang/glog"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
)
func podLabels(crt *v1alpha1.Certificate, domain string) map[string]string {
return map[string]string{
domainLabelKey: domain,
orderURLLabelKey: crt.Status.ACME.OrderURL,
}
}
func (s *Solver) ensurePod(crt *v1alpha1.Certificate, domain, token, key string) (*corev1.Pod, error) {
existingPods, err := s.getPodsForCertificate(crt, domain)
if err != nil {
return nil, err
}
var pod *corev1.Pod
if len(existingPods) > 0 {
// we will only care about the first pod if there are multiple returned
// here. The others should be cleaned up after a call to CleanUp is
// complete.
pod = existingPods[0]
}
if len(existingPods) == 0 {
glog.Infof("No existing HTTP01 challenge solver pod found for Certificate %q. One will be created.")
pod, err = s.createPod(crt, domain, token, key)
if err != nil {
return nil, err
}
}
return pod, nil
}
// getPodsForCertificate returns a list of pods that were created to solve
// http challenges for the given domain
func (s *Solver) getPodsForCertificate(crt *v1alpha1.Certificate, domain string) ([]*corev1.Pod, error) {
if crt.Status.ACME.OrderURL == "" {
return nil, fmt.Errorf("Certificate order URL must be set")
}
podLabels := podLabels(crt, domain)
orderSelector := labels.NewSelector()
for key, val := range podLabels {
req, err := labels.NewRequirement(key, selection.Equals, []string{val})
if err != nil {
return nil, err
}
orderSelector = orderSelector.Add(*req)
}
podList, err := s.podLister.Pods(crt.Namespace).List(orderSelector)
if err != nil {
return nil, err
}
var relevantPods []*corev1.Pod
for _, pod := range podList {
if !metav1.IsControlledBy(pod, crt) {
glog.Infof("Found pod %q with acme-order-url annotation set to that of Certificate %q"+
"but it is not owned by the Certificate resource, so skipping it.", pod.Name, crt.Name)
continue
}
relevantPods = append(relevantPods, pod)
}
return relevantPods, nil
}
func (s *Solver) cleanupPods(crt *v1alpha1.Certificate, domain string) error {
pods, err := s.getPodsForCertificate(crt, domain)
if err != nil {
return err
}
var errs []error
for _, pod := range pods {
// TODO: should we call DeleteCollection here? We'd need to somehow
// also ensure ownership as part of that request using a FieldSelector.
err := s.client.CoreV1().Pods(pod.Namespace).Delete(pod.Name, nil)
if err != nil {
errs = append(errs, err)
}
}
return utilerrors.NewAggregate(errs)
}
// createPod will create a challenge solving pod for the given certificate,
// domain, token and key.
func (s *Solver) createPod(crt *v1alpha1.Certificate, domain, token, key string) (*corev1.Pod, error) {
podLabels := podLabels(crt, domain)
return s.client.CoreV1().Pods(crt.Namespace).Create(&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "cm-acme-http-solver-",
Namespace: crt.Namespace,
Labels: podLabels,
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyOnFailure,
Containers: []corev1.Container{
{
Name: "acmesolver",
// TODO: use an image as specified as a config option
Image: s.solverImage,
ImagePullPolicy: corev1.PullIfNotPresent,
// TODO: replace this with some kind of cmdline generator
Args: []string{
fmt.Sprintf("--listen-port=%d", acmeSolverListenPort),
fmt.Sprintf("--domain=%s", domain),
fmt.Sprintf("--token=%s", token),
fmt.Sprintf("--key=%s", key),
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("10m"),
corev1.ResourceMemory: resource.MustParse("2Mi"),
},
},
Ports: []corev1.ContainerPort{
{
Name: "http",
ContainerPort: acmeSolverListenPort,
},
},
},
},
},
})
}

View File

@ -0,0 +1,114 @@
package http
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/golang/glog"
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
)
func (s *Solver) ensureService(crt *v1alpha1.Certificate, domain, token, key string) (*corev1.Service, error) {
existingServices, err := s.getServicesForCertificate(crt, domain)
if err != nil {
return nil, err
}
var service *corev1.Service
if len(existingServices) > 0 {
// we will only care about the first service if there are multiple returned
// here. The others should be cleaned up after a call to CleanUp is
// complete.
service = existingServices[0]
}
if len(existingServices) == 0 {
glog.Infof("No existing HTTP01 challenge solver service found for Certificate %q. One will be created.")
service, err = s.createService(crt, domain)
if err != nil {
return nil, err
}
}
return service, nil
}
// getServicesForCertificate returns a list of services that were created to solve
// http challenges for the given domain
func (s *Solver) getServicesForCertificate(crt *v1alpha1.Certificate, domain string) ([]*corev1.Service, error) {
if crt.Status.ACME.OrderURL == "" {
return []*corev1.Service{}, nil
}
podLabels := podLabels(crt, domain)
selector := labels.NewSelector()
for key, val := range podLabels {
req, err := labels.NewRequirement(key, selection.Equals, []string{val})
if err != nil {
return nil, err
}
selector = selector.Add(*req)
}
serviceList, err := s.serviceLister.Services(crt.Namespace).List(selector)
if err != nil {
return nil, err
}
var relevantServices []*corev1.Service
for _, service := range serviceList {
if !metav1.IsControlledBy(service, crt) {
glog.Infof("Found service %q with acme-order-url annotation set to that of Certificate %q"+
"but it is not owned by the Certificate resource, so skipping it.", service.Name, crt.Name)
continue
}
if service.Labels == nil ||
service.Labels[domainLabelKey] != domain {
continue
}
relevantServices = append(relevantServices, service)
}
return relevantServices, nil
}
// createService will create the service required to solve this challenge
// in the target API server.
func (s *Solver) createService(crt *v1alpha1.Certificate, domain string) (*corev1.Service, error) {
podLabels := podLabels(crt, domain)
return s.client.CoreV1().Services(crt.Namespace).Create(&corev1.Service{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "cm-acme-http-solver-",
Namespace: crt.Namespace,
Labels: podLabels,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeNodePort,
Ports: []corev1.ServicePort{
{
Name: "http",
Port: acmeSolverListenPort,
TargetPort: intstr.FromInt(acmeSolverListenPort),
},
},
Selector: podLabels,
},
})
}
func (s *Solver) cleanupServices(crt *v1alpha1.Certificate, domain string) error {
services, err := s.getServicesForCertificate(crt, domain)
if err != nil {
return err
}
var errs []error
for _, service := range services {
// TODO: should we call DeleteCollection here? We'd need to somehow
// also ensure ownership as part of that request using a FieldSelector.
err := s.client.CoreV1().Services(service.Namespace).Delete(service.Name, nil)
if err != nil {
errs = append(errs, err)
}
}
return utilerrors.NewAggregate(errs)
}

View File

@ -276,6 +276,11 @@ func partitionAuthorizations(authzs ...*acme.Authorization) (failed, pending, va
return failed, pending, valid
}
// pickChallengeType will select a challenge type to used based on the types
// offered by the ACME server (i.e. auth.Challenges), the options configured on
// the Certificate resource (cfg) and the providers configured on the
// corresponding Issuer resource. If there is no challenge type that can be
// used, it will return an error.
func (a *Acme) pickChallengeType(domain string, auth *acme.Authorization, cfg []v1alpha1.ACMECertificateDomainConfig) (string, error) {
for _, d := range cfg {
for _, dom := range d.Domains {