Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

πŸš€ feat: adds ReplicaSet in Deployments #656

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions cyclops-ctrl/internal/models/dto/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ type Resource interface {
}

type Deployment struct {
Group string `json:"group"`
Version string `json:"version"`
Kind string `json:"kind"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Replicas int `json:"replicas"`
Pods []Pod `json:"pods"`
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"`
Replicas int `json:"replicas"`
Pods []Pod `json:"pods"`
Status string `json:"status"`
Deleted bool `json:"deleted"`
ActiveReplicaSet string `json:"activeReplicaSet"`
}

func (d *Deployment) GetGroupVersionKind() string {
Expand Down Expand Up @@ -222,6 +223,7 @@ type Pod struct {
Status bool `json:"status"`
Started *metav1.Time `json:"started"`
Deleted bool `json:"deleted"`
ReplicaSet string `json:"replicaSet"`
}

func (p *Pod) GetGroupVersionKind() string {
Expand Down
43 changes: 35 additions & 8 deletions cyclops-ctrl/pkg/cluster/k8sclient/mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package k8sclient

import (
"context"
"strings"

appsv1 "k8s.io/api/apps/v1"
apiv1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -22,14 +25,15 @@ func (k *KubernetesClient) mapDeployment(group, version, kind, name, namespace s
}

return &dto.Deployment{
Group: group,
Version: version,
Kind: kind,
Name: deployment.Name,
Namespace: deployment.Namespace,
Replicas: int(*deployment.Spec.Replicas),
Pods: pods,
Status: getDeploymentStatus(deployment),
Group: group,
Version: version,
Kind: kind,
Name: deployment.Name,
Namespace: deployment.Namespace,
Replicas: int(*deployment.Spec.Replicas),
Pods: pods,
Status: getDeploymentStatus(deployment),
ActiveReplicaSet: getActiveReplicaSet(deployment.Status.Conditions),
}, nil
}

Expand Down Expand Up @@ -455,3 +459,26 @@ func mapIPBlock(block *networkingv1.IPBlock) *dto.IPBlock {
Except: block.Except,
}
}

func getActiveReplicaSet(conditions []appsv1.DeploymentCondition) string {
var latestProgressingCondition *appsv1.DeploymentCondition

for _, condition := range conditions {
if condition.Type == appsv1.DeploymentProgressing {
if latestProgressingCondition == nil || condition.LastTransitionTime.After(latestProgressingCondition.LastTransitionTime.Time) {
latestProgressingCondition = &condition
}
}
}

if latestProgressingCondition != nil {
if strings.Contains(latestProgressingCondition.Message, "ReplicaSet") {
parts := strings.Split(latestProgressingCondition.Message, "\"")
if len(parts) > 1 {
return parts[1] // ReplicaSet \".+\" is progressing. status: Progressing
}
}
}

return ""
}
11 changes: 11 additions & 0 deletions cyclops-ctrl/pkg/cluster/k8sclient/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ func (k *KubernetesClient) getPods(deployment appsv1.Deployment) ([]dto.Pod, err
Node: item.Spec.NodeName,
PodPhase: string(item.Status.Phase),
Started: item.Status.StartTime,
ReplicaSet: getReplicaSetOwner(&item),
})
}

Expand Down Expand Up @@ -841,6 +842,16 @@ func containerStatus(status apiv1.ContainerStatus) dto.ContainerStatus {
}
}

func getReplicaSetOwner(pod *apiv1.Pod) string {
for _, reference := range pod.OwnerReferences {
if reference.Kind == "ReplicaSet" {
return reference.Name
}
}

return ""
}

func getDeploymentStatus(deployment *appsv1.Deployment) string {
if isDeploymentProgressing(deployment.Status.Conditions) {
return statusProgressing
Expand Down
22 changes: 15 additions & 7 deletions cyclops-ui/src/components/k8s-resources/Deployment.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useCallback, useEffect, useState } from "react";
import { Col, Divider, Row, Alert } from "antd";
import { Alert, Col, Divider, Row } from "antd";
import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { isStreamingEnabled } from "../../utils/api/common";
import { mapResponseError } from "../../utils/api/errors";
import PodTable from "./common/PodTable/PodTable";
import { isStreamingEnabled } from "../../utils/api/common";
import ReplicaSetProgress from "./common/ReplicaSetProgress";

interface Props {
name: string;
Expand All @@ -15,6 +16,8 @@ const Deployment = ({ name, namespace, workload }: Props) => {
const [deployment, setDeployment] = useState({
status: "",
pods: [],
replicas: 0,
activeReplicaSet: "",
});
const [error, setError] = useState({
message: "",
Expand Down Expand Up @@ -43,10 +46,6 @@ const Deployment = ({ name, namespace, workload }: Props) => {
useEffect(() => {
fetchDeployment();

if (isStreamingEnabled()) {
return;
}

const interval = setInterval(() => fetchDeployment(), 15000);
return () => {
clearInterval(interval);
Expand All @@ -71,6 +70,8 @@ const Deployment = ({ name, namespace, workload }: Props) => {
return 0;
}

const { pods, replicas, activeReplicaSet } = deployment;

return (
<div>
{error.message.length !== 0 && (
Expand All @@ -97,6 +98,13 @@ const Deployment = ({ name, namespace, workload }: Props) => {
Replicas: {getPodsLength()}
</Divider>
<Col span={24} style={{ overflowX: "auto" }}>
{replicas && activeReplicaSet && (
<ReplicaSetProgress
pods={pods}
replicas={replicas}
activeReplicaSet={activeReplicaSet}
/>
)}
<PodTable
namespace={namespace}
pods={getPods()}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CheckCircleOutlined, SyncOutlined } from "@ant-design/icons";
import { Progress, Tooltip } from "antd";

interface ReplicaSetType {
pods: any[];
replicas: number;
activeReplicaSet: string;
}

const ReplicaSetProgress = ({
pods,
replicas,
activeReplicaSet,
}: ReplicaSetType) => {
// Healthy Replicas (matching replicasets pods) / Total Replicas (defined in the deployment)
const totalReplicas = replicas;

const healthyReplicas = pods.filter((elem) => {
return elem.podPhase === "Running" && elem.replicaSet === activeReplicaSet;
}).length;

const healthyReplicasPercent = (healthyReplicas / totalReplicas) * 100;

return (
<div
style={{
paddingInline: "3rem",
paddingBottom: "1rem",
}}
>
<Tooltip title={`${healthyReplicas} healthy / ${totalReplicas} total`}>
<Progress
percent={100}
success={{ percent: healthyReplicasPercent }}
format={() => {
return healthyReplicasPercent === 100 ? (
<CheckCircleOutlined />
) : (
<SyncOutlined spin />
);
}}
/>
</Tooltip>
</div>
);
};

export default ReplicaSetProgress;
40 changes: 20 additions & 20 deletions cyclops-ui/src/components/pages/ModuleDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import React, { useEffect, useState, useCallback } from "react";
import {
BookOutlined,
CheckCircleTwoTone,
ClockCircleTwoTone,
CloseSquareTwoTone,
CopyOutlined,
DeleteOutlined,
EditOutlined,
FileTextOutlined,
UndoOutlined,
} from "@ant-design/icons";
import "ace-builds/src-noconflict/ace";
import {
Alert,
Button,
Expand All @@ -13,40 +24,29 @@ import {
Tooltip,
Typography,
} from "antd";
import "ace-builds/src-noconflict/ace";
import { useParams } from "react-router-dom";
import axios from "axios";
import {
BookOutlined,
CheckCircleTwoTone,
ClockCircleTwoTone,
CloseSquareTwoTone,
CopyOutlined,
DeleteOutlined,
EditOutlined,
FileTextOutlined,
UndoOutlined,
} from "@ant-design/icons";
import { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import "./custom.css";

import "ace-builds/src-noconflict/mode-jsx";
import ReactAce from "react-ace";

import {
moduleTemplateReferenceView,
templateRef,
} from "../../utils/templateRef";
import { mapResponseError } from "../../utils/api/errors";
import YAML from "yaml";
import { isStreamingEnabled } from "../../utils/api/common";
import { mapResponseError } from "../../utils/api/errors";
import { resourcesStream } from "../../utils/api/sse/resources";
import { Workload } from "../../utils/k8s/workload";
import {
isWorkload,
ResourceRef,
resourceRefKey,
} from "../../utils/resourceRef";
import {
moduleTemplateReferenceView,
templateRef,
} from "../../utils/templateRef";
import ResourceList from "../k8s-resources/ResourceList/ResourceList";
import { Workload } from "../../utils/k8s/workload";

const languages = [
"javascript",
Expand Down
Loading