akshaychitneni commented on code in PR #2244:
URL: 
https://github.com/apache/datafusion-ballista/pull/2244#discussion_r3760079034


##########
chaos-testing/src/k8s.rs:
##########
@@ -0,0 +1,526 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! A Kubernetes (kind) backend for the chaos harness.
+//!
+//! Where [`crate::cluster::TestCluster`] spawns the scheduler and executors as
+//! local OS processes, [`K8sCluster`] runs them as pods in a `kind` cluster: 
the
+//! scheduler as a Deployment behind a `ClusterIP` Service, and the executors 
as
+//! a labelled Deployment (so a later scenario can `scale`/`delete pod` them).
+//!
+//! The fixture is shared through a `hostPath` volume mounted into both pods.
+//! The harness writes the parquet under [`fixture_dir`] on the host; kind's
+//! `extraMounts` bind that path into the node, and each pod `hostPath`-mounts
+//! it, so the path string is identical on host, node, and pod and the
+//! schema-inferring `CREATE EXTERNAL TABLE ... LOCATION` that
+//! `Fixture::register_sql` emits resolves the same everywhere — no object 
store.
+//! The directory lives under `$HOME` rather than `/tmp`: Docker Desktop 
reliably
+//! shares the home directory into its VM (and thus the kind node), whereas the
+//! VM's `/tmp` is not the host's. Because kind is single-node, a rescheduled 
pod
+//! re-mounts the same directory, so the fixture survives executor kills.
+//!
+//! The harness process runs outside the cluster, so it reaches the scheduler's
+//! gRPC + REST (both on one port) through a `kubectl port-forward`. Results 
are
+//! fetched through the scheduler's embedded flight proxy
+//! (`advertise_flight_sql_endpoint`), so the client never contacts executor 
pod
+//! IPs directly.
+//!
+//! This backend shells out to `kubectl`; it assumes a `kind` cluster already
+//! exists, `kubectl` is on `PATH` pointed at it, and the chaos image has been
+//! `kind load`ed. See `chaos-testing/k8s/` and the crate README for the 
runbook.
+
+use std::net::TcpListener;
+use std::path::{Path, PathBuf};
+use std::process::{Child, Command, Stdio};
+use std::time::{Duration, Instant};
+
+/// Directory shared between the harness and the pods, holding the fixture.
+///
+/// Defaults to `$HOME/.ballista-chaos-fixtures` and can be overridden with
+/// `CHAOS_FIXTURE_DIR` (the run script sets it so the kind `extraMounts` and
+/// this backend agree). A path under `$HOME` is used rather than `/tmp` 
because
+/// Docker Desktop reliably shares the home directory into its VM (and thus 
into
+/// the kind node), whereas the VM has its own `/tmp` that is not the host's.
+/// The path is identical on host, node, and pod, so the schema-inferring
+/// `CREATE EXTERNAL TABLE ... LOCATION` that `Fixture::register_sql` emits
+/// resolves the same everywhere.
+fn fixture_dir() -> String {
+    std::env::var("CHAOS_FIXTURE_DIR").unwrap_or_else(|_| {
+        let home = std::env::var("HOME").unwrap_or_else(|_| 
"/tmp".to_string());
+        format!("{home}/.ballista-chaos-fixtures")
+    })
+}
+
+const CHAOS_IMAGE: &str = "ballista-chaos:test";
+const SCHEDULER_PORT: u16 = 50050;
+const EXECUTOR_DEPLOYMENT: &str = "ballista-executor";
+
+/// How an executor pod is removed.
+#[derive(Clone, Copy, Debug)]
+pub enum KillMode {
+    /// `kubectl delete pod` — SIGTERM plus the termination grace period, so 
the
+    /// executor's graceful-shutdown path runs (the path a raw process 
`SIGKILL`
+    /// can never reach).
+    Graceful,
+    /// `kubectl delete pod --grace-period=0 --force` — an abrupt loss, the
+    /// closest k8s analogue of the process harness's `SIGKILL`.
+    Forced,
+}
+
+/// A Ballista cluster running as pods in a kind cluster.
+pub struct K8sCluster {
+    namespace: String,
+    scheduler_local_port: u16,
+    port_forward: Child,
+    shared_dir: PathBuf,
+}
+
+impl K8sCluster {
+    /// Deploy a scheduler + `executors` executor pods, wait until all 
executors
+    /// have registered, and open a port-forward to the scheduler.
+    pub async fn start(executors: usize) -> Result<Self, String> {
+        require_kubectl()?;
+
+        // One cluster per process; --test-threads=1 keeps it to one at a time.
+        let namespace = format!("chaos-{}", std::process::id());
+        let shared_dir = PathBuf::from(fixture_dir());
+
+        // Ensure the shared dir exists. Do NOT remove/recreate it: it is the
+        // bind-mount root, and deleting it can sever the mount so pod writes 
no
+        // longer reach the node. `Fixture::write` overwrites the parquet in
+        // place, so a stale deterministic fixture is harmless.
+        std::fs::create_dir_all(&shared_dir)
+            .map_err(|e| format!("create shared dir {}: {e}", 
shared_dir.display()))?;
+
+        let manifests = render_manifests(&namespace, executors, &shared_dir);
+        kubectl_apply(&manifests).await?;
+
+        // Guard so the namespace is torn down even if a later step fails.
+        let guard = NamespaceGuard {
+            namespace: namespace.clone(),
+        };
+
+        kubectl(&[
+            "-n",
+            &namespace,
+            "rollout",
+            "status",
+            "deploy/ballista-scheduler",
+            "--timeout=120s",
+        ])
+        .await?;
+
+        let scheduler_local_port = free_port()?;
+        let port_forward = spawn_port_forward(&namespace, 
scheduler_local_port)?;
+
+        let cluster = Self {
+            namespace,
+            scheduler_local_port,
+            port_forward,
+            shared_dir,
+        };
+
+        cluster.await_executors(executors).await?;
+
+        // Everything is up; keep the namespace (transfer ownership to 
`cluster`).
+        std::mem::forget(guard);
+        Ok(cluster)
+    }
+
+    /// `df://…` endpoint for the `ballista` client, via the port-forward.
+    pub fn scheduler_url(&self) -> String {
+        format!("df://127.0.0.1:{}", self.scheduler_local_port)
+    }
+
+    /// `http://…` endpoint for the scheduler REST API, via the port-forward.
+    pub fn rest_url(&self) -> String {
+        format!("http://127.0.0.1:{}";, self.scheduler_local_port)
+    }
+
+    /// The host directory shared into every pod; write the fixture here.
+    pub fn shared_dir(&self) -> &Path {
+        &self.shared_dir
+    }
+
+    /// Block until `n` executors have registered with the scheduler.
+    pub async fn await_executors(&self, n: usize) -> Result<(), String> {
+        let deadline = Instant::now() + Duration::from_secs(120);
+        loop {
+            if let Ok(count) = self.registered_executors().await
+                && count == n
+            {
+                return Ok(());
+            }
+            if Instant::now() > deadline {
+                self.dump_diagnostics().await;
+                return Err(format!(
+                    "timed out waiting for {n} executors to register with the 
scheduler"
+                ));
+            }
+            tokio::time::sleep(Duration::from_millis(500)).await;
+        }
+    }
+
+    /// Print pod status and scheduler/executor logs to stderr — invoked when a
+    /// wait times out, so a failed run is diagnosable even though the 
namespace
+    /// is torn down afterwards. Set `CHAOS_KEEP_NS=1` to keep the namespace 
for
+    /// manual `kubectl` inspection.
+    pub async fn dump_diagnostics(&self) {
+        eprintln!("==> chaos k8s diagnostics for namespace {}", 
self.namespace);
+        for args in [
+            vec!["-n", &self.namespace, "get", "pods", "-o", "wide"],
+            vec![
+                "-n",
+                &self.namespace,
+                "logs",
+                "-l",
+                "app=ballista-scheduler",
+                "--tail=40",
+            ],
+            vec![
+                "-n",
+                &self.namespace,
+                "logs",
+                "-l",
+                "app=ballista-executor",
+                "--tail=40",
+                "--prefix",
+            ],
+        ] {
+            match kubectl(&args).await {
+                Ok(out) => eprintln!("$ kubectl {}\n{out}", args.join(" ")),
+                Err(e) => eprintln!("$ kubectl {} -> {e}", args.join(" ")),
+            }
+        }
+    }
+
+    /// How many executors the scheduler currently considers registered.
+    pub async fn registered_executors(&self) -> Result<usize, String> {
+        let body: serde_json::Value =
+            reqwest::get(format!("{}/api/executors", self.rest_url()))
+                .await
+                .map_err(|e| e.to_string())?
+                .json()
+                .await
+                .map_err(|e| e.to_string())?;
+        Ok(body.as_array().map(|a| a.len()).unwrap_or(0))
+    }
+
+    /// Scale the executor Deployment. `0` is a total loss that stays lost (the
+    /// controller does not recreate the pods); scaling back up recovers.
+    pub async fn scale_executors(&self, replicas: usize) -> Result<(), String> 
{

Review Comment:
   it's the k8s primitive the executor kill/loss scenarios (the #2029 
follow-ups) will drive; the baseline scenario only needs a healthy cluster. 
Added a doc comment saying so, so it doesn't read as dead code



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to