diff --git a/cyclops-ctrl/.env b/cyclops-ctrl/.env index 78d4024a..b49b53cd 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 +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 2f549a36..9e2265da 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" @@ -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/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/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/childlabels.go b/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go new file mode 100644 index 00000000..fc25fdcd --- /dev/null +++ b/cyclops-ctrl/pkg/cluster/k8sclient/childlabels.go @@ -0,0 +1,101 @@ +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" +) + +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, + }] + + if !exists { + return nil, false, nil + } + + matchLabels := make(map[string]string) + for k, v := range labels { + t, err := template.New("matchLabel").Parse(v) + if err != nil { + return nil, false, err + } + + var o bytes.Buffer + err = t.Execute(&o, obj.Object) + if err != nil { + return nil, false, err + } + + matchLabels[k] = o.String() + } + + 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 54cc96ed..4831df2f 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/client.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/client.go @@ -2,7 +2,6 @@ package k8sclient import ( "context" - apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -18,25 +17,16 @@ import ( "github.com/cyclops-ui/cyclops/cyclops-ctrl/internal/models/dto" ) -const ( - cyclopsNamespace = "cyclops" -) - type KubernetesClient struct { - Dynamic dynamic.Interface - + Dynamic dynamic.Interface clientset *kubernetes.Clientset - discovery *discovery.DiscoveryClient - moduleset *client.CyclopsV1Alpha1Client -} -func New() (*KubernetesClient, error) { - return createLocalClient() + childLabels ChildLabels } -func createLocalClient() (*KubernetesClient, error) { +func New() (*KubernetesClient, error) { config := ctrl.GetConfigOrDie() clientset, err := kubernetes.NewForConfig(config) if err != nil { @@ -55,12 +45,16 @@ func createLocalClient() (*KubernetesClient, error) { panic(err.Error()) } - return &KubernetesClient{ + k := &KubernetesClient{ Dynamic: dynamic, discovery: discovery, clientset: clientset, moduleset: moduleSet, - }, nil + } + + k.loadResourceRelationsLabels() + + return k, nil } type IKubernetesClient interface { diff --git a/cyclops-ctrl/pkg/cluster/k8sclient/modules.go b/cyclops-ctrl/pkg/cluster/k8sclient/modules.go index e7902a09..e238e25f 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(childLabels map[string]string, 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: labels.Set(childLabels).String(), + }) + 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..321f46e5 100644 --- a/cyclops-ctrl/pkg/cluster/k8sclient/resources.go +++ b/cyclops-ctrl/pkg/cluster/k8sclient/resources.go @@ -141,11 +141,51 @@ 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) { + 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 + } + + var children []dto.Resource + if exists { + children, err = k.GetResourcesForCRD(childLabels, 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..5373ca05 --- /dev/null +++ b/cyclops-ui/src/components/k8s-resources/DefaultResource.tsx @@ -0,0 +1,106 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { 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, group, version, kind]); + + 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} -

- -