Skip to content

Commit eeb0379

Browse files
committed
Use Route traffic for Knative DomainMappings
Signed-off-by: kahirokunn <okinakahiro@gmail.com>
1 parent 47ddb67 commit eeb0379

7 files changed

Lines changed: 807 additions & 86 deletions

File tree

‎pkg/apis/serving/v1beta1/domainmapping_lifecycle.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,18 @@ func (dms *DomainMappingStatus) MarkIngressNotConfigured() {
127127
"IngressNotConfigured", "Ingress has not yet been reconciled.")
128128
}
129129

130+
// MarkTargetIngressNotConfigured marks DomainMappingConditionIngressReady unknown.
131+
func (dms *DomainMappingStatus) MarkTargetIngressNotConfigured(message string) {
132+
domainMappingCondSet.Manage(dms).MarkUnknown(DomainMappingConditionIngressReady,
133+
"IngressNotConfigured", message)
134+
}
135+
136+
// MarkTargetNotOwned marks DomainMappingConditionIngressReady false.
137+
func (dms *DomainMappingStatus) MarkTargetNotOwned(message string) {
138+
domainMappingCondSet.Manage(dms).MarkFalse(DomainMappingConditionIngressReady,
139+
"NotOwned", message)
140+
}
141+
130142
// MarkDomainClaimed updates the DomainMappingConditionDomainClaimed condition
131143
// to indicate that the domain was successfully claimed.
132144
func (dms *DomainMappingStatus) MarkDomainClaimed() {

‎pkg/apis/serving/v1beta1/domainmapping_lifecycle_test.go‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,57 @@ func TestReferenceResolvedCondition(t *testing.T) {
131131
apistest.CheckConditionFailed(dms, DomainMappingConditionReady, t)
132132
}
133133

134+
func TestTargetIngressConditions(t *testing.T) {
135+
tests := []struct {
136+
name string
137+
mark func(*DomainMappingStatus)
138+
wantStatus corev1.ConditionStatus
139+
wantReason string
140+
wantMsg string
141+
}{
142+
{
143+
name: "target ingress not configured",
144+
mark: func(dms *DomainMappingStatus) {
145+
dms.MarkTargetIngressNotConfigured("Waiting for target Ingress.")
146+
},
147+
wantStatus: corev1.ConditionUnknown,
148+
wantReason: "IngressNotConfigured",
149+
wantMsg: "Waiting for target Ingress.",
150+
},
151+
{
152+
name: "target resource not owned",
153+
mark: func(dms *DomainMappingStatus) {
154+
dms.MarkTargetNotOwned("Route does not own Ingress.")
155+
},
156+
wantStatus: corev1.ConditionFalse,
157+
wantReason: "NotOwned",
158+
wantMsg: "Route does not own Ingress.",
159+
},
160+
}
161+
162+
for _, test := range tests {
163+
t.Run(test.name, func(t *testing.T) {
164+
dms := &DomainMappingStatus{}
165+
dms.InitializeConditions()
166+
test.mark(dms)
167+
168+
got := dms.GetCondition(DomainMappingConditionIngressReady)
169+
if got == nil {
170+
t.Fatal("IngressReady condition is nil")
171+
}
172+
if got.Status != test.wantStatus {
173+
t.Errorf("Status = %q, want %q", got.Status, test.wantStatus)
174+
}
175+
if got.Reason != test.wantReason {
176+
t.Errorf("Reason = %q, want %q", got.Reason, test.wantReason)
177+
}
178+
if got.Message != test.wantMsg {
179+
t.Errorf("Message = %q, want %q", got.Message, test.wantMsg)
180+
}
181+
})
182+
}
183+
}
184+
134185
func TestCertificateNotReady(t *testing.T) {
135186
dms := &DomainMappingStatus{}
136187

‎pkg/reconciler/domainmapping/controller.go‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121

2222
"k8s.io/client-go/tools/cache"
23+
netv1alpha1 "knative.dev/networking/pkg/apis/networking/v1alpha1"
2324
netclient "knative.dev/networking/pkg/client/injection/client"
2425
certificateinformer "knative.dev/networking/pkg/client/injection/informers/networking/v1alpha1/certificate"
2526
domainclaiminformer "knative.dev/networking/pkg/client/injection/informers/networking/v1alpha1/clusterdomainclaim"
@@ -29,7 +30,10 @@ import (
2930
"knative.dev/pkg/controller"
3031
"knative.dev/pkg/logging"
3132
"knative.dev/pkg/resolver"
33+
servingv1 "knative.dev/serving/pkg/apis/serving/v1"
3234
"knative.dev/serving/pkg/apis/serving/v1beta1"
35+
routeinformer "knative.dev/serving/pkg/client/injection/informers/serving/v1/route"
36+
serviceinformer "knative.dev/serving/pkg/client/injection/informers/serving/v1/service"
3337
"knative.dev/serving/pkg/client/injection/informers/serving/v1beta1/domainmapping"
3438
kindreconciler "knative.dev/serving/pkg/client/injection/reconciler/serving/v1beta1/domainmapping"
3539
"knative.dev/serving/pkg/reconciler/domainmapping/config"
@@ -42,11 +46,15 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl
4246
domainmappingInformer := domainmapping.Get(ctx)
4347
ingressInformer := ingressinformer.Get(ctx)
4448
domainClaimInformer := domainclaiminformer.Get(ctx)
49+
routeInformer := routeinformer.Get(ctx)
50+
serviceInformer := serviceinformer.Get(ctx)
4551

4652
r := &Reconciler{
4753
certificateLister: certificateInformer.Lister(),
4854
ingressLister: ingressInformer.Lister(),
4955
domainClaimLister: domainClaimInformer.Lister(),
56+
routeLister: routeInformer.Lister(),
57+
serviceLister: serviceInformer.Lister(),
5058
netclient: netclient.Get(ctx),
5159
}
5260

@@ -71,7 +79,24 @@ func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl
7179
certificateInformer.Informer().AddEventHandler(handleControllerOf)
7280
ingressInformer.Informer().AddEventHandler(handleControllerOf)
7381

74-
r.resolver = resolver.NewURIResolverFromTracker(ctx, impl.Tracker)
82+
r.tracker = impl.Tracker
83+
r.resolver = resolver.NewURIResolverFromTracker(ctx, r.tracker)
84+
85+
// Track Route and Ingress changes because their HTTP paths are copied into
86+
// DomainMapping Ingresses. The resolver already tracks referenced Services.
87+
// Informer events may omit TypeMeta, so populate it before notifying the tracker.
88+
routeInformer.Informer().AddEventHandler(controller.HandleAll(
89+
controller.EnsureTypeMeta(r.tracker.OnChanged, servingv1.SchemeGroupVersion.WithKind("Route")),
90+
))
91+
ingressInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{
92+
FilterFunc: controller.FilterController(&servingv1.Route{}),
93+
Handler: controller.HandleAll(
94+
controller.EnsureTypeMeta(r.tracker.OnChanged, netv1alpha1.SchemeGroupVersion.WithKind("Ingress")),
95+
),
96+
})
97+
domainmappingInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
98+
DeleteFunc: r.tracker.OnDeletedObserver,
99+
})
75100

76101
return impl
77102
}

‎pkg/reconciler/domainmapping/reconciler.go‎

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package domainmapping
1818

1919
import (
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.
5764
type 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+
320456
func (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

Comments
 (0)