mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-28 06:43:44 +00:00
cmd/k8s-operator,k8s-operator: allow optionally using LE staging endpoint for Ingress (#15360)
cmd/k8s-operator,k8s-operator: allow using LE staging endpoint for Ingress Allow to optionally use LetsEncrypt staging endpoint to issue certs for Ingress/HA Ingress, so that it is easier to experiment with initial Ingress setup without hiting rate limits. Updates tailscale/corp#24795 Signed-off-by: Irbe Krumina <irbe@tailscale.com>
This commit is contained in:
parent
f3f2f72f96
commit
196ae1cd74
@ -2215,6 +2215,22 @@ spec:
|
|||||||
https://tailscale.com/kb/1019/subnets#use-your-subnet-routes-from-other-devices
|
https://tailscale.com/kb/1019/subnets#use-your-subnet-routes-from-other-devices
|
||||||
Defaults to false.
|
Defaults to false.
|
||||||
type: boolean
|
type: boolean
|
||||||
|
useLetsEncryptStagingEnvironment:
|
||||||
|
description: |-
|
||||||
|
Set UseLetsEncryptStagingEnvironment to true to issue TLS
|
||||||
|
certificates for any HTTPS endpoints exposed to the tailnet from
|
||||||
|
LetsEncrypt's staging environment.
|
||||||
|
https://letsencrypt.org/docs/staging-environment/
|
||||||
|
This setting only affects Tailscale Ingress resources.
|
||||||
|
By default Ingress TLS certificates are issued from LetsEncrypt's
|
||||||
|
production environment.
|
||||||
|
Changing this setting true -> false, will result in any
|
||||||
|
existing certs being re-issued from the production environment.
|
||||||
|
Changing this setting false (default) -> true, when certs have already
|
||||||
|
been provisioned from production environment will NOT result in certs
|
||||||
|
being re-issued from the staging environment before they need to be
|
||||||
|
renewed.
|
||||||
|
type: boolean
|
||||||
status:
|
status:
|
||||||
description: |-
|
description: |-
|
||||||
Status of the ProxyClass. This is set and managed automatically.
|
Status of the ProxyClass. This is set and managed automatically.
|
||||||
|
@ -2685,6 +2685,22 @@ spec:
|
|||||||
Defaults to false.
|
Defaults to false.
|
||||||
type: boolean
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
|
useLetsEncryptStagingEnvironment:
|
||||||
|
description: |-
|
||||||
|
Set UseLetsEncryptStagingEnvironment to true to issue TLS
|
||||||
|
certificates for any HTTPS endpoints exposed to the tailnet from
|
||||||
|
LetsEncrypt's staging environment.
|
||||||
|
https://letsencrypt.org/docs/staging-environment/
|
||||||
|
This setting only affects Tailscale Ingress resources.
|
||||||
|
By default Ingress TLS certificates are issued from LetsEncrypt's
|
||||||
|
production environment.
|
||||||
|
Changing this setting true -> false, will result in any
|
||||||
|
existing certs being re-issued from the production environment.
|
||||||
|
Changing this setting false (default) -> true, when certs have already
|
||||||
|
been provisioned from production environment will NOT result in certs
|
||||||
|
being re-issued from the staging environment before they need to be
|
||||||
|
renewed.
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
description: |-
|
description: |-
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -15,17 +16,18 @@ 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"
|
||||||
|
"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"
|
||||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||||
"tailscale.com/kube/kubetypes"
|
"tailscale.com/kube/kubetypes"
|
||||||
|
"tailscale.com/tstest"
|
||||||
"tailscale.com/types/ptr"
|
"tailscale.com/types/ptr"
|
||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTailscaleIngress(t *testing.T) {
|
func TestTailscaleIngress(t *testing.T) {
|
||||||
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
|
fc := fake.NewFakeClient(ingressClass())
|
||||||
fc := fake.NewFakeClient(tsIngressClass)
|
|
||||||
ft := &fakeTSClient{}
|
ft := &fakeTSClient{}
|
||||||
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
||||||
zl, err := zap.NewDevelopment()
|
zl, err := zap.NewDevelopment()
|
||||||
@ -46,45 +48,8 @@ func TestTailscaleIngress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. Resources get created for regular Ingress
|
// 1. Resources get created for regular Ingress
|
||||||
ing := &networkingv1.Ingress{
|
mustCreate(t, fc, ingress())
|
||||||
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
mustCreate(t, fc, service())
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
Namespace: "default",
|
|
||||||
// The apiserver is supposed to set the UID, but the fake client
|
|
||||||
// doesn't. So, set it explicitly because other code later depends
|
|
||||||
// on it being set.
|
|
||||||
UID: types.UID("1234-UID"),
|
|
||||||
},
|
|
||||||
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{"default-test"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mustCreate(t, fc, ing)
|
|
||||||
mustCreate(t, fc, &corev1.Service{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
Namespace: "default",
|
|
||||||
},
|
|
||||||
Spec: corev1.ServiceSpec{
|
|
||||||
ClusterIP: "1.2.3.4",
|
|
||||||
Ports: []corev1.ServicePort{{
|
|
||||||
Port: 8080,
|
|
||||||
Name: "http"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expectReconciled(t, ingR, "default", "test")
|
expectReconciled(t, ingR, "default", "test")
|
||||||
|
|
||||||
@ -114,6 +79,9 @@ func TestTailscaleIngress(t *testing.T) {
|
|||||||
mak.Set(&secret.Data, "device_fqdn", []byte("foo.tailnetxyz.ts.net"))
|
mak.Set(&secret.Data, "device_fqdn", []byte("foo.tailnetxyz.ts.net"))
|
||||||
})
|
})
|
||||||
expectReconciled(t, ingR, "default", "test")
|
expectReconciled(t, ingR, "default", "test")
|
||||||
|
|
||||||
|
// Get the ingress and update it with expected changes
|
||||||
|
ing := ingress()
|
||||||
ing.Finalizers = append(ing.Finalizers, "tailscale.com/finalizer")
|
ing.Finalizers = append(ing.Finalizers, "tailscale.com/finalizer")
|
||||||
ing.Status.LoadBalancer = networkingv1.IngressLoadBalancerStatus{
|
ing.Status.LoadBalancer = networkingv1.IngressLoadBalancerStatus{
|
||||||
Ingress: []networkingv1.IngressLoadBalancerIngress{
|
Ingress: []networkingv1.IngressLoadBalancerIngress{
|
||||||
@ -143,8 +111,7 @@ func TestTailscaleIngress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTailscaleIngressHostname(t *testing.T) {
|
func TestTailscaleIngressHostname(t *testing.T) {
|
||||||
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
|
fc := fake.NewFakeClient(ingressClass())
|
||||||
fc := fake.NewFakeClient(tsIngressClass)
|
|
||||||
ft := &fakeTSClient{}
|
ft := &fakeTSClient{}
|
||||||
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
||||||
zl, err := zap.NewDevelopment()
|
zl, err := zap.NewDevelopment()
|
||||||
@ -165,45 +132,8 @@ func TestTailscaleIngressHostname(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. Resources get created for regular Ingress
|
// 1. Resources get created for regular Ingress
|
||||||
ing := &networkingv1.Ingress{
|
mustCreate(t, fc, ingress())
|
||||||
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
mustCreate(t, fc, service())
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
Namespace: "default",
|
|
||||||
// The apiserver is supposed to set the UID, but the fake client
|
|
||||||
// doesn't. So, set it explicitly because other code later depends
|
|
||||||
// on it being set.
|
|
||||||
UID: types.UID("1234-UID"),
|
|
||||||
},
|
|
||||||
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{"default-test"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mustCreate(t, fc, ing)
|
|
||||||
mustCreate(t, fc, &corev1.Service{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
Namespace: "default",
|
|
||||||
},
|
|
||||||
Spec: corev1.ServiceSpec{
|
|
||||||
ClusterIP: "1.2.3.4",
|
|
||||||
Ports: []corev1.ServicePort{{
|
|
||||||
Port: 8080,
|
|
||||||
Name: "http"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expectReconciled(t, ingR, "default", "test")
|
expectReconciled(t, ingR, "default", "test")
|
||||||
|
|
||||||
@ -241,8 +171,10 @@ func TestTailscaleIngressHostname(t *testing.T) {
|
|||||||
mak.Set(&secret.Data, "device_fqdn", []byte("foo.tailnetxyz.ts.net"))
|
mak.Set(&secret.Data, "device_fqdn", []byte("foo.tailnetxyz.ts.net"))
|
||||||
})
|
})
|
||||||
expectReconciled(t, ingR, "default", "test")
|
expectReconciled(t, ingR, "default", "test")
|
||||||
ing.Finalizers = append(ing.Finalizers, "tailscale.com/finalizer")
|
|
||||||
|
|
||||||
|
// Get the ingress and update it with expected changes
|
||||||
|
ing := ingress()
|
||||||
|
ing.Finalizers = append(ing.Finalizers, "tailscale.com/finalizer")
|
||||||
expectEqual(t, fc, ing)
|
expectEqual(t, fc, ing)
|
||||||
|
|
||||||
// 3. Ingress proxy with capability version >= 110 advertises HTTPS endpoint
|
// 3. Ingress proxy with capability version >= 110 advertises HTTPS endpoint
|
||||||
@ -299,10 +231,9 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
|
|||||||
Annotations: map[string]string{"bar.io/foo": "some-val"},
|
Annotations: map[string]string{"bar.io/foo": "some-val"},
|
||||||
Pod: &tsapi.Pod{Annotations: map[string]string{"foo.io/bar": "some-val"}}}},
|
Pod: &tsapi.Pod{Annotations: map[string]string{"foo.io/bar": "some-val"}}}},
|
||||||
}
|
}
|
||||||
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
|
|
||||||
fc := fake.NewClientBuilder().
|
fc := fake.NewClientBuilder().
|
||||||
WithScheme(tsapi.GlobalScheme).
|
WithScheme(tsapi.GlobalScheme).
|
||||||
WithObjects(pc, tsIngressClass).
|
WithObjects(pc, ingressClass()).
|
||||||
WithStatusSubresource(pc).
|
WithStatusSubresource(pc).
|
||||||
Build()
|
Build()
|
||||||
ft := &fakeTSClient{}
|
ft := &fakeTSClient{}
|
||||||
@ -326,45 +257,8 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
|
|||||||
|
|
||||||
// 1. Ingress is created with no ProxyClass specified, default proxy
|
// 1. Ingress is created with no ProxyClass specified, default proxy
|
||||||
// resources get configured.
|
// resources get configured.
|
||||||
ing := &networkingv1.Ingress{
|
mustCreate(t, fc, ingress())
|
||||||
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
mustCreate(t, fc, service())
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
Namespace: "default",
|
|
||||||
// The apiserver is supposed to set the UID, but the fake client
|
|
||||||
// doesn't. So, set it explicitly because other code later depends
|
|
||||||
// on it being set.
|
|
||||||
UID: types.UID("1234-UID"),
|
|
||||||
},
|
|
||||||
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{"default-test"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mustCreate(t, fc, ing)
|
|
||||||
mustCreate(t, fc, &corev1.Service{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
Namespace: "default",
|
|
||||||
},
|
|
||||||
Spec: corev1.ServiceSpec{
|
|
||||||
ClusterIP: "1.2.3.4",
|
|
||||||
Ports: []corev1.ServicePort{{
|
|
||||||
Port: 8080,
|
|
||||||
Name: "http"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expectReconciled(t, ingR, "default", "test")
|
expectReconciled(t, ingR, "default", "test")
|
||||||
|
|
||||||
@ -432,54 +326,19 @@ func TestTailscaleIngressWithServiceMonitor(t *testing.T) {
|
|||||||
ObservedGeneration: 1,
|
ObservedGeneration: 1,
|
||||||
}}},
|
}}},
|
||||||
}
|
}
|
||||||
ing := &networkingv1.Ingress{
|
|
||||||
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
Namespace: "default",
|
|
||||||
// The apiserver is supposed to set the UID, but the fake client
|
|
||||||
// doesn't. So, set it explicitly because other code later depends
|
|
||||||
// on it being set.
|
|
||||||
UID: types.UID("1234-UID"),
|
|
||||||
Labels: map[string]string{
|
|
||||||
"tailscale.com/proxy-class": "metrics",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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{"default-test"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
svc := &corev1.Service{
|
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
|
||||||
Name: "test",
|
|
||||||
Namespace: "default",
|
|
||||||
},
|
|
||||||
Spec: corev1.ServiceSpec{
|
|
||||||
ClusterIP: "1.2.3.4",
|
|
||||||
Ports: []corev1.ServicePort{{
|
|
||||||
Port: 8080,
|
|
||||||
Name: "http"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
crd := &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: serviceMonitorCRD}}
|
crd := &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: serviceMonitorCRD}}
|
||||||
tsIngressClass := &networkingv1.IngressClass{ObjectMeta: metav1.ObjectMeta{Name: "tailscale"}, Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"}}
|
|
||||||
|
// Create fake client with ProxyClass, IngressClass, Ingress with metrics ProxyClass, and Service
|
||||||
|
ing := ingress()
|
||||||
|
ing.Labels = map[string]string{
|
||||||
|
LabelProxyClass: "metrics",
|
||||||
|
}
|
||||||
fc := fake.NewClientBuilder().
|
fc := fake.NewClientBuilder().
|
||||||
WithScheme(tsapi.GlobalScheme).
|
WithScheme(tsapi.GlobalScheme).
|
||||||
WithObjects(pc, tsIngressClass, ing, svc).
|
WithObjects(pc, ingressClass(), ing, service()).
|
||||||
WithStatusSubresource(pc).
|
WithStatusSubresource(pc).
|
||||||
Build()
|
Build()
|
||||||
|
|
||||||
ft := &fakeTSClient{}
|
ft := &fakeTSClient{}
|
||||||
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
|
||||||
zl, err := zap.NewDevelopment()
|
zl, err := zap.NewDevelopment()
|
||||||
@ -560,3 +419,118 @@ func TestTailscaleIngressWithServiceMonitor(t *testing.T) {
|
|||||||
expectMissing[corev1.Service](t, fc, "operator-ns", metricsResourceName(shortName))
|
expectMissing[corev1.Service](t, fc, "operator-ns", metricsResourceName(shortName))
|
||||||
// ServiceMonitor gets garbage collected when the Service is deleted - we cannot test that here.
|
// ServiceMonitor gets garbage collected when the Service is deleted - we cannot test that here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIngressLetsEncryptStaging(t *testing.T) {
|
||||||
|
cl := tstest.NewClock(tstest.ClockOpts{})
|
||||||
|
zl := zap.Must(zap.NewDevelopment())
|
||||||
|
|
||||||
|
pcLEStaging, pcLEStagingFalse, pcOther := proxyClassesForLEStagingTest()
|
||||||
|
|
||||||
|
testCases := testCasesForLEStagingTests(pcLEStaging, pcLEStagingFalse, pcOther)
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
builder := fake.NewClientBuilder().
|
||||||
|
WithScheme(tsapi.GlobalScheme)
|
||||||
|
|
||||||
|
builder = builder.WithObjects(pcLEStaging, pcLEStagingFalse, pcOther).
|
||||||
|
WithStatusSubresource(pcLEStaging, pcLEStagingFalse, pcOther)
|
||||||
|
|
||||||
|
fc := builder.Build()
|
||||||
|
|
||||||
|
if tt.proxyClassPerResource != "" || tt.defaultProxyClass != "" {
|
||||||
|
name := tt.proxyClassPerResource
|
||||||
|
if name == "" {
|
||||||
|
name = tt.defaultProxyClass
|
||||||
|
}
|
||||||
|
setProxyClassReady(t, fc, cl, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
mustCreate(t, fc, ingressClass())
|
||||||
|
mustCreate(t, fc, service())
|
||||||
|
ing := ingress()
|
||||||
|
if tt.proxyClassPerResource != "" {
|
||||||
|
ing.Labels = map[string]string{
|
||||||
|
LabelProxyClass: tt.proxyClassPerResource,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mustCreate(t, fc, ing)
|
||||||
|
|
||||||
|
ingR := &IngressReconciler{
|
||||||
|
Client: fc,
|
||||||
|
ssr: &tailscaleSTSReconciler{
|
||||||
|
Client: fc,
|
||||||
|
tsClient: &fakeTSClient{},
|
||||||
|
tsnetServer: &fakeTSNetServer{certDomains: []string{"test-host"}},
|
||||||
|
defaultTags: []string{"tag:test"},
|
||||||
|
operatorNamespace: "operator-ns",
|
||||||
|
proxyImage: "tailscale/tailscale:test",
|
||||||
|
},
|
||||||
|
logger: zl.Sugar(),
|
||||||
|
defaultProxyClass: tt.defaultProxyClass,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectReconciled(t, ingR, "default", "test")
|
||||||
|
|
||||||
|
_, shortName := findGenName(t, fc, "default", "test", "ingress")
|
||||||
|
sts := &appsv1.StatefulSet{}
|
||||||
|
if err := fc.Get(context.Background(), client.ObjectKey{Namespace: "operator-ns", Name: shortName}, sts); err != nil {
|
||||||
|
t.Fatalf("failed to get StatefulSet: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.useLEStagingEndpoint {
|
||||||
|
verifyEnvVar(t, sts, "TS_DEBUG_ACME_DIRECTORY_URL", letsEncryptStagingEndpoint)
|
||||||
|
} else {
|
||||||
|
verifyEnvVarNotPresent(t, sts, "TS_DEBUG_ACME_DIRECTORY_URL")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ingressClass() *networkingv1.IngressClass {
|
||||||
|
return &networkingv1.IngressClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "tailscale"},
|
||||||
|
Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func service() *corev1.Service {
|
||||||
|
return &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
ClusterIP: "1.2.3.4",
|
||||||
|
Ports: []corev1.ServicePort{{
|
||||||
|
Port: 8080,
|
||||||
|
Name: "http"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ingress() *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"),
|
||||||
|
DefaultBackend: &networkingv1.IngressBackend{
|
||||||
|
Service: &networkingv1.IngressServiceBackend{
|
||||||
|
Name: "test",
|
||||||
|
Port: networkingv1.ServiceBackendPort{
|
||||||
|
Number: 8080,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TLS: []networkingv1.IngressTLS{
|
||||||
|
{Hosts: []string{"default-test"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -302,7 +302,10 @@ func (r *ProxyGroupReconciler) maybeProvision(ctx context.Context, pg *tsapi.Pro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error generating StatefulSet spec: %w", err)
|
return fmt.Errorf("error generating StatefulSet spec: %w", err)
|
||||||
}
|
}
|
||||||
ss = applyProxyClassToStatefulSet(proxyClass, ss, nil, logger)
|
cfg := &tailscaleSTSConfig{
|
||||||
|
proxyType: string(pg.Spec.Type),
|
||||||
|
}
|
||||||
|
ss = applyProxyClassToStatefulSet(proxyClass, ss, cfg, logger)
|
||||||
capver, err := r.capVerForPG(ctx, pg, logger)
|
capver, err := r.capVerForPG(ctx, pg, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting device info: %w", err)
|
return fmt.Errorf("error getting device info: %w", err)
|
||||||
|
@ -518,6 +518,60 @@ func TestIngressAdvertiseServicesConfigPreserved(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func proxyClassesForLEStagingTest() (*tsapi.ProxyClass, *tsapi.ProxyClass, *tsapi.ProxyClass) {
|
||||||
|
pcLEStaging := &tsapi.ProxyClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "le-staging",
|
||||||
|
Generation: 1,
|
||||||
|
},
|
||||||
|
Spec: tsapi.ProxyClassSpec{
|
||||||
|
UseLetsEncryptStagingEnvironment: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pcLEStagingFalse := &tsapi.ProxyClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "le-staging-false",
|
||||||
|
Generation: 1,
|
||||||
|
},
|
||||||
|
Spec: tsapi.ProxyClassSpec{
|
||||||
|
UseLetsEncryptStagingEnvironment: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pcOther := &tsapi.ProxyClass{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "other",
|
||||||
|
Generation: 1,
|
||||||
|
},
|
||||||
|
Spec: tsapi.ProxyClassSpec{},
|
||||||
|
}
|
||||||
|
|
||||||
|
return pcLEStaging, pcLEStagingFalse, pcOther
|
||||||
|
}
|
||||||
|
|
||||||
|
func setProxyClassReady(t *testing.T, fc client.Client, cl *tstest.Clock, name string) *tsapi.ProxyClass {
|
||||||
|
t.Helper()
|
||||||
|
pc := &tsapi.ProxyClass{}
|
||||||
|
if err := fc.Get(context.Background(), client.ObjectKey{Name: name}, pc); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pc.Status = tsapi.ProxyClassStatus{
|
||||||
|
Conditions: []metav1.Condition{{
|
||||||
|
Type: string(tsapi.ProxyClassReady),
|
||||||
|
Status: metav1.ConditionTrue,
|
||||||
|
Reason: reasonProxyClassValid,
|
||||||
|
Message: reasonProxyClassValid,
|
||||||
|
LastTransitionTime: metav1.Time{Time: cl.Now().Truncate(time.Second)},
|
||||||
|
ObservedGeneration: pc.Generation,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
if err := fc.Status().Update(context.Background(), pc); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return pc
|
||||||
|
}
|
||||||
|
|
||||||
func verifyProxyGroupCounts(t *testing.T, r *ProxyGroupReconciler, wantIngress, wantEgress int) {
|
func verifyProxyGroupCounts(t *testing.T, r *ProxyGroupReconciler, wantIngress, wantEgress int) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if r.ingressProxyGroups.Len() != wantIngress {
|
if r.ingressProxyGroups.Len() != wantIngress {
|
||||||
@ -541,6 +595,16 @@ func verifyEnvVar(t *testing.T, sts *appsv1.StatefulSet, name, expectedValue str
|
|||||||
t.Errorf("%s environment variable not found", name)
|
t.Errorf("%s environment variable not found", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func verifyEnvVarNotPresent(t *testing.T, sts *appsv1.StatefulSet, name string) {
|
||||||
|
t.Helper()
|
||||||
|
for _, env := range sts.Spec.Template.Spec.Containers[0].Env {
|
||||||
|
if env.Name == name {
|
||||||
|
t.Errorf("environment variable %s should not be present", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyGroup, shouldExist bool, cfgHash string, proxyClass *tsapi.ProxyClass) {
|
func expectProxyGroupResources(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyGroup, shouldExist bool, cfgHash string, proxyClass *tsapi.ProxyClass) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -618,3 +682,146 @@ func addNodeIDToStateSecrets(t *testing.T, fc client.WithWatch, pg *tsapi.ProxyG
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProxyGroupLetsEncryptStaging(t *testing.T) {
|
||||||
|
cl := tstest.NewClock(tstest.ClockOpts{})
|
||||||
|
zl := zap.Must(zap.NewDevelopment())
|
||||||
|
|
||||||
|
// Set up test cases- most are shared with non-HA Ingress.
|
||||||
|
type proxyGroupLETestCase struct {
|
||||||
|
leStagingTestCase
|
||||||
|
pgType tsapi.ProxyGroupType
|
||||||
|
}
|
||||||
|
pcLEStaging, pcLEStagingFalse, pcOther := proxyClassesForLEStagingTest()
|
||||||
|
sharedTestCases := testCasesForLEStagingTests(pcLEStaging, pcLEStagingFalse, pcOther)
|
||||||
|
var tests []proxyGroupLETestCase
|
||||||
|
for _, tt := range sharedTestCases {
|
||||||
|
tests = append(tests, proxyGroupLETestCase{
|
||||||
|
leStagingTestCase: tt,
|
||||||
|
pgType: tsapi.ProxyGroupTypeIngress,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
tests = append(tests, proxyGroupLETestCase{
|
||||||
|
leStagingTestCase: leStagingTestCase{
|
||||||
|
name: "egress_pg_with_staging_proxyclass",
|
||||||
|
proxyClassPerResource: "le-staging",
|
||||||
|
useLEStagingEndpoint: false,
|
||||||
|
},
|
||||||
|
pgType: tsapi.ProxyGroupTypeEgress,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
builder := fake.NewClientBuilder().
|
||||||
|
WithScheme(tsapi.GlobalScheme)
|
||||||
|
|
||||||
|
// Pre-populate the fake client with ProxyClasses.
|
||||||
|
builder = builder.WithObjects(pcLEStaging, pcLEStagingFalse, pcOther).
|
||||||
|
WithStatusSubresource(pcLEStaging, pcLEStagingFalse, pcOther)
|
||||||
|
|
||||||
|
fc := builder.Build()
|
||||||
|
|
||||||
|
// If the test case needs a ProxyClass to exist, ensure it is set to Ready.
|
||||||
|
if tt.proxyClassPerResource != "" || tt.defaultProxyClass != "" {
|
||||||
|
name := tt.proxyClassPerResource
|
||||||
|
if name == "" {
|
||||||
|
name = tt.defaultProxyClass
|
||||||
|
}
|
||||||
|
setProxyClassReady(t, fc, cl, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ProxyGroup
|
||||||
|
pg := &tsapi.ProxyGroup{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test",
|
||||||
|
},
|
||||||
|
Spec: tsapi.ProxyGroupSpec{
|
||||||
|
Type: tt.pgType,
|
||||||
|
Replicas: ptr.To[int32](1),
|
||||||
|
ProxyClass: tt.proxyClassPerResource,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mustCreate(t, fc, pg)
|
||||||
|
|
||||||
|
reconciler := &ProxyGroupReconciler{
|
||||||
|
tsNamespace: tsNamespace,
|
||||||
|
proxyImage: testProxyImage,
|
||||||
|
defaultTags: []string{"tag:test"},
|
||||||
|
defaultProxyClass: tt.defaultProxyClass,
|
||||||
|
Client: fc,
|
||||||
|
tsClient: &fakeTSClient{},
|
||||||
|
l: zl.Sugar(),
|
||||||
|
clock: cl,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectReconciled(t, reconciler, "", pg.Name)
|
||||||
|
|
||||||
|
// Verify that the StatefulSet created for ProxyGrup has
|
||||||
|
// the expected setting for the staging endpoint.
|
||||||
|
sts := &appsv1.StatefulSet{}
|
||||||
|
if err := fc.Get(context.Background(), client.ObjectKey{Namespace: tsNamespace, Name: pg.Name}, sts); err != nil {
|
||||||
|
t.Fatalf("failed to get StatefulSet: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.useLEStagingEndpoint {
|
||||||
|
verifyEnvVar(t, sts, "TS_DEBUG_ACME_DIRECTORY_URL", letsEncryptStagingEndpoint)
|
||||||
|
} else {
|
||||||
|
verifyEnvVarNotPresent(t, sts, "TS_DEBUG_ACME_DIRECTORY_URL")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type leStagingTestCase struct {
|
||||||
|
name string
|
||||||
|
// ProxyClass set on ProxyGroup or Ingress resource.
|
||||||
|
proxyClassPerResource string
|
||||||
|
// Default ProxyClass.
|
||||||
|
defaultProxyClass string
|
||||||
|
useLEStagingEndpoint bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared test cases for LE staging endpoint configuration for ProxyGroup and
|
||||||
|
// non-HA Ingress.
|
||||||
|
func testCasesForLEStagingTests(pcLEStaging, pcLEStagingFalse, pcOther *tsapi.ProxyClass) []leStagingTestCase {
|
||||||
|
return []leStagingTestCase{
|
||||||
|
{
|
||||||
|
name: "with_staging_proxyclass",
|
||||||
|
proxyClassPerResource: "le-staging",
|
||||||
|
useLEStagingEndpoint: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with_staging_proxyclass_false",
|
||||||
|
proxyClassPerResource: "le-staging-false",
|
||||||
|
useLEStagingEndpoint: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with_other_proxyclass",
|
||||||
|
proxyClassPerResource: "other",
|
||||||
|
useLEStagingEndpoint: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no_proxyclass",
|
||||||
|
proxyClassPerResource: "",
|
||||||
|
useLEStagingEndpoint: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with_default_staging_proxyclass",
|
||||||
|
proxyClassPerResource: "",
|
||||||
|
defaultProxyClass: "le-staging",
|
||||||
|
useLEStagingEndpoint: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with_default_other_proxyclass",
|
||||||
|
proxyClassPerResource: "",
|
||||||
|
defaultProxyClass: "other",
|
||||||
|
useLEStagingEndpoint: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with_default_staging_proxyclass_false",
|
||||||
|
proxyClassPerResource: "",
|
||||||
|
defaultProxyClass: "le-staging-false",
|
||||||
|
useLEStagingEndpoint: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -102,6 +102,8 @@ const (
|
|||||||
|
|
||||||
envVarTSLocalAddrPort = "TS_LOCAL_ADDR_PORT"
|
envVarTSLocalAddrPort = "TS_LOCAL_ADDR_PORT"
|
||||||
defaultLocalAddrPort = 9002 // metrics and health check port
|
defaultLocalAddrPort = 9002 // metrics and health check port
|
||||||
|
|
||||||
|
letsEncryptStagingEndpoint = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -783,6 +785,17 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet,
|
|||||||
enableEndpoints(ss, metricsEnabled, debugEnabled)
|
enableEndpoints(ss, metricsEnabled, debugEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if pc.Spec.UseLetsEncryptStagingEnvironment && (stsCfg.proxyType == proxyTypeIngressResource || stsCfg.proxyType == string(tsapi.ProxyGroupTypeIngress)) {
|
||||||
|
for i, c := range ss.Spec.Template.Spec.Containers {
|
||||||
|
if c.Name == "tailscale" {
|
||||||
|
ss.Spec.Template.Spec.Containers[i].Env = append(ss.Spec.Template.Spec.Containers[i].Env, corev1.EnvVar{
|
||||||
|
Name: "TS_DEBUG_ACME_DIRECTORY_URL",
|
||||||
|
Value: letsEncryptStagingEndpoint,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if pc.Spec.StatefulSet == nil {
|
if pc.Spec.StatefulSet == nil {
|
||||||
return ss
|
return ss
|
||||||
|
@ -517,6 +517,7 @@ _Appears in:_
|
|||||||
| `statefulSet` _[StatefulSet](#statefulset)_ | Configuration parameters for the proxy's StatefulSet. Tailscale<br />Kubernetes operator deploys a StatefulSet for each of the user<br />configured proxies (Tailscale Ingress, Tailscale Service, Connector). | | |
|
| `statefulSet` _[StatefulSet](#statefulset)_ | Configuration parameters for the proxy's StatefulSet. Tailscale<br />Kubernetes operator deploys a StatefulSet for each of the user<br />configured proxies (Tailscale Ingress, Tailscale Service, Connector). | | |
|
||||||
| `metrics` _[Metrics](#metrics)_ | Configuration for proxy metrics. Metrics are currently not supported<br />for egress proxies and for Ingress proxies that have been configured<br />with tailscale.com/experimental-forward-cluster-traffic-via-ingress<br />annotation. Note that the metrics are currently considered unstable<br />and will likely change in breaking ways in the future - we only<br />recommend that you use those for debugging purposes. | | |
|
| `metrics` _[Metrics](#metrics)_ | Configuration for proxy metrics. Metrics are currently not supported<br />for egress proxies and for Ingress proxies that have been configured<br />with tailscale.com/experimental-forward-cluster-traffic-via-ingress<br />annotation. Note that the metrics are currently considered unstable<br />and will likely change in breaking ways in the future - we only<br />recommend that you use those for debugging purposes. | | |
|
||||||
| `tailscale` _[TailscaleConfig](#tailscaleconfig)_ | TailscaleConfig contains options to configure the tailscale-specific<br />parameters of proxies. | | |
|
| `tailscale` _[TailscaleConfig](#tailscaleconfig)_ | TailscaleConfig contains options to configure the tailscale-specific<br />parameters of proxies. | | |
|
||||||
|
| `useLetsEncryptStagingEnvironment` _boolean_ | Set UseLetsEncryptStagingEnvironment to true to issue TLS<br />certificates for any HTTPS endpoints exposed to the tailnet from<br />LetsEncrypt's staging environment.<br />https://letsencrypt.org/docs/staging-environment/<br />This setting only affects Tailscale Ingress resources.<br />By default Ingress TLS certificates are issued from LetsEncrypt's<br />production environment.<br />Changing this setting true -> false, will result in any<br />existing certs being re-issued from the production environment.<br />Changing this setting false (default) -> true, when certs have already<br />been provisioned from production environment will NOT result in certs<br />being re-issued from the staging environment before they need to be<br />renewed. | | |
|
||||||
|
|
||||||
|
|
||||||
#### ProxyClassStatus
|
#### ProxyClassStatus
|
||||||
|
@ -66,6 +66,21 @@ type ProxyClassSpec struct {
|
|||||||
// parameters of proxies.
|
// parameters of proxies.
|
||||||
// +optional
|
// +optional
|
||||||
TailscaleConfig *TailscaleConfig `json:"tailscale,omitempty"`
|
TailscaleConfig *TailscaleConfig `json:"tailscale,omitempty"`
|
||||||
|
// Set UseLetsEncryptStagingEnvironment to true to issue TLS
|
||||||
|
// certificates for any HTTPS endpoints exposed to the tailnet from
|
||||||
|
// LetsEncrypt's staging environment.
|
||||||
|
// https://letsencrypt.org/docs/staging-environment/
|
||||||
|
// This setting only affects Tailscale Ingress resources.
|
||||||
|
// By default Ingress TLS certificates are issued from LetsEncrypt's
|
||||||
|
// production environment.
|
||||||
|
// Changing this setting true -> false, will result in any
|
||||||
|
// existing certs being re-issued from the production environment.
|
||||||
|
// Changing this setting false (default) -> true, when certs have already
|
||||||
|
// been provisioned from production environment will NOT result in certs
|
||||||
|
// being re-issued from the staging environment before they need to be
|
||||||
|
// renewed.
|
||||||
|
// +optional
|
||||||
|
UseLetsEncryptStagingEnvironment bool `json:"useLetsEncryptStagingEnvironment,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TailscaleConfig struct {
|
type TailscaleConfig struct {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user