mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Add Kubernetes Cluster Management and Monitoring Agent
- Implemented a new migration for the KubernetesCluster and KubernetesClusterLabel tables in the database. - Created a KubernetesClusterService for managing cluster instances, including methods for finding or creating clusters, updating their status, and marking disconnected clusters. - Introduced a Helm chart for the OneUptime Kubernetes Monitoring Agent, including configuration files, deployment templates, and RBAC settings. - Added support for collecting metrics and logs from Kubernetes clusters using OpenTelemetry. - Configured service accounts, secrets, and resource limits for the agent's deployment and daemonset. - Provided detailed notes and helper templates for the Helm chart to facilitate installation and configuration.
This commit is contained in:
@@ -181,6 +181,15 @@ const ServiceRoutes: React.LazyExoticComponent<
|
||||
};
|
||||
});
|
||||
});
|
||||
const KubernetesRoutes: React.LazyExoticComponent<
|
||||
AllRoutesModule["KubernetesRoutes"]
|
||||
> = lazy(() => {
|
||||
return import("./Routes/AllRoutes").then((m: AllRoutesModule) => {
|
||||
return {
|
||||
default: m.KubernetesRoutes,
|
||||
};
|
||||
});
|
||||
});
|
||||
const CodeRepositoryRoutes: React.LazyExoticComponent<
|
||||
AllRoutesModule["CodeRepositoryRoutes"]
|
||||
> = lazy(() => {
|
||||
@@ -528,6 +537,12 @@ const App: () => JSX.Element = () => {
|
||||
element={<ServiceRoutes {...commonPageProps} />}
|
||||
/>
|
||||
|
||||
{/* Kubernetes */}
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.KUBERNETES_ROOT]?.toString() || ""}
|
||||
element={<KubernetesRoutes {...commonPageProps} />}
|
||||
/>
|
||||
|
||||
{/* Code Repository */}
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.CODE_REPOSITORY_ROOT]?.toString() || ""}
|
||||
|
||||
@@ -144,6 +144,17 @@ const DashboardNavbar: FunctionComponent<ComponentProps> = (
|
||||
iconColor: "indigo",
|
||||
category: "Observability",
|
||||
},
|
||||
{
|
||||
title: "Kubernetes",
|
||||
description: "Monitor Kubernetes clusters.",
|
||||
route: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route,
|
||||
),
|
||||
activeRoute: RouteMap[PageMap.KUBERNETES_CLUSTERS],
|
||||
icon: IconProp.Cube,
|
||||
iconColor: "teal",
|
||||
category: "Observability",
|
||||
},
|
||||
// Automation & Analytics
|
||||
{
|
||||
title: "Dashboards",
|
||||
|
||||
125
App/FeatureSet/Dashboard/src/Pages/Kubernetes/Clusters.tsx
Normal file
125
App/FeatureSet/Dashboard/src/Pages/Kubernetes/Clusters.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
import PageComponentProps from "../PageComponentProps";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
import ModelTable from "Common/UI/Components/ModelTable/ModelTable";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType";
|
||||
|
||||
const KubernetesClusters: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<Fragment>
|
||||
<ModelTable<KubernetesCluster>
|
||||
modelType={KubernetesCluster}
|
||||
id="kubernetes-clusters-table"
|
||||
userPreferencesKey="kubernetes-clusters-table"
|
||||
isDeleteable={false}
|
||||
isEditable={false}
|
||||
isCreateable={true}
|
||||
name="Kubernetes Clusters"
|
||||
isViewable={true}
|
||||
filters={[]}
|
||||
cardProps={{
|
||||
title: "Kubernetes Clusters",
|
||||
description:
|
||||
"Clusters being monitored in this project. Install the OneUptime kubernetes-agent Helm chart to connect a cluster.",
|
||||
}}
|
||||
noItemsMessage="No Kubernetes clusters connected yet."
|
||||
showViewIdButton={true}
|
||||
formFields={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "production-us-east",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
title: "Cluster Identifier",
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
required: true,
|
||||
placeholder: "production-us-east-1",
|
||||
description:
|
||||
"This should match the clusterName value in your kubernetes-agent Helm chart.",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: "Description",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: false,
|
||||
placeholder: "Production cluster running in US East",
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
title: "Cluster Identifier",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
otelCollectorStatus: true,
|
||||
},
|
||||
title: "Status",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
nodeCount: true,
|
||||
},
|
||||
title: "Nodes",
|
||||
type: FieldType.Number,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
podCount: true,
|
||||
},
|
||||
title: "Pods",
|
||||
type: FieldType.Number,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
provider: true,
|
||||
},
|
||||
title: "Provider",
|
||||
type: FieldType.Text,
|
||||
},
|
||||
]}
|
||||
onViewPage={(item: KubernetesCluster): Promise<Route> => {
|
||||
return Promise.resolve(
|
||||
new Route(
|
||||
RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW] as Route,
|
||||
{
|
||||
modelId: item._id,
|
||||
},
|
||||
).toString(),
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusters;
|
||||
25
App/FeatureSet/Dashboard/src/Pages/Kubernetes/Layout.tsx
Normal file
25
App/FeatureSet/Dashboard/src/Pages/Kubernetes/Layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getKubernetesBreadcrumbs } from "../../Utils/Breadcrumbs";
|
||||
import { RouteUtil } from "../../Utils/RouteMap";
|
||||
import LayoutPageComponentProps from "../LayoutPageComponentProps";
|
||||
import SideMenu from "./SideMenu";
|
||||
import Page from "Common/UI/Components/Page/Page";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
const KubernetesLayout: FunctionComponent<LayoutPageComponentProps> = (
|
||||
_props: LayoutPageComponentProps,
|
||||
): ReactElement => {
|
||||
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
|
||||
return (
|
||||
<Page
|
||||
title={"Kubernetes"}
|
||||
sideMenu={<SideMenu />}
|
||||
breadcrumbLinks={getKubernetesBreadcrumbs(path)}
|
||||
>
|
||||
<Outlet />
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesLayout;
|
||||
31
App/FeatureSet/Dashboard/src/Pages/Kubernetes/SideMenu.tsx
Normal file
31
App/FeatureSet/Dashboard/src/Pages/Kubernetes/SideMenu.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import PageMap from "../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import SideMenu, {
|
||||
SideMenuSectionProps,
|
||||
} from "Common/UI/Components/SideMenu/SideMenu";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const KubernetesSideMenu: FunctionComponent = (): ReactElement => {
|
||||
const sections: SideMenuSectionProps[] = [
|
||||
{
|
||||
title: "Kubernetes",
|
||||
items: [
|
||||
{
|
||||
link: {
|
||||
title: "All Clusters",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route,
|
||||
),
|
||||
},
|
||||
icon: IconProp.List,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return <SideMenu sections={sections} />;
|
||||
};
|
||||
|
||||
export default KubernetesSideMenu;
|
||||
@@ -0,0 +1,301 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
|
||||
const KubernetesClusterControlPlane: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
// etcd metrics (scraped via prometheus receiver)
|
||||
const etcdDbSizeQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "etcd_db_size",
|
||||
title: "etcd Database Size",
|
||||
description: "Total size of the etcd database",
|
||||
legend: "DB Size",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "etcd_mvcc_db_total_size_in_bytes",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// API Server request rate
|
||||
const apiServerRequestRateQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "apiserver_requests",
|
||||
title: "API Server Request Rate",
|
||||
description: "Total API server requests by verb",
|
||||
legend: "Requests",
|
||||
legendUnit: "req/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "apiserver_request_total",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Sum,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// API Server request latency
|
||||
const apiServerLatencyQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "apiserver_latency",
|
||||
title: "API Server Request Latency",
|
||||
description: "API server request duration",
|
||||
legend: "Latency",
|
||||
legendUnit: "seconds",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "apiserver_request_duration_seconds",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Scheduler pending pods
|
||||
const schedulerPendingQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "scheduler_pending",
|
||||
title: "Scheduler Pending Pods",
|
||||
description: "Number of pods pending scheduling",
|
||||
legend: "Pending Pods",
|
||||
legendUnit: "",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "scheduler_pending_pods",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Scheduler latency
|
||||
const schedulerLatencyQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "scheduler_latency",
|
||||
title: "Scheduler Latency",
|
||||
description: "End-to-end scheduling latency",
|
||||
legend: "Latency",
|
||||
legendUnit: "seconds",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "scheduler_e2e_scheduling_duration_seconds",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Controller Manager work queue depth
|
||||
const controllerQueueDepthQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "controller_queue",
|
||||
title: "Controller Manager Queue Depth",
|
||||
description: "Work queue depth for controller manager",
|
||||
legend: "Queue Depth",
|
||||
legendUnit: "",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "workqueue_depth",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [etcdMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [etcdDbSizeQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
const [apiServerMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [apiServerRequestRateQuery, apiServerLatencyQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
const [schedulerMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [schedulerPendingQuery, schedulerLatencyQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
const [controllerMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [controllerQueueDepthQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<p className="text-sm text-blue-700">
|
||||
Control plane metrics require the <code>controlPlane.enabled</code>{" "}
|
||||
flag to be set to <code>true</code> in the kubernetes-agent Helm chart
|
||||
values. This is typically only available for self-managed Kubernetes
|
||||
clusters, not managed services like EKS, GKE, or AKS.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title="etcd"
|
||||
description="etcd is the consistent, distributed key-value store used as the backing store for all cluster data."
|
||||
>
|
||||
<MetricView
|
||||
data={etcdMetricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="API Server"
|
||||
description="The Kubernetes API server validates and configures data for API objects and serves REST operations."
|
||||
>
|
||||
<MetricView
|
||||
data={apiServerMetricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Scheduler"
|
||||
description="The scheduler watches for newly created pods that have no node assigned and selects a node for them to run on."
|
||||
>
|
||||
<MetricView
|
||||
data={schedulerMetricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Controller Manager"
|
||||
description="The controller manager runs controller processes that regulate the state of the cluster."
|
||||
>
|
||||
<MetricView
|
||||
data={controllerMetricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterControlPlane;
|
||||
@@ -0,0 +1,34 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import ModelDelete from "Common/UI/Components/ModelDelete/ModelDelete";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const KubernetesClusterDelete: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ModelDelete
|
||||
modelType={KubernetesCluster}
|
||||
modelId={modelId}
|
||||
onDeleteSuccess={() => {
|
||||
Navigation.navigate(
|
||||
RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route,
|
||||
{ modelId },
|
||||
),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterDelete;
|
||||
253
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx
Normal file
253
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Events.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import AnalyticsModelAPI, {
|
||||
ListResult,
|
||||
} from "Common/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI";
|
||||
import Log from "Common/Models/AnalyticsModels/Log";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
|
||||
interface KubernetesEvent {
|
||||
timestamp: string;
|
||||
type: string;
|
||||
reason: string;
|
||||
objectKind: string;
|
||||
objectName: string;
|
||||
namespace: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const KubernetesClusterEvents: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [events, setEvents] = useState<Array<KubernetesEvent>>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchData: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
|
||||
if (!item?.clusterIdentifier) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -24);
|
||||
|
||||
const listResult: ListResult<Log> =
|
||||
await AnalyticsModelAPI.getList<Log>({
|
||||
modelType: Log,
|
||||
query: {
|
||||
projectId: ProjectUtil.getCurrentProjectId()!.toString(),
|
||||
time: {
|
||||
startValue: startDate,
|
||||
endValue: endDate,
|
||||
} as any,
|
||||
},
|
||||
limit: 200,
|
||||
skip: 0,
|
||||
select: {
|
||||
time: true,
|
||||
body: true,
|
||||
severityText: true,
|
||||
attributes: true,
|
||||
},
|
||||
sort: {
|
||||
time: SortOrder.Descending,
|
||||
},
|
||||
requestOptions: {},
|
||||
});
|
||||
|
||||
const k8sEvents: Array<KubernetesEvent> = [];
|
||||
|
||||
for (const log of listResult.data) {
|
||||
const attrs: JSONObject = log.attributes || {};
|
||||
|
||||
// Filter to only k8s events from this cluster
|
||||
if (
|
||||
attrs["k8s.cluster.name"] !== item.clusterIdentifier &&
|
||||
attrs["k8s_cluster_name"] !== item.clusterIdentifier
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// k8sobjects receiver events have k8s event attributes
|
||||
const eventType: string =
|
||||
(attrs["k8s.event.type"] as string) ||
|
||||
(attrs["type"] as string) ||
|
||||
"";
|
||||
const reason: string =
|
||||
(attrs["k8s.event.reason"] as string) ||
|
||||
(attrs["reason"] as string) ||
|
||||
"";
|
||||
const objectKind: string =
|
||||
(attrs["k8s.object.kind"] as string) ||
|
||||
(attrs["involvedObject.kind"] as string) ||
|
||||
"";
|
||||
const objectName: string =
|
||||
(attrs["k8s.object.name"] as string) ||
|
||||
(attrs["involvedObject.name"] as string) ||
|
||||
"";
|
||||
const namespace: string =
|
||||
(attrs["k8s.namespace.name"] as string) ||
|
||||
(attrs["namespace"] as string) ||
|
||||
"";
|
||||
|
||||
if (eventType || reason || objectKind) {
|
||||
k8sEvents.push({
|
||||
timestamp: log.time
|
||||
? OneUptimeDate.getDateAsLocalFormattedString(log.time)
|
||||
: "",
|
||||
type: eventType || "Unknown",
|
||||
reason: reason || "Unknown",
|
||||
objectKind: objectKind || "Unknown",
|
||||
objectName: objectName || "Unknown",
|
||||
namespace: namespace || "default",
|
||||
message: log.body || "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setEvents(k8sEvents);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Card
|
||||
title="Kubernetes Events"
|
||||
description="Events from the last 24 hours collected by the k8sobjects receiver. Warning events may indicate issues that need attention."
|
||||
>
|
||||
{events.length === 0 ? (
|
||||
<p className="text-gray-500 text-sm">
|
||||
No Kubernetes events found in the last 24 hours. Events will appear
|
||||
here once the kubernetes-agent is sending data.
|
||||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Time
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Reason
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Object
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Namespace
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Message
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{events.map(
|
||||
(event: KubernetesEvent, index: number) => {
|
||||
const isWarning: boolean =
|
||||
event.type.toLowerCase() === "warning";
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
className={
|
||||
isWarning ? "bg-yellow-50" : ""
|
||||
}
|
||||
>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||
{event.timestamp}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm">
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
isWarning
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-green-100 text-green-800"
|
||||
}`}
|
||||
>
|
||||
{event.type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||
{event.reason}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-900">
|
||||
{event.objectKind}/{event.objectName}
|
||||
</td>
|
||||
<td className="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||
{event.namespace}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-500 max-w-md truncate">
|
||||
{event.message}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterEvents;
|
||||
329
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx
Normal file
329
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Index.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
|
||||
const KubernetesClusterOverview: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
name: true,
|
||||
clusterIdentifier: true,
|
||||
provider: true,
|
||||
otelCollectorStatus: true,
|
||||
lastSeenAt: true,
|
||||
nodeCount: true,
|
||||
podCount: true,
|
||||
namespaceCount: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
|
||||
// Time range: past 6 hours
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const getNodeSeries = (data: AggregateModel): ChartSeries => {
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const nodeName: string =
|
||||
(attributes["k8s.node.name"] as string) || "Unknown Node";
|
||||
return { title: nodeName };
|
||||
};
|
||||
|
||||
// CPU utilization metric query
|
||||
const cpuQueryConfig: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_cpu",
|
||||
title: "Node CPU Utilization",
|
||||
description: "CPU utilization across cluster nodes",
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.cpu.utilization",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
// Memory utilization metric query
|
||||
const memoryQueryConfig: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_memory",
|
||||
title: "Node Memory Usage",
|
||||
description: "Memory usage across cluster nodes",
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.memory.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
// Pod CPU usage
|
||||
const podCpuQueryConfig: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_cpu",
|
||||
title: "Pod CPU Usage (Top Consumers)",
|
||||
description: "CPU usage by pod across the cluster",
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.cpu.utilization",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: (data: AggregateModel): ChartSeries => {
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const podName: string =
|
||||
(attributes["k8s.pod.name"] as string) || "Unknown Pod";
|
||||
const namespace: string =
|
||||
(attributes["k8s.namespace.name"] as string) || "";
|
||||
return { title: namespace ? `${namespace}/${podName}` : podName };
|
||||
},
|
||||
};
|
||||
|
||||
// Pod Memory usage
|
||||
const podMemoryQueryConfig: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_memory",
|
||||
title: "Pod Memory Usage (Top Consumers)",
|
||||
description: "Memory usage by pod across the cluster",
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.memory.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: (data: AggregateModel): ChartSeries => {
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const podName: string =
|
||||
(attributes["k8s.pod.name"] as string) || "Unknown Pod";
|
||||
const namespace: string =
|
||||
(attributes["k8s.namespace.name"] as string) || "";
|
||||
return { title: namespace ? `${namespace}/${podName}` : podName };
|
||||
},
|
||||
};
|
||||
|
||||
const [metricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [
|
||||
cpuQueryConfig,
|
||||
memoryQueryConfig,
|
||||
podCpuQueryConfig,
|
||||
podMemoryQueryConfig,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
const statusColor: string =
|
||||
cluster.otelCollectorStatus === "connected"
|
||||
? "text-green-600"
|
||||
: "text-red-600";
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-5">
|
||||
<InfoCard
|
||||
title="Nodes"
|
||||
value={
|
||||
<span className="text-2xl font-semibold">
|
||||
{cluster.nodeCount?.toString() || "0"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
title="Pods"
|
||||
value={
|
||||
<span className="text-2xl font-semibold">
|
||||
{cluster.podCount?.toString() || "0"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
title="Namespaces"
|
||||
value={
|
||||
<span className="text-2xl font-semibold">
|
||||
{cluster.namespaceCount?.toString() || "0"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<InfoCard
|
||||
title="Agent Status"
|
||||
value={
|
||||
<span className={`text-2xl font-semibold ${statusColor}`}>
|
||||
{cluster.otelCollectorStatus === "connected"
|
||||
? "Connected"
|
||||
: "Disconnected"}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Cluster Details */}
|
||||
<CardModelDetail<KubernetesCluster>
|
||||
name="Cluster Overview"
|
||||
cardProps={{
|
||||
title: "Cluster Details",
|
||||
description: "Basic information about this Kubernetes cluster.",
|
||||
}}
|
||||
isEditable={false}
|
||||
modelDetailProps={{
|
||||
showDetailsInNumberOfColumns: 2,
|
||||
modelType: KubernetesCluster,
|
||||
id: "kubernetes-cluster-overview",
|
||||
modelId: modelId,
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Cluster Name",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
title: "Cluster Identifier",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
provider: true,
|
||||
},
|
||||
title: "Provider",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
lastSeenAt: true,
|
||||
},
|
||||
title: "Last Seen",
|
||||
fieldType: FieldType.DateTime,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Resource Utilization Charts */}
|
||||
<Card
|
||||
title="Resource Utilization"
|
||||
description="CPU and memory usage trends across the cluster over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={metricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterOverview;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getKubernetesBreadcrumbs } from "../../../Utils/Breadcrumbs";
|
||||
import { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import SideMenu from "./SideMenu";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import ModelPage from "Common/UI/Components/Page/ModelPage";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import { Outlet, useParams } from "react-router-dom";
|
||||
|
||||
const KubernetesClusterViewLayout: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const { id } = useParams();
|
||||
const modelId: ObjectID = new ObjectID(id || "");
|
||||
const path: string = Navigation.getRoutePath(RouteUtil.getRoutes());
|
||||
return (
|
||||
<ModelPage
|
||||
title="Kubernetes Cluster"
|
||||
modelType={KubernetesCluster}
|
||||
modelId={modelId}
|
||||
modelNameField="name"
|
||||
breadcrumbLinks={getKubernetesBreadcrumbs(path)}
|
||||
sideMenu={<SideMenu modelId={modelId} />}
|
||||
>
|
||||
<Outlet />
|
||||
</ModelPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterViewLayout;
|
||||
@@ -0,0 +1,242 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
|
||||
const KubernetesClusterNodeDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(2);
|
||||
const nodeName: string = Navigation.getLastParam()?.toString() || "";
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const cpuQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_cpu",
|
||||
title: "CPU Utilization",
|
||||
description: `CPU utilization for node ${nodeName}`,
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.cpu.utilization",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.node.name": nodeName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const memoryQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_memory",
|
||||
title: "Memory Usage",
|
||||
description: `Memory usage for node ${nodeName}`,
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.memory.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.node.name": nodeName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const filesystemQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_filesystem",
|
||||
title: "Filesystem Usage",
|
||||
description: `Filesystem usage for node ${nodeName}`,
|
||||
legend: "Filesystem",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.filesystem.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.node.name": nodeName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const networkRxQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_network_rx",
|
||||
title: "Network Receive",
|
||||
description: `Network bytes received for node ${nodeName}`,
|
||||
legend: "Network RX",
|
||||
legendUnit: "bytes/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.network.io.receive",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.node.name": nodeName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const networkTxQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_network_tx",
|
||||
title: "Network Transmit",
|
||||
description: `Network bytes transmitted for node ${nodeName}`,
|
||||
legend: "Network TX",
|
||||
legendUnit: "bytes/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.network.io.transmit",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.node.name": nodeName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [
|
||||
cpuQuery,
|
||||
memoryQuery,
|
||||
filesystemQuery,
|
||||
networkRxQuery,
|
||||
networkTxQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Node Name" value={nodeName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`Node Metrics: ${nodeName}`}
|
||||
description="CPU, memory, filesystem, and network usage for this node over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={metricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [
|
||||
cpuQuery,
|
||||
memoryQuery,
|
||||
filesystemQuery,
|
||||
networkRxQuery,
|
||||
networkTxQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterNodeDetail;
|
||||
213
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx
Normal file
213
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Nodes.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
|
||||
const KubernetesClusterNodes: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const getNodeSeries = (data: AggregateModel): ChartSeries => {
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const nodeName: string =
|
||||
(attributes["k8s.node.name"] as string) || "Unknown Node";
|
||||
return { title: nodeName };
|
||||
};
|
||||
|
||||
const nodeCpuQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_cpu",
|
||||
title: "Node CPU Utilization",
|
||||
description: "CPU utilization by node",
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.cpu.utilization",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
const nodeMemoryQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_memory",
|
||||
title: "Node Memory Usage",
|
||||
description: "Memory usage by node",
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.memory.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
const nodeFilesystemQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_filesystem",
|
||||
title: "Node Filesystem Usage",
|
||||
description: "Filesystem usage by node",
|
||||
legend: "Filesystem",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.filesystem.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
const nodeNetworkRxQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "node_network_rx",
|
||||
title: "Node Network Receive",
|
||||
description: "Network bytes received by node",
|
||||
legend: "Network RX",
|
||||
legendUnit: "bytes/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.node.network.io.receive",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getNodeSeries,
|
||||
};
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [
|
||||
nodeCpuQuery,
|
||||
nodeMemoryQuery,
|
||||
nodeFilesystemQuery,
|
||||
nodeNetworkRxQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MetricView
|
||||
data={metricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [
|
||||
nodeCpuQuery,
|
||||
nodeMemoryQuery,
|
||||
nodeFilesystemQuery,
|
||||
nodeNetworkRxQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterNodes;
|
||||
218
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx
Normal file
218
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/PodDetail.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import InfoCard from "Common/UI/Components/InfoCard/InfoCard";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
|
||||
const KubernetesClusterPodDetail: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(2);
|
||||
const podName: string = Navigation.getLastParam()?.toString() || "";
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const getContainerSeries = (data: AggregateModel): ChartSeries => {
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const containerName: string =
|
||||
(attributes["k8s.container.name"] as string) || "Unknown Container";
|
||||
return { title: containerName };
|
||||
};
|
||||
|
||||
const cpuQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "container_cpu",
|
||||
title: "Container CPU Utilization",
|
||||
description: `CPU utilization for containers in pod ${podName}`,
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "container.cpu.utilization",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.pod.name": podName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getContainerSeries,
|
||||
};
|
||||
|
||||
const memoryQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "container_memory",
|
||||
title: "Container Memory Usage",
|
||||
description: `Memory usage for containers in pod ${podName}`,
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "container.memory.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.pod.name": podName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getContainerSeries,
|
||||
};
|
||||
|
||||
const podCpuQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_cpu",
|
||||
title: "Pod CPU Utilization",
|
||||
description: `CPU utilization for pod ${podName}`,
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.cpu.utilization",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.pod.name": podName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const podMemoryQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_memory",
|
||||
title: "Pod Memory Usage",
|
||||
description: `Memory usage for pod ${podName}`,
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.memory.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
"k8s.pod.name": podName,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [podCpuQuery, podMemoryQuery, cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
||||
<InfoCard title="Pod Name" value={podName || "Unknown"} />
|
||||
<InfoCard title="Cluster" value={clusterIdentifier} />
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={`Pod Metrics: ${podName}`}
|
||||
description="CPU, memory, and container-level resource usage for this pod over the last 6 hours."
|
||||
>
|
||||
<MetricView
|
||||
data={metricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [podCpuQuery, podMemoryQuery, cpuQuery, memoryQuery],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterPodDetail;
|
||||
215
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx
Normal file
215
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/Pods.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import MetricView from "../../../Components/Metrics/MetricView";
|
||||
import MetricViewData from "Common/Types/Metrics/MetricViewData";
|
||||
import MetricQueryConfigData from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
import AggregationType from "Common/Types/BaseDatabase/AggregationType";
|
||||
import OneUptimeDate from "Common/Types/Date";
|
||||
import InBetween from "Common/Types/BaseDatabase/InBetween";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import API from "Common/UI/Utils/API/API";
|
||||
import PageLoader from "Common/UI/Components/Loader/PageLoader";
|
||||
import ErrorMessage from "Common/UI/Components/ErrorMessage/ErrorMessage";
|
||||
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
|
||||
import AggregateModel from "Common/Types/BaseDatabase/AggregatedModel";
|
||||
import { ChartSeries } from "Common/Types/Metrics/MetricQueryConfigData";
|
||||
|
||||
const KubernetesClusterPods: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
const [cluster, setCluster] = useState<KubernetesCluster | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const fetchCluster: PromiseVoidFunction = async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const item: KubernetesCluster | null = await ModelAPI.getItem({
|
||||
modelType: KubernetesCluster,
|
||||
id: modelId,
|
||||
select: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
});
|
||||
setCluster(item);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCluster().catch((err: Error) => {
|
||||
setError(API.getFriendlyMessage(err));
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader isVisible={true} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorMessage message={error} />;
|
||||
}
|
||||
|
||||
if (!cluster) {
|
||||
return <ErrorMessage message="Cluster not found." />;
|
||||
}
|
||||
|
||||
const clusterIdentifier: string = cluster.clusterIdentifier || "";
|
||||
|
||||
const endDate: Date = OneUptimeDate.getCurrentDate();
|
||||
const startDate: Date = OneUptimeDate.addRemoveHours(endDate, -6);
|
||||
const startAndEndDate: InBetween<Date> = new InBetween(startDate, endDate);
|
||||
|
||||
const getPodSeries = (data: AggregateModel): ChartSeries => {
|
||||
const attributes: Record<string, unknown> =
|
||||
(data["attributes"] as Record<string, unknown>) || {};
|
||||
const podName: string =
|
||||
(attributes["k8s.pod.name"] as string) || "Unknown Pod";
|
||||
const namespace: string =
|
||||
(attributes["k8s.namespace.name"] as string) || "";
|
||||
return { title: namespace ? `${namespace}/${podName}` : podName };
|
||||
};
|
||||
|
||||
const podCpuQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_cpu",
|
||||
title: "Pod CPU Utilization",
|
||||
description: "CPU utilization by pod",
|
||||
legend: "CPU",
|
||||
legendUnit: "%",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.cpu.utilization",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getPodSeries,
|
||||
};
|
||||
|
||||
const podMemoryQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_memory",
|
||||
title: "Pod Memory Usage",
|
||||
description: "Memory usage by pod",
|
||||
legend: "Memory",
|
||||
legendUnit: "bytes",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.memory.usage",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getPodSeries,
|
||||
};
|
||||
|
||||
const podNetworkRxQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_network_rx",
|
||||
title: "Pod Network Receive",
|
||||
description: "Network bytes received by pod",
|
||||
legend: "Network RX",
|
||||
legendUnit: "bytes/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.network.io.receive",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getPodSeries,
|
||||
};
|
||||
|
||||
const podNetworkTxQuery: MetricQueryConfigData = {
|
||||
metricAliasData: {
|
||||
metricVariable: "pod_network_tx",
|
||||
title: "Pod Network Transmit",
|
||||
description: "Network bytes transmitted by pod",
|
||||
legend: "Network TX",
|
||||
legendUnit: "bytes/s",
|
||||
},
|
||||
metricQueryData: {
|
||||
filterData: {
|
||||
metricName: "k8s.pod.network.io.transmit",
|
||||
attributes: {
|
||||
"k8s.cluster.name": clusterIdentifier,
|
||||
},
|
||||
aggegationType: AggregationType.Avg,
|
||||
aggregateBy: {},
|
||||
},
|
||||
groupBy: {
|
||||
attributes: true,
|
||||
},
|
||||
},
|
||||
getSeries: getPodSeries,
|
||||
};
|
||||
|
||||
const [metricViewData, setMetricViewData] = useState<MetricViewData>({
|
||||
startAndEndDate: startAndEndDate,
|
||||
queryConfigs: [
|
||||
podCpuQuery,
|
||||
podMemoryQuery,
|
||||
podNetworkRxQuery,
|
||||
podNetworkTxQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<MetricView
|
||||
data={metricViewData}
|
||||
hideQueryElements={true}
|
||||
onChange={(data: MetricViewData) => {
|
||||
setMetricViewData({
|
||||
...data,
|
||||
queryConfigs: [
|
||||
podCpuQuery,
|
||||
podMemoryQuery,
|
||||
podNetworkRxQuery,
|
||||
podNetworkTxQuery,
|
||||
],
|
||||
formulaConfigs: [],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterPods;
|
||||
@@ -0,0 +1,64 @@
|
||||
import PageComponentProps from "../../PageComponentProps";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import Navigation from "Common/UI/Utils/Navigation";
|
||||
import KubernetesCluster from "Common/Models/DatabaseModels/KubernetesCluster";
|
||||
import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
|
||||
import FieldType from "Common/UI/Components/Types/FieldType";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const KubernetesClusterSettings: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CardModelDetail<KubernetesCluster>
|
||||
name="Cluster Settings"
|
||||
cardProps={{
|
||||
title: "Cluster Settings",
|
||||
description: "Manage settings for this Kubernetes cluster.",
|
||||
}}
|
||||
isEditable={true}
|
||||
editButtonText="Edit Settings"
|
||||
modelDetailProps={{
|
||||
modelType: KubernetesCluster,
|
||||
id: "kubernetes-cluster-settings",
|
||||
modelId: modelId,
|
||||
fields: [
|
||||
{
|
||||
field: {
|
||||
name: true,
|
||||
},
|
||||
title: "Name",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
description: true,
|
||||
},
|
||||
title: "Description",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
title: "Cluster Identifier",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
provider: true,
|
||||
},
|
||||
title: "Provider",
|
||||
fieldType: FieldType.Text,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterSettings;
|
||||
103
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx
Normal file
103
App/FeatureSet/Dashboard/src/Pages/Kubernetes/View/SideMenu.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import PageMap from "../../../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import IconProp from "Common/Types/Icon/IconProp";
|
||||
import ObjectID from "Common/Types/ObjectID";
|
||||
import SideMenu from "Common/UI/Components/SideMenu/SideMenu";
|
||||
import SideMenuItem from "Common/UI/Components/SideMenu/SideMenuItem";
|
||||
import SideMenuSection from "Common/UI/Components/SideMenu/SideMenuSection";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface ComponentProps {
|
||||
modelId: ObjectID;
|
||||
}
|
||||
|
||||
const KubernetesClusterSideMenu: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<SideMenu>
|
||||
<SideMenuSection title="Basic">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Overview",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Info}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Settings",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Settings}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Delete Cluster",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Trash}
|
||||
className="danger-on-hover"
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Resources">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Pods",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_PODS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Circle}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Nodes",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODES] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Server}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
|
||||
<SideMenuSection title="Observability">
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Events",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Logs}
|
||||
/>
|
||||
<SideMenuItem
|
||||
link={{
|
||||
title: "Control Plane",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE] as Route,
|
||||
{ modelId: props.modelId },
|
||||
),
|
||||
}}
|
||||
icon={IconProp.Settings}
|
||||
/>
|
||||
</SideMenuSection>
|
||||
</SideMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesClusterSideMenu;
|
||||
@@ -20,6 +20,7 @@ export { default as StatusPagesRoutes } from "./StatusPagesRoutes";
|
||||
export { default as DashboardRoutes } from "./DashboardRoutes";
|
||||
export { default as ServiceRoutes } from "./ServiceRoutes";
|
||||
export { default as CodeRepositoryRoutes } from "./CodeRepositoryRoutes";
|
||||
export { default as KubernetesRoutes } from "./KubernetesRoutes";
|
||||
export { default as AIAgentTasksRoutes } from "./AIAgentTasksRoutes";
|
||||
|
||||
// Settings
|
||||
|
||||
137
App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx
Normal file
137
App/FeatureSet/Dashboard/src/Routes/KubernetesRoutes.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import ComponentProps from "../Pages/PageComponentProps";
|
||||
import KubernetesLayout from "../Pages/Kubernetes/Layout";
|
||||
import KubernetesClusterViewLayout from "../Pages/Kubernetes/View/Layout";
|
||||
import PageMap from "../Utils/PageMap";
|
||||
import RouteMap, { RouteUtil, KubernetesRoutePath } from "../Utils/RouteMap";
|
||||
import Route from "Common/Types/API/Route";
|
||||
import React, { FunctionComponent, ReactElement } from "react";
|
||||
import { Route as PageRoute, Routes } from "react-router-dom";
|
||||
|
||||
// Pages
|
||||
import KubernetesClusters from "../Pages/Kubernetes/Clusters";
|
||||
import KubernetesClusterView from "../Pages/Kubernetes/View/Index";
|
||||
import KubernetesClusterViewPods from "../Pages/Kubernetes/View/Pods";
|
||||
import KubernetesClusterViewPodDetail from "../Pages/Kubernetes/View/PodDetail";
|
||||
import KubernetesClusterViewNodes from "../Pages/Kubernetes/View/Nodes";
|
||||
import KubernetesClusterViewNodeDetail from "../Pages/Kubernetes/View/NodeDetail";
|
||||
import KubernetesClusterViewEvents from "../Pages/Kubernetes/View/Events";
|
||||
import KubernetesClusterViewControlPlane from "../Pages/Kubernetes/View/ControlPlane";
|
||||
import KubernetesClusterViewDelete from "../Pages/Kubernetes/View/Delete";
|
||||
import KubernetesClusterViewSettings from "../Pages/Kubernetes/View/Settings";
|
||||
|
||||
const KubernetesRoutes: FunctionComponent<ComponentProps> = (
|
||||
props: ComponentProps,
|
||||
): ReactElement => {
|
||||
return (
|
||||
<Routes>
|
||||
<PageRoute path="/" element={<KubernetesLayout {...props} />}>
|
||||
<PageRoute
|
||||
path=""
|
||||
element={
|
||||
<KubernetesClusters
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTERS] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
|
||||
<PageRoute
|
||||
path={KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW] || ""}
|
||||
element={<KubernetesClusterViewLayout {...props} />}
|
||||
>
|
||||
<PageRoute
|
||||
index
|
||||
element={
|
||||
<KubernetesClusterView
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_CLUSTER_VIEW_PODS)}
|
||||
element={
|
||||
<KubernetesClusterViewPods
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_PODS] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL)}
|
||||
element={
|
||||
<KubernetesClusterViewPodDetail
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_CLUSTER_VIEW_NODES)}
|
||||
element={
|
||||
<KubernetesClusterViewNodes
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODES] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL)}
|
||||
element={
|
||||
<KubernetesClusterViewNodeDetail
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS)}
|
||||
element={
|
||||
<KubernetesClusterViewEvents
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE)}
|
||||
element={
|
||||
<KubernetesClusterViewControlPlane
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_CLUSTER_VIEW_DELETE)}
|
||||
element={
|
||||
<KubernetesClusterViewDelete
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS)}
|
||||
element={
|
||||
<KubernetesClusterViewSettings
|
||||
{...props}
|
||||
pageRoute={RouteMap[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageRoute>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default KubernetesRoutes;
|
||||
@@ -0,0 +1,64 @@
|
||||
import PageMap from "../PageMap";
|
||||
import { BuildBreadcrumbLinksByTitles } from "./Helper";
|
||||
import Dictionary from "Common/Types/Dictionary";
|
||||
import Link from "Common/Types/Link";
|
||||
|
||||
export function getKubernetesBreadcrumbs(
|
||||
path: string,
|
||||
): Array<Link> | undefined {
|
||||
const breadcrumpLinksMap: Dictionary<Link[]> = {
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTERS, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"Clusters",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_PODS, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
"Pods",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL,
|
||||
["Project", "Kubernetes", "View Cluster", "Pods", "Pod Detail"],
|
||||
),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_NODES, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
"Nodes",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL,
|
||||
["Project", "Kubernetes", "View Cluster", "Nodes", "Node Detail"],
|
||||
),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
"Events",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(
|
||||
PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE,
|
||||
["Project", "Kubernetes", "View Cluster", "Control Plane"],
|
||||
),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_DELETE, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
"Delete Cluster",
|
||||
]),
|
||||
...BuildBreadcrumbLinksByTitles(PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS, [
|
||||
"Project",
|
||||
"Kubernetes",
|
||||
"View Cluster",
|
||||
"Settings",
|
||||
]),
|
||||
};
|
||||
return breadcrumpLinksMap[path];
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export * from "./SettingsBreadcrumbs";
|
||||
export * from "./MonitorGroupBreadcrumbs";
|
||||
export * from "./ServiceBreadcrumbs";
|
||||
export * from "./CodeRepositoryBreadcrumbs";
|
||||
export * from "./KubernetesBreadcrumbs";
|
||||
export * from "./DashboardBreadCrumbs";
|
||||
export * from "./AIAgentTasksBreadcrumbs";
|
||||
export * from "./ExceptionsBreadcrumbs";
|
||||
|
||||
@@ -214,6 +214,19 @@ enum PageMap {
|
||||
SERVICE_VIEW_CODE_REPOSITORIES = "SERVICE_VIEW_CODE_REPOSITORIES",
|
||||
SERVICE_DEPENDENCY_GRAPH = "SERVICE_DEPENDENCY_GRAPH",
|
||||
|
||||
// Kubernetes (standalone product)
|
||||
KUBERNETES_ROOT = "KUBERNETES_ROOT",
|
||||
KUBERNETES_CLUSTERS = "KUBERNETES_CLUSTERS",
|
||||
KUBERNETES_CLUSTER_VIEW = "KUBERNETES_CLUSTER_VIEW",
|
||||
KUBERNETES_CLUSTER_VIEW_PODS = "KUBERNETES_CLUSTER_VIEW_PODS",
|
||||
KUBERNETES_CLUSTER_VIEW_POD_DETAIL = "KUBERNETES_CLUSTER_VIEW_POD_DETAIL",
|
||||
KUBERNETES_CLUSTER_VIEW_NODES = "KUBERNETES_CLUSTER_VIEW_NODES",
|
||||
KUBERNETES_CLUSTER_VIEW_NODE_DETAIL = "KUBERNETES_CLUSTER_VIEW_NODE_DETAIL",
|
||||
KUBERNETES_CLUSTER_VIEW_EVENTS = "KUBERNETES_CLUSTER_VIEW_EVENTS",
|
||||
KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE = "KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE",
|
||||
KUBERNETES_CLUSTER_VIEW_DELETE = "KUBERNETES_CLUSTER_VIEW_DELETE",
|
||||
KUBERNETES_CLUSTER_VIEW_SETTINGS = "KUBERNETES_CLUSTER_VIEW_SETTINGS",
|
||||
|
||||
// Code Repository
|
||||
CODE_REPOSITORY_ROOT = "CODE_REPOSITORY_ROOT",
|
||||
CODE_REPOSITORY = "CODE_REPOSITORY",
|
||||
|
||||
@@ -59,6 +59,18 @@ export const CodeRepositoryRoutePath: Dictionary<string> = {
|
||||
[PageMap.CODE_REPOSITORY_VIEW_SERVICES]: `${RouteParams.ModelID}/services`,
|
||||
};
|
||||
|
||||
export const KubernetesRoutePath: Dictionary<string> = {
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW]: `${RouteParams.ModelID}`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_PODS]: `${RouteParams.ModelID}/pods`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]: `${RouteParams.ModelID}/pods/${RouteParams.SubModelID}`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_NODES]: `${RouteParams.ModelID}/nodes`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]: `${RouteParams.ModelID}/nodes/${RouteParams.SubModelID}`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: `${RouteParams.ModelID}/events`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]: `${RouteParams.ModelID}/control-plane`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]: `${RouteParams.ModelID}/delete`,
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS]: `${RouteParams.ModelID}/settings`,
|
||||
};
|
||||
|
||||
export const WorkflowRoutePath: Dictionary<string> = {
|
||||
[PageMap.WORKFLOWS_LOGS]: "logs",
|
||||
[PageMap.WORKFLOWS_VARIABLES]: "variables",
|
||||
@@ -1465,6 +1477,70 @@ const RouteMap: Dictionary<Route> = {
|
||||
}`,
|
||||
),
|
||||
|
||||
// Kubernetes
|
||||
|
||||
[PageMap.KUBERNETES_ROOT]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/*`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTERS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_PODS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_PODS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_POD_DETAIL]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_NODES]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_NODES]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_NODE_DETAIL]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_EVENTS]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_CONTROL_PLANE]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_DELETE]
|
||||
}`,
|
||||
),
|
||||
|
||||
[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS]: new Route(
|
||||
`/dashboard/${RouteParams.ProjectID}/kubernetes/${
|
||||
KubernetesRoutePath[PageMap.KUBERNETES_CLUSTER_VIEW_SETTINGS]
|
||||
}`,
|
||||
),
|
||||
|
||||
// Dashboards
|
||||
|
||||
[PageMap.DASHBOARDS_ROOT]: new Route(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AcmeCertificate from "./AcmeCertificate";
|
||||
import AcmeChallenge from "./AcmeChallenge";
|
||||
import KubernetesCluster from "./KubernetesCluster";
|
||||
// API Keys
|
||||
import ApiKey from "./ApiKey";
|
||||
import ApiKeyPermission from "./ApiKeyPermission";
|
||||
@@ -499,6 +500,8 @@ const AllModelTypes: Array<{
|
||||
ProjectSCIM,
|
||||
ProjectSCIMLog,
|
||||
StatusPageSCIMLog,
|
||||
|
||||
KubernetesCluster,
|
||||
];
|
||||
|
||||
const modelTypeMap: { [key: string]: { new (): BaseModel } } = {};
|
||||
|
||||
640
Common/Models/DatabaseModels/KubernetesCluster.ts
Normal file
640
Common/Models/DatabaseModels/KubernetesCluster.ts
Normal file
@@ -0,0 +1,640 @@
|
||||
import Label from "./Label";
|
||||
import Project from "./Project";
|
||||
import User from "./User";
|
||||
import BaseModel from "./DatabaseBaseModel/DatabaseBaseModel";
|
||||
import Route from "../../Types/API/Route";
|
||||
import ColumnAccessControl from "../../Types/Database/AccessControl/ColumnAccessControl";
|
||||
import TableAccessControl from "../../Types/Database/AccessControl/TableAccessControl";
|
||||
import AccessControlColumn from "../../Types/Database/AccessControlColumn";
|
||||
import ColumnLength from "../../Types/Database/ColumnLength";
|
||||
import ColumnType from "../../Types/Database/ColumnType";
|
||||
import CrudApiEndpoint from "../../Types/Database/CrudApiEndpoint";
|
||||
import EnableDocumentation from "../../Types/Database/EnableDocumentation";
|
||||
import EnableWorkflow from "../../Types/Database/EnableWorkflow";
|
||||
import SlugifyColumn from "../../Types/Database/SlugifyColumn";
|
||||
import TableColumn from "../../Types/Database/TableColumn";
|
||||
import TableColumnType from "../../Types/Database/TableColumnType";
|
||||
import TableMetadata from "../../Types/Database/TableMetadata";
|
||||
import TenantColumn from "../../Types/Database/TenantColumn";
|
||||
import UniqueColumnBy from "../../Types/Database/UniqueColumnBy";
|
||||
import IconProp from "../../Types/Icon/IconProp";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import Permission from "../../Types/Permission";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
} from "typeorm";
|
||||
|
||||
@AccessControlColumn("labels")
|
||||
@EnableDocumentation()
|
||||
@TenantColumn("projectId")
|
||||
@TableAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
delete: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.DeleteKubernetesCluster,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@EnableWorkflow({
|
||||
create: true,
|
||||
delete: true,
|
||||
update: true,
|
||||
read: true,
|
||||
})
|
||||
@CrudApiEndpoint(new Route("/kubernetes-cluster"))
|
||||
@SlugifyColumn("name", "slug")
|
||||
@TableMetadata({
|
||||
tableName: "KubernetesCluster",
|
||||
singularName: "Kubernetes Cluster",
|
||||
pluralName: "Kubernetes Clusters",
|
||||
icon: IconProp.Cube,
|
||||
tableDescription:
|
||||
"Kubernetes Clusters that are being monitored in this project. Each cluster is auto-discovered when the OneUptime kubernetes-agent sends metrics, or can be manually registered.",
|
||||
})
|
||||
@Entity({
|
||||
name: "KubernetesCluster",
|
||||
})
|
||||
export default class KubernetesCluster extends BaseModel {
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "projectId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: Project,
|
||||
title: "Project",
|
||||
description: "Relation to Project Resource in which this object belongs",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return Project;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "CASCADE",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "projectId" })
|
||||
public project?: Project = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description: "ID of your OneUptime Project in which this object belongs",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: false,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public projectId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Name",
|
||||
description: "Friendly name for this Kubernetes cluster",
|
||||
example: "production-us-east",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
@UniqueColumnBy("projectId")
|
||||
public name?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
required: true,
|
||||
unique: true,
|
||||
type: TableColumnType.Slug,
|
||||
computed: true,
|
||||
title: "Slug",
|
||||
description: "Friendly globally unique name for your object",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.Slug,
|
||||
length: ColumnLength.Slug,
|
||||
})
|
||||
public slug?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.LongText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Description",
|
||||
description: "Friendly description for this Kubernetes cluster",
|
||||
example: "Production cluster running in US East region on EKS",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.LongText,
|
||||
length: ColumnLength.LongText,
|
||||
})
|
||||
public description?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@Index()
|
||||
@TableColumn({
|
||||
required: true,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Cluster Identifier",
|
||||
description:
|
||||
"Unique identifier for this cluster, sourced from the k8s.cluster.name OTel resource attribute",
|
||||
example: "production-us-east-1",
|
||||
})
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
})
|
||||
@UniqueColumnBy("projectId")
|
||||
public clusterIdentifier?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Provider",
|
||||
description:
|
||||
"Cloud provider or platform running this cluster (EKS, GKE, AKS, self-managed, unknown)",
|
||||
example: "EKS",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
default: "unknown",
|
||||
})
|
||||
public provider?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.ShortText,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "OTel Collector Status",
|
||||
description:
|
||||
"Connection status of the OTel Collector agent (connected or disconnected)",
|
||||
example: "connected",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.ShortText,
|
||||
length: ColumnLength.ShortText,
|
||||
default: "disconnected",
|
||||
})
|
||||
public otelCollectorStatus?: string = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.Date,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Last Seen At",
|
||||
description: "When metrics were last received from this cluster",
|
||||
})
|
||||
@Column({
|
||||
nullable: true,
|
||||
type: ColumnType.Date,
|
||||
})
|
||||
public lastSeenAt?: Date = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Number,
|
||||
title: "Node Count",
|
||||
description: "Cached count of nodes in this cluster",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Number,
|
||||
nullable: true,
|
||||
default: 0,
|
||||
})
|
||||
public nodeCount?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Number,
|
||||
title: "Pod Count",
|
||||
description: "Cached count of pods in this cluster",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Number,
|
||||
nullable: true,
|
||||
default: 0,
|
||||
})
|
||||
public podCount?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.Number,
|
||||
title: "Namespace Count",
|
||||
description: "Cached count of namespaces in this cluster",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.Number,
|
||||
nullable: true,
|
||||
default: 0,
|
||||
})
|
||||
public namespaceCount?: number = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "createdByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
modelType: User,
|
||||
title: "Created by User",
|
||||
description:
|
||||
"Relation to User who created this object (if this object was created by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "createdByUserId" })
|
||||
public createdByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created by User ID",
|
||||
description:
|
||||
"User ID who created this object (if this object was created by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public createdByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
manyToOneRelationColumn: "deletedByUserId",
|
||||
type: TableColumnType.Entity,
|
||||
title: "Deleted by User",
|
||||
modelType: User,
|
||||
description:
|
||||
"Relation to User who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
return User;
|
||||
},
|
||||
{
|
||||
cascade: false,
|
||||
eager: false,
|
||||
nullable: true,
|
||||
onDelete: "SET NULL",
|
||||
orphanedRowAction: "nullify",
|
||||
},
|
||||
)
|
||||
@JoinColumn({ name: "deletedByUserId" })
|
||||
public deletedByUser?: User = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [],
|
||||
})
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Deleted by User ID",
|
||||
description:
|
||||
"User ID who deleted this object (if this object was deleted by a User)",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
nullable: true,
|
||||
transformer: ObjectID.getDatabaseTransformer(),
|
||||
})
|
||||
public deletedByUserId?: ObjectID = undefined;
|
||||
|
||||
@ColumnAccessControl({
|
||||
create: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.CreateKubernetesCluster,
|
||||
],
|
||||
read: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.ReadKubernetesCluster,
|
||||
Permission.ReadAllProjectResources,
|
||||
],
|
||||
update: [
|
||||
Permission.ProjectOwner,
|
||||
Permission.ProjectAdmin,
|
||||
Permission.ProjectMember,
|
||||
Permission.EditKubernetesCluster,
|
||||
],
|
||||
})
|
||||
@TableColumn({
|
||||
required: false,
|
||||
type: TableColumnType.EntityArray,
|
||||
modelType: Label,
|
||||
title: "Labels",
|
||||
description:
|
||||
"Relation to Labels Array where this object is categorized in.",
|
||||
})
|
||||
@ManyToMany(
|
||||
() => {
|
||||
return Label;
|
||||
},
|
||||
{ eager: false },
|
||||
)
|
||||
@JoinTable({
|
||||
name: "KubernetesClusterLabel",
|
||||
inverseJoinColumn: {
|
||||
name: "labelId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
joinColumn: {
|
||||
name: "kubernetesClusterId",
|
||||
referencedColumnName: "_id",
|
||||
},
|
||||
})
|
||||
public labels?: Array<Label> = undefined;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1774000000000 implements MigrationInterface {
|
||||
public name = "MigrationName1774000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "KubernetesCluster" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(100) NOT NULL, "slug" character varying(100) NOT NULL, "description" character varying(500), "clusterIdentifier" character varying(100) NOT NULL, "provider" character varying(100) DEFAULT 'unknown', "otelCollectorStatus" character varying(100) DEFAULT 'disconnected', "lastSeenAt" TIMESTAMP WITH TIME ZONE, "nodeCount" integer DEFAULT '0', "podCount" integer DEFAULT '0', "namespaceCount" integer DEFAULT '0', "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_kubernetes_cluster_id" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_kubernetes_cluster_projectId" ON "KubernetesCluster" ("projectId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_kubernetes_cluster_clusterIdentifier" ON "KubernetesCluster" ("clusterIdentifier")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_kubernetes_cluster_slug" ON "KubernetesCluster" ("slug")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" ADD CONSTRAINT "FK_kubernetes_cluster_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
// Label join table
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "KubernetesClusterLabel" ("kubernetesClusterId" uuid NOT NULL, "labelId" uuid NOT NULL, CONSTRAINT "PK_kubernetes_cluster_label" PRIMARY KEY ("kubernetesClusterId", "labelId"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_kubernetes_cluster_label_clusterId" ON "KubernetesClusterLabel" ("kubernetesClusterId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_kubernetes_cluster_label_labelId" ON "KubernetesClusterLabel" ("labelId")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_clusterId" FOREIGN KEY ("kubernetesClusterId") REFERENCES "KubernetesCluster"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" ADD CONSTRAINT "FK_kubernetes_cluster_label_labelId" FOREIGN KEY ("labelId") REFERENCES "Label"("_id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_labelId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesClusterLabel" DROP CONSTRAINT "FK_kubernetes_cluster_label_clusterId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_label_labelId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_label_clusterId"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "KubernetesClusterLabel"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_deletedByUserId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_createdByUserId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "KubernetesCluster" DROP CONSTRAINT "FK_kubernetes_cluster_projectId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_slug"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_clusterIdentifier"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_kubernetes_cluster_projectId"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "KubernetesCluster"`);
|
||||
}
|
||||
}
|
||||
@@ -266,6 +266,7 @@ import { AddLogSavedView1772355000000 } from "./1772355000000-AddLogSavedView";
|
||||
import { MigrationName1773344537755 } from "./1773344537755-MigrationName";
|
||||
import { MigrationName1773402621107 } from "./1773402621107-MigrationName";
|
||||
import { MigrationName1773676206197 } from "./1773676206197-MigrationName";
|
||||
import { MigrationName1774000000000 } from "./1774000000000-MigrationName";
|
||||
|
||||
export default [
|
||||
InitialMigration,
|
||||
@@ -536,4 +537,5 @@ export default [
|
||||
MigrationName1773344537755,
|
||||
MigrationName1773402621107,
|
||||
MigrationName1773676206197,
|
||||
MigrationName1774000000000,
|
||||
];
|
||||
|
||||
109
Common/Server/Services/KubernetesClusterService.ts
Normal file
109
Common/Server/Services/KubernetesClusterService.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import DatabaseService from "./DatabaseService";
|
||||
import Model from "../../Models/DatabaseModels/KubernetesCluster";
|
||||
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
||||
import ObjectID from "../../Types/ObjectID";
|
||||
import QueryHelper from "../Types/Database/QueryHelper";
|
||||
import OneUptimeDate from "../../Types/Date";
|
||||
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
||||
|
||||
export class Service extends DatabaseService<Model> {
|
||||
public constructor() {
|
||||
super(Model);
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async findOrCreateByClusterIdentifier(data: {
|
||||
projectId: ObjectID;
|
||||
clusterIdentifier: string;
|
||||
}): Promise<Model> {
|
||||
// Try to find existing cluster
|
||||
const existingCluster: Model | null = await this.findOneBy({
|
||||
query: {
|
||||
projectId: data.projectId,
|
||||
clusterIdentifier: data.clusterIdentifier,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
projectId: true,
|
||||
clusterIdentifier: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingCluster) {
|
||||
return existingCluster;
|
||||
}
|
||||
|
||||
// Create new cluster
|
||||
const newCluster: Model = new Model();
|
||||
newCluster.projectId = data.projectId;
|
||||
newCluster.name = data.clusterIdentifier;
|
||||
newCluster.clusterIdentifier = data.clusterIdentifier;
|
||||
newCluster.otelCollectorStatus = "connected";
|
||||
newCluster.lastSeenAt = OneUptimeDate.getCurrentDate();
|
||||
|
||||
const createdCluster: Model = await this.create({
|
||||
data: newCluster,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
return createdCluster;
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async updateLastSeen(clusterId: ObjectID): Promise<void> {
|
||||
await this.updateOneById({
|
||||
id: clusterId,
|
||||
data: {
|
||||
lastSeenAt: OneUptimeDate.getCurrentDate(),
|
||||
otelCollectorStatus: "connected",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@CaptureSpan()
|
||||
public async markDisconnectedClusters(): Promise<void> {
|
||||
const fiveMinutesAgo: Date = OneUptimeDate.addRemoveMinutes(
|
||||
OneUptimeDate.getCurrentDate(),
|
||||
-5,
|
||||
);
|
||||
|
||||
const connectedClusters: Array<Model> = await this.findBy({
|
||||
query: {
|
||||
otelCollectorStatus: "connected",
|
||||
lastSeenAt: QueryHelper.lessThan(fiveMinutesAgo),
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
},
|
||||
limit: LIMIT_MAX,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const cluster of connectedClusters) {
|
||||
if (cluster._id) {
|
||||
await this.updateOneById({
|
||||
id: new ObjectID(cluster._id.toString()),
|
||||
data: {
|
||||
otelCollectorStatus: "disconnected",
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new Service();
|
||||
@@ -705,6 +705,11 @@ enum Permission {
|
||||
DeleteAlertSeverity = "DeleteAlertSeverity",
|
||||
ReadAlertSeverity = "ReadAlertSeverity",
|
||||
|
||||
CreateKubernetesCluster = "CreateKubernetesCluster",
|
||||
DeleteKubernetesCluster = "DeleteKubernetesCluster",
|
||||
EditKubernetesCluster = "EditKubernetesCluster",
|
||||
ReadKubernetesCluster = "ReadKubernetesCluster",
|
||||
|
||||
CreateService = "CreateService",
|
||||
DeleteService = "DeleteService",
|
||||
EditService = "EditService",
|
||||
@@ -4324,6 +4329,43 @@ export class PermissionHelper {
|
||||
group: PermissionGroup.AIAgent,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CreateKubernetesCluster,
|
||||
title: "Create Kubernetes Cluster",
|
||||
description:
|
||||
"This permission can create Kubernetes Cluster in this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: true,
|
||||
group: PermissionGroup.Telemetry,
|
||||
},
|
||||
{
|
||||
permission: Permission.DeleteKubernetesCluster,
|
||||
title: "Delete Kubernetes Cluster",
|
||||
description:
|
||||
"This permission can delete Kubernetes Cluster of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
group: PermissionGroup.Telemetry,
|
||||
},
|
||||
{
|
||||
permission: Permission.EditKubernetesCluster,
|
||||
title: "Edit Kubernetes Cluster",
|
||||
description:
|
||||
"This permission can edit Kubernetes Cluster of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
group: PermissionGroup.Telemetry,
|
||||
},
|
||||
{
|
||||
permission: Permission.ReadKubernetesCluster,
|
||||
title: "Read Kubernetes Cluster",
|
||||
description:
|
||||
"This permission can read Kubernetes Cluster of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
group: PermissionGroup.Telemetry,
|
||||
},
|
||||
|
||||
{
|
||||
permission: Permission.CreateService,
|
||||
title: "Create Service",
|
||||
|
||||
18
HelmChart/Public/kubernetes-agent/.helmignore
Normal file
18
HelmChart/Public/kubernetes-agent/.helmignore
Normal file
@@ -0,0 +1,18 @@
|
||||
# Patterns to ignore when building packages.
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
15
HelmChart/Public/kubernetes-agent/Chart.yaml
Normal file
15
HelmChart/Public/kubernetes-agent/Chart.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v2
|
||||
name: kubernetes-agent
|
||||
description: OneUptime Kubernetes Monitoring Agent — collects cluster metrics, events, and logs via OpenTelemetry and sends them to your OneUptime instance.
|
||||
icon: https://raw.githubusercontent.com/OneUptime/oneuptime/master/Home/Static/img/OneUptimePNG/1.png
|
||||
|
||||
type: application
|
||||
|
||||
version: 0.1.0
|
||||
|
||||
appVersion: "1.0.0"
|
||||
|
||||
annotations:
|
||||
artifacthub.io/license: MIT
|
||||
artifacthub.io/category: monitoring-logging
|
||||
artifacthub.io/prerelease: "false"
|
||||
24
HelmChart/Public/kubernetes-agent/templates/NOTES.txt
Normal file
24
HelmChart/Public/kubernetes-agent/templates/NOTES.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
OneUptime Kubernetes Agent has been installed.
|
||||
|
||||
Cluster Name: {{ .Values.clusterName }}
|
||||
OneUptime URL: {{ .Values.oneuptime.url }}
|
||||
|
||||
The agent is now collecting:
|
||||
- Node, pod, and container resource metrics (kubeletstats)
|
||||
- Cluster-level metrics: deployments, replicas, pod phases (k8s_cluster)
|
||||
- Kubernetes events (k8sobjects)
|
||||
{{- if .Values.controlPlane.enabled }}
|
||||
- Control plane metrics: etcd, API server, scheduler, controller manager (prometheus)
|
||||
{{- end }}
|
||||
{{- if .Values.logs.enabled }}
|
||||
- Pod logs from /var/log/pods (filelog DaemonSet)
|
||||
{{- end }}
|
||||
|
||||
To verify the agent is running:
|
||||
kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "kubernetes-agent.name" . }}
|
||||
|
||||
To check collector logs:
|
||||
kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "kubernetes-agent.name" . }} -c otel-collector
|
||||
|
||||
Your cluster should appear in OneUptime within a few minutes at:
|
||||
{{ .Values.oneuptime.url }}/dashboard/<project-id>/kubernetes
|
||||
59
HelmChart/Public/kubernetes-agent/templates/_helpers.tpl
Normal file
59
HelmChart/Public/kubernetes-agent/templates/_helpers.tpl
Normal file
@@ -0,0 +1,59 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "kubernetes-agent.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
*/}}
|
||||
{{- define "kubernetes-agent.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "kubernetes-agent.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "kubernetes-agent.labels" -}}
|
||||
helm.sh/chart: {{ include "kubernetes-agent.chart" . }}
|
||||
{{ include "kubernetes-agent.selectorLabels" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: oneuptime
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "kubernetes-agent.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "kubernetes-agent.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Service account name
|
||||
*/}}
|
||||
{{- define "kubernetes-agent.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.name }}
|
||||
{{- .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- include "kubernetes-agent.fullname" . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,134 @@
|
||||
{{- if .Values.logs.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}-daemonset
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
data:
|
||||
otel-collector-config.yaml: |
|
||||
receivers:
|
||||
# Collect pod logs from /var/log/pods
|
||||
filelog:
|
||||
include:
|
||||
- /var/log/pods/*/*/*.log
|
||||
exclude:
|
||||
# Exclude the agent's own logs to avoid feedback loop
|
||||
- /var/log/pods/{{ "{{ .Release.Namespace }}" }}_{{ "{{ include \"kubernetes-agent.fullname\" . }}" }}*/**/*.log
|
||||
start_at: end
|
||||
include_file_path: true
|
||||
include_file_name: false
|
||||
operators:
|
||||
# Parse CRI log format
|
||||
- type: router
|
||||
id: get-format
|
||||
routes:
|
||||
- output: parser-docker
|
||||
expr: 'body matches "^\\{"'
|
||||
- output: parser-cri
|
||||
expr: 'body matches "^[^ Z]+ "'
|
||||
- output: parser-containerd
|
||||
expr: 'body matches "^[^ Z]+Z"'
|
||||
# Docker JSON log format
|
||||
- type: json_parser
|
||||
id: parser-docker
|
||||
output: extract-metadata-from-filepath
|
||||
timestamp:
|
||||
parse_from: attributes.time
|
||||
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
||||
# CRI log format
|
||||
- type: regex_parser
|
||||
id: parser-cri
|
||||
regex: '^(?P<time>[^ Z]+) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
|
||||
output: extract-metadata-from-filepath
|
||||
timestamp:
|
||||
parse_from: attributes.time
|
||||
layout: '%Y-%m-%dT%H:%M:%S.%L%j'
|
||||
# Containerd log format
|
||||
- type: regex_parser
|
||||
id: parser-containerd
|
||||
regex: '^(?P<time>[^ ^Z]+Z) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
|
||||
output: extract-metadata-from-filepath
|
||||
timestamp:
|
||||
parse_from: attributes.time
|
||||
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
||||
# Extract k8s metadata from file path
|
||||
- type: regex_parser
|
||||
id: extract-metadata-from-filepath
|
||||
regex: '^.*\/(?P<namespace>[^_]+)_(?P<pod_name>[^_]+)_(?P<uid>[a-f0-9\-]+)\/(?P<container_name>[^\._]+)\/(?P<restart_count>\d+)\.log$'
|
||||
parse_from: attributes["log.file.path"]
|
||||
- type: move
|
||||
from: attributes.log
|
||||
to: body
|
||||
- type: move
|
||||
from: attributes.stream
|
||||
to: attributes["log.iostream"]
|
||||
- type: move
|
||||
from: attributes.namespace
|
||||
to: resource["k8s.namespace.name"]
|
||||
- type: move
|
||||
from: attributes.pod_name
|
||||
to: resource["k8s.pod.name"]
|
||||
- type: move
|
||||
from: attributes.container_name
|
||||
to: resource["k8s.container.name"]
|
||||
- type: move
|
||||
from: attributes.uid
|
||||
to: resource["k8s.pod.uid"]
|
||||
|
||||
processors:
|
||||
# Enrich with K8s metadata
|
||||
k8sattributes:
|
||||
auth_type: serviceAccount
|
||||
extract:
|
||||
metadata:
|
||||
- k8s.pod.name
|
||||
- k8s.pod.uid
|
||||
- k8s.namespace.name
|
||||
- k8s.node.name
|
||||
- k8s.deployment.name
|
||||
- k8s.replicaset.name
|
||||
- k8s.statefulset.name
|
||||
- k8s.daemonset.name
|
||||
- k8s.container.name
|
||||
pod_association:
|
||||
- sources:
|
||||
- from: resource_attribute
|
||||
name: k8s.pod.uid
|
||||
|
||||
# Stamp with cluster name
|
||||
resource:
|
||||
attributes:
|
||||
- key: k8s.cluster.name
|
||||
value: {{ .Values.clusterName | quote }}
|
||||
action: upsert
|
||||
|
||||
batch:
|
||||
send_batch_size: 1024
|
||||
timeout: 10s
|
||||
|
||||
memory_limiter:
|
||||
check_interval: 5s
|
||||
limit_mib: 200
|
||||
spike_limit_mib: 50
|
||||
|
||||
exporters:
|
||||
otlphttp:
|
||||
endpoint: "{{ .Values.oneuptime.url }}"
|
||||
headers:
|
||||
x-oneuptime-token: "${env:ONEUPTIME_API_KEY}"
|
||||
|
||||
service:
|
||||
pipelines:
|
||||
logs:
|
||||
receivers:
|
||||
- filelog
|
||||
processors:
|
||||
- memory_limiter
|
||||
- k8sattributes
|
||||
- resource
|
||||
- batch
|
||||
exporters:
|
||||
- otlphttp
|
||||
{{- end }}
|
||||
@@ -0,0 +1,176 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}-deployment
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
data:
|
||||
otel-collector-config.yaml: |
|
||||
receivers:
|
||||
# Collect node, pod, and container resource metrics from kubelet
|
||||
kubeletstats:
|
||||
collection_interval: {{ .Values.collectionInterval }}
|
||||
auth_type: serviceAccount
|
||||
endpoint: "https://${env:NODE_NAME}:10250"
|
||||
insecure_skip_verify: true
|
||||
metric_groups:
|
||||
- node
|
||||
- pod
|
||||
- container
|
||||
extra_metadata_labels:
|
||||
- container.id
|
||||
k8s_api_config:
|
||||
auth_type: serviceAccount
|
||||
|
||||
# Collect cluster-level metrics from the Kubernetes API
|
||||
k8s_cluster:
|
||||
collection_interval: {{ .Values.collectionInterval }}
|
||||
node_conditions_to_report:
|
||||
- Ready
|
||||
- MemoryPressure
|
||||
- DiskPressure
|
||||
- PIDPressure
|
||||
- NetworkUnavailable
|
||||
allocatable_types_to_report:
|
||||
- cpu
|
||||
- memory
|
||||
- storage
|
||||
|
||||
# Watch Kubernetes events and ingest as logs
|
||||
k8sobjects:
|
||||
objects:
|
||||
- name: events
|
||||
mode: watch
|
||||
group: events.k8s.io
|
||||
|
||||
{{- if .Values.controlPlane.enabled }}
|
||||
# Scrape control plane metrics via Prometheus endpoints
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
- job_name: etcd
|
||||
scheme: https
|
||||
tls_config:
|
||||
insecure_skip_verify: {{ .Values.controlPlane.etcd.insecureSkipVerify }}
|
||||
static_configs:
|
||||
{{- range .Values.controlPlane.etcd.endpoints }}
|
||||
- targets:
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
- job_name: kube-apiserver
|
||||
scheme: https
|
||||
tls_config:
|
||||
insecure_skip_verify: {{ .Values.controlPlane.apiServer.insecureSkipVerify }}
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
static_configs:
|
||||
{{- range .Values.controlPlane.apiServer.endpoints }}
|
||||
- targets:
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
- job_name: kube-scheduler
|
||||
scheme: https
|
||||
tls_config:
|
||||
insecure_skip_verify: {{ .Values.controlPlane.scheduler.insecureSkipVerify }}
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
static_configs:
|
||||
{{- range .Values.controlPlane.scheduler.endpoints }}
|
||||
- targets:
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
- job_name: kube-controller-manager
|
||||
scheme: https
|
||||
tls_config:
|
||||
insecure_skip_verify: {{ .Values.controlPlane.controllerManager.insecureSkipVerify }}
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
static_configs:
|
||||
{{- range .Values.controlPlane.controllerManager.endpoints }}
|
||||
- targets:
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
processors:
|
||||
# Enrich all telemetry with Kubernetes metadata
|
||||
k8sattributes:
|
||||
auth_type: serviceAccount
|
||||
extract:
|
||||
metadata:
|
||||
- k8s.pod.name
|
||||
- k8s.pod.uid
|
||||
- k8s.namespace.name
|
||||
- k8s.node.name
|
||||
- k8s.deployment.name
|
||||
- k8s.replicaset.name
|
||||
- k8s.statefulset.name
|
||||
- k8s.daemonset.name
|
||||
- k8s.job.name
|
||||
- k8s.cronjob.name
|
||||
- k8s.container.name
|
||||
labels:
|
||||
- tag_name: k8s.pod.label.app
|
||||
key: app
|
||||
from: pod
|
||||
- tag_name: k8s.pod.label.app.kubernetes.io/name
|
||||
key: app.kubernetes.io/name
|
||||
from: pod
|
||||
pod_association:
|
||||
- sources:
|
||||
- from: resource_attribute
|
||||
name: k8s.pod.ip
|
||||
- sources:
|
||||
- from: resource_attribute
|
||||
name: k8s.pod.uid
|
||||
- sources:
|
||||
- from: connection
|
||||
|
||||
# Stamp all telemetry with the cluster name
|
||||
resource:
|
||||
attributes:
|
||||
- key: k8s.cluster.name
|
||||
value: {{ .Values.clusterName | quote }}
|
||||
action: upsert
|
||||
|
||||
# Batch telemetry for efficient export
|
||||
batch:
|
||||
send_batch_size: 1024
|
||||
timeout: 10s
|
||||
|
||||
# Limit memory usage
|
||||
memory_limiter:
|
||||
check_interval: 5s
|
||||
limit_mib: 400
|
||||
spike_limit_mib: 100
|
||||
|
||||
exporters:
|
||||
otlphttp:
|
||||
endpoint: "{{ .Values.oneuptime.url }}"
|
||||
headers:
|
||||
x-oneuptime-token: "${env:ONEUPTIME_API_KEY}"
|
||||
|
||||
service:
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers:
|
||||
- kubeletstats
|
||||
- k8s_cluster
|
||||
{{- if .Values.controlPlane.enabled }}
|
||||
- prometheus
|
||||
{{- end }}
|
||||
processors:
|
||||
- memory_limiter
|
||||
- k8sattributes
|
||||
- resource
|
||||
- batch
|
||||
exporters:
|
||||
- otlphttp
|
||||
logs:
|
||||
receivers:
|
||||
- k8sobjects
|
||||
processors:
|
||||
- memory_limiter
|
||||
- k8sattributes
|
||||
- resource
|
||||
- batch
|
||||
exporters:
|
||||
- otlphttp
|
||||
56
HelmChart/Public/kubernetes-agent/templates/daemonset.yaml
Normal file
56
HelmChart/Public/kubernetes-agent/templates/daemonset.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
{{- if .Values.logs.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}-logs
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
component: log-collector
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "kubernetes-agent.selectorLabels" . | nindent 6 }}
|
||||
component: log-collector
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "kubernetes-agent.selectorLabels" . | nindent 8 }}
|
||||
component: log-collector
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap-daemonset.yaml") . | sha256sum }}
|
||||
spec:
|
||||
serviceAccountName: {{ include "kubernetes-agent.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: otel-collector
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
- "--config=/etc/otel/otel-collector-config.yaml"
|
||||
env:
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- name: ONEUPTIME_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}
|
||||
key: api-key
|
||||
resources:
|
||||
{{- toYaml .Values.logs.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/otel
|
||||
readOnly: true
|
||||
- name: varlogpods
|
||||
mountPath: /var/log/pods
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}-daemonset
|
||||
- name: varlogpods
|
||||
hostPath:
|
||||
path: /var/log/pods
|
||||
{{- end }}
|
||||
67
HelmChart/Public/kubernetes-agent/templates/deployment.yaml
Normal file
67
HelmChart/Public/kubernetes-agent/templates/deployment.yaml
Normal file
@@ -0,0 +1,67 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
component: metrics-collector
|
||||
spec:
|
||||
replicas: {{ .Values.deployment.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "kubernetes-agent.selectorLabels" . | nindent 6 }}
|
||||
component: metrics-collector
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "kubernetes-agent.selectorLabels" . | nindent 8 }}
|
||||
component: metrics-collector
|
||||
annotations:
|
||||
checksum/config: {{ include (print $.Template.BasePath "/configmap-deployment.yaml") . | sha256sum }}
|
||||
spec:
|
||||
serviceAccountName: {{ include "kubernetes-agent.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: otel-collector
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
- "--config=/etc/otel/otel-collector-config.yaml"
|
||||
env:
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- name: CLUSTER_NAME
|
||||
value: {{ .Values.clusterName | quote }}
|
||||
- name: ONEUPTIME_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}
|
||||
key: api-key
|
||||
ports:
|
||||
- name: health
|
||||
containerPort: 13133
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: health
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: health
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
{{- toYaml .Values.deployment.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/otel
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}-deployment
|
||||
88
HelmChart/Public/kubernetes-agent/templates/rbac.yaml
Normal file
88
HelmChart/Public/kubernetes-agent/templates/rbac.yaml
Normal file
@@ -0,0 +1,88 @@
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
rules:
|
||||
# For k8s_cluster receiver and k8sattributes processor
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- pods
|
||||
- nodes
|
||||
- nodes/proxy
|
||||
- nodes/stats
|
||||
- services
|
||||
- endpoints
|
||||
- namespaces
|
||||
- events
|
||||
- replicationcontrollers
|
||||
- resourcequotas
|
||||
- limitranges
|
||||
- configmaps
|
||||
- persistentvolumeclaims
|
||||
- persistentvolumes
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["apps"]
|
||||
resources:
|
||||
- deployments
|
||||
- replicasets
|
||||
- statefulsets
|
||||
- daemonsets
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["batch"]
|
||||
resources:
|
||||
- jobs
|
||||
- cronjobs
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["autoscaling"]
|
||||
resources:
|
||||
- horizontalpodautoscalers
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources:
|
||||
- ingresses
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["extensions"]
|
||||
resources:
|
||||
- ingresses
|
||||
verbs: ["get", "list", "watch"]
|
||||
# For k8sobjects receiver to watch events
|
||||
- apiGroups: ["events.k8s.io"]
|
||||
resources:
|
||||
- events
|
||||
verbs: ["get", "list", "watch"]
|
||||
# For kubeletstats receiver
|
||||
- nonResourceURLs:
|
||||
- /metrics
|
||||
- /metrics/cadvisor
|
||||
verbs: ["get"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ include "kubernetes-agent.fullname" . }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "kubernetes-agent.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
10
HelmChart/Public/kubernetes-agent/templates/secret.yaml
Normal file
10
HelmChart/Public/kubernetes-agent/templates/secret.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "kubernetes-agent.fullname" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "kubernetes-agent.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
api-key: {{ .Values.oneuptime.apiKey | b64enc | quote }}
|
||||
80
HelmChart/Public/kubernetes-agent/values.yaml
Normal file
80
HelmChart/Public/kubernetes-agent/values.yaml
Normal file
@@ -0,0 +1,80 @@
|
||||
# OneUptime Kubernetes Agent Configuration
|
||||
|
||||
# Required: Your OneUptime instance connection details
|
||||
oneuptime:
|
||||
# URL of your OneUptime instance (e.g., https://oneuptime.example.com)
|
||||
url: ""
|
||||
# Project API key from OneUptime (Settings > API Keys)
|
||||
apiKey: ""
|
||||
|
||||
# Required: Unique name for this cluster (used as k8s.cluster.name attribute)
|
||||
clusterName: ""
|
||||
|
||||
# Namespace filters — limit which namespaces are monitored
|
||||
namespaceFilters:
|
||||
# If set, only these namespaces are monitored (empty = all namespaces)
|
||||
include: []
|
||||
# Namespaces to exclude from monitoring
|
||||
exclude:
|
||||
- kube-system
|
||||
|
||||
# OTel Collector image configuration
|
||||
image:
|
||||
repository: otel/opentelemetry-collector-contrib
|
||||
tag: "0.96.0"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# Deployment (metrics + events collector) resource configuration
|
||||
deployment:
|
||||
replicas: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
# Control plane monitoring (etcd, API server, scheduler, controller manager)
|
||||
# Disabled by default — enable for self-managed clusters.
|
||||
# Managed K8s (EKS, GKE, AKS) typically don't expose control plane metrics.
|
||||
controlPlane:
|
||||
enabled: false
|
||||
etcd:
|
||||
# Endpoints to scrape etcd metrics from
|
||||
endpoints:
|
||||
- https://localhost:2379/metrics
|
||||
# TLS configuration for etcd
|
||||
insecureSkipVerify: true
|
||||
apiServer:
|
||||
endpoints:
|
||||
- https://localhost:6443/metrics
|
||||
insecureSkipVerify: true
|
||||
scheduler:
|
||||
endpoints:
|
||||
- https://localhost:10259/metrics
|
||||
insecureSkipVerify: true
|
||||
controllerManager:
|
||||
endpoints:
|
||||
- https://localhost:10257/metrics
|
||||
insecureSkipVerify: true
|
||||
|
||||
# Pod log collection via DaemonSet with filelog receiver
|
||||
logs:
|
||||
enabled: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
|
||||
# Collection intervals
|
||||
collectionInterval: 30s
|
||||
|
||||
# Service account configuration
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: ""
|
||||
annotations: {}
|
||||
Reference in New Issue
Block a user