Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add functionality to get logs for jobs #276

Merged
merged 9 commits into from
Mar 27, 2023
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
1 change: 1 addition & 0 deletions app/kube-knots/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ fn main() {
// jobs
workloads::jobs::create_job,
workloads::jobs::delete_job,
workloads::jobs::get_job_logs,
workloads::jobs::get_jobs,
workloads::jobs::update_job,
// pod metrics
Expand Down
36 changes: 32 additions & 4 deletions app/kube-knots/src-tauri/src/workloads/jobs.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use k8s_openapi::api::batch::v1::Job;
use kube::core::ObjectList;
use k8s_openapi::api::{batch::v1::Job, core::v1::Pod};
use kube::{api::ListParams, core::ObjectList, Api};

use crate::internal::resources::{
create_resource, delete_resource, get_resources, update_resource,
use crate::internal::{
client::get_resource_api,
resources::{create_resource, delete_resource, get_resources, update_resource},
};

use super::pods::get_pod_logs;

#[tauri::command]
pub async fn get_jobs(
context: Option<String>,
Expand Down Expand Up @@ -36,3 +39,28 @@ pub async fn delete_job(
) -> Result<bool, String> {
return delete_resource::<Job>(context, namespace, name).await;
}

#[tauri::command]
pub async fn get_job_logs(
context: Option<String>,
namespace: Option<String>,
job_name: String,
) -> Result<String, String> {
let api: Api<Pod> = get_resource_api(context.clone(), namespace.clone()).await?;
let mut lp = ListParams::default();
lp.label_selector = Some(format!("job-name={}", job_name));

let result = api.list(&lp).await.unwrap();

let pod = result.items.get(0);

if pod.is_none() {
return Ok("No pod found for job".to_string());
}

let pod = pod.unwrap();

let pod_name = pod.metadata.name.clone().unwrap();

return get_pod_logs(context, namespace, pod_name, None).await;
}
119 changes: 72 additions & 47 deletions app/kube-knots/src/components/pod-logs.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Listbox } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
import { type V1Pod } from "@kubernetes/client-node";
import type { V1Pod, V1Job } from "@kubernetes/client-node";
import { useQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api";
import { type ChangeEvent, useCallback, useEffect, useState } from "react";
import { toast } from "react-toastify";

import { useScrollBottom } from "../hooks/use-scroll-bottom";
import { useCurrentContext } from "../providers/current-context-provider";
Expand All @@ -12,14 +13,22 @@ import { ToggleButton } from "./base/toggle-button";

interface PodLogsProps {
isOpen: boolean;
selectedPod: V1Pod | null;
selected: V1Job | V1Pod | null;
handleClose: () => void;
}

export function PodLogs({ isOpen, selectedPod, handleClose }: PodLogsProps) {
const podName = selectedPod?.metadata?.name;
const namespace = selectedPod?.metadata?.namespace;
const [container, setContainer] = useState(selectedPod?.spec?.containers[0].name);
function isPod(resource: V1Job | V1Pod | null): resource is V1Pod {
return (resource as V1Pod)?.kind === "Pod";
}

export function PodLogs({ isOpen, selected, handleClose }: PodLogsProps) {
const name = selected?.metadata?.name;
const namespace = selected?.metadata?.namespace;

const containerName =
selected && isPod(selected) ? selected?.spec?.containers[0].name : undefined;

const [container, setContainer] = useState(containerName);
const { currentContext } = useCurrentContext();

const [followLogs, setFollowLogs] = useState(true);
Expand All @@ -33,69 +42,85 @@ export function PodLogs({ isOpen, selectedPod, handleClose }: PodLogsProps) {
}, []);

useEffect(() => {
setContainer(selectedPod?.spec?.containers[0].name);
setContainer(isPod(selected) ? selected?.spec?.containers[0].name : undefined);
setSearch("");
}, [selectedPod]);
}, [selected]);

const command = isPod(selected) ? "get_pod_logs" : "get_job_logs";

const result = useQuery(
["pod-logs", currentContext, namespace, podName, container],
[command, currentContext, namespace, name, container],
() => {
return invoke<string>("get_pod_logs", {
podName: podName,
const sharedArgs = {
namespace,
container,
context: currentContext,
});
};

const args = isPod(selected)
? { container, podName: name }
: { jobName: selected?.metadata?.labels?.["job-name"] };

return invoke<string>(command, { ...sharedArgs, ...args });
},
{ enabled: !!podName, refetchInterval: 1000 }
{
enabled: !!name,
onError: (error) => toast.error(error as string),
retry: 1,
}
);

const data = result.data?.split("\n") ?? [];

const filteredData = data.filter((item) => {
let filteredData = data.filter((item) => {
return item.toLowerCase().includes(search.toLowerCase());
});

if ((filteredData.length === 1 && filteredData[0] === "") || filteredData.length === 0) {
filteredData = ["Waiting for logs..."];
}

const logBottomRef = useScrollBottom([result.data, followLogs]);

return (
<Drawer
isOpen={isOpen}
handleClose={handleClose}
title={selectedPod?.metadata?.name ?? ""}
title={selected?.metadata?.name ?? ""}
description={
<div className="flex items-center gap-4">
<Listbox value={container} onChange={(e) => setContainer(e)}>
<div className="relative z-10 w-60">
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-gray-100 py-2 pl-3 pr-10 text-left shadow-md dark:bg-gray-800">
<span className="block truncate">{container}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>

<Listbox.Options className="absolute mt-1 w-full overflow-auto rounded-md bg-gray-100 text-gray-800 shadow-lg dark:bg-gray-900 dark:text-gray-100">
{selectedPod?.spec?.containers.map((container, idx) => (
<Listbox.Option
key={idx}
className={`relative cursor-pointer select-none py-2 pl-10 pr-4 hover:bg-gray-200 dark:hover:bg-gray-800`}
value={container.name}
>
{({ selected }) => (
<>
<span className={`block truncate`}>{container.name}</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
{isPod(selected) && (
<Listbox value={container} onChange={(e) => setContainer(e)}>
<div className="relative z-10 w-60">
<Listbox.Button className="relative w-full cursor-default rounded-lg bg-gray-100 py-2 pl-3 pr-10 text-left shadow-md dark:bg-gray-800">
<span className="block truncate">{container}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</Listbox.Button>

<Listbox.Options className="absolute mt-1 w-full overflow-auto rounded-md bg-gray-100 text-gray-800 shadow-lg dark:bg-gray-900 dark:text-gray-100">
{selected?.spec?.containers.map((container, idx) => (
<Listbox.Option
key={idx}
className={`relative cursor-pointer select-none py-2 pl-10 pr-4 hover:bg-gray-200 dark:hover:bg-gray-800`}
value={container.name}
>
{({ selected }) => (
<>
<span className={`block truncate`}>{container.name}</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</div>
</Listbox>
)}

<ToggleButton
checked={followLogs}
Expand Down
2 changes: 1 addition & 1 deletion app/kube-knots/src/components/resource-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function ResourceTable<T extends ResourceBase>({

<Suspense fallback={<div>Loading Logs</div>}>
{actions.includes("logs") && (
<PodLogs isOpen={action === "logs"} handleClose={handleClose} selectedPod={selected} />
<PodLogs isOpen={action === "logs"} handleClose={handleClose} selected={selected} />
)}
</Suspense>
</QueryWrapper>
Expand Down
2 changes: 1 addition & 1 deletion app/kube-knots/src/workloads/jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function Jobs() {
<ResourceTable<V1Job>
command="get_jobs"
headers={["Name", "Image", "Last Run"]}
actions={["edit", "delete"]}
actions={["logs", "edit", "delete"]}
renderData={(item) => {
return (
<>
Expand Down