From b870902c149f8335bcedcac991fb7edf3c5ff872 Mon Sep 17 00:00:00 2001 From: Raj Singh Date: Tue, 20 Jan 2026 13:27:55 -0500 Subject: [PATCH] cmd/k8s-operator: allow VIP ownership reclaim after operator recreation Fixes #18466 Signed-off-by: Raj Singh --- cmd/k8s-operator/api-server-proxy-pg.go | 9 +++++++-- cmd/k8s-operator/api-server-proxy-pg_test.go | 13 +++++++++++-- tool/gocross/gocross-wrapper.ps1 | 7 ++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/cmd/k8s-operator/api-server-proxy-pg.go b/cmd/k8s-operator/api-server-proxy-pg.go index 1a81e4967..27563c2e2 100644 --- a/cmd/k8s-operator/api-server-proxy-pg.go +++ b/cmd/k8s-operator/api-server-proxy-pg.go @@ -446,8 +446,8 @@ func exclusiveOwnerAnnotations(pg *tsapi.ProxyGroup, operatorID string, svc *tai if o == nil || len(o.OwnerRefs) == 0 { return nil, fmt.Errorf("Tailscale Service %s exists, but does not contain owner annotation with owner references; not proceeding as this is likely a resource created by something other than the Tailscale Kubernetes operator", svc.Name) } - if len(o.OwnerRefs) > 1 || o.OwnerRefs[0].OperatorID != operatorID { - return nil, fmt.Errorf("Tailscale Service %s is already owned by other operator(s) and cannot be shared across multiple clusters; configure a difference Service name to continue", svc.Name) + if len(o.OwnerRefs) > 1 { + return nil, fmt.Errorf("Tailscale Service %s is already owned by multiple operators and cannot be shared across multiple clusters; configure a difference Service name to continue", svc.Name) } if o.OwnerRefs[0].Resource == nil { return nil, fmt.Errorf("Tailscale Service %s exists, but does not reference an owning resource; not proceeding as this is likely a Service already owned by an Ingress", svc.Name) @@ -455,6 +455,11 @@ func exclusiveOwnerAnnotations(pg *tsapi.ProxyGroup, operatorID string, svc *tai if o.OwnerRefs[0].Resource.Kind != "ProxyGroup" || o.OwnerRefs[0].Resource.UID != string(pg.UID) { return nil, fmt.Errorf("Tailscale Service %s is already owned by another resource: %#v; configure a difference Service name to continue", svc.Name, o.OwnerRefs[0].Resource) } + // Allow operator pod recreation: if the ProxyGroup UID matches but operatorID differs, + // the operator was recreated and should reclaim ownership. + if o.OwnerRefs[0].OperatorID != operatorID { + o.OwnerRefs[0].OperatorID = operatorID + } if o.OwnerRefs[0].Resource.Name != pg.Name { // ProxyGroup name can be updated in place. o.OwnerRefs[0].Resource.Name = pg.Name diff --git a/cmd/k8s-operator/api-server-proxy-pg_test.go b/cmd/k8s-operator/api-server-proxy-pg_test.go index dee505723..c00bc36e9 100644 --- a/cmd/k8s-operator/api-server-proxy-pg_test.go +++ b/cmd/k8s-operator/api-server-proxy-pg_test.go @@ -323,13 +323,22 @@ func TestExclusiveOwnerAnnotations(t *testing.T) { }, }, }, - "owned_by_another_operator": { + "owned_by_another_operator_no_resource": { svc: &tailscale.VIPService{ Annotations: map[string]string{ ownerAnnotation: `{"ownerRefs":[{"operatorID":"operator-2"}]}`, }, }, - wantErr: "already owned by other operator(s)", + wantErr: "does not reference an owning resource", + }, + "operator_recreated_same_pg": { + // Different operatorID but same ProxyGroup UID - operator pod was recreated. + // Should allow reclaiming ownership. + svc: &tailscale.VIPService{ + Annotations: map[string]string{ + ownerAnnotation: `{"ownerRefs":[{"operatorID":"old-operator-id","resource":{"kind":"ProxyGroup","name":"pg1","uid":"pg1-uid"}}]}`, + }, + }, }, "owned_by_an_ingress": { svc: &tailscale.VIPService{ diff --git a/tool/gocross/gocross-wrapper.ps1 b/tool/gocross/gocross-wrapper.ps1 index fe0b46996..324b220c8 100644 --- a/tool/gocross/gocross-wrapper.ps1 +++ b/tool/gocross/gocross-wrapper.ps1 @@ -114,7 +114,12 @@ $bootstrapScriptBlock = { New-Item -Force -Path $toolchain -ItemType Directory | Out-Null Start-ChildScope -ScriptBlock { Set-Location -LiteralPath $toolchain - tar --strip-components=1 -xf "$toolchain.tar.gz" + + # Using an absolute path to the tar that ships with Windows + # to avoid conflicts with others (eg msys2). + $system32 = [System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::System) + $tar = Join-Path $system32 'tar.exe' -Resolve + & $tar --strip-components=1 -xf "$toolchain.tar.gz" if ($LASTEXITCODE -ne 0) { throw "tar failed with exit code $LASTEXITCODE" }