From bde5ad2a223000162336aaa903d02af3c40941c1 Mon Sep 17 00:00:00 2001 From: Sheikh-Abubaker Date: Mon, 28 Oct 2024 17:44:45 +0530 Subject: [PATCH 1/3] Add button to fetch Deployment/StatefulSet logs Signed-off-by: Sheikh-Abubaker --- .../components/k8s-resources/Deployment.tsx | 274 ++++++++++++++++- .../ResourceList/ResourceList.tsx | 22 +- .../components/k8s-resources/StatefulSet.tsx | 277 +++++++++++++++++- 3 files changed, 561 insertions(+), 12 deletions(-) diff --git a/cyclops-ui/src/components/k8s-resources/Deployment.tsx b/cyclops-ui/src/components/k8s-resources/Deployment.tsx index e94cc92e..d87a2fd4 100644 --- a/cyclops-ui/src/components/k8s-resources/Deployment.tsx +++ b/cyclops-ui/src/components/k8s-resources/Deployment.tsx @@ -1,9 +1,12 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { Col, Divider, Row, Alert } from "antd"; +import React, { useCallback, useEffect, useState, useRef } from "react"; +import { Col, Divider, Row, Alert, TabsProps, Button, Tabs, Modal } from "antd"; import axios from "axios"; import { mapResponseError } from "../../utils/api/errors"; import PodTable from "./common/PodTable/PodTable"; import { isStreamingEnabled } from "../../utils/api/common"; +import { logStream } from "../../utils/api/sse/logs"; +import ReactAce from "react-ace/lib/ace"; +import { ReadOutlined } from "@ant-design/icons"; interface Props { name: string; @@ -11,7 +14,7 @@ interface Props { workload: any; } -const Deployment = ({ name, namespace, workload }: Props) => { +export const Deployment = ({ name, namespace, workload }: Props) => { const [deployment, setDeployment] = useState({ status: "", pods: [], @@ -108,4 +111,267 @@ const Deployment = ({ name, namespace, workload }: Props) => { ); }; -export default Deployment; +export const DeploymentLogsButton = ({ name, namespace, workload }: Props) => { + const [logs, setLogs] = useState([]); + const [logsModal, setLogsModal] = useState({ + on: false, + containers: [], + initContainers: [], + }); + + const logsSignalControllerRef = useRef(null); + + const [error, setError] = useState({ + message: "", + description: "", + }); + + const handleCancelLogs = () => { + setLogsModal({ + on: false, + containers: [], + initContainers: [], + }); + setLogs([]); + + // send the abort signal + if (logsSignalControllerRef.current !== null) { + logsSignalControllerRef.current.abort(); + } + }; + + const getTabItems = () => { + let items: TabsProps["items"] = []; + + let container: any; + + if (logsModal.containers !== null) { + for (container of logsModal.containers) { + items.push({ + key: container.name, + label: container.name, + children: ( + + + + + + ), + }); + } + } + + if (logsModal.initContainers !== null) { + for (container of logsModal.initContainers) { + items.push({ + key: container.name, + label: "(init container) " + container.name, + children: ( + + + + + + ), + }); + } + } + + return items; + }; + + const onLogsTabsChange = (container: string) => { + const controller = new AbortController(); + if (logsSignalControllerRef.current !== null) { + logsSignalControllerRef.current.abort(); + } + logsSignalControllerRef.current = controller; // store the controller to be able to abort the request + setLogs(() => []); + + if (isStreamingEnabled()) { + logStream( + name, + namespace, + workload.pods[0].containers[0].name, + (log, isReset = false) => { + if (isReset) { + setLogs(() => []); + } else { + setLogs((prevLogs) => { + return [...prevLogs, log]; + }); + } + }, + (err, isReset = false) => { + if (isReset) { + setError({ + message: "", + description: "", + }); + } else { + setError(mapResponseError(err)); + } + }, + controller, + ); + } else { + axios + .get( + "/api/resources/deployments/" + + namespace + + "/" + + name + + "/" + + workload.pods[0].containers[0].name + + "/logs", + ) + .then((res) => { + if (res.data) { + setLogs(res.data); + } else { + setLogs(() => []); + } + }) + .catch((error) => { + setError(mapResponseError(error)); + }); + } + }; + + const downloadLogs = (container: string) => { + return function () { + window.location.href = + "/api/resources/pods/" + + namespace + + "/" + + workload.pods[0].name + + "/" + + container + + "/logs/download"; + }; + }; + + return ( + <> + + + {error.message.length !== 0 && ( + { + setError({ + message: "", + description: "", + }); + }} + style={{ paddingBottom: "20px" }} + /> + )} + + + + ); +}; diff --git a/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx b/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx index 15ee2328..41a6b0bc 100644 --- a/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx +++ b/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx @@ -20,11 +20,11 @@ import { ResourceRef, resourceRefKey, } from "../../../utils/resourceRef"; -import Deployment from "../Deployment"; +import { Deployment, DeploymentLogsButton } from "../Deployment"; import CronJob from "../CronJob"; import Job from "../Job"; import DaemonSet from "../DaemonSet"; -import StatefulSet from "../StatefulSet"; +import { StatefulSet, StatefulSetLogsButton } from "../StatefulSet"; import Pod from "../Pod"; import Service from "../Service"; import ConfigMap from "../ConfigMap"; @@ -560,6 +560,24 @@ const ResourceList = ({ /> )} + {resource.kind === "Deployment" && ( + + + + )} + {resource.kind === "StatefulSet" && ( + + + + )} {resourceDetails} , diff --git a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx index a967cfc9..81180126 100644 --- a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx +++ b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx @@ -1,9 +1,12 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { Col, Divider, Row, Alert } from "antd"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Col, Divider, Row, Alert, TabsProps, Button, Modal, Tabs } from "antd"; import axios from "axios"; import { mapResponseError } from "../../utils/api/errors"; import PodTable from "./common/PodTable/PodTable"; import { isStreamingEnabled } from "../../utils/api/common"; +import ReactAce from "react-ace/lib/ace"; +import { logStream } from "../../utils/api/sse/logs"; +import { ReadOutlined } from "@ant-design/icons"; interface Props { name: string; @@ -11,12 +14,11 @@ interface Props { workload: any; } -const StatefulSet = ({ name, namespace, workload }: Props) => { +export const StatefulSet = ({ name, namespace, workload }: Props) => { const [statefulSet, setStatefulSet] = useState({ status: "", pods: [], }); - const [error, setError] = useState({ message: "", description: "", @@ -86,7 +88,7 @@ const StatefulSet = ({ name, namespace, workload }: Props) => { description: "", }); }} - style={{ marginBottom: "20px" }} + style={{ paddingBottom: "20px" }} /> )} @@ -109,4 +111,267 @@ const StatefulSet = ({ name, namespace, workload }: Props) => { ); }; -export default StatefulSet; +export const StatefulSetLogsButton = ({ name, namespace, workload }: Props) => { + const [logs, setLogs] = useState([]); + const [logsModal, setLogsModal] = useState({ + on: false, + containers: [], + initContainers: [], + }); + + const logsSignalControllerRef = useRef(null); + + const [error, setError] = useState({ + message: "", + description: "", + }); + + const handleCancelLogs = () => { + setLogsModal({ + on: false, + containers: [], + initContainers: [], + }); + setLogs([]); + + // send the abort signal + if (logsSignalControllerRef.current !== null) { + logsSignalControllerRef.current.abort(); + } + }; + + const getTabItems = () => { + let items: TabsProps["items"] = []; + + let container: any; + + if (logsModal.containers !== null) { + for (container of logsModal.containers) { + items.push({ + key: container.name, + label: container.name, + children: ( + + + + + + ), + }); + } + } + + if (logsModal.initContainers !== null) { + for (container of logsModal.initContainers) { + items.push({ + key: container.name, + label: "(init container) " + container.name, + children: ( + + + + + + ), + }); + } + } + + return items; + }; + + const onLogsTabsChange = (container: string) => { + const controller = new AbortController(); + if (logsSignalControllerRef.current !== null) { + logsSignalControllerRef.current.abort(); + } + logsSignalControllerRef.current = controller; // store the controller to be able to abort the request + setLogs(() => []); + + if (isStreamingEnabled()) { + logStream( + name, + namespace, + workload.pods[0].containers[0].name, + (log, isReset = false) => { + if (isReset) { + setLogs(() => []); + } else { + setLogs((prevLogs) => { + return [...prevLogs, log]; + }); + } + }, + (err, isReset = false) => { + if (isReset) { + setError({ + message: "", + description: "", + }); + } else { + setError(mapResponseError(err)); + } + }, + controller, + ); + } else { + axios + .get( + "/api/resources/deployments/" + + namespace + + "/" + + name + + "/" + + workload.pods[0].containers[0].name + + "/logs", + ) + .then((res) => { + if (res.data) { + setLogs(res.data); + } else { + setLogs(() => []); + } + }) + .catch((error) => { + setError(mapResponseError(error)); + }); + } + }; + + const downloadLogs = (container: string) => { + return function () { + window.location.href = + "/api/resources/pods/" + + namespace + + "/" + + workload.pods[0].name + + "/" + + container + + "/logs/download"; + }; + }; + + return ( + <> + + + {error.message.length !== 0 && ( + { + setError({ + message: "", + description: "", + }); + }} + style={{ paddingBottom: "20px" }} + /> + )} + + + + ); +}; From 0e9b4911971cb33d635537bf60a2473f80176b42 Mon Sep 17 00:00:00 2001 From: Sheikh-Abubaker Date: Mon, 28 Oct 2024 21:47:47 +0530 Subject: [PATCH 2/3] Revert style to marginBottom and remove container parameter from onLogsTabsChange Signed-off-by: Sheikh-Abubaker --- cyclops-ui/src/components/k8s-resources/Deployment.tsx | 4 ++-- cyclops-ui/src/components/k8s-resources/StatefulSet.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cyclops-ui/src/components/k8s-resources/Deployment.tsx b/cyclops-ui/src/components/k8s-resources/Deployment.tsx index d87a2fd4..37f1adc1 100644 --- a/cyclops-ui/src/components/k8s-resources/Deployment.tsx +++ b/cyclops-ui/src/components/k8s-resources/Deployment.tsx @@ -208,7 +208,7 @@ export const DeploymentLogsButton = ({ name, namespace, workload }: Props) => { return items; }; - const onLogsTabsChange = (container: string) => { + const onLogsTabsChange = () => { const controller = new AbortController(); if (logsSignalControllerRef.current !== null) { logsSignalControllerRef.current.abort(); @@ -367,7 +367,7 @@ export const DeploymentLogsButton = ({ name, namespace, workload }: Props) => { description: "", }); }} - style={{ paddingBottom: "20px" }} + style={{ marginBottom: "20px" }} /> )} diff --git a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx index 81180126..4174e4f4 100644 --- a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx +++ b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx @@ -88,7 +88,7 @@ export const StatefulSet = ({ name, namespace, workload }: Props) => { description: "", }); }} - style={{ paddingBottom: "20px" }} + style={{ marginBottom: "20px" }} /> )} @@ -208,7 +208,7 @@ export const StatefulSetLogsButton = ({ name, namespace, workload }: Props) => { return items; }; - const onLogsTabsChange = (container: string) => { + const onLogsTabsChange = () => { const controller = new AbortController(); if (logsSignalControllerRef.current !== null) { logsSignalControllerRef.current.abort(); @@ -367,7 +367,7 @@ export const StatefulSetLogsButton = ({ name, namespace, workload }: Props) => { description: "", }); }} - style={{ paddingBottom: "20px" }} + style={{ marginBottom: "20px" }} /> )} From 47a9c933dd41edd5527326afb6b4eed4197d7df6 Mon Sep 17 00:00:00 2001 From: Sheikh-Abubaker Date: Tue, 29 Oct 2024 02:32:01 +0530 Subject: [PATCH 3/3] Add handler functions for Deployment/Sts logs and point axios.get to correct sts api Signed-off-by: Sheikh-Abubaker --- cyclops-ctrl/internal/handler/handler.go | 5 ++++- cyclops-ui/src/components/k8s-resources/StatefulSet.tsx | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cyclops-ctrl/internal/handler/handler.go b/cyclops-ctrl/internal/handler/handler.go index 08e5019d..e4c1f32c 100644 --- a/cyclops-ctrl/internal/handler/handler.go +++ b/cyclops-ctrl/internal/handler/handler.go @@ -1,9 +1,10 @@ package handler import ( + "net/http" + "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/controller/sse" "github.com/gin-gonic/gin" - "net/http" "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/controller" "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/prometheus" @@ -86,6 +87,8 @@ func (h *Handler) Start() error { h.router.GET("/resources/pods/:namespace/:name/:container/logs", modulesController.GetLogs) h.router.GET("/resources/pods/:namespace/:name/:container/logs/stream", sse.HeadersMiddleware(), modulesController.GetLogsStream) h.router.GET("/resources/pods/:namespace/:name/:container/logs/download", modulesController.DownloadLogs) + h.router.GET("/resources/deployments/:namespace/:deployment/:container/logs", modulesController.GetDeploymentLogs) + h.router.GET("/resources/statefulsets/:namespace/:name/:container/logs", modulesController.GetStatefulSetsLogs) h.router.GET("/manifest", modulesController.GetManifest) h.router.GET("/resources", modulesController.GetResource) diff --git a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx index 4174e4f4..8bd5e494 100644 --- a/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx +++ b/cyclops-ui/src/components/k8s-resources/StatefulSet.tsx @@ -245,7 +245,7 @@ export const StatefulSetLogsButton = ({ name, namespace, workload }: Props) => { } else { axios .get( - "/api/resources/deployments/" + + "/api/resources/statefulsets/" + namespace + "/" + name + @@ -316,7 +316,7 @@ export const StatefulSetLogsButton = ({ name, namespace, workload }: Props) => { } else { axios .get( - "/api/resources/deployments/" + + "/api/resources/statefulsets/" + namespace + "/" + name +