cmd/k8s-operator: default ingress paths to '/' if not specified by user (#15706)

in resource

Fixes #14908

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
This commit is contained in:
Tom Meadows 2025-04-17 16:14:34 +01:00 committed by GitHub
parent 26f31f73f4
commit 9666c2e700
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 178 additions and 8 deletions

View File

@ -292,9 +292,15 @@ func validateIngressClass(ctx context.Context, cl client.Client) error {
func handlersForIngress(ctx context.Context, ing *networkingv1.Ingress, cl client.Client, rec record.EventRecorder, tlsHost string, logger *zap.SugaredLogger) (handlers map[string]*ipn.HTTPHandler, err error) { func handlersForIngress(ctx context.Context, ing *networkingv1.Ingress, cl client.Client, rec record.EventRecorder, tlsHost string, logger *zap.SugaredLogger) (handlers map[string]*ipn.HTTPHandler, err error) {
addIngressBackend := func(b *networkingv1.IngressBackend, path string) { addIngressBackend := func(b *networkingv1.IngressBackend, path string) {
if path == "" {
path = "/"
rec.Eventf(ing, corev1.EventTypeNormal, "PathUndefined", "configured backend is missing a path, defaulting to '/'")
}
if b == nil { if b == nil {
return return
} }
if b.Service == nil { if b.Service == nil {
rec.Eventf(ing, corev1.EventTypeWarning, "InvalidIngressBackend", "backend for path %q is missing service", path) rec.Eventf(ing, corev1.EventTypeWarning, "InvalidIngressBackend", "backend for path %q is missing service", path)
return return

View File

@ -16,6 +16,7 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/fake"
"tailscale.com/ipn" "tailscale.com/ipn"
@ -487,6 +488,138 @@ func TestIngressLetsEncryptStaging(t *testing.T) {
} }
} }
func TestEmptyPath(t *testing.T) {
testCases := []struct {
name string
paths []networkingv1.HTTPIngressPath
expectedEvents []string
}{
{
name: "empty_path_with_prefix_type",
paths: []networkingv1.HTTPIngressPath{
{
PathType: ptrPathType(networkingv1.PathTypePrefix),
Path: "",
Backend: *backend(),
},
},
expectedEvents: []string{
"Normal PathUndefined configured backend is missing a path, defaulting to '/'",
},
},
{
name: "empty_path_with_implementation_specific_type",
paths: []networkingv1.HTTPIngressPath{
{
PathType: ptrPathType(networkingv1.PathTypeImplementationSpecific),
Path: "",
Backend: *backend(),
},
},
expectedEvents: []string{
"Normal PathUndefined configured backend is missing a path, defaulting to '/'",
},
},
{
name: "empty_path_with_exact_type",
paths: []networkingv1.HTTPIngressPath{
{
PathType: ptrPathType(networkingv1.PathTypeExact),
Path: "",
Backend: *backend(),
},
},
expectedEvents: []string{
"Warning UnsupportedPathTypeExact Exact path type strict matching is currently not supported and requests will be routed as for Prefix path type. This behaviour might change in the future.",
"Normal PathUndefined configured backend is missing a path, defaulting to '/'",
},
},
{
name: "two_competing_but_not_identical_paths_including_one_empty",
paths: []networkingv1.HTTPIngressPath{
{
PathType: ptrPathType(networkingv1.PathTypeImplementationSpecific),
Path: "",
Backend: *backend(),
},
{
PathType: ptrPathType(networkingv1.PathTypeImplementationSpecific),
Path: "/",
Backend: *backend(),
},
},
expectedEvents: []string{
"Normal PathUndefined configured backend is missing a path, defaulting to '/'",
},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
fc := fake.NewFakeClient(ingressClass())
ft := &fakeTSClient{}
fr := record.NewFakeRecorder(3) // bump this if you expect a test case to throw more events
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
ingR := &IngressReconciler{
recorder: fr,
Client: fc,
ssr: &tailscaleSTSReconciler{
Client: fc,
tsClient: ft,
tsnetServer: fakeTsnetServer,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
},
logger: zl.Sugar(),
}
// 1. Resources get created for regular Ingress
mustCreate(t, fc, ingressWithPaths(tt.paths))
mustCreate(t, fc, service())
expectReconciled(t, ingR, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test", "ingress")
mustCreate(t, fc, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: fullName,
Namespace: "operator-ns",
UID: "test-uid",
},
})
opts := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "ingress",
hostname: "foo",
app: kubetypes.AppIngressResource,
}
serveConfig := &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{"${TS_CERT_DOMAIN}:443": {Handlers: map[string]*ipn.HTTPHandler{"/": {Proxy: "http://1.2.3.4:8080/"}}}},
}
opts.serveConfig = serveConfig
expectEqual(t, fc, expectedSecret(t, fc, opts))
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts), removeHashAnnotation, removeResourceReqs)
expectEvents(t, fr, tt.expectedEvents)
})
}
}
// ptrPathType is a helper function to return a pointer to the pathtype string (required for TestEmptyPath)
func ptrPathType(p networkingv1.PathType) *networkingv1.PathType {
return &p
}
func ingressClass() *networkingv1.IngressClass { func ingressClass() *networkingv1.IngressClass {
return &networkingv1.IngressClass{ return &networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, ObjectMeta: metav1.ObjectMeta{Name: "tailscale"},
@ -520,17 +653,48 @@ func ingress() *networkingv1.Ingress {
}, },
Spec: networkingv1.IngressSpec{ Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("tailscale"), IngressClassName: ptr.To("tailscale"),
DefaultBackend: &networkingv1.IngressBackend{ DefaultBackend: backend(),
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
TLS: []networkingv1.IngressTLS{ TLS: []networkingv1.IngressTLS{
{Hosts: []string{"default-test"}}, {Hosts: []string{"default-test"}},
}, },
}, },
} }
} }
func ingressWithPaths(paths []networkingv1.HTTPIngressPath) *networkingv1.Ingress {
return &networkingv1.Ingress{
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
UID: types.UID("1234-UID"),
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("tailscale"),
Rules: []networkingv1.IngressRule{
{
Host: "foo.tailnetxyz.ts.net",
IngressRuleValue: networkingv1.IngressRuleValue{
HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: paths,
},
},
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"foo.tailnetxyz.ts.net"}},
},
},
}
}
func backend() *networkingv1.IngressBackend {
return &networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
}
}