diff --git a/config/core/300-resources/route.yaml b/config/core/300-resources/route.yaml index 725bb4cc0928..5c98afd97215 100644 --- a/config/core/300-resources/route.yaml +++ b/config/core/300-resources/route.yaml @@ -93,6 +93,13 @@ spec: description: TrafficTarget holds a single entry of the routing table for a Route. type: object properties: + appendHeaders: + description: |- + AppendHeaders allow specifying additional HTTP headers to add + before forwarding a request to the destination service. + type: object + additionalProperties: + type: string configurationName: description: |- ConfigurationName of a configuration to whose latest revision we will send @@ -219,6 +226,13 @@ spec: description: TrafficTarget holds a single entry of the routing table for a Route. type: object properties: + appendHeaders: + description: |- + AppendHeaders allow specifying additional HTTP headers to add + before forwarding a request to the destination service. + type: object + additionalProperties: + type: string configurationName: description: |- ConfigurationName of a configuration to whose latest revision we will send diff --git a/config/core/300-resources/service.yaml b/config/core/300-resources/service.yaml index e2b14b4fbbed..627420799ca8 100644 --- a/config/core/300-resources/service.yaml +++ b/config/core/300-resources/service.yaml @@ -1520,6 +1520,13 @@ spec: description: TrafficTarget holds a single entry of the routing table for a Route. type: object properties: + appendHeaders: + description: |- + AppendHeaders allow specifying additional HTTP headers to add + before forwarding a request to the destination service. + type: object + additionalProperties: + type: string configurationName: description: |- ConfigurationName of a configuration to whose latest revision we will send @@ -1656,6 +1663,13 @@ spec: description: TrafficTarget holds a single entry of the routing table for a Route. type: object properties: + appendHeaders: + description: |- + AppendHeaders allow specifying additional HTTP headers to add + before forwarding a request to the destination service. + type: object + additionalProperties: + type: string configurationName: description: |- ConfigurationName of a configuration to whose latest revision we will send diff --git a/docs/serving-api.md b/docs/serving-api.md index 77aad5ea6a63..6609f0485fdc 100644 --- a/docs/serving-api.md +++ b/docs/serving-api.md @@ -2082,6 +2082,19 @@ status, and is disallowed on spec. URL must contain a scheme (e.g. http://) and a hostname, but may not contain anything else (e.g. basic auth, url path, etc.)

+ + +appendHeaders
+ +map[string]string + + + +(Optional) +

AppendHeaders allow specifying additional HTTP headers to add +before forwarding a request to the destination service.

+ +
diff --git a/pkg/apis/serving/v1/route_types.go b/pkg/apis/serving/v1/route_types.go index a07112c87ee1..cad7b5a3188d 100644 --- a/pkg/apis/serving/v1/route_types.go +++ b/pkg/apis/serving/v1/route_types.go @@ -108,6 +108,12 @@ type TrafficTarget struct { // a hostname, but may not contain anything else (e.g. basic auth, url path, etc.) // +optional URL *apis.URL `json:"url,omitempty"` + + // AppendHeaders allow specifying additional HTTP headers to add + // before forwarding a request to the destination service. + // + // +optional + AppendHeaders map[string]string `json:"appendHeaders,omitempty"` } // RouteSpec holds the desired state of the Route (from the client). diff --git a/pkg/apis/serving/v1/route_validation.go b/pkg/apis/serving/v1/route_validation.go index a30b047fa88e..7b4b0bbe79c8 100644 --- a/pkg/apis/serving/v1/route_validation.go +++ b/pkg/apis/serving/v1/route_validation.go @@ -101,7 +101,8 @@ func (tt *TrafficTarget) Validate(ctx context.Context) *apis.FieldError { errs := tt.validateLatestRevision(ctx) errs = tt.validateRevisionAndConfiguration(ctx, errs) errs = tt.validateTrafficPercentage(errs) - return tt.validateURL(ctx, errs) + errs = tt.validateURL(ctx, errs) + return tt.validateAppendHeaders(ctx, errs) } func (tt *TrafficTarget) validateRevisionAndConfiguration(ctx context.Context, errs *apis.FieldError) *apis.FieldError { @@ -191,6 +192,24 @@ func (tt *TrafficTarget) validateURL(ctx context.Context, errs *apis.FieldError) return errs } +func (tt *TrafficTarget) validateAppendHeaders(ctx context.Context, errs *apis.FieldError) *apis.FieldError { + if len(tt.AppendHeaders) == 0 { + return errs + } + + // AppendHeaders is not allowed in status. + if apis.IsInStatus(ctx) { + return errs.Also(apis.ErrDisallowedFields("appendHeaders")) + } + + for key := range tt.AppendHeaders { + if len(validation.IsHTTPHeaderName(key)) > 0 { + errs = errs.Also(apis.ErrInvalidKeyName(key, "appendHeaders")) + } + } + return errs +} + // validateLabels function validates route labels. func (r *Route) validateLabels() (errs *apis.FieldError) { if val, ok := r.Labels[serving.ServiceLabelKey]; ok { diff --git a/pkg/apis/serving/v1/route_validation_test.go b/pkg/apis/serving/v1/route_validation_test.go index 50b3a62cf98a..3246db68b037 100644 --- a/pkg/apis/serving/v1/route_validation_test.go +++ b/pkg/apis/serving/v1/route_validation_test.go @@ -255,6 +255,38 @@ func TestTrafficTargetValidation(t *testing.T) { }, wc: apis.WithinSpec, want: apis.ErrDisallowedFields("url"), + }, { + name: "valid append headers", + tt: &TrafficTarget{ + ConfigurationName: "foo", + Percent: ptr.Int64(100), + AppendHeaders: map[string]string{ + "foo": "bar", + }, + }, + wc: apis.WithinSpec, + }, { + name: "invalid append headers (status)", + tt: &TrafficTarget{ + RevisionName: "v1", + Percent: ptr.Int64(100), + AppendHeaders: map[string]string{ + "foo": "bar", + }, + }, + wc: apis.WithinStatus, + want: apis.ErrDisallowedFields("appendHeaders"), + }, { + name: "invalid append headers (invalid key)", + tt: &TrafficTarget{ + ConfigurationName: "foo", + Percent: ptr.Int64(100), + AppendHeaders: map[string]string{ + "foo/bar": "bar", + }, + }, + wc: apis.WithinSpec, + want: apis.ErrInvalidKeyName("foo/bar", "appendHeaders"), }} for _, test := range tests { diff --git a/pkg/apis/serving/v1/zz_generated.deepcopy.go b/pkg/apis/serving/v1/zz_generated.deepcopy.go index c42bb4b927c7..576cd7b9e9a0 100644 --- a/pkg/apis/serving/v1/zz_generated.deepcopy.go +++ b/pkg/apis/serving/v1/zz_generated.deepcopy.go @@ -559,6 +559,13 @@ func (in *TrafficTarget) DeepCopyInto(out *TrafficTarget) { *out = new(apis.URL) (*in).DeepCopyInto(*out) } + if in.AppendHeaders != nil { + in, out := &in.AppendHeaders, &out.AppendHeaders + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } diff --git a/pkg/reconciler/route/resources/ingress.go b/pkg/reconciler/route/resources/ingress.go index ba593ebe421d..ce24020d4ad3 100644 --- a/pkg/reconciler/route/resources/ingress.go +++ b/pkg/reconciler/route/resources/ingress.go @@ -322,10 +322,10 @@ func makeBaseIngressPath(ns string, targets traffic.RevisionTargets, ServicePort: servicePort, }, Percent: int(*t.Percent), - AppendHeaders: map[string]string{ + AppendHeaders: kmeta.UnionMaps(map[string]string{ activator.RevisionHeaderName: t.TrafficTarget.RevisionName, activator.RevisionHeaderNamespace: ns, - }, + }, t.TrafficTarget.AppendHeaders), }) } else { for i := range cfg.Revisions { @@ -334,15 +334,13 @@ func makeBaseIngressPath(ns string, targets traffic.RevisionTargets, IngressBackend: netv1alpha1.IngressBackend{ ServiceNamespace: ns, ServiceName: rev.RevisionName, - // Port on the public service must match port on the activator. - // Otherwise, the serverless services can't guarantee seamless positive handoff. - ServicePort: servicePort, + ServicePort: servicePort, }, Percent: rev.Percent, - AppendHeaders: map[string]string{ + AppendHeaders: kmeta.UnionMaps(map[string]string{ activator.RevisionHeaderName: rev.RevisionName, activator.RevisionHeaderNamespace: ns, - }, + }, t.TrafficTarget.AppendHeaders), }) } } diff --git a/pkg/reconciler/route/resources/ingress_test.go b/pkg/reconciler/route/resources/ingress_test.go index 2bfd12db0ff8..c7dacff2f51f 100644 --- a/pkg/reconciler/route/resources/ingress_test.go +++ b/pkg/reconciler/route/resources/ingress_test.go @@ -679,6 +679,82 @@ func TestMakeIngressSpecCorrectRuleVisibility(t *testing.T) { } } +func TestMakeIngressSpecWithAppendHeaders(t *testing.T) { + targets := map[string]traffic.RevisionTargets{ + traffic.DefaultTarget: {{ + TrafficTarget: v1.TrafficTarget{ + ConfigurationName: "config", + RevisionName: "v2", + Percent: ptr.Int64(100), + AppendHeaders: map[string]string{ + "Foo": "Bar", + }, + }, + }}, + } + + r := Route(ns, "test-route", WithURL) + + expected := []netv1alpha1.IngressRule{{ + Hosts: []string{ + "test-route." + ns, + "test-route." + ns + ".svc", + pkgnet.GetServiceHostname("test-route", ns), + }, + HTTP: &netv1alpha1.HTTPIngressRuleValue{ + Paths: []netv1alpha1.HTTPIngressPath{{ + Splits: []netv1alpha1.IngressBackendSplit{{ + IngressBackend: netv1alpha1.IngressBackend{ + ServiceNamespace: ns, + ServiceName: "v2", + ServicePort: intstr.FromInt(80), + }, + Percent: 100, + AppendHeaders: map[string]string{ + "Knative-Serving-Revision": "v2", + "Knative-Serving-Namespace": ns, + "Foo": "Bar", + }, + }}, + }}, + }, + Visibility: netv1alpha1.IngressVisibilityClusterLocal, + }, { + Hosts: []string{ + "test-route." + ns + ".example.com", + }, + HTTP: &netv1alpha1.HTTPIngressRuleValue{ + Paths: []netv1alpha1.HTTPIngressPath{{ + Splits: []netv1alpha1.IngressBackendSplit{{ + IngressBackend: netv1alpha1.IngressBackend{ + ServiceNamespace: ns, + ServiceName: "v2", + ServicePort: intstr.FromInt(80), + }, + Percent: 100, + AppendHeaders: map[string]string{ + "Knative-Serving-Revision": "v2", + "Knative-Serving-Namespace": ns, + "Foo": "Bar", + }, + }}, + }}, + }, + Visibility: netv1alpha1.IngressVisibilityExternalIP, + }} + + tc := &traffic.Config{Targets: targets} + ro := tc.BuildRollout() + ci, err := makeIngressSpec(testContext(), r, nil /*tls*/, tc, ro) + if err != nil { + t.Error("Unexpected error", err) + } + + if !cmp.Equal(expected, ci.Rules) { + t.Error("Unexpected rules (-want, +got):", cmp.Diff(expected, ci.Rules)) + } +} + func TestMakeIngressSpecCorrectRulesWithTagBasedRouting(t *testing.T) { targets := map[string]traffic.RevisionTargets{ traffic.DefaultTarget: {{