From c127716ac5e739626e5362e635d7d98a74f19383 Mon Sep 17 00:00:00 2001 From: James Munnelly Date: Thu, 18 Apr 2019 15:47:17 +0100 Subject: [PATCH 1/2] Add DNS01 webhook provider API fields Signed-off-by: James Munnelly --- .../output/reference/api-docs/index.html | 53 ++++++++++++++++++- .../output/reference/api-docs/navData.js | 2 +- pkg/apis/certmanager/v1alpha1/BUILD.bazel | 1 + pkg/apis/certmanager/v1alpha1/types_issuer.go | 37 +++++++++++-- .../v1alpha1/zz_generated.deepcopy.go | 27 ++++++++++ 5 files changed, 115 insertions(+), 5 deletions(-) diff --git a/docs/generated/reference/output/reference/api-docs/index.html b/docs/generated/reference/output/reference/api-docs/index.html index e85eda6c3..2aa3e49f6 100755 --- a/docs/generated/reference/output/reference/api-docs/index.html +++ b/docs/generated/reference/output/reference/api-docs/index.html @@ -11,7 +11,7 @@ - +

    cert-manager

    @@ -811,6 +811,10 @@ Appears In: route53
    ACMEIssuerDNS01ProviderRoute53 + +webhook
    ACMEIssuerDNS01ProviderWebhook + +

    ACMEIssuerDNS01ProviderAcmeDNS v1alpha1

    @@ -1193,6 +1197,53 @@ Appears In: +

    ACMEIssuerDNS01ProviderWebhook v1alpha1

    + + + + + + + + + + + + + + + +
    GroupVersionKind
    certmanagerv1alpha1ACMEIssuerDNS01ProviderWebhook
    +

    ACMEIssuerDNS01ProviderWebhook specifies configuration for a webhook DNS01 provider, including where to POST ChallengePayload resources.

    + + + + + + + + + + + + + + + + + + + + + + + +
    FieldDescription
    config
    JSON
    Additional configuration that should be passed to the webhook apiserver when challenges are processed. This can contain arbitrary JSON data. Secret values should not be specified in this stanza. If secret values are needed (e.g. credentials for a DNS service), you should use a SecretKeySelector to reference a Secret resource. For details on the schema of this field, consult the webhook provider implementation's documentation.
    groupName
    string
    The API group name that should be used when POSTing ChallengePayload resources to the webhook apiserver. This should be the same as the GroupName specified in the webhook provider implementation.
    solverName
    string
    The name of the solver to use, as defined in the webhook provider implementation. This will typically be the name of the provider, e.g. 'cloudflare'.

    ACMEIssuerHTTP01Config v1alpha1

    diff --git a/docs/generated/reference/output/reference/api-docs/navData.js b/docs/generated/reference/output/reference/api-docs/navData.js index 5c6aa3f03..de2ca385f 100755 --- a/docs/generated/reference/output/reference/api-docs/navData.js +++ b/docs/generated/reference/output/reference/api-docs/navData.js @@ -1 +1 @@ -(function(){navData = {"toc":[{"section":"-strong-field-definitions-strong-","subsections":[{"section":"venafitpp-v1alpha1"},{"section":"venafiissuer-v1alpha1"},{"section":"venaficloud-v1alpha1"},{"section":"vaultissuer-v1alpha1"},{"section":"vaultauth-v1alpha1"},{"section":"vaultapprole-v1alpha1"},{"section":"time-v1"},{"section":"statusdetails-v1"},{"section":"statuscause-v1"},{"section":"status-v1"},{"section":"solverconfig-v1alpha1"},{"section":"selfsignedissuer-v1alpha1"},{"section":"secretkeyselector-v1alpha1"},{"section":"ownerreference-v1"},{"section":"objectreference-v1alpha1"},{"section":"objectmeta-v1"},{"section":"managedfieldsentry-v1"},{"section":"localobjectreference-v1alpha1"},{"section":"listmeta-v1"},{"section":"issuercondition-v1alpha1"},{"section":"initializers-v1"},{"section":"initializer-v1"},{"section":"http01solverconfig-v1alpha1"},{"section":"fields-v1"},{"section":"duration-v1"},{"section":"domainsolverconfig-v1alpha1"},{"section":"dns01solverconfig-v1alpha1"},{"section":"certificatecondition-v1alpha1"},{"section":"caissuer-v1alpha1"},{"section":"acmeissuerhttp01config-v1alpha1"},{"section":"acmeissuerdns01providerroute53-v1alpha1"},{"section":"acmeissuerdns01providerrfc2136-v1alpha1"},{"section":"acmeissuerdns01providerdigitalocean-v1alpha1"},{"section":"acmeissuerdns01providercloudflare-v1alpha1"},{"section":"acmeissuerdns01providerclouddns-v1alpha1"},{"section":"acmeissuerdns01providerazuredns-v1alpha1"},{"section":"acmeissuerdns01providerakamai-v1alpha1"},{"section":"acmeissuerdns01provideracmedns-v1alpha1"},{"section":"acmeissuerdns01provider-v1alpha1"},{"section":"acmeissuerdns01config-v1alpha1"},{"section":"acmeissuer-v1alpha1"},{"section":"acmecertificateconfig-v1alpha1"}]},{"section":"-strong-old-api-versions-strong-","subsections":[]},{"section":"challenge-v1alpha1","subsections":[]},{"section":"order-v1alpha1","subsections":[]},{"section":"issuer-v1alpha1","subsections":[]},{"section":"clusterissuer-v1alpha1","subsections":[]},{"section":"certificate-v1alpha1","subsections":[]},{"section":"-strong-cert-manager-strong-","subsections":[]}],"flatToc":["venafitpp-v1alpha1","venafiissuer-v1alpha1","venaficloud-v1alpha1","vaultissuer-v1alpha1","vaultauth-v1alpha1","vaultapprole-v1alpha1","time-v1","statusdetails-v1","statuscause-v1","status-v1","solverconfig-v1alpha1","selfsignedissuer-v1alpha1","secretkeyselector-v1alpha1","ownerreference-v1","objectreference-v1alpha1","objectmeta-v1","managedfieldsentry-v1","localobjectreference-v1alpha1","listmeta-v1","issuercondition-v1alpha1","initializers-v1","initializer-v1","http01solverconfig-v1alpha1","fields-v1","duration-v1","domainsolverconfig-v1alpha1","dns01solverconfig-v1alpha1","certificatecondition-v1alpha1","caissuer-v1alpha1","acmeissuerhttp01config-v1alpha1","acmeissuerdns01providerroute53-v1alpha1","acmeissuerdns01providerrfc2136-v1alpha1","acmeissuerdns01providerdigitalocean-v1alpha1","acmeissuerdns01providercloudflare-v1alpha1","acmeissuerdns01providerclouddns-v1alpha1","acmeissuerdns01providerazuredns-v1alpha1","acmeissuerdns01providerakamai-v1alpha1","acmeissuerdns01provideracmedns-v1alpha1","acmeissuerdns01provider-v1alpha1","acmeissuerdns01config-v1alpha1","acmeissuer-v1alpha1","acmecertificateconfig-v1alpha1","-strong-field-definitions-strong-","-strong-old-api-versions-strong-","challenge-v1alpha1","order-v1alpha1","issuer-v1alpha1","clusterissuer-v1alpha1","certificate-v1alpha1","-strong-cert-manager-strong-"]};})(); \ No newline at end of file +(function(){navData = {"toc":[{"section":"-strong-field-definitions-strong-","subsections":[{"section":"venafitpp-v1alpha1"},{"section":"venafiissuer-v1alpha1"},{"section":"venaficloud-v1alpha1"},{"section":"vaultissuer-v1alpha1"},{"section":"vaultauth-v1alpha1"},{"section":"vaultapprole-v1alpha1"},{"section":"time-v1"},{"section":"statusdetails-v1"},{"section":"statuscause-v1"},{"section":"status-v1"},{"section":"solverconfig-v1alpha1"},{"section":"selfsignedissuer-v1alpha1"},{"section":"secretkeyselector-v1alpha1"},{"section":"ownerreference-v1"},{"section":"objectreference-v1alpha1"},{"section":"objectmeta-v1"},{"section":"managedfieldsentry-v1"},{"section":"localobjectreference-v1alpha1"},{"section":"listmeta-v1"},{"section":"issuercondition-v1alpha1"},{"section":"initializers-v1"},{"section":"initializer-v1"},{"section":"http01solverconfig-v1alpha1"},{"section":"fields-v1"},{"section":"duration-v1"},{"section":"domainsolverconfig-v1alpha1"},{"section":"dns01solverconfig-v1alpha1"},{"section":"certificatecondition-v1alpha1"},{"section":"caissuer-v1alpha1"},{"section":"acmeissuerhttp01config-v1alpha1"},{"section":"acmeissuerdns01providerwebhook-v1alpha1"},{"section":"acmeissuerdns01providerroute53-v1alpha1"},{"section":"acmeissuerdns01providerrfc2136-v1alpha1"},{"section":"acmeissuerdns01providerdigitalocean-v1alpha1"},{"section":"acmeissuerdns01providercloudflare-v1alpha1"},{"section":"acmeissuerdns01providerclouddns-v1alpha1"},{"section":"acmeissuerdns01providerazuredns-v1alpha1"},{"section":"acmeissuerdns01providerakamai-v1alpha1"},{"section":"acmeissuerdns01provideracmedns-v1alpha1"},{"section":"acmeissuerdns01provider-v1alpha1"},{"section":"acmeissuerdns01config-v1alpha1"},{"section":"acmeissuer-v1alpha1"},{"section":"acmecertificateconfig-v1alpha1"}]},{"section":"-strong-old-api-versions-strong-","subsections":[]},{"section":"challenge-v1alpha1","subsections":[]},{"section":"order-v1alpha1","subsections":[]},{"section":"issuer-v1alpha1","subsections":[]},{"section":"clusterissuer-v1alpha1","subsections":[]},{"section":"certificate-v1alpha1","subsections":[]},{"section":"-strong-cert-manager-strong-","subsections":[]}],"flatToc":["venafitpp-v1alpha1","venafiissuer-v1alpha1","venaficloud-v1alpha1","vaultissuer-v1alpha1","vaultauth-v1alpha1","vaultapprole-v1alpha1","time-v1","statusdetails-v1","statuscause-v1","status-v1","solverconfig-v1alpha1","selfsignedissuer-v1alpha1","secretkeyselector-v1alpha1","ownerreference-v1","objectreference-v1alpha1","objectmeta-v1","managedfieldsentry-v1","localobjectreference-v1alpha1","listmeta-v1","issuercondition-v1alpha1","initializers-v1","initializer-v1","http01solverconfig-v1alpha1","fields-v1","duration-v1","domainsolverconfig-v1alpha1","dns01solverconfig-v1alpha1","certificatecondition-v1alpha1","caissuer-v1alpha1","acmeissuerhttp01config-v1alpha1","acmeissuerdns01providerwebhook-v1alpha1","acmeissuerdns01providerroute53-v1alpha1","acmeissuerdns01providerrfc2136-v1alpha1","acmeissuerdns01providerdigitalocean-v1alpha1","acmeissuerdns01providercloudflare-v1alpha1","acmeissuerdns01providerclouddns-v1alpha1","acmeissuerdns01providerazuredns-v1alpha1","acmeissuerdns01providerakamai-v1alpha1","acmeissuerdns01provideracmedns-v1alpha1","acmeissuerdns01provider-v1alpha1","acmeissuerdns01config-v1alpha1","acmeissuer-v1alpha1","acmecertificateconfig-v1alpha1","-strong-field-definitions-strong-","-strong-old-api-versions-strong-","challenge-v1alpha1","order-v1alpha1","issuer-v1alpha1","clusterissuer-v1alpha1","certificate-v1alpha1","-strong-cert-manager-strong-"]};})(); \ No newline at end of file diff --git a/pkg/apis/certmanager/v1alpha1/BUILD.bazel b/pkg/apis/certmanager/v1alpha1/BUILD.bazel index 34b48849d..c342f3272 100644 --- a/pkg/apis/certmanager/v1alpha1/BUILD.bazel +++ b/pkg/apis/certmanager/v1alpha1/BUILD.bazel @@ -22,6 +22,7 @@ go_library( deps = [ "//pkg/apis/certmanager:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", diff --git a/pkg/apis/certmanager/v1alpha1/types_issuer.go b/pkg/apis/certmanager/v1alpha1/types_issuer.go index f3f24ee34..04ec91e6a 100644 --- a/pkg/apis/certmanager/v1alpha1/types_issuer.go +++ b/pkg/apis/certmanager/v1alpha1/types_issuer.go @@ -18,6 +18,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -86,7 +87,9 @@ type IssuerConfig struct { // +optional SelfSigned *SelfSignedIssuer `json:"selfSigned,omitempty"` - Venafi *VenafiIssuer `json:"venafi,omitempty"` + + // +optional + Venafi *VenafiIssuer `json:"venafi,omitempty"` } // VenafiIssuer describes issuer configuration details for Venafi Cloud. @@ -137,8 +140,7 @@ type VenafiCloud struct { APITokenSecretRef SecretKeySelector `json:"apiTokenSecretRef"` } -type SelfSignedIssuer struct { -} +type SelfSignedIssuer struct{} type VaultIssuer struct { // Vault authentication @@ -261,6 +263,9 @@ type ACMEIssuerDNS01Provider struct { // +optional RFC2136 *ACMEIssuerDNS01ProviderRFC2136 `json:"rfc2136,omitempty"` + + // +optional + Webhook *ACMEIssuerDNS01ProviderWebhook `json:"webhook,omitempty"` } // CNAMEStrategy configures how the DNS01 provider should handle CNAME records @@ -373,6 +378,32 @@ type ACMEIssuerDNS01ProviderRFC2136 struct { TSIGAlgorithm string `json:"tsigAlgorithm,omitempty"` } +// ACMEIssuerDNS01ProviderWebhook specifies configuration for a webhook DNS01 +// provider, including where to POST ChallengePayload resources. +type ACMEIssuerDNS01ProviderWebhook struct { + // The API group name that should be used when POSTing ChallengePayload + // resources to the webhook apiserver. + // This should be the same as the GroupName specified in the webhook + // provider implementation. + GroupName string `json:"groupName"` + + // The name of the solver to use, as defined in the webhook provider + // implementation. + // This will typically be the name of the provider, e.g. 'cloudflare'. + SolverName string `json:"solverName"` + + // Additional configuration that should be passed to the webhook apiserver + // when challenges are processed. + // This can contain arbitrary JSON data. + // Secret values should not be specified in this stanza. + // If secret values are needed (e.g. credentials for a DNS service), you + // should use a SecretKeySelector to reference a Secret resource. + // For details on the schema of this field, consult the webhook provider + // implementation's documentation. + // +optional + Config *apiext.JSON `json:"config,omitempty"` +} + // IssuerStatus contains status information about an Issuer type IssuerStatus struct { // +optional diff --git a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go index 74c778c1a..b0999aba0 100644 --- a/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/certmanager/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -141,6 +142,11 @@ func (in *ACMEIssuerDNS01Provider) DeepCopyInto(out *ACMEIssuerDNS01Provider) { *out = new(ACMEIssuerDNS01ProviderRFC2136) **out = **in } + if in.Webhook != nil { + in, out := &in.Webhook, &out.Webhook + *out = new(ACMEIssuerDNS01ProviderWebhook) + (*in).DeepCopyInto(*out) + } return } @@ -292,6 +298,27 @@ func (in *ACMEIssuerDNS01ProviderRoute53) DeepCopy() *ACMEIssuerDNS01ProviderRou return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ACMEIssuerDNS01ProviderWebhook) DeepCopyInto(out *ACMEIssuerDNS01ProviderWebhook) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = new(v1beta1.JSON) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ACMEIssuerDNS01ProviderWebhook. +func (in *ACMEIssuerDNS01ProviderWebhook) DeepCopy() *ACMEIssuerDNS01ProviderWebhook { + if in == nil { + return nil + } + out := new(ACMEIssuerDNS01ProviderWebhook) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ACMEIssuerHTTP01Config) DeepCopyInto(out *ACMEIssuerHTTP01Config) { *out = *in From af9bce7221887a7f4a1131f40bdf75de724b1a60 Mon Sep 17 00:00:00 2001 From: James Munnelly Date: Thu, 18 Apr 2019 15:49:36 +0100 Subject: [PATCH 2/2] Add 'webhook' DNS01 provider type Signed-off-by: James Munnelly --- pkg/controller/acmechallenges/BUILD.bazel | 1 + pkg/controller/acmechallenges/controller.go | 6 +- pkg/controller/acmechallenges/sync_test.go | 5 + pkg/issuer/acme/dns/BUILD.bazel | 5 + pkg/issuer/acme/dns/dns.go | 139 ++++++++++++++-- pkg/issuer/acme/dns/webhook/BUILD.bazel | 33 ++++ pkg/issuer/acme/dns/webhook/webhook.go | 169 ++++++++++++++++++++ 7 files changed, 345 insertions(+), 13 deletions(-) create mode 100644 pkg/issuer/acme/dns/webhook/BUILD.bazel create mode 100644 pkg/issuer/acme/dns/webhook/webhook.go diff --git a/pkg/controller/acmechallenges/BUILD.bazel b/pkg/controller/acmechallenges/BUILD.bazel index d88281691..9198a524c 100644 --- a/pkg/controller/acmechallenges/BUILD.bazel +++ b/pkg/controller/acmechallenges/BUILD.bazel @@ -43,6 +43,7 @@ go_test( "//pkg/acme/client:go_default_library", "//pkg/apis/certmanager/v1alpha1:go_default_library", "//pkg/controller/test:go_default_library", + "//pkg/issuer/acme/dns:go_default_library", "//test/unit/gen:go_default_library", "//third_party/crypto/acme:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", diff --git a/pkg/controller/acmechallenges/controller.go b/pkg/controller/acmechallenges/controller.go index fe5cfa752..2f6a9407b 100644 --- a/pkg/controller/acmechallenges/controller.go +++ b/pkg/controller/acmechallenges/controller.go @@ -106,7 +106,11 @@ func New(ctx *controllerpkg.Context) (*Controller, error) { ctrl.acmeHelper = acme.NewHelper(ctrl.secretLister, ctrl.Context.ClusterResourceNamespace) ctrl.httpSolver = http.NewSolver(ctx) - ctrl.dnsSolver = dns.NewSolver(ctx) + var err error + ctrl.dnsSolver, err = dns.NewSolver(ctx) + if err != nil { + return nil, err + } ctrl.scheduler = scheduler.New(ctrl.challengeLister) ctrl.ctx = logf.NewContext(ctx.RootContext, nil, ControllerName) diff --git a/pkg/controller/acmechallenges/sync_test.go b/pkg/controller/acmechallenges/sync_test.go index 1237678a7..95b3e2b35 100644 --- a/pkg/controller/acmechallenges/sync_test.go +++ b/pkg/controller/acmechallenges/sync_test.go @@ -27,6 +27,7 @@ import ( acmecl "github.com/jetstack/cert-manager/pkg/acme/client" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" testpkg "github.com/jetstack/cert-manager/pkg/controller/test" + "github.com/jetstack/cert-manager/pkg/issuer/acme/dns" "github.com/jetstack/cert-manager/test/unit/gen" acmeapi "github.com/jetstack/cert-manager/third_party/crypto/acme" ) @@ -323,6 +324,10 @@ func TestSyncHappyPath(t *testing.T) { if test.Builder == nil { test.Builder = &testpkg.Builder{} } + // Don't initialise webhook based DNS solvers during tests as we do + // not have a valid RESTConfig that can be used in the Initialize + // functions. + dns.WebhookSolvers = nil test.Setup(t) chalCopy := test.Challenge.DeepCopy() err := test.Controller.Sync(test.Ctx, chalCopy) diff --git a/pkg/issuer/acme/dns/BUILD.bazel b/pkg/issuer/acme/dns/BUILD.bazel index 112dfb7a9..f79eb40b9 100644 --- a/pkg/issuer/acme/dns/BUILD.bazel +++ b/pkg/issuer/acme/dns/BUILD.bazel @@ -6,6 +6,8 @@ go_library( importpath = "github.com/jetstack/cert-manager/pkg/issuer/acme/dns", visibility = ["//visibility:public"], deps = [ + "//pkg/acme/webhook:go_default_library", + "//pkg/acme/webhook/apis/acme/v1alpha1:go_default_library", "//pkg/apis/certmanager/v1alpha1:go_default_library", "//pkg/controller:go_default_library", "//pkg/issuer/acme/dns/acmedns:go_default_library", @@ -17,7 +19,9 @@ go_library( "//pkg/issuer/acme/dns/rfc2136:go_default_library", "//pkg/issuer/acme/dns/route53:go_default_library", "//pkg/issuer/acme/dns/util:go_default_library", + "//pkg/issuer/acme/dns/webhook:go_default_library", "//vendor/github.com/pkg/errors:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//vendor/k8s.io/client-go/listers/core/v1:go_default_library", "//vendor/k8s.io/klog:go_default_library", ], @@ -69,6 +73,7 @@ filegroup( "//pkg/issuer/acme/dns/rfc2136:all-srcs", "//pkg/issuer/acme/dns/route53:all-srcs", "//pkg/issuer/acme/dns/util:all-srcs", + "//pkg/issuer/acme/dns/webhook:all-srcs", ], tags = ["automanaged"], visibility = ["//visibility:public"], diff --git a/pkg/issuer/acme/dns/dns.go b/pkg/issuer/acme/dns/dns.go index 7d00db809..2e1474d4b 100644 --- a/pkg/issuer/acme/dns/dns.go +++ b/pkg/issuer/acme/dns/dns.go @@ -18,14 +18,18 @@ package dns import ( "context" + "encoding/json" "fmt" "strings" "time" "github.com/pkg/errors" + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" corev1listers "k8s.io/client-go/listers/core/v1" "k8s.io/klog" + "github.com/jetstack/cert-manager/pkg/acme/webhook" + whapi "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" "github.com/jetstack/cert-manager/pkg/controller" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/acmedns" @@ -37,12 +41,15 @@ import ( "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/rfc2136" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/route53" "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/util" + webhookslv "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/webhook" ) const ( cloudDNSServiceAccountKey = "service-account.json" ) +// solver is the old solver type interface. +// All new solvers should be implemented using the new webhook.Solver interface. type solver interface { Present(domain, fqdn, value string) error CleanUp(domain, fqdn, value string) error @@ -68,6 +75,7 @@ type Solver struct { *controller.Context secretLister corev1listers.SecretLister dnsProviderConstructors dnsProviderConstructors + webhookSolvers map[string]webhook.Solver } // Present performs the work to configure DNS to resolve a DNS01 challenge. @@ -76,6 +84,15 @@ func (s *Solver) Present(ctx context.Context, issuer v1alpha1.GenericIssuer, ch return fmt.Errorf("challenge dns config must be specified") } + webhookSolver, req, err := s.prepareChallengeRequest(issuer, ch) + if err != nil && err != errNotFound { + return err + } + if err == nil { + klog.Infof("Presenting DNS01 challenge for domain %q", ch.Spec.DNSName) + return webhookSolver.Present(req) + } + slv, providerConfig, err := s.solverForChallenge(issuer, ch) if err != nil { return err @@ -123,6 +140,15 @@ func (s *Solver) CleanUp(ctx context.Context, issuer v1alpha1.GenericIssuer, ch return fmt.Errorf("challenge dns config must be specified") } + webhookSolver, req, err := s.prepareChallengeRequest(issuer, ch) + if err != nil && err != errNotFound { + return err + } + if err == nil { + klog.Infof("Cleaning up DNS01 challenge for domain %q", ch.Spec.DNSName) + return webhookSolver.CleanUp(req) + } + slv, providerConfig, err := s.solverForChallenge(issuer, ch) if err != nil { return err @@ -150,12 +176,7 @@ func (s *Solver) solverForChallenge(issuer v1alpha1.GenericIssuer, ch *v1alpha1. resourceNamespace := s.ResourceNamespace(issuer) canUseAmbientCredentials := s.CanUseAmbientCredentials(issuer) - providerName := ch.Spec.Config.DNS01.Provider - if providerName == "" { - return nil, nil, fmt.Errorf("dns01 challenge provider name must be set") - } - - providerConfig, err := issuer.GetSpec().ACME.DNS01.Provider(providerName) + providerConfig, err := s.dns01ConfigForChallenge(issuer, ch) if err != nil { return nil, nil, err } @@ -337,19 +358,112 @@ func (s *Solver) solverForChallenge(issuer v1alpha1.GenericIssuer, ch *v1alpha1. return nil, nil, fmt.Errorf("error instantiating rfc2136 challenge solver: %s", err.Error()) } default: - return nil, nil, fmt.Errorf("no dns provider config specified for provider %q", providerName) + return nil, nil, fmt.Errorf("no dns provider config specified for provider %q", ch.Spec.Config.DNS01.Provider) } return impl, providerConfig, nil } +func (s *Solver) prepareChallengeRequest(issuer v1alpha1.GenericIssuer, ch *v1alpha1.Challenge) (webhook.Solver, *whapi.ChallengeRequest, error) { + dns01Config, err := s.dns01ConfigForChallenge(issuer, ch) + if err != nil { + return nil, nil, err + } + + webhookSolver, cfg, err := s.dns01SolverForConfig(dns01Config) + if err != nil { + return nil, nil, err + } + + fqdn, err := util.DNS01LookupFQDN(ch.Spec.DNSName, followCNAME(dns01Config.CNAMEStrategy), s.DNS01Nameservers...) + if err != nil { + return nil, nil, err + } + + zone, err := util.FindZoneByFqdn(fqdn, s.DNS01Nameservers) + if err != nil { + return nil, nil, err + } + + resourceNamespace := s.ResourceNamespace(issuer) + canUseAmbientCredentials := s.CanUseAmbientCredentials(issuer) + + // construct a ChallengeRequest which can be passed to DNS solvers. + // The provided config will be encoded to JSON in order to avoid a coupling + // between cert-manager and any particular DNS provider implementation. + b, err := json.Marshal(cfg) + if err != nil { + return nil, nil, err + } + + req := &whapi.ChallengeRequest{ + Type: "dns-01", + ResolvedFQDN: fqdn, + ResolvedZone: zone, + AllowAmbientCredentials: canUseAmbientCredentials, + ResourceNamespace: resourceNamespace, + Key: ch.Spec.Key, + Config: &apiext.JSON{Raw: b}, + } + + return webhookSolver, req, nil +} + +var errNotFound = fmt.Errorf("failed to determine DNS01 solver type") + +func (s *Solver) dns01SolverForConfig(config *v1alpha1.ACMEIssuerDNS01Provider) (webhook.Solver, interface{}, error) { + solverName := "" + var c interface{} + switch { + case config.Webhook != nil: + solverName = "webhook" + c = config.Webhook + } + if solverName == "" { + return nil, nil, errNotFound + } + p := s.webhookSolvers[solverName] + if p == nil { + return nil, c, fmt.Errorf("no solver provider configured for %q", solverName) + } + return p, c, nil +} + +func (s *Solver) dns01ConfigForChallenge(issuer v1alpha1.GenericIssuer, ch *v1alpha1.Challenge) (*v1alpha1.ACMEIssuerDNS01Provider, error) { + providerName := ch.Spec.Config.DNS01.Provider + if providerName == "" { + return nil, fmt.Errorf("dns01 challenge provider name must be set") + } + + dns01Config, err := issuer.GetSpec().ACME.DNS01.Provider(providerName) + if err != nil { + return nil, err + } + + return dns01Config, nil +} + +var WebhookSolvers = []webhook.Solver{ + &webhookslv.Webhook{}, +} + // NewSolver creates a Solver which can instantiate the appropriate DNS // provider. -func NewSolver(ctx *controller.Context) *Solver { +func NewSolver(ctx *controller.Context) (*Solver, error) { + initialized := make(map[string]webhook.Solver) + // initialize all DNS providers + for _, s := range WebhookSolvers { + err := s.Initialize(ctx.RESTConfig, ctx.StopCh) + if err != nil { + return nil, fmt.Errorf("error intializing DNS provider %q: %v", s.Name(), err) + } + initialized[s.Name()] = s + } + return &Solver{ - ctx, - ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister(), - dnsProviderConstructors{ + Context: ctx, + secretLister: ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister(), + dnsProviderConstructors: dnsProviderConstructors{ clouddns.NewDNSProvider, cloudflare.NewDNSProviderCredentials, route53.NewDNSProvider, @@ -358,7 +472,8 @@ func NewSolver(ctx *controller.Context) *Solver { rfc2136.NewDNSProviderCredentials, digitalocean.NewDNSProviderCredentials, }, - } + webhookSolvers: initialized, + }, nil } func (s *Solver) loadSecretData(selector *v1alpha1.SecretKeySelector, ns string) ([]byte, error) { diff --git a/pkg/issuer/acme/dns/webhook/BUILD.bazel b/pkg/issuer/acme/dns/webhook/BUILD.bazel new file mode 100644 index 000000000..c62be88e4 --- /dev/null +++ b/pkg/issuer/acme/dns/webhook/BUILD.bazel @@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["webhook.go"], + importpath = "github.com/jetstack/cert-manager/pkg/issuer/acme/dns/webhook", + visibility = ["//visibility:public"], + deps = [ + "//pkg/acme/webhook/apis/acme/v1alpha1:go_default_library", + "//pkg/apis/certmanager/v1alpha1:go_default_library", + "//pkg/client/clientset/versioned/scheme:go_default_library", + "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", + "//vendor/k8s.io/client-go/rest:go_default_library", + "//vendor/k8s.io/klog:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/issuer/acme/dns/webhook/webhook.go b/pkg/issuer/acme/dns/webhook/webhook.go new file mode 100644 index 000000000..444c69fde --- /dev/null +++ b/pkg/issuer/acme/dns/webhook/webhook.go @@ -0,0 +1,169 @@ +/* +Copyright 2019 The Jetstack cert-manager contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "encoding/json" + "errors" + "fmt" + + apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/rest" + "k8s.io/klog" + + "github.com/jetstack/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1" + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1" + "github.com/jetstack/cert-manager/pkg/client/clientset/versioned/scheme" +) + +type Webhook struct { + restConfigShallowCopy rest.Config +} + +func (r *Webhook) Name() string { + return "webhook" +} + +// Present creates a TXT record using the specified parameters +func (r *Webhook) Present(ch *v1alpha1.ChallengeRequest) error { + pl := &v1alpha1.ChallengePayload{ + Request: ch, + } + pl.Request.Action = v1alpha1.ChallengeActionPresent + + cfg, err := r.loadConfig(*ch.Config) + if err != nil { + return err + } + + cl, err := r.restClientForGroup(cfg.GroupName) + if err != nil { + return err + } + + result := cl.Post().Resource(cfg.SolverName).Body(&pl).Do() + // we will check this error after parsing the response + resErr := result.Error() + + // TODO: handle metav1.Status response type and print better error messages + var respPayload v1alpha1.ChallengePayload + if err := result.Into(&respPayload); err != nil { + return utilerrors.NewAggregate([]error{resErr, err}) + } + + if respPayload.Response.Success && resErr == nil { + klog.Infof("Present call succeeded") + return nil + } + + if respPayload.Response.Result == nil { + return utilerrors.NewAggregate([]error{ + resErr, + fmt.Errorf("invalid payload response, did not succeed but no result provided"), + }) + } + + if respPayload.Response.Result.Message != "" { + return errors.New(respPayload.Response.Result.Message) + } + + return resErr +} + +// CleanUp removes the TXT record matching the specified parameters +func (r *Webhook) CleanUp(ch *v1alpha1.ChallengeRequest) error { + pl := &v1alpha1.ChallengePayload{ + Request: ch, + } + pl.Request.Action = v1alpha1.ChallengeActionCleanUp + + cfg, err := r.loadConfig(*ch.Config) + if err != nil { + return err + } + + cl, err := r.restClientForGroup(cfg.GroupName) + if err != nil { + return err + } + + result := cl.Post().Resource(cfg.SolverName).Body(&pl).Do() + // we will check this error after parsing the response + resErr := result.Error() + + // TODO: handle metav1.Status response type and print better error messages + var respPayload v1alpha1.ChallengePayload + if err := result.Into(&respPayload); err != nil { + return utilerrors.NewAggregate([]error{resErr, err}) + } + + if respPayload.Response.Success && resErr == nil { + klog.Infof("CleanUp call succeeded") + return nil + } + + if respPayload.Response.Result == nil { + return utilerrors.NewAggregate([]error{ + resErr, + fmt.Errorf("invalid payload response, did not succeed but no result provided"), + }) + } + + if respPayload.Response.Result.Message != "" { + return errors.New(respPayload.Response.Result.Message) + } + + return resErr +} + +func (r *Webhook) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error { + cfgShallowCopy := *kubeClientConfig + cfgShallowCopy.APIPath = "/apis" + cfgShallowCopy.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} + // We defer setting the GroupVersion of the rest client config to the + // restClientForGroup function. + + if cfgShallowCopy.UserAgent == "" { + cfgShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + + r.restConfigShallowCopy = cfgShallowCopy + + return nil +} + +func (r *Webhook) loadConfig(cfgJSON apiext.JSON) (*cmapi.ACMEIssuerDNS01ProviderWebhook, error) { + cfg := cmapi.ACMEIssuerDNS01ProviderWebhook{} + if err := json.Unmarshal(cfgJSON.Raw, &cfg); err != nil { + return nil, fmt.Errorf("error decoding solver config: %v", err) + } + + return &cfg, nil +} + +func (r *Webhook) restClientForGroup(g string) (*rest.RESTClient, error) { + cfg := r.restConfigShallowCopy + cfg.GroupVersion = &schema.GroupVersion{ + Group: g, + Version: v1alpha1.SchemeGroupVersion.Version, + } + + return rest.RESTClientFor(&cfg) +}