diff --git a/pkg/admin/local.go b/pkg/admin/local.go index 268a4c1..8e07b0c 100644 --- a/pkg/admin/local.go +++ b/pkg/admin/local.go @@ -144,6 +144,7 @@ func (l *adminService) CreateBridge(ctx context.Context, req *bridgev1.CreateBri // Transforms applied to all bridge deployments. transforms := []resources.Transformer{ + resources.StripScalingResources(), resources.SetNamespace(targetNS), resources.PruneAllMetadata(), resources.StripOrphanedVolumes(), diff --git a/pkg/k8s/resources/transforms.go b/pkg/k8s/resources/transforms.go index 57a2f5c..1b74712 100644 --- a/pkg/k8s/resources/transforms.go +++ b/pkg/k8s/resources/transforms.go @@ -8,9 +8,38 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" ) +// scalingGroupKinds are source kinds that act on the source workload rather +// than the bridge. An HPA's scaleTargetRef and a PDB's selector still name the +// real Deployment and its pods after the bridge transforms, so a copy breaks +// the real service: its HPA reports AmbiguousSelector and stops scaling, and +// its pods, covered by more than one PDB, can't be evicted. A bridge is a +// single pod with nothing to scale. +var scalingGroupKinds = map[schema.GroupKind]bool{ + {Group: "autoscaling", Kind: "HorizontalPodAutoscaler"}: true, + {Group: "policy", Kind: "PodDisruptionBudget"}: true, + {Group: "keda.sh", Kind: "ScaledObject"}: true, +} + +// StripScalingResources returns a Transformer that drops autoscalers and +// disruption budgets (scalingGroupKinds) from the bundle. +func StripScalingResources() Transformer { + return TransformFunc(func(_ *TransformContext, b *Bundle) error { + kept := b.Resources[:0] + for _, r := range b.Resources { + if scalingGroupKinds[r.GVK.GroupKind()] { + continue + } + kept = append(kept, r) + } + b.Resources = kept + return nil + }) +} + // SetNamespace returns a Transformer that sets .metadata.namespace on all resources. func SetNamespace(ns string) Transformer { return TransformFunc(func(_ *TransformContext, b *Bundle) error { diff --git a/pkg/k8s/resources/transforms_test.go b/pkg/k8s/resources/transforms_test.go index 9116232..2fbcda0 100644 --- a/pkg/k8s/resources/transforms_test.go +++ b/pkg/k8s/resources/transforms_test.go @@ -194,3 +194,76 @@ func TestInjectProxyImage_OmitsMountRootsWhenNoVolumes(t *testing.T) { assert.NotEqual(t, "--mount-roots", a, "should not pass --mount-roots when source has no VolumeMounts") } } + +// TestStripScalingResources verifies that autoscalers and disruption budgets +// from source manifests never reach the bridge: they would act on the source +// workload, not the bridge. +func TestStripScalingResources(t *testing.T) { + 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 +--- +apiVersion: v1 +kind: Service +metadata: + name: app +spec: + selector: + app: app + ports: + - port: 80 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: app +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: app + minReplicas: 2 + maxReplicas: 10 +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: app-pdb +spec: + maxUnavailable: 1 + selector: + matchLabels: + app: app +--- +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: app +spec: + scaleTargetRef: + name: app +`})) + require.NoError(t, err) + + require.Len(t, bundle.Resources, 5, "the source keeps all five") + require.NoError(t, StripScalingResources().Apply(&TransformContext{}, bundle)) + + var kinds []string + for _, r := range bundle.Resources { + kinds = append(kinds, r.GVK.Kind) + } + assert.ElementsMatch(t, []string{"Deployment", "Service"}, kinds) +}