From 8df970ebb4539d673acc7467dae7df5d89f69063 Mon Sep 17 00:00:00 2001 From: Dave Protasowski Date: Thu, 12 Mar 2026 20:33:03 -0400 Subject: [PATCH 1/3] add an annotation to the kingress mapping tags to hostnames --- pkg/reconciler/route/resources/ingress.go | 53 ++++++++++++++++--- .../route/resources/ingress_test.go | 14 ++--- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/pkg/reconciler/route/resources/ingress.go b/pkg/reconciler/route/resources/ingress.go index ba593ebe421d..7fc4d361bd2e 100644 --- a/pkg/reconciler/route/resources/ingress.go +++ b/pkg/reconciler/route/resources/ingress.go @@ -43,6 +43,10 @@ import ( "knative.dev/serving/pkg/reconciler/route/traffic" ) +const ( + TagToHostAnnotationKey = networking.GroupName + "/tag-to-host" +) + // MakeIngressTLS creates IngressTLS to configure the ingress TLS. func MakeIngressTLS(cert *netv1alpha1.Certificate, hostNames []string) netv1alpha1.IngressTLS { return netv1alpha1.IngressTLS{ @@ -79,11 +83,12 @@ func MakeIngressWithRollout( ingressClass string, acmeChallenges ...netv1alpha1.HTTP01Challenge, ) (*netv1alpha1.Ingress, error) { - spec, err := makeIngressSpec(ctx, r, tls, tc, ro, acmeChallenges...) + spec, tagToHost, err := makeIngressSpec(ctx, r, tls, tc, ro, acmeChallenges...) if err != nil { return nil, err } - return &netv1alpha1.Ingress{ + + ing := &netv1alpha1.Ingress{ ObjectMeta: metav1.ObjectMeta{ Name: names.Ingress(r), Namespace: r.Namespace, @@ -98,7 +103,24 @@ func MakeIngressWithRollout( OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(r)}, }, Spec: spec, - }, nil + } + + if len(tagToHost) > 0 { + ing.Annotations[TagToHostAnnotationKey] = serializeTagToHostMap(ctx, tagToHost) + } + + return ing, nil +} + +func serializeTagToHostMap(ctx context.Context, mapping map[string][]string) string { + sr, err := json.Marshal(mapping) + if err != nil { + // This must never happen in the normal course of things. + logging.FromContext(ctx).Warnw("Error serializing tag to host mapping: "+spew.Sprint(mapping), + zap.Error(err)) + return "" + } + return string(sr) } func serializeRollout(ctx context.Context, r *traffic.Rollout) string { @@ -120,7 +142,9 @@ func makeIngressSpec( tc *traffic.Config, ro *traffic.Rollout, acmeChallenges ...netv1alpha1.HTTP01Challenge, -) (netv1alpha1.IngressSpec, error) { +) (netv1alpha1.IngressSpec, map[string][]string, error) { + tagToHost := make(map[string][]string) + // Domain should have been specified in route status // before calling this func. names := make([]string, 0, len(tc.Targets)) @@ -152,7 +176,7 @@ func makeIngressSpec( for _, visibility := range visibilities { domains, err := domains.GetDomainsForVisibility(ctx, name, r, visibility) if err != nil { - return netv1alpha1.IngressSpec{}, err + return netv1alpha1.IngressSpec{}, nil, err } domainRules := makeIngressRules(domains, r.Namespace, visibility, tc.Targets[name], ro.RolloutsByTag(name), networkConfig.SystemInternalTLSEnabled()) @@ -205,19 +229,32 @@ func makeIngressSpec( } rules = append(rules, domainRules...) + + if name != traffic.DefaultTarget { + longestDomain := "" + for d := range domains { + if len(longestDomain) < len(d) { + longestDomain = d + } + } + + tagToHost[name] = append(tagToHost[name], longestDomain) + } } } httpOption, err := servingnetworking.GetHTTPOption(ctx, config.FromContext(ctx).Network, r.GetAnnotations()) if err != nil { - return netv1alpha1.IngressSpec{}, err + return netv1alpha1.IngressSpec{}, nil, err } - return netv1alpha1.IngressSpec{ + spec := netv1alpha1.IngressSpec{ Rules: rules, TLS: tls, HTTPOption: httpOption, - }, nil + } + + return spec, tagToHost, nil } // MakeACMEIngressPath converts an ACME challenge into an HTTPIngressPath. diff --git a/pkg/reconciler/route/resources/ingress_test.go b/pkg/reconciler/route/resources/ingress_test.go index 2bfd12db0ff8..5f9bbbd3a9d4 100644 --- a/pkg/reconciler/route/resources/ingress_test.go +++ b/pkg/reconciler/route/resources/ingress_test.go @@ -141,6 +141,7 @@ func TestMakeIngressWithTaggedRollout(t *testing.T) { networking.IngressClassAnnotationKey: ingressClass, networking.RolloutAnnotationKey: `{"configurations":[{"configurationName":"valhalla","percent":100,"revisions":[{"revisionName":"valhalla-01982","percent":100}],"stepParams":{}},{"configurationName":"thor","tag":"tagged","percent":100,"revisions":[{"revisionName":"thor-02020","percent":100}],"stepParams":{}}]}`, "test-annotation": "bar", + TagToHostAnnotationKey: `{"tagged":["tagged-test-route.test-ns.svc.cluster.local","tagged-test-route.test-ns.example.com"]}`, }, OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(r)}, } @@ -245,6 +246,7 @@ func TestMakeIngressWithActualRollout(t *testing.T) { Annotations: map[string]string{ networking.IngressClassAnnotationKey: ingressClass, networking.RolloutAnnotationKey: serializeRollout(context.Background(), ro), + TagToHostAnnotationKey: `{"hammer":["hammer-test-route.test-ns.svc.cluster.local","hammer-test-route.test-ns.example.com"]}`, }, OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(r)}, } @@ -587,7 +589,7 @@ func TestMakeIngressSpecCorrectRules(t *testing.T) { tc := &traffic.Config{Targets: targets} ro := tc.BuildRollout() - ci, err := makeIngressSpec(testContext(), r, nil /*tls*/, tc, ro) + ci, _, err := makeIngressSpec(testContext(), r, nil /*tls*/, tc, ro) if err != nil { t.Error("Unexpected error", err) } @@ -662,7 +664,7 @@ func TestMakeIngressSpecCorrectRuleVisibility(t *testing.T) { Visibility: c.serviceVisibility, } ro := tc.BuildRollout() - ci, err := makeIngressSpec(testContext(), c.route, nil /*tls*/, tc, ro) + ci, _, err := makeIngressSpec(testContext(), c.route, nil /*tls*/, tc, ro) if err != nil { t.Error("Unexpected error", err) } @@ -842,7 +844,7 @@ func TestMakeIngressSpecCorrectRulesWithTagBasedRouting(t *testing.T) { tc := &traffic.Config{Targets: targets} ro := tc.BuildRollout() - ci, err := makeIngressSpec(ctx, r, nil /*tls*/, tc, ro) + ci, _, err := makeIngressSpec(ctx, r, nil /*tls*/, tc, ro) if err != nil { t.Error("Unexpected error", err) } @@ -1221,7 +1223,7 @@ func TestMakeIngressWithActivatorCA(t *testing.T) { tc := &traffic.Config{Targets: targets} ro := tc.BuildRollout() - ci, err := makeIngressSpec(testContextWithActivatorCA(), r, nil /*tls*/, tc, ro) + ci, _, err := makeIngressSpec(testContextWithActivatorCA(), r, nil /*tls*/, tc, ro) if err != nil { t.Error("Unexpected error", err) } @@ -1352,7 +1354,7 @@ func TestMakeIngressACMEChallenges(t *testing.T) { } ro := tc.BuildRollout() - ci, err := makeIngressSpec(testContext(), r, nil /*tls*/, tc, ro, acmeChallenge) + ci, _, err := makeIngressSpec(testContext(), r, nil /*tls*/, tc, ro, acmeChallenge) if err != nil { t.Error("Unexpected error", err) } @@ -1442,7 +1444,7 @@ func TestMakeIngressACMEChallengesWithTrafficTags(t *testing.T) { } ro := tc.BuildRollout() - ci, err := makeIngressSpec(testContext(), r, nil /*tls*/, tc, ro, acmeChallenges...) + ci, _, err := makeIngressSpec(testContext(), r, nil /*tls*/, tc, ro, acmeChallenges...) if err != nil { t.Fatal("Unexpected error", err) } From 4b6cbc2d7f736bb637261a8d7682341499ef2209 Mon Sep 17 00:00:00 2001 From: Dave Protasowski Date: Mon, 16 Mar 2026 10:22:38 -0400 Subject: [PATCH 2/3] rebase --- pkg/reconciler/route/resources/ingress.go | 6 +----- pkg/reconciler/route/resources/ingress_test.go | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/reconciler/route/resources/ingress.go b/pkg/reconciler/route/resources/ingress.go index 7fc4d361bd2e..4db20c5277a4 100644 --- a/pkg/reconciler/route/resources/ingress.go +++ b/pkg/reconciler/route/resources/ingress.go @@ -43,10 +43,6 @@ import ( "knative.dev/serving/pkg/reconciler/route/traffic" ) -const ( - TagToHostAnnotationKey = networking.GroupName + "/tag-to-host" -) - // MakeIngressTLS creates IngressTLS to configure the ingress TLS. func MakeIngressTLS(cert *netv1alpha1.Certificate, hostNames []string) netv1alpha1.IngressTLS { return netv1alpha1.IngressTLS{ @@ -106,7 +102,7 @@ func MakeIngressWithRollout( } if len(tagToHost) > 0 { - ing.Annotations[TagToHostAnnotationKey] = serializeTagToHostMap(ctx, tagToHost) + ing.Annotations[networking.TagToHostAnnotationKey] = serializeTagToHostMap(ctx, tagToHost) } return ing, nil diff --git a/pkg/reconciler/route/resources/ingress_test.go b/pkg/reconciler/route/resources/ingress_test.go index 5f9bbbd3a9d4..195b9e93e0b5 100644 --- a/pkg/reconciler/route/resources/ingress_test.go +++ b/pkg/reconciler/route/resources/ingress_test.go @@ -141,7 +141,7 @@ func TestMakeIngressWithTaggedRollout(t *testing.T) { networking.IngressClassAnnotationKey: ingressClass, networking.RolloutAnnotationKey: `{"configurations":[{"configurationName":"valhalla","percent":100,"revisions":[{"revisionName":"valhalla-01982","percent":100}],"stepParams":{}},{"configurationName":"thor","tag":"tagged","percent":100,"revisions":[{"revisionName":"thor-02020","percent":100}],"stepParams":{}}]}`, "test-annotation": "bar", - TagToHostAnnotationKey: `{"tagged":["tagged-test-route.test-ns.svc.cluster.local","tagged-test-route.test-ns.example.com"]}`, + networking.TagToHostAnnotationKey: `{"tagged":["tagged-test-route.test-ns.svc.cluster.local","tagged-test-route.test-ns.example.com"]}`, }, OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(r)}, } @@ -246,7 +246,7 @@ func TestMakeIngressWithActualRollout(t *testing.T) { Annotations: map[string]string{ networking.IngressClassAnnotationKey: ingressClass, networking.RolloutAnnotationKey: serializeRollout(context.Background(), ro), - TagToHostAnnotationKey: `{"hammer":["hammer-test-route.test-ns.svc.cluster.local","hammer-test-route.test-ns.example.com"]}`, + networking.TagToHostAnnotationKey: `{"hammer":["hammer-test-route.test-ns.svc.cluster.local","hammer-test-route.test-ns.example.com"]}`, }, OwnerReferences: []metav1.OwnerReference{*kmeta.NewControllerRef(r)}, } From 9d93ac9ff103261ec0f8f52621d0db97975636d5 Mon Sep 17 00:00:00 2001 From: Dave Protasowski Date: Fri, 13 Mar 2026 20:24:44 -0400 Subject: [PATCH 3/3] return a structured response when getting a list of domains --- pkg/reconciler/route/domains/domains.go | 20 ++++++++++++++------ pkg/reconciler/route/domains/domains_test.go | 16 +++++++++++++++- pkg/reconciler/route/resources/ingress.go | 11 ++--------- pkg/reconciler/route/route.go | 8 ++++---- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/pkg/reconciler/route/domains/domains.go b/pkg/reconciler/route/domains/domains.go index 50cce3e561f6..6bd9ac077823 100644 --- a/pkg/reconciler/route/domains/domains.go +++ b/pkg/reconciler/route/domains/domains.go @@ -45,6 +45,11 @@ const HTTPScheme string = "http" var ErrDomainName = errors.New("domain name error") +type Domains struct { + Primary string + Expanded sets.Set[string] +} + // GetAllDomainsAndTags returns all of the domains and tags(including subdomains) associated with a Route func GetAllDomainsAndTags(ctx context.Context, r *v1.Route, names []string, visibility map[string]netv1alpha1.IngressVisibility) (map[string]string, error) { domainTagMap := make(map[string]string) @@ -69,10 +74,11 @@ func GetAllDomainsAndTags(ctx context.Context, r *v1.Route, names []string, visi } // GetDomainsForVisibility return all domains for the specified visibility. -func GetDomainsForVisibility(ctx context.Context, targetName string, r *v1.Route, visibility netv1alpha1.IngressVisibility) (sets.Set[string], error) { +func GetDomainsForVisibility(ctx context.Context, targetName string, r *v1.Route, visibility netv1alpha1.IngressVisibility) (Domains, error) { + domains := Domains{} hostname, err := HostnameFromTemplate(ctx, r.Name, targetName) if err != nil { - return nil, err + return domains, err } meta := r.ObjectMeta.DeepCopy() @@ -81,13 +87,15 @@ func GetDomainsForVisibility(ctx context.Context, targetName string, r *v1.Route domain, err := DomainNameFromTemplate(ctx, *meta, hostname) if err != nil { - return nil, err + return domains, err } - domains := []string{domain} + + domains.Primary = domain + domains.Expanded = sets.New(domain) if isClusterLocal { - domains = sets.List(ingress.ExpandedHosts(sets.New(domains...))) + domains.Expanded = ingress.ExpandedHosts(domains.Expanded) } - return sets.New(domains...), err + return domains, err } // DomainNameFromTemplate generates domain name base on the template specified in the `config-network` ConfigMap. diff --git a/pkg/reconciler/route/domains/domains_test.go b/pkg/reconciler/route/domains/domains_test.go index eac174d9637b..3fd3125a3f0d 100644 --- a/pkg/reconciler/route/domains/domains_test.go +++ b/pkg/reconciler/route/domains/domains_test.go @@ -281,6 +281,7 @@ func TestGetDomainsForVisibility(t *testing.T) { visibility v1alpha1.IngressVisibility domainTemplate string tagTemplate string + primary string want sets.Set[string] }{ { @@ -289,6 +290,7 @@ func TestGetDomainsForVisibility(t *testing.T) { visibility: v1alpha1.IngressVisibilityClusterLocal, domainTemplate: "{{.Name}}.{{.Namespace}}.{{.Domain}}", tagTemplate: "{{.Name}}-{{.Tag}}", + primary: "myroute.default.svc.cluster.local", want: sets.New( "myroute.default", "myroute.default.svc", @@ -300,6 +302,7 @@ func TestGetDomainsForVisibility(t *testing.T) { visibility: v1alpha1.IngressVisibilityExternalIP, domainTemplate: "{{.Name}}.{{.Namespace}}.{{.Domain}}", tagTemplate: "{{.Name}}-{{.Tag}}", + primary: "myroute.default.example.com", want: sets.New( "myroute.default.example.com", ), @@ -309,6 +312,7 @@ func TestGetDomainsForVisibility(t *testing.T) { visibility: v1alpha1.IngressVisibilityClusterLocal, domainTemplate: "{{.Name}}.{{.Namespace}}.{{.Domain}}", tagTemplate: "{{.Name}}-{{.Tag}}", + primary: "myroute-test.default.svc.cluster.local", want: sets.New( "myroute-test.default", "myroute-test.default.svc", @@ -320,6 +324,7 @@ func TestGetDomainsForVisibility(t *testing.T) { visibility: v1alpha1.IngressVisibilityExternalIP, domainTemplate: "{{.Name}}.{{.Namespace}}.{{.Domain}}", tagTemplate: "{{.Name}}-{{.Tag}}", + primary: "myroute-test.default.example.com", want: sets.New( "myroute-test.default.example.com", ), @@ -329,6 +334,7 @@ func TestGetDomainsForVisibility(t *testing.T) { visibility: v1alpha1.IngressVisibilityClusterLocal, domainTemplate: "{{.Name}}.{{.Namespace}}.{{.Domain}}", tagTemplate: "{{.Tag}}-{{.Name}}", + primary: "myroute.default.svc.cluster.local", want: sets.New( "myroute.default", "myroute.default.svc", @@ -340,6 +346,7 @@ func TestGetDomainsForVisibility(t *testing.T) { visibility: v1alpha1.IngressVisibilityExternalIP, domainTemplate: "{{.Name}}.{{.Namespace}}.{{.Domain}}", tagTemplate: "{{.Tag}}-{{.Name}}", + primary: "myroute.default.example.com", want: sets.New( "myroute.default.example.com", ), @@ -349,6 +356,7 @@ func TestGetDomainsForVisibility(t *testing.T) { visibility: v1alpha1.IngressVisibilityClusterLocal, domainTemplate: "{{.Name}}.{{.Namespace}}.{{.Domain}}", tagTemplate: "{{.Tag}}-{{.Name}}", + primary: "test-myroute.default.svc.cluster.local", want: sets.New( "test-myroute.default", "test-myroute.default.svc", @@ -360,6 +368,7 @@ func TestGetDomainsForVisibility(t *testing.T) { visibility: v1alpha1.IngressVisibilityExternalIP, domainTemplate: "{{.Name}}.{{.Namespace}}.{{.Domain}}", tagTemplate: "{{.Tag}}-{{.Name}}", + primary: "test-myroute.default.example.com", want: sets.New( "test-myroute.default.example.com", ), @@ -384,11 +393,16 @@ func TestGetDomainsForVisibility(t *testing.T) { cfg.Network.TagTemplate = tt.tagTemplate ctx = config.ToContext(ctx, cfg) + want := Domains{ + Primary: tt.primary, + Expanded: tt.want, + } + got, err := GetDomainsForVisibility(ctx, tt.tag, route, tt.visibility) if err != nil { t.Errorf("failed calling GetDomainsForVisibility: %v", err) } - if diff := cmp.Diff(tt.want, got); diff != "" { + if diff := cmp.Diff(want, got); diff != "" { t.Error("GetDomainsForVisibility() diff (-want +got):", diff) } }) diff --git a/pkg/reconciler/route/resources/ingress.go b/pkg/reconciler/route/resources/ingress.go index 4db20c5277a4..5a1ff1ab7c1f 100644 --- a/pkg/reconciler/route/resources/ingress.go +++ b/pkg/reconciler/route/resources/ingress.go @@ -174,7 +174,7 @@ func makeIngressSpec( if err != nil { return netv1alpha1.IngressSpec{}, nil, err } - domainRules := makeIngressRules(domains, r.Namespace, + domainRules := makeIngressRules(domains.Expanded, r.Namespace, visibility, tc.Targets[name], ro.RolloutsByTag(name), networkConfig.SystemInternalTLSEnabled()) // Apply tag header routing and ACME merging to each rule @@ -227,14 +227,7 @@ func makeIngressSpec( rules = append(rules, domainRules...) if name != traffic.DefaultTarget { - longestDomain := "" - for d := range domains { - if len(longestDomain) < len(d) { - longestDomain = d - } - } - - tagToHost[name] = append(tagToHost[name], longestDomain) + tagToHost[name] = append(tagToHost[name], domains.Primary) } } } diff --git a/pkg/reconciler/route/route.go b/pkg/reconciler/route/route.go index d6be181c957e..c5293efb98b7 100644 --- a/pkg/reconciler/route/route.go +++ b/pkg/reconciler/route/route.go @@ -326,7 +326,7 @@ func (c *Reconciler) clusterLocalDomainTLS(ctx context.Context, r *v1.Route, tc return nil, err } - desiredCert := resources.MakeClusterLocalCertificate(r, name, localDomains, certClass(ctx, r)) + desiredCert := resources.MakeClusterLocalCertificate(r, name, localDomains.Expanded, certClass(ctx, r)) cert, err := networkaccessor.ReconcileCertificate(ctx, r, desiredCert, c) if err != nil { if kaccessor.IsNotOwned(err) { @@ -342,19 +342,19 @@ func (c *Reconciler) clusterLocalDomainTLS(ctx context.Context, r *v1.Route, tc // r.Status.URL contains the major domain, // so only change if the cert is for the major domain - if localDomains.Has(r.Status.URL.Host) { + if localDomains.Expanded.Has(r.Status.URL.Host) { r.Status.URL.Scheme = "https" } r.Status.MarkCertificateReady(cert.Name) - tls = append(tls, resources.MakeIngressTLS(cert, sets.List(localDomains))) + tls = append(tls, resources.MakeIngressTLS(cert, sets.List(localDomains.Expanded))) } else if cert.IsFailed() { r.Status.MarkCertificateProvisionFailed(cert) } else { r.Status.MarkCertificateNotReady(cert) } - for s := range localDomains { + for s := range localDomains.Expanded { usedDomains[s] = s } }