From 9af4a27885a7b0cd4547447bfacae31338c84fe0 Mon Sep 17 00:00:00 2001 From: petar-cvit Date: Mon, 14 Oct 2024 18:44:15 +0200 Subject: [PATCH 1/8] list custom resource managed resources --- cyclops-ctrl/cluster.yaml | 51 ++ cyclops-ctrl/internal/models/dto/k8s.go | 15 +- cyclops-ctrl/pkg/cluster/k8sclient/client.go | 10 +- cyclops-ctrl/pkg/cluster/k8sclient/modules.go | 50 ++ .../pkg/cluster/k8sclient/resources.go | 18 + .../k8s-resources/DefaultResource.tsx | 106 +++ .../ResourceList/ResourceList.tsx | 724 ++++++++++++++++++ .../src/components/pages/ModuleDetails.tsx | 659 +--------------- cyclops-ui/src/utils/k8s/workload.tsx | 4 + 9 files changed, 978 insertions(+), 659 deletions(-) create mode 100644 cyclops-ctrl/cluster.yaml create mode 100644 cyclops-ui/src/components/k8s-resources/DefaultResource.tsx create mode 100644 cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx create mode 100644 cyclops-ui/src/utils/k8s/workload.tsx diff --git a/cyclops-ctrl/cluster.yaml b/cyclops-ctrl/cluster.yaml new file mode 100644 index 00000000..485e41f3 --- /dev/null +++ b/cyclops-ctrl/cluster.yaml @@ -0,0 +1,51 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: moj-prvi-cluster + labels: + helm.sh/chart: cluster-0.0.11 + app.kubernetes.io/name: cluster + app.kubernetes.io/instance: moj-prvi-cluster + app.kubernetes.io/part-of: cloudnative-pg + app.kubernetes.io/managed-by: Helm + cyclops.module: moj-prvi-cluster +spec: + instances: 3 + + imageName: ghcr.io/cloudnative-pg/postgresql:16 + imagePullPolicy: IfNotPresent + postgresUID: 26 + postgresGID: 26 + storage: + size: 8Gi + storageClass: + affinity: + topologyKey: topology.kubernetes.io/zone + priorityClassName: + + primaryUpdateMethod: switchover + primaryUpdateStrategy: unsupervised + logLevel: info + enableSuperuserAccess: true + postgresql: + shared_preload_libraries: + pg_hba: + [] + pg_ident: + [] + parameters: + {} + + + managed: + + monitoring: + enablePodMonitor: false + + + bootstrap: + initdb: + postInitApplicationSQL: + + + diff --git a/cyclops-ctrl/internal/models/dto/k8s.go b/cyclops-ctrl/internal/models/dto/k8s.go index f3f880d3..91c2418c 100644 --- a/cyclops-ctrl/internal/models/dto/k8s.go +++ b/cyclops-ctrl/internal/models/dto/k8s.go @@ -626,13 +626,14 @@ type IPBlock struct { } type Other struct { - Group string `json:"group"` - Version string `json:"version"` - Kind string `json:"kind"` - Name string `json:"name"` - Namespace string `json:"namespace"` - Status string `json:"status"` - Deleted bool `json:"deleted"` + Group string `json:"group"` + Version string `json:"version"` + Kind string `json:"kind"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Status string `json:"status"` + Deleted bool `json:"deleted"` + Children []Resource `json:"children"` } func (s *Other) GetGroupVersionKind() string { diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/client.go b/cyclops-ctrl/pkg/cluster/k8sclient/client.go index 54cc96ed..90f5fe65 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/client.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/client.go @@ -23,13 +23,12 @@ const ( ) type KubernetesClient struct { - Dynamic dynamic.Interface - + Dynamic dynamic.Interface clientset *kubernetes.Clientset - discovery *discovery.DiscoveryClient - moduleset *client.CyclopsV1Alpha1Client + + parentLabels []string } func New() (*KubernetesClient, error) { @@ -60,6 +59,9 @@ func createLocalClient() (*KubernetesClient, error) { discovery: discovery, clientset: clientset, moduleset: moduleSet, + parentLabels: []string{ + "cnpg.io/cluster", + }, }, nil } diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/modules.go b/cyclops-ctrl/pkg/cluster/k8sclient/modules.go index e7902a09..3b9d9fdf 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/modules.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/modules.go @@ -107,6 +107,56 @@ func (k *KubernetesClient) GetResourcesForModule(name string) ([]dto.Resource, e return out, nil } +func (k *KubernetesClient) GetResourcesForCRD(label, name string) ([]dto.Resource, error) { + out := make([]dto.Resource, 0, 0) + + managedGVRs, err := k.getManagedGVRs("") + if err != nil { + return nil, err + } + + other := make([]unstructured.Unstructured, 0) + for _, gvr := range managedGVRs { + rs, err := k.Dynamic.Resource(gvr).List(context.Background(), metav1.ListOptions{ + LabelSelector: label + "=" + name, + }) + if err != nil { + continue + } + + for _, item := range rs.Items { + other = append(other, item) + } + } + + for _, o := range other { + status, err := k.getResourceStatus(o) + if err != nil { + return nil, err + } + + out = append(out, &dto.Other{ + Group: o.GroupVersionKind().Group, + Version: o.GroupVersionKind().Version, + Kind: o.GroupVersionKind().Kind, + Name: o.GetName(), + Namespace: o.GetNamespace(), + Status: status, + Deleted: false, + }) + } + + sort.Slice(out, func(i, j int) bool { + if out[i].GetGroupVersionKind() != out[j].GetGroupVersionKind() { + return out[i].GetGroupVersionKind() < out[j].GetGroupVersionKind() + } + + return out[i].GetName() < out[j].GetName() + }) + + return out, nil +} + func (k *KubernetesClient) GetWorkloadsForModule(name string) ([]dto.Resource, error) { out := make([]dto.Resource, 0, 0) diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/resources.go b/cyclops-ctrl/pkg/cluster/k8sclient/resources.go index 8f22c1f2..2b6d6928 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/resources.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/resources.go @@ -141,11 +141,29 @@ func (k *KubernetesClient) GetResource(group, version, kind, name, namespace str return k.mapRole(group, version, kind, name, namespace) case isNetworkPolicy(group, version, kind): return k.mapNetworkPolicy(group, version, kind, name, namespace) + default: + return k.mapDefaultResource(group, version, kind, name, namespace) } return nil, nil } +func (k *KubernetesClient) mapDefaultResource(group, version, kind, name, namespace string) (any, error) { + children, err := k.GetResourcesForCRD(k.parentLabels[0], name) + if err != nil { + return nil, err + } + + return &dto.Other{ + Group: group, + Version: version, + Kind: kind, + Name: name, + Namespace: namespace, + Children: children, + }, nil +} + func (k *KubernetesClient) Delete(resource dto.Resource) error { apiResourceName, err := k.GVKtoAPIResourceName( schema.GroupVersion{ diff --git a/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx b/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx new file mode 100644 index 00000000..1dc6e666 --- /dev/null +++ b/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx @@ -0,0 +1,106 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Divider, Row, Alert } from "antd"; +import axios from "axios"; +import { mapResponseError } from "../../utils/api/errors"; +import ResourceList from "./ResourceList/ResourceList"; + +interface Props { + group: string; + version: string; + kind: string; + name: string; + namespace: string; + onResourceDelete: () => void; +} + +const DefaultResource = ({ + group, + version, + kind, + name, + namespace, + onResourceDelete, +}: Props) => { + const [resource, setResource] = useState({ + children: [], + }); + const [error, setError] = useState({ + message: "", + description: "", + }); + const [loadResources, setLoadResources] = useState(false); + + const fetchResource = useCallback(() => { + axios + .get(`/api/resources`, { + params: { + group: group, + version: version, + kind: kind, + name: name, + namespace: namespace, + }, + }) + .then((res) => { + setResource(res.data); + }) + .catch((error) => { + setError(mapResponseError(error)); + }) + .finally(() => { + setLoadResources(true); + }); + }, [name, namespace]); + + useEffect(() => { + fetchResource(); + + const interval = setInterval(() => fetchResource(), 15000); + return () => { + clearInterval(interval); + }; + }, [fetchResource]); + + const resourceList = () => { + if (resource.children) { + return ( + + ); + } + }; + + return ( +
+ {error.message.length !== 0 && ( + { + setError({ + message: "", + description: "", + }); + }} + style={{ marginBottom: "20px" }} + /> + )} +
+ {resourceList()} +
+
+ ); +}; + +export default DefaultResource; diff --git a/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx b/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx new file mode 100644 index 00000000..ffb13be4 --- /dev/null +++ b/cyclops-ui/src/components/k8s-resources/ResourceList/ResourceList.tsx @@ -0,0 +1,724 @@ +import React, { useState } from "react"; +import { + Divider, + Row, + Tooltip, + Button, + Collapse, + Col, + Checkbox, + Spin, + Popover, + Input, + Modal, + Alert, +} from "antd"; +import axios from "axios"; +import ReactAce from "react-ace"; +import { + isWorkload, + ResourceRef, + resourceRefKey, +} from "../../../utils/resourceRef"; +import Deployment from "../Deployment"; +import CronJob from "../CronJob"; +import Job from "../Job"; +import DaemonSet from "../DaemonSet"; +import StatefulSet from "../StatefulSet"; +import Pod from "../Pod"; +import Service from "../Service"; +import ConfigMap from "../ConfigMap"; +import PersistentVolumeClaim from "../PersistentVolumeClaim"; +import Secret from "../Secret"; +import { + CaretRightOutlined, + CheckCircleTwoTone, + ClockCircleTwoTone, + CloseSquareTwoTone, + CopyOutlined, + FileTextOutlined, + SearchOutlined, + WarningTwoTone, +} from "@ant-design/icons"; +import { isStreamingEnabled } from "../../../utils/api/common"; +import { canRestart, RestartButton } from "../common/RestartButton"; +import { gvkString } from "../../../utils/k8s/gvk"; +import Title from "antd/es/typography/Title"; +import { mapResponseError } from "../../../utils/api/errors"; +import { CheckboxChangeEvent } from "antd/es/checkbox"; +import DefaultResource from "../DefaultResource"; +import { Workload } from "../../../utils/k8s/workload"; + +interface Props { + loadResources: boolean; + resources: any[]; + workloads: Map; + onResourceDelete: () => void; +} + +const ResourceList = ({ + loadResources, + resources, + workloads, + onResourceDelete, +}: Props) => { + const [error, setError] = useState({ + message: "", + description: "", + }); + + const [manifestModal, setManifestModal] = useState({ + on: false, + resource: { + group: "", + version: "", + kind: "", + name: "", + namespace: "", + }, + manifest: "", + }); + + const [deleteResourceModal, setDeleteResourceModal] = useState(false); + const [deleteResourceRef, setDeleteResourceRef] = useState({ + group: "", + version: "", + kind: "", + name: "", + namespace: "", + }); + const [deleteResourceVerify, setDeleteResourceVerify] = useState(""); + + const [showManagedFields, setShowManagedFields] = useState(false); + + const [resourceFilter, setResourceFilter] = useState([]); + const [activeCollapses, setActiveCollapses] = useState(new Map()); + + function getWorkload(ref: ResourceRef): Workload | undefined { + let k = resourceRefKey(ref); + return workloads.get(k); + } + + const handleCancelDeleteResource = () => { + setDeleteResourceModal(false); + setDeleteResourceRef({ + group: "", + version: "", + kind: "", + name: "", + namespace: "", + }); + setDeleteResourceVerify(""); + }; + + const deleteResource = () => { + axios + .delete(`/api/resources`, { + data: { + group: deleteResourceRef.group, + version: deleteResourceRef.version, + kind: deleteResourceRef.kind, + name: deleteResourceRef.name, + namespace: deleteResourceRef.namespace, + }, + }) + .then(() => { + onResourceDelete(); + // setLoadResources(false); TODO + setDeleteResourceModal(false); + // fetchModuleResources(); TODO + }) + .catch((error) => { + // setLoading(false); TODO + setError(mapResponseError(error)); + }); + }; + + const changeDeleteResourceVerify = (e: any) => { + setDeleteResourceVerify(e.target.value); + }; + + const onResourceFilterUpdate = (kinds: string[]) => { + setResourceFilter(kinds); + }; + + const handleCancelManifest = () => { + setManifestModal({ + ...manifestModal, + on: false, + }); + }; + + const handleCheckboxChange = (e: CheckboxChangeEvent) => { + setShowManagedFields(e.target.checked); + fetchManifest( + manifestModal.resource.group, + manifestModal.resource.version, + manifestModal.resource.kind, + manifestModal.resource.namespace, + manifestModal.resource.name, + e.target.checked, + ); + }; + + function fetchManifest( + group: string, + version: string, + kind: string, + namespace: string, + name: string, + showManagedFields: boolean, + ) { + axios + .get(`/api/manifest`, { + params: { + group: group, + version: version, + kind: kind, + name: name, + namespace: namespace, + includeManagedFields: showManagedFields, + }, + }) + .then((res) => { + setManifestModal((prev) => ({ + ...prev, + manifest: res.data, + })); + }) + .catch((error) => { + setError(mapResponseError(error)); + }); + } + + const resourceFilterOptions = () => { + if (!loadResources) { + return ; + } + + let uniqueGVKs = new Set(); + resources.forEach(function (resource: any) { + uniqueGVKs.add( + gvkString(resource.group, resource.version, resource.kind), + ); + }); + + let options: any[] = []; + uniqueGVKs.forEach(function (gvk: string) { + options.push( + + {gvk} + , + ); + }); + + return options; + }; + + const resourceFilterPopover = () => { + return ( + + {resourceFilterOptions()} + + ); + }; + + const getCollapseColor = (fieldName: string) => { + if ( + activeCollapses.get(fieldName) && + activeCollapses.get(fieldName) === true + ) { + return "#EFEFEF"; + } else { + return "#FAFAFA"; + } + }; + + const getCollapseWidth = (fieldName: string) => { + if ( + activeCollapses.get(fieldName) && + activeCollapses.get(fieldName) === true + ) { + return "166%"; + } else { + return "100%"; + } + }; + + const resourceCollapses: {} | any = []; + resources.forEach((resource: any, index) => { + let collapseKey = + resource.kind + "/" + resource.namespace + "/" + resource.name; + + let resourceDetails =

; + + let resourceRef: ResourceRef = { + group: resource.group, + version: resource.version, + kind: resource.kind, + name: resource.name, + namespace: resource.namespace, + }; + + switch (resource.kind) { + case "Deployment": + resourceDetails = ( + + ); + break; + case "CronJob": + resourceDetails = ( + + ); + break; + case "Job": + resourceDetails = ( + + ); + break; + case "DaemonSet": + resourceDetails = ( + + ); + break; + case "StatefulSet": + resourceDetails = ( + + ); + break; + case "Pod": + resourceDetails = ( + + ); + break; + case "Service": + resourceDetails = ( + + ); + break; + case "ConfigMap": + resourceDetails = ( + + ); + break; + case "PersistentVolumeClaim": + resourceDetails = ( + + ); + break; + case "Secret": + resourceDetails = ( + + ); + break; + default: + resourceDetails = ( + + ); + break; + } + + let deletedWarning =

; + + if (resource.deleted) { + deletedWarning = ( + + + + ); + } + + let deleteButton =

; + + if (resource.deleted) { + deleteButton = ( + + ); + } + + let resourceStatus = resource.status; + if (isStreamingEnabled() && isWorkload(resourceRef)) { + resourceStatus = getWorkload(resourceRef)?.status; + } + + const genExtra = (resource: any, status?: string) => { + let statusIcon = <>; + if (status === "progressing") { + statusIcon = ( + + ); + } + if (status === "healthy") { + statusIcon = ( + + ); + } + if (status === "unhealthy") { + statusIcon = ( + + ); + } + + let deletedIcon = <>; + if (resource.deleted) { + deletedIcon = ( + + ); + } + + return ( + + +

+ {resource.name} {resource.kind} {statusIcon} +

+ + + {deletedIcon} + + + ); + }; + + const getResourceDisplay = ( + group: string, + version: string, + kind: string, + ): string => { + if (resourceFilter.length === 0) { + return ""; + } + + for (let filter of resourceFilter) { + if (gvkString(group, version, kind) === filter) { + return ""; + } + } + + return "none"; + }; + + const getStatusColor = (status: string, deleted: boolean) => { + if (status === "unhealthy") { + return "#FF0000"; + } + + if (deleted) { + return "#ff9f1a"; + } + + if (status === "progressing") { + return "#ffcc00"; + } + + return "#27D507"; + }; + + const handleManifestClick = (resource: any) => { + setManifestModal({ + on: true, + resource: { + group: resource.group, + version: resource.version, + kind: resource.kind, + name: resource.name, + namespace: resource.namespace, + }, + manifest: "", + }); + fetchManifest( + resource.group, + resource.version, + resource.kind, + resource.namespace, + resource.name, + showManagedFields, + ); + }; + + resourceCollapses.push( + + + {deletedWarning} + + + + {resource.name} + + + + + {deleteButton} + + + + {resource.namespace} + + + + + + {canRestart(resource.group, resource.version, resource.kind) && ( + + + + )} + + {resourceDetails} + , + ); + }); + + const loadingResourceCollapses = () => { + if (loadResources) { + return ( + ( + + )} + style={{ + width: "60%", + border: "none", + backgroundColor: "#FFF", + }} + onChange={function (values: string | string[]) { + let m = new Map(); + for (let value of values) { + m.set(value, true); + } + + setActiveCollapses(m); + }} + > + {resourceCollapses} + + ); + } + + return ; + }; + + return ( +
+ {error.message.length !== 0 && ( + { + setError({ + message: "", + description: "", + }); + }} + style={{ marginBottom: "20px" }} + /> + )} + + {"Resources "} + + + + + {loadingResourceCollapses()} + + + Include Managed Fields + + +
+ + + + +
+
+ + Delete + + } + width={"40%"} + > +

+ In order to confirm deleting this resource, type:{" "} + {deleteResourceRef.kind + " " + deleteResourceRef.name} +

+ +
+
+ ); +}; + +export default ResourceList; diff --git a/cyclops-ui/src/components/pages/ModuleDetails.tsx b/cyclops-ui/src/components/pages/ModuleDetails.tsx index e743c6a4..18073c09 100644 --- a/cyclops-ui/src/components/pages/ModuleDetails.tsx +++ b/cyclops-ui/src/components/pages/ModuleDetails.tsx @@ -2,15 +2,12 @@ import React, { useEffect, useState, useCallback } from "react"; import { Alert, Button, - Checkbox, Col, - Collapse, Descriptions, Divider, Input, Modal, notification, - Popover, Row, Spin, Tooltip, @@ -21,7 +18,6 @@ import { useParams } from "react-router-dom"; import axios from "axios"; import { BookOutlined, - CaretRightOutlined, CheckCircleTwoTone, ClockCircleTwoTone, CloseSquareTwoTone, @@ -29,36 +25,18 @@ import { DeleteOutlined, EditOutlined, FileTextOutlined, - SearchOutlined, UndoOutlined, - WarningTwoTone, } from "@ant-design/icons"; import "./custom.css"; import "ace-builds/src-noconflict/mode-jsx"; import ReactAce from "react-ace"; -import Deployment from "../k8s-resources/Deployment"; -import CronJob from "../k8s-resources/CronJob"; -import Job from "../k8s-resources/Job"; -import DaemonSet from "../k8s-resources/DaemonSet"; -import StatefulSet from "../k8s-resources/StatefulSet"; -import Pod from "../k8s-resources/Pod"; -import Service from "../k8s-resources/Service"; -import ConfigMap from "../k8s-resources/ConfigMap"; -import PersistentVolumeClaim from "../k8s-resources/PersistentVolumeClaim"; import { moduleTemplateReferenceView, templateRef, } from "../../utils/templateRef"; -import { gvkString } from "../../utils/k8s/gvk"; import { mapResponseError } from "../../utils/api/errors"; -import Secret from "../k8s-resources/Secret"; -import { CheckboxChangeEvent } from "antd/es/checkbox"; -import { - canRestart, - RestartButton, -} from "../k8s-resources/common/RestartButton"; import YAML from "yaml"; import { isStreamingEnabled } from "../../utils/api/common"; import { resourcesStream } from "../../utils/api/sse/resources"; @@ -67,6 +45,8 @@ import { ResourceRef, resourceRefKey, } from "../../utils/resourceRef"; +import ResourceList from "../k8s-resources/ResourceList/ResourceList"; +import { Workload } from "../../utils/k8s/workload"; const languages = [ "javascript", @@ -116,41 +96,24 @@ interface module { iconURL: string; } -interface workload { - status: string; - pods: any[]; -} - const ModuleDetails = () => { - const [manifestModal, setManifestModal] = useState({ - on: false, - resource: { - group: "", - version: "", - kind: "", - name: "", - namespace: "", - }, - manifest: "", - }); const [loading, setLoading] = useState(false); const [loadModule, setLoadModule] = useState(false); const [loadResources, setLoadResources] = useState(false); const [loadingReconciliation, setLoadingReconciliation] = useState(false); const [deleteName, setDeleteName] = useState(""); - const [deleteResourceVerify, setDeleteResourceVerify] = useState(""); const [resources, setResources] = useState([]); - const [workloads, setWorkloads] = useState>(new Map()); + const [workloads, setWorkloads] = useState>(new Map()); - function getWorkload(ref: ResourceRef): workload | undefined { + function getWorkload(ref: ResourceRef): Workload | undefined { let k = resourceRefKey(ref); return workloads.get(k); } - function putWorkload(ref: ResourceRef, workload: workload) { + function putWorkload(ref: ResourceRef, workload: Workload) { let k = resourceRefKey(ref); setWorkloads((prev) => { @@ -174,15 +137,6 @@ const ModuleDetails = () => { iconURL: "", }); - const [deleteResourceModal, setDeleteResourceModal] = useState(false); - const [deleteResourceRef, setDeleteResourceRef] = useState({ - group: "", - version: "", - kind: "", - name: "", - namespace: "", - }); - const [loadingRawManifest, setLoadingRawManifest] = useState(false); const [viewRawManifest, setViewRawManifest] = useState(false); const [rawModuleManifest, setRawModuleManifest] = useState(""); @@ -191,10 +145,6 @@ const ModuleDetails = () => { const [viewRenderedManifest, setViewRenderedManifest] = useState(false); const [renderedManifest, setRenderedManifest] = useState(""); - const [resourceFilter, setResourceFilter] = useState([]); - - const [activeCollapses, setActiveCollapses] = useState(new Map()); - const [error, setError] = useState({ message: "", description: "", @@ -202,74 +152,6 @@ const ModuleDetails = () => { let { moduleName } = useParams(); - const [showManagedFields, setShowManagedFields] = useState(false); - - const handleCheckboxChange = (e: CheckboxChangeEvent) => { - setShowManagedFields(e.target.checked); - fetchManifest( - manifestModal.resource.group, - manifestModal.resource.version, - manifestModal.resource.kind, - manifestModal.resource.namespace, - manifestModal.resource.name, - e.target.checked, - ); - }; - - const handleManifestClick = (resource: any) => { - setManifestModal({ - on: true, - resource: { - group: resource.group, - version: resource.version, - kind: resource.kind, - name: resource.name, - namespace: resource.namespace, - }, - manifest: "", - }); - fetchManifest( - resource.group, - resource.version, - resource.kind, - resource.namespace, - resource.name, - showManagedFields, - ); - }; - - function fetchManifest( - group: string, - version: string, - kind: string, - namespace: string, - name: string, - showManagedFields: boolean, - ) { - axios - .get(`/api/manifest`, { - params: { - group: group, - version: version, - kind: kind, - name: name, - namespace: namespace, - includeManagedFields: showManagedFields, - }, - }) - .then((res) => { - setManifestModal((prev) => ({ - ...prev, - manifest: res.data, - })); - }) - .catch((error) => { - setLoading(false); - setLoadModule(true); - setError(mapResponseError(error)); - }); - } - const fetchModuleResources = useCallback(() => { axios .get(`/api/modules/` + moduleName + `/resources`) @@ -323,55 +205,10 @@ const ModuleDetails = () => { } }, [moduleName]); - const getCollapseColor = (fieldName: string) => { - if ( - activeCollapses.get(fieldName) && - activeCollapses.get(fieldName) === true - ) { - return "#EFEFEF"; - } else { - return "#FAFAFA"; - } - }; - - const getCollapseWidth = (fieldName: string) => { - if ( - activeCollapses.get(fieldName) && - activeCollapses.get(fieldName) === true - ) { - return "166%"; - } else { - return "100%"; - } - }; - const changeDeleteName = (e: any) => { setDeleteName(e.target.value); }; - const changeDeleteResourceVerify = (e: any) => { - setDeleteResourceVerify(e.target.value); - }; - - const handleCancelManifest = () => { - setManifestModal({ - ...manifestModal, - on: false, - }); - }; - - const handleCancelDeleteResource = () => { - setDeleteResourceModal(false); - setDeleteResourceRef({ - group: "", - version: "", - kind: "", - name: "", - namespace: "", - }); - setDeleteResourceVerify(""); - }; - const handleCancel = () => { setLoading(false); }; @@ -406,332 +243,6 @@ const ModuleDetails = () => { return resourcesToDelete; }; - const resourceCollapses: {} | any = []; - - const genExtra = (resource: any, status?: string) => { - let statusIcon = <>; - if (status === "progressing") { - statusIcon = ( - - ); - } - if (status === "healthy") { - statusIcon = ( - - ); - } - if (status === "unhealthy") { - statusIcon = ( - - ); - } - - let deletedIcon = <>; - if (resource.deleted) { - deletedIcon = ( - - ); - } - - return ( - - -

- {resource.name} {resource.kind} {statusIcon} -

- - - {deletedIcon} - -
- ); - }; - - const getResourceDisplay = ( - group: string, - version: string, - kind: string, - ): string => { - if (resourceFilter.length === 0) { - return ""; - } - - for (let filter of resourceFilter) { - if (gvkString(group, version, kind) === filter) { - return ""; - } - } - - return "none"; - }; - - const getStatusColor = (status: string, deleted: boolean) => { - if (status === "unhealthy") { - return "#FF0000"; - } - - if (deleted) { - return "#ff9f1a"; - } - - if (status === "progressing") { - return "#ffcc00"; - } - - return "#27D507"; - }; - - resources.forEach((resource: any, index) => { - let collapseKey = - resource.kind + "/" + resource.namespace + "/" + resource.name; - - let resourceDetails =

; - - let resourceRef: ResourceRef = { - group: resource.group, - version: resource.version, - kind: resource.kind, - name: resource.name, - namespace: resource.namespace, - }; - - switch (resource.kind) { - case "Deployment": - resourceDetails = ( - - ); - break; - case "CronJob": - resourceDetails = ( - - ); - break; - case "Job": - resourceDetails = ( - - ); - break; - case "DaemonSet": - resourceDetails = ( - - ); - break; - case "StatefulSet": - resourceDetails = ( - - ); - break; - case "Pod": - resourceDetails = ( - - ); - break; - case "Service": - resourceDetails = ( - - ); - break; - case "ConfigMap": - resourceDetails = ( - - ); - break; - case "PersistentVolumeClaim": - resourceDetails = ( - - ); - break; - case "Secret": - resourceDetails = ( - - ); - break; - } - - let deletedWarning =

; - - if (resource.deleted) { - deletedWarning = ( - - - - ); - } - - let deleteButton =

; - - if (resource.deleted) { - deleteButton = ( - - ); - } - - let resourceStatus = resource.status; - if (isStreamingEnabled() && isWorkload(resourceRef)) { - resourceStatus = getWorkload(resourceRef)?.status; - } - - resourceCollapses.push( - - - {deletedWarning} - - - - {resource.name} - - - - - {deleteButton} - - - - {resource.namespace} - - - - - - {canRestart(resource.group, resource.version, resource.kind) && ( - - - - )} - - {resourceDetails} - , - ); - }); - - const resourcesLoading = () => { - if (loadResources) { - return ( - ( - - )} - style={{ - width: "60%", - border: "none", - backgroundColor: "#FFF", - }} - onChange={function (values: string | string[]) { - let m = new Map(); - for (let value of values) { - m.set(value, true); - } - - setActiveCollapses(m); - }} - > - {resourceCollapses} - - ); - } else { - return ; - } - }; - const moduleLoading = () => { if (!loadModule) { return ; @@ -880,68 +391,6 @@ const ModuleDetails = () => { ); }; - const deleteResource = () => { - axios - .delete(`/api/resources`, { - data: { - group: deleteResourceRef.group, - version: deleteResourceRef.version, - kind: deleteResourceRef.kind, - name: deleteResourceRef.name, - namespace: deleteResourceRef.namespace, - }, - }) - .then(() => { - setLoadResources(false); - setDeleteResourceModal(false); - fetchModuleResources(); - }) - .catch((error) => { - setLoading(false); - setError(mapResponseError(error)); - }); - }; - - const onResourceFilterUpdate = (kinds: string[]) => { - setResourceFilter(kinds); - }; - - const resourceFilterOptions = () => { - if (!loadResources) { - return ; - } - - let uniqueGVKs = new Set(); - resources.forEach(function (resource: any) { - uniqueGVKs.add( - gvkString(resource.group, resource.version, resource.kind), - ); - }); - - let options: any[] = []; - uniqueGVKs.forEach(function (gvk: string) { - options.push( - - {gvk} - , - ); - }); - - return options; - }; - - const resourceFilterPopover = () => { - return ( - - {resourceFilterOptions()} - - ); - }; - const handleViewRawModuleManifest = () => { setLoadingRawManifest(true); setViewRawManifest(true); @@ -1160,22 +609,12 @@ const ModuleDetails = () => { - - {"Resources "} - - - - - {resourcesLoading()} + { the module in the box below - - - Include Managed Fields - - -

- - - - -
- - - Delete - - } - width={"40%"} - > -

- In order to confirm deleting this resource, type:{" "} - {deleteResourceRef.kind + " " + deleteResourceRef.name} -

- -
Date: Mon, 14 Oct 2024 20:02:38 +0200 Subject: [PATCH 2/8] identify child label by crd gvk --- cyclops-ctrl/.env | 1 + cyclops-ctrl/cluster.yaml | 51 ------------------- cyclops-ctrl/cmd/main/main.go | 26 +++++++++- cyclops-ctrl/pkg/cluster/k8sclient/client.go | 28 +++++----- .../pkg/cluster/k8sclient/resources.go | 12 +++-- 5 files changed, 50 insertions(+), 68 deletions(-) delete mode 100644 cyclops-ctrl/cluster.yaml diff --git a/cyclops-ctrl/.env b/cyclops-ctrl/.env index 78d4024a..d7f0559d 100644 --- a/cyclops-ctrl/.env +++ b/cyclops-ctrl/.env @@ -2,3 +2,4 @@ DISABLE_TELEMETRY=true PORT=8888 WATCH_NAMESPACE=cyclops CYCLOPS_VERSION=v0.0.0 +# CHILD_LABEL_SELECTOR=Cluster.postgresql.cnpg.io/v1:cnpg.io/cluster example of custom selector labels for a CRD diff --git a/cyclops-ctrl/cluster.yaml b/cyclops-ctrl/cluster.yaml deleted file mode 100644 index 485e41f3..00000000 --- a/cyclops-ctrl/cluster.yaml +++ /dev/null @@ -1,51 +0,0 @@ -apiVersion: postgresql.cnpg.io/v1 -kind: Cluster -metadata: - name: moj-prvi-cluster - labels: - helm.sh/chart: cluster-0.0.11 - app.kubernetes.io/name: cluster - app.kubernetes.io/instance: moj-prvi-cluster - app.kubernetes.io/part-of: cloudnative-pg - app.kubernetes.io/managed-by: Helm - cyclops.module: moj-prvi-cluster -spec: - instances: 3 - - imageName: ghcr.io/cloudnative-pg/postgresql:16 - imagePullPolicy: IfNotPresent - postgresUID: 26 - postgresGID: 26 - storage: - size: 8Gi - storageClass: - affinity: - topologyKey: topology.kubernetes.io/zone - priorityClassName: - - primaryUpdateMethod: switchover - primaryUpdateStrategy: unsupervised - logLevel: info - enableSuperuserAccess: true - postgresql: - shared_preload_libraries: - pg_hba: - [] - pg_ident: - [] - parameters: - {} - - - managed: - - monitoring: - enablePodMonitor: false - - - bootstrap: - initdb: - postInitApplicationSQL: - - - diff --git a/cyclops-ctrl/cmd/main/main.go b/cyclops-ctrl/cmd/main/main.go index 2f549a36..b8b5cbc4 100644 --- a/cyclops-ctrl/cmd/main/main.go +++ b/cyclops-ctrl/cmd/main/main.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "strings" "time" _ "github.com/joho/godotenv/autoload" @@ -70,7 +71,7 @@ func main() { ) telemetryClient.InstanceStart() - k8sClient, err := k8sclient.New() + k8sClient, err := k8sclient.New(getCustomChildLabels()) if err != nil { fmt.Println("error bootstrapping Kubernetes client", err) panic(err) @@ -168,3 +169,26 @@ func getWatchNamespace(key string) string { } return value } + +func getCustomChildLabels() map[string]string { + labels := make(map[string]string) + + value := os.Getenv("CHILD_LABEL_SELECTOR") + if value == "" { + return labels + } + + pairs := strings.Split(value, ";") + + for _, pair := range pairs { + kv := strings.SplitN(pair, ":", 2) + + if len(kv) == 2 { + key := kv[0] + value := kv[1] + labels[key] = value + } + } + + return labels +} diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/client.go b/cyclops-ctrl/pkg/cluster/k8sclient/client.go index 90f5fe65..030af532 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/client.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/client.go @@ -2,6 +2,7 @@ package k8sclient import ( "context" + "fmt" apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -28,14 +29,10 @@ type KubernetesClient struct { discovery *discovery.DiscoveryClient moduleset *client.CyclopsV1Alpha1Client - parentLabels []string + childLabels map[string]string } -func New() (*KubernetesClient, error) { - return createLocalClient() -} - -func createLocalClient() (*KubernetesClient, error) { +func New(childLabels map[string]string) (*KubernetesClient, error) { config := ctrl.GetConfigOrDie() clientset, err := kubernetes.NewForConfig(config) if err != nil { @@ -55,16 +52,21 @@ func createLocalClient() (*KubernetesClient, error) { } return &KubernetesClient{ - Dynamic: dynamic, - discovery: discovery, - clientset: clientset, - moduleset: moduleSet, - parentLabels: []string{ - "cnpg.io/cluster", - }, + Dynamic: dynamic, + discovery: discovery, + clientset: clientset, + moduleset: moduleSet, + childLabels: childLabels, }, nil } +func (k *KubernetesClient) getChildLabel(group, version, kind string) (string, bool) { + key := fmt.Sprintf("%s.%s/%s", kind, group, version) + label, exists := k.childLabels[key] + + return label, exists +} + type IKubernetesClient interface { GetStreamedPodLogs(ctx context.Context, namespace, container, name string, logCount *int64, logChan chan<- string) error GetPodLogs(namespace, container, name string, numLogs *int64) ([]string, error) diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/resources.go b/cyclops-ctrl/pkg/cluster/k8sclient/resources.go index 2b6d6928..f4ba0b14 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/resources.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/resources.go @@ -149,9 +149,15 @@ func (k *KubernetesClient) GetResource(group, version, kind, name, namespace str } func (k *KubernetesClient) mapDefaultResource(group, version, kind, name, namespace string) (any, error) { - children, err := k.GetResourcesForCRD(k.parentLabels[0], name) - if err != nil { - return nil, err + var children []dto.Resource + var err error + + childLabel, exists := k.getChildLabel(group, version, kind) + if exists { + children, err = k.GetResourcesForCRD(childLabel, name) + if err != nil { + return nil, err + } } return &dto.Other{ From 6d392f523b9a65200d70524bfc44a1386ea93ca5 Mon Sep 17 00:00:00 2001 From: petar-cvit Date: Mon, 14 Oct 2024 20:09:32 +0200 Subject: [PATCH 3/8] add use state deps --- cyclops-ui/src/components/k8s-resources/DefaultResource.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx b/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx index 1dc6e666..75b51a25 100644 --- a/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx +++ b/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; -import { Divider, Row, Alert } from "antd"; +import { Alert } from "antd"; import axios from "axios"; import { mapResponseError } from "../../utils/api/errors"; import ResourceList from "./ResourceList/ResourceList"; @@ -59,7 +59,7 @@ const DefaultResource = ({ return () => { clearInterval(interval); }; - }, [fetchResource]); + }, [fetchResource, group, version, kind]); const resourceList = () => { if (resource.children) { From 3033505922a5689f94f03a03edcd9264485b7869 Mon Sep 17 00:00:00 2001 From: petar-cvit Date: Tue, 15 Oct 2024 10:57:25 +0200 Subject: [PATCH 4/8] add use callback deps --- cyclops-ui/src/components/k8s-resources/DefaultResource.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx b/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx index 75b51a25..5373ca05 100644 --- a/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx +++ b/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx @@ -50,7 +50,7 @@ const DefaultResource = ({ .finally(() => { setLoadResources(true); }); - }, [name, namespace]); + }, [name, namespace, group, version, kind]); useEffect(() => { fetchResource(); @@ -59,7 +59,7 @@ const DefaultResource = ({ return () => { clearInterval(interval); }; - }, [fetchResource, group, version, kind]); + }, [fetchResource]); const resourceList = () => { if (resource.children) { From 32205d5db4b9bd660ff75548eebd3307e0d6f98f Mon Sep 17 00:00:00 2001 From: petar-cvit Date: Tue, 15 Oct 2024 19:24:23 +0200 Subject: [PATCH 5/8] child labels configmap --- cyclops-ctrl/.env | 2 +- cyclops-ctrl/cmd/main/config.go | 67 +++++++++++++++++++ cyclops-ctrl/cmd/main/main.go | 4 +- cyclops-ctrl/config.yaml | 6 ++ .../pkg/cluster/k8sclient/childlabels.go | 64 ++++++++++++++++++ cyclops-ctrl/pkg/cluster/k8sclient/client.go | 17 +---- cyclops-ctrl/pkg/cluster/k8sclient/modules.go | 4 +- .../pkg/cluster/k8sclient/resources.go | 26 +++++-- install/cyclops-install.yaml | 26 +++++++ 9 files changed, 193 insertions(+), 23 deletions(-) create mode 100644 cyclops-ctrl/cmd/main/config.go create mode 100644 cyclops-ctrl/config.yaml create mode 100644 cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go diff --git a/cyclops-ctrl/.env b/cyclops-ctrl/.env index d7f0559d..b49b53cd 100644 --- a/cyclops-ctrl/.env +++ b/cyclops-ctrl/.env @@ -2,4 +2,4 @@ DISABLE_TELEMETRY=true PORT=8888 WATCH_NAMESPACE=cyclops CYCLOPS_VERSION=v0.0.0 -# CHILD_LABEL_SELECTOR=Cluster.postgresql.cnpg.io/v1:cnpg.io/cluster example of custom selector labels for a CRD +CONFIG_PATH=config.yaml diff --git a/cyclops-ctrl/cmd/main/config.go b/cyclops-ctrl/cmd/main/config.go new file mode 100644 index 00000000..2d732e7d --- /dev/null +++ b/cyclops-ctrl/cmd/main/config.go @@ -0,0 +1,67 @@ +package main + +import ( + "github.com/cyclops-ui/cyclops/cyclops-ctrl/pkg/cluster/k8sclient" + "gopkg.in/yaml.v3" + "io" + "os" +) + +type ControllerConfig struct { + ChildLabels k8sclient.ChildLabels `json:"childLabels"` +} + +func getConfig() ControllerConfig { + configPath := os.Getenv("CONFIG_PATH") + if configPath == "" { + configPath = "/etc/config/config.yaml" + } + + configFile, err := os.Open(configPath) + if err != nil { + setupLog.Error(err, "Failed to open controller config", "configPath", configPath) + return ControllerConfig{} + } + defer configFile.Close() + + b, err := io.ReadAll(configFile) + if err != nil { + setupLog.Error(err, "Failed to read controller config", "configPath", configPath) + return ControllerConfig{} + } + + type resourceChildLabels struct { + Group string `yaml:"group"` + Version string `yaml:"version"` + Kind string `yaml:"kind"` + MatchLabels map[string]string `yaml:"matchLabels"` + } + + type yamlConfig struct { + ChildLabels []resourceChildLabels `yaml:"childLabels"` + } + + var c *yamlConfig + err = yaml.Unmarshal(b, &c) + if err != nil { + setupLog.Error(err, "Failed to YAML unmarshal controller config", "configPath", configPath) + return ControllerConfig{} + } + + if c == nil { + return ControllerConfig{} + } + + childLabels := make(map[k8sclient.GroupVersionKind]map[string]string) + for _, label := range c.ChildLabels { + childLabels[k8sclient.GroupVersionKind{ + Group: label.Group, + Version: label.Version, + Kind: label.Kind, + }] = label.MatchLabels + } + + return ControllerConfig{ + ChildLabels: childLabels, + } +} diff --git a/cyclops-ctrl/cmd/main/main.go b/cyclops-ctrl/cmd/main/main.go index b8b5cbc4..9b6abc37 100644 --- a/cyclops-ctrl/cmd/main/main.go +++ b/cyclops-ctrl/cmd/main/main.go @@ -71,7 +71,9 @@ func main() { ) telemetryClient.InstanceStart() - k8sClient, err := k8sclient.New(getCustomChildLabels()) + config := getConfig() + + k8sClient, err := k8sclient.New(config.ChildLabels) if err != nil { fmt.Println("error bootstrapping Kubernetes client", err) panic(err) diff --git a/cyclops-ctrl/config.yaml b/cyclops-ctrl/config.yaml new file mode 100644 index 00000000..1b4df7bc --- /dev/null +++ b/cyclops-ctrl/config.yaml @@ -0,0 +1,6 @@ +childLabels: + - group: postgresql.cnpg.io + version: v1 + kind: Cluster + matchLabels: + cnpg.io/cluster: "{{ .metadata.name }}" diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go b/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go new file mode 100644 index 00000000..d235ed2c --- /dev/null +++ b/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go @@ -0,0 +1,64 @@ +package k8sclient + +import ( + "bytes" + "fmt" + "text/template" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +const ( + cyclopsNamespace = "cyclops" +) + +type GroupVersionKind struct { + Group string `json:"group"` + Version string `json:"version"` + Kind string `json:"kind"` +} + +type ChildLabels map[GroupVersionKind]map[string]string + +func (k *KubernetesClient) getChildLabel( + group, version, kind string, + obj *unstructured.Unstructured, +) (map[string]string, bool, error) { + labels, exists := k.childLabels[GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }] + + fmt.Println("all", k.childLabels, labels, exists, GroupVersionKind{ + Group: group, + Version: version, + Kind: kind, + }) + + if !exists { + return nil, false, nil + } + + matchLabels := make(map[string]string) + for k, v := range labels { + fmt.Println("parsing", k, v) + t, err := template.New("matchLabel").Parse(v) + if err != nil { + fmt.Println("err1", err) + return nil, false, err + } + + var o bytes.Buffer + err = t.Execute(&o, obj.Object) + if err != nil { + fmt.Println("err2", err) + return nil, false, err + } + + fmt.Println("gotov", o.String()) + matchLabels[k] = o.String() + } + + return matchLabels, exists, nil +} diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/client.go b/cyclops-ctrl/pkg/cluster/k8sclient/client.go index 030af532..f6f1e81a 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/client.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/client.go @@ -2,8 +2,6 @@ package k8sclient import ( "context" - "fmt" - apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -19,20 +17,16 @@ import ( "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/models/dto" ) -const ( - cyclopsNamespace = "cyclops" -) - type KubernetesClient struct { Dynamic dynamic.Interface clientset *kubernetes.Clientset discovery *discovery.DiscoveryClient moduleset *client.CyclopsV1Alpha1Client - childLabels map[string]string + childLabels ChildLabels } -func New(childLabels map[string]string) (*KubernetesClient, error) { +func New(childLabels ChildLabels) (*KubernetesClient, error) { config := ctrl.GetConfigOrDie() clientset, err := kubernetes.NewForConfig(config) if err != nil { @@ -60,13 +54,6 @@ func New(childLabels map[string]string) (*KubernetesClient, error) { }, nil } -func (k *KubernetesClient) getChildLabel(group, version, kind string) (string, bool) { - key := fmt.Sprintf("%s.%s/%s", kind, group, version) - label, exists := k.childLabels[key] - - return label, exists -} - type IKubernetesClient interface { GetStreamedPodLogs(ctx context.Context, namespace, container, name string, logCount *int64, logChan chan<- string) error GetPodLogs(namespace, container, name string, numLogs *int64) ([]string, error) diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/modules.go b/cyclops-ctrl/pkg/cluster/k8sclient/modules.go index 3b9d9fdf..e238e25f 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/modules.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/modules.go @@ -107,7 +107,7 @@ func (k *KubernetesClient) GetResourcesForModule(name string) ([]dto.Resource, e return out, nil } -func (k *KubernetesClient) GetResourcesForCRD(label, name string) ([]dto.Resource, error) { +func (k *KubernetesClient) GetResourcesForCRD(childLabels map[string]string, name string) ([]dto.Resource, error) { out := make([]dto.Resource, 0, 0) managedGVRs, err := k.getManagedGVRs("") @@ -118,7 +118,7 @@ func (k *KubernetesClient) GetResourcesForCRD(label, name string) ([]dto.Resourc other := make([]unstructured.Unstructured, 0) for _, gvr := range managedGVRs { rs, err := k.Dynamic.Resource(gvr).List(context.Background(), metav1.ListOptions{ - LabelSelector: label + "=" + name, + LabelSelector: labels.Set(childLabels).String(), }) if err != nil { continue diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/resources.go b/cyclops-ctrl/pkg/cluster/k8sclient/resources.go index f4ba0b14..7aa98545 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/resources.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/resources.go @@ -149,12 +149,30 @@ func (k *KubernetesClient) GetResource(group, version, kind, name, namespace str } func (k *KubernetesClient) mapDefaultResource(group, version, kind, name, namespace string) (any, error) { - var children []dto.Resource - var err error + apiResourceName, err := k.GVKtoAPIResourceName(schema.GroupVersion{Group: group, Version: version}, kind) + if err != nil { + return nil, err + } + + resource, err := k.Dynamic.Resource(schema.GroupVersionResource{ + Group: group, + Version: version, + Resource: apiResourceName, + }).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{}) + if err != nil { + return "", err + } + + childLabels, exists, err := k.getChildLabel(group, version, kind, resource) + if err != nil { + return nil, err + } - childLabel, exists := k.getChildLabel(group, version, kind) + fmt.Println("childLabels", childLabels) + + var children []dto.Resource if exists { - children, err = k.GetResourcesForCRD(childLabel, name) + children, err = k.GetResourcesForCRD(childLabels, name) if err != nil { return nil, err } diff --git a/install/cyclops-install.yaml b/install/cyclops-install.yaml index 9fc8fe6f..274344ef 100644 --- a/install/cyclops-install.yaml +++ b/install/cyclops-install.yaml @@ -427,6 +427,20 @@ spec: policyTypes: - Ingress - Egress +#--- +#apiVersion: v1 +#kind: ConfigMap +#metadata: +# name: cyclops-ctrl +# namespace: cyclops +#data: +# config.yaml: | +# childLabels: +# - group: postgresql.cnpg.io +# version: v1 +# kind: Cluster +# matchLabels: +# cnpg.io/cluster: "{{ .metadata.name }}" --- apiVersion: apps/v1 kind: Deployment @@ -454,6 +468,8 @@ spec: env: - name: PORT value: "8080" + - name: DISABLE_TELEMETRY + value: "true" livenessProbe: httpGet: path: /healthz @@ -466,6 +482,16 @@ spec: port: 8082 initialDelaySeconds: 5 periodSeconds: 10 +# volumeMounts: +# - name: cyclops-ctrl-config +# mountPath: /etc/config +# volumes: +# - name: cyclops-ctrl-config +# configMap: +# name: cyclops-ctrl +# items: +# - key: config.yaml +# path: config.yaml --- apiVersion: v1 kind: Service From 85e8c24ebd8be08199eeb8575b815c8c3671de9f Mon Sep 17 00:00:00 2001 From: petar-cvit Date: Wed, 16 Oct 2024 22:11:40 +0200 Subject: [PATCH 6/8] remove print --- cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go | 11 ----------- cyclops-ctrl/pkg/cluster/k8sclient/resources.go | 2 -- 2 files changed, 13 deletions(-) diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go b/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go index d235ed2c..5cfe358a 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go @@ -2,7 +2,6 @@ package k8sclient import ( "bytes" - "fmt" "text/template" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -30,33 +29,23 @@ func (k *KubernetesClient) getChildLabel( Kind: kind, }] - fmt.Println("all", k.childLabels, labels, exists, GroupVersionKind{ - Group: group, - Version: version, - Kind: kind, - }) - if !exists { return nil, false, nil } matchLabels := make(map[string]string) for k, v := range labels { - fmt.Println("parsing", k, v) t, err := template.New("matchLabel").Parse(v) if err != nil { - fmt.Println("err1", err) return nil, false, err } var o bytes.Buffer err = t.Execute(&o, obj.Object) if err != nil { - fmt.Println("err2", err) return nil, false, err } - fmt.Println("gotov", o.String()) matchLabels[k] = o.String() } diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/resources.go b/cyclops-ctrl/pkg/cluster/k8sclient/resources.go index 7aa98545..321f46e5 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/resources.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/resources.go @@ -168,8 +168,6 @@ func (k *KubernetesClient) mapDefaultResource(group, version, kind, name, namesp return nil, err } - fmt.Println("childLabels", childLabels) - var children []dto.Resource if exists { children, err = k.GetResourcesForCRD(childLabels, name) From 9b9480d3fe20c5d6caa2d5767b2de9f25162a2d1 Mon Sep 17 00:00:00 2001 From: petar-cvit Date: Tue, 29 Oct 2024 15:48:33 +0100 Subject: [PATCH 7/8] fetch resource relations from configmap --- cyclops-ctrl/cmd/main/main.go | 4 +- .../pkg/cluster/k8sclient/childlabels.go | 48 +++++++++++++++++++ cyclops-ctrl/pkg/cluster/k8sclient/client.go | 19 ++++---- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/cyclops-ctrl/cmd/main/main.go b/cyclops-ctrl/cmd/main/main.go index 9b6abc37..9e2265da 100644 --- a/cyclops-ctrl/cmd/main/main.go +++ b/cyclops-ctrl/cmd/main/main.go @@ -71,9 +71,7 @@ func main() { ) telemetryClient.InstanceStart() - config := getConfig() - - k8sClient, err := k8sclient.New(config.ChildLabels) + k8sClient, err := k8sclient.New() if err != nil { fmt.Println("error bootstrapping Kubernetes client", err) panic(err) diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go b/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go index 5cfe358a..fc25fdcd 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go @@ -2,8 +2,12 @@ package k8sclient import ( "bytes" + "context" "text/template" + "gopkg.in/yaml.v3" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -51,3 +55,47 @@ func (k *KubernetesClient) getChildLabel( return matchLabels, exists, nil } + +func (k *KubernetesClient) loadResourceRelationsLabels() { + configmap, err := k.clientset.CoreV1().ConfigMaps("cyclops").Get(context.Background(), "cyclops-ctrl", v1.GetOptions{}) + if err != nil { + return + } + + d, ok := configmap.Data["resource-relations"] + if !ok { + return + } + + type resourceChildLabels struct { + Group string `yaml:"group"` + Version string `yaml:"version"` + Kind string `yaml:"kind"` + MatchLabels map[string]string `yaml:"matchLabels"` + } + + type yamlConfig struct { + ChildLabels []resourceChildLabels `yaml:"childLabels"` + } + + var c *yamlConfig + err = yaml.Unmarshal([]byte(d), &c) + if err != nil { + return + } + + if c == nil { + return + } + + childLabels := make(map[GroupVersionKind]map[string]string) + for _, label := range c.ChildLabels { + childLabels[GroupVersionKind{ + Group: label.Group, + Version: label.Version, + Kind: label.Kind, + }] = label.MatchLabels + } + + k.childLabels = childLabels +} diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/client.go b/cyclops-ctrl/pkg/cluster/k8sclient/client.go index f6f1e81a..4831df2f 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/client.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/client.go @@ -26,7 +26,7 @@ type KubernetesClient struct { childLabels ChildLabels } -func New(childLabels ChildLabels) (*KubernetesClient, error) { +func New() (*KubernetesClient, error) { config := ctrl.GetConfigOrDie() clientset, err := kubernetes.NewForConfig(config) if err != nil { @@ -45,13 +45,16 @@ func New(childLabels ChildLabels) (*KubernetesClient, error) { panic(err.Error()) } - return &KubernetesClient{ - Dynamic: dynamic, - discovery: discovery, - clientset: clientset, - moduleset: moduleSet, - childLabels: childLabels, - }, nil + k := &KubernetesClient{ + Dynamic: dynamic, + discovery: discovery, + clientset: clientset, + moduleset: moduleSet, + } + + k.loadResourceRelationsLabels() + + return k, nil } type IKubernetesClient interface { From f220b9ed9a6a99319a771a94945f129f0486eeb6 Mon Sep 17 00:00:00 2001 From: petar-cvit Date: Tue, 29 Oct 2024 15:49:11 +0100 Subject: [PATCH 8/8] remove volume mount --- install/cyclops-install.yaml | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/install/cyclops-install.yaml b/install/cyclops-install.yaml index 274344ef..b0b6a870 100644 --- a/install/cyclops-install.yaml +++ b/install/cyclops-install.yaml @@ -427,14 +427,14 @@ spec: policyTypes: - Ingress - Egress -#--- -#apiVersion: v1 -#kind: ConfigMap -#metadata: -# name: cyclops-ctrl -# namespace: cyclops +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cyclops-ctrl + namespace: cyclops #data: -# config.yaml: | +# resource-relations: | # childLabels: # - group: postgresql.cnpg.io # version: v1 @@ -482,16 +482,6 @@ spec: port: 8082 initialDelaySeconds: 5 periodSeconds: 10 -# volumeMounts: -# - name: cyclops-ctrl-config -# mountPath: /etc/config -# volumes: -# - name: cyclops-ctrl-config -# configMap: -# name: cyclops-ctrl -# items: -# - key: config.yaml -# path: config.yaml --- apiVersion: v1 kind: Service