mirror of
https://github.com/alibaba/higress.git
synced 2026-05-29 23:27:28 +08:00
feat: support KnativeIngress (#524)
This commit is contained in:
19
pkg/ingress/kube/kingress/resources/doc.go
Normal file
19
pkg/ingress/kube/kingress/resources/doc.go
Normal 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
|
||||
148
pkg/ingress/kube/kingress/resources/virtual_service.go
Normal file
148
pkg/ingress/kube/kingress/resources/virtual_service.go
Normal 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)
|
||||
}
|
||||
258
pkg/ingress/kube/kingress/resources/virtual_service_test.go
Normal file
258
pkg/ingress/kube/kingress/resources/virtual_service_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user