cmd/k8s-operator,k8s-operator: allow to optionally configure proxies to --accept-routes

A new ProxyClass.tailscaledConfig.acceptRoutes field (defaults to false)
can be used to configure proxies created by the operator to be ran with
--accept-routes via the declarative config.

Updates tailscale/tailscale#10684

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
This commit is contained in:
Irbe Krumina 2024-02-27 22:27:33 +00:00
parent 7912d76da0
commit d481ec4560
10 changed files with 239 additions and 67 deletions

View File

@ -73,7 +73,7 @@ func TestConnector(t *testing.T) {
hostname: "test-connector",
isExitNode: true,
subnetRoutes: "10.40.0.0/14",
confFileHash: "9321660203effb80983eaecc7b5ac5a8c53934926f46e895b9fe295dcfc5a904",
confFileHash: "9c66c269f3aa779e4e63ca414c147fd419caa62f9220a535da619f21b6bdf2b9",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -83,7 +83,7 @@ func TestConnector(t *testing.T) {
conn.Spec.SubnetRouter.AdvertiseRoutes = []tsapi.Route{"10.40.0.0/14", "10.44.0.0/20"}
})
opts.subnetRoutes = "10.40.0.0/14,10.44.0.0/20"
opts.confFileHash = "fb6c4daf67425f983985750cd8d6f2beae77e614fcb34176604571f5623d6862"
opts.confFileHash = "ff6e3c6ca4188a1eeab248397cd8155f08b5530ae0bf1bf0fcc7f28bdfee3217"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -93,7 +93,7 @@ func TestConnector(t *testing.T) {
conn.Spec.SubnetRouter.AdvertiseRoutes = []tsapi.Route{"10.44.0.0/20"}
})
opts.subnetRoutes = "10.44.0.0/20"
opts.confFileHash = "bacba177bcfe3849065cf6fee53d658a9bb4144197ac5b861727d69ea99742bb"
opts.confFileHash = "2e06dc5fc2b05207007dcc89a5b31b9f4362299af24aa2110d16481dba5d338b"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -102,7 +102,7 @@ func TestConnector(t *testing.T) {
conn.Spec.SubnetRouter = nil
})
opts.subnetRoutes = ""
opts.confFileHash = "7c421a99128eb80e79a285a82702f19f8f720615542a15bd794858a6275d8079"
opts.confFileHash = "4697dc19021cf7ce95feeaa49af81f1ac81420ef0023acdd1689df272d134c60"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -113,7 +113,7 @@ func TestConnector(t *testing.T) {
}
})
opts.subnetRoutes = "10.44.0.0/20"
opts.confFileHash = "bacba177bcfe3849065cf6fee53d658a9bb4144197ac5b861727d69ea99742bb"
opts.confFileHash = "2e06dc5fc2b05207007dcc89a5b31b9f4362299af24aa2110d16481dba5d338b"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -156,7 +156,7 @@ func TestConnector(t *testing.T) {
parentType: "connector",
subnetRoutes: "10.40.0.0/14",
hostname: "test-connector",
confFileHash: "57d922331890c9b1c8c6ae664394cb254334c551d9cd9db14537b5d9da9fb17e",
confFileHash: "341dc67b44be0c81a0f31f2d3b9ae67084e88435894a0543b04d7fd97bfedf24",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -166,7 +166,7 @@ func TestConnector(t *testing.T) {
conn.Spec.ExitNode = true
})
opts.isExitNode = true
opts.confFileHash = "1499b591fd97a50f0330db6ec09979792c49890cf31f5da5bb6a3f50dba1e77a"
opts.confFileHash = "8f6a5d5895cc8a1ff7896d25ac5794b44f4d3c0e74588cab4674ecb323a676dc"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -243,7 +243,7 @@ func TestConnectorWithProxyClass(t *testing.T) {
hostname: "test-connector",
isExitNode: true,
subnetRoutes: "10.40.0.0/14",
confFileHash: "9321660203effb80983eaecc7b5ac5a8c53934926f46e895b9fe295dcfc5a904",
confFileHash: "9c66c269f3aa779e4e63ca414c147fd419caa62f9220a535da619f21b6bdf2b9",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -272,7 +272,7 @@ func TestConnectorWithProxyClass(t *testing.T) {
// We lose the auth key on second reconcile, because in code it's set to
// StringData, but is actually read from Data. This works with a real
// API server, but not with our test setup here.
opts.confFileHash = "1499b591fd97a50f0330db6ec09979792c49890cf31f5da5bb6a3f50dba1e77a"
opts.confFileHash = "8f6a5d5895cc8a1ff7896d25ac5794b44f4d3c0e74588cab4674ecb323a676dc"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))

View File

@ -35,15 +35,13 @@ spec:
type: object
spec:
type: object
required:
- statefulSet
properties:
statefulSet:
description: Proxy's StatefulSet spec.
type: object
properties:
annotations:
description: Annotations that will be added to the StatefulSet created for the proxy. Any Annotations specified here will be merged with the default annotations applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other annotations that might have been applied by other actors. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
description: Annotations that will be added to the StatefulSet created for the proxy. Any annotations specified here will be merged with the default annotations applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other annotations that might have been applied by other actors. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
type: object
additionalProperties:
type: string
@ -452,6 +450,16 @@ spec:
value:
description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
tailscaledConfig:
description: Configuration for tailscaled running in the proxy.
type: object
properties:
acceptRoutes:
description: AcceptRoutes can be set to "true" to configure the proxy to accept routes advertized by by other nodes on your tailnet, such as subnet routers and app connectors. This is equivalent of running 'tailscale up --accept-routes'. https://tailscale.com/kb/1072/client-preferences#use-tailscale-subnets The value of this field must be a string ("true" or "false"), defaults to "false".
type: string
x-kubernetes-validations:
- rule: type(self) == string && (self=='true' || self=='false')
message: acceptRoutes must be set to a string value. Accepted values are 'true' and 'false'
status:
type: object
properties:

View File

@ -196,7 +196,7 @@ spec:
annotations:
additionalProperties:
type: string
description: Annotations that will be added to the StatefulSet created for the proxy. Any Annotations specified here will be merged with the default annotations applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other annotations that might have been applied by other actors. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
description: Annotations that will be added to the StatefulSet created for the proxy. Any annotations specified here will be merged with the default annotations applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other annotations that might have been applied by other actors. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set
type: object
labels:
additionalProperties:
@ -604,8 +604,16 @@ spec:
type: array
type: object
type: object
required:
- statefulSet
tailscaledConfig:
description: Configuration for tailscaled running in the proxy.
properties:
acceptRoutes:
description: AcceptRoutes can be set to true to make the proxy to accept routes. from subnet routers and route traffic via exit nodes (defaults to false). https://tailscale.com/kb/1019/subnets
type: string
x-kubernetes-validations:
- message: acceptRoutes must be set to a string value. Accepted values are 'true' and 'false'
rule: type(self) == string && (self=='true' || self=='false')
type: object
type: object
status:
properties:

View File

@ -93,7 +93,7 @@ func TestTailscaleIngress(t *testing.T) {
namespace: "default",
parentType: "ingress",
hostname: "default-test",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
serveConfig := &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
@ -128,7 +128,7 @@ func TestTailscaleIngress(t *testing.T) {
opts.shouldEnableForwardingClusterTrafficViaIngress = true
// configfile hash changed at this point in test env only because we
// lost auth key due to how changes are applied in test client.
opts.confFileHash = "fb9006e30ecda75e88c29dcd0ca2dd28a2ae964d001c66e1be3efe159cc3821d"
opts.confFileHash = "5c7ed263a3bde718d485eb836f307d76f8778bda1f0303b0ea554d69b5829d80"
expectReconciled(t, ingR, "default", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -228,7 +228,7 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
namespace: "default",
parentType: "ingress",
hostname: "default-test",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
serveConfig := &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
@ -263,7 +263,7 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
opts.proxyClass = pc.Name
// configfile hash changed at this point in test env only because we
// lost auth key due to how changes are applied in test client.
opts.confFileHash = "fb9006e30ecda75e88c29dcd0ca2dd28a2ae964d001c66e1be3efe159cc3821d"
opts.confFileHash = "5c7ed263a3bde718d485eb836f307d76f8778bda1f0303b0ea554d69b5829d80"
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
// 4. tailscale.com/proxy-class label is removed from the Ingress, the

View File

@ -71,7 +71,7 @@ func TestLoadBalancerClass(t *testing.T) {
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSecret(t, opts))
@ -213,7 +213,7 @@ func TestTailnetTargetFQDNAnnotation(t *testing.T) {
parentType: "svc",
tailnetTargetFQDN: tailnetTargetFQDN,
hostname: "default-test",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSecret(t, o))
@ -324,7 +324,7 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
parentType: "svc",
tailnetTargetIP: tailnetTargetIP,
hostname: "default-test",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSecret(t, o))
@ -432,7 +432,7 @@ func TestAnnotations(t *testing.T) {
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSecret(t, o))
@ -541,7 +541,7 @@ func TestAnnotationIntoLB(t *testing.T) {
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSecret(t, o))
@ -591,7 +591,7 @@ func TestAnnotationIntoLB(t *testing.T) {
expectReconciled(t, sr, "default", "test")
// None of the proxy machinery should have changed...
// (although configfile hash will change in test env only because we lose auth key due to out test not syncing secret.StringData -> secret.Data)
o.confFileHash = "fb9006e30ecda75e88c29dcd0ca2dd28a2ae964d001c66e1be3efe159cc3821d"
o.confFileHash = "5c7ed263a3bde718d485eb836f307d76f8778bda1f0303b0ea554d69b5829d80"
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
// ... but the service should have a LoadBalancer status.
@ -675,7 +675,7 @@ func TestLBIntoAnnotation(t *testing.T) {
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSecret(t, o))
@ -745,7 +745,7 @@ func TestLBIntoAnnotation(t *testing.T) {
// configfile hash changes on a re-apply in this case in tests only as
// we lose the auth key due to the test apply not syncing
// secret.StringData -> Data.
o.confFileHash = "fb9006e30ecda75e88c29dcd0ca2dd28a2ae964d001c66e1be3efe159cc3821d"
o.confFileHash = "5c7ed263a3bde718d485eb836f307d76f8778bda1f0303b0ea554d69b5829d80"
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
@ -821,7 +821,7 @@ func TestCustomHostname(t *testing.T) {
parentType: "svc",
hostname: "reindeer-flotilla",
clusterTargetIP: "10.20.30.40",
confFileHash: "42376226c7d76ed6d6318315dc6c402f7d993bc0b01a5b0e6c8a833106b7509e",
confFileHash: "37426401f55a3e9e48d7e076a2d859386df39138858f8e2e34280555759fb4d8",
}
expectEqual(t, fc, expectedSecret(t, o))
@ -937,7 +937,7 @@ func TestCustomPriorityClassName(t *testing.T) {
hostname: "tailscale-critical",
priorityClassName: "custom-priority-class-name",
clusterTargetIP: "10.20.30.40",
confFileHash: "13cdef0d5f6f0f2406af028710ea1e0f99f65aba4021e4e70ac75a73cf141fd1",
confFileHash: "3bf08bbd3c5ef664ce6c25f1ff6d307c411db4c8c5e05ba2aeefb81d7c9de79d",
}
expectEqual(t, fc, expectedSTS(t, fc, o))
@ -1000,7 +1000,7 @@ func TestProxyClassForService(t *testing.T) {
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
@ -1030,7 +1030,7 @@ func TestProxyClassForService(t *testing.T) {
// configfile hash changes on a second apply in test env only because we
// lose auth key due to out test not syncing secret.StringData ->
// secret.Data
opts.confFileHash = "fb9006e30ecda75e88c29dcd0ca2dd28a2ae964d001c66e1be3efe159cc3821d"
opts.confFileHash = "5c7ed263a3bde718d485eb836f307d76f8778bda1f0303b0ea554d69b5829d80"
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
@ -1094,7 +1094,7 @@ func TestDefaultLoadBalancer(t *testing.T) {
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSTS(t, fc, o))
}
@ -1148,7 +1148,79 @@ func TestProxyFirewallMode(t *testing.T) {
hostname: "default-test",
firewallMode: "nftables",
clusterTargetIP: "10.20.30.40",
confFileHash: "6cceb342cd3e1c56cd1bd94c29df63df3653c35fe98a7e7afcdee0dcaa2ad549",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSTS(t, fc, o))
}
func TestAcceptRoutes(t *testing.T) {
pc := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{Name: "accept-routes"},
Spec: tsapi.ProxyClassSpec{TailscaledConfig: &tsapi.TailscaledConfig{AcceptRoutes: "true"}}}
fc := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme).
WithObjects(pc).
WithStatusSubresource(pc).
Build()
mustUpdateStatus(t, fc, "", "accept-routes", func(pc *tsapi.ProxyClass) {
pc.Status = tsapi.ProxyClassStatus{
Conditions: []tsapi.ConnectorCondition{{
Status: metav1.ConditionTrue,
Type: tsapi.ProxyClassready,
ObservedGeneration: pc.Generation,
}}}
})
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
},
logger: zl.Sugar(),
}
// Create a service that we should manage, and check that the initial round
// of objects looks right.
mustCreate(t, fc, &corev1.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"),
Labels: map[string]string{
LabelProxyClass: "accept-routes",
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
LoadBalancerClass: ptr.To("tailscale"),
},
})
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
proxyClass: "accept-routes",
acceptRoutes: true,
confFileHash: "5ec91709575f0f51af26f0ab6f86c7ebb3d6007ec59186c72da67a292d11f268",
}
expectEqual(t, fc, expectedSTS(t, fc, o))

View File

@ -55,18 +55,19 @@ const (
FinalizerName = "tailscale.com/finalizer"
// Annotations settable by users on services.
// Annotations settable by users on tailscale Services.
AnnotationExpose = "tailscale.com/expose"
AnnotationTags = "tailscale.com/tags"
AnnotationHostname = "tailscale.com/hostname"
annotationTailnetTargetIPOld = "tailscale.com/ts-tailnet-target-ip"
AnnotationTailnetTargetIP = "tailscale.com/tailnet-ip"
//MagicDNS name of tailnet node.
AnnotationTailnetTargetFQDN = "tailscale.com/tailnet-fqdn"
AnnotationTailnetTargetFQDN = "tailscale.com/tailnet-fqdn"
// Annotations settable by users on ingresses.
// Annotations settable by users on tailscale Ingresses.
AnnotationFunnel = "tailscale.com/funnel"
// Annotations settable by users on tailscale Ingresses and Services.
AnnotationTags = "tailscale.com/tags"
// 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
// on an Ingress. This is useful in cases where a cluster target needs
@ -134,6 +135,7 @@ type connector struct {
// isExitNode defines whether this Connector should act as an exit node.
isExitNode bool
}
type tsnetServer interface {
CertDomains() []string
}
@ -171,11 +173,22 @@ func (a *tailscaleSTSReconciler) Provision(ctx context.Context, logger *zap.Suga
return nil, fmt.Errorf("failed to reconcile headless service: %w", err)
}
secretName, tsConfigHash, err := a.createOrGetSecret(ctx, logger, sts, hsvc)
proxyClass := new(tsapi.ProxyClass)
if sts.ProxyClass != "" {
if err := a.Get(ctx, types.NamespacedName{Name: sts.ProxyClass}, proxyClass); err != nil {
return nil, fmt.Errorf("failed to get ProxyClass: %w", err)
}
if !tsoperator.ProxyClassIsReady(proxyClass) {
logger.Infof("ProxyClass %s specified for the proxy, but it is not (yet) in a ready state, waiting..")
return nil, nil
}
}
secretName, tsConfigHash, err := a.createOrGetSecret(ctx, logger, sts, hsvc, proxyClass)
if err != nil {
return nil, fmt.Errorf("failed to create or get API key secret: %w", err)
}
_, err = a.reconcileSTS(ctx, logger, sts, hsvc, secretName, tsConfigHash)
_, err = a.reconcileSTS(ctx, logger, sts, hsvc, secretName, tsConfigHash, proxyClass)
if err != nil {
return nil, fmt.Errorf("failed to reconcile statefulset: %w", err)
}
@ -288,7 +301,7 @@ func (a *tailscaleSTSReconciler) reconcileHeadlessService(ctx context.Context, l
return createOrUpdate(ctx, a.Client, a.operatorNamespace, hsvc, func(svc *corev1.Service) { svc.Spec = hsvc.Spec })
}
func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *zap.SugaredLogger, stsC *tailscaleSTSConfig, hsvc *corev1.Service) (string, string, error) {
func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *zap.SugaredLogger, stsC *tailscaleSTSConfig, hsvc *corev1.Service, pc *tsapi.ProxyClass) (string, string, error) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
// Hardcode a -0 suffix so that in future, if we support
@ -336,7 +349,7 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
return "", "", err
}
}
confFileBytes, h, err := tailscaledConfig(stsC, authKey, orig)
confFileBytes, h, err := tailscaledConfig(stsC, authKey, orig, pc)
if err != nil {
return "", "", fmt.Errorf("error creating tailscaled config: %w", err)
}
@ -417,7 +430,7 @@ var proxyYaml []byte
//go:embed deploy/manifests/userspace-proxy.yaml
var userspaceProxyYaml []byte
func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig, headlessSvc *corev1.Service, proxySecret, tsConfigHash string) (*appsv1.StatefulSet, error) {
func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig, headlessSvc *corev1.Service, proxySecret, tsConfigHash string, proxyClass *tsapi.ProxyClass) (*appsv1.StatefulSet, error) {
ss := new(appsv1.StatefulSet)
if sts.ServeConfig != nil && sts.ForwardClusterTrafficViaL7IngressProxy != true { // If forwarding cluster traffic via is required we need non-userspace + NET_ADMIN + forwarding
if err := yaml.Unmarshal(userspaceProxyYaml, &ss); err != nil {
@ -437,16 +450,6 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
}
pod := &ss.Spec.Template
container := &pod.Spec.Containers[0]
proxyClass := new(tsapi.ProxyClass)
if sts.ProxyClass != "" {
if err := a.Get(ctx, types.NamespacedName{Name: sts.ProxyClass}, proxyClass); err != nil {
return nil, fmt.Errorf("failed to get ProxyClass: %w", err)
}
if !tsoperator.ProxyClassIsReady(proxyClass) {
logger.Infof("ProxyClass %s specified for the proxy, but it is not (yet) in a ready state, waiting..")
return nil, nil
}
}
container.Image = a.proxyImage
ss.ObjectMeta = metav1.ObjectMeta{
Name: headlessSvc.Name,
@ -648,12 +651,16 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet)
// tailscaledConfig takes a proxy config, a newly generated auth key if
// generated and a Secret with the previous proxy state and auth key and
// produces returns tailscaled configuration and a hash of that configuration.
func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *corev1.Secret) ([]byte, string, error) {
func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *corev1.Secret, proxyClass *tsapi.ProxyClass) ([]byte, string, error) {
conf := ipn.ConfigVAlpha{
Version: "alpha0",
AcceptDNS: "false",
Locked: "false",
Hostname: &stsC.Hostname,
Version: "alpha0",
AcceptDNS: "false",
Locked: "false",
Hostname: &stsC.Hostname,
AcceptRoutes: opt.NewBool(false), // always set it explicitly
}
if proxyClass != nil && proxyClass.Spec.TailscaledConfig != nil {
conf.AcceptRoutes = opt.Bool(proxyClass.Spec.TailscaledConfig.AcceptRoutes)
}
if stsC.Connector != nil {
routes, err := netutil.CalcAdvertiseRoutes(stsC.Connector.routes, stsC.Connector.isExitNode)

View File

@ -25,6 +25,7 @@ import (
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
)
@ -48,6 +49,7 @@ type configOpts struct {
serveConfig *ipn.ServeConfig
shouldEnableForwardingClusterTrafficViaIngress bool
proxyClass string // configuration from the named ProxyClass should be applied to proxy resources
acceptRoutes bool // --accept-routes
}
func expectedSTS(t *testing.T, cl client.Client, opts configOpts) *appsv1.StatefulSet {
@ -339,11 +341,12 @@ func expectedSecret(t *testing.T, opts configOpts) *corev1.Secret {
mak.Set(&s.StringData, "serve-config", string(serveConfigBs))
}
conf := &ipn.ConfigVAlpha{
Version: "alpha0",
AcceptDNS: "false",
Hostname: &opts.hostname,
Locked: "false",
AuthKey: ptr.To("secret-authkey"),
Version: "alpha0",
AcceptDNS: "false",
Hostname: &opts.hostname,
Locked: "false",
AuthKey: ptr.To("secret-authkey"),
AcceptRoutes: opt.NewBool(opts.acceptRoutes),
}
var routes []netip.Prefix
if opts.subnetRoutes != "" || opts.isExitNode {

View File

@ -335,7 +335,14 @@ ConnectorCondition contains condition information for a Connector.
<td>
Proxy's StatefulSet spec.<br/>
</td>
<td>true</td>
<td>false</td>
</tr><tr>
<td><b><a href="#proxyclassspectailscaledconfig">tailscaledConfig</a></b></td>
<td>object</td>
<td>
Configuration for tailscaled running in the proxy.<br/>
</td>
<td>false</td>
</tr></tbody>
</table>
@ -360,7 +367,7 @@ Proxy's StatefulSet spec.
<td><b>annotations</b></td>
<td>map[string]string</td>
<td>
Annotations that will be added to the StatefulSet created for the proxy. Any Annotations specified here will be merged with the default annotations applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other annotations that might have been applied by other actors. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set<br/>
Annotations that will be added to the StatefulSet created for the proxy. Any annotations specified here will be merged with the default annotations applied to the StatefulSet by the Tailscale Kubernetes operator as well as any other annotations that might have been applied by other actors. Annotations must be valid Kubernetes annotations. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set<br/>
</td>
<td>false</td>
</tr><tr>
@ -1551,6 +1558,35 @@ The pod this Toleration is attached to tolerates any taint that matches the trip
</table>
### ProxyClass.spec.tailscaledConfig
<sup><sup>[↩ Parent](#proxyclassspec)</sup></sup>
Configuration for tailscaled running in the proxy.
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Required</th>
</tr>
</thead>
<tbody><tr>
<td><b>acceptRoutes</b></td>
<td>string</td>
<td>
AcceptRoutes can be set to "true" to configure the proxy to accept routes advertized by by other nodes on your tailnet, such as subnet routers and app connectors. This is equivalent of running 'tailscale up --accept-routes'. https://tailscale.com/kb/1072/client-preferences#use-tailscale-subnets The value of this field must be a string ("true" or "false"), defaults to "false".<br/>
<br/>
<i>Validations</i>:<li>type(self) == string && (self=='true' || self=='false'): acceptRoutes must be set to a string value. Accepted values are 'true' and 'false'</li>
</td>
<td>false</td>
</tr></tbody>
</table>
### ProxyClass.status
<sup><sup>[↩ Parent](#proxyclass)</sup></sup>

View File

@ -36,10 +36,28 @@ type ProxyClassList struct {
}
type ProxyClassSpec struct {
// Configuration for tailscaled running in the proxy.
// +optional
TailscaledConfig *TailscaledConfig `json:"tailscaledConfig,omitempty"`
// Proxy's StatefulSet spec.
StatefulSet *StatefulSet `json:"statefulSet"`
// +optional
StatefulSet *StatefulSet `json:"statefulSet,omitempty"`
}
type TailscaledConfig struct {
// AcceptRoutes can be set to "true" to configure the proxy to accept
// routes advertised by other nodes on your tailnet, such as subnet
// routers and app connectors.
// This is equivalent of running 'tailscale up --accept-routes'.
// https://tailscale.com/kb/1072/client-preferences#use-tailscale-subnets
// The value of this field must be a string ("true" or "false"),
// defaults to "false".
AcceptRoutes Bool `json:"acceptRoutes,omitempty"`
}
// +kubebuilder:validation:XValidation:rule="type(self) == string && (self=='true' || self=='false')",message="acceptRoutes must be set to a string value. Accepted values are 'true' and 'false'"
type Bool string
type StatefulSet struct {
// Labels that will be added to the StatefulSet created for the proxy.
// Any labels specified here will be merged with the default labels
@ -51,7 +69,7 @@ type StatefulSet struct {
// +optional
Labels map[string]string `json:"labels,omitempty"`
// Annotations that will be added to the StatefulSet created for the proxy.
// Any Annotations specified here will be merged with the default annotations
// Any annotations specified here will be merged with the default annotations
// applied to the StatefulSet by the Tailscale Kubernetes operator as
// well as any other annotations that might have been applied by other
// actors.

View File

@ -283,6 +283,11 @@ func (in *ProxyClassList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProxyClassSpec) DeepCopyInto(out *ProxyClassSpec) {
*out = *in
if in.TailscaledConfig != nil {
in, out := &in.TailscaledConfig, &out.TailscaledConfig
*out = new(TailscaledConfig)
**out = **in
}
if in.StatefulSet != nil {
in, out := &in.StatefulSet, &out.StatefulSet
*out = new(StatefulSet)
@ -413,3 +418,18 @@ func (in Tags) DeepCopy() Tags {
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TailscaledConfig) DeepCopyInto(out *TailscaledConfig) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TailscaledConfig.
func (in *TailscaledConfig) DeepCopy() *TailscaledConfig {
if in == nil {
return nil
}
out := new(TailscaledConfig)
in.DeepCopyInto(out)
return out
}