@@ -18,6 +18,7 @@ package domainmapping
1818
1919import (
2020 "context"
21+ "errors"
2122 "fmt"
2223 "slices"
2324 "sort"
@@ -31,6 +32,7 @@ import (
3132 "k8s.io/apimachinery/pkg/api/equality"
3233 apierrs "k8s.io/apimachinery/pkg/api/errors"
3334 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35+ "k8s.io/apimachinery/pkg/runtime/schema"
3436
3537 netapi "knative.dev/networking/pkg/apis/networking"
3638 netv1alpha1 "knative.dev/networking/pkg/apis/networking/v1alpha1"
@@ -44,22 +46,30 @@ import (
4446 "knative.dev/pkg/network"
4547 "knative.dev/pkg/reconciler"
4648 "knative.dev/pkg/resolver"
47- v1 "knative.dev/serving/pkg/apis/serving/v1"
49+ "knative.dev/pkg/tracker"
50+ "knative.dev/serving/pkg/apis/serving"
51+ servingv1 "knative.dev/serving/pkg/apis/serving/v1"
4852 "knative.dev/serving/pkg/apis/serving/v1beta1"
4953 domainmappingreconciler "knative.dev/serving/pkg/client/injection/reconciler/serving/v1beta1/domainmapping"
54+ servinglisters "knative.dev/serving/pkg/client/listers/serving/v1"
5055 servingnetworking "knative.dev/serving/pkg/networking"
5156 "knative.dev/serving/pkg/reconciler/domainmapping/config"
5257 "knative.dev/serving/pkg/reconciler/domainmapping/resources"
5358 routeresources "knative.dev/serving/pkg/reconciler/route/resources"
59+ routenames "knative.dev/serving/pkg/reconciler/route/resources/names"
60+ servicenames "knative.dev/serving/pkg/reconciler/service/resources/names"
5461)
5562
5663// Reconciler implements controller.Reconciler for DomainMapping resources.
5764type Reconciler struct {
5865 certificateLister networkinglisters.CertificateLister
5966 ingressLister networkinglisters.IngressLister
6067 domainClaimLister networkinglisters.ClusterDomainClaimLister
68+ serviceLister servinglisters.ServiceLister
69+ routeLister servinglisters.RouteLister
6170 netclient netclientset.Interface
6271 resolver * resolver.URIResolver
72+ tracker tracker.Interface
6373}
6474
6575// Check that our Reconciler implements Interface
@@ -116,12 +126,32 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, dm *v1beta1.DomainMappin
116126 return err
117127 }
118128
129+ // For Knative targets, preserve Route traffic instead of sending everything
130+ // to the Service resolved from the Addressable URL.
131+ targetKind , preserveRouteTraffic := servingReferenceKind (dm .Spec .Ref )
132+
119133 // Resolve the spec.Ref to a URI following the Addressable contract.
120134 targetHost , targetBackendSvc , err := r .resolveRef (ctx , dm )
121135 if err != nil {
136+ if ! preserveRouteTraffic {
137+ return err
138+ }
139+ dm .Status .MarkIngressNotConfigured ()
122140 return err
123141 }
124142
143+ var routePaths []netv1alpha1.HTTPIngressPath
144+ if preserveRouteTraffic {
145+ var found bool
146+ routePaths , found , err = r .routeHTTPPaths (dm , targetKind , targetHost )
147+ if err != nil {
148+ return err
149+ }
150+ if ! found {
151+ return nil
152+ }
153+ }
154+
125155 // HTTPOption can be set via annotations or in the config map.
126156 httpOption , err := servingnetworking .GetHTTPOption (ctx , config .FromContext (ctx ).Network , dm .GetAnnotations ())
127157 if err != nil {
@@ -130,7 +160,12 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, dm *v1beta1.DomainMappin
130160
131161 // Reconcile the Ingress resource corresponding to the requested Mapping.
132162 logger .Debugf ("Mapping %s to ref %s/%s (host: %q, svc: %q)" , url , dm .Spec .Ref .Namespace , dm .Spec .Ref .Name , targetHost , targetBackendSvc )
133- desired := resources .MakeIngress (dm , targetBackendSvc , targetHost , ingressClass , httpOption , tls , acmeChallenges ... )
163+ var desired * netv1alpha1.Ingress
164+ if preserveRouteTraffic {
165+ desired = resources .MakeIngressWithHTTPPaths (dm , routePaths , ingressClass , httpOption , tls , acmeChallenges ... )
166+ } else {
167+ desired = resources .MakeIngress (dm , targetBackendSvc , targetHost , ingressClass , httpOption , tls , acmeChallenges ... )
168+ }
134169 ingress , err := r .reconcileIngress (ctx , dm , desired )
135170 if err != nil {
136171 return err
@@ -205,7 +240,7 @@ func (r *Reconciler) tls(ctx context.Context, dm *v1beta1.DomainMapping) ([]netv
205240 }
206241
207242 if ! externalDomainTLSEnabled (ctx , dm ) {
208- dm .Status .MarkTLSNotEnabled (v1 .ExternalDomainTLSNotEnabledMessage )
243+ dm .Status .MarkTLSNotEnabled (servingv1 .ExternalDomainTLSNotEnabledMessage )
209244 return nil , nil , nil
210245 }
211246
@@ -317,6 +352,107 @@ func (r *Reconciler) resolveRef(ctx context.Context, dm *v1beta1.DomainMapping)
317352 return resolved .Host , parts [0 ], nil
318353}
319354
355+ // routeHTTPPaths gets the first matching cluster-local paths for a Service or
356+ // Route target. If no expected source can be found, it updates IngressReady and
357+ // returns found=false. The source Ingress is responsible for validating paths.
358+ // The returned paths belong to the informer cache and must not be mutated.
359+ func (r * Reconciler ) routeHTTPPaths (dm * v1beta1.DomainMapping , kind , targetHost string ) (paths []netv1alpha1.HTTPIngressPath , found bool , err error ) {
360+ ref := dm .Spec .Ref
361+
362+ if r .tracker == nil {
363+ return nil , false , errors .New ("DomainMapping target tracker is not configured" )
364+ }
365+
366+ routeName := ref .Name
367+ var service * servingv1.Service
368+ if kind == "Service" {
369+ service , err = r .serviceLister .Services (ref .Namespace ).Get (ref .Name )
370+ if apierrs .IsNotFound (err ) {
371+ dm .Status .MarkTargetIngressNotConfigured (fmt .Sprintf ("Waiting for target Service %s/%s to be observed." , ref .Namespace , ref .Name ))
372+ return nil , false , nil
373+ } else if err != nil {
374+ return nil , false , fmt .Errorf ("getting target Service %s/%s: %w" , ref .Namespace , ref .Name , err )
375+ }
376+ routeName = servicenames .Route (service )
377+ }
378+
379+ if err := r .tracker .TrackReference (tracker.Reference {
380+ APIVersion : servingv1 .SchemeGroupVersion .String (),
381+ Kind : "Route" ,
382+ Namespace : ref .Namespace ,
383+ Name : routeName ,
384+ }, dm ); err != nil {
385+ return nil , false , fmt .Errorf ("tracking target Route: %w" , err )
386+ }
387+
388+ route , err := r .routeLister .Routes (ref .Namespace ).Get (routeName )
389+ if apierrs .IsNotFound (err ) {
390+ dm .Status .MarkTargetIngressNotConfigured (fmt .Sprintf ("Waiting for target Route %s/%s to be created." , ref .Namespace , routeName ))
391+ return nil , false , nil
392+ } else if err != nil {
393+ return nil , false , fmt .Errorf ("getting target Route %s/%s: %w" , ref .Namespace , routeName , err )
394+ }
395+
396+ if service != nil && ! metav1 .IsControlledBy (route , service ) {
397+ dm .Status .MarkTargetNotOwned (fmt .Sprintf ("Service %s/%s does not own Route %s/%s." , service .Namespace , service .Name , route .Namespace , route .Name ))
398+ return nil , false , nil
399+ }
400+
401+ ingressName := routenames .Ingress (route )
402+ if err := r .tracker .TrackReference (tracker.Reference {
403+ APIVersion : netv1alpha1 .SchemeGroupVersion .String (),
404+ Kind : "Ingress" ,
405+ Namespace : route .Namespace ,
406+ Name : ingressName ,
407+ }, dm ); err != nil {
408+ return nil , false , fmt .Errorf ("tracking target Ingress: %w" , err )
409+ }
410+
411+ ingress , err := r .ingressLister .Ingresses (route .Namespace ).Get (ingressName )
412+ if apierrs .IsNotFound (err ) {
413+ dm .Status .MarkTargetIngressNotConfigured (fmt .Sprintf ("Waiting for target Route %s/%s to configure Ingress %s/%s." , route .Namespace , route .Name , route .Namespace , ingressName ))
414+ return nil , false , nil
415+ } else if err != nil {
416+ return nil , false , fmt .Errorf ("getting target Ingress %s/%s: %w" , route .Namespace , ingressName , err )
417+ }
418+ if ! metav1 .IsControlledBy (ingress , route ) {
419+ dm .Status .MarkTargetNotOwned (fmt .Sprintf ("Route %s/%s does not own Ingress %s/%s." , route .Namespace , route .Name , ingress .Namespace , ingress .Name ))
420+ return nil , false , nil
421+ }
422+
423+ for i := range ingress .Spec .Rules {
424+ rule := & ingress .Spec .Rules [i ]
425+ if rule .Visibility == netv1alpha1 .IngressVisibilityClusterLocal && slices .Contains (rule .Hosts , targetHost ) {
426+ if rule .HTTP == nil {
427+ return nil , false , fmt .Errorf ("target Ingress %s/%s has a matching cluster-local rule without HTTP" , ingress .Namespace , ingress .Name )
428+ }
429+ return rule .HTTP .Paths , true , nil
430+ }
431+ }
432+
433+ dm .Status .MarkTargetIngressNotConfigured (fmt .Sprintf ("Ingress %s/%s has no cluster-local rule for host %q." , ingress .Namespace , ingress .Name , targetHost ))
434+ return nil , false , nil
435+ }
436+
437+ func servingReferenceKind (ref duckv1.KReference ) (string , bool ) {
438+ group := ref .Group
439+ if ref .APIVersion != "" {
440+ gv , err := schema .ParseGroupVersion (ref .APIVersion )
441+ if err != nil {
442+ return "" , false
443+ }
444+ group = gv .Group
445+ }
446+ if group != serving .GroupName {
447+ return "" , false
448+ }
449+ switch ref .Kind {
450+ case "Service" , "Route" :
451+ return ref .Kind , true
452+ }
453+ return "" , false
454+ }
455+
320456func (r * Reconciler ) reconcileDomainClaim (ctx context.Context , dm * v1beta1.DomainMapping ) error {
321457 dc , err := r .domainClaimLister .Get (dm .Name )
322458 if err != nil && ! apierrs .IsNotFound (err ) {
0 commit comments