diff --git a/cmd/k8s-operator/connector.go b/cmd/k8s-operator/connector.go index 2062741d7..a6235da3b 100644 --- a/cmd/k8s-operator/connector.go +++ b/cmd/k8s-operator/connector.go @@ -10,7 +10,6 @@ import ( "fmt" "net/netip" "slices" - "strings" "sync" "time" @@ -33,17 +32,16 @@ import ( ) const ( - reasonSubnetRouterCreationFailed = "SubnetRouterCreationFailed" - reasonSubnetRouterCreated = "SubnetRouterCreated" - reasonSubnetRouterCleanupFailed = "SubnetRouterCleanupFailed" - reasonSubnetRouterCleanupInProgress = "SubnetRouterCleanupInProgress" - reasonSubnetRouterInvalid = "SubnetRouterInvalid" - - messageSubnetRouterCreationFailed = "Failed creating subnet router for routes %s: %v" - messageSubnetRouterInvalid = "Subnet router is invalid: %v" - messageSubnetRouterCreated = "Created subnet router for routes %s" - messageSubnetRouterCleanupFailed = "Failed cleaning up subnet router resources: %v" - msgSubnetRouterCleanupInProgress = "SubnetRouterCleanupInProgress" + reasonConnectorCreationFailed = "ConnectorCreationFailed" + + reasonConnectorCreated = "ConnectorCreated" + reasonConnectorCleanupFailed = "ConnectorCleanupFailed" + reasonConnectorCleanupInProgress = "ConnectorCleanupInProgress" + reasonConnectorInvalid = "ConnectorInvalid" + + messageConnectorCreationFailed = "Failed creating Connector: %v" + messageConnectorInvalid = "Connector is invalid: %v" + messageSubnetRouterCleanupFailed = "Failed cleaning up Connector resources: %v" shortRequeue = time.Second * 5 ) @@ -61,42 +59,39 @@ type ConnectorReconciler struct { mu sync.Mutex // protects following - // subnetRouters tracks the subnet routers managed by this Tailscale - // Operator instance. - subnetRouters set.Slice[types.UID] + connectors set.Slice[types.UID] // for connectors gauge } var ( - // gaugeIngressResources tracks the number of subnet routers that we're - // currently managing. - gaugeSubnetRouterResources = clientmetric.NewGauge("k8s_subnet_router_resources") + // gaugeConnectorResources tracks the number of Connectors currently managed by this operator instance + gaugeConnectorResources = clientmetric.NewGauge("k8s_connector_resources") ) func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, err error) { - logger := a.logger.With("connector", req.Name) + logger := a.logger.With("Connector", req.Name) logger.Debugf("starting reconcile") defer logger.Debugf("reconcile finished") cn := new(tsapi.Connector) err = a.Get(ctx, req.NamespacedName, cn) if apierrors.IsNotFound(err) { - logger.Debugf("connector not found, assuming it was deleted") + logger.Debugf("Connector not found, assuming it was deleted") return reconcile.Result{}, nil } else if err != nil { return reconcile.Result{}, fmt.Errorf("failed to get tailscale.com Connector: %w", err) } if !cn.DeletionTimestamp.IsZero() { - logger.Debugf("connector is being deleted or should not be exposed, cleaning up components") + logger.Debugf("Connector is being deleted or should not be exposed, cleaning up resources") ix := xslices.Index(cn.Finalizers, FinalizerName) if ix < 0 { logger.Debugf("no finalizer, nothing to do") return reconcile.Result{}, nil } - if done, err := a.maybeCleanupSubnetRouter(ctx, logger, cn); err != nil { + if done, err := a.maybeCleanupConnector(ctx, logger, cn); err != nil { return reconcile.Result{}, err } else if !done { - logger.Debugf("cleanup not finished, will retry...") + logger.Debugf("Connector resource cleanup not yet finished, will retry...") return reconcile.Result{RequeueAfter: shortRequeue}, nil } @@ -104,21 +99,20 @@ func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Reque if err := a.Update(ctx, cn); err != nil { return reconcile.Result{}, err } - logger.Infof("connector resources cleaned up") + logger.Infof("Connector resources cleaned up") return reconcile.Result{}, nil } + var ( + reason, message string + readyStatus metav1.ConditionStatus + ) + oldCnStatus := cn.Status.DeepCopy() defer func() { - if cn.Status.SubnetRouter == nil { - tsoperator.SetConnectorCondition(cn, tsapi.ConnectorReady, metav1.ConditionUnknown, "", "", cn.Generation, a.clock, logger) - } else if cn.Status.SubnetRouter.Ready == metav1.ConditionTrue { - tsoperator.SetConnectorCondition(cn, tsapi.ConnectorReady, metav1.ConditionTrue, reasonSubnetRouterCreated, reasonSubnetRouterCreated, cn.Generation, a.clock, logger) - } else { - tsoperator.SetConnectorCondition(cn, tsapi.ConnectorReady, metav1.ConditionFalse, cn.Status.SubnetRouter.Reason, cn.Status.SubnetRouter.Reason, cn.Generation, a.clock, logger) - } + tsoperator.SetConnectorCondition(cn, tsapi.ConnectorReady, readyStatus, reason, message, cn.Generation, a.clock, logger) if !apiequality.Semantic.DeepEqual(oldCnStatus, cn.Status) { - // an error encountered here should get returned by the Reconcile function + // An error encountered here should get returned by the Reconcile function. if updateErr := a.Client.Status().Update(ctx, cn); updateErr != nil { err = updateErr } @@ -130,67 +124,85 @@ func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Reque // because once the finalizer is in place this block gets skipped. So, // this is a nice place to tell the operator that the high level, // multi-reconcile operation is underway. - logger.Infof("ensuring connector is set up") + logger.Infof("ensuring Connector is set up") cn.Finalizers = append(cn.Finalizers, FinalizerName) if err := a.Update(ctx, cn); err != nil { err = fmt.Errorf("failed to add finalizer: %w", err) logger.Errorf("error adding finalizer: %v", err) + reason = reasonConnectorCreationFailed + message = fmt.Sprintf(messageConnectorCreationFailed, err) + readyStatus = metav1.ConditionFalse return reconcile.Result{}, err } } - // A Connector with unset .spec.subnetRouter and unset - // cn.spec.subnetRouter.Routes will be rejected at apply time (because - // these fields are set as required by our CRD validation). This check - // is here for if our CRD validation breaks unnoticed we don't crash the - // operator with nil pointer exception. - if cn.Spec.SubnetRouter == nil || len(cn.Spec.SubnetRouter.Routes) < 1 { + if err := a.validate(cn); err != nil { + logger.Errorf("error validating Connector spec: %w", err) + reason = reasonConnectorInvalid + message = fmt.Sprintf(messageConnectorInvalid, err) + readyStatus = metav1.ConditionFalse + a.recorder.Eventf(cn, corev1.EventTypeWarning, reasonConnectorInvalid, message) return reconcile.Result{}, nil } - if err := validateSubnetRouter(*cn.Spec.SubnetRouter); err != nil { - msg := fmt.Sprintf(messageSubnetRouterInvalid, err) - cn.Status.SubnetRouter = &tsapi.SubnetRouterStatus{ - Ready: metav1.ConditionFalse, - Reason: reasonSubnetRouterInvalid, - Message: msg, + if err = a.maybeProvisionConnector(ctx, logger, cn); err != nil { + logger.Errorf("error creating Connector resources: %w", err) + reason = reasonConnectorCreationFailed + message = fmt.Sprintf(messageConnectorCreationFailed, err) + readyStatus = metav1.ConditionFalse + a.recorder.Eventf(cn, corev1.EventTypeWarning, reason, message) + } else { + logger.Info("Connector resources synced") + reason = reasonConnectorCreated + message = reasonConnectorCreated + readyStatus = metav1.ConditionTrue + cn.Status.IsExitNode = cn.Spec.IsExitNode + if cn.Spec.SubnetRouter != nil { + cn.Status.SubnetRoutes = cn.Spec.SubnetRouter.Routes.Stringify() + } else { + cn.Status.SubnetRoutes = "" } - a.recorder.Eventf(cn, corev1.EventTypeWarning, reasonSubnetRouterInvalid, msg) - return reconcile.Result{}, nil } + return reconcile.Result{}, err +} - var sb strings.Builder - sb.WriteString(string(cn.Spec.SubnetRouter.Routes[0])) - for _, r := range cn.Spec.SubnetRouter.Routes[1:] { - sb.WriteString(fmt.Sprintf(",%s", r)) +// maybeProvisionConnector ensures that any new resources required for this +// Connector instance are deployed to the cluster. +func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logger *zap.SugaredLogger, cn *tsapi.Connector) error { + hostname := cn.Name + "-connector" + if cn.Spec.Hostname != "" { + hostname = string(cn.Spec.Hostname) } - cidrsS := sb.String() - logger.Debugf("ensuring a subnet router is deployed") - err = a.maybeProvisionSubnetRouter(ctx, logger, cn, cidrsS) - if err != nil { - msg := fmt.Sprintf(messageSubnetRouterCreationFailed, cidrsS, err) - cn.Status.SubnetRouter = &tsapi.SubnetRouterStatus{ - Ready: metav1.ConditionFalse, - Reason: reasonSubnetRouterCreationFailed, - Message: msg, - } - a.recorder.Eventf(cn, corev1.EventTypeWarning, reasonSubnetRouterCreationFailed, msg) - return reconcile.Result{}, err + crl := childResourceLabels(cn.Name, a.tsnamespace, "connector") + sts := &tailscaleSTSConfig{ + ParentResourceName: cn.Name, + ParentResourceUID: string(cn.UID), + Hostname: hostname, + ChildResourceLabels: crl, + Tags: cn.Spec.Tags.Stringify(), + Connector: &connector{ + isExitNode: cn.Spec.IsExitNode, + }, } - cn.Status.SubnetRouter = &tsapi.SubnetRouterStatus{ - Routes: cidrsS, - Ready: metav1.ConditionTrue, - Reason: reasonSubnetRouterCreated, - Message: fmt.Sprintf(messageSubnetRouterCreated, cidrsS), + + if cn.Spec.SubnetRouter != nil && len(cn.Spec.SubnetRouter.Routes) > 0 { + sts.Connector.routes = cn.Spec.SubnetRouter.Routes.Stringify() } - return reconcile.Result{}, nil + + a.mu.Lock() + a.connectors.Add(cn.UID) + gaugeConnectorResources.Set(int64(a.connectors.Len())) + a.mu.Unlock() + + _, err := a.ssr.Provision(ctx, logger, sts) + return err } -func (a *ConnectorReconciler) maybeCleanupSubnetRouter(ctx context.Context, logger *zap.SugaredLogger, cn *tsapi.Connector) (bool, error) { - if done, err := a.ssr.Cleanup(ctx, logger, childResourceLabels(cn.Name, a.tsnamespace, "subnetrouter")); err != nil { - return false, fmt.Errorf("failed to cleanup: %w", err) +func (a *ConnectorReconciler) maybeCleanupConnector(ctx context.Context, logger *zap.SugaredLogger, cn *tsapi.Connector) (bool, error) { + if done, err := a.ssr.Cleanup(ctx, logger, childResourceLabels(cn.Name, a.tsnamespace, "connector")); err != nil { + return false, fmt.Errorf("failed to cleanup Connector resources: %w", err) } else if !done { - logger.Debugf("cleanup not done yet, waiting for next reconcile") + logger.Debugf("Connector cleanup not done yet, waiting for next reconcile") return false, nil } @@ -198,42 +210,31 @@ func (a *ConnectorReconciler) maybeCleanupSubnetRouter(ctx context.Context, logg // exactly once at the very end of cleanup, because the final step of // cleanup removes the tailscale finalizer, which will make all future // reconciles exit early. - logger.Infof("cleaned up subnet router") + logger.Infof("cleaned up Connector resources") a.mu.Lock() defer a.mu.Unlock() - a.subnetRouters.Remove(cn.UID) - gaugeSubnetRouterResources.Set(int64(a.subnetRouters.Len())) + a.connectors.Remove(cn.UID) + gaugeConnectorResources.Set(int64(a.connectors.Len())) return true, nil } -// maybeProvisionSubnetRouter maybe deploys subnet router that exposes a subset of cluster cidrs to the tailnet -func (a *ConnectorReconciler) maybeProvisionSubnetRouter(ctx context.Context, logger *zap.SugaredLogger, cn *tsapi.Connector, cidrs string) error { - if cn.Spec.SubnetRouter == nil || len(cn.Spec.SubnetRouter.Routes) < 1 { +func (a *ConnectorReconciler) validate(cn *tsapi.Connector) error { + // Connector fields are already validated at apply time with CEL validation + // on custom resource fields. The checks here are a backup in case the + // CEL validation breaks without us noticing. + if !(cn.Spec.SubnetRouter != nil || cn.Spec.IsExitNode) { + return errors.New("invalid Connector spec- a Connector must be either expose subnet routes or act as exit node (or both)") + } + if cn.Spec.SubnetRouter == nil { return nil } - a.mu.Lock() - a.subnetRouters.Add(cn.UID) - gaugeSubnetRouterResources.Set(int64(a.subnetRouters.Len())) - a.mu.Unlock() + return validateSubnetRouter(cn.Spec.SubnetRouter) +} - crl := childResourceLabels(cn.Name, a.tsnamespace, "subnetrouter") - hostname := hostnameForSubnetRouter(cn) - sts := &tailscaleSTSConfig{ - ParentResourceName: cn.Name, - ParentResourceUID: string(cn.UID), - Hostname: hostname, - ChildResourceLabels: crl, - Routes: cidrs, - } - for _, tag := range cn.Spec.SubnetRouter.Tags { - sts.Tags = append(sts.Tags, string(tag)) +func validateSubnetRouter(sb *tsapi.SubnetRouter) error { + if len(sb.Routes) < 1 { + return errors.New("invalid subnet router spec: no routes defined") } - - _, err := a.ssr.Provision(ctx, logger, sts) - - return err -} -func validateSubnetRouter(sb tsapi.SubnetRouter) error { var err error for _, route := range sb.Routes { pfx, e := netip.ParsePrefix(string(route)) @@ -247,13 +248,3 @@ func validateSubnetRouter(sb tsapi.SubnetRouter) error { } return err } - -func hostnameForSubnetRouter(cn *tsapi.Connector) string { - if cn.Spec.SubnetRouter == nil { - return "" - } - if cn.Spec.SubnetRouter.Hostname != "" { - return string(cn.Spec.SubnetRouter.Hostname) - } - return cn.Name + "-" + "subnetrouter" -} diff --git a/cmd/k8s-operator/connector_test.go b/cmd/k8s-operator/connector_test.go index 754eb7bdd..382abce9e 100644 --- a/cmd/k8s-operator/connector_test.go +++ b/cmd/k8s-operator/connector_test.go @@ -7,6 +7,7 @@ package main import ( "context" + "fmt" "testing" "go.uber.org/zap" @@ -21,6 +22,8 @@ import ( ) func TestConnector(t *testing.T) { + // Create a Connector that defines a Tailscale node that advertises + // 10.40.0.0/14 route and acts as an exit node. cn := &tsapi.Connector{ ObjectMeta: metav1.ObjectMeta{ Name: "test", @@ -34,6 +37,7 @@ func TestConnector(t *testing.T) { SubnetRouter: &tsapi.SubnetRouter{ Routes: []tsapi.Route{"10.40.0.0/14"}, }, + IsExitNode: true, }, } fc := fake.NewClientBuilder(). @@ -48,7 +52,6 @@ func TestConnector(t *testing.T) { } cl := tstest.NewClock(tstest.ClockOpts{}) - // Create a Connector with a subnet router definition cr := &ConnectorReconciler{ Client: fc, ssr: &tailscaleSTSReconciler{ @@ -63,26 +66,95 @@ func TestConnector(t *testing.T) { } expectReconciled(t, cr, "", "test") - fullName, shortName := findGenName(t, fc, "", "test", "subnetrouter") + fullName, shortName := findGenName(t, fc, "", "test", "connector") - expectEqual(t, fc, expectedSecret(fullName, "", "subnetrouter")) - expectEqual(t, fc, expectedConnectorSTS(shortName, fullName, "10.40.0.0/14")) + expectEqual(t, fc, expectedSecret(fullName, "", "connector")) + opts := connectorSTSOpts{ + connectorName: "test", + stsName: shortName, + secretName: fullName, + routes: "10.40.0.0/14", + isExitNode: true, + } + expectEqual(t, fc, expectedConnectorSTS(opts)) - // Add another CIDR + // Add another route to be advertised. mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) { conn.Spec.SubnetRouter.Routes = []tsapi.Route{"10.40.0.0/14", "10.44.0.0/20"} }) expectReconciled(t, cr, "", "test") - expectEqual(t, fc, expectedConnectorSTS(shortName, fullName, "10.40.0.0/14,10.44.0.0/20")) + opts.routes = "10.40.0.0/14,10.44.0.0/20" + + expectEqual(t, fc, expectedConnectorSTS(opts)) - // Remove a CIDR + // Remove a route. mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) { conn.Spec.SubnetRouter.Routes = []tsapi.Route{"10.44.0.0/20"} }) expectReconciled(t, cr, "", "test") - expectEqual(t, fc, expectedConnectorSTS(shortName, fullName, "10.44.0.0/20")) + opts.routes = "10.44.0.0/20" + expectEqual(t, fc, expectedConnectorSTS(opts)) + + // Remove the subnet router. + mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) { + conn.Spec.SubnetRouter = nil + }) + expectReconciled(t, cr, "", "test") + opts.routes = "" + expectEqual(t, fc, expectedConnectorSTS(opts)) + + // Re-add the subnet router. + mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) { + conn.Spec.SubnetRouter = &tsapi.SubnetRouter{ + Routes: []tsapi.Route{"10.44.0.0/20"}, + } + }) + expectReconciled(t, cr, "", "test") + opts.routes = "10.44.0.0/20" + expectEqual(t, fc, expectedConnectorSTS(opts)) + + // Delete the Connector. + if err = fc.Delete(context.Background(), cn); err != nil { + t.Fatalf("error deleting Connector: %v", err) + } + + expectRequeue(t, cr, "", "test") + expectReconciled(t, cr, "", "test") + + expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) + expectMissing[corev1.Secret](t, fc, "operator-ns", fullName) + + // Create a Connector that advertises a route and is not an exit node. + cn = &tsapi.Connector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + UID: types.UID("1234-UID"), + }, + TypeMeta: metav1.TypeMeta{ + Kind: tsapi.ConnectorKind, + APIVersion: "tailscale.io/v1alpha1", + }, + Spec: tsapi.ConnectorSpec{ + SubnetRouter: &tsapi.SubnetRouter{ + Routes: []tsapi.Route{"10.40.0.0/14"}, + }, + }, + } + mustCreate(t, fc, cn) + expectReconciled(t, cr, "", "test") + fullName, shortName = findGenName(t, fc, "", "test", "connector") + + expectEqual(t, fc, expectedSecret(fullName, "", "connector")) + opts = connectorSTSOpts{ + connectorName: "test", + stsName: shortName, + secretName: fullName, + routes: "10.40.0.0/14", + isExitNode: false, + } + expectEqual(t, fc, expectedConnectorSTS(opts)) - // Delete the Connector + // Delete the Connector. if err = fc.Delete(context.Background(), cn); err != nil { t.Fatalf("error deleting Connector: %v", err) } @@ -92,23 +164,45 @@ func TestConnector(t *testing.T) { expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName) expectMissing[corev1.Secret](t, fc, "operator-ns", fullName) +} +type connectorSTSOpts struct { + stsName string + secretName string + connectorName string + hostname string + routes string + isExitNode bool } -func expectedConnectorSTS(stsName, secretName, routes string) *appsv1.StatefulSet { - return &appsv1.StatefulSet{ +func expectedConnectorSTS(opts connectorSTSOpts) *appsv1.StatefulSet { + var hostname string + if opts.hostname != "" { + hostname = opts.hostname + } else { + hostname = opts.connectorName + "-connector" + } + containerEnv := []corev1.EnvVar{ + {Name: "TS_USERSPACE", Value: "false"}, + {Name: "TS_AUTH_ONCE", Value: "true"}, + {Name: "TS_KUBE_SECRET", Value: opts.secretName}, + {Name: "TS_HOSTNAME", Value: hostname}, + {Name: "TS_EXTRA_ARGS", Value: fmt.Sprintf("--advertise-exit-node=%v", opts.isExitNode)}, + {Name: "TS_ROUTES", Value: opts.routes}, + } + sts := &appsv1.StatefulSet{ TypeMeta: metav1.TypeMeta{ Kind: "StatefulSet", APIVersion: "apps/v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: stsName, + Name: opts.stsName, Namespace: "operator-ns", Labels: map[string]string{ "tailscale.com/managed": "true", "tailscale.com/parent-resource": "test", "tailscale.com/parent-resource-ns": "", - "tailscale.com/parent-resource-type": "subnetrouter", + "tailscale.com/parent-resource-type": "connector", }, }, Spec: appsv1.StatefulSetSpec{ @@ -116,13 +210,13 @@ func expectedConnectorSTS(stsName, secretName, routes string) *appsv1.StatefulSe Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": "1234-UID"}, }, - ServiceName: stsName, + ServiceName: opts.stsName, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ DeletionGracePeriodSeconds: ptr.To[int64](10), Labels: map[string]string{"app": "1234-UID"}, Annotations: map[string]string{ - "tailscale.com/operator-last-set-hostname": "test-subnetrouter", + "tailscale.com/operator-last-set-hostname": hostname, }, }, Spec: corev1.PodSpec{ @@ -142,13 +236,7 @@ func expectedConnectorSTS(stsName, secretName, routes string) *appsv1.StatefulSe { Name: "tailscale", Image: "tailscale/tailscale", - Env: []corev1.EnvVar{ - {Name: "TS_USERSPACE", Value: "false"}, - {Name: "TS_AUTH_ONCE", Value: "true"}, - {Name: "TS_KUBE_SECRET", Value: secretName}, - {Name: "TS_HOSTNAME", Value: "test-subnetrouter"}, - {Name: "TS_ROUTES", Value: routes}, - }, + Env: containerEnv, SecurityContext: &corev1.SecurityContext{ Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_ADMIN"}, @@ -161,4 +249,5 @@ func expectedConnectorSTS(stsName, secretName, routes string) *appsv1.StatefulSe }, }, } + return sts } diff --git a/cmd/k8s-operator/sts.go b/cmd/k8s-operator/sts.go index 6e54d9741..9b8c857ec 100644 --- a/cmd/k8s-operator/sts.go +++ b/cmd/k8s-operator/sts.go @@ -79,9 +79,16 @@ type tailscaleSTSConfig struct { Hostname string Tags []string // if empty, use defaultTags - // Routes is a list of CIDRs to pass via --advertise-routes flag - // Should only be set if this is config for subnetRouter - Routes string + // Connector specifies a configuration of a Connector instance if that's + // what this StatefulSet should be created for. + Connector *connector +} + +type connector struct { + // routes is a list of subnet routes that this Connector should expose. + routes string + // isExitNode defines whether this Connector should act as an exit node. + isExitNode bool } type tailscaleSTSReconciler struct { @@ -419,19 +426,24 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S }, }, }) - } else if len(sts.Routes) > 0 { + } else if sts.Connector != nil { + // We need to provide these env vars even if the values are empty to + // ensure that a transition from a Connector with a defined subnet + // router or exit node to one without succeeds. + container.Env = append(container.Env, corev1.EnvVar{ + Name: "TS_EXTRA_ARGS", + Value: fmt.Sprintf("--advertise-exit-node=%v", sts.Connector.isExitNode), + }) container.Env = append(container.Env, corev1.EnvVar{ Name: "TS_ROUTES", - Value: sts.Routes, + Value: sts.Connector.routes, }) - } if a.tsFirewallMode != "" { container.Env = append(container.Env, corev1.EnvVar{ Name: "TS_DEBUG_FIREWALL_MODE", Value: a.tsFirewallMode, - }, - ) + }) } ss.ObjectMeta = metav1.ObjectMeta{ Name: headlessSvc.Name,