Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions backend/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
import yaml from "js-yaml";

export const app = express();
app.use(express.json());

app.use(cors({ origin: "*" }));

const kc = new KubeConfig();
Expand Down Expand Up @@ -479,6 +481,249 @@ app.get("/api/all-pods", async (req, res) => {
}
});

// POST /api/pods - Create a pod
app.post("/api/pods", async (req, res) => {
let podManifest = req.body;
console.log("Received pod manifest:", JSON.stringify(podManifest, null, 2));

try {
// Defensive: check manifest shape
if (
!podManifest ||
!podManifest.metadata ||
!podManifest.metadata.name ||
!podManifest.spec
) {
return res.status(400).json({ error: "Invalid pod manifest" });
}

// Fix the apiVersion for regular Pods
if (podManifest.apiVersion === "scheduling.volcano.sh/v1beta1") {
podManifest.apiVersion = "v1";
}

// Fix the kind
if (podManifest.kind !== "Pod") {
podManifest.kind = "Pod";
}

// Validate that this is a proper Pod spec (not Queue spec)
if (podManifest.spec.weight || podManifest.spec.reclaimable) {
return res.status(400).json({
error: "Invalid Pod spec. Use proper Pod specification with containers, not Queue fields.",
});
}

// Ensure Pod spec has required containers field
if (
!podManifest.spec.containers ||
!Array.isArray(podManifest.spec.containers)
) {
return res.status(400).json({
error: "Pod spec must include 'containers' array",
});
}

// Defensive: never allow "All", fallback to "default" if missing
let namespace = podManifest.metadata.namespace;
if (
!namespace ||
namespace === "All" ||
typeof namespace !== "string" ||
!namespace.trim()
) {
namespace = "default";
podManifest.metadata.namespace = namespace;
}

console.log("Creating pod in namespace:", namespace);
console.log(
"Final pod manifest:",
JSON.stringify(podManifest, null, 2),
);

// Use object-based syntax for consistency
const response = await k8sCoreApi.createNamespacedPod({
namespace: namespace,
body: podManifest,
});

res.status(201).json({
message: "Pod created successfully",
data: response.body,
});
} catch (error) {
console.error("Error creating pod:", error?.body || error);
let msg = "Failed to create pod";
if (error?.body?.message) {
msg = error.body.message;
} else if (error?.message) {
msg = error.message;
}
res.status(500).json({ error: msg });
}
});
// POST /api/jobs - Create a job (Volcano custom job)
app.post("/api/jobs", async (req, res) => {
const jobManifest = req.body;
try {
if (
!jobManifest ||
!jobManifest.metadata ||
!jobManifest.metadata.name ||
!jobManifest.spec
) {
return res.status(400).json({ error: "Invalid job manifest" });
}

const namespace = jobManifest.metadata.namespace || "default";

const response = await k8sApi.createNamespacedCustomObject({
group: "batch.volcano.sh",
version: "v1alpha1",
namespace,
plural: "jobs",
body: jobManifest,
});

res.status(201).json({
message: "Job created successfully",
data: response.body,
});
} catch (error) {
console.error("Error creating job:", error?.body || error);
let msg = "Failed to create job";
if (error?.body?.message) msg = error.body.message;
res.status(500).json({ error: msg });
}
});

//create a queue
app.post("/api/queues", async (req, res) => {
const queueManifest = req.body;

try {
if (
!queueManifest ||
!queueManifest.metadata ||
!queueManifest.metadata.name ||
!queueManifest.spec
) {
return res.status(400).json({ error: "Invalid queue manifest" });
}

const customAnnotations = queueManifest.annotations || {};
queueManifest.metadata.annotations = {
...(queueManifest.metadata.annotations || {}),
...customAnnotations,
};

// Debug the API client
console.log("API client type:", typeof k8sApi);
console.log(
"createClusterCustomObject method exists:",
typeof k8sApi.createClusterCustomObject,
);

// Test if we can list queues first (simpler operation)
console.log("Testing list operation first...");
try {
const listResponse = await k8sApi.listClusterCustomObject({
group: "scheduling.volcano.sh",
version: "v1beta1",
plural: "queues",
});
console.log(
"List operation successful, found",
listResponse.items?.length || 0,
"queues",
);
} catch (listError) {
console.error("List operation failed:", listError.message);
}

// Add timeout and more detailed logging
console.log("About to call k8sApi.createClusterCustomObject...");

const response = await Promise.race([
k8sApi.createClusterCustomObject({
group: "scheduling.volcano.sh",
version: "v1beta1",
plural: "queues",
body: queueManifest,
}),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("API call timeout after 30s")),
30000,
),
),
]);

console.log("API call successful, response:", response);

res.status(201).json({
message: "Queue created successfully",
data: response.body,
});
} catch (error) {
console.error("Error creating queue:", error?.body || error);
let msg = "Failed to create queue";
if (error?.body?.message) msg = error.body.message;
res.status(500).json({ error: msg });
}
});
// Delete a Volcano Job
app.delete("/api/jobs/:namespace/:name", async (req, res) => {
const { namespace, name } = req.params;
try {
// Try to delete the job directly (Kubernetes will 404 if not found)
const response = await k8sApi.deleteNamespacedCustomObject({
group: "batch.volcano.sh",
version: "v1alpha1",
namespace,
plural: "jobs",
name,
body: { propagationPolicy: "Foreground" },
});

return res.json({
message: "Job deleted successfully",
data: response.body,
});
} catch (err) {
// If Kubernetes provides a status code, use it
const statusCode = err?.statusCode || err?.response?.statusCode || 500;
let details = "Unknown error";
let k8sBody = err?.body || err?.response?.body;

// Try to parse the Kubernetes error body as JSON for best info
try {
if (typeof k8sBody === "string") {
k8sBody = JSON.parse(k8sBody);
}
if (k8sBody?.message) {
details = k8sBody.message;
} else if (k8sBody?.details) {
details = k8sBody.details;
}
} catch {
// If parsing fails, just use the raw body string
details = typeof k8sBody === "string" ? k8sBody : details;
}

// Log full error object for debugging
console.error("Full Kubernetes Error Object:", err);

// Respond with as much info from Kubernetes as possible
return res.status(statusCode).json({
error: k8sBody?.reason || "KubernetesError",
message: details,
code: statusCode,
k8s: k8sBody,
});
}
});
app.delete("/api/queues/:name", async (req, res) => {
const { name } = req.params;
const queueName = name.toLowerCase();
Expand Down
6 changes: 5 additions & 1 deletion deployment/volcano-dashboard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ rules:
- get
- list
- watch
- create
- delete

- apiGroups:
- ""
resources:
Expand All @@ -128,6 +131,7 @@ rules:
- list
- watch
- delete
- create
---

# volcano dashboard service
Expand All @@ -147,4 +151,4 @@ spec:
protocol: TCP
targetPort: 8080
selector:
app: volcano-dashboard
app: volcano-dashboard
Loading