From 1f3d57afae51c11bab7ccfd8bd4057b320d12ee4 Mon Sep 17 00:00:00 2001 From: zherman0 Date: Tue, 2 Jul 2019 11:58:09 -0600 Subject: [PATCH] Remove Volume feature --- frontend/public/components/daemon-set.jsx | 2 +- .../public/components/deployment-config.tsx | 2 +- frontend/public/components/deployment.tsx | 2 +- frontend/public/components/modals/index.ts | 3 + .../components/modals/remove-volume-modal.tsx | 86 +++++++ frontend/public/components/pod.tsx | 2 +- frontend/public/components/replicaset.jsx | 2 +- .../components/replication-controller.jsx | 2 +- frontend/public/components/stateful-set.jsx | 2 +- frontend/public/components/volumes-table.tsx | 224 +++++++++++------- 10 files changed, 239 insertions(+), 88 deletions(-) create mode 100644 frontend/public/components/modals/remove-volume-modal.tsx diff --git a/frontend/public/components/daemon-set.jsx b/frontend/public/components/daemon-set.jsx index 3c28546152a..b693c7c62c3 100644 --- a/frontend/public/components/daemon-set.jsx +++ b/frontend/public/components/daemon-set.jsx @@ -120,7 +120,7 @@ const Details = ({obj: daemonset}) =>
- +
; diff --git a/frontend/public/components/deployment-config.tsx b/frontend/public/components/deployment-config.tsx index a63ccf8bcff..7b50d7d1383 100644 --- a/frontend/public/components/deployment-config.tsx +++ b/frontend/public/components/deployment-config.tsx @@ -155,7 +155,7 @@ export const DeploymentConfigsDetails: React.FC<{obj: K8sResourceKind}> = ({obj:
- +
diff --git a/frontend/public/components/deployment.tsx b/frontend/public/components/deployment.tsx index bd20250cf32..6e228e67b79 100644 --- a/frontend/public/components/deployment.tsx +++ b/frontend/public/components/deployment.tsx @@ -114,7 +114,7 @@ const DeploymentDetails: React.FC = ({obj: deployment})
- +
diff --git a/frontend/public/components/modals/index.ts b/frontend/public/components/modals/index.ts index 7c1352c5746..55cd9664bdf 100644 --- a/frontend/public/components/modals/index.ts +++ b/frontend/public/components/modals/index.ts @@ -63,5 +63,8 @@ export const installPlanPreviewModal = (props) => import('./installplan-preview- export const expandPVCModal = (props) => import('./expand-pvc-modal' /* webpackChunkName: "expand-pvc-modal" */) .then(m => m.expandPVCModal(props)); +export const removeVolumeModal = (props) => import('./remove-volume-modal' /* webpackChunkName: "remove-volume-modal" */) + .then(m => m.removeVolumeModal(props)); + export const configureMachineAutoscalerModal = (props) => import('./configure-machine-autoscaler-modal' /* webpackChunkName: "configure-machine-autoscaler-modal" */) .then(m => m.configureMachineAutoscalerModal(props)); diff --git a/frontend/public/components/modals/remove-volume-modal.tsx b/frontend/public/components/modals/remove-volume-modal.tsx new file mode 100644 index 00000000000..6566f19ad4c --- /dev/null +++ b/frontend/public/components/modals/remove-volume-modal.tsx @@ -0,0 +1,86 @@ +import * as _ from 'lodash-es'; +import * as React from 'react'; + +import { createModalLauncher, ModalTitle, ModalBody, ModalSubmitFooter } from '../factory'; +import { ContainerSpec, getVolumeType, K8sKind, k8sPatch, K8sResourceKind, Volume, VolumeMount } from '../../module/k8s/'; +import { RowVolumeData } from '../volumes-table'; + +export const RemoveVolumeModal: React.FC = (props) => { + const [inProgress, setInProgress] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + + const getRemoveVolumePatch = (resource: K8sResourceKind, rowVolumeData: RowVolumeData) => { + const containers: ContainerSpec[] = _.get(resource, 'spec.template.spec.containers', []); + const patches = []; + let allowRemoveVolume = true; + containers.forEach((container: ContainerSpec, i: number) => { + const mounts: VolumeMount[] = _.get(container, 'volumeMounts', []); + mounts.forEach((mount: VolumeMount, j: number) => { + if (mount.name !== rowVolumeData.name) { + return; + } + if (mount.mountPath === rowVolumeData.mountPath) { + patches.push({op: 'remove', path: `/spec/template/spec/containers/${i}/volumeMounts/${j}`}); + } else { + allowRemoveVolume = false; + } + }); + }); + + // if the mountCount is greater than zero, then the volume is still being used at a different mount point or in a different container + // Either way, we cannot give the cmd to remove it + if (allowRemoveVolume) { + const volumes: Volume[] = _.get(resource, 'spec.template.spec.volumes', []); + const volumeIndex = volumes.findIndex((v: Volume) => v.name === rowVolumeData.volumeDetail.name); + patches.push({op: 'remove', path: `/spec/template/spec/volumes/${volumeIndex}`}); + } + return patches; + }; + + const submit = (event: React.FormEvent) => { + event.preventDefault(); + setErrorMessage(''); + setInProgress(true); + const { kind, resource, volume } = props; + k8sPatch(kind, resource, getRemoveVolumePatch(resource, volume)).then(() => { + setInProgress(false); + props.close(); + }).catch(({message: errMessage}) => { + setErrorMessage(errMessage); + setInProgress(false); + }); + }; + + const {kind, resource, volume} = props; + const type: string = _.get(getVolumeType(volume.volumeDetail), 'id', ''); + return
+ Remove Volume + +
+
+
+ + ; +}; + +export const removeVolumeModal = createModalLauncher(RemoveVolumeModal); + +export type RemoveVolumeModalProps = { + cancel: (e: Event) => void; + close: () => void; + volume: RowVolumeData; + kind: K8sKind; + resource: K8sResourceKind; +}; diff --git a/frontend/public/components/pod.tsx b/frontend/public/components/pod.tsx index 31e1d1a2460..30585d55c55 100644 --- a/frontend/public/components/pod.tsx +++ b/frontend/public/components/pod.tsx @@ -276,7 +276,7 @@ const Details: React.FC = ({obj: pod}) => {
- +
; }; diff --git a/frontend/public/components/replicaset.jsx b/frontend/public/components/replicaset.jsx index fda16fe0bb7..21ddc3d903a 100644 --- a/frontend/public/components/replicaset.jsx +++ b/frontend/public/components/replicaset.jsx @@ -50,7 +50,7 @@ const Details = ({obj: replicaSet}) => {
- +
; }; diff --git a/frontend/public/components/replication-controller.jsx b/frontend/public/components/replication-controller.jsx index ed771b54d45..50d2dfab326 100644 --- a/frontend/public/components/replication-controller.jsx +++ b/frontend/public/components/replication-controller.jsx @@ -44,7 +44,7 @@ const Details = ({obj: replicationController}) => {
- +
; }; diff --git a/frontend/public/components/stateful-set.jsx b/frontend/public/components/stateful-set.jsx index c74afd724f0..a5e919c8d25 100644 --- a/frontend/public/components/stateful-set.jsx +++ b/frontend/public/components/stateful-set.jsx @@ -50,7 +50,7 @@ const Details = ({obj: ss}) =>
- +
; diff --git a/frontend/public/components/volumes-table.tsx b/frontend/public/components/volumes-table.tsx index cd201fc1ad7..bb0564ef98b 100644 --- a/frontend/public/components/volumes-table.tsx +++ b/frontend/public/components/volumes-table.tsx @@ -5,28 +5,47 @@ import * as classNames from 'classnames'; import { ContainerSpec, + getVolumeType, + getVolumeLocation, + K8sKind, + K8sResourceKind, + K8sResourceKindReference, PodKind, PodTemplate, Volume, VolumeMount, } from '../module/k8s'; -import { - getVolumeType, - getVolumeLocation, -} from '../module/k8s/pods'; -import { - VolumeIcon, - ResourceIcon, - EmptyBox, - SectionHeading, -} from './utils'; +import { asAccessReview, EmptyBox, Kebab, KebabOption, VolumeIcon, ResourceIcon, SectionHeading } from './utils'; +import { Table, TableData, TableRow } from './factory'; +import { sortable } from '@patternfly/react-table'; +import { removeVolumeModal } from './modals'; +import {connectToModel} from '../kinds'; + +const removeVolume = (kind: K8sKind, obj: K8sResourceKind, volume: RowVolumeData): KebabOption => { + return { + label: 'Remove Volume', + callback: () => removeVolumeModal({ + kind, + resource: obj, + volume, + }), + accessReview: asAccessReview(kind, obj, 'patch'), + }; +}; + +const menuActions = [removeVolume]; + +const getPodTemplate = (resource: K8sResourceKind): PodTemplate => { + return resource.kind === 'Pod' ? resource as PodKind : resource.spec.template; +}; -const getVolumeMountsByPermissions = (pod: PodTemplate): VolumeData[] => { +const getRowVolumeData = (resource: K8sResourceKind): RowVolumeData[] => { + const pod: PodTemplate = getPodTemplate(resource); if (!pod || !pod.spec || !pod.spec.volumes) { return []; } - const m = {}; + const m = {}; const volumes = (pod.spec.volumes || []).reduce((p, v: Volume) => { p[v.name] = v; return p; @@ -34,15 +53,12 @@ const getVolumeMountsByPermissions = (pod: PodTemplate): VolumeData[] => { _.forEach(pod.spec.containers, (c: ContainerSpec) => { _.forEach(c.volumeMounts, (v: VolumeMount) => { - const k = `${v.name}_${v.readOnly ? 'ro' : 'rw'}`; + const k = `${v.name}_${v.readOnly ? 'ro' : 'rw'}_${v.mountPath}`; const mount = {container: c.name, mountPath: v.mountPath, subPath: v.subPath}; - if (k in m) { - return m[k].mounts.push(mount); - } - m[k] = {mounts: [mount], name: v.name, readOnly: !!v.readOnly, volume: volumes[v.name]}; + m[k] = {name: v.name, readOnly: !!v.readOnly, volumeDetail: volumes[v.name], + container: mount.container, mountPath: mount.mountPath, subPath: mount.subPath,resource}; }); }); - return _.values(m); }; @@ -52,70 +68,125 @@ const ContainerLink: React.FC = ({name, pod}) => ; ContainerLink.displayName = 'ContainerLink'; -const VolumeRow: React.FC = ({value, pod}) => { - const kind = _.get(getVolumeType(value.volume), 'id', ''); - const loc = getVolumeLocation(value.volume); - const name = value.name; - const permission = value.readOnly ? 'Read-only' : 'Read/Write'; - - return
- {value.mounts.map((m: Mount, i: number) => -
-
{name}
-
{_.get(m, 'mountPath', '-')}
-
- {_.get(m, 'subPath', '-')} -
-
- - {loc && ` (${loc})`} -
-
{permission}
-
- { _.get(pod, 'kind') === 'Pod' - ? - :
{m.container}
- } -
-
-
)} -
; +const volumeRoColumnClasses = [ + classNames('col-lg-2', 'col-md-3', 'col-sm-4', 'col-xs-5'), + classNames('col-lg-2', 'col-md-3', 'col-sm-4', 'col-xs-7'), + classNames('col-lg-2', 'col-md-2', 'col-sm-4', 'hidden-xs'), + classNames('col-lg-2', 'col-md-2', 'hidden-sm', 'hidden-xs'), + classNames('col-lg-2', 'col-md-2', 'hidden-sm', 'hidden-xs'), + classNames('col-lg-2', 'hidden-md', 'hidden-sm', 'hidden-xs'), + Kebab.columnClass, +]; + +const VolumesTableHeader = () => { + return [ + { + title: 'Name', sortField: 'name', transforms: [sortable], + props: { className: volumeRoColumnClasses[0]}, + }, + { + title: 'Mount Path', sortField: 'mountPath', transforms: [sortable], + props: { className: volumeRoColumnClasses[1]}, + }, + { + title: 'SubPath', sortField: 'subPath', transforms: [sortable], + props: { className: volumeRoColumnClasses[2]}, + }, + { + title: 'Type', + props: { className: volumeRoColumnClasses[3]}, + }, + { + title: 'Permissions', sortField: 'readOnly', transforms: [sortable], + props: { className: volumeRoColumnClasses[4]}, + }, + { + title: 'Utilized By', sortField: 'container', transforms: [sortable], + props: { className: volumeRoColumnClasses[5]}, + }, + { + title: '', + props: { className: volumeRoColumnClasses[6]}, + }, + ]; +}; +VolumesTableHeader.displayName = 'VolumesTableHeader'; + +const VolumesTableRow = ({obj: volume, index, key, style}) => { + const type = _.get(getVolumeType(volume.volumeDetail), 'id', ''); + const loc = getVolumeLocation(volume.volumeDetail); + const name = volume.name; + const permission = volume.readOnly ? 'Read-only' : 'Read/Write'; + const { resource } = volume; + const pod: PodTemplate = getPodTemplate(resource); + + return ( + + {name} + {volume.mountPath} + {volume.subPath} + + + {loc && ` (${loc})`} + + {permission} + + {_.get(pod, 'kind') === 'Pod' ? :
{volume.container}
} +
+ + + +
+ ); }; -VolumeRow.displayName = 'VolumeRow'; +VolumesTableRow.displayName = 'VolumesTableRow'; -export const VolumesTable: React.FC = ({podTemplate, heading}) => ( - - {heading && } - {_.isEmpty(podTemplate.spec.volumes) +export const VolumesTable = props => { + const { resource, ...tableProps } = props; + const data: RowVolumeData[] = getRowVolumeData(resource); + const pod: PodTemplate = getPodTemplate(resource); + return + {props.heading && } + {_.isEmpty(pod.spec.volumes) ? : ( -
-
-
Name
-
Mount Path
-
SubPath
-
Type
-
Permissions
-
Utilized By
-
-
- {getVolumeMountsByPermissions(podTemplate).map((v, i) => )} -
-
+ )} - -); + ; +}; + VolumesTable.displayName = 'VolumesTable'; -type Mount = { - container: string; -} & VolumeMount; +const VolumeKebab = connectToModel((props: VolumeKebabProps) => { + const {actions, kindObj, resource, isDisabled, rowVolumeData} = props; + if (!kindObj || kindObj.kind === 'Pod') { + return null; + } + const options = actions.map(b => b(kindObj, resource, rowVolumeData)); + return ; +}); + +type VolumeKebabAction = (kind: K8sKind, obj: K8sResourceKind, rowVolumeData: RowVolumeData) => KebabOption; +type VolumeKebabProps = { + kindObj: K8sKind; + actions: VolumeKebabAction[]; + kind: K8sResourceKindReference; + resource: K8sResourceKind; + isDisabled?: boolean; + rowVolumeData: RowVolumeData; +}; -type VolumeData = { +export type RowVolumeData = { name: string; readOnly: boolean; - volume: Volume; - mounts: Mount[]; + volumeDetail: Volume; + container: string; + mountPath: string; + subPath?: string; + resource: K8sResourceKind; }; type ContainerLinkProps = { @@ -123,12 +194,3 @@ type ContainerLinkProps = { name: string; }; -type VolumeRowProps = { - pod: PodKind | PodTemplate; - value: VolumeData; -}; - -type VolumesTableProps = { - podTemplate: PodKind | PodTemplate; - heading?: string; -};