mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-05 09:21:36 +01:00
cmd/k8s-operator: add support for taiscale.com/http-redirect (#17596)
* cmd/k8s-operator: add support for taiscale.com/http-redirect The k8s-operator now supports a tailscale.com/http-redirect annotation on Ingress resources. When enabled, this automatically creates port 80 handlers that automatically redirect to the equivalent HTTPS location. Fixes #11252 Signed-off-by: Fernando Serboncini <fserb@tailscale.com> * Fix for permanent redirect Signed-off-by: Fernando Serboncini <fserb@tailscale.com> * lint Signed-off-by: Fernando Serboncini <fserb@tailscale.com> * warn for redirect+endpoint Signed-off-by: Fernando Serboncini <fserb@tailscale.com> * tests Signed-off-by: Fernando Serboncini <fserb@tailscale.com> --------- Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
This commit is contained in:
parent
411cee0dc9
commit
7c5c02b77a
@ -290,6 +290,25 @@ func (r *HAIngressReconciler) maybeProvision(ctx context.Context, hostname strin
|
|||||||
ingCfg.Web[epHTTP] = &ipn.WebServerConfig{
|
ingCfg.Web[epHTTP] = &ipn.WebServerConfig{
|
||||||
Handlers: handlers,
|
Handlers: handlers,
|
||||||
}
|
}
|
||||||
|
if isHTTPRedirectEnabled(ing) {
|
||||||
|
logger.Warnf("Both HTTP endpoint and HTTP redirect flags are enabled: ignoring HTTP redirect.")
|
||||||
|
}
|
||||||
|
} else if isHTTPRedirectEnabled(ing) {
|
||||||
|
logger.Infof("HTTP redirect enabled, setting up port 80 redirect handlers")
|
||||||
|
epHTTP := ipn.HostPort(fmt.Sprintf("%s:80", dnsName))
|
||||||
|
ingCfg.TCP[80] = &ipn.TCPPortHandler{HTTP: true}
|
||||||
|
ingCfg.Web[epHTTP] = &ipn.WebServerConfig{
|
||||||
|
Handlers: map[string]*ipn.HTTPHandler{},
|
||||||
|
}
|
||||||
|
web80 := ingCfg.Web[epHTTP]
|
||||||
|
for mountPoint := range handlers {
|
||||||
|
// We send a 301 - Moved Permanently redirect from HTTP to HTTPS
|
||||||
|
redirectURL := "301:https://${HOST}${REQUEST_URI}"
|
||||||
|
logger.Debugf("Creating redirect handler: %s -> %s", mountPoint, redirectURL)
|
||||||
|
web80.Handlers[mountPoint] = &ipn.HTTPHandler{
|
||||||
|
Redirect: redirectURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var gotCfg *ipn.ServiceConfig
|
var gotCfg *ipn.ServiceConfig
|
||||||
@ -316,7 +335,7 @@ func (r *HAIngressReconciler) maybeProvision(ctx context.Context, hostname strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
tsSvcPorts := []string{"tcp:443"} // always 443 for Ingress
|
tsSvcPorts := []string{"tcp:443"} // always 443 for Ingress
|
||||||
if isHTTPEndpointEnabled(ing) {
|
if isHTTPEndpointEnabled(ing) || isHTTPRedirectEnabled(ing) {
|
||||||
tsSvcPorts = append(tsSvcPorts, "tcp:80")
|
tsSvcPorts = append(tsSvcPorts, "tcp:80")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,7 +365,7 @@ func (r *HAIngressReconciler) maybeProvision(ctx context.Context, hostname strin
|
|||||||
// 5. Update tailscaled's AdvertiseServices config, which should add the Tailscale Service
|
// 5. Update tailscaled's AdvertiseServices config, which should add the Tailscale Service
|
||||||
// IPs to the ProxyGroup Pods' AllowedIPs in the next netmap update if approved.
|
// IPs to the ProxyGroup Pods' AllowedIPs in the next netmap update if approved.
|
||||||
mode := serviceAdvertisementHTTPS
|
mode := serviceAdvertisementHTTPS
|
||||||
if isHTTPEndpointEnabled(ing) {
|
if isHTTPEndpointEnabled(ing) || isHTTPRedirectEnabled(ing) {
|
||||||
mode = serviceAdvertisementHTTPAndHTTPS
|
mode = serviceAdvertisementHTTPAndHTTPS
|
||||||
}
|
}
|
||||||
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg.Name, serviceName, mode, logger); err != nil {
|
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg.Name, serviceName, mode, logger); err != nil {
|
||||||
@ -377,7 +396,7 @@ func (r *HAIngressReconciler) maybeProvision(ctx context.Context, hostname strin
|
|||||||
Port: 443,
|
Port: 443,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if isHTTPEndpointEnabled(ing) {
|
if isHTTPEndpointEnabled(ing) || isHTTPRedirectEnabled(ing) {
|
||||||
ports = append(ports, networkingv1.IngressPortStatus{
|
ports = append(ports, networkingv1.IngressPortStatus{
|
||||||
Protocol: "TCP",
|
Protocol: "TCP",
|
||||||
Port: 80,
|
Port: 80,
|
||||||
|
|||||||
@ -618,6 +618,236 @@ func TestIngressPGReconciler_HTTPEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIngressPGReconciler_HTTPRedirect(t *testing.T) {
|
||||||
|
ingPGR, fc, ft := setupIngressTest(t)
|
||||||
|
|
||||||
|
// Create backend Service that the Ingress will route to
|
||||||
|
backendSvc := &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
ClusterIP: "10.0.0.1",
|
||||||
|
Ports: []corev1.ServicePort{
|
||||||
|
{
|
||||||
|
Port: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mustCreate(t, fc, backendSvc)
|
||||||
|
|
||||||
|
// Create test Ingress with HTTP redirect enabled
|
||||||
|
ing := &networkingv1.Ingress{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-ingress",
|
||||||
|
Namespace: "default",
|
||||||
|
UID: types.UID("1234-UID"),
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"tailscale.com/proxy-group": "test-pg",
|
||||||
|
"tailscale.com/http-redirect": "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: networkingv1.IngressSpec{
|
||||||
|
IngressClassName: ptr.To("tailscale"),
|
||||||
|
DefaultBackend: &networkingv1.IngressBackend{
|
||||||
|
Service: &networkingv1.IngressServiceBackend{
|
||||||
|
Name: "test",
|
||||||
|
Port: networkingv1.ServiceBackendPort{
|
||||||
|
Number: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TLS: []networkingv1.IngressTLS{
|
||||||
|
{Hosts: []string{"my-svc"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := fc.Create(context.Background(), ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify initial reconciliation with HTTP redirect enabled
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
populateTLSSecret(context.Background(), fc, "test-pg", "my-svc.ts.net")
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
|
||||||
|
// Verify Tailscale Service includes both tcp:80 and tcp:443
|
||||||
|
verifyTailscaleService(t, ft, "svc:my-svc", []string{"tcp:80", "tcp:443"})
|
||||||
|
|
||||||
|
// Verify Ingress status includes port 80
|
||||||
|
ing = &networkingv1.Ingress{}
|
||||||
|
if err := fc.Get(context.Background(), types.NamespacedName{
|
||||||
|
Name: "test-ingress",
|
||||||
|
Namespace: "default",
|
||||||
|
}, ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the Tailscale Service to prefs to have the Ingress recognised as ready
|
||||||
|
mustCreate(t, fc, &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pg-0",
|
||||||
|
Namespace: "operator-ns",
|
||||||
|
Labels: pgSecretLabels("test-pg", kubetypes.LabelSecretTypeState),
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"_current-profile": []byte("profile-foo"),
|
||||||
|
"profile-foo": []byte(`{"AdvertiseServices":["svc:my-svc"],"Config":{"NodeID":"node-foo"}}`),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reconcile and re-fetch Ingress
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
if err := fc.Get(context.Background(), client.ObjectKeyFromObject(ing), ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantStatus := []networkingv1.IngressPortStatus{
|
||||||
|
{Port: 443, Protocol: "TCP"},
|
||||||
|
{Port: 80, Protocol: "TCP"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus) {
|
||||||
|
t.Errorf("incorrect status ports: got %v, want %v",
|
||||||
|
ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove HTTP redirect annotation
|
||||||
|
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
|
||||||
|
delete(ing.Annotations, "tailscale.com/http-redirect")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verify reconciliation after removing HTTP redirect
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
verifyTailscaleService(t, ft, "svc:my-svc", []string{"tcp:443"})
|
||||||
|
|
||||||
|
// Verify Ingress status no longer includes port 80
|
||||||
|
ing = &networkingv1.Ingress{}
|
||||||
|
if err := fc.Get(context.Background(), types.NamespacedName{
|
||||||
|
Name: "test-ingress",
|
||||||
|
Namespace: "default",
|
||||||
|
}, ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantStatus = []networkingv1.IngressPortStatus{
|
||||||
|
{Port: 443, Protocol: "TCP"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus) {
|
||||||
|
t.Errorf("incorrect status ports after removing redirect: got %v, want %v",
|
||||||
|
ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIngressPGReconciler_HTTPEndpointAndRedirectConflict(t *testing.T) {
|
||||||
|
ingPGR, fc, ft := setupIngressTest(t)
|
||||||
|
|
||||||
|
// Create backend Service that the Ingress will route to
|
||||||
|
backendSvc := &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
ClusterIP: "10.0.0.1",
|
||||||
|
Ports: []corev1.ServicePort{
|
||||||
|
{
|
||||||
|
Port: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mustCreate(t, fc, backendSvc)
|
||||||
|
|
||||||
|
// Create test Ingress with both HTTP endpoint and HTTP redirect enabled
|
||||||
|
ing := &networkingv1.Ingress{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-ingress",
|
||||||
|
Namespace: "default",
|
||||||
|
UID: types.UID("1234-UID"),
|
||||||
|
Annotations: map[string]string{
|
||||||
|
"tailscale.com/proxy-group": "test-pg",
|
||||||
|
"tailscale.com/http-endpoint": "enabled",
|
||||||
|
"tailscale.com/http-redirect": "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: networkingv1.IngressSpec{
|
||||||
|
IngressClassName: ptr.To("tailscale"),
|
||||||
|
DefaultBackend: &networkingv1.IngressBackend{
|
||||||
|
Service: &networkingv1.IngressServiceBackend{
|
||||||
|
Name: "test",
|
||||||
|
Port: networkingv1.ServiceBackendPort{
|
||||||
|
Number: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TLS: []networkingv1.IngressTLS{
|
||||||
|
{Hosts: []string{"my-svc"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := fc.Create(context.Background(), ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify initial reconciliation - HTTP endpoint should take precedence
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
populateTLSSecret(context.Background(), fc, "test-pg", "my-svc.ts.net")
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
|
||||||
|
// Verify Tailscale Service includes both tcp:80 and tcp:443
|
||||||
|
verifyTailscaleService(t, ft, "svc:my-svc", []string{"tcp:80", "tcp:443"})
|
||||||
|
|
||||||
|
// Verify the serve config has HTTP endpoint handlers on port 80, NOT redirect handlers
|
||||||
|
cm := &corev1.ConfigMap{}
|
||||||
|
if err := fc.Get(context.Background(), types.NamespacedName{
|
||||||
|
Name: "test-pg-ingress-config",
|
||||||
|
Namespace: "operator-ns",
|
||||||
|
}, cm); err != nil {
|
||||||
|
t.Fatalf("getting ConfigMap: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Ingress status includes port 80
|
||||||
|
ing = &networkingv1.Ingress{}
|
||||||
|
if err := fc.Get(context.Background(), types.NamespacedName{
|
||||||
|
Name: "test-ingress",
|
||||||
|
Namespace: "default",
|
||||||
|
}, ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the Tailscale Service to prefs to have the Ingress recognised as ready
|
||||||
|
mustCreate(t, fc, &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pg-0",
|
||||||
|
Namespace: "operator-ns",
|
||||||
|
Labels: pgSecretLabels("test-pg", kubetypes.LabelSecretTypeState),
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"_current-profile": []byte("profile-foo"),
|
||||||
|
"profile-foo": []byte(`{"AdvertiseServices":["svc:my-svc"],"Config":{"NodeID":"node-foo"}}`),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reconcile and re-fetch Ingress
|
||||||
|
expectReconciled(t, ingPGR, "default", "test-ingress")
|
||||||
|
if err := fc.Get(context.Background(), client.ObjectKeyFromObject(ing), ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantStatus := []networkingv1.IngressPortStatus{
|
||||||
|
{Port: 443, Protocol: "TCP"},
|
||||||
|
{Port: 80, Protocol: "TCP"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus) {
|
||||||
|
t.Errorf("incorrect status ports: got %v, want %v",
|
||||||
|
ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIngressPGReconciler_MultiCluster(t *testing.T) {
|
func TestIngressPGReconciler_MultiCluster(t *testing.T) {
|
||||||
ingPGR, fc, ft := setupIngressTest(t)
|
ingPGR, fc, ft := setupIngressTest(t)
|
||||||
ingPGR.operatorID = "operator-1"
|
ingPGR.operatorID = "operator-1"
|
||||||
|
|||||||
@ -204,6 +204,27 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isHTTPRedirectEnabled(ing) {
|
||||||
|
logger.Infof("HTTP redirect enabled, setting up port 80 redirect handlers")
|
||||||
|
const magic80 = "${TS_CERT_DOMAIN}:80"
|
||||||
|
sc.TCP[80] = &ipn.TCPPortHandler{HTTP: true}
|
||||||
|
sc.Web[magic80] = &ipn.WebServerConfig{
|
||||||
|
Handlers: map[string]*ipn.HTTPHandler{},
|
||||||
|
}
|
||||||
|
if sc.AllowFunnel != nil && sc.AllowFunnel[magic443] {
|
||||||
|
sc.AllowFunnel[magic80] = true
|
||||||
|
}
|
||||||
|
web80 := sc.Web[magic80]
|
||||||
|
for mountPoint := range handlers {
|
||||||
|
// We send a 301 - Moved Permanently redirect from HTTP to HTTPS
|
||||||
|
redirectURL := "301:https://${HOST}${REQUEST_URI}"
|
||||||
|
logger.Debugf("Creating redirect handler: %s -> %s", mountPoint, redirectURL)
|
||||||
|
web80.Handlers[mountPoint] = &ipn.HTTPHandler{
|
||||||
|
Redirect: redirectURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
crl := childResourceLabels(ing.Name, ing.Namespace, "ingress")
|
crl := childResourceLabels(ing.Name, ing.Namespace, "ingress")
|
||||||
var tags []string
|
var tags []string
|
||||||
if tstr, ok := ing.Annotations[AnnotationTags]; ok {
|
if tstr, ok := ing.Annotations[AnnotationTags]; ok {
|
||||||
@ -244,14 +265,21 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf("setting Ingress hostname to %q", dev.ingressDNSName)
|
logger.Debugf("setting Ingress hostname to %q", dev.ingressDNSName)
|
||||||
|
ports := []networkingv1.IngressPortStatus{
|
||||||
|
{
|
||||||
|
Protocol: "TCP",
|
||||||
|
Port: 443,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if isHTTPRedirectEnabled(ing) {
|
||||||
|
ports = append(ports, networkingv1.IngressPortStatus{
|
||||||
|
Protocol: "TCP",
|
||||||
|
Port: 80,
|
||||||
|
})
|
||||||
|
}
|
||||||
ing.Status.LoadBalancer.Ingress = append(ing.Status.LoadBalancer.Ingress, networkingv1.IngressLoadBalancerIngress{
|
ing.Status.LoadBalancer.Ingress = append(ing.Status.LoadBalancer.Ingress, networkingv1.IngressLoadBalancerIngress{
|
||||||
Hostname: dev.ingressDNSName,
|
Hostname: dev.ingressDNSName,
|
||||||
Ports: []networkingv1.IngressPortStatus{
|
Ports: ports,
|
||||||
{
|
|
||||||
Protocol: "TCP",
|
|
||||||
Port: 443,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,6 +391,12 @@ func handlersForIngress(ctx context.Context, ing *networkingv1.Ingress, cl clien
|
|||||||
return handlers, nil
|
return handlers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isHTTPRedirectEnabled returns true if HTTP redirect is enabled for the Ingress.
|
||||||
|
// The annotation is tailscale.com/http-redirect and it should be set to "true".
|
||||||
|
func isHTTPRedirectEnabled(ing *networkingv1.Ingress) bool {
|
||||||
|
return ing.Annotations != nil && opt.Bool(ing.Annotations[AnnotationHTTPRedirect]).EqualBool(true)
|
||||||
|
}
|
||||||
|
|
||||||
// hostnameForIngress returns the hostname for an Ingress resource.
|
// hostnameForIngress returns the hostname for an Ingress resource.
|
||||||
// If the Ingress has TLS configured with a host, it returns the first component of that host.
|
// If the Ingress has TLS configured with a host, it returns the first component of that host.
|
||||||
// Otherwise, it returns a hostname derived from the Ingress name and namespace.
|
// Otherwise, it returns a hostname derived from the Ingress name and namespace.
|
||||||
|
|||||||
@ -7,6 +7,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -64,12 +65,14 @@ func TestTailscaleIngress(t *testing.T) {
|
|||||||
parentType: "ingress",
|
parentType: "ingress",
|
||||||
hostname: "default-test",
|
hostname: "default-test",
|
||||||
app: kubetypes.AppIngressResource,
|
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/"},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
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, expectedSecret(t, fc, opts))
|
||||||
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
||||||
@ -156,12 +159,14 @@ func TestTailscaleIngressHostname(t *testing.T) {
|
|||||||
parentType: "ingress",
|
parentType: "ingress",
|
||||||
hostname: "default-test",
|
hostname: "default-test",
|
||||||
app: kubetypes.AppIngressResource,
|
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/"},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
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, expectedSecret(t, fc, opts))
|
||||||
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
||||||
@ -276,12 +281,14 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
|
|||||||
parentType: "ingress",
|
parentType: "ingress",
|
||||||
hostname: "default-test",
|
hostname: "default-test",
|
||||||
app: kubetypes.AppIngressResource,
|
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/"},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
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, expectedSecret(t, fc, opts))
|
||||||
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
||||||
@ -368,10 +375,6 @@ func TestTailscaleIngressWithServiceMonitor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
expectReconciled(t, ingR, "default", "test")
|
expectReconciled(t, ingR, "default", "test")
|
||||||
fullName, shortName := findGenName(t, fc, "default", "test", "ingress")
|
fullName, shortName := findGenName(t, fc, "default", "test", "ingress")
|
||||||
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 := configOpts{
|
opts := configOpts{
|
||||||
stsName: shortName,
|
stsName: shortName,
|
||||||
secretName: fullName,
|
secretName: fullName,
|
||||||
@ -382,8 +385,14 @@ func TestTailscaleIngressWithServiceMonitor(t *testing.T) {
|
|||||||
app: kubetypes.AppIngressResource,
|
app: kubetypes.AppIngressResource,
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
proxyType: proxyTypeIngressResource,
|
proxyType: proxyTypeIngressResource,
|
||||||
serveConfig: serveConfig,
|
serveConfig: &ipn.ServeConfig{
|
||||||
resourceVersion: "1",
|
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/"},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
resourceVersion: "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Enable metrics- expect metrics Service to be created
|
// 1. Enable metrics- expect metrics Service to be created
|
||||||
@ -717,12 +726,14 @@ func TestEmptyPath(t *testing.T) {
|
|||||||
parentType: "ingress",
|
parentType: "ingress",
|
||||||
hostname: "foo",
|
hostname: "foo",
|
||||||
app: kubetypes.AppIngressResource,
|
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/"},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
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, expectedSecret(t, fc, opts))
|
||||||
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
||||||
@ -816,3 +827,101 @@ func backend() *networkingv1.IngressBackend {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTailscaleIngressWithHTTPRedirect(t *testing.T) {
|
||||||
|
fc := fake.NewFakeClient(ingressClass())
|
||||||
|
ft := &fakeTSClient{}
|
||||||
|
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
||||||
|
zl, err := zap.NewDevelopment()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ingR := &IngressReconciler{
|
||||||
|
Client: fc,
|
||||||
|
ingressClassName: "tailscale",
|
||||||
|
ssr: &tailscaleSTSReconciler{
|
||||||
|
Client: fc,
|
||||||
|
tsClient: ft,
|
||||||
|
tsnetServer: fakeTsnetServer,
|
||||||
|
defaultTags: []string{"tag:k8s"},
|
||||||
|
operatorNamespace: "operator-ns",
|
||||||
|
proxyImage: "tailscale/tailscale",
|
||||||
|
},
|
||||||
|
logger: zl.Sugar(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Create Ingress with HTTP redirect annotation
|
||||||
|
ing := ingress()
|
||||||
|
mak.Set(&ing.Annotations, AnnotationHTTPRedirect, "true")
|
||||||
|
mustCreate(t, fc, ing)
|
||||||
|
mustCreate(t, fc, service())
|
||||||
|
|
||||||
|
expectReconciled(t, ingR, "default", "test")
|
||||||
|
|
||||||
|
fullName, shortName := findGenName(t, fc, "default", "test", "ingress")
|
||||||
|
opts := configOpts{
|
||||||
|
replicas: ptr.To[int32](1),
|
||||||
|
stsName: shortName,
|
||||||
|
secretName: fullName,
|
||||||
|
namespace: "default",
|
||||||
|
parentType: "ingress",
|
||||||
|
hostname: "default-test",
|
||||||
|
app: kubetypes.AppIngressResource,
|
||||||
|
serveConfig: &ipn.ServeConfig{
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
443: {HTTPS: true},
|
||||||
|
80: {HTTP: true},
|
||||||
|
},
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"${TS_CERT_DOMAIN}:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Proxy: "http://1.2.3.4:8080/"},
|
||||||
|
}},
|
||||||
|
"${TS_CERT_DOMAIN}:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Redirect: "301:https://${HOST}${REQUEST_URI}"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectEqual(t, fc, expectedSecret(t, fc, opts))
|
||||||
|
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
|
||||||
|
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts), removeResourceReqs)
|
||||||
|
|
||||||
|
// 2. Update device info to get status updated
|
||||||
|
mustUpdate(t, fc, "operator-ns", opts.secretName, func(secret *corev1.Secret) {
|
||||||
|
mak.Set(&secret.Data, "device_id", []byte("1234"))
|
||||||
|
mak.Set(&secret.Data, "device_fqdn", []byte("foo.tailnetxyz.ts.net"))
|
||||||
|
})
|
||||||
|
expectReconciled(t, ingR, "default", "test")
|
||||||
|
|
||||||
|
// Verify Ingress status includes both ports 80 and 443
|
||||||
|
ing = &networkingv1.Ingress{}
|
||||||
|
if err := fc.Get(context.Background(), types.NamespacedName{Name: "test", Namespace: "default"}, ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantPorts := []networkingv1.IngressPortStatus{
|
||||||
|
{Port: 443, Protocol: "TCP"},
|
||||||
|
{Port: 80, Protocol: "TCP"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress[0].Ports, wantPorts) {
|
||||||
|
t.Errorf("incorrect status ports: got %v, want %v", ing.Status.LoadBalancer.Ingress[0].Ports, wantPorts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Remove HTTP redirect annotation
|
||||||
|
mustUpdate(t, fc, "default", "test", func(ing *networkingv1.Ingress) {
|
||||||
|
delete(ing.Annotations, AnnotationHTTPRedirect)
|
||||||
|
})
|
||||||
|
expectReconciled(t, ingR, "default", "test")
|
||||||
|
|
||||||
|
// 4. Verify Ingress status no longer includes port 80
|
||||||
|
ing = &networkingv1.Ingress{}
|
||||||
|
if err := fc.Get(context.Background(), types.NamespacedName{Name: "test", Namespace: "default"}, ing); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantPorts = []networkingv1.IngressPortStatus{
|
||||||
|
{Port: 443, Protocol: "TCP"},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress[0].Ports, wantPorts) {
|
||||||
|
t.Errorf("incorrect status ports after removing redirect: got %v, want %v", ing.Status.LoadBalancer.Ingress[0].Ports, wantPorts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -69,7 +69,8 @@ const (
|
|||||||
AnnotationProxyGroup = "tailscale.com/proxy-group"
|
AnnotationProxyGroup = "tailscale.com/proxy-group"
|
||||||
|
|
||||||
// Annotations settable by users on ingresses.
|
// Annotations settable by users on ingresses.
|
||||||
AnnotationFunnel = "tailscale.com/funnel"
|
AnnotationFunnel = "tailscale.com/funnel"
|
||||||
|
AnnotationHTTPRedirect = "tailscale.com/http-redirect"
|
||||||
|
|
||||||
// If set to true, set up iptables/nftables rules in the proxy forward
|
// If set to true, set up iptables/nftables rules in the proxy forward
|
||||||
// cluster traffic to the tailnet IP of that proxy. This can only be set
|
// cluster traffic to the tailnet IP of that proxy. This can only be set
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user