From 2e4eb2fea2057a426e9a3e69fc5910a567d67430 Mon Sep 17 00:00:00 2001 From: edwardowens Date: Thu, 1 Oct 2026 16:48:43 -0700 Subject: [PATCH] fix(k8s): give each bridge its own CA secret InjectCA named the secret "bridge-ca", and Rename only appends the device ID, so every bridge from a device shared bdg-bridge-ca-: - each create generated a new CA and replaced the one the device's other bridges had mounted; - two concurrent creates both found it missing and raced to create it, so the loser failed with AlreadyExists; - each create relabeled it with its own bridge's labels, so removing that bridge deleted the secret out from under the others, whose pods then can't restart. The secret is now named after the bridge (bdg-bridge-ca--), so it carries only its own bridge's labels and is removed with it. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/k8s/resources/transform_ca.go | 9 ++- pkg/k8s/resources/transform_ca_test.go | 83 ++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 pkg/k8s/resources/transform_ca_test.go diff --git a/pkg/k8s/resources/transform_ca.go b/pkg/k8s/resources/transform_ca.go index 1be6a12..c9fa427 100644 --- a/pkg/k8s/resources/transform_ca.go +++ b/pkg/k8s/resources/transform_ca.go @@ -33,7 +33,14 @@ func InjectCA(ns string) Transformer { return fmt.Errorf("generate CA: %w", err) } + // One secret per bridge. Rename only suffixes the device ID, so a fixed + // name was shared by every bridge from a device: each create replaced the + // CA the others had mounted, concurrent creates raced to create it, and + // removing the bridge that last labeled it deleted it for all of them. secretName := "bridge-ca" + if tc.BridgeName != "" { + secretName += "-" + tc.BridgeName + } secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -51,7 +58,7 @@ func InjectCA(ns string) Transformer { }) // Mount the secret into the deployment. The volume references - // secretName "bridge-ca" which SuffixNames + RewriteRefs will update. + // secretName, which SuffixNames + RewriteRefs will update. deploy, err := findApplicationDeployment(b, tc.SourceName) if err != nil { return &DeploymentNotFoundError{Name: tc.SourceName, Namespace: tc.SourceNamespace} diff --git a/pkg/k8s/resources/transform_ca_test.go b/pkg/k8s/resources/transform_ca_test.go new file mode 100644 index 0000000..b890ca3 --- /dev/null +++ b/pkg/k8s/resources/transform_ca_test.go @@ -0,0 +1,83 @@ +package resources + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/vercel/bridge/pkg/k8s/meta" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +// TestInjectCA_SecretPerBridge verifies that two bridges from one device get +// their own CA secrets, each labeled for, and mounted by, its own bridge. A +// shared secret let one bridge's create replace another's CA and its remove +// delete it. +func TestInjectCA_SecretPerBridge(t *testing.T) { + const suffix = "-dev123" + type bridged struct { + secretName, secretDeployLabel, mountedSecret string + } + bridge := func(bridgeName string) bridged { + bundle, err := SourceFromManifests(packTestManifests(t, map[string]string{"manifests.yaml": `apiVersion: apps/v1 +kind: Deployment +metadata: + name: app +spec: + selector: + matchLabels: + app: app + template: + metadata: + labels: + app: app + spec: + containers: + - name: app + image: app:latest +`})) + require.NoError(t, err) + + tc := &TransformContext{Context: context.Background(), DeviceID: "dev123", BridgeName: bridgeName, SourceName: "app", SourceNamespace: "default"} + require.NoError(t, Transform(tc, bundle, []Transformer{ + InjectCA("default"), + Rename(func(name string) string { + if name == "app" { + return "bdg-" + bridgeName + suffix + } + return "bdg-" + name + suffix + }), + InjectLabels(), + RewriteRefs(), + })) + + var out bridged + for _, r := range bundle.Resources { + switch obj := r.Object.(type) { + case *corev1.Secret: + out.secretName = obj.Name + out.secretDeployLabel = obj.Labels[meta.LabelBridgeDeployment] + case *appsv1.Deployment: + for _, v := range obj.Spec.Template.Spec.Volumes { + if v.Name == caSecretVolumeName { + out.mountedSecret = v.Secret.SecretName + } + } + } + } + return out + } + + a, b := bridge("alpha"), bridge("beta") + + assert.Equal(t, "bdg-bridge-ca-alpha"+suffix, a.secretName) + assert.Equal(t, "bdg-bridge-ca-beta"+suffix, b.secretName) + assert.Equal(t, "bdg-alpha"+suffix, a.secretDeployLabel, "removed with its own bridge") + assert.Equal(t, "bdg-beta"+suffix, b.secretDeployLabel, "removed with its own bridge") + assert.Equal(t, a.secretName, a.mountedSecret) + assert.Equal(t, b.secretName, b.mountedSecret) +}