From 0843986a0dc90b233b353d956109fb3c369570ab Mon Sep 17 00:00:00 2001 From: Raj Singh Date: Tue, 31 Mar 2026 13:00:41 -0500 Subject: [PATCH] k8s-operator: remove minItems=1 from appConnector routes AppConnector.Routes is documented as optional (routes are discovered dynamically when not set), but the shared Routes type enforces MinItems=1 via kubebuilder validation. This makes appConnector: {} fail CRD validation through server-side apply, breaking GitOps deployments (Flux, ArgoCD with SSA). Decouple AppConnector.Routes from the shared Routes type so the MinItems constraint only applies to SubnetRouter.AdvertiseRoutes where it's actually needed. Fixes #19195 Signed-off-by: Raj Singh --- cmd/k8s-operator/connector.go | 2 +- cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml | 1 - cmd/k8s-operator/deploy/manifests/operator.yaml | 1 - k8s-operator/api.md | 4 ++-- k8s-operator/apis/v1alpha1/types_connector.go | 2 +- k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go | 2 +- 6 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cmd/k8s-operator/connector.go b/cmd/k8s-operator/connector.go index 0c2d32482..4c35b442b 100644 --- a/cmd/k8s-operator/connector.go +++ b/cmd/k8s-operator/connector.go @@ -218,7 +218,7 @@ func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logge if cn.Spec.AppConnector != nil { sts.Connector.isAppConnector = true if len(cn.Spec.AppConnector.Routes) != 0 { - sts.Connector.routes = cn.Spec.AppConnector.Routes.Stringify() + sts.Connector.routes = tsapi.Routes(cn.Spec.AppConnector.Routes).Stringify() } } diff --git a/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml b/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml index 03c51c755..91e81ae71 100644 --- a/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml +++ b/cmd/k8s-operator/deploy/crds/tailscale.com_connectors.yaml @@ -99,7 +99,6 @@ spec: also dynamically discover other routes. https://tailscale.com/kb/1332/apps-best-practices#preconfiguration type: array - minItems: 1 items: type: string format: cidr diff --git a/cmd/k8s-operator/deploy/manifests/operator.yaml b/cmd/k8s-operator/deploy/manifests/operator.yaml index 597641bde..14df9fbb8 100644 --- a/cmd/k8s-operator/deploy/manifests/operator.yaml +++ b/cmd/k8s-operator/deploy/manifests/operator.yaml @@ -125,7 +125,6 @@ spec: items: format: cidr type: string - minItems: 1 type: array type: object exitNode: diff --git a/k8s-operator/api.md b/k8s-operator/api.md index 5a60f66e0..762c1b424 100644 --- a/k8s-operator/api.md +++ b/k8s-operator/api.md @@ -53,7 +53,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `routes` _[Routes](#routes)_ | Routes are optional preconfigured routes for the domains routed via the app connector.
If not set, routes for the domains will be discovered dynamically.
If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may
also dynamically discover other routes.
https://tailscale.com/kb/1332/apps-best-practices#preconfiguration | | Format: cidr
MinItems: 1
Type: string
| +| `routes` _[Route](#route) array_ | Routes are optional preconfigured routes for the domains routed via the app connector.
If not set, routes for the domains will be discovered dynamically.
If set, the app connector will immediately be able to route traffic using the preconfigured routes, but may
also dynamically discover other routes.
https://tailscale.com/kb/1332/apps-best-practices#preconfiguration | | Format: cidr
Type: string
| @@ -1049,6 +1049,7 @@ _Validation:_ - Type: string _Appears in:_ +- [AppConnector](#appconnector) - [Routes](#routes) @@ -1065,7 +1066,6 @@ _Validation:_ - Type: string _Appears in:_ -- [AppConnector](#appconnector) - [SubnetRouter](#subnetrouter) diff --git a/k8s-operator/apis/v1alpha1/types_connector.go b/k8s-operator/apis/v1alpha1/types_connector.go index af2df58af..4b18fb7fc 100644 --- a/k8s-operator/apis/v1alpha1/types_connector.go +++ b/k8s-operator/apis/v1alpha1/types_connector.go @@ -159,7 +159,7 @@ type AppConnector struct { // also dynamically discover other routes. // https://tailscale.com/kb/1332/apps-best-practices#preconfiguration // +optional - Routes Routes `json:"routes"` + Routes []Route `json:"routes,omitempty"` } type Tags []Tag diff --git a/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go b/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go index 2528c89f3..a29aacae6 100644 --- a/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go +++ b/k8s-operator/apis/v1alpha1/zz_generated.deepcopy.go @@ -18,7 +18,7 @@ func (in *AppConnector) DeepCopyInto(out *AppConnector) { *out = *in if in.Routes != nil { in, out := &in.Routes, &out.Routes - *out = make(Routes, len(*in)) + *out = make([]Route, len(*in)) copy(*out, *in) } }