diff --git a/pkg/reconciler/domainmapping/controller.go b/pkg/reconciler/domainmapping/controller.go index 45005a29634d..d0782c475dfe 100644 --- a/pkg/reconciler/domainmapping/controller.go +++ b/pkg/reconciler/domainmapping/controller.go @@ -22,6 +22,7 @@ import ( "k8s.io/client-go/tools/cache" network "knative.dev/networking/pkg" netclient "knative.dev/networking/pkg/client/injection/client" + certificateinformer "knative.dev/networking/pkg/client/injection/informers/networking/v1alpha1/certificate" ingressinformer "knative.dev/networking/pkg/client/injection/informers/networking/v1alpha1/ingress" "knative.dev/pkg/configmap" "knative.dev/pkg/controller" @@ -36,12 +37,14 @@ import ( // NewController creates a new DomainMapping controller. func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { logger := logging.FromContext(ctx) + certificateInformer := certificateinformer.Get(ctx) domainmappingInformer := domainmapping.Get(ctx) ingressInformer := ingressinformer.Get(ctx) r := &Reconciler{ - ingressLister: ingressInformer.Lister(), - netclient: netclient.Get(ctx), + certificateLister: certificateInformer.Lister(), + ingressLister: ingressInformer.Lister(), + netclient: netclient.Get(ctx), } impl := kindreconciler.NewImpl(ctx, r, func(impl *controller.Impl) controller.Options { @@ -63,6 +66,7 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl FilterFunc: controller.FilterControllerGK(v1alpha1.Kind("DomainMapping")), Handler: controller.HandleAll(impl.EnqueueControllerOf), } + certificateInformer.Informer().AddEventHandler(handleControllerOf) ingressInformer.Informer().AddEventHandler(handleControllerOf) r.resolver = resolver.NewURIResolver(ctx, impl.EnqueueKey) diff --git a/pkg/reconciler/domainmapping/reconciler.go b/pkg/reconciler/domainmapping/reconciler.go index fbf5b1e836c8..70f18de43e47 100644 --- a/pkg/reconciler/domainmapping/reconciler.go +++ b/pkg/reconciler/domainmapping/reconciler.go @@ -19,8 +19,13 @@ package domainmapping import ( "context" "fmt" + "sort" + "strconv" "strings" + kaccessor "knative.dev/serving/pkg/reconciler/accessor" + networkaccessor "knative.dev/serving/pkg/reconciler/accessor/networking" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" apierrs "k8s.io/apimachinery/pkg/api/errors" @@ -37,30 +42,43 @@ import ( "knative.dev/pkg/network" "knative.dev/pkg/reconciler" "knative.dev/pkg/resolver" + v1 "knative.dev/serving/pkg/apis/serving/v1" "knative.dev/serving/pkg/apis/serving/v1alpha1" domainmappingreconciler "knative.dev/serving/pkg/client/injection/reconciler/serving/v1alpha1/domainmapping" "knative.dev/serving/pkg/reconciler/domainmapping/config" "knative.dev/serving/pkg/reconciler/domainmapping/resources" + routeresources "knative.dev/serving/pkg/reconciler/route/resources" ) // Reconciler implements controller.Reconciler for DomainMapping resources. type Reconciler struct { - ingressLister networkinglisters.IngressLister - netclient netclientset.Interface - resolver *resolver.URIResolver + certificateLister networkinglisters.CertificateLister + ingressLister networkinglisters.IngressLister + netclient netclientset.Interface + resolver *resolver.URIResolver } // Check that our Reconciler implements Interface var _ domainmappingreconciler.Interface = (*Reconciler)(nil) +// Check that our Reconciler implements CertificateAccessor +var _ networkaccessor.CertificateAccessor = (*Reconciler)(nil) + +// GetNetworkingClient implements networking.CertificateAccessor +func (r *Reconciler) GetNetworkingClient() netclientset.Interface { + return r.netclient +} + +// GetCertificateLister implements networking.CertificateAccessor +func (r *Reconciler) GetCertificateLister() networkinglisters.CertificateLister { + return r.certificateLister +} + // ReconcileKind implements Interface.ReconcileKind. func (r *Reconciler) ReconcileKind(ctx context.Context, dm *v1alpha1.DomainMapping) reconciler.Event { logger := logging.FromContext(ctx) logger.Debugf("Reconciling DomainMapping %s/%s", dm.Namespace, dm.Name) - // TODO(https://github.com/knative/serving/issues/10247) - dm.Status.MarkTLSNotEnabled("AutoTLS for DomainMapping is not implemented") - // Defensively assume the ingress is not configured until we manage to // successfully reconcile it below. This avoids error cases where we fail // before we've reconciled the ingress and get a new ObservedGeneration but @@ -74,6 +92,11 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, dm *v1alpha1.DomainMappi dm.Status.URL = url dm.Status.Address = &duckv1.Addressable{URL: url} + tls, acmeChallenges, err := r.tls(ctx, dm) + if err != nil { + return err + } + // IngressClass can be set via annotations or in the config map. ingressClass := dm.Annotations[networking.IngressClassAnnotationKey] if ingressClass == "" { @@ -94,7 +117,7 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, dm *v1alpha1.DomainMappi // Reconcile the Ingress resource corresponding to the requested Mapping. logger.Debugf("Mapping %s to ref %s/%s (host: %q, svc: %q)", url, dm.Spec.Ref.Namespace, dm.Spec.Ref.Name, targetHost, targetBackendSvc) - desired := resources.MakeIngress(dm, targetBackendSvc, targetHost, ingressClass, nil /* tls */) + desired := resources.MakeIngress(dm, targetBackendSvc, targetHost, ingressClass, tls, acmeChallenges...) ingress, err := r.reconcileIngress(ctx, dm, desired) if err != nil { return err @@ -110,6 +133,64 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, dm *v1alpha1.DomainMappi return err } +func autoTLSEnabled(ctx context.Context, dm *v1alpha1.DomainMapping) bool { + if !config.FromContext(ctx).Network.AutoTLS { + return false + } + annotationValue := dm.Annotations[networking.DisableAutoTLSAnnotationKey] + disabledByAnnotation, err := strconv.ParseBool(annotationValue) + if annotationValue != "" && err != nil { + logger := logging.FromContext(ctx) + // Validation should've caught an invalid value here. + // If we have one anyway, assume not disabled and log a warning. + logger.Warnf("DM.Annotations[%s] = %q is invalid", + networking.DisableAutoTLSAnnotationKey, annotationValue) + } + + return !disabledByAnnotation +} + +func certClass(ctx context.Context) string { + return config.FromContext(ctx).Network.DefaultCertificateClass +} + +func (r *Reconciler) tls(ctx context.Context, dm *v1alpha1.DomainMapping) ([]netv1alpha1.IngressTLS, []netv1alpha1.HTTP01Challenge, error) { + if !autoTLSEnabled(ctx, dm) { + dm.Status.MarkTLSNotEnabled(v1.AutoTLSNotEnabledMessage) + return nil, nil, nil + } + + acmeChallenges := []netv1alpha1.HTTP01Challenge{} + desiredCert := resources.MakeCertificate(dm, certClass(ctx)) + cert, err := networkaccessor.ReconcileCertificate(ctx, dm, desiredCert, r) + if err != nil { + if kaccessor.IsNotOwned(err) { + dm.Status.MarkCertificateNotOwned(desiredCert.Name) + } else { + dm.Status.MarkCertificateProvisionFailed(desiredCert.Name) + } + return nil, nil, err + } + + for _, dnsName := range desiredCert.Spec.DNSNames { + if dnsName == dm.Name { + dm.Status.URL.Scheme = "https" + break + } + } + if cert.IsReady() { + dm.Status.MarkCertificateReady(cert.Name) + return []netv1alpha1.IngressTLS{routeresources.MakeIngressTLS(cert, desiredCert.Spec.DNSNames)}, nil, nil + } + acmeChallenges = append(acmeChallenges, cert.Status.HTTP01Challenges...) + dm.Status.MarkCertificateNotReady(cert.Name) + + sort.Slice(acmeChallenges, func(i, j int) bool { + return acmeChallenges[i].URL.String() < acmeChallenges[j].URL.String() + }) + return nil, acmeChallenges, nil +} + func (r *Reconciler) reconcileIngress(ctx context.Context, dm *v1alpha1.DomainMapping, desired *netv1alpha1.Ingress) (*netv1alpha1.Ingress, error) { recorder := controller.GetEventRecorder(ctx) ingress, err := r.ingressLister.Ingresses(desired.Namespace).Get(desired.Name) diff --git a/pkg/reconciler/domainmapping/reconciler_test.go b/pkg/reconciler/domainmapping/reconciler_test.go new file mode 100644 index 000000000000..1bf42664057c --- /dev/null +++ b/pkg/reconciler/domainmapping/reconciler_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2021 The Knative Authors + +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 domainmapping + +import ( + "context" + "testing" + + network "knative.dev/networking/pkg" + "knative.dev/networking/pkg/apis/networking" + "knative.dev/serving/pkg/reconciler/domainmapping/config" +) + +func TestAutoTLSEnabled(t *testing.T) { + dm := domainMapping("test-ns", "test-route") + + for _, tc := range []struct { + name string + configAutoTLSEnabled bool + tlsDisabledAnnotation string + wantAutoTLSEnabled bool + }{{ + name: "AutoTLS enabled by config, not disabled by annotation", + configAutoTLSEnabled: true, + wantAutoTLSEnabled: true, + }, { + name: "AutoTLS enabled by config, disabled by annotation", + configAutoTLSEnabled: true, + tlsDisabledAnnotation: "true", + wantAutoTLSEnabled: false, + }, { + name: "AutoTLS disabled by config, not disabled by annotation", + configAutoTLSEnabled: false, + wantAutoTLSEnabled: false, + }, { + name: "AutoTLS disabled by config, disabled by annotation", + configAutoTLSEnabled: false, + tlsDisabledAnnotation: "true", + wantAutoTLSEnabled: false, + }, { + name: "AutoTLS enabled by config, invalid annotation", + configAutoTLSEnabled: true, + tlsDisabledAnnotation: "foo", + wantAutoTLSEnabled: true, + }, { + name: "AutoTLS disabled by config, invalid annotation", + configAutoTLSEnabled: false, + tlsDisabledAnnotation: "foo", + wantAutoTLSEnabled: false, + }, { + name: "AutoTLS disabled by config nil annotations", + configAutoTLSEnabled: false, + wantAutoTLSEnabled: false, + }, { + name: "AutoTLS enabled by config, nil annotations", + configAutoTLSEnabled: true, + wantAutoTLSEnabled: true, + }} { + t.Run(tc.name, func(t *testing.T) { + ctx := config.ToContext(context.Background(), &config.Config{ + Network: &network.Config{ + AutoTLS: tc.configAutoTLSEnabled, + }, + }) + if tc.tlsDisabledAnnotation != "" { + dm.Annotations = map[string]string{ + networking.DisableAutoTLSAnnotationKey: tc.tlsDisabledAnnotation, + } + } + if got := autoTLSEnabled(ctx, dm); got != tc.wantAutoTLSEnabled { + t.Errorf("autoTLSEnabled = %t, want %t", got, tc.wantAutoTLSEnabled) + } + }) + } +} diff --git a/pkg/reconciler/domainmapping/table_test.go b/pkg/reconciler/domainmapping/table_test.go index a569445e9a2a..98c6bc01a42a 100644 --- a/pkg/reconciler/domainmapping/table_test.go +++ b/pkg/reconciler/domainmapping/table_test.go @@ -24,6 +24,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" clientgotesting "k8s.io/client-go/testing" network "knative.dev/networking/pkg" @@ -34,10 +35,12 @@ import ( duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/pkg/configmap" "knative.dev/pkg/controller" + "knative.dev/pkg/kmeta" "knative.dev/pkg/logging" pkgnetwork "knative.dev/pkg/network" pkgreconciler "knative.dev/pkg/reconciler" "knative.dev/pkg/resolver" + "knative.dev/serving/pkg/apis/serving" servingv1 "knative.dev/serving/pkg/apis/serving/v1" "knative.dev/serving/pkg/apis/serving/v1alpha1" servingclient "knative.dev/serving/pkg/client/injection/client/fake" @@ -73,6 +76,7 @@ func TestReconcile(t *testing.T) { withURL("http", "first-reconcile.com"), withAddress("http", "first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withIngressNotConfigured, withReferenceResolved, @@ -99,6 +103,7 @@ func TestReconcile(t *testing.T) { withURL("http", "first-reconcile.com"), withAddress("http", "first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withIngressNotConfigured, withReferenceResolved, @@ -127,6 +132,7 @@ func TestReconcile(t *testing.T) { withURL("http", "first-reconcile.com"), withAddress("http", "first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceNotResolved(`services.serving.knative.dev "target" not found`), ), @@ -152,6 +158,7 @@ func TestReconcile(t *testing.T) { withURL("http", "first-reconcile.com"), withAddress("http", "first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceNotResolved(`resolved URI "http://the-target-svc.svc.cluster.local/path" contains a path`), ), @@ -177,6 +184,7 @@ func TestReconcile(t *testing.T) { withURL("http", "first-reconcile.com"), withAddress("http", "first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceNotResolved(`resolved URI "http://notasvc.cluster.local" must be of the form {name}.{namespace}.svc.cluster.local`), ), @@ -202,6 +210,7 @@ func TestReconcile(t *testing.T) { withURL("http", "first-reconcile.com"), withAddress("http", "first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceNotResolved(`resolved URI "http://name.anothernamespace.svc.cluster.local" must be in same namespace as DomainMapping`), ), @@ -227,6 +236,7 @@ func TestReconcile(t *testing.T) { withURL("http", "first-reconcile.com"), withAddress("http", "first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, withIngressNotConfigured, withDomainClaimed, withReferenceResolved, @@ -255,6 +265,7 @@ func TestReconcile(t *testing.T) { withURL("http", "first-reconcile.com"), withAddress("http", "first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, ), }}, WantCreates: []runtime.Object{ @@ -282,6 +293,7 @@ func TestReconcile(t *testing.T) { withURL("http", "first-reconcile.com"), withAddress("http", "first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimNotOwned, ), }}, @@ -305,6 +317,7 @@ func TestReconcile(t *testing.T) { withURL("http", "ingressclass.first-reconcile.com"), withAddress("http", "ingressclass.first-reconcile.com"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withIngressNotConfigured, withReferenceResolved, @@ -337,6 +350,7 @@ func TestReconcile(t *testing.T) { withURL("http", "ingress-exists.org"), withAddress("http", "ingress-exists.org"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withIngressNotConfigured, withReferenceResolved, @@ -366,6 +380,7 @@ func TestReconcile(t *testing.T) { withURL("http", "ingress-failed.me"), withAddress("http", "ingress-failed.me"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceResolved, withPropagatedStatus(ingress(domainMapping("default", "failed.default.svc.cluster.local"), "", WithLoadbalancerFailed("fell over", "hurt myself")).Status), @@ -392,6 +407,7 @@ func TestReconcile(t *testing.T) { withURL("http", "ingress-unknown.me"), withAddress("http", "ingress-unknown.me"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceResolved, withPropagatedStatus(ingress(domainMapping("default", "ingress-unknown.me"), "", withIngressNotReady).Status), @@ -418,6 +434,7 @@ func TestReconcile(t *testing.T) { withURL("http", "ingress-ready.me"), withAddress("http", "ingress-ready.me"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceResolved, withPropagatedStatus(ingress(domainMapping("default", "ingress-ready.me"), "", withIngressReady).Status), @@ -436,6 +453,7 @@ func TestReconcile(t *testing.T) { withURL("http", "cantcreate.this"), withAddress("http", "cantcreate.this"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceResolved, withGeneration(1), @@ -453,6 +471,7 @@ func TestReconcile(t *testing.T) { withURL("http", "cantcreate.this"), withAddress("http", "cantcreate.this"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceResolved, withIngressNotConfigured, @@ -477,6 +496,7 @@ func TestReconcile(t *testing.T) { withURL("http", "cantupdate.this"), withAddress("http", "cantupdate.this"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceResolved, withGeneration(1), @@ -495,6 +515,7 @@ func TestReconcile(t *testing.T) { withURL("http", "cantupdate.this"), withAddress("http", "cantupdate.this"), withInitDomainMappingConditions, + withTLSNotEnabled, withDomainClaimed, withReferenceResolved, withIngressNotConfigured, @@ -510,9 +531,10 @@ func TestReconcile(t *testing.T) { table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler { ctx = addressable.WithDuck(ctx) r := &Reconciler{ - netclient: networkingclient.Get(ctx), - ingressLister: listers.GetIngressLister(), - resolver: resolver.NewURIResolver(ctx, func(types.NamespacedName) {}), + certificateLister: listers.GetCertificateLister(), + ingressLister: listers.GetIngressLister(), + netclient: networkingclient.Get(ctx), + resolver: resolver.NewURIResolver(ctx, func(types.NamespacedName) {}), } return domainmappingreconciler.NewReconciler(ctx, logging.FromContext(ctx), @@ -528,6 +550,290 @@ func TestReconcile(t *testing.T) { })) } +func TestReconcileTLSEnabled(t *testing.T) { + table := TableTest{{ + Name: "first reconcile", + Key: "default/first.reconcile.io", + Objects: []runtime.Object{ + ksvc("default", "ready", "ready.default.svc.cluster.local", ""), + domainMapping("default", "first.reconcile.io", + withRef("default", "ready"), + withURL("http", "first.reconcile.io"), + withAddress("http", "first.reconcile.io"), + ), + resources.MakeDomainClaim(domainMapping("default", "first.reconcile.io", withRef("default", "ready"))), + }, + WantCreates: []runtime.Object{ + resources.MakeCertificate(domainMapping("default", "first.reconcile.io", + withRef("default", "ready"), + withURL("http", "first.reconcile.io"), + withAddress("http", "first.reconcile.io"), + ), "the-cert-class"), + ingress(domainMapping("default", "first.reconcile.io", withRef("default", "ready")), "the-ingress-class"), + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: domainMapping("default", "first.reconcile.io", + withRef("default", "ready"), + withURL("https", "first.reconcile.io"), + withAddress("https", "first.reconcile.io"), + withCertificateNotReady, + withInitDomainMappingConditions, + withIngressNotConfigured, + withDomainClaimed, + withReferenceResolved, + ), + }}, + WantEvents: []string{ + Eventf(corev1.EventTypeNormal, "Created", "Created Certificate %s/%s", "default", "first.reconcile.io"), + Eventf(corev1.EventTypeNormal, "Created", "Created Ingress %q", "first.reconcile.io"), + }, + }, { + Name: "becomes ready", + Key: "default/becomes.ready.run", + Objects: []runtime.Object{ + ksvc("default", "ready", "ready.default.svc.cluster.local", ""), + domainMapping("default", "becomes.ready.run", + withRef("default", "ready"), + withURL("http", "becomes.ready.run"), + withAddress("http", "becomes.ready.run"), + ), + resources.MakeDomainClaim(domainMapping("default", "becomes.ready.run", withRef("default", "ready"))), + &netv1alpha1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "becomes.ready.run", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef( + domainMapping("default", "becomes.ready.run", + withRef("default", "ready"), + withURL("http", "becomes.ready.run"), + withAddress("http", "becomes.ready.run")))}, + Annotations: map[string]string{ + networking.CertificateClassAnnotationKey: "the-cert-class", + }, + Labels: map[string]string{ + serving.DomainMappingLabelKey: "becomes.ready.run", + }, + }, + Spec: netv1alpha1.CertificateSpec{ + DNSNames: []string{"becomes.ready.run"}, + SecretName: "becomes.ready.run", + }, + Status: readyCertStatus(), + }, + ingress(domainMapping("default", "becomes.ready.run", withRef("default", "ready")), "the-ingress-class", withIngressReady), + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: domainMapping("default", "becomes.ready.run", + withRef("default", "ready"), + withURL("https", "becomes.ready.run"), + withAddress("https", "becomes.ready.run"), + withCertificateReady, + withInitDomainMappingConditions, + withDomainClaimed, + withReferenceResolved, + withPropagatedStatus(ingress(domainMapping("default", "becomes.ready.run"), "", withIngressReady).Status), + ), + }}, + WantUpdates: []clientgotesting.UpdateActionImpl{{ + Object: ingress(domainMapping("default", "becomes.ready.run", withRef("default", "ready")), "the-ingress-class", withIngressReady, withIngressTLS(netv1alpha1.IngressTLS{ + Hosts: []string{"becomes.ready.run"}, + SecretName: "becomes.ready.run", + SecretNamespace: "default", + })), + }}, + }, { + Name: "cert not owned", + WantErr: true, + Key: "default/cert.not.owned.ru", + Objects: []runtime.Object{ + ksvc("default", "ready", "ready.default.svc.cluster.local", ""), + domainMapping("default", "cert.not.owned.ru", + withRef("default", "ready"), + withURL("http", "cert.not.owned.ru"), + withAddress("http", "cert.not.owned.ru"), + ), + resources.MakeDomainClaim(domainMapping("default", "cert.not.owned.ru", withRef("default", "ready"))), + &netv1alpha1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cert.not.owned.ru", + Namespace: "default", + Annotations: map[string]string{ + networking.CertificateClassAnnotationKey: "the-cert-class", + }, + }, + }, + ingress(domainMapping("default", "cert.not.owned.ru", withRef("default", "ready")), "the-ingress-class", withIngressReady), + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: domainMapping("default", "cert.not.owned.ru", + withRef("default", "ready"), + withURL("http", "cert.not.owned.ru"), + withAddress("http", "cert.not.owned.ru"), + withCertificateNotOwned, + withInitDomainMappingConditions, + ), + }}, + WantEvents: []string{ + Eventf(corev1.EventTypeWarning, "InternalError", `notowned: owner: cert.not.owned.ru with Type *v1alpha1.DomainMapping does not own Certificate: "cert.not.owned.ru"`), + }, + }, { + Name: "cert creation failed", + WantErr: true, + Key: "default/cert.creation.failed.ly", + WithReactors: []clientgotesting.ReactionFunc{ + InduceFailure("create", "certificates"), + }, + WantCreates: []runtime.Object{ + resources.MakeCertificate(domainMapping("default", "cert.creation.failed.ly", + withRef("default", "ready"), + withURL("http", "cert.creation.failed.ly"), + withAddress("http", "cert.creation.failed.ly"), + ), "the-cert-class"), + }, + Objects: []runtime.Object{ + ksvc("default", "ready", "ready.default.svc.cluster.local", ""), + domainMapping("default", "cert.creation.failed.ly", + withRef("default", "ready"), + withURL("http", "cert.creation.failed.ly"), + withAddress("http", "cert.creation.failed.ly"), + ), + resources.MakeDomainClaim(domainMapping("default", "cert.creation.failed.ly", withRef("default", "ready"))), + ingress(domainMapping("default", "cert.creation.failed.ly", withRef("default", "ready")), "the-ingress-class", withIngressReady), + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: domainMapping("default", "cert.creation.failed.ly", + withRef("default", "ready"), + withURL("http", "cert.creation.failed.ly"), + withAddress("http", "cert.creation.failed.ly"), + withCertificateFail, + withInitDomainMappingConditions, + ), + }}, + WantEvents: []string{ + Eventf(corev1.EventTypeWarning, "CreationFailed", `Failed to create Certificate default/cert.creation.failed.ly: inducing failure for create certificates`), + Eventf(corev1.EventTypeWarning, "InternalError", `failed to create Certificate: inducing failure for create certificates`), + }, + }, { + Name: "with challenges", + Key: "default/challenged.com", + Objects: []runtime.Object{ + ksvc("default", "ready", "ready.default.svc.cluster.local", ""), + domainMapping("default", "challenged.com", + withRef("default", "ready"), + withURL("http", "challenged.com"), + withAddress("http", "challenged.com"), + ), + resources.MakeDomainClaim(domainMapping("default", "challenged.com", withRef("default", "ready"))), + ingress(domainMapping("default", "challenged.com", withRef("default", "ready")), "the-ingress-class", withIngressReady), + &netv1alpha1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "challenged.com", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef( + domainMapping("default", "challenged.com", + withRef("default", "ready"), + withURL("http", "challenged.com"), + withAddress("http", "challenged.com"), + ))}, + Annotations: map[string]string{ + networking.CertificateClassAnnotationKey: "the-cert-class", + }, + Labels: map[string]string{ + serving.DomainMappingLabelKey: "challenged.com", + }, + }, + Spec: netv1alpha1.CertificateSpec{ + DNSNames: []string{"challenged.com"}, + SecretName: "challenged.com", + }, + Status: netv1alpha1.CertificateStatus{ + HTTP01Challenges: []netv1alpha1.HTTP01Challenge{{ + URL: &apis.URL{ + Scheme: "http", + Host: "challenged.com", + Path: "/.well-known/acme-challenge/challengeToken", + }, + ServiceName: "cm-solver", + ServicePort: intstr.FromInt(8090), + ServiceNamespace: "default", + }, { + URL: &apis.URL{ + Scheme: "http", + Host: "challenged.com", + Path: "/.well-known/acme-challenge/challengeToken-two", + }, + ServiceName: "cm-solver", + ServicePort: intstr.FromInt(8090), + ServiceNamespace: "default", + }}, + }, + }, + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{{ + Object: domainMapping("default", "challenged.com", + withRef("default", "ready"), + withURL("https", "challenged.com"), + withAddress("https", "challenged.com"), + withInitDomainMappingConditions, + withDomainClaimed, + withReferenceResolved, + withCertificateNotReady, + withPropagatedStatus(ingress(domainMapping("default", "challenged.com"), "", withIngressReady).Status), + ), + }}, + WantUpdates: []clientgotesting.UpdateActionImpl{{ + // Ingress should be updated with correct challenges + Object: ingressWithChallenges(domainMapping("default", "challenged.com", + withRef("default", "ready"), + withURL("https", "challenged.com"), + withAddress("https", "challenged.com")), + "the-ingress-class", + []netv1alpha1.HTTP01Challenge{{ + URL: &apis.URL{ + Scheme: "http", + Host: "challenged.com", + Path: "/.well-known/acme-challenge/challengeToken", + }, + ServiceName: "cm-solver", + ServicePort: intstr.FromInt(8090), + ServiceNamespace: "default", + }, { + URL: &apis.URL{ + Scheme: "http", + Host: "challenged.com", + Path: "/.well-known/acme-challenge/challengeToken-two", + }, + ServiceName: "cm-solver", + ServicePort: intstr.FromInt(8090), + ServiceNamespace: "default", + }}, withIngressReady), + }}, + }} + + table.Test(t, MakeFactory(func(ctx context.Context, listers *Listers, cmw configmap.Watcher) controller.Reconciler { + ctx = addressable.WithDuck(ctx) + r := &Reconciler{ + certificateLister: listers.GetCertificateLister(), + ingressLister: listers.GetIngressLister(), + netclient: networkingclient.Get(ctx), + resolver: resolver.NewURIResolver(ctx, func(types.NamespacedName) {}), + } + + return domainmappingreconciler.NewReconciler(ctx, logging.FromContext(ctx), + servingclient.Get(ctx), listers.GetDomainMappingLister(), controller.GetEventRecorder(ctx), r, + controller.Options{ConfigStore: &testConfigStore{ + config: &config.Config{ + Network: &network.Config{ + DefaultIngressClass: "the-ingress-class", + DefaultCertificateClass: "the-cert-class", + AutoTLS: true, + }, + }, + }}, + ) + })) +} + type domainMappingOption func(dm *v1alpha1.DomainMapping) func domainMapping(namespace, name string, opt ...domainMappingOption) *v1alpha1.DomainMapping { @@ -604,7 +910,26 @@ func withPropagatedStatus(status netv1alpha1.IngressStatus) domainMappingOption func withInitDomainMappingConditions(dm *v1alpha1.DomainMapping) { dm.Status.InitializeConditions() - dm.Status.MarkTLSNotEnabled("AutoTLS for DomainMapping is not implemented") +} + +func withTLSNotEnabled(dm *v1alpha1.DomainMapping) { + dm.Status.MarkTLSNotEnabled(servingv1.AutoTLSNotEnabledMessage) +} + +func withCertificateNotReady(dm *v1alpha1.DomainMapping) { + dm.Status.MarkCertificateNotReady(dm.Name) +} + +func withCertificateReady(dm *v1alpha1.DomainMapping) { + dm.Status.MarkCertificateReady(dm.Name) +} + +func withCertificateFail(dm *v1alpha1.DomainMapping) { + dm.Status.MarkCertificateProvisionFailed(dm.Name) +} + +func withCertificateNotOwned(dm *v1alpha1.DomainMapping) { + dm.Status.MarkCertificateNotOwned(dm.Name) } func withDomainClaimNotOwned(dm *v1alpha1.DomainMapping) { @@ -636,7 +961,11 @@ func withObservedGeneration(dm *v1alpha1.DomainMapping) { } func ingress(dm *v1alpha1.DomainMapping, ingressClass string, opt ...IngressOption) *netv1alpha1.Ingress { - ing := resources.MakeIngress(dm, dm.Spec.Ref.Name, dm.Spec.Ref.Name+"."+dm.Spec.Ref.Namespace+".svc.cluster.local", ingressClass, nil /* tls */) + return ingressWithChallenges(dm, ingressClass, nil /* challenges */, opt...) +} + +func ingressWithChallenges(dm *v1alpha1.DomainMapping, ingressClass string, challenges []netv1alpha1.HTTP01Challenge, opt ...IngressOption) *netv1alpha1.Ingress { + ing := resources.MakeIngress(dm, dm.Spec.Ref.Name, dm.Spec.Ref.Name+"."+dm.Spec.Ref.Namespace+".svc.cluster.local", ingressClass, nil /* tls */, challenges...) for _, o := range opt { o(ing) } @@ -680,6 +1009,12 @@ func ksvc(ns, name, host, path string) *servingv1.Service { } } +func readyCertStatus() netv1alpha1.CertificateStatus { + certStatus := &netv1alpha1.CertificateStatus{} + certStatus.MarkReady() + return *certStatus +} + func service(ns, name string) *corev1.Service { return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -693,6 +1028,12 @@ func withIngressNotReady(ing *netv1alpha1.Ingress) { ing.Status.MarkIngressNotReady("progressing", "hold your horses") } +func withIngressTLS(tls netv1alpha1.IngressTLS) func(ing *netv1alpha1.Ingress) { + return func(ing *netv1alpha1.Ingress) { + ing.Spec.TLS = []netv1alpha1.IngressTLS{tls} + } +} + type testConfigStore struct { config *config.Config }