traefik/pkg/provider/kubernetes/knative/kubernetes_test.go
2025-10-08 09:32:05 +01:00

479 lines
15 KiB
Go

package knative
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/paerser/types"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/k8s"
"k8s.io/apimachinery/pkg/runtime"
kubefake "k8s.io/client-go/kubernetes/fake"
kscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/ptr"
knativenetworkingv1alpha1 "knative.dev/networking/pkg/apis/networking/v1alpha1"
knfake "knative.dev/networking/pkg/client/clientset/versioned/fake"
)
func init() {
// required by k8s.MustParseYaml
if err := knativenetworkingv1alpha1.AddToScheme(kscheme.Scheme); err != nil {
panic(err)
}
}
func Test_loadConfiguration(t *testing.T) {
testCases := []struct {
desc string
paths []string
want *dynamic.Configuration
wantLen int
}{
{
desc: "Wrong ingress class",
paths: []string{"wrong_ingress_class.yaml"},
wantLen: 0,
want: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{},
Services: map[string]*dynamic.Service{},
Middlewares: map[string]*dynamic.Middleware{},
},
},
},
{
desc: "Cluster Local",
paths: []string{"cluster_local.yaml", "services.yaml"},
wantLen: 1,
want: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-helloworld-go-rule-0-path-0": {
EntryPoints: []string{"priv-http", "priv-https"},
Service: "default-helloworld-go-rule-0-path-0-wrr",
Rule: "(Host(`helloworld-go.default`) || Host(`helloworld-go.default.svc`) || Host(`helloworld-go.default.svc.cluster.local`))",
Middlewares: []string{},
},
},
Services: map[string]*dynamic.Service{
"default-helloworld-go-rule-0-path-0-split-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: types.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{
{
URL: "http://10.43.38.208:80",
},
},
},
},
"default-helloworld-go-rule-0-path-0-split-1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: types.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{
{
URL: "http://10.43.44.18:80",
},
},
},
},
"default-helloworld-go-rule-0-path-0-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-helloworld-go-rule-0-path-0-split-0",
Weight: ptr.To(50),
Headers: map[string]string{
"Knative-Serving-Namespace": "default",
"Knative-Serving-Revision": "helloworld-go-00001",
},
},
{
Name: "default-helloworld-go-rule-0-path-0-split-1",
Weight: ptr.To(50),
Headers: map[string]string{
"Knative-Serving-Namespace": "default",
"Knative-Serving-Revision": "helloworld-go-00002",
},
},
},
},
},
},
Middlewares: map[string]*dynamic.Middleware{},
},
},
},
{
desc: "External IP",
paths: []string{"external_ip.yaml", "services.yaml"},
wantLen: 1,
want: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-helloworld-go-rule-0-path-0": {
EntryPoints: []string{"http", "https"},
Service: "default-helloworld-go-rule-0-path-0-wrr",
Rule: "(Host(`helloworld-go.default`) || Host(`helloworld-go.default.svc`) || Host(`helloworld-go.default.svc.cluster.local`))",
Middlewares: []string{},
},
},
Services: map[string]*dynamic.Service{
"default-helloworld-go-rule-0-path-0-split-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: types.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{
{
URL: "http://10.43.38.208:80",
},
},
},
},
"default-helloworld-go-rule-0-path-0-split-1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: types.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{
{
URL: "http://10.43.44.18:80",
},
},
},
},
"default-helloworld-go-rule-0-path-0-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-helloworld-go-rule-0-path-0-split-0",
Weight: ptr.To(50),
Headers: map[string]string{
"Knative-Serving-Namespace": "default",
"Knative-Serving-Revision": "helloworld-go-00001",
},
},
{
Name: "default-helloworld-go-rule-0-path-0-split-1",
Weight: ptr.To(50),
Headers: map[string]string{
"Knative-Serving-Namespace": "default",
"Knative-Serving-Revision": "helloworld-go-00002",
},
},
},
},
},
},
Middlewares: map[string]*dynamic.Middleware{},
},
},
},
{
desc: "TLS",
paths: []string{"tls.yaml", "services.yaml"},
wantLen: 1,
want: &dynamic.Configuration{
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-helloworld-go-rule-0-path-0": {
EntryPoints: []string{"http", "https"},
Service: "default-helloworld-go-rule-0-path-0-wrr",
Rule: "(Host(`helloworld-go.default`) || Host(`helloworld-go.default.svc`) || Host(`helloworld-go.default.svc.cluster.local`))",
Middlewares: []string{},
},
"default-helloworld-go-rule-0-path-0-tls": {
EntryPoints: []string{"http", "https"},
Service: "default-helloworld-go-rule-0-path-0-wrr",
Rule: "(Host(`helloworld-go.default`) || Host(`helloworld-go.default.svc`) || Host(`helloworld-go.default.svc.cluster.local`))",
Middlewares: []string{},
TLS: &dynamic.RouterTLSConfig{},
},
},
Services: map[string]*dynamic.Service{
"default-helloworld-go-rule-0-path-0-split-0": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: types.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{
{
URL: "http://10.43.38.208:80",
},
},
},
},
"default-helloworld-go-rule-0-path-0-split-1": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: types.Duration(100 * time.Millisecond),
},
Servers: []dynamic.Server{
{
URL: "http://10.43.44.18:80",
},
},
},
},
"default-helloworld-go-rule-0-path-0-wrr": {
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{
{
Name: "default-helloworld-go-rule-0-path-0-split-0",
Weight: ptr.To(50),
Headers: map[string]string{
"Knative-Serving-Namespace": "default",
"Knative-Serving-Revision": "helloworld-go-00001",
},
},
{
Name: "default-helloworld-go-rule-0-path-0-split-1",
Weight: ptr.To(50),
Headers: map[string]string{
"Knative-Serving-Namespace": "default",
"Knative-Serving-Revision": "helloworld-go-00002",
},
},
},
},
},
},
Middlewares: map[string]*dynamic.Middleware{},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.desc, func(t *testing.T) {
t.Parallel()
k8sObjects, knObjects := readResources(t, testCase.paths)
k8sClient := kubefake.NewClientset(k8sObjects...)
knClient := knfake.NewSimpleClientset(knObjects...)
client := newClientImpl(knClient, k8sClient)
eventCh, err := client.WatchAll(nil, make(chan struct{}))
require.NoError(t, err)
if len(k8sObjects) > 0 || len(knObjects) > 0 {
// just wait for the first event
<-eventCh
}
p := Provider{
PublicEntrypoints: []string{"http", "https"},
PrivateEntrypoints: []string{"priv-http", "priv-https"},
client: client,
}
got, gotIngresses := p.loadConfiguration(t.Context())
assert.Len(t, gotIngresses, testCase.wantLen)
assert.Equal(t, testCase.want, got)
})
}
}
func Test_buildRule(t *testing.T) {
testCases := []struct {
desc string
hosts []string
headers map[string]knativenetworkingv1alpha1.HeaderMatch
path string
want string
}{
{
desc: "single host, no headers, no path",
hosts: []string{"example.com"},
want: "(Host(`example.com`))",
},
{
desc: "multiple hosts, no headers, no path",
hosts: []string{"example.com", "foo.com"},
want: "(Host(`example.com`) || Host(`foo.com`))",
},
{
desc: "single host, single header, no path",
hosts: []string{"example.com"},
headers: map[string]knativenetworkingv1alpha1.HeaderMatch{
"X-Header": {Exact: "value"},
},
want: "(Host(`example.com`)) && (Header(`X-Header`,`value`))",
},
{
desc: "single host, multiple headers, no path",
hosts: []string{"example.com"},
headers: map[string]knativenetworkingv1alpha1.HeaderMatch{
"X-Header": {Exact: "value"},
"X-Header2": {Exact: "value2"},
},
want: "(Host(`example.com`)) && (Header(`X-Header`,`value`) && Header(`X-Header2`,`value2`))",
},
{
desc: "single host, multiple headers, with path",
hosts: []string{"example.com"},
headers: map[string]knativenetworkingv1alpha1.HeaderMatch{
"X-Header": {Exact: "value"},
"X-Header2": {Exact: "value2"},
},
path: "/foo",
want: "(Host(`example.com`)) && (Header(`X-Header`,`value`) && Header(`X-Header2`,`value2`)) && PathPrefix(`/foo`)",
},
{
desc: "single host, no headers, with path",
hosts: []string{"example.com"},
path: "/foo",
want: "(Host(`example.com`)) && PathPrefix(`/foo`)",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
got := buildRule(test.hosts, test.headers, test.path)
assert.Equal(t, test.want, got)
})
}
}
func Test_mergeHTTPConfigs(t *testing.T) {
testCases := []struct {
desc string
configs []*dynamic.HTTPConfiguration
want *dynamic.HTTPConfiguration
}{
{
desc: "one empty configuration",
configs: []*dynamic.HTTPConfiguration{
{
Routers: map[string]*dynamic.Router{
"router1": {Rule: "Host(`example.com`)"},
},
Middlewares: map[string]*dynamic.Middleware{
"middleware1": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
},
Services: map[string]*dynamic.Service{
"service1": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
},
},
{
Routers: map[string]*dynamic.Router{},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{},
},
},
want: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"router1": {Rule: "Host(`example.com`)"},
},
Middlewares: map[string]*dynamic.Middleware{
"middleware1": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
},
Services: map[string]*dynamic.Service{
"service1": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
},
},
},
{
desc: "merging two non-empty configurations",
configs: []*dynamic.HTTPConfiguration{
{
Routers: map[string]*dynamic.Router{
"router1": {Rule: "Host(`example.com`)"},
},
Middlewares: map[string]*dynamic.Middleware{
"middleware1": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
},
Services: map[string]*dynamic.Service{
"service1": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
},
},
{
Routers: map[string]*dynamic.Router{
"router2": {Rule: "PathPrefix(`/test`)"},
},
Middlewares: map[string]*dynamic.Middleware{
"middleware2": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
},
Services: map[string]*dynamic.Service{
"service2": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
},
},
},
want: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"router1": {Rule: "Host(`example.com`)"},
"router2": {Rule: "PathPrefix(`/test`)"},
},
Middlewares: map[string]*dynamic.Middleware{
"middleware1": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
"middleware2": {Headers: &dynamic.Headers{CustomRequestHeaders: map[string]string{"X-Test": "value"}}},
},
Services: map[string]*dynamic.Service{
"service1": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
"service2": {LoadBalancer: &dynamic.ServersLoadBalancer{Servers: []dynamic.Server{{URL: "http://example.com"}}}},
},
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
got := mergeHTTPConfigs(test.configs...)
assert.Equal(t, test.want, got)
})
}
}
func readResources(t *testing.T, paths []string) ([]runtime.Object, []runtime.Object) {
t.Helper()
var (
k8sObjects []runtime.Object
knObjects []runtime.Object
)
for _, path := range paths {
yamlContent, err := os.ReadFile(filepath.FromSlash("./fixtures/" + path))
if err != nil {
panic(err)
}
objects := k8s.MustParseYaml(yamlContent)
for _, obj := range objects {
switch obj.GetObjectKind().GroupVersionKind().Group {
case "networking.internal.knative.dev":
knObjects = append(knObjects, obj)
default:
k8sObjects = append(k8sObjects, obj)
}
}
}
return k8sObjects, knObjects
}