feat: support KnativeIngress (#524)

This commit is contained in:
Vikizhao
2023-09-22 09:32:02 +08:00
committed by GitHub
parent fab734d39a
commit 2da1c62c69
16 changed files with 3431 additions and 218 deletions

View File

@@ -0,0 +1,19 @@
/*
Copyright 2019 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 resources holds simple functions for synthesizing child resources from
// an Ingress resource and any relevant Ingress controller configuration.
package resources

View File

@@ -0,0 +1,148 @@
/*
Copyright 2019 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 resources
import (
"strings"
"k8s.io/apimachinery/pkg/util/sets"
istiov1alpha3 "istio.io/api/networking/v1alpha3"
"knative.dev/networking/pkg/apis/networking/v1alpha1"
"knative.dev/pkg/network"
)
func MakeVirtualServiceRoute(hosts sets.String, http *v1alpha1.HTTPIngressPath) *istiov1alpha3.HTTPRoute {
matches := []*istiov1alpha3.HTTPMatchRequest{}
// Deduplicate hosts to avoid excessive matches, which cause a combinatorial expansion in Istio
for _, host := range hosts.List() {
matches = append(matches, makeMatch(host, http.Path, http.Headers))
}
weights := []*istiov1alpha3.HTTPRouteDestination{}
for _, split := range http.Splits {
var h *istiov1alpha3.Headers
if len(split.AppendHeaders) > 0 {
h = &istiov1alpha3.Headers{
Request: &istiov1alpha3.Headers_HeaderOperations{
Set: split.AppendHeaders,
},
}
}
weights = append(weights, &istiov1alpha3.HTTPRouteDestination{
Destination: &istiov1alpha3.Destination{
Host: network.GetServiceHostname(
split.ServiceName, split.ServiceNamespace),
Port: &istiov1alpha3.PortSelector{
Number: uint32(split.ServicePort.IntValue()),
},
},
Weight: int32(split.Percent),
Headers: h,
})
}
var h *istiov1alpha3.Headers
if len(http.AppendHeaders) > 0 {
h = &istiov1alpha3.Headers{
Request: &istiov1alpha3.Headers_HeaderOperations{
Set: http.AppendHeaders,
},
}
}
var rewrite *istiov1alpha3.HTTPRewrite
if http.RewriteHost != "" {
rewrite = &istiov1alpha3.HTTPRewrite{
Authority: http.RewriteHost,
}
}
route := &istiov1alpha3.HTTPRoute{
Retries: &istiov1alpha3.HTTPRetry{}, // Override default istio behaviour of retrying twice.
Match: matches,
Route: weights,
Rewrite: rewrite,
Headers: h,
}
return route
}
// getDistinctHostPrefixes deduplicate a set of prefix matches. For example, the set {a, aabb} can be
// reduced to {a}, as a prefix match on {a} accepts all the same inputs as {a, aabb}.
func getDistinctHostPrefixes(hosts sets.String) sets.String {
// First we sort the list. This ensures that we always process the smallest elements (which match against
// the most patterns, as they are less specific) first.
all := hosts.List()
ns := sets.NewString()
for _, h := range all {
prefixExists := false
h = hostPrefix(h)
// For each element, check if any existing elements are a prefix. We only insert if none are
// // For example, if we already have {a} and we are looking at "ab", we would not add it as it has a prefix of "a"
for e := range ns {
if strings.HasPrefix(h, e) {
prefixExists = true
break
}
}
if !prefixExists {
ns.Insert(h)
}
}
return ns
}
func makeMatch(host, path string, headers map[string]v1alpha1.HeaderMatch) *istiov1alpha3.HTTPMatchRequest {
match := &istiov1alpha3.HTTPMatchRequest{
Authority: &istiov1alpha3.StringMatch{
// Do not use Regex as Istio 1.4 or later has 100 bytes limitation.
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: host},
},
}
// Empty path is considered match all path. We only need to consider path
// when it's non-empty.
if path != "" {
match.Uri = &istiov1alpha3.StringMatch{
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: path},
}
}
for k, v := range headers {
match.Headers = map[string]*istiov1alpha3.StringMatch{
k: {
MatchType: &istiov1alpha3.StringMatch_Exact{
Exact: v.Exact,
},
},
}
}
return match
}
// hostPrefix returns an host to match either host or host:<any port>.
// For clusterLocalHost, it trims .svc.<local domain> from the host to match short host.
func hostPrefix(host string) string {
localDomainSuffix := ".svc." + network.GetClusterDomainName()
if !strings.HasSuffix(host, localDomainSuffix) {
return host
}
return strings.TrimSuffix(host, localDomainSuffix)
}

View File

@@ -0,0 +1,258 @@
/*
Copyright 2019 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 resources
import (
"testing"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/testing/protocmp"
istiov1alpha3 "istio.io/api/networking/v1alpha3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
"knative.dev/networking/pkg/apis/networking/v1alpha1"
"knative.dev/pkg/system"
_ "knative.dev/pkg/system/testing"
)
var (
defaultIngressRuleValue = &v1alpha1.HTTPIngressRuleValue{
Paths: []v1alpha1.HTTPIngressPath{{
Splits: []v1alpha1.IngressBackendSplit{{
Percent: 100,
IngressBackend: v1alpha1.IngressBackend{
ServiceNamespace: "test",
ServiceName: "test.svc.cluster.local",
ServicePort: intstr.FromInt(8080),
},
}},
}},
}
defaultIngress = v1alpha1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: system.Namespace(),
},
Spec: v1alpha1.IngressSpec{Rules: []v1alpha1.IngressRule{{
Hosts: []string{
"test-route.test-ns.svc.cluster.local",
},
HTTP: defaultIngressRuleValue,
}}},
}
defaultVSCmpOpts = protocmp.Transform()
)
func TestMakeVirtualServiceRoute_RewriteHost(t *testing.T) {
ingressPath := &v1alpha1.HTTPIngressPath{
RewriteHost: "the.target.host",
Splits: []v1alpha1.IngressBackendSplit{{
Percent: 100,
IngressBackend: v1alpha1.IngressBackend{
ServiceName: "the-svc",
ServiceNamespace: "the-ns",
ServicePort: intstr.FromInt(8080),
},
}},
}
route := MakeVirtualServiceRoute(sets.NewString("a.vanity.url", "another.vanity.url"), ingressPath)
expected := &istiov1alpha3.HTTPRoute{
Retries: &istiov1alpha3.HTTPRetry{},
Match: []*istiov1alpha3.HTTPMatchRequest{{
Authority: &istiov1alpha3.StringMatch{
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `a.vanity.url`},
},
}, {
Authority: &istiov1alpha3.StringMatch{
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `another.vanity.url`},
},
}},
Rewrite: &istiov1alpha3.HTTPRewrite{
Authority: "the.target.host",
},
Route: []*istiov1alpha3.HTTPRouteDestination{{
Destination: &istiov1alpha3.Destination{
Host: "the-svc.the-ns.svc.cluster.local",
Port: &istiov1alpha3.PortSelector{
Number: 8080,
},
},
Weight: 100,
}},
}
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
t.Error("Unexpected route (-want +got):", diff)
}
}
// One active target.
func TestMakeVirtualServiceRoute_Vanilla(t *testing.T) {
ingressPath := &v1alpha1.HTTPIngressPath{
Headers: map[string]v1alpha1.HeaderMatch{
"my-header": {
Exact: "my-header-value",
},
},
Splits: []v1alpha1.IngressBackendSplit{{
IngressBackend: v1alpha1.IngressBackend{
ServiceNamespace: "test-ns",
ServiceName: "revision-service",
ServicePort: intstr.FromInt(80),
},
Percent: 100,
}},
}
route := MakeVirtualServiceRoute(sets.NewString("a.com", "b.org"), ingressPath)
expected := &istiov1alpha3.HTTPRoute{
Retries: &istiov1alpha3.HTTPRetry{},
Match: []*istiov1alpha3.HTTPMatchRequest{{
Authority: &istiov1alpha3.StringMatch{
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `a.com`},
},
Headers: map[string]*istiov1alpha3.StringMatch{
"my-header": {
MatchType: &istiov1alpha3.StringMatch_Exact{
Exact: "my-header-value",
},
},
},
}, {
Authority: &istiov1alpha3.StringMatch{
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `b.org`},
},
Headers: map[string]*istiov1alpha3.StringMatch{
"my-header": {
MatchType: &istiov1alpha3.StringMatch_Exact{
Exact: "my-header-value",
},
},
},
}},
Route: []*istiov1alpha3.HTTPRouteDestination{{
Destination: &istiov1alpha3.Destination{
Host: "revision-service.test-ns.svc.cluster.local",
Port: &istiov1alpha3.PortSelector{Number: 80},
},
Weight: 100,
}},
}
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
t.Error("Unexpected route (-want +got):", diff)
}
}
// One active target.
func TestMakeVirtualServiceRoute_Internal(t *testing.T) {
ingressPath := &v1alpha1.HTTPIngressPath{
Splits: []v1alpha1.IngressBackendSplit{{
IngressBackend: v1alpha1.IngressBackend{
ServiceNamespace: "test-ns",
ServiceName: "revision-service",
ServicePort: intstr.FromInt(80),
},
Percent: 100,
}},
}
route := MakeVirtualServiceRoute(sets.NewString("a.default"), ingressPath)
expected := &istiov1alpha3.HTTPRoute{
Retries: &istiov1alpha3.HTTPRetry{},
Match: []*istiov1alpha3.HTTPMatchRequest{{
Authority: &istiov1alpha3.StringMatch{
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `a.default`},
},
}},
Route: []*istiov1alpha3.HTTPRouteDestination{{
Destination: &istiov1alpha3.Destination{
Host: "revision-service.test-ns.svc.cluster.local",
Port: &istiov1alpha3.PortSelector{Number: 80},
},
Weight: 100,
}},
}
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
t.Error("Unexpected route (-want +got):", diff)
}
}
// Two active targets.
func TestMakeVirtualServiceRoute_TwoTargets(t *testing.T) {
ingressPath := &v1alpha1.HTTPIngressPath{
Splits: []v1alpha1.IngressBackendSplit{{
IngressBackend: v1alpha1.IngressBackend{
ServiceNamespace: "test-ns",
ServiceName: "revision-service",
ServicePort: intstr.FromInt(80),
},
Percent: 90,
}, {
IngressBackend: v1alpha1.IngressBackend{
ServiceNamespace: "test-ns",
ServiceName: "new-revision-service",
ServicePort: intstr.FromInt(81),
},
Percent: 10,
}},
}
route := MakeVirtualServiceRoute(sets.NewString("test.org"), ingressPath)
expected := &istiov1alpha3.HTTPRoute{
Retries: &istiov1alpha3.HTTPRetry{},
Match: []*istiov1alpha3.HTTPMatchRequest{{
Authority: &istiov1alpha3.StringMatch{
MatchType: &istiov1alpha3.StringMatch_Prefix{Prefix: `test.org`},
},
}},
Route: []*istiov1alpha3.HTTPRouteDestination{{
Destination: &istiov1alpha3.Destination{
Host: "revision-service.test-ns.svc.cluster.local",
Port: &istiov1alpha3.PortSelector{Number: 80},
},
Weight: 90,
}, {
Destination: &istiov1alpha3.Destination{
Host: "new-revision-service.test-ns.svc.cluster.local",
Port: &istiov1alpha3.PortSelector{Number: 81},
},
Weight: 10,
}},
}
if diff := cmp.Diff(expected, route, defaultVSCmpOpts); diff != "" {
t.Error("Unexpected route (-want +got):", diff)
}
}
func TestGetDistinctHostPrefixes(t *testing.T) {
cases := []struct {
name string
in sets.String
out sets.String
}{
{"empty", sets.NewString(), sets.NewString()},
{"single element", sets.NewString("a"), sets.NewString("a")},
{"no overlap", sets.NewString("a", "b"), sets.NewString("a", "b")},
{"overlap", sets.NewString("a", "ab", "abc"), sets.NewString("a")},
{"multiple overlaps", sets.NewString("a", "ab", "abc", "xyz", "xy", "m"), sets.NewString("a", "xy", "m")},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
got := getDistinctHostPrefixes(tt.in)
if !tt.out.Equal(got) {
t.Fatalf("Expected %v, got %v", tt.out, got)
}
})
}
}