@@ -23,6 +23,17 @@ def _oauth_secret_name(crd_name):
2323 return f"tailscale-oauth-{ crd_name } "
2424
2525
26+ def _proxy_group_name (crd_name ):
27+ return f"ingress-{ crd_name } "
28+
29+
30+ PROXY_GROUP_API = {
31+ "group" : "tailscale.com" ,
32+ "version" : "v1alpha1" ,
33+ "plural" : "proxygroups" ,
34+ }
35+
36+
2637def _labels (org_slug ):
2738 return {
2839 LABEL_KEY : LABEL_VALUE ,
@@ -213,6 +224,52 @@ def _delete_if_exists(fn, *args, logger=None):
213224 logger .warning (f"Failed to delete { args } : { exc } " )
214225
215226
227+ def _ensure_proxy_group (custom_api , namespace , crd_name , labels , logger ):
228+ """Create the ProxyGroup so ingresses appear as Tailscale Services."""
229+ pg_name = _proxy_group_name (crd_name )
230+ body = {
231+ "apiVersion" : f"{ PROXY_GROUP_API ['group' ]} /{ PROXY_GROUP_API ['version' ]} " ,
232+ "kind" : "ProxyGroup" ,
233+ "metadata" : {
234+ "name" : pg_name ,
235+ "namespace" : namespace ,
236+ "labels" : labels ,
237+ },
238+ "spec" : {
239+ "type" : "ingress" ,
240+ },
241+ }
242+ try :
243+ custom_api .get_namespaced_custom_object (
244+ PROXY_GROUP_API ["group" ], PROXY_GROUP_API ["version" ],
245+ namespace , PROXY_GROUP_API ["plural" ], pg_name ,
246+ )
247+ logger .info (f"ProxyGroup { pg_name } already exists" )
248+ except ApiException as exc :
249+ if exc .status == 404 :
250+ custom_api .create_namespaced_custom_object (
251+ PROXY_GROUP_API ["group" ], PROXY_GROUP_API ["version" ],
252+ namespace , PROXY_GROUP_API ["plural" ], body ,
253+ )
254+ logger .info (f"Created ProxyGroup { pg_name } " )
255+ else :
256+ raise
257+
258+
259+ def _delete_proxy_group (custom_api , namespace , crd_name , logger ):
260+ """Delete the ProxyGroup."""
261+ pg_name = _proxy_group_name (crd_name )
262+ try :
263+ custom_api .delete_namespaced_custom_object (
264+ PROXY_GROUP_API ["group" ], PROXY_GROUP_API ["version" ],
265+ namespace , PROXY_GROUP_API ["plural" ], pg_name ,
266+ )
267+ logger .info (f"Deleted ProxyGroup { pg_name } " )
268+ except ApiException as exc :
269+ if exc .status != 404 :
270+ logger .warning (f"Failed to delete ProxyGroup { pg_name } : { exc } " )
271+
272+
216273# ---------------------------------------------------------------------------
217274# Operator role rules (what the Tailscale operator needs within its namespace)
218275# ---------------------------------------------------------------------------
@@ -305,6 +362,7 @@ def reconcile_operator(spec, name, namespace, memo, logger, retry, **kwargs):
305362 core_api = kubernetes .client .CoreV1Api ()
306363 apps_api = kubernetes .client .AppsV1Api ()
307364 rbac_api = kubernetes .client .RbacAuthorizationV1Api ()
365+ custom_api = kubernetes .client .CustomObjectsApi ()
308366
309367 # 1. OAuth Secret
310368 _ensure_secret (core_api , namespace , secret_name , {
@@ -395,6 +453,9 @@ def reconcile_operator(spec, name, namespace, memo, logger, retry, **kwargs):
395453 raise
396454 logger .info (f"Ensured Deployment { deploy_name } " )
397455
456+ # 7. ProxyGroup for HA ingress (Tailscale Services instead of devices)
457+ _ensure_proxy_group (custom_api , namespace , name , labels , logger )
458+
398459 # Extract version from image tag
399460 version = operator_image .rsplit (":" , 1 )[- 1 ] if ":" in operator_image else "unknown"
400461
@@ -410,8 +471,10 @@ def delete_operator(spec, name, namespace, logger, **kwargs):
410471 core_api = kubernetes .client .CoreV1Api ()
411472 apps_api = kubernetes .client .AppsV1Api ()
412473 rbac_api = kubernetes .client .RbacAuthorizationV1Api ()
474+ custom_api = kubernetes .client .CustomObjectsApi ()
413475
414476 # Delete in reverse order
477+ _delete_proxy_group (custom_api , namespace , name , logger )
415478 _delete_if_exists (apps_api .delete_namespaced_deployment , operator_sa , namespace , logger = logger )
416479 _delete_if_exists (rbac_api .delete_namespaced_role_binding , operator_sa , namespace , logger = logger )
417480 _delete_if_exists (rbac_api .delete_namespaced_role , operator_sa , namespace , logger = logger )
0 commit comments