Skip to content

Commit 1cf0494

Browse files
stubbiclaude
andcommitted
fix: add K8s API egress and sandbox scheduling env vars
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 165cfe8 commit 1cf0494

3 files changed

Lines changed: 130 additions & 1 deletion

File tree

internal/resources/networkpolicy.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ func BuildNetworkPolicy(instance *paperclipv1alpha1.Instance) *networkingv1.Netw
6161
},
6262
}
6363

64+
// Allow egress to K8s API server when cloud sandbox is enabled.
65+
// The server needs to create/manage sandbox pods via the K8s API.
66+
// An explicit rule is needed because some CNIs (k3s Flannel, Calico)
67+
// do not match host-network destinations with portOnly egress rules.
68+
if instance.Spec.Adapters.CloudSandbox != nil && instance.Spec.Adapters.CloudSandbox.Enabled {
69+
np.Spec.Egress = append(np.Spec.Egress, networkingv1.NetworkPolicyEgressRule{
70+
Ports: []networkingv1.NetworkPolicyPort{
71+
{
72+
Port: Ptr(intstr.FromInt32(6443)),
73+
Protocol: Ptr(corev1.ProtocolTCP),
74+
},
75+
},
76+
})
77+
}
78+
6479
// Allow egress to managed database if applicable
6580
if instance.Spec.Database.Mode == "managed" || instance.Spec.Database.Mode == "" {
6681
np.Spec.Egress = append(np.Spec.Egress, networkingv1.NetworkPolicyEgressRule{

internal/resources/resources_test.go

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
paperclipv1alpha1 "github.com/paperclipinc/paperclip-operator/api/v1alpha1"
1111
)
1212

13-
//nolint:unparam // test helper kept flexible for future test cases
1413
func newTestInstance(name string) *paperclipv1alpha1.Instance {
1514
return &paperclipv1alpha1.Instance{
1615
ObjectMeta: metav1.ObjectMeta{
@@ -277,6 +276,39 @@ func TestBuildNetworkPolicy(t *testing.T) {
277276
}
278277
}
279278

279+
func TestBuildNetworkPolicyCloudSandboxK8sAPIEgress(t *testing.T) {
280+
instance := newTestInstance("my-paperclip")
281+
instance.Spec.Adapters.CloudSandbox = &paperclipv1alpha1.CloudSandboxSpec{
282+
Enabled: true,
283+
}
284+
np := BuildNetworkPolicy(instance)
285+
286+
found := false
287+
for _, rule := range np.Spec.Egress {
288+
for _, port := range rule.Ports {
289+
if port.Port != nil && port.Port.IntValue() == 6443 {
290+
found = true
291+
}
292+
}
293+
}
294+
if !found {
295+
t.Error("expected egress rule for K8s API port 6443 when cloud sandbox enabled")
296+
}
297+
}
298+
299+
func TestBuildNetworkPolicyNoK8sAPIEgressWithoutSandbox(t *testing.T) {
300+
instance := newTestInstance("my-paperclip")
301+
np := BuildNetworkPolicy(instance)
302+
303+
for _, rule := range np.Spec.Egress {
304+
for _, port := range rule.Ports {
305+
if port.Port != nil && port.Port.IntValue() == 6443 {
306+
t.Error("should not have K8s API egress rule when cloud sandbox is not enabled")
307+
}
308+
}
309+
}
310+
}
311+
280312
func TestBuildIngress(t *testing.T) {
281313
instance := newTestInstance("my-paperclip")
282314
instance.Spec.Networking.Ingress = &paperclipv1alpha1.IngressSpec{
@@ -666,6 +698,68 @@ func TestBuildStatefulSetNoCloudSandbox(t *testing.T) {
666698
}
667699
}
668700

701+
func TestBuildStatefulSetCloudSandboxSchedulingEnvVars(t *testing.T) {
702+
instance := newTestInstance("my-paperclip")
703+
instance.Spec.Adapters.CloudSandbox = &paperclipv1alpha1.CloudSandboxSpec{
704+
Enabled: true,
705+
}
706+
instance.Spec.Availability.NodeSelector = map[string]string{
707+
"cloud.google.com/gke-nodepool": "sandbox",
708+
}
709+
instance.Spec.Availability.Tolerations = []corev1.Toleration{
710+
{
711+
Key: "sandbox",
712+
Operator: corev1.TolerationOpEqual,
713+
Value: "true",
714+
Effect: corev1.TaintEffectNoSchedule,
715+
},
716+
}
717+
718+
sts := BuildStatefulSet(instance, nil)
719+
container := sts.Spec.Template.Spec.Containers[0]
720+
721+
envMap := make(map[string]string)
722+
for _, env := range container.Env {
723+
if env.Value != "" {
724+
envMap[env.Name] = env.Value
725+
}
726+
}
727+
728+
// Verify nodeSelector env var
729+
nsVal, ok := envMap["PAPERCLIP_CLOUD_SANDBOX_NODE_SELECTOR"]
730+
if !ok {
731+
t.Fatal("expected PAPERCLIP_CLOUD_SANDBOX_NODE_SELECTOR to be set")
732+
}
733+
if nsVal != `{"cloud.google.com/gke-nodepool":"sandbox"}` {
734+
t.Errorf("unexpected nodeSelector JSON: %s", nsVal)
735+
}
736+
737+
// Verify tolerations env var
738+
tolVal, ok := envMap["PAPERCLIP_CLOUD_SANDBOX_TOLERATIONS"]
739+
if !ok {
740+
t.Fatal("expected PAPERCLIP_CLOUD_SANDBOX_TOLERATIONS to be set")
741+
}
742+
if tolVal != `[{"key":"sandbox","operator":"Equal","value":"true","effect":"NoSchedule"}]` {
743+
t.Errorf("unexpected tolerations JSON: %s", tolVal)
744+
}
745+
746+
// Verify these are NOT set when availability scheduling is empty
747+
instance2 := newTestInstance("my-paperclip-2")
748+
instance2.Spec.Adapters.CloudSandbox = &paperclipv1alpha1.CloudSandboxSpec{
749+
Enabled: true,
750+
}
751+
sts2 := BuildStatefulSet(instance2, nil)
752+
container2 := sts2.Spec.Template.Spec.Containers[0]
753+
for _, env := range container2.Env {
754+
if env.Name == "PAPERCLIP_CLOUD_SANDBOX_NODE_SELECTOR" {
755+
t.Error("unexpected PAPERCLIP_CLOUD_SANDBOX_NODE_SELECTOR when nodeSelector is empty")
756+
}
757+
if env.Name == "PAPERCLIP_CLOUD_SANDBOX_TOLERATIONS" {
758+
t.Error("unexpected PAPERCLIP_CLOUD_SANDBOX_TOLERATIONS when tolerations is empty")
759+
}
760+
}
761+
}
762+
669763
func TestBuildSandboxRole(t *testing.T) {
670764
instance := newTestInstance("my-paperclip")
671765
role := BuildSandboxRole(instance, "test-ns")

internal/resources/statefulset.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package resources
22

33
import (
4+
"encoding/json"
45
"fmt"
56

67
appsv1 "k8s.io/api/apps/v1"
@@ -447,6 +448,25 @@ func buildCloudSandboxEnvVars(instance *paperclipv1alpha1.Instance) []corev1.Env
447448
vars = append(vars, corev1.EnvVar{Name: "PAPERCLIP_CLOUD_SANDBOX_MULTI_NAMESPACE", Value: "true"})
448449
}
449450

451+
// Node scheduling: pass the instance's scheduling constraints so the
452+
// Paperclip server can apply them to sandbox pods it creates.
453+
if len(instance.Spec.Availability.NodeSelector) > 0 {
454+
if b, err := json.Marshal(instance.Spec.Availability.NodeSelector); err == nil {
455+
vars = append(vars, corev1.EnvVar{
456+
Name: "PAPERCLIP_CLOUD_SANDBOX_NODE_SELECTOR",
457+
Value: string(b),
458+
})
459+
}
460+
}
461+
if len(instance.Spec.Availability.Tolerations) > 0 {
462+
if b, err := json.Marshal(instance.Spec.Availability.Tolerations); err == nil {
463+
vars = append(vars, corev1.EnvVar{
464+
Name: "PAPERCLIP_CLOUD_SANDBOX_TOLERATIONS",
465+
Value: string(b),
466+
})
467+
}
468+
}
469+
450470
return vars
451471
}
452472

0 commit comments

Comments
 (0)