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: Add node drain functionality to dashboard #1376

Merged
Show file tree
Hide file tree
Changes from 15 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
42 changes: 35 additions & 7 deletions assets/schema/kubernetes-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3823,7 +3823,7 @@ type v1_ConfigMapKeySelector {
"""The key to select."""
key: String!
"""
Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
"""
name: String
"""Specify whether the ConfigMap or its key must be defined"""
Expand Down Expand Up @@ -3863,7 +3863,7 @@ type v1_SecretKeySelector {
"""The key of the secret to select from. Must be a valid secret key."""
key: String!
"""
Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
"""
name: String
"""Specify whether the Secret or its key must be defined"""
Expand Down Expand Up @@ -4335,7 +4335,7 @@ LocalObjectReference contains enough information to let you locate the reference
"""
type v1_LocalObjectReference {
"""
Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
"""
name: String
}
Expand Down Expand Up @@ -4374,7 +4374,7 @@ type v1_ConfigMapVolumeSource {
"""
items: [v1_KeyToPath]
"""
Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
"""
name: String
"""optional specify whether the ConfigMap or its keys must be defined"""
Expand Down Expand Up @@ -4855,7 +4855,7 @@ type v1_ConfigMapProjection {
"""
items: [v1_KeyToPath]
"""
Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
"""
name: String
"""optional specify whether the ConfigMap or its keys must be defined"""
Expand All @@ -4881,7 +4881,7 @@ type v1_SecretProjection {
"""
items: [v1_KeyToPath]
"""
Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
Name of the referent. This field is effectively required, but due to backwards compatibility is allowed to be empty. Instances of this type with an empty value here are almost certainly wrong. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
"""
name: String
"""optional field specify whether the Secret or its key must be defined"""
Expand Down Expand Up @@ -5554,6 +5554,26 @@ type Mutation {
aggregations: String
input: namespace_NamespaceSpec_Input!
): namespace_NamespaceSpec! @httpOperation(subgraph: "api", path: "/api/v1/namespace", operationSpecificHeaders: "{\"Content-Type\":\"application/json\",\"Accept\":\"application/json\"}", httpMethod: POST, queryParamArgMap: "{\"filterBy\":\"filterBy\",\"sortBy\":\"sortBy\",\"itemsPerPage\":\"itemsPerPage\",\"page\":\"page\",\"metricNames\":\"metricNames\",\"aggregations\":\"aggregations\"}")
"""drains Node"""
handleNodeDrain(
"""name of the Node"""
name: String!
"""
Comma delimited string used to apply filtering: 'propertyName,filterValue'
"""
filterBy: String
"""Name of the column to sort by"""
sortBy: String
"""Number of items to return when pagination is applied"""
itemsPerPage: String
"""Page number to return items from"""
page: String
"""Metric names to download"""
metricNames: String
"""Aggregations to be performed for each metric (default: sum)"""
aggregations: String
input: node_NodeDrainSpec_Input!
): JSON @httpOperation(subgraph: "api", path: "/api/v1/node/{args.name}/drain", operationSpecificHeaders: "{\"Content-Type\":\"application/json\",\"Accept\":\"application/json\"}", httpMethod: PUT, queryParamArgMap: "{\"filterBy\":\"filterBy\",\"sortBy\":\"sortBy\",\"itemsPerPage\":\"itemsPerPage\",\"page\":\"page\",\"metricNames\":\"metricNames\",\"aggregations\":\"aggregations\"}")
"""scales ReplicationController to a number of replicas"""
handleUpdateReplicasCount(
"""namespace of the ReplicationController"""
Expand Down Expand Up @@ -5774,6 +5794,14 @@ input namespace_NamespaceSpec_Input {
name: String!
}

input node_NodeDrainSpec_Input {
deleteEmptyDirData: Boolean
force: Boolean
gracePeriodSeconds: Int
ignoreAllDaemonSets: Boolean
maciaszczykm marked this conversation as resolved.
Show resolved Hide resolved
timeout: BigInt
}

input replicationcontroller_ReplicationControllerSpec_Input {
replicas: Int!
}
Expand Down Expand Up @@ -5817,4 +5845,4 @@ type StringWrapper {

type StateWrapper {
state: ContainerState!
}
}
12 changes: 10 additions & 2 deletions assets/src/components/kubernetes/cluster/Node.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { ReactElement, useMemo } from 'react'
import {
Card,
Chip,
ChipList,
SidecarItem,
Table,
Expand Down Expand Up @@ -98,9 +99,16 @@ export default function Node(): ReactElement {
<ResourceReadyChip ready={node?.ready} />
</SidecarItem>
<SidecarItem heading="Unschedulable">
{node?.unschedulable ? 'True' : 'False'}
<Chip
size="small"
severity={node?.unschedulable ? 'danger' : 'success'}
>
{node?.unschedulable ? 'True' : 'False'}
</Chip>
</SidecarItem>
<SidecarItem heading="Pod CIDR">{node?.podCIDR}</SidecarItem>
{node.podCIDR && (
<SidecarItem heading="Pod CIDR">{node?.podCIDR}</SidecarItem>
)}
</MetadataSidecar>
}
>
Expand Down
59 changes: 57 additions & 2 deletions assets/src/components/kubernetes/cluster/Nodes.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { createColumnHelper } from '@tanstack/react-table'
import { useMemo } from 'react'
import { useMemo, useState } from 'react'
import { filesize } from 'filesize'
import { useSetBreadcrumbs } from '@pluralsh/design-system'
import {
IconFrame,
SortDescIcon,
useSetBreadcrumbs,
} from '@pluralsh/design-system'

import {
DrainNodeMutationVariables,
Maybe,
Node_NodeList as NodeListT,
Node_Node as NodeT,
NodesQuery,
NodesQueryVariables,
useDrainNodeMutation,
useNodesQuery,
} from '../../../generated/graphql-kubernetes'
import { ResourceReadyChip, useDefaultColumns } from '../common/utils'
Expand All @@ -22,6 +28,10 @@ import {
} from '../../../routes/kubernetesRoutesConsts'
import { useCluster } from '../Cluster'

import { Confirm } from '../../utils/Confirm'

import { KubernetesClient } from '../../../helpers/kubernetes.client'

import { getClusterBreadcrumbs } from './Cluster'

export const getBreadcrumbs = (cluster?: Maybe<KubernetesClusterFragment>) => [
Expand Down Expand Up @@ -103,6 +113,50 @@ const colPods = columnHelper.accessor((node) => node?.allocatedResources, {
},
})

const colActions = columnHelper.accessor(() => null, {
id: 'actions',
header: '',
cell: function Cell({ row: { original } }) {
const cluster = useCluster()
const [open, setOpen] = useState(false)
const [mutation, { loading, error }] = useDrainNodeMutation({
client: KubernetesClient(cluster?.id ?? ''),
variables: {
name: original.objectMeta.name ?? '',
input: {},
} as DrainNodeMutationVariables,
onCompleted: () => setOpen(false),
})

return (
<>
<IconFrame
clickable
icon={<SortDescIcon color="icon-danger" />}
tooltip="Drain node"
onClick={(e) => {
e.stopPropagation()
setOpen(true)
}}
/>
{open && (
<Confirm
close={() => setOpen(false)}
destructive
label="Drain node"
loading={loading}
error={error}
open={open}
submit={() => mutation()}
title="Drain node"
text={`Are you sure you want to drain ${original?.objectMeta.name} node? Node will be cordoned first. Please note that it may take a while to complete.`}
/>
)}
</>
)
},
})

export default function Nodes() {
const cluster = useCluster()

Expand All @@ -119,6 +173,7 @@ export default function Nodes() {
colPods,
colLabels,
colCreationTimestamp,
colActions,
],
[colName, colLabels, colCreationTimestamp]
)
Expand Down
6 changes: 5 additions & 1 deletion assets/src/components/kubernetes/common/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import moment from 'moment/moment'
import yaml from 'js-yaml'
import { capitalize } from 'lodash'

import { ChipProps } from '@pluralsh/design-system/dist/components/Chip'

import {
Types_ListMeta as ListMetaT,
Maybe,
Expand Down Expand Up @@ -110,9 +112,10 @@ const resourceConditionSeverity = {

export function ResourceReadyChip({
ready,
...props
}: {
ready: string | boolean | undefined
}) {
} & ChipProps) {
if (ready === undefined) return undefined

const r = ready.toString()
Expand All @@ -122,6 +125,7 @@ export function ResourceReadyChip({
<Chip
size="small"
severity={severity}
{...props}
>
{capitalize(r)}
</Chip>
Expand Down
Loading
Loading