Skip to content

Commit 63905fa

Browse files
committed
fix: validate envFrom sources and ignore configmap binary data
1 parent e0fc7a4 commit 63905fa

4 files changed

Lines changed: 86 additions & 18 deletions

File tree

api/v1alpha1/codercontrolplane_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ type CoderControlPlaneSpec struct {
9090
// +optional
9191
Expose *ExposeSpec `json:"expose,omitempty"`
9292

93+
// +kubebuilder:validation:XValidation:rule="self.all(e, !(has(e.configMapRef) && has(e.secretRef)))",message="each envFrom entry may specify at most one of configMapRef or secretRef"
9394
// EnvFrom injects environment variables from ConfigMaps/Secrets.
9495
EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty"`
9596
// Volumes are additional volumes to add to the pod.

config/crd/bases/coder.com_codercontrolplanes.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,10 @@ spec:
10221022
x-kubernetes-map-type: atomic
10231023
type: object
10241024
type: array
1025+
x-kubernetes-validations:
1026+
- message: each envFrom entry may specify at most one of configMapRef
1027+
or secretRef
1028+
rule: self.all(e, !(has(e.configMapRef) && has(e.secretRef)))
10251029
envUseClusterAccessURL:
10261030
default: true
10271031
description: EnvUseClusterAccessURL injects a default CODER_ACCESS_URL

internal/controller/codercontrolplane_controller.go

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2102,10 +2102,6 @@ func (r *CoderControlPlaneReconciler) envFromDefinesEnvVar(
21022102

21032103
for i := range envFromSources {
21042104
envFromSource := envFromSources[i]
2105-
if envFromSource.ConfigMapRef != nil && envFromSource.SecretRef != nil {
2106-
return false, fmt.Errorf("assertion failed: envFrom[%d] must not set both configMapRef and secretRef", i)
2107-
}
2108-
21092105
lookupKey, includeSource, err := envFromLookupKeyForEnvVar(envFromSource.Prefix, envVarName)
21102106
if err != nil {
21112107
return false, err
@@ -2123,18 +2119,12 @@ func (r *CoderControlPlaneReconciler) envFromDefinesEnvVar(
21232119
configMap := &corev1.ConfigMap{}
21242120
err := reader.Get(ctx, types.NamespacedName{Namespace: namespace, Name: configMapName}, configMap)
21252121
if err != nil {
2126-
if apierrors.IsNotFound(err) {
2127-
continue
2122+
if !apierrors.IsNotFound(err) {
2123+
return false, fmt.Errorf("get envFrom[%d] configmap %s/%s: %w", i, namespace, configMapName, err)
21282124
}
2129-
return false, fmt.Errorf("get envFrom[%d] configmap %s/%s: %w", i, namespace, configMapName, err)
2130-
}
2131-
if _, ok := configMap.Data[lookupKey]; ok {
2125+
} else if _, ok := configMap.Data[lookupKey]; ok {
21322126
return true, nil
21332127
}
2134-
if _, ok := configMap.BinaryData[lookupKey]; ok {
2135-
return true, nil
2136-
}
2137-
continue
21382128
}
21392129

21402130
if envFromSource.SecretRef != nil {
@@ -2146,12 +2136,10 @@ func (r *CoderControlPlaneReconciler) envFromDefinesEnvVar(
21462136
secret := &corev1.Secret{}
21472137
err := reader.Get(ctx, types.NamespacedName{Namespace: namespace, Name: secretName}, secret)
21482138
if err != nil {
2149-
if apierrors.IsNotFound(err) {
2150-
continue
2139+
if !apierrors.IsNotFound(err) {
2140+
return false, fmt.Errorf("get envFrom[%d] secret %s/%s: %w", i, namespace, secretName, err)
21512141
}
2152-
return false, fmt.Errorf("get envFrom[%d] secret %s/%s: %w", i, namespace, secretName, err)
2153-
}
2154-
if _, ok := secret.Data[lookupKey]; ok {
2142+
} else if _, ok := secret.Data[lookupKey]; ok {
21552143
return true, nil
21562144
}
21572145
}

internal/controller/codercontrolplane_controller_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,6 +3031,81 @@ func TestReconcile_DeploymentAlignment(t *testing.T) {
30313031
}
30323032
})
30333033

3034+
t.Run("EnvFromBinaryDataDoesNotSuppressDefault", func(t *testing.T) {
3035+
configMap := &corev1.ConfigMap{
3036+
ObjectMeta: metav1.ObjectMeta{Name: "test-deployment-alignment-envfrom-binary", Namespace: "default"},
3037+
BinaryData: map[string][]byte{
3038+
"CODER_ACCESS_URL": []byte("https://binary-data.example.test"),
3039+
},
3040+
}
3041+
if err := k8sClient.Create(ctx, configMap); err != nil {
3042+
t.Fatalf("create configmap: %v", err)
3043+
}
3044+
t.Cleanup(func() {
3045+
_ = k8sClient.Delete(ctx, configMap)
3046+
})
3047+
3048+
cp := &coderv1alpha1.CoderControlPlane{
3049+
ObjectMeta: metav1.ObjectMeta{Name: "test-deployment-alignment-envfrom-binary", Namespace: "default"},
3050+
Spec: coderv1alpha1.CoderControlPlaneSpec{
3051+
Image: "test-deployment-alignment:latest",
3052+
EnvFrom: []corev1.EnvFromSource{{
3053+
ConfigMapRef: &corev1.ConfigMapEnvSource{
3054+
LocalObjectReference: corev1.LocalObjectReference{Name: configMap.Name},
3055+
},
3056+
}},
3057+
},
3058+
}
3059+
if err := k8sClient.Create(ctx, cp); err != nil {
3060+
t.Fatalf("create control plane: %v", err)
3061+
}
3062+
t.Cleanup(func() {
3063+
_ = k8sClient.Delete(ctx, cp)
3064+
})
3065+
3066+
r := &controller.CoderControlPlaneReconciler{Client: k8sClient, APIReader: k8sClient, Scheme: scheme}
3067+
if _, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: cp.Name, Namespace: cp.Namespace}}); err != nil {
3068+
t.Fatalf("reconcile control plane: %v", err)
3069+
}
3070+
3071+
deployment := &appsv1.Deployment{}
3072+
if err := k8sClient.Get(ctx, types.NamespacedName{Name: cp.Name, Namespace: cp.Namespace}, deployment); err != nil {
3073+
t.Fatalf("get deployment: %v", err)
3074+
}
3075+
container := deployment.Spec.Template.Spec.Containers[0]
3076+
expectedAccessURL := "http://" + cp.Name + "." + cp.Namespace + ".svc.cluster.local"
3077+
if got := mustFindEnvVar(t, container.Env, "CODER_ACCESS_URL").Value; got != expectedAccessURL {
3078+
t.Fatalf("expected default CODER_ACCESS_URL %q, got %q", expectedAccessURL, got)
3079+
}
3080+
})
3081+
3082+
t.Run("EnvFromWithConfigMapAndSecretRefsIsRejectedAtAdmission", func(t *testing.T) {
3083+
cp := &coderv1alpha1.CoderControlPlane{
3084+
ObjectMeta: metav1.ObjectMeta{Name: "test-deployment-alignment-envfrom-both-refs", Namespace: "default"},
3085+
Spec: coderv1alpha1.CoderControlPlaneSpec{
3086+
Image: "test-deployment-alignment:latest",
3087+
EnvFrom: []corev1.EnvFromSource{{
3088+
ConfigMapRef: &corev1.ConfigMapEnvSource{
3089+
LocalObjectReference: corev1.LocalObjectReference{Name: "both-refs-configmap"},
3090+
},
3091+
SecretRef: &corev1.SecretEnvSource{
3092+
LocalObjectReference: corev1.LocalObjectReference{Name: "both-refs-secret"},
3093+
},
3094+
}},
3095+
},
3096+
}
3097+
err := k8sClient.Create(ctx, cp)
3098+
if err == nil {
3099+
t.Fatal("expected create to fail when envFrom entry sets both configMapRef and secretRef")
3100+
}
3101+
if !apierrors.IsInvalid(err) {
3102+
t.Fatalf("expected invalid error for envFrom entry with both refs, got: %v", err)
3103+
}
3104+
if !strings.Contains(err.Error(), "envFrom") {
3105+
t.Fatalf("expected envFrom validation error, got: %v", err)
3106+
}
3107+
})
3108+
30343109
t.Run("ResourcesAndSecurityContextsApplied", func(t *testing.T) {
30353110
runAsUser := int64(1001)
30363111
allowPrivilegeEscalation := false

0 commit comments

Comments
 (0)