Skip to content

Commit af6af19

Browse files
committed
tighten up the tailscale-operator-manager
- remove unnecessary CRD props - validate CRD values - remove dead code - don't swallow errors
1 parent f54a55f commit af6af19

3 files changed

Lines changed: 31 additions & 46 deletions

File tree

containers/tailscale-operator-manager/Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ USER root
44
COPY requirements.txt /opt/cabotage/requirements.txt
55

66
RUN pip --no-cache-dir --disable-pip-version-check install -r /opt/cabotage/requirements.txt
7-
USER nobody
87

98
COPY operator.py /opt/cabotage/operator.py
109
ENTRYPOINT ["kopf", "run", "/opt/cabotage/operator.py", "--all-namespaces"]

containers/tailscale-operator-manager/crds/crd.yml

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ spec:
2020
- name: State
2121
type: string
2222
jsonPath: .status.reconcile_operator.state
23-
- name: Version
24-
type: string
25-
jsonPath: .status.reconcile_operator.operatorVersion
2623
- name: Age
2724
type: date
2825
jsonPath: .metadata.creationTimestamp
@@ -36,27 +33,26 @@ spec:
3633
type: object
3734
required:
3835
- clientId
39-
- operatorImage
4036
- organizationSlug
4137
properties:
4238
clientId:
4339
type: string
40+
maxLength: 255
4441
description: >
4542
Tailscale OIDC federated identity client ID.
4643
Cabotage mints JWTs signed by its OIDC issuer and
4744
writes them to a K8s Secret for the operator to use.
48-
operatorImage:
49-
type: string
50-
description: >
51-
Full image reference for the Tailscale operator
52-
(e.g. ghcr.io/tailscale/k8s-operator:v1.94.2).
5345
defaultTags:
5446
type: string
47+
maxLength: 512
48+
pattern: "^(tag:[a-z0-9-]+(,\\s*tag:[a-z0-9-]+)*)?$"
5549
description: >
5650
Default ACL tags for operator-created nodes
57-
(e.g. tag:k8s).
51+
(e.g. tag:cabotage).
5852
organizationSlug:
5953
type: string
54+
maxLength: 63
55+
pattern: "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
6056
description: >
6157
Cabotage organization slug for labeling.
6258
status:

containers/tailscale-operator-manager/operator.py

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,30 +45,6 @@ def startup_fn(logger, memo, settings, **kwargs):
4545
# ---------------------------------------------------------------------------
4646

4747

48-
def _ensure_resource(read_fn, create_fn, replace_fn, *args, **kwargs):
49-
"""Generic create-or-update pattern."""
50-
resource = kwargs.pop("resource")
51-
try:
52-
read_fn(*args)
53-
replace_fn(*args, resource)
54-
except ApiException as exc:
55-
if exc.status == 404:
56-
# For create, the name is part of the resource body, not a positional arg
57-
# create_fn signature varies: (namespace, body) or just (body)
58-
create_fn(resource)
59-
else:
60-
raise
61-
62-
63-
def _delete_if_exists(fn, *args, logger=None):
64-
try:
65-
fn(*args)
66-
except ApiException as exc:
67-
if exc.status != 404:
68-
if logger:
69-
logger.warning(f"Failed to delete {args}: {exc}")
70-
71-
7248
TAILNET_API = {
7349
"group": "tailscale.com",
7450
"version": "v1alpha1",
@@ -92,11 +68,27 @@ def _ensure_tailnet(custom_api, crd_name, secret_name, labels, logger):
9268
},
9369
}
9470
try:
95-
custom_api.get_cluster_custom_object(
71+
existing = custom_api.get_cluster_custom_object(
9672
TAILNET_API["group"], TAILNET_API["version"],
9773
TAILNET_API["plural"], crd_name,
9874
)
99-
logger.info(f"Tailnet {crd_name} already exists")
75+
existing_secret = existing.get("spec", {}).get("credentials", {}).get("secretName", "")
76+
if existing_secret != secret_name:
77+
logger.info(
78+
f"Tailnet {crd_name} has secretName={existing_secret!r}, "
79+
f"expected {secret_name!r} — recreating"
80+
)
81+
custom_api.delete_cluster_custom_object(
82+
TAILNET_API["group"], TAILNET_API["version"],
83+
TAILNET_API["plural"], crd_name,
84+
)
85+
custom_api.create_cluster_custom_object(
86+
TAILNET_API["group"], TAILNET_API["version"],
87+
TAILNET_API["plural"], body,
88+
)
89+
logger.info(f"Recreated Tailnet {crd_name}")
90+
else:
91+
logger.info(f"Tailnet {crd_name} already exists")
10092
except ApiException as exc:
10193
if exc.status == 404:
10294
custom_api.create_cluster_custom_object(
@@ -119,6 +111,7 @@ def _delete_tailnet(custom_api, crd_name, logger):
119111
except ApiException as exc:
120112
if exc.status != 404:
121113
logger.warning(f"Failed to delete Tailnet {crd_name}: {exc}")
114+
raise
122115

123116

124117
def _ensure_proxy_group(custom_api, crd_name, labels, default_tags, logger):
@@ -200,6 +193,7 @@ def _delete_proxy_group(custom_api, crd_name, logger):
200193
except ApiException as exc:
201194
if exc.status != 404:
202195
logger.warning(f"Failed to delete ProxyGroup {pg_name}: {exc}")
196+
raise
203197

204198

205199
# ---------------------------------------------------------------------------
@@ -212,30 +206,26 @@ def _delete_proxy_group(custom_api, crd_name, logger):
212206
def reconcile_operator(spec, name, namespace, memo, logger, retry, **kwargs):
213207
if not spec and retry < 5:
214208
raise kopf.TemporaryError("spec is not yet populated", delay=1)
209+
if not spec:
210+
raise kopf.PermanentError("spec is empty after 5 retries, giving up")
215211

216-
client_id = spec["clientId"]
217-
operator_image = spec["operatorImage"]
212+
default_tags = spec.get("defaultTags", "")
218213
default_tags = spec.get("defaultTags", "")
219214
org_slug = spec["organizationSlug"]
220215

221216
labels = _labels(org_slug)
222217
tailnet_secret_name = f"tailscale-tailnet-{name}"
223-
# The single operator runs in the cabotage namespace
224-
operator_namespace = "tailscale"
225218

226219
custom_api = kubernetes.client.CustomObjectsApi()
227220

228221
# 1. Tailnet CRD (cluster-scoped) — references the credential Secret
229222
# (cabotage-app creates the Secret before enqueuing the CRD)
230223
_ensure_tailnet(custom_api, name, tailnet_secret_name, labels, logger)
231224

232-
# 3. ProxyGroup (cluster-scoped) — references the Tailnet
225+
# 2. ProxyGroup (cluster-scoped) — references the Tailnet
233226
_ensure_proxy_group(custom_api, name, labels, default_tags, logger)
234227

235-
# Extract version from operator image tag
236-
version = operator_image.rsplit(":", 1)[-1] if ":" in operator_image else "unknown"
237-
238-
return {"state": "deployed", "operatorVersion": version}
228+
return {"state": "deployed"}
239229

240230

241231
@kopf.on.delete("cabotagetailscaleoperatorconfigs")

0 commit comments

Comments
 (0)