|
| 1 | +package e2erayjob |
| 2 | + |
| 3 | +import ( |
| 4 | + "testing" |
| 5 | + |
| 6 | + . "github.com/onsi/gomega" |
| 7 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 8 | + |
| 9 | + rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1" |
| 10 | + "github.com/ray-project/kuberay/ray-operator/controllers/ray/utils" |
| 11 | + rayv1ac "github.com/ray-project/kuberay/ray-operator/pkg/client/applyconfiguration/ray/v1" |
| 12 | + . "github.com/ray-project/kuberay/ray-operator/test/support" |
| 13 | +) |
| 14 | + |
| 15 | +// RayJob without clusterSelector, under NetworkPolicy DenyAll. The operator owns |
| 16 | +// the RayCluster so it adds the submitter ingress rule itself; we only add DNS |
| 17 | +// egress. Kindnet enforces NetworkPolicy on k8s >= 1.32 (CI runs on 1.35), so a |
| 18 | +// wrong head/worker policy means the pods can't resolve each other and the job |
| 19 | +// never finishes. |
| 20 | +func TestRayJobWithNetworkPolicy(t *testing.T) { |
| 21 | + test := With(t) |
| 22 | + g := NewWithT(t) |
| 23 | + |
| 24 | + namespace := test.NewTestNamespace() |
| 25 | + |
| 26 | + // Job scripts. |
| 27 | + jobsAC := NewConfigMap(namespace.Name, Files(test, "counter.py")) |
| 28 | + jobs, err := test.Client().Core().CoreV1().ConfigMaps(namespace.Name).Apply(test.Ctx(), jobsAC, TestApplyOptions) |
| 29 | + g.Expect(err).NotTo(HaveOccurred()) |
| 30 | + LogWithTimestamp(test.T(), "Created ConfigMap %s/%s successfully", jobs.Namespace, jobs.Name) |
| 31 | + |
| 32 | + // DenyAll: intra-cluster and submitter ingress are handled by the operator, |
| 33 | + // we just need DNS egress. |
| 34 | + networkPolicy := rayv1ac.NetworkPolicyConfig(). |
| 35 | + WithMode(rayv1.NetworkPolicyDenyAll). |
| 36 | + WithHead(rayv1ac.NetworkPolicyRules().WithEgressRules(DNSEgressRule())). |
| 37 | + WithWorker(rayv1ac.NetworkPolicyRules().WithEgressRules(DNSEgressRule())) |
| 38 | + |
| 39 | + rayJobAC := rayv1ac.RayJob("counter", namespace.Name). |
| 40 | + WithSpec(rayv1ac.RayJobSpec(). |
| 41 | + WithRayClusterSpec(NewRayClusterSpec(MountConfigMap[rayv1ac.RayClusterSpecApplyConfiguration](jobs, "/home/ray/jobs")). |
| 42 | + WithNetworkPolicy(networkPolicy)). |
| 43 | + WithEntrypoint("python /home/ray/jobs/counter.py"). |
| 44 | + WithRuntimeEnvYAML(` |
| 45 | +env_vars: |
| 46 | + counter_name: test_counter |
| 47 | +`). |
| 48 | + WithShutdownAfterJobFinishes(false). |
| 49 | + WithSubmitterPodTemplate(JobSubmitterPodTemplateApplyConfiguration())) |
| 50 | + |
| 51 | + rayJob, err := test.Client().Ray().RayV1().RayJobs(namespace.Name).Apply(test.Ctx(), rayJobAC, TestApplyOptions) |
| 52 | + g.Expect(err).NotTo(HaveOccurred()) |
| 53 | + LogWithTimestamp(test.T(), "Created RayJob %s/%s successfully", rayJob.Namespace, rayJob.Name) |
| 54 | + |
| 55 | + // Wait for the operator to spin up the RayCluster. |
| 56 | + LogWithTimestamp(test.T(), "Waiting for RayJob %s/%s to start its RayCluster", rayJob.Namespace, rayJob.Name) |
| 57 | + g.Eventually(func(gg Gomega) string { |
| 58 | + rayJob, err = GetRayJob(test, rayJob.Namespace, rayJob.Name) |
| 59 | + gg.Expect(err).NotTo(HaveOccurred()) |
| 60 | + return rayJob.Status.RayClusterName |
| 61 | + }, TestTimeoutShort).ShouldNot(BeEmpty()) |
| 62 | + |
| 63 | + rayClusterName := rayJob.Status.RayClusterName |
| 64 | + // Both policies should exist before the cluster can become ready. |
| 65 | + g.Eventually(func(gg Gomega) { |
| 66 | + _, err := test.Client().Core().NetworkingV1().NetworkPolicies(namespace.Name). |
| 67 | + Get(test.Ctx(), rayClusterName+"-head", metav1.GetOptions{}) |
| 68 | + gg.Expect(err).NotTo(HaveOccurred()) |
| 69 | + _, err = test.Client().Core().NetworkingV1().NetworkPolicies(namespace.Name). |
| 70 | + Get(test.Ctx(), rayClusterName+"-workers", metav1.GetOptions{}) |
| 71 | + gg.Expect(err).NotTo(HaveOccurred()) |
| 72 | + }, TestTimeoutShort).Should(Succeed()) |
| 73 | + LogWithTimestamp(test.T(), "Head and worker NetworkPolicies created for RayCluster %s/%s", namespace.Name, rayClusterName) |
| 74 | + |
| 75 | + // If the policies are right the cluster starts and the job completes. |
| 76 | + LogWithTimestamp(test.T(), "Waiting for RayJob %s/%s to complete", rayJob.Namespace, rayJob.Name) |
| 77 | + g.Eventually(RayJob(test, rayJob.Namespace, rayJob.Name), TestTimeoutLong). |
| 78 | + Should(WithTransform(RayJobStatus, Satisfy(rayv1.IsJobTerminal))) |
| 79 | + |
| 80 | + g.Expect(GetRayJob(test, rayJob.Namespace, rayJob.Name)). |
| 81 | + To(WithTransform(RayJobStatus, Equal(rayv1.JobStatusSucceeded))) |
| 82 | +} |
| 83 | + |
| 84 | +// RayJob with clusterSelector pointing at a RayCluster we create ourselves, under |
| 85 | +// NetworkPolicy DenyAll. Since the RayCluster isn't owned by the RayJob, the |
| 86 | +// operator won't add the submitter ingress rule for us, so we add it by hand |
| 87 | +// (matching the submitter pod's labels) plus DNS egress. |
| 88 | +func TestRayJobWithClusterSelectorAndNetworkPolicy(t *testing.T) { |
| 89 | + test := With(t) |
| 90 | + g := NewWithT(t) |
| 91 | + |
| 92 | + namespace := test.NewTestNamespace() |
| 93 | + |
| 94 | + jobsAC := NewConfigMap(namespace.Name, Files(test, "counter.py")) |
| 95 | + jobs, err := test.Client().Core().CoreV1().ConfigMaps(namespace.Name).Apply(test.Ctx(), jobsAC, TestApplyOptions) |
| 96 | + g.Expect(err).NotTo(HaveOccurred()) |
| 97 | + LogWithTimestamp(test.T(), "Created ConfigMap %s/%s successfully", jobs.Namespace, jobs.Name) |
| 98 | + |
| 99 | + rayJobName := "counter" |
| 100 | + |
| 101 | + // Head needs the submitter ingress rule (dashboard, default 8265) + DNS; |
| 102 | + // workers just need DNS. |
| 103 | + networkPolicy := rayv1ac.NetworkPolicyConfig(). |
| 104 | + WithMode(rayv1.NetworkPolicyDenyAll). |
| 105 | + WithHead(rayv1ac.NetworkPolicyRules(). |
| 106 | + WithIngressRules(SubmitterIngressRule(rayJobName, utils.DefaultDashboardPort)). |
| 107 | + WithEgressRules(DNSEgressRule())). |
| 108 | + WithWorker(rayv1ac.NetworkPolicyRules().WithEgressRules(DNSEgressRule())) |
| 109 | + |
| 110 | + rayClusterAC := rayv1ac.RayCluster("raycluster", namespace.Name). |
| 111 | + WithSpec(NewRayClusterSpec(MountConfigMap[rayv1ac.RayClusterSpecApplyConfiguration](jobs, "/home/ray/jobs")). |
| 112 | + WithNetworkPolicy(networkPolicy)) |
| 113 | + |
| 114 | + rayCluster, err := test.Client().Ray().RayV1().RayClusters(namespace.Name).Apply(test.Ctx(), rayClusterAC, TestApplyOptions) |
| 115 | + g.Expect(err).NotTo(HaveOccurred()) |
| 116 | + LogWithTimestamp(test.T(), "Created RayCluster %s/%s successfully", rayCluster.Namespace, rayCluster.Name) |
| 117 | + |
| 118 | + // Sanity check that both policies landed. |
| 119 | + g.Eventually(func(gg Gomega) { |
| 120 | + _, err := test.Client().Core().NetworkingV1().NetworkPolicies(namespace.Name). |
| 121 | + Get(test.Ctx(), rayCluster.Name+"-head", metav1.GetOptions{}) |
| 122 | + gg.Expect(err).NotTo(HaveOccurred()) |
| 123 | + _, err = test.Client().Core().NetworkingV1().NetworkPolicies(namespace.Name). |
| 124 | + Get(test.Ctx(), rayCluster.Name+"-workers", metav1.GetOptions{}) |
| 125 | + gg.Expect(err).NotTo(HaveOccurred()) |
| 126 | + }, TestTimeoutShort).Should(Succeed()) |
| 127 | + |
| 128 | + LogWithTimestamp(test.T(), "Waiting for RayCluster %s/%s to become ready", rayCluster.Namespace, rayCluster.Name) |
| 129 | + g.Eventually(RayCluster(test, rayCluster.Namespace, rayCluster.Name), TestTimeoutLong). |
| 130 | + Should(WithTransform(RayClusterState, Equal(rayv1.Ready))) |
| 131 | + |
| 132 | + // RayJob targeting the existing cluster by label. |
| 133 | + rayJobAC := rayv1ac.RayJob(rayJobName, namespace.Name). |
| 134 | + WithSpec(rayv1ac.RayJobSpec(). |
| 135 | + WithClusterSelector(map[string]string{utils.RayClusterLabelKey: rayCluster.Name}). |
| 136 | + WithEntrypoint("python /home/ray/jobs/counter.py"). |
| 137 | + WithRuntimeEnvYAML(` |
| 138 | +env_vars: |
| 139 | + counter_name: test_counter |
| 140 | +`). |
| 141 | + WithSubmitterPodTemplate(JobSubmitterPodTemplateApplyConfiguration())) |
| 142 | + |
| 143 | + rayJob, err := test.Client().Ray().RayV1().RayJobs(namespace.Name).Apply(test.Ctx(), rayJobAC, TestApplyOptions) |
| 144 | + g.Expect(err).NotTo(HaveOccurred()) |
| 145 | + LogWithTimestamp(test.T(), "Created RayJob %s/%s successfully", rayJob.Namespace, rayJob.Name) |
| 146 | + |
| 147 | + // The submitter has to get through the head policy to reach the dashboard, so |
| 148 | + // the job succeeding is what proves our ingress rule is correct. |
| 149 | + LogWithTimestamp(test.T(), "Waiting for RayJob %s/%s to complete", rayJob.Namespace, rayJob.Name) |
| 150 | + g.Eventually(RayJob(test, rayJob.Namespace, rayJob.Name), TestTimeoutLong). |
| 151 | + Should(WithTransform(RayJobStatus, Satisfy(rayv1.IsJobTerminal))) |
| 152 | + |
| 153 | + g.Expect(GetRayJob(test, rayJob.Namespace, rayJob.Name)). |
| 154 | + To(WithTransform(RayJobStatus, Equal(rayv1.JobStatusSucceeded))) |
| 155 | +} |
0 commit comments