Add Edit pool capability (#1806)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-04-07 20:24:37 -06:00
committed by GitHub
parent 0aa9c7b36e
commit bfbaaf12fb
20 changed files with 2373 additions and 123 deletions

View File

@@ -2321,6 +2321,21 @@ func parseTenantPool(pool *miniov2.Pool) *models.Pool {
tolerations = append(tolerations, toleration)
}
var securityContext models.SecurityContext
if pool.SecurityContext != nil {
fsGroup := strconv.Itoa(int(*pool.SecurityContext.FSGroup))
runAsGroup := strconv.Itoa(int(*pool.SecurityContext.RunAsGroup))
runAsUser := strconv.Itoa(int(*pool.SecurityContext.RunAsUser))
securityContext = models.SecurityContext{
FsGroup: &fsGroup,
RunAsGroup: &runAsGroup,
RunAsNonRoot: pool.SecurityContext.RunAsNonRoot,
RunAsUser: &runAsUser,
}
}
poolModel := &models.Pool{
Name: pool.Name,
Servers: swag.Int64(int64(pool.Servers)),
@@ -2329,10 +2344,11 @@ func parseTenantPool(pool *miniov2.Pool) *models.Pool {
Size: size,
StorageClassName: storageClassName,
},
NodeSelector: pool.NodeSelector,
Resources: resources,
Affinity: affinity,
Tolerations: tolerations,
NodeSelector: pool.NodeSelector,
Resources: resources,
Affinity: affinity,
Tolerations: tolerations,
SecurityContext: &securityContext,
}
return poolModel
}

View File

@@ -183,6 +183,8 @@ export const IAM_PAGES = {
"/namespaces/:tenantNamespace/tenants/:tenantName/pools",
NAMESPACE_TENANT_POOLS_ADD:
"/namespaces/:tenantNamespace/tenants/:tenantName/add-pool",
NAMESPACE_TENANT_POOLS_EDIT:
"/namespaces/:tenantNamespace/tenants/:tenantName/edit-pool",
NAMESPACE_TENANT_VOLUMES:
"/namespaces/:tenantNamespace/tenants/:tenantName/volumes",
NAMESPACE_TENANT_LICENSE:

View File

@@ -125,7 +125,7 @@ export interface INodeAffinityTerms {
}
export interface INodeAffinityLabelsSelector {
matchExpressions: object[];
matchExpressions: IMatchExpressionItem[];
}
export interface IMatchExpressionItem {

View File

@@ -50,6 +50,7 @@ import {
import { hasPermission } from "../../common/SecureComponent";
import { IRouteRule } from "./Menu/types";
import LoadingComponent from "../../common/LoadingComponent";
import EditPool from "./Tenants/TenantDetails/Pools/EditPool/EditPool";
const Trace = React.lazy(() => import("./Trace/Trace"));
const Heal = React.lazy(() => import("./Heal/Heal"));
@@ -114,7 +115,7 @@ const ConfigurationOptions = React.lazy(
() => import("./Configurations/ConfigurationPanels/ConfigurationOptions")
);
const AddPool = React.lazy(
() => import("./Tenants/TenantDetails/Pools/AddPool")
() => import("./Tenants/TenantDetails/Pools/AddPool/AddPool")
);
const styles = (theme: Theme) =>
@@ -439,6 +440,11 @@ const Console = ({
path: IAM_PAGES.NAMESPACE_TENANT_POOLS_ADD,
forceDisplay: true,
},
{
component: EditPool,
path: IAM_PAGES.NAMESPACE_TENANT_POOLS_EDIT,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_VOLUMES,

View File

@@ -277,7 +277,7 @@ const Affinity = ({
}}
selectorOptions={[
{ label: "None", value: "none" },
{ label: "Default (Pod Anti-Affinnity)", value: "default" },
{ label: "Default (Pod Anti-Affinity)", value: "default" },
{ label: "Node Selector", value: "nodeSelector" },
]}
/>

View File

@@ -54,6 +54,9 @@ export interface IPool {
volumes: number;
label?: string;
resources?: IResources;
affinity?: IAffinityModel;
tolerations?: ITolerationModel[];
securityContext?: ISecurityContext | null;
}
export interface IPodListElement {
@@ -253,3 +256,17 @@ export interface CapacityValue {
label: string;
color: string;
}
export interface IEditPoolItem {
name: string;
servers: number;
volumes_per_server: number;
volume_configuration: IVolumeConfiguration;
affinity?: IAffinityModel;
tolerations?: ITolerationModel[];
securityContext?: ISecurityContext | null;
}
export interface IEditPoolRequest {
pools: IEditPoolItem[]
}

View File

@@ -21,38 +21,38 @@ import withStyles from "@mui/styles/withStyles";
import {
formFieldStyles,
modalStyleUtils,
} from "../../../Common/FormComponents/common/styleLibrary";
} from "../../../../Common/FormComponents/common/styleLibrary";
import Grid from "@mui/material/Grid";
import { generatePoolName, niceBytes } from "../../../../../common/utils";
import { generatePoolName, niceBytes } from "../../../../../../common/utils";
import { LinearProgress } from "@mui/material";
import { IAddPoolRequest, ITenant } from "../../ListTenants/types";
import PageHeader from "../../../Common/PageHeader/PageHeader";
import PageLayout from "../../../Common/Layout/PageLayout";
import GenericWizard from "../../../Common/GenericWizard/GenericWizard";
import { IWizardElement } from "../../../Common/GenericWizard/types";
import history from "../../../../../history";
import { IAddPoolRequest, ITenant } from "../../../ListTenants/types";
import PageHeader from "../../../../Common/PageHeader/PageHeader";
import PageLayout from "../../../../Common/Layout/PageLayout";
import GenericWizard from "../../../../Common/GenericWizard/GenericWizard";
import { IWizardElement } from "../../../../Common/GenericWizard/types";
import history from "../../../../../../history";
import PoolResources from "./PoolResources";
import ScreenTitle from "../../../Common/ScreenTitle/ScreenTitle";
import TenantsIcon from "../../../../../icons/TenantsIcon";
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
import TenantsIcon from "../../../../../../icons/TenantsIcon";
import {
isPoolPageValid,
resetPoolForm,
setPoolField,
setTenantDetailsLoad,
} from "../../actions";
import { AppState } from "../../../../../store";
} from "../../../actions";
import { AppState } from "../../../../../../store";
import { connect } from "react-redux";
import PoolConfiguration from "./PoolConfiguration";
import PoolPodPlacement from "./PoolPodPlacement";
import {
ErrorResponseHandler,
ITolerationModel,
} from "../../../../../common/types";
import { getDefaultAffinity, getNodeSelector } from "../utils";
import api from "../../../../../common/api";
import { ISecurityContext } from "../../types";
import BackLink from "../../../../../common/BackLink";
import { setErrorSnackMessage } from "../../../../../actions";
} from "../../../../../../common/types";
import { getDefaultAffinity, getNodeSelector } from "../../utils";
import api from "../../../../../../common/api";
import { ISecurityContext } from "../../../types";
import BackLink from "../../../../../../common/BackLink";
import { setErrorSnackMessage } from "../../../../../../actions";
interface IAddPoolProps {
tenant: ITenant | null;
@@ -138,6 +138,7 @@ const AddPool = ({
securityContext,
volumesPerServer,
setTenantDetailsLoad,
setErrorSnackMessage,
}: IAddPoolProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
@@ -220,6 +221,7 @@ const AddPool = ({
volumeSize,
volumesPerServer,
withPodAntiAffinity,
setErrorSnackMessage,
]);
const cancelButton = {

View File

@@ -24,17 +24,17 @@ import {
createTenantCommon,
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { isPoolPageValid, setPoolField } from "../../actions";
import { AppState } from "../../../../../store";
import { clearValidationError } from "../../utils";
} from "../../../../Common/FormComponents/common/styleLibrary";
import { isPoolPageValid, setPoolField } from "../../../actions";
import { AppState } from "../../../../../../store";
import { clearValidationError } from "../../../utils";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { ISecurityContext } from "../../types";
} from "../../../../../../utils/validationFunctions";
import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { ISecurityContext } from "../../../types";
interface IConfigureProps {
setPoolField: typeof setPoolField;

View File

@@ -20,7 +20,7 @@ import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, IconButton, Paper, SelectChangeEvent } from "@mui/material";
import { AppState } from "../../../../../store";
import { AppState } from "../../../../../../store";
import {
isPoolPageValid,
setPoolField,
@@ -28,29 +28,29 @@ import {
addNewPoolToleration,
removePoolToleration,
setPoolKeyValuePairs,
} from "../../actions";
import { setModalErrorSnackMessage } from "../../../../../actions";
} from "../../../actions";
import { setModalErrorSnackMessage } from "../../../../../../actions";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
} from "../../../../Common/FormComponents/common/styleLibrary";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
} from "../../../../../../utils/validationFunctions";
import {
ErrorResponseHandler,
ITolerationModel,
} from "../../../../../common/types";
import { LabelKeyPair } from "../../types";
import RadioGroupSelector from "../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import api from "../../../../../common/api";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import AddIcon from "../../../../../icons/AddIcon";
import RemoveIcon from "../../../../../icons/RemoveIcon";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import TolerationSelector from "../../../Common/TolerationSelector/TolerationSelector";
} from "../../../../../../common/types";
import { LabelKeyPair } from "../../../types";
import RadioGroupSelector from "../../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import api from "../../../../../../common/api";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import AddIcon from "../../../../../../icons/AddIcon";
import RemoveIcon from "../../../../../../icons/RemoveIcon";
import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import TolerationSelector from "../../../../Common/TolerationSelector/TolerationSelector";
interface IAffinityProps {
classes: any;
@@ -277,7 +277,7 @@ const Affinity = ({
}}
selectorOptions={[
{ label: "None", value: "none" },
{ label: "Default (Pod Anti-Affinnity)", value: "default" },
{ label: "Default (Pod Anti-Affinity)", value: "default" },
{ label: "Node Selector", value: "nodeSelector" },
]}
/>

View File

@@ -22,28 +22,28 @@ import withStyles from "@mui/styles/withStyles";
import {
formFieldStyles,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
} from "../../../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import { niceBytes } from "../../../../../common/utils";
import { niceBytes } from "../../../../../../common/utils";
import { Paper, SelectChangeEvent } from "@mui/material";
import api from "../../../../../common/api";
import { ITenant } from "../../ListTenants/types";
import { ErrorResponseHandler } from "../../../../../common/types";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IQuotaElement, IQuotas, Opts } from "../../ListTenants/utils";
import { AppState } from "../../../../../store";
import api from "../../../../../../common/api";
import { ITenant } from "../../../ListTenants/types";
import { ErrorResponseHandler } from "../../../../../../common/types";
import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IQuotaElement, IQuotas, Opts } from "../../../ListTenants/utils";
import { AppState } from "../../../../../../store";
import { connect } from "react-redux";
import {
isPoolPageValid,
setPoolField,
setPoolStorageClasses,
} from "../../actions";
} from "../../../actions";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
} from "../../../../../../utils/validationFunctions";
import InputUnitMenu from "../../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
interface IPoolResourcesProps {
tenant: ITenant | null;

View File

@@ -34,13 +34,16 @@ import { ITenant } from "../../../ListTenants/types";
import Grid from "@mui/material/Grid";
import LabelValuePair from "../../../../Common/UsageBarWrapper/LabelValuePair";
import { niceBytesInt } from "../../../../../../common/utils";
import StackRow from "../../../../Common/UsageBarWrapper/StackRow";
import RBIconButton from "../../../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import { EditTenantIcon } from "../../../../../../icons";
interface IPoolDetails {
classes: any;
history: any;
loadingTenant: boolean;
tenant: ITenant | null;
selectedPool: string | null;
closeDetailsView: () => void;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
}
@@ -53,18 +56,22 @@ const styles = (theme: Theme) =>
...containerForHeader(theme.spacing(4)),
});
const stylingLayout = {
border: "#EAEAEA 1px solid",
borderRadius: "3px",
padding: "0px 20px",
position: "relative",
};
const twoColCssGridLayoutConfig = {
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
gridAutoFlow: { xs: "dense", sm: "row" },
gap: 2,
padding: "15px",
};
const PoolDetails = ({
closeDetailsView,
tenant,
selectedPool,
}: IPoolDetails) => {
const PoolDetails = ({ tenant, selectedPool, history }: IPoolDetails) => {
const poolInformation =
tenant?.pools.find((pool) => pool.name === selectedPool) || null;
@@ -72,17 +79,50 @@ const PoolDetails = ({
return null;
}
let affinityType = "None";
if (poolInformation.affinity) {
if (poolInformation.affinity.nodeAffinity) {
affinityType = "Node Selector";
} else {
affinityType = "Default (Pod Anti-Affinity)";
}
}
const HeaderSection = ({ title }: { title: string }) => {
return (
<StackRow
sx={{
borderBottom: "1px solid #eaeaea",
margin: 0,
marginBottom: "20px",
}}
>
<h3>{title}</h3>
</StackRow>
);
};
return (
<Fragment>
<Grid item xs={12}>
<Grid container>
<Grid item xs={8}>
<h4>Pool Configuration</h4>
</Grid>
<Grid item xs={4} />
</Grid>
<Grid item xs={12} sx={{ ...stylingLayout }}>
<div style={{ position: "absolute", right: 20, top: 18 }}>
<RBIconButton
icon={<EditTenantIcon />}
onClick={() => {
history.push(
`/namespaces/${tenant?.namespace || ""}/tenants/${
tenant?.name || ""
}/edit-pool`
);
}}
text={"Edit Pool"}
id={"editPool"}
/>
</div>
<HeaderSection title={"Pool Configuration"} />
<Box sx={{ ...twoColCssGridLayoutConfig }}>
<LabelValuePair label={"Pool Name"} value={poolInformation.label} />
<LabelValuePair label={"Pool Name"} value={poolInformation.name} />
<LabelValuePair
label={"Total Volumes"}
value={poolInformation.volumes}
@@ -93,12 +133,7 @@ const PoolDetails = ({
/>
<LabelValuePair label={"Capacity"} value={poolInformation.capacity} />
</Box>
<Grid container>
<Grid item xs={8}>
<h4>Resources</h4>
</Grid>
<Grid item xs={4} />
</Grid>
<HeaderSection title={"Resources"} />
<Box sx={{ ...twoColCssGridLayoutConfig }}>
{poolInformation.resources && (
<Fragment>
@@ -121,6 +156,123 @@ const PoolDetails = ({
value={poolInformation.volume_configuration.storage_class_name}
/>
</Box>
{poolInformation.securityContext &&
(poolInformation.securityContext.runAsNonRoot ||
poolInformation.securityContext.runAsUser ||
poolInformation.securityContext.runAsGroup ||
poolInformation.securityContext.fsGroup) && (
<Fragment>
<HeaderSection title={"Security Context"} />
<Box>
{poolInformation.securityContext.runAsNonRoot !== null && (
<Box sx={{ ...twoColCssGridLayoutConfig }}>
<LabelValuePair
label={"Run as Non Root"}
value={
poolInformation.securityContext.runAsNonRoot
? "Yes"
: "No"
}
/>
</Box>
)}
<Box
sx={{
...twoColCssGridLayoutConfig,
gridTemplateColumns: {
xs: "1fr",
sm: "2fr 1fr",
md: "1fr 1fr 1fr",
},
}}
>
{poolInformation.securityContext.runAsUser && (
<LabelValuePair
label={"Run as User"}
value={poolInformation.securityContext.runAsUser}
/>
)}
{poolInformation.securityContext.runAsGroup && (
<LabelValuePair
label={"Run as Group"}
value={poolInformation.securityContext.runAsGroup}
/>
)}
{poolInformation.securityContext.fsGroup && (
<LabelValuePair
label={"FsGroup"}
value={poolInformation.securityContext.fsGroup}
/>
)}
</Box>
</Box>
</Fragment>
)}
<HeaderSection title={"Affinity"} />
<Box>
<Box sx={{ ...twoColCssGridLayoutConfig }}>
<LabelValuePair label={"Type"} value={affinityType} />
{poolInformation.affinity?.nodeAffinity &&
poolInformation.affinity?.podAntiAffinity ? (
<LabelValuePair label={"With Pod Anti affinity"} value={"Yes"} />
) : (
<span />
)}
</Box>
{poolInformation.affinity?.nodeAffinity && (
<Fragment>
<HeaderSection title={"Labels"} />
<ul>
{poolInformation.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.map(
(term) => {
return term.matchExpressions.map((trm) => {
return (
<li>
{trm.key} - {trm.values.join(", ")}
</li>
);
});
}
)}
</ul>
</Fragment>
)}
</Box>
{poolInformation.tolerations && poolInformation.tolerations.length > 0 && (
<Fragment>
<HeaderSection title={"Tolerations"} />
<Box>
<ul>
{poolInformation.tolerations.map((tolItem) => {
return (
<li>
{tolItem.operator === "Equal" ? (
<Fragment>
If <strong>{tolItem.key}</strong> is equal to{" "}
<strong>{tolItem.value}</strong> then{" "}
<strong>{tolItem.effect}</strong> after{" "}
<strong>
{tolItem.tolerationSeconds?.seconds || 0}
</strong>{" "}
seconds
</Fragment>
) : (
<Fragment>
If <strong>{tolItem.key}</strong> exists then{" "}
<strong>{tolItem.effect}</strong> after{" "}
<strong>
{tolItem.tolerationSeconds?.seconds || 0}
</strong>{" "}
seconds
</Fragment>
)}
</li>
);
})}
</ul>
</Box>
</Fragment>
)}
</Grid>
</Fragment>
);

View File

@@ -0,0 +1,390 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import PageHeader from "../../../../Common/PageHeader/PageHeader";
import PageLayout from "../../../../Common/Layout/PageLayout";
import GenericWizard from "../../../../Common/GenericWizard/GenericWizard";
import api from "../../../../../../common/api";
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
import TenantsIcon from "../../../../../../icons/TenantsIcon";
import BackLink from "../../../../../../common/BackLink";
import EditPoolResources from "./EditPoolResources";
import EditPoolConfiguration from "./EditPoolConfiguration";
import EditPoolPlacement from "./EditPoolPlacement";
import history from "../../../../../../history";
import { IWizardElement } from "../../../../Common/GenericWizard/types";
import { LinearProgress } from "@mui/material";
import { generatePoolName, niceBytes } from "../../../../../../common/utils";
import {
formFieldStyles,
modalStyleUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import {
IEditPoolItem,
IEditPoolRequest,
ITenant,
} from "../../../ListTenants/types";
import {
isPoolPageValid,
resetPoolForm,
setInitialPoolDetails,
setPoolField,
setTenantDetailsLoad,
} from "../../../actions";
import { AppState } from "../../../../../../store";
import {
ErrorResponseHandler,
ITolerationModel,
} from "../../../../../../common/types";
import { getDefaultAffinity, getNodeSelector } from "../../utils";
import { ISecurityContext } from "../../../types";
import { setErrorSnackMessage } from "../../../../../../actions";
interface IEditPoolProps {
tenant: ITenant | null;
classes: any;
open: boolean;
match: any;
selectedStorageClass: string;
selectedPool: string | null;
validPages: string[];
numberOfNodes: number;
volumeSize: number;
volumesPerServer: number;
affinityType: string;
nodeSelectorLabels: string;
withPodAntiAffinity: boolean;
securityContextEnabled: boolean;
tolerations: ITolerationModel[];
securityContext: ISecurityContext;
resetPoolForm: typeof resetPoolForm;
setErrorSnackMessage: typeof setErrorSnackMessage;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
setInitialPoolDetails: typeof setInitialPoolDetails;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
margin: "auto",
justifyContent: "center",
"& div": {
width: 150,
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
},
factorElements: {
display: "flex",
justifyContent: "flex-start",
marginLeft: 30,
},
sizeNumber: {
fontSize: 35,
fontWeight: 700,
textAlign: "center",
},
sizeDescription: {
fontSize: 14,
color: "#777",
textAlign: "center",
},
pageBox: {
border: "1px solid #EAEAEA",
borderTop: 0,
},
editPoolTitle: {
border: "1px solid #EAEAEA",
borderBottom: 0,
},
...formFieldStyles,
...modalStyleUtils,
});
const requiredPages = ["setup", "affinity", "configure"];
const EditPool = ({
tenant,
classes,
resetPoolForm,
selectedPool,
selectedStorageClass,
validPages,
numberOfNodes,
volumeSize,
affinityType,
nodeSelectorLabels,
withPodAntiAffinity,
tolerations,
securityContextEnabled,
securityContext,
volumesPerServer,
setTenantDetailsLoad,
setInitialPoolDetails,
setErrorSnackMessage,
}: IEditPoolProps) => {
const [editSending, setEditSending] = useState<boolean>(false);
const poolsURL = `/namespaces/${tenant?.namespace || ""}/tenants/${
tenant?.name || ""
}/pools`;
useEffect(() => {
if (selectedPool) {
const poolDetails = tenant?.pools.find(
(pool) => pool.name === selectedPool
);
if (poolDetails) {
setInitialPoolDetails(poolDetails);
} else {
history.push("/tenants");
}
}
}, [selectedPool, setInitialPoolDetails, tenant]);
useEffect(() => {
if (editSending && tenant) {
const poolName = generatePoolName(tenant.pools);
let affinityObject = {};
switch (affinityType) {
case "default":
affinityObject = {
affinity: getDefaultAffinity(tenant.name, poolName),
};
break;
case "nodeSelector":
affinityObject = {
affinity: getNodeSelector(
nodeSelectorLabels,
withPodAntiAffinity,
tenant.name,
poolName
),
};
break;
}
const tolerationValues = tolerations.filter(
(toleration) => toleration.key.trim() !== ""
);
const cleanPools = tenant.pools
.filter((pool) => pool.name !== selectedPool)
.map((pool) => {
let securityContextOption = null;
if (pool.securityContext) {
if (
!!pool.securityContext.runAsUser ||
!!pool.securityContext.runAsGroup ||
!!pool.securityContext.fsGroup
) {
securityContextOption = { ...pool.securityContext };
}
}
const request: IEditPoolItem = {
...pool,
securityContext: securityContextOption,
};
return request;
});
const data: IEditPoolRequest = {
pools: [
...cleanPools,
{
name: selectedPool || poolName,
servers: numberOfNodes,
volumes_per_server: volumesPerServer,
volume_configuration: {
size: volumeSize * 1073741824,
storage_class_name: selectedStorageClass,
labels: null,
},
tolerations: tolerationValues,
securityContext: securityContextEnabled ? securityContext : null,
...affinityObject,
},
],
};
api
.invoke(
"PUT",
`/api/v1/namespaces/${tenant.namespace}/tenants/${tenant.name}/pools`,
data
)
.then(() => {
setEditSending(false);
resetPoolForm();
setTenantDetailsLoad(true);
history.push(poolsURL);
})
.catch((err: ErrorResponseHandler) => {
setEditSending(false);
setErrorSnackMessage(err);
});
}
}, [
selectedPool,
setErrorSnackMessage,
editSending,
poolsURL,
resetPoolForm,
setTenantDetailsLoad,
affinityType,
nodeSelectorLabels,
numberOfNodes,
securityContext,
securityContextEnabled,
selectedStorageClass,
tenant,
tolerations,
volumeSize,
volumesPerServer,
withPodAntiAffinity,
]);
const cancelButton = {
label: "Cancel",
type: "other",
enabled: true,
action: () => {
resetPoolForm();
history.push(poolsURL);
},
};
const createButton = {
label: "Update",
type: "submit",
enabled:
!editSending &&
selectedStorageClass !== "" &&
requiredPages.every((v) => validPages.includes(v)),
action: () => {
setEditSending(true);
},
};
const wizardSteps: IWizardElement[] = [
{
label: "Pool Resources",
componentRender: <EditPoolResources />,
buttons: [cancelButton, createButton],
},
{
label: "Configuration",
advancedOnly: true,
componentRender: <EditPoolConfiguration />,
buttons: [cancelButton, createButton],
},
{
label: "Pod Placement",
advancedOnly: true,
componentRender: <EditPoolPlacement />,
buttons: [cancelButton, createButton],
},
];
return (
<Fragment>
<Grid item xs={12}>
<PageHeader
label={
<Fragment>
<BackLink to={poolsURL} label={`Pool Details`} />
</Fragment>
}
/>
<PageLayout>
<Grid item xs={12} className={classes.editPoolTitle}>
<ScreenTitle
icon={<TenantsIcon />}
title={`Edit Pool - ${selectedPool}`}
subTitle={
<Fragment>
Namespace: {tenant?.namespace || ""} / Current Capacity:{" "}
{niceBytes((tenant?.total_size || 0).toString(10))} / Tenant:{" "}
{tenant?.name || ""}
</Fragment>
}
/>
</Grid>
{editSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid item xs={12} className={classes.pageBox}>
<GenericWizard wizardSteps={wizardSteps} />
</Grid>
</PageLayout>
</Grid>
</Fragment>
);
};
const mapState = (state: AppState) => {
const editPool = state.tenants.editPool;
return {
tenant: state.tenants.tenantDetails.tenantInfo,
selectedPool: state.tenants.tenantDetails.selectedPool,
selectedStorageClass: editPool.fields.setup.storageClass,
validPages: editPool.validPages,
storageClasses: editPool.storageClasses,
numberOfNodes: editPool.fields.setup.numberOfNodes,
volumeSize: editPool.fields.setup.volumeSize,
volumesPerServer: editPool.fields.setup.volumesPerServer,
affinityType: editPool.fields.affinity.podAffinity,
nodeSelectorLabels: editPool.fields.affinity.nodeSelectorLabels,
withPodAntiAffinity: editPool.fields.affinity.withPodAntiAffinity,
tolerations: editPool.fields.tolerations,
securityContextEnabled:
editPool.fields.configuration.securityContextEnabled,
securityContext: editPool.fields.configuration.securityContext,
};
};
const connector = connect(mapState, {
resetPoolForm,
setPoolField,
isPoolPageValid,
setErrorSnackMessage,
setTenantDetailsLoad,
setInitialPoolDetails,
});
export default withStyles(styles)(connector(EditPool));

View File

@@ -0,0 +1,289 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, Paper } from "@mui/material";
import {
createTenantCommon,
modalBasic,
wizardCommon,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { isEditPoolPageValid, setEditPoolField } from "../../../actions";
import { AppState } from "../../../../../../store";
import { clearValidationError } from "../../../utils";
import {
commonFormValidation,
IValidation,
} from "../../../../../../utils/validationFunctions";
import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { ISecurityContext } from "../../../types";
interface IConfigureProps {
setEditPoolField: typeof setEditPoolField;
isEditPoolPageValid: typeof isEditPoolPageValid;
classes: any;
securityContextEnabled: boolean;
securityContext: ISecurityContext;
}
const styles = (theme: Theme) =>
createStyles({
configSectionItem: {
marginRight: 15,
"& .multiContainer": {
border: "1px solid red",
},
},
tenantCustomizationFields: {
marginLeft: 30, // 2nd Level(15+15)
width: "88%",
margin: "auto",
},
containerItem: {
marginRight: 15,
},
fieldGroup: {
...createTenantCommon.fieldGroup,
paddingTop: 15,
marginBottom: 25,
},
responsiveSectionItem: {
"@media (max-width: 900px)": {
flexFlow: "column",
alignItems: "flex-start",
"& div > div": {
marginBottom: 5,
marginRight: 0,
},
},
},
fieldSpaceTop: {
marginTop: 15,
},
...modalBasic,
...wizardCommon,
});
const PoolConfiguration = ({
classes,
setEditPoolField,
securityContextEnabled,
isEditPoolPageValid,
securityContext,
}: IConfigureProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
// Common
const updateField = useCallback(
(field: string, value: any) => {
setEditPoolField("configuration", field, value);
},
[setEditPoolField]
);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [];
if (securityContextEnabled) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "pool_securityContext_runAsUser",
required: true,
value: securityContext.runAsUser,
customValidation:
securityContext.runAsUser === "" ||
parseInt(securityContext.runAsUser) < 0,
customValidationMessage: `runAsUser must be present and be 0 or more`,
},
{
fieldKey: "pool_securityContext_runAsGroup",
required: true,
value: securityContext.runAsGroup,
customValidation:
securityContext.runAsGroup === "" ||
parseInt(securityContext.runAsGroup) < 0,
customValidationMessage: `runAsGroup must be present and be 0 or more`,
},
{
fieldKey: "pool_securityContext_fsGroup",
required: true,
value: securityContext.fsGroup,
customValidation:
securityContext.fsGroup === "" ||
parseInt(securityContext.fsGroup) < 0,
customValidationMessage: `fsGroup must be present and be 0 or more`,
},
];
}
const commonVal = commonFormValidation(customAccountValidation);
isEditPoolPageValid("configure", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [isEditPoolPageValid, securityContextEnabled, securityContext]);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
return (
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Configure</h3>
</div>
<Grid item xs={12} className={classes.configSectionItem}>
<FormSwitchWrapper
value="tenantConfig"
id="pool_configuration"
name="pool_configuration"
checked={securityContextEnabled}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("securityContextEnabled", checked);
}}
label={"Security Context"}
/>
</Grid>
{securityContextEnabled && (
<Grid item xs={12} className={classes.tenantCustomizationFields}>
<fieldset className={classes.fieldGroup}>
<legend className={classes.descriptionText}>
Pool's Security Context
</legend>
<Grid item xs={12} className={`${classes.configSectionItem}`}>
<div
className={`${classes.multiContainer} ${classes.responsiveSectionItem}`}
>
<div className={classes.containerItem}>
<InputBoxWrapper
type="number"
id="pool_securityContext_runAsUser"
name="pool_securityContext_runAsUser"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("securityContext", {
...securityContext,
runAsUser: e.target.value,
});
cleanValidation("pool_securityContext_runAsUser");
}}
label="Run As User"
value={securityContext.runAsUser}
required
error={
validationErrors["pool_securityContext_runAsUser"] || ""
}
min="0"
/>
</div>
<div className={classes.containerItem}>
<InputBoxWrapper
type="number"
id="pool_securityContext_runAsGroup"
name="pool_securityContext_runAsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("securityContext", {
...securityContext,
runAsGroup: e.target.value,
});
cleanValidation("pool_securityContext_runAsGroup");
}}
label="Run As Group"
value={securityContext.runAsGroup}
required
error={
validationErrors["pool_securityContext_runAsGroup"] || ""
}
min="0"
/>
</div>
<div className={classes.containerItem}>
<InputBoxWrapper
type="number"
id="pool_securityContext_fsGroup"
name="pool_securityContext_fsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("securityContext", {
...securityContext,
fsGroup: e.target.value,
});
cleanValidation("pool_securityContext_fsGroup");
}}
label="FsGroup"
value={securityContext.fsGroup}
required
error={
validationErrors["pool_securityContext_fsGroup"] || ""
}
min="0"
/>
</div>
</div>
</Grid>
<br />
<Grid item xs={12} className={classes.configSectionItem}>
<div className={classes.multiContainer}>
<FormSwitchWrapper
value="securityContextRunAsNonRoot"
id="pool_securityContext_runAsNonRoot"
name="pool_securityContext_runAsNonRoot"
checked={securityContext.runAsNonRoot}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("securityContext", {
...securityContext,
runAsNonRoot: checked,
});
}}
label={"Do not run as Root"}
/>
</div>
</Grid>
</fieldset>
</Grid>
)}
</Paper>
);
};
const mapState = (state: AppState) => {
const configuration = state.tenants.editPool.fields.configuration;
return {
securityContextEnabled: configuration.securityContextEnabled,
securityContext: configuration.securityContext,
};
};
const connector = connect(mapState, {
setEditPoolField,
isEditPoolPageValid,
});
export default withStyles(styles)(connector(PoolConfiguration));

View File

@@ -0,0 +1,534 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, IconButton, Paper, SelectChangeEvent } from "@mui/material";
import { AppState } from "../../../../../../store";
import {
setEditPoolField,
isEditPoolPageValid,
setEditPoolKeyValuePairs,
setEditPoolTolerationInfo,
addNewEditPoolToleration,
removeEditPoolToleration,
} from "../../../actions";
import { setModalErrorSnackMessage } from "../../../../../../actions";
import {
modalBasic,
wizardCommon,
} from "../../../../Common/FormComponents/common/styleLibrary";
import {
commonFormValidation,
IValidation,
} from "../../../../../../utils/validationFunctions";
import {
ErrorResponseHandler,
ITolerationModel,
} from "../../../../../../common/types";
import { LabelKeyPair } from "../../../types";
import RadioGroupSelector from "../../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import api from "../../../../../../common/api";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import AddIcon from "../../../../../../icons/AddIcon";
import RemoveIcon from "../../../../../../icons/RemoveIcon";
import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import TolerationSelector from "../../../../Common/TolerationSelector/TolerationSelector";
interface IAffinityProps {
classes: any;
podAffinity: string;
nodeSelectorLabels: string;
withPodAntiAffinity: boolean;
keyValuePairs: LabelKeyPair[];
tolerations: ITolerationModel[];
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
setEditPoolField: typeof setEditPoolField;
isEditPoolPageValid: typeof isEditPoolPageValid;
setEditPoolKeyValuePairs: typeof setEditPoolKeyValuePairs;
setEditPoolTolerationInfo: typeof setEditPoolTolerationInfo;
removeEditPoolToleration: typeof removeEditPoolToleration;
addNewEditPoolToleration: typeof addNewEditPoolToleration;
}
const styles = (theme: Theme) =>
createStyles({
overlayAction: {
marginLeft: 10,
display: "flex",
alignItems: "center",
"& svg": {
maxWidth: 15,
maxHeight: 15,
},
"& button": {
background: "#EAEAEA",
},
},
affinityConfigField: {
display: "flex",
},
affinityFieldLabel: {
display: "flex",
flexFlow: "column",
flex: 1,
},
radioField: {
display: "flex",
alignItems: "flex-start",
marginTop: 10,
"& div:first-child": {
display: "flex",
flexFlow: "column",
alignItems: "baseline",
textAlign: "left !important",
},
},
affinityLabelKey: {
"& div:first-child": {
marginBottom: 0,
},
},
affinityLabelValue: {
marginLeft: 10,
"& div:first-child": {
marginBottom: 0,
},
},
rowActions: {
display: "flex",
alignItems: "center",
},
fieldContainer: {
marginBottom: 0,
},
affinityRow: {
marginBottom: 10,
display: "flex",
},
...modalBasic,
...wizardCommon,
});
interface OptionPair {
label: string;
value: string;
}
const Affinity = ({
classes,
podAffinity,
nodeSelectorLabels,
withPodAntiAffinity,
setModalErrorSnackMessage,
keyValuePairs,
setEditPoolField,
isEditPoolPageValid,
setEditPoolKeyValuePairs,
setEditPoolTolerationInfo,
addNewEditPoolToleration,
removeEditPoolToleration,
tolerations,
}: IAffinityProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true);
const [keyValueMap, setKeyValueMap] = useState<{ [key: string]: string[] }>(
{}
);
const [keyOptions, setKeyOptions] = useState<OptionPair[]>([]);
// Common
const updateField = useCallback(
(field: string, value: any) => {
setEditPoolField("affinity", field, value);
},
[setEditPoolField]
);
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/nodes/labels`)
.then((res: { [key: string]: string[] }) => {
setLoading(false);
setKeyValueMap(res);
let keys: OptionPair[] = [];
for (let k in res) {
keys.push({
label: k,
value: k,
});
}
setKeyOptions(keys);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setModalErrorSnackMessage(err);
setKeyValueMap({});
});
}
}, [setModalErrorSnackMessage, loading]);
useEffect(() => {
if (keyValuePairs) {
const vlr = keyValuePairs
.filter((kvp) => kvp.key !== "")
.map((kvp) => `${kvp.key}=${kvp.value}`)
.filter((kvs, i, a) => a.indexOf(kvs) === i);
const vl = vlr.join("&");
updateField("nodeSelectorLabels", vl);
}
}, [keyValuePairs, updateField]);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [];
if (podAffinity === "nodeSelector") {
let valid = true;
const splittedLabels = nodeSelectorLabels.split("&");
if (splittedLabels.length === 1 && splittedLabels[0] === "") {
valid = false;
}
splittedLabels.forEach((item: string, index: number) => {
const splitItem = item.split("=");
if (splitItem.length !== 2) {
valid = false;
}
if (index + 1 !== splittedLabels.length) {
if (splitItem[0] === "" || splitItem[1] === "") {
valid = false;
}
}
});
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "labels",
required: true,
value: nodeSelectorLabels,
customValidation: !valid,
customValidationMessage:
"You need to add at least one label key-pair",
},
];
}
const commonVal = commonFormValidation(customAccountValidation);
isEditPoolPageValid("affinity", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [isEditPoolPageValid, podAffinity, nodeSelectorLabels]);
const updateToleration = (index: number, field: string, value: any) => {
const alterToleration = { ...tolerations[index], [field]: value };
setEditPoolTolerationInfo(index, alterToleration);
};
return (
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Pod Placement</h3>
</div>
<Grid item xs={12} className={classes.affinityConfigField}>
<Grid item className={classes.affinityFieldLabel}>
<div className={classes.label}>Type</div>
<div
className={`${classes.descriptionText} ${classes.affinityHelpText}`}
>
MinIO supports multiple configurations for Pod Affinity
</div>
<Grid item className={classes.radioField}>
<RadioGroupSelector
currentSelection={podAffinity}
id="affinity-options"
name="affinity-options"
label={" "}
onChange={(e) => {
updateField("podAffinity", e.target.value);
}}
selectorOptions={[
{ label: "None", value: "none" },
{ label: "Default (Pod Anti-Affinity)", value: "default" },
{ label: "Node Selector", value: "nodeSelector" },
]}
/>
</Grid>
</Grid>
</Grid>
{podAffinity === "nodeSelector" && (
<Fragment>
<br />
<Grid item xs={12}>
<FormSwitchWrapper
value="with_pod_anti_affinity"
id="with_pod_anti_affinity"
name="with_pod_anti_affinity"
checked={withPodAntiAffinity}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("withPodAntiAffinity", checked);
}}
label={"With Pod Anti-Affinity"}
/>
</Grid>
<Grid item xs={12}>
<h3>Labels</h3>
<span className={classes.error}>{validationErrors["labels"]}</span>
<Grid container>
{keyValuePairs &&
keyValuePairs.map((kvp, i) => {
return (
<Grid
item
xs={12}
className={classes.affinityRow}
key={`affinity-keyVal-${i.toString()}`}
>
<Grid item xs={5} className={classes.affinityLabelKey}>
{keyOptions.length > 0 && (
<SelectWrapper
onChange={(e: SelectChangeEvent<string>) => {
const newKey = e.target.value as string;
const arrCp: LabelKeyPair[] = Object.assign(
[],
keyValuePairs
);
arrCp[i].key = e.target.value as string;
arrCp[i].value = keyValueMap[newKey][0];
setEditPoolKeyValuePairs(arrCp);
}}
id="select-access-policy"
name="select-access-policy"
label={""}
value={kvp.key}
options={keyOptions}
/>
)}
{keyOptions.length === 0 && (
<InputBoxWrapper
id={`nodeselector-key-${i.toString()}`}
label={""}
name={`nodeselector-${i.toString()}`}
value={kvp.key}
onChange={(e) => {
const arrCp: LabelKeyPair[] = Object.assign(
[],
keyValuePairs
);
arrCp[i].key = e.target.value;
setEditPoolKeyValuePairs(arrCp);
}}
index={i}
placeholder={"Key"}
/>
)}
</Grid>
<Grid item xs={5} className={classes.affinityLabelValue}>
{keyOptions.length > 0 && (
<SelectWrapper
onChange={(e: SelectChangeEvent<string>) => {
const arrCp: LabelKeyPair[] = Object.assign(
[],
keyValuePairs
);
arrCp[i].value = e.target.value as string;
setEditPoolKeyValuePairs(arrCp);
}}
id="select-access-policy"
name="select-access-policy"
label={""}
value={kvp.value}
options={
keyValueMap[kvp.key]
? keyValueMap[kvp.key].map((v) => {
return { label: v, value: v };
})
: []
}
/>
)}
{keyOptions.length === 0 && (
<InputBoxWrapper
id={`nodeselector-value-${i.toString()}`}
label={""}
name={`nodeselector-${i.toString()}`}
value={kvp.value}
onChange={(e) => {
const arrCp: LabelKeyPair[] = Object.assign(
[],
keyValuePairs
);
arrCp[i].value = e.target.value;
setEditPoolKeyValuePairs(arrCp);
}}
index={i}
placeholder={"value"}
/>
)}
</Grid>
<Grid item xs={2} className={classes.rowActions}>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => {
const arrCp = Object.assign([], keyValuePairs);
if (keyOptions.length > 0) {
arrCp.push({
key: keyOptions[0].value,
value: keyValueMap[keyOptions[0].value][0],
});
} else {
arrCp.push({ key: "", value: "" });
}
setEditPoolKeyValuePairs(arrCp);
}}
>
<AddIcon />
</IconButton>
</div>
{keyValuePairs.length > 1 && (
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => {
const arrCp = keyValuePairs.filter(
(item, index) => index !== i
);
setEditPoolKeyValuePairs(arrCp);
}}
>
<RemoveIcon />
</IconButton>
</div>
)}
</Grid>
</Grid>
);
})}
</Grid>
</Grid>
</Fragment>
)}
<Grid item xs={12} className={classes.affinityConfigField}>
<Grid item className={classes.affinityFieldLabel}>
<h3>Tolerations</h3>
<span className={classes.error}>
{validationErrors["tolerations"]}
</span>
<Grid container>
{tolerations &&
tolerations.map((tol, i) => {
return (
<Grid
item
xs={12}
className={classes.affinityRow}
key={`affinity-keyVal-${i.toString()}`}
>
<TolerationSelector
effect={tol.effect}
onEffectChange={(value) => {
updateToleration(i, "effect", value);
}}
tolerationKey={tol.key}
onTolerationKeyChange={(value) => {
updateToleration(i, "key", value);
}}
operator={tol.operator}
onOperatorChange={(value) => {
updateToleration(i, "operator", value);
}}
value={tol.value}
onValueChange={(value) => {
updateToleration(i, "value", value);
}}
tolerationSeconds={tol.tolerationSeconds?.seconds || 0}
onSecondsChange={(value) => {
updateToleration(i, "tolerationSeconds", {
seconds: value,
});
}}
index={i}
/>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={addNewEditPoolToleration}
disabled={i !== tolerations.length - 1}
>
<AddIcon />
</IconButton>
</div>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => removeEditPoolToleration(i)}
disabled={tolerations.length <= 1}
>
<RemoveIcon />
</IconButton>
</div>
</Grid>
);
})}
</Grid>
</Grid>
</Grid>
</Paper>
);
};
const mapState = (state: AppState) => {
const editPool = state.tenants.editPool;
return {
podAffinity: editPool.fields.affinity.podAffinity,
nodeSelectorLabels: editPool.fields.affinity.nodeSelectorLabels,
withPodAntiAffinity: editPool.fields.affinity.withPodAntiAffinity,
keyValuePairs: editPool.fields.nodeSelectorPairs,
tolerations: editPool.fields.tolerations,
};
};
const connector = connect(mapState, {
setModalErrorSnackMessage,
setEditPoolField,
isEditPoolPageValid,
setEditPoolKeyValuePairs,
setEditPoolTolerationInfo,
addNewEditPoolToleration,
removeEditPoolToleration,
});
export default withStyles(styles)(connector(Affinity));

View File

@@ -0,0 +1,313 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect, useState } from "react";
import get from "lodash/get";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
formFieldStyles,
wizardCommon,
} from "../../../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import { niceBytes } from "../../../../../../common/utils";
import { Paper, SelectChangeEvent } from "@mui/material";
import api from "../../../../../../common/api";
import { ITenant } from "../../../ListTenants/types";
import { ErrorResponseHandler } from "../../../../../../common/types";
import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IQuotaElement, IQuotas, Opts } from "../../../ListTenants/utils";
import { AppState } from "../../../../../../store";
import { connect } from "react-redux";
import {
setEditPoolField,
isEditPoolPageValid,
setEditPoolStorageClasses,
} from "../../../actions";
import {
commonFormValidation,
IValidation,
} from "../../../../../../utils/validationFunctions";
import InputUnitMenu from "../../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
interface IPoolResourcesProps {
tenant: ITenant | null;
classes: any;
storageClasses: Opts[];
numberOfNodes: string;
storageClass: string;
volumeSize: string;
volumesPerServer: string;
setEditPoolField: typeof setEditPoolField;
isEditPoolPageValid: typeof isEditPoolPageValid;
setEditPoolStorageClasses: typeof setEditPoolStorageClasses;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
margin: "auto",
justifyContent: "center",
"& div": {
width: 150,
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
},
factorElements: {
display: "flex",
justifyContent: "flex-start",
marginLeft: 30,
},
sizeNumber: {
fontSize: 35,
fontWeight: 700,
textAlign: "center",
},
sizeDescription: {
fontSize: 14,
color: "#777",
textAlign: "center",
},
...formFieldStyles,
...wizardCommon,
});
const PoolResources = ({
tenant,
classes,
storageClasses,
numberOfNodes,
storageClass,
volumeSize,
volumesPerServer,
setEditPoolField,
setEditPoolStorageClasses,
isEditPoolPageValid,
}: IPoolResourcesProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
const instanceCapacity: number =
parseInt(volumeSize) * 1073741824 * parseInt(volumesPerServer);
const totalCapacity: number = instanceCapacity * parseInt(numberOfNodes);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [
{
fieldKey: "number_of_nodes",
required: true,
value: numberOfNodes.toString(),
customValidation:
parseInt(numberOfNodes) < 1 || isNaN(parseInt(numberOfNodes)),
customValidationMessage: "Number of servers must be at least 1",
},
{
fieldKey: "pool_size",
required: true,
value: volumeSize.toString(),
customValidation:
parseInt(volumeSize) < 1 || isNaN(parseInt(volumeSize)),
customValidationMessage: "Pool Size cannot be 0",
},
{
fieldKey: "volumes_per_server",
required: true,
value: volumesPerServer.toString(),
customValidation:
parseInt(volumesPerServer) < 1 || isNaN(parseInt(volumesPerServer)),
customValidationMessage: "1 volume or more are required",
},
];
const commonVal = commonFormValidation(customAccountValidation);
isEditPoolPageValid("setup", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [
isEditPoolPageValid,
numberOfNodes,
volumeSize,
volumesPerServer,
storageClass,
]);
useEffect(() => {
if (storageClasses.length === 0 && tenant) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenant.namespace}/resourcequotas/${tenant.namespace}-storagequota`
)
.then((res: IQuotas) => {
const elements: IQuotaElement[] = get(res, "elements", []);
const newStorage = elements.map((storageClass: any) => {
const name = get(storageClass, "name", "").split(
".storageclass.storage.k8s.io/requests.storage"
)[0];
return { label: name, value: name };
});
setEditPoolField("setup", "storageClass", newStorage[0].value);
setEditPoolStorageClasses(newStorage);
})
.catch((err: ErrorResponseHandler) => {
console.error(err);
});
}
}, [tenant, storageClasses, setEditPoolStorageClasses, setEditPoolField]);
const setFieldInfo = (fieldName: string, value: any) => {
setEditPoolField("setup", fieldName, value);
};
return (
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Pool Resources</h3>
</div>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="number_of_nodes"
name="number_of_nodes"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const intValue = parseInt(e.target.value);
if (e.target.validity.valid && !isNaN(intValue)) {
setFieldInfo("numberOfNodes", intValue);
} else if (isNaN(intValue)) {
setFieldInfo("numberOfNodes", 0);
}
}}
label="Number of Servers"
value={numberOfNodes}
error={validationErrors["number_of_nodes"] || ""}
pattern={"[0-9]*"}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="pool_size"
name="pool_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const intValue = parseInt(e.target.value);
if (e.target.validity.valid && !isNaN(intValue)) {
setFieldInfo("volumeSize", intValue);
} else if (isNaN(intValue)) {
setFieldInfo("volumeSize", 0);
}
}}
label="Volume Size"
value={volumeSize}
error={validationErrors["pool_size"] || ""}
pattern={"[0-9]*"}
overlayObject={
<InputUnitMenu
id={"quota_unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="volumes_per_sever"
name="volumes_per_sever"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const intValue = parseInt(e.target.value);
if (e.target.validity.valid && !isNaN(intValue)) {
setFieldInfo("volumesPerServer", intValue);
} else if (isNaN(intValue)) {
setFieldInfo("volumesPerServer", 0);
}
}}
label="Volumes per Server"
value={volumesPerServer}
error={validationErrors["volumes_per_server"] || ""}
pattern={"[0-9]*"}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: SelectChangeEvent<string>) => {
setFieldInfo("storageClasses", e.target.value as string);
}}
label="Storage Class"
value={storageClass}
options={storageClasses}
disabled={storageClasses.length < 1}
/>
</Grid>
<Grid item xs={12} className={classes.bottomContainer}>
<div className={classes.factorElements}>
<div>
<div className={classes.sizeNumber}>
{niceBytes(instanceCapacity.toString(10))}
</div>
<div className={classes.sizeDescription}>Instance Capacity</div>
</div>
<div>
<div className={classes.sizeNumber}>
{niceBytes(totalCapacity.toString(10))}
</div>
<div className={classes.sizeDescription}>Total Capacity</div>
</div>
</div>
</Grid>
</Paper>
);
};
const mapState = (state: AppState) => {
const setupFields = state.tenants.editPool.fields.setup;
return {
tenant: state.tenants.tenantDetails.tenantInfo,
storageClasses: state.tenants.editPool.storageClasses,
numberOfNodes: setupFields.numberOfNodes.toString(),
storageClass: setupFields.storageClass,
volumeSize: setupFields.volumeSize.toString(),
volumesPerServer: setupFields.volumesPerServer.toString(),
};
};
const connector = connect(mapState, {
setEditPoolField,
isEditPoolPageValid,
setEditPoolStorageClasses,
});
export default withStyles(styles)(connector(PoolResources));

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useState } from "react";
import React, { Fragment } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
@@ -28,7 +28,11 @@ import {
import Grid from "@mui/material/Grid";
import { setErrorSnackMessage } from "../../../../actions";
import { AppState } from "../../../../store";
import { setSelectedPool, setTenantDetailsLoad } from "../actions";
import {
setOpenPoolDetails,
setSelectedPool,
setTenantDetailsLoad,
} from "../actions";
import PoolsListing from "./Pools/Details/PoolsListing";
import PoolDetails from "./Pools/Details/PoolDetails";
import BackLink from "../../../../common/BackLink";
@@ -39,9 +43,11 @@ interface IPoolsSummary {
history: any;
match: any;
selectedPool: string | null;
poolDetailsOpen: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
setSelectedPool: typeof setSelectedPool;
setOpenPoolDetails: typeof setOpenPoolDetails;
}
const styles = (theme: Theme) =>
@@ -57,15 +63,16 @@ const PoolsSummary = ({
history,
selectedPool,
match,
poolDetailsOpen,
setOpenPoolDetails,
}: IPoolsSummary) => {
const [poolDetailsOpen, setPoolDetailsOpen] = useState<boolean>(false);
return (
<Fragment>
{poolDetailsOpen && (
<Grid item xs={12}>
<BackLink
executeOnClick={() => {
setPoolDetailsOpen(false);
setOpenPoolDetails(false);
}}
label={"Back to Pools list"}
to={match.url}
@@ -77,15 +84,11 @@ const PoolsSummary = ({
</h1>
<Grid container>
{poolDetailsOpen ? (
<PoolDetails
closeDetailsView={() => {
setPoolDetailsOpen(false);
}}
/>
<PoolDetails history={history} />
) : (
<PoolsListing
setPoolDetailsView={() => {
setPoolDetailsOpen(true);
setOpenPoolDetails(true);
}}
history={history}
/>
@@ -100,12 +103,14 @@ const mapState = (state: AppState) => ({
selectedTenant: state.tenants.tenantDetails.currentTenant,
selectedPool: state.tenants.tenantDetails.selectedPool,
tenant: state.tenants.tenantDetails.tenantInfo,
poolDetailsOpen: state.tenants.tenantDetails.poolDetailsOpen,
});
const connector = connect(mapState, {
setErrorSnackMessage,
setTenantDetailsLoad,
setSelectedPool,
setOpenPoolDetails,
});
export default withStyles(styles)(connector(PoolsSummary));

View File

@@ -78,6 +78,5 @@ export const getNodeSelector = (
const def = getDefaultAffinity(tenantName, poolName);
nodeSelector.podAntiAffinity = def.podAntiAffinity;
}
console.log(nodeSelector);
return nodeSelector;
};

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ITenant } from "./ListTenants/types";
import { IPool, ITenant } from "./ListTenants/types";
import { Opts } from "./ListTenants/utils";
import {
ADD_TENANT_ADD_CA_KEYPAIR,
@@ -33,7 +33,6 @@ import {
ADD_TENANT_ENCRYPTION_VAULT_CA,
ADD_TENANT_ENCRYPTION_VAULT_CERT,
ADD_TENANT_RESET_FORM,
ADD_TENANT_SET_CURRENT_PAGE,
ADD_TENANT_SET_LIMIT_SIZE,
ADD_TENANT_SET_PAGE_VALID,
ADD_TENANT_SET_STORAGE_CLASSES_LIST,
@@ -59,17 +58,22 @@ import {
ADD_POOL_REMOVE_TOLERATION_ROW,
ADD_POOL_SET_KEY_PAIR_VALUE,
POOL_DETAILS_SET_SELECTED_POOL,
POOL_DETAILS_SET_OPEN_DETAILS,
EDIT_POOL_SET_INITIAL_INFO,
EDIT_POOL_SET_LOADING,
EDIT_POOL_RESET_FORM,
IEditPoolFields,
EDIT_POOL_SET_VALUE,
EDIT_POOL_SET_PAGE_VALID,
EDIT_POOL_SET_POOL_STORAGE_CLASSES,
EDIT_POOL_SET_TOLERATION_VALUE,
EDIT_POOL_ADD_NEW_TOLERATION,
EDIT_POOL_REMOVE_TOLERATION_ROW,
EDIT_POOL_SET_KEY_PAIR_VALUE,
} from "./types";
import { ITolerationModel } from "../../../common/types";
// Basic actions
export const setWizardPage = (page: number) => {
return {
type: ADD_TENANT_SET_CURRENT_PAGE,
page,
};
};
export const updateAddField = (
pageName: string,
fieldName: string,
@@ -409,9 +413,99 @@ export const setPoolKeyValuePairs = (newArray: LabelKeyPair[]) => {
};
};
//Pool Details
export const setSelectedPool = (newPool: string | null) => {
return {
type: POOL_DETAILS_SET_SELECTED_POOL,
pool: newPool,
};
};
export const setOpenPoolDetails = (state: boolean) => {
return {
type: POOL_DETAILS_SET_OPEN_DETAILS,
state,
};
};
// Edit Pool
export const setInitialPoolDetails = (pool: IPool) => {
return {
type: EDIT_POOL_SET_INITIAL_INFO,
pool,
};
};
export const setEditPoolLoading = (state: boolean) => {
return {
type: EDIT_POOL_SET_LOADING,
state,
};
};
export const resetEditPoolForm = () => {
return {
type: EDIT_POOL_RESET_FORM,
};
};
export const setEditPoolField = (
page: keyof IEditPoolFields,
field: string,
value: any
) => {
return {
type: EDIT_POOL_SET_VALUE,
page,
field,
value,
};
};
export const isEditPoolPageValid = (page: string, status: boolean) => {
return {
type: EDIT_POOL_SET_PAGE_VALID,
page,
status,
};
};
export const setEditPoolStorageClasses = (storageClasses: Opts[]) => {
return {
type: EDIT_POOL_SET_POOL_STORAGE_CLASSES,
storageClasses,
};
};
export const setEditPoolTolerationInfo = (
index: number,
tolerationValue: ITolerationModel
) => {
return {
type: EDIT_POOL_SET_TOLERATION_VALUE,
index,
toleration: tolerationValue,
};
};
export const addNewEditPoolToleration = () => {
return {
type: EDIT_POOL_ADD_NEW_TOLERATION,
};
};
export const removeEditPoolToleration = (index: number) => {
return {
type: EDIT_POOL_REMOVE_TOLERATION_ROW,
index,
};
};
export const setEditPoolKeyValuePairs = (newArray: LabelKeyPair[]) => {
return {
type: EDIT_POOL_SET_KEY_PAIR_VALUE,
newArray,
};
};

View File

@@ -16,6 +16,15 @@
import has from "lodash/has";
import get from "lodash/get";
import {
ADD_POOL_ADD_NEW_TOLERATION,
ADD_POOL_REMOVE_TOLERATION_ROW,
ADD_POOL_RESET_FORM,
ADD_POOL_SET_KEY_PAIR_VALUE,
ADD_POOL_SET_LOADING,
ADD_POOL_SET_PAGE_VALID,
ADD_POOL_SET_POOL_STORAGE_CLASSES,
ADD_POOL_SET_TOLERATION_VALUE,
ADD_POOL_SET_VALUE,
ADD_TENANT_ADD_CA_KEYPAIR,
ADD_TENANT_ADD_CONSOLE_CA_KEYPAIR,
ADD_TENANT_ADD_CONSOLE_CERT,
@@ -42,27 +51,35 @@ import {
ADD_TENANT_SET_STORAGE_TYPE,
ADD_TENANT_SET_TOLERATION_VALUE,
ADD_TENANT_UPDATE_FIELD,
EDIT_POOL_ADD_NEW_TOLERATION,
EDIT_POOL_REMOVE_TOLERATION_ROW,
EDIT_POOL_RESET_FORM,
EDIT_POOL_SET_INITIAL_INFO,
EDIT_POOL_SET_KEY_PAIR_VALUE,
EDIT_POOL_SET_LOADING,
EDIT_POOL_SET_PAGE_VALID,
EDIT_POOL_SET_POOL_STORAGE_CLASSES,
EDIT_POOL_SET_TOLERATION_VALUE,
EDIT_POOL_SET_VALUE,
IEditPoolFields,
ITenantState,
LabelKeyPair,
POOL_DETAILS_SET_OPEN_DETAILS,
POOL_DETAILS_SET_SELECTED_POOL,
TENANT_DETAILS_SET_CURRENT_TENANT,
TENANT_DETAILS_SET_LOADING,
TENANT_DETAILS_SET_TAB,
TENANT_DETAILS_SET_TENANT,
ADD_POOL_SET_LOADING,
ADD_POOL_SET_VALUE,
ADD_POOL_RESET_FORM,
ITenantState,
TenantsManagementTypes,
ADD_POOL_SET_PAGE_VALID,
ADD_POOL_SET_POOL_STORAGE_CLASSES,
ADD_POOL_ADD_NEW_TOLERATION,
ADD_POOL_SET_TOLERATION_VALUE,
ADD_POOL_REMOVE_TOLERATION_ROW,
ADD_POOL_SET_KEY_PAIR_VALUE,
POOL_DETAILS_SET_SELECTED_POOL,
} from "./types";
import { KeyPair } from "./ListTenants/utils";
import { getRandomString } from "./utils";
import { addTenantSetStorageTypeReducer } from "./reducers/add-tenant-reducer";
import { ITolerationEffect, ITolerationOperator } from "../../../common/types";
import {
ITolerationEffect,
ITolerationModel,
ITolerationOperator,
} from "../../../common/types";
const initialState: ITenantState = {
createTenant: {
@@ -365,6 +382,7 @@ const initialState: ITenantState = {
tenantInfo: null,
currentTab: "summary",
selectedPool: null,
poolDetailsOpen: false,
},
addPool: {
addPoolLoading: false,
@@ -404,6 +422,44 @@ const initialState: ITenantState = {
],
},
},
editPool: {
editPoolLoading: false,
validPages: ["setup", "affinity", "configure"],
storageClasses: [],
limitSize: {},
fields: {
setup: {
numberOfNodes: 0,
storageClass: "",
volumeSize: 0,
volumesPerServer: 0,
},
affinity: {
nodeSelectorLabels: "",
podAffinity: "default",
withPodAntiAffinity: true,
},
configuration: {
securityContextEnabled: false,
securityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
},
nodeSelectorPairs: [{ key: "", value: "" }],
tolerations: [
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
},
],
},
},
};
export function tenantsReducer(
@@ -1165,14 +1221,6 @@ export function tenantsReducer(
},
},
};
case POOL_DETAILS_SET_SELECTED_POOL:
return {
...state,
tenantDetails: {
...state.tenantDetails,
selectedPool: action.pool,
},
};
case ADD_POOL_RESET_FORM:
return {
...state,
@@ -1215,6 +1263,281 @@ export function tenantsReducer(
},
},
};
case POOL_DETAILS_SET_SELECTED_POOL:
return {
...state,
tenantDetails: {
...state.tenantDetails,
selectedPool: action.pool,
},
};
case POOL_DETAILS_SET_OPEN_DETAILS:
return {
...state,
tenantDetails: {
...state.tenantDetails,
poolDetailsOpen: action.state,
},
};
case EDIT_POOL_SET_INITIAL_INFO:
let podAffinity: "default" | "nodeSelector" | "none" = "none";
let withPodAntiAffinity = false;
let nodeSelectorLabels = "";
let tolerations: ITolerationModel[] = [
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
},
];
let nodeSelectorPairs: LabelKeyPair[] = [{ key: "", value: "" }];
if (action.pool.affinity?.nodeAffinity) {
podAffinity = "nodeSelector";
if (action.pool.affinity?.podAntiAffinity) {
withPodAntiAffinity = true;
}
} else if (action.pool.affinity?.podAntiAffinity) {
podAffinity = "default";
}
if (action.pool.affinity?.nodeAffinity) {
let labelItems: string[] = [];
nodeSelectorPairs = [];
action.pool.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.forEach(
(labels) => {
labels.matchExpressions.forEach((exp) => {
labelItems.push(`${exp.key}=${exp.values.join(",")}`);
nodeSelectorPairs.push({
key: exp.key,
value: exp.values.join(", "),
});
});
}
);
nodeSelectorLabels = labelItems.join("&");
}
let securityContextOption = false;
if (action.pool.securityContext) {
securityContextOption =
!!action.pool.securityContext.runAsUser ||
!!action.pool.securityContext.runAsGroup ||
!!action.pool.securityContext.fsGroup;
}
if (action.pool.tolerations) {
tolerations = action.pool.tolerations?.map((toleration) => {
const tolerationItem: ITolerationModel = {
key: toleration.key,
tolerationSeconds: toleration.tolerationSeconds,
value: toleration.value,
effect: toleration.effect,
operator: toleration.operator,
};
return tolerationItem;
});
}
const volSizeVars = action.pool.volume_configuration.size / 1073741824;
const newPoolInfoFields: IEditPoolFields = {
setup: {
numberOfNodes: action.pool.servers,
storageClass: action.pool.volume_configuration.storage_class_name,
volumeSize: volSizeVars,
volumesPerServer: action.pool.volumes_per_server,
},
configuration: {
securityContextEnabled: securityContextOption,
securityContext: {
runAsUser: action.pool.securityContext?.runAsUser || "",
runAsGroup: action.pool.securityContext?.runAsGroup || "",
fsGroup: action.pool.securityContext?.fsGroup || "",
runAsNonRoot: !!action.pool.securityContext?.runAsNonRoot,
},
},
affinity: {
podAffinity,
withPodAntiAffinity,
nodeSelectorLabels,
},
tolerations,
nodeSelectorPairs,
};
return {
...state,
editPool: {
...state.editPool,
fields: {
...state.editPool.fields,
...newPoolInfoFields,
},
},
};
case EDIT_POOL_SET_LOADING:
return {
...state,
editPool: {
...state.editPool,
editPoolLoading: action.state,
},
};
case EDIT_POOL_SET_VALUE:
if (has(newState.editPool.fields, `${action.page}.${action.field}`)) {
const originPageNameItems = get(
newState.editPool.fields,
`${action.page}`,
{}
);
let newValue: any = {};
newValue[action.field] = action.value;
const joinValue = { ...originPageNameItems, ...newValue };
newState.editPool.fields[action.page] = { ...joinValue };
return { ...newState };
}
return state;
case EDIT_POOL_SET_PAGE_VALID:
const edPoolPV = [...state.editPool.validPages];
if (action.status) {
if (!edPoolPV.includes(action.page)) {
edPoolPV.push(action.page);
newState.editPool.validPages = [...edPoolPV];
}
} else {
const newSetOfPages = edPoolPV.filter((elm) => elm !== action.page);
newState.editPool.validPages = [...newSetOfPages];
}
return { ...newState };
case EDIT_POOL_SET_POOL_STORAGE_CLASSES:
return {
...newState,
editPool: {
...newState.editPool,
storageClasses: action.storageClasses,
},
};
case EDIT_POOL_SET_TOLERATION_VALUE:
const editPoolTolerationValue = [...state.editPool.fields.tolerations];
editPoolTolerationValue[action.index] = action.toleration;
return {
...state,
editPool: {
...state.editPool,
fields: {
...state.editPool.fields,
tolerations: [...editPoolTolerationValue],
},
},
};
case EDIT_POOL_ADD_NEW_TOLERATION:
const editPoolTolerationArray = [
...state.editPool.fields.tolerations,
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
},
];
return {
...state,
editPool: {
...state.editPool,
fields: {
...state.editPool.fields,
tolerations: [...editPoolTolerationArray],
},
},
};
case EDIT_POOL_REMOVE_TOLERATION_ROW:
const removePoolTolerationArray =
state.editPool.fields.tolerations.filter(
(_, index) => index !== action.index
);
return {
...state,
editPool: {
...state.editPool,
fields: {
...state.editPool.fields,
tolerations: [...removePoolTolerationArray],
},
},
};
case EDIT_POOL_SET_KEY_PAIR_VALUE:
return {
...state,
editPool: {
...state.editPool,
fields: {
...state.editPool.fields,
nodeSelectorPairs: action.newArray,
},
},
};
case EDIT_POOL_RESET_FORM:
return {
...state,
editPool: {
editPoolLoading: false,
validPages: ["setup", "affinity", "configure"],
storageClasses: [],
limitSize: {},
fields: {
setup: {
numberOfNodes: 0,
storageClass: "",
volumeSize: 0,
volumesPerServer: 0,
},
affinity: {
nodeSelectorLabels: "",
podAffinity: "default",
withPodAntiAffinity: true,
},
configuration: {
securityContextEnabled: false,
securityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
},
nodeSelectorPairs: [{ key: "", value: "" }],
tolerations: [
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
},
],
},
},
};
default:
return state;
}

View File

@@ -22,7 +22,7 @@ import {
IGemaltoCredentials,
ITolerationModel,
} from "../../../common/types";
import { IResourcesSize, ITenant } from "./ListTenants/types";
import { IPool, IResourcesSize, ITenant } from "./ListTenants/types";
import { KeyPair, Opts } from "./ListTenants/utils";
import { IntegrationConfiguration } from "./AddTenant/Steps/TenantResources/utils";
@@ -99,8 +99,23 @@ export const ADD_POOL_ADD_NEW_TOLERATION = "ADD_POOL/ADD_NEW_TOLERATION";
export const ADD_POOL_REMOVE_TOLERATION_ROW = "ADD_POOL/REMOVE_TOLERATION_ROW";
// Pool Details
export const POOL_DETAILS_SET_OPEN_DETAILS = "POOL_DETAILS/SET_OPEN_DETAILS";
export const POOL_DETAILS_SET_SELECTED_POOL = "POOL_DETAILS/SET_SELECTED_POOL";
// Edit Pool
export const EDIT_POOL_SET_INITIAL_INFO = "EDIT_POOL/SET_INITIAL_INFO";
export const EDIT_POOL_SET_POOL_STORAGE_CLASSES =
"EDIT_POOL/SET_POOL_STORAGE_CLASSES";
export const EDIT_POOL_SET_PAGE_VALID = "EDIT_POOL/SET_PAGE_VALID";
export const EDIT_POOL_SET_VALUE = "EDIT_POOL/SET_VALUE";
export const EDIT_POOL_SET_LOADING = "EDIT_POOL/SET_LOADING";
export const EDIT_POOL_RESET_FORM = "EDIT_POOL/RESET_FORM";
export const EDIT_POOL_SET_KEY_PAIR_VALUE = "EDIT_POOL/SET_KEY_PAIR_VALUE";
export const EDIT_POOL_SET_TOLERATION_VALUE = "EDIT_POOL/SET_TOLERATION_VALUE";
export const EDIT_POOL_ADD_NEW_TOLERATION = "EDIT_POOL/ADD_NEW_TOLERATION";
export const EDIT_POOL_REMOVE_TOLERATION_ROW =
"EDIT_POOL/REMOVE_TOLERATION_ROW";
export interface ICertificateInfo {
name: string;
serialNumber: string;
@@ -367,6 +382,7 @@ export interface ITenantDetails {
loadingTenant: boolean;
tenantInfo: ITenant | null;
currentTab: string;
poolDetailsOpen: boolean;
selectedPool: string | null;
}
@@ -374,6 +390,7 @@ export interface ITenantState {
createTenant: ICreateTenant;
tenantDetails: ITenantDetails;
addPool: IAddPool;
editPool: IEditPool;
}
export interface ILabelKeyPair {
@@ -421,6 +438,29 @@ export interface IAddPool {
fields: IAddPoolFields;
}
export interface IEditPoolSetup {
numberOfNodes: number;
volumeSize: number;
volumesPerServer: number;
storageClass: string;
}
export interface IEditPoolFields {
setup: IEditPoolSetup;
affinity: ITenantAffinity;
configuration: IPoolConfiguration;
tolerations: ITolerationModel[];
nodeSelectorPairs: LabelKeyPair[];
}
export interface IEditPool {
editPoolLoading: boolean;
validPages: string[];
storageClasses: Opts[];
limitSize: any;
fields: IEditPoolFields;
}
export interface ITenantIdentityProviderResponse {
oidc?: {
callback_url: string;
@@ -662,11 +702,68 @@ interface SetPoolSelectorKeyPairValueArray {
newArray: LabelKeyPair[];
}
interface SetDetailsOpen {
type: typeof POOL_DETAILS_SET_OPEN_DETAILS;
state: boolean;
}
interface SetSelectedPool {
type: typeof POOL_DETAILS_SET_SELECTED_POOL;
pool: string | null;
}
interface EditPoolSetInitialInformation {
type: typeof EDIT_POOL_SET_INITIAL_INFO;
pool: IPool;
}
interface EditPoolLoading {
type: typeof EDIT_POOL_SET_LOADING;
state: boolean;
}
interface ResetEditPoolForm {
type: typeof EDIT_POOL_RESET_FORM;
}
interface SetEditPoolFieldValue {
type: typeof EDIT_POOL_SET_VALUE;
page: keyof IAddPoolFields;
field: string;
value: any;
}
interface EditPoolPageValid {
type: typeof EDIT_POOL_SET_PAGE_VALID;
page: string;
status: boolean;
}
interface EditPoolStorageClasses {
type: typeof EDIT_POOL_SET_POOL_STORAGE_CLASSES;
storageClasses: Opts[];
}
interface EditPoolTolerationValue {
type: typeof EDIT_POOL_SET_TOLERATION_VALUE;
index: number;
toleration: ITolerationModel;
}
interface EditPoolToleration {
type: typeof EDIT_POOL_ADD_NEW_TOLERATION;
}
interface EditRemovePoolTolerationRow {
type: typeof EDIT_POOL_REMOVE_TOLERATION_ROW;
index: number;
}
interface EditPoolSelectorKeyPairValueArray {
type: typeof EDIT_POOL_SET_KEY_PAIR_VALUE;
newArray: LabelKeyPair[];
}
export type FieldsToHandle = INameTenantFields;
export type TenantsManagementTypes =
@@ -709,4 +806,15 @@ export type TenantsManagementTypes =
| AddNewPoolToleration
| RemovePoolTolerationRow
| SetPoolSelectorKeyPairValueArray
| SetSelectedPool;
| SetSelectedPool
| SetDetailsOpen
| EditPoolLoading
| ResetEditPoolForm
| SetEditPoolFieldValue
| EditPoolPageValid
| EditPoolStorageClasses
| EditPoolTolerationValue
| EditPoolToleration
| EditRemovePoolTolerationRow
| EditPoolSelectorKeyPairValueArray
| EditPoolSetInitialInformation;