From cb7513e9f07b80fd181fb2b8d7ef9f6489fc4f2d Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Wed, 16 Sep 2020 23:47:38 -0500 Subject: [PATCH] Replaces create tenant functionality (#278) Co-authored-by: Benjamin Perez --- portal-ui/src/common/types.ts | 333 +++ portal-ui/src/common/utils.ts | 235 ++- .../Buckets/ListBuckets/DeleteBucket.tsx | 16 +- .../FileSelector/FileSelector.tsx | 113 ++ .../FormComponents/FileSelector/utils.ts | 34 + .../SelectWrapper/SelectWrapper.tsx | 5 +- .../Console/Tenants/ListTenants/AddTenant.tsx | 1785 ++++++++++++----- portal-ui/src/utils/validationFunctions.ts | 8 + 8 files changed, 2029 insertions(+), 500 deletions(-) create mode 100644 portal-ui/src/common/types.ts create mode 100644 portal-ui/src/screens/Console/Common/FormComponents/FileSelector/FileSelector.tsx create mode 100644 portal-ui/src/screens/Console/Common/FormComponents/FileSelector/utils.ts diff --git a/portal-ui/src/common/types.ts b/portal-ui/src/common/types.ts new file mode 100644 index 000000000..a2907e8a3 --- /dev/null +++ b/portal-ui/src/common/types.ts @@ -0,0 +1,333 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2020 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 . + +/* Copyright (c) 2020 MinIO, Inc. All rights reserved. */ + +export interface ITenantsObject { + tenants: ITenant[]; +} + +export interface ITenant { + creation_date: string; + deletion_date: string; + currentState: string; + image: string; + console_image: string; + instance_count: string; + name: string; + namespace?: string; + total_size: string; + used_size: string; + volume_count: string; + volume_size: string; + volumes_per_server?: string; + zone_count: string; + zones?: IZoneModel[]; + used_capacity?: string; + endpoint?: string; + storage_class?: string; + enable_prometheus: boolean; +} + +export interface IVolumeConfiguration { + size: string; + storage_class_name: string; + labels?: any; +} + +export interface ITenantCreator { + name: string; + service_name: string; + enable_console: boolean; + enable_prometheus: boolean; + enable_tls: boolean; + access_key: string; + secret_key: string; + image: string; + console_image: string; + zones: IZoneModel[]; + namespace: string; + erasureCodingParity: number; + tls?: ITLSTenantConfiguration; + encryption?: IEncryptionConfiguration; + idp?: IIDPConfiguration; + annotations?: Object; +} + +export interface ITenantUpdateObject { + image: string; + image_registry?: IRegistryObject; +} + +export interface IRegistryObject { + registry: string; + username: string; + password: string; +} + +export interface ITenantUsage { + used: string; + disk_used: string; +} + +export interface IAffinityModel { + podAntiAffinity: IPodAntiAffinityModel; +} + +export interface IPodAntiAffinityModel { + requiredDuringSchedulingIgnoredDuringExecution: IPodAffinityTerm[]; +} + +export interface IPodAffinityTerm { + labelSelector: IPodAffinityTermLabelSelector; + topologyKey: string; +} + +export interface IPodAffinityTermLabelSelector { + matchExpressions: IMatchExpressionItem[]; +} + +export interface IMatchExpressionItem { + key: string; + operator: string; + values: string[]; +} + +export interface ITolerationModel { + effect: string; + key: string; + operator: string; + value?: string; + tolerationSeconds?: ITolerationSeconds; +} + +export interface ITolerationSeconds { + seconds: number; +} + +export interface IResourceModel { + requests: IResourceRequests; + limits?: IResourceLimits; +} + +export interface IResourceRequests { + memory: number; +} + +export interface IResourceLimits { + memory: number; +} + +export interface ITLSTenantConfiguration { + minio: ITLSConfiguration; + console: ITLSConfiguration; +} + +export interface ITLSConfiguration { + crt: string; + key: string; +} + +export interface IEncryptionConfiguration { + server: ITLSConfiguration; + client: ITLSConfiguration; + master_key?: string; + gemalto?: IGemaltoConfig; + aws?: IAWSConfig; + vault?: IVaultConfig; +} + +export interface IVaultConfig { + endpoint: string; + engine?: string; + namespace?: string; + prefix?: string; + approle: IApproleConfig; + tls: IVaultTLSConfig; + status: IVaultStatusConfig; +} + +export interface IGemaltoConfig { + keysecure: IKeysecureConfig; +} + +export interface IAWSConfig { + secretsmanager: ISecretsManagerConfig; +} + +export interface IApproleConfig { + engine: string; + id: string; + secret: string; + retry: number; +} + +export interface IVaultTLSConfig { + key: string; + crt: string; + ca: string; +} + +export interface IVaultStatusConfig { + ping: number; +} + +export interface IKeysecureConfig { + endpoint: string; + credentials: IGemaltoCredentials; + tls: IGemaltoTLS; +} + +export interface IGemaltoCredentials { + token: string; + domain: string; + retry?: number; +} + +export interface IGemaltoTLS { + ca: string; +} + +export interface ISecretsManagerConfig { + endpoint: string; + region: string; + kmskey?: string; + credentials: IAWSCredentials; +} + +export interface IAWSCredentials { + accesskey: string; + secretkey: string; + token?: string; +} + +export interface IIDPConfiguration { + oidc?: IOpenIDConfiguration; + active_directory: IActiveDirectoryConfiguration; +} + +export interface IOpenIDConfiguration { + url: string; + client_id: string; + secret_id: string; +} + +export interface IActiveDirectoryConfiguration { + url: string; + skip_tls_verification: boolean; + server_insecure: boolean; + user_search_filter: string; + group_Search_base_dn: string; + group_search_filter: string; + group_name_attribute: string; +} + +export interface IStorageDistribution { + error: number; + nodes: number; + persistentVolumes: number; + disks: number; + pvSize: number; +} + +export interface IErasureCodeCalc { + error: number; + maxEC: string; + erasureCodeSet: number; + rawCapacity: string; + storageFactors: IStorageFactors[]; + defaultEC: string; +} + +export interface IStorageFactors { + erasureCode: string; + storageFactor: number; + maxCapacity: string; +} + +export interface ITenantHealthInList { + name: string; + namespace: string; + status?: string; + message?: string; +} + +export interface ITenantsListHealthRequest { + tenants: ITenantHealthInList[]; +} + +export interface IMaxAllocatableMemoryRequest { + num_nodes: number; +} + +export interface IMaxAllocatableMemoryResponse { + max_memory: number; +} + +export interface IEncryptionUpdateRequest { + encryption: IEncryptionConfiguration; +} + +export interface IArchivedTenantsList { + tenants: IArchivedTenant[]; +} + +export interface IArchivedTenant { + namespace: string; + tenant: string; + number_volumes: number; + capacity: number; +} + +export interface IZoneModel { + name?: string; + servers: number; + volumes_per_server: number; + volume_configuration: IVolumeConfiguration; + affinity?: IAffinityModel; + tolerations?: ITolerationModel[]; + resources?: IResourceModel; +} + +export interface IUpdateZone { + zones: IZoneModel[]; +} + +export interface INode { + name: string; + freeSpace: string; + totalSpace: string; + disks: IDisk[]; +} + +export interface IStorageType { + freeSpace: string; + totalSpace: string; + storageClasses: string[]; + nodes: INode[]; + schedulableNodes: INode[]; +} + +export interface IDisk { + name: string; + freeSpace: string; + totalSpace: string; +} + +export interface ICapacity { + value: string; + unit: string; +} diff --git a/portal-ui/src/common/utils.ts b/portal-ui/src/common/utils.ts index 3b3e1626d..2b0ac970d 100644 --- a/portal-ui/src/common/utils.ts +++ b/portal-ui/src/common/utils.ts @@ -15,6 +15,10 @@ // along with this program. If not, see . import storage from "local-storage-fallback"; +import { ICapacity, IStorageType, IZoneModel } from "./types"; + +const minStReq = 1073741824; // Minimal Space required for MinIO +const minMemReq = 2147483648; // Minimal Memory required for MinIO in bytes export const units = [ "B", @@ -28,6 +32,8 @@ export const units = [ "YiB", ]; export const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"]; +export const k8sCalcUnits = ["B", ...k8sUnits]; + export const niceBytes = (x: string) => { let l = 0, n = parseInt(x, 10) || 0; @@ -90,12 +96,19 @@ export const k8sfactorForDropdown = () => { }; //getBytes, converts from a value and a unit from units array to bytes -export const getBytes = (value: string, unit: string) => { +export const getBytes = ( + value: string, + unit: string, + fork8s: boolean = false +) => { const vl: number = parseFloat(value); - const powFactor = units.findIndex((element) => element === unit); + + const unitsTake = fork8s ? k8sCalcUnits : units; + + const powFactor = unitsTake.findIndex((element) => element === unit); if (powFactor == -1) { - return 0; + return "0"; } const factor = Math.pow(1024, powFactor); const total = vl * factor; @@ -105,6 +118,220 @@ export const getBytes = (value: string, unit: string) => { //getTotalSize gets the total size of a value & unit export const getTotalSize = (value: string, unit: string) => { - const bytes = getBytes(value, unit).toString(10); + const bytes = getBytes(value, unit, true).toString(); return niceBytes(bytes); }; + +export const setMemoryResource = ( + memorySize: number, + capacitySize: string, + maxMemorySize: number +) => { + // value always comes as Gi + const requestedSizeBytes = getBytes(memorySize.toString(10), "Gi", true); + const memReqSize = parseInt(requestedSizeBytes, 10); + if (maxMemorySize === 0) { + return { + error: "There is no memory available for the selected number of nodes", + request: 0, + limit: 0, + }; + } + + if (maxMemorySize < minMemReq) { + return { + error: "There are not enough memory resources available", + request: 0, + limit: 0, + }; + } + + if (memReqSize < minMemReq) { + return { + error: "The requested memory size must be greater than 2Gi", + request: 0, + limit: 0, + }; + } + if (memReqSize > maxMemorySize) { + return { + error: + "The requested memory is greater than the max available memory for the selected number of nodes", + request: 0, + limit: 0, + }; + } + + const capSize = parseInt(capacitySize, 10); + let memLimitSize = memReqSize; + // set memory limit based on the capacitySize + // if capacity size is lower than 1TiB we use the limit equal to request + if (capSize >= parseInt(getBytes("1", "Pi", true), 10)) { + memLimitSize = Math.max( + memReqSize, + parseInt(getBytes("64", "Gi", true), 10) + ); + } else if (capSize >= parseInt(getBytes("100", "Ti"), 10)) { + memLimitSize = Math.max( + memReqSize, + parseInt(getBytes("32", "Gi", true), 10) + ); + } else if (capSize >= parseInt(getBytes("10", "Ti"), 10)) { + memLimitSize = Math.max( + memReqSize, + parseInt(getBytes("16", "Gi", true), 10) + ); + } else if (capSize >= parseInt(getBytes("1", "Ti"), 10)) { + memLimitSize = Math.max( + memReqSize, + parseInt(getBytes("8", "Gi", true), 10) + ); + } + + return { + error: "", + request: memReqSize, + limit: memLimitSize, + }; +}; + +export const calculateDistribution = ( + capacityToUse: ICapacity, + forcedNodes: number = 0, + limitSize: number = 0 +) => { + let numberOfNodes = {}; + const requestedSizeBytes = getBytes( + capacityToUse.value, + capacityToUse.unit, + true + ); + + if (parseInt(requestedSizeBytes, 10) < minStReq) { + return { + error: "The zone size must be greater than 1Gi", + nodes: 0, + persistentVolumes: 0, + disks: 0, + pvSize: 0, + }; + } + + if (forcedNodes < 4) { + return { + error: "Number of nodes cannot be less than 4", + nodes: 0, + persistentVolumes: 0, + disks: 0, + pvSize: 0, + }; + } + + numberOfNodes = calculateStorage(requestedSizeBytes, forcedNodes, limitSize); + + return numberOfNodes; +}; + +const calculateStorage = ( + requestedBytes: string, + forcedNodes: number, + limitSize: number +) => { + // Size validation + const intReqBytes = parseInt(requestedBytes, 10); + const maxDiskSize = minStReq * 256; // 256 GiB + + // We get the distribution + return structureCalc(forcedNodes, intReqBytes, maxDiskSize, limitSize); +}; + +const structureCalc = ( + nodes: number, + desiredCapacity: number, + maxDiskSize: number, + maxClusterSize: number, + disksPerNode: number = 0 +) => { + if ( + isNaN(nodes) || + isNaN(desiredCapacity) || + isNaN(maxDiskSize) || + isNaN(maxClusterSize) + ) { + return { + error: "Some provided data is invalid, please try again.", + nodes: 0, + persistentVolumes: 0, + disks: 0, + volumePerDisk: 0, + }; // Invalid Data + } + + let persistentVolumeSize = 0; + let numberPersistentVolumes = 0; + let volumesPerServer = 0; + + if (disksPerNode === 0) { + persistentVolumeSize = Math.floor( + Math.min(desiredCapacity / Math.max(4, nodes), maxDiskSize) + ); // pVS = min((desiredCapacity / max(4 | nodes)) | maxDiskSize) + + numberPersistentVolumes = desiredCapacity / persistentVolumeSize; // nPV = dC / pVS + volumesPerServer = numberPersistentVolumes / nodes; // vPS = nPV / n + } + + if (disksPerNode) { + volumesPerServer = disksPerNode; + numberPersistentVolumes = volumesPerServer * nodes; + persistentVolumeSize = Math.floor( + desiredCapacity / numberPersistentVolumes + ); + } + + // Volumes are not exact, we force the volumes number & minimize the volume size + if (volumesPerServer % 1 > 0) { + volumesPerServer = Math.ceil(volumesPerServer); // Increment of volumes per server + numberPersistentVolumes = volumesPerServer * nodes; // nPV = vPS * n + persistentVolumeSize = Math.floor( + desiredCapacity / numberPersistentVolumes + ); // pVS = dC / nPV + + const limitSize = persistentVolumeSize * volumesPerServer * nodes; // lS = pVS * vPS * n + + if (limitSize > maxClusterSize) { + return { + error: "We were not able to allocate this server.", + nodes: 0, + persistentVolumes: 0, + disks: 0, + volumePerDisk: 0, + }; // Cannot allocate this server + } + } + + if (persistentVolumeSize < minStReq) { + return { + error: + "Disk Size with this combination would be less than 1Gi, please try another combination", + nodes: 0, + persistentVolumes: 0, + disks: 0, + volumePerDisk: 0, + }; // Cannot allocate this volume size + } + + return { + error: "", + nodes, + persistentVolumes: numberPersistentVolumes, + disks: volumesPerServer, + pvSize: persistentVolumeSize, + }; +}; + +// Zone Name Generator +export const generateZoneName = (zones: IZoneModel[]) => { + const zoneCounter = zones.length; + + return `zone-${zoneCounter}`; +}; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/DeleteBucket.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/DeleteBucket.tsx index b4689e84d..6761a2dcf 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/DeleteBucket.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/DeleteBucket.tsx @@ -23,7 +23,7 @@ import { DialogContent, DialogContentText, DialogTitle, - LinearProgress + LinearProgress, } from "@material-ui/core"; import api from "../../../../common/api"; import { BucketList } from "../types"; @@ -32,8 +32,8 @@ import Typography from "@material-ui/core/Typography"; const styles = (theme: Theme) => createStyles({ errorBlock: { - color: "red" - } + color: "red", + }, }); interface IDeleteBucketProps { @@ -54,7 +54,7 @@ class DeleteBucket extends React.Component< > { state: IDeleteBucketState = { deleteLoading: false, - deleteError: "" + deleteError: "", }; removeRecord() { @@ -66,23 +66,23 @@ class DeleteBucket extends React.Component< this.setState({ deleteLoading: true }, () => { api .invoke("DELETE", `/api/v1/buckets/${selectedBucket}`, { - name: selectedBucket + name: selectedBucket, }) .then((res: BucketList) => { this.setState( { deleteLoading: false, - deleteError: "" + deleteError: "", }, () => { this.props.closeDeleteModalAndRefresh(true); } ); }) - .catch(err => { + .catch((err) => { this.setState({ deleteLoading: false, - deleteError: err + deleteError: err, }); }); }); diff --git a/portal-ui/src/screens/Console/Common/FormComponents/FileSelector/FileSelector.tsx b/portal-ui/src/screens/Console/Common/FormComponents/FileSelector/FileSelector.tsx new file mode 100644 index 000000000..87ae53e56 --- /dev/null +++ b/portal-ui/src/screens/Console/Common/FormComponents/FileSelector/FileSelector.tsx @@ -0,0 +1,113 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2020 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 . + +import React from "react"; +import { Grid, InputLabel, Tooltip } from "@material-ui/core"; +import HelpIcon from "@material-ui/icons/Help"; +import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; +import { fieldBasic, tooltipHelper } from "../common/styleLibrary"; +import { fileProcess } from "./utils"; + +interface InputBoxProps { + label: string; + classes: any; + onChange: (e: string) => void; + id: string; + name: string; + disabled?: boolean; + tooltip?: string; + required?: boolean; + error?: string; + accept?: string; +} + +const styles = (theme: Theme) => + createStyles({ + ...fieldBasic, + ...tooltipHelper, + textBoxContainer: { + flexGrow: 1, + position: "relative", + }, + errorState: { + color: "#b53b4b", + fontSize: 14, + position: "absolute", + top: 7, + right: 7, + }, + }); + +const FileSelector = ({ + label, + classes, + onChange, + id, + name, + disabled = false, + tooltip = "", + required, + error = "", + accept = "", +}: InputBoxProps) => { + return ( + + + {label !== "" && ( + + + {label} + {required ? "*" : ""} + + {tooltip !== "" && ( +
+ + + +
+ )} +
+ )} +
+ { + fileProcess(e, (data: any) => { + onChange(data); + }); + }} + accept={accept} + required + /> +
+
+
+ ); +}; + +export default withStyles(styles)(FileSelector); diff --git a/portal-ui/src/screens/Console/Common/FormComponents/FileSelector/utils.ts b/portal-ui/src/screens/Console/Common/FormComponents/FileSelector/utils.ts new file mode 100644 index 000000000..cb137871e --- /dev/null +++ b/portal-ui/src/screens/Console/Common/FormComponents/FileSelector/utils.ts @@ -0,0 +1,34 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2020 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 . + +export const fileProcess = (evt: any, callback: any) => { + const file = evt.target.files[0]; + const reader = new FileReader(); + reader.readAsDataURL(file); + + reader.onload = () => { + // reader.readAsDataURL(file) output will be something like: data:application/x-x509-ca-cert;base64,LS0tLS1CRUdJTiBDRVJUSU + // we care only about the actual base64 part (everything after "data:application/x-x509-ca-cert;base64,") + const fileBase64 = reader.result; + if (fileBase64) { + const fileArray = fileBase64.toString().split("base64,"); + + if (fileArray.length === 2) { + callback(fileArray[1]); + } + } + }; +}; diff --git a/portal-ui/src/screens/Console/Common/FormComponents/SelectWrapper/SelectWrapper.tsx b/portal-ui/src/screens/Console/Common/FormComponents/SelectWrapper/SelectWrapper.tsx index 35865412d..38846cc36 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/SelectWrapper/SelectWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/FormComponents/SelectWrapper/SelectWrapper.tsx @@ -42,6 +42,7 @@ interface SelectProps { onChange: ( e: React.ChangeEvent<{ name?: string | undefined; value: unknown }> ) => void; + disabled?: boolean; classes: any; } @@ -51,7 +52,7 @@ const styles = (theme: Theme) => ...tooltipHelper, inputLabel: { ...fieldBasic.inputLabel, - width: 116, + width: 215, }, }); @@ -88,6 +89,7 @@ const SelectWrapper = ({ label, tooltip = "", value, + disabled = false, }: SelectProps) => { return ( @@ -111,6 +113,7 @@ const SelectWrapper = ({ value={value} onChange={onChange} input={} + disabled={disabled} > {options.map((option) => ( . -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import debounce from "lodash/debounce"; +import get from "lodash/get"; import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; import Grid from "@material-ui/core/Grid"; import Typography from "@material-ui/core/Typography"; @@ -27,11 +29,15 @@ import TableRow from "@material-ui/core/TableRow"; import api from "../../../../common/api"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { modalBasic } from "../../Common/FormComponents/common/styleLibrary"; -import { IVolumeConfiguration, IZone } from "./types"; import CheckboxWrapper from "../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper"; import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper"; -import { k8sfactorForDropdown } from "../../../../common/utils"; -import ZonesMultiSelector from "./ZonesMultiSelector"; +import { + calculateDistribution, + generateZoneName, + getBytes, + k8sfactorForDropdown, + niceBytes, +} from "../../../../common/utils"; import { commonFormValidation, IValidation, @@ -39,6 +45,13 @@ import { import GenericWizard from "../../Common/GenericWizard/GenericWizard"; import { IWizardElement } from "../../Common/GenericWizard/types"; import { NewServiceAccount } from "../../Common/CredentialsPrompt/types"; +import RadioGroupSelector from "../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector"; +import FileSelector from "../../Common/FormComponents/FileSelector/FileSelector"; +import { + IAffinityModel, + ICapacity, + ITenantCreator, +} from "../../../../common/types"; interface IAddTenantProps { open: boolean; @@ -83,6 +96,10 @@ const styles = (theme: Theme) => fontSize: "0.75rem", paddingLeft: 120, }, + error: { + color: "#dc1f2e", + fontSize: "0.75rem", + }, ...modalBasic, }); @@ -101,87 +118,235 @@ const AddTenant = ({ const [addError, setAddError] = useState(""); const [tenantName, setTenantName] = useState(""); const [imageName, setImageName] = useState(""); - const [serviceName, setServiceName] = useState(""); - const [zones, setZones] = useState([]); - const [volumesPerServer, setVolumesPerServer] = useState(0); - const [volumeConfiguration, setVolumeConfiguration] = useState< - IVolumeConfiguration - >({ size: 0, storage_class: "" }); - const [mountPath, setMountPath] = useState(""); - const [accessKey, setAccessKey] = useState(""); - const [secretKey, setSecretKey] = useState(""); - const [enableConsole, setEnableConsole] = useState(true); - const [enableTLS, setEnableTLS] = useState(false); + const [volumeSize, setVolumeSize] = useState("100"); + const [enableTLS, setEnableTLS] = useState(true); const [sizeFactor, setSizeFactor] = useState("Gi"); const [storageClasses, setStorageClassesList] = useState([]); + const [selectedStorageClass, setSelectedStorageClass] = useState(""); const [validationErrors, setValidationErrors] = useState({}); const [namespace, setNamespace] = useState(""); const [advancedMode, setAdvancedMode] = useState(false); + const [enablePrometheus, setEnablePrometheus] = useState(false); + const [consoleImage, setConsoleImage] = useState(""); + const [idpSelection, setIdpSelection] = useState("none"); + const [openIDURL, setOpenIDURL] = useState(""); + const [openIDClientID, setOpenIDClientID] = useState(""); + const [openIDSecretID, setOpenIDSecretID] = useState(""); + const [ADURL, setADURL] = useState(""); + const [ADSkipTLS, setADSkipTLS] = useState(false); + const [ADServerInsecure, setADServerInsecure] = useState(false); + const [ADUserNameFilter, setADUserNameFilter] = useState(""); + const [ADGroupBaseDN, setADGroupBaseDN] = useState(""); + const [ADGroupSearchFilter, setADGroupSearchFilter] = useState(""); + const [ADNameAttribute, setADNameAttribute] = useState(""); + const [tlsType, setTLSType] = useState("autocert"); + const [enableEncryption, setEnableEncryption] = useState(false); + const [encryptionType, setEncryptionType] = useState("vault"); + const [gemaltoEndpoint, setGemaltoEndpoint] = useState(""); + const [gemaltoToken, setGemaltoToken] = useState(""); + const [gemaltoDomain, setGemaltoDomain] = useState(""); + const [gemaltoRetry, setGemaltoRetry] = useState("0"); + const [awsEndpoint, setAWSEndpoint] = useState(""); + const [awsRegion, setAWSRegion] = useState(""); + const [awsKMSKey, setAWSKMSKey] = useState(""); + const [awsAccessKey, setAWSAccessKey] = useState(""); + const [awsSecretKey, setAWSSecretKey] = useState(""); + const [awsToken, setAWSToken] = useState(""); + const [vaultEndpoint, setVaultEndpoint] = useState(""); + const [vaultEngine, setVaultEngine] = useState(""); + const [vaultNamespace, setVaultNamespace] = useState(""); + const [vaultPrefix, setVaultPrefix] = useState(""); + const [vaultAppRoleEngine, setVaultAppRoleEngine] = useState(""); + const [vaultId, setVaultId] = useState(""); + const [vaultSecret, setVaultSecret] = useState(""); + const [vaultRetry, setVaultRetry] = useState("0"); + const [vaultPing, setVaultPing] = useState("0"); + const [ecParityChoices, setECParityChoices] = useState([]); + const [nodes, setNodes] = useState("4"); + const [memoryNode, setMemoryNode] = useState("2"); + const [ecParity, setECParity] = useState(""); + const [distribution, setDistribution] = useState({ + error: "", + nodes: 0, + persistentVolumes: 0, + disks: 0, + volumePerDisk: 0, + }); // Forms Validation const [nameTenantValid, setNameTenantValid] = useState(false); const [configValid, setConfigValid] = useState(false); const [configureValid, setConfigureValid] = useState(false); - const [zonesValid, setZonesValid] = useState(false); // Custom Elements - const [customACCK, setCustomACCK] = useState(false); const [customDockerhub, setCustomDockerhub] = useState(false); + // FilesBase64 + const [filesBase64, setFilesBase64] = useState({ + tlsKey: "", + tlsCert: "", + consoleKey: "", + consoleCert: "", + serverKey: "", + serverCert: "", + clientKey: "", + clientCert: "", + vaultKey: "", + vaultCert: "", + vaultCA: "", + gemaltoCA: "", + }); + + /*Debounce functions*/ + + // Storage Quotas + const getNamespaceInformation = () => { + setSelectedStorageClass(""); + setStorageClassesList([]); + api + .invoke( + "GET", + `/api/v1/namespaces/${namespace}/resourcequotas/${namespace}-storagequota` + ) + .then((res: string[]) => { + const elements = get(res, "elements", []); + + const newStorage = elements.map((storageClass: any) => { + const name = get(storageClass, "name", "").split(".")[0]; + + return { label: name, value: name }; + }); + + setStorageClassesList(newStorage); + + if (newStorage.length > 0) { + setSelectedStorageClass(newStorage[0].value); + } + }) + .catch((err: any) => { + console.log(err); + }); + }; + + const debounceNamespace = useCallback( + debounce(getNamespaceInformation, 500), + [namespace] + ); + useEffect(() => { - fetchStorageClassList(); - }, []); + if (namespace !== "") { + debounceNamespace(); + + // Cancel previous debounce calls during useEffect cleanup. + return debounceNamespace.cancel; + } + }, [namespace, debounceNamespace]); + /*End debounce functions*/ + + /*Calculate Allocation*/ + useEffect(() => { + validateClusterSize(); + }, [nodes, volumeSize, sizeFactor]); + + const validateClusterSize = () => { + const size = volumeSize; + const factor = sizeFactor; + const limitSize = getBytes("12", "Ti", true); + + const capacityElement: ICapacity = { + unit: factor, + value: size.toString(), + }; + + const distrCalculate = calculateDistribution( + capacityElement, + parseInt(nodes), + parseInt(limitSize) + ); + + setDistribution(distrCalculate); + + /*const errorDistribution = get(distrCalculate, "error", ""); + + if (errorDistribution === "") { + const disksPerServer = get(distrCalculate, "disks", 0); + const totalNodes = get(distrCalculate, "nodes", 0); + const sizePerVolume = get(distrCalculate, "pvSize", 0); + + getParity(totalNodes, disksPerServer, sizePerVolume); + }*/ + }; + + /*Calculate Allocation End*/ /* Validations of pages */ useEffect(() => { - const commonValidation = commonFormValidation([validationElements[0]]); - - setNameTenantValid(!("tenant-name" in commonValidation)); - - setValidationErrors(commonValidation); - }, [tenantName]); - - useEffect(() => { - let subValidation = validationElements.slice(1, 3); - - if (!advancedMode) { - subValidation.push({ - fieldKey: "servers", + const commonValidation = commonFormValidation([ + { + fieldKey: "tenant-name", required: true, - pattern: /\d+/, - customPatternMessage: "Field must be numeric", - value: zones.length > 0 ? zones[0].servers.toString(10) : "0", - }); - } + pattern: /^[a-z0-9-]{3,63}$/, + customPatternMessage: + "Name only can contain lowercase letters, numbers and '-'. Min. Length: 3", + value: tenantName, + }, + { + fieldKey: "namespace", + required: true, + value: tenantName, + customValidation: storageClasses.length < 1, + customValidationMessage: "Please enter a valid namespace", + }, + ]); - const commonValidation = commonFormValidation(subValidation); - - setConfigValid( - !("volumes_per_server" in commonValidation) && - !("volume_size" in commonValidation) && - !("servers" in commonValidation) + setNameTenantValid( + !("tenant-name" in commonValidation) && + !("namespace" in commonValidation) && + storageClasses.length > 0 ); setValidationErrors(commonValidation); - }, [volumesPerServer, volumeConfiguration, zones]); + }, [tenantName, namespace, selectedStorageClass, storageClasses]); + + useEffect(() => { + const parsedSize = getBytes(volumeSize, sizeFactor, true); + + const commonValidation = commonFormValidation([ + { + fieldKey: "nodes", + required: true, + value: nodes, + customValidation: parseInt(nodes) < 4, + customValidationMessage: "Number of nodes cannot be less than 4", + }, + { + fieldKey: "volume_size", + required: true, + value: volumeSize, + customValidation: parseInt(parsedSize) < 1073741824, + customValidationMessage: "Volume size must be greater than 1Gi", + }, + { + fieldKey: "memory_per_node", + required: true, + value: memoryNode, + customValidation: parseInt(memoryNode) < 2, + customValidationMessage: "Memory size must be greater than 2Gi", + }, + ]); + + setConfigValid( + !("nodes" in commonValidation) && + !("volume_size" in commonValidation) && + !("memory_per_node" in commonValidation) && + distribution.error === "" + ); + + setValidationErrors(commonValidation); + }, [nodes, volumeSize, sizeFactor, memoryNode, distribution]); useEffect(() => { let customAccountValidation: IValidation[] = []; - if (customACCK) { - customAccountValidation = [ - ...customAccountValidation, - { - fieldKey: "access_key", - required: true, - value: accessKey, - }, - { - fieldKey: "secret_key", - required: true, - value: secretKey, - }, - ]; - } if (customDockerhub) { customAccountValidation = [ @@ -201,56 +366,7 @@ const AddTenant = ({ setConfigureValid(Object.keys(commonVal).length === 0); setValidationErrors(commonVal); - }, [customACCK, customDockerhub, accessKey, secretKey, imageName]); - - useEffect(() => { - const filteredZones = zones.filter( - (zone) => zone.name !== "" && zone.servers !== 0 && !isNaN(zone.servers) - ); - - if (filteredZones.length > 0) { - setZonesValid(true); - setValidationErrors({}); - - return; - } - - setZonesValid(false); - setValidationErrors({ zones_selector: "Please add a valid zone" }); - }, [zones]); - - /* End Validation of pages */ - - const validationElements: IValidation[] = [ - { - fieldKey: "tenant-name", - required: true, - pattern: /^[a-z0-9-]{3,63}$/, - customPatternMessage: - "Name only can contain lowercase letters, numbers and '-'. Min. Length: 3", - value: tenantName, - }, - { - fieldKey: "volumes_per_server", - required: true, - pattern: /\d+/, - customPatternMessage: "Field must be numeric", - value: volumesPerServer.toString(10), - }, - { - fieldKey: "volume_size", - required: true, - pattern: /\d+/, - customPatternMessage: "Field must be numeric", - value: volumeConfiguration.size.toString(10), - }, - - { - fieldKey: "service_name", - required: false, - value: serviceName, - }, - ]; + }, [customDockerhub, imageName]); const clearValidationError = (fieldKey: string) => { const newValidationElement = { ...validationErrors }; @@ -259,105 +375,243 @@ const AddTenant = ({ setValidationErrors(newValidationElement); }; + /* End Validation of pages */ + + /* Send Information to backend */ useEffect(() => { if (addSending) { - let cleanZones = zones.filter( - (zone) => zone.name !== "" && zone.servers > 0 && !isNaN(zone.servers) - ); + const zoneName = generateZoneName([]); - const commonValidation = commonFormValidation(validationElements); + const hardCodedAffinity: IAffinityModel = { + podAntiAffinity: { + requiredDuringSchedulingIgnoredDuringExecution: [ + { + labelSelector: { + matchExpressions: [ + { + key: "v1.min.io/tenant", + operator: "In", + values: [tenantName], + }, + { + key: "v1.min.io/zone", + operator: "In", + values: [zoneName], + }, + ], + }, + topologyKey: "kubernetes.io/hostname", + }, + ], + }, + }; - setValidationErrors(commonValidation); + const ecLimit = "EC:0"; - if (Object.keys(commonValidation).length === 0) { - const data: { [key: string]: any } = { - name: tenantName, - service_name: tenantName, - image: imageName, - enable_tls: enableTLS, - enable_console: enableConsole, - access_key: accessKey, - secret_key: secretKey, - volumes_per_server: volumesPerServer, - volume_configuration: { - size: `${volumeConfiguration.size}${sizeFactor}`, - storage_class: volumeConfiguration.storage_class, + const erasureCode = ecLimit.split(":")[1]; + + let dataSend: ITenantCreator = { + name: tenantName, + namespace: namespace, + access_key: "", + secret_key: "", + enable_tls: enableTLS && tlsType === "autocert", + enable_console: true, + enable_prometheus: enablePrometheus, + service_name: "", + image: imageName, + console_image: consoleImage, + zones: [ + { + name: zoneName, + servers: distribution.nodes, + volumes_per_server: distribution.disks, + volume_configuration: { + size: distribution.pvSize, + storage_class_name: selectedStorageClass, + }, + resources: { + requests: { + memory: parseInt(getBytes(memoryNode, "Gi")), + }, + }, + affinity: hardCodedAffinity, }, - zones: cleanZones, - }; + ], + erasureCodingParity: parseInt(erasureCode, 10), + }; - api - .invoke("POST", `/api/v1/tenants`, data) - .then((res) => { - const newSrvAcc: NewServiceAccount = { - accessKey: res.access_key, - secretKey: res.secret_key, - }; + if (tlsType === "customcert") { + let tenantCerts: any = null; + let consoleCerts: any = null; + if (filesBase64.tlsCert !== "" && filesBase64.tlsKey !== "") { + tenantCerts = { + minio: { + crt: filesBase64.tlsCert, + key: filesBase64.tlsKey, + }, + }; + } - setAddSending(false); - setAddError(""); - closeModalAndRefresh(true, newSrvAcc); - }) - .catch((err) => { - setAddSending(false); - setAddError(err); - }); - } else { - setAddSending(false); - setAddError("Please fix the errors in the form and try again"); + if (filesBase64.consoleCert !== "" && filesBase64.consoleKey !== "") { + consoleCerts = { + console: { + crt: filesBase64.consoleCert, + key: filesBase64.consoleKey, + }, + }; + } + + if (tenantCerts || consoleCerts) { + dataSend = { + ...dataSend, + tls: { + ...tenantCerts, + ...consoleCerts, + }, + }; + } } + + if (enableEncryption) { + let insertEncrypt = {}; + + switch (encryptionType) { + case "gemalto": + insertEncrypt = { + gemalto: { + keysecure: { + endpoint: gemaltoEndpoint, + credentials: { + token: gemaltoToken, + domain: gemaltoDomain, + retry: gemaltoRetry, + }, + tls: { + ca: filesBase64.gemaltoCA, + }, + }, + }, + }; + break; + case "AWS": + insertEncrypt = { + aws: { + secretsmanager: { + endpoint: awsEndpoint, + region: awsRegion, + kmskey: awsKMSKey, + credentials: { + accesskey: awsAccessKey, + secretkey: awsSecretKey, + token: awsToken, + }, + }, + }, + }; + break; + case "vault": + insertEncrypt = { + vault: { + endpoint: vaultEndpoint, + engine: vaultEngine, + namespace: vaultNamespace, + prefix: vaultPrefix, + approle: { + engine: vaultAppRoleEngine, + id: vaultId, + secret: vaultSecret, + retry: vaultRetry, + }, + tls: { + key: filesBase64.vaultKey, + crt: filesBase64.vaultCert, + ca: filesBase64.vaultCA, + }, + status: { + ping: vaultPing, + }, + }, + }; + break; + } + + dataSend = { + ...dataSend, + encryption: { + client: { + key: filesBase64.clientKey, + crt: filesBase64.clientCert, + }, + server: { + key: filesBase64.serverKey, + crt: filesBase64.serverCert, + }, + ...insertEncrypt, + }, + }; + } + + if (idpSelection !== "none") { + let dataIDP: any = {}; + + switch (idpSelection) { + case "OpenID": + dataIDP = { + oidc: { + url: openIDURL, + client_id: openIDClientID, + secret_id: openIDSecretID, + }, + }; + break; + case "AD": + dataIDP = { + active_directory: { + url: ADURL, + skip_tls_verification: ADSkipTLS, + server_insecure: ADServerInsecure, + username_format: "", + user_search_filter: ADUserNameFilter, + group_search_base_dn: ADGroupBaseDN, + group_search_filter: ADGroupSearchFilter, + group_name_attribute: ADNameAttribute, + }, + }; + break; + } + + dataSend = { + ...dataSend, + idp: { ...dataIDP }, + }; + } + + api + .invoke("POST", `/api/v1/tenants`, dataSend) + .then((res) => { + const newSrvAcc: NewServiceAccount = { + accessKey: res.access_key, + secretKey: res.secret_key, + }; + + setAddSending(false); + setAddError(""); + closeModalAndRefresh(true, newSrvAcc); + }) + .catch((err) => { + setAddSending(false); + setAddError(err); + }); } }, [addSending]); - useEffect(() => { - if (advancedMode) { - setZones([{ name: "zone-1", servers: 0, capacity: "0", volumes: 0 }]); - } else { - setZones([{ name: "zone-1", servers: 1, capacity: "0", volumes: 0 }]); - } - }, [advancedMode]); + const storeCertInObject = (certName: string, certValue: string) => { + const copyCurrentList = { ...filesBase64 }; - const setVolumeConfig = (item: string, value: string) => { - const volumeCopy: IVolumeConfiguration = { - size: item !== "size" ? volumeConfiguration.size : parseInt(value), - storage_class: - item !== "storage_class" ? volumeConfiguration.storage_class : value, - }; + copyCurrentList[certName] = certValue; - setVolumeConfiguration(volumeCopy); - }; - - const setServersSimple = (value: string) => { - const copyZone = [...zones]; - - copyZone[0].servers = parseInt(value, 10); - - setZones(copyZone); - }; - - const fetchStorageClassList = () => { - api - .invoke("GET", `/api/v1/storage-classes`) - .then((res: string[]) => { - let classes: string[] = []; - if (res !== null) { - classes = res; - } - setStorageClassesList( - classes.map((s: string) => ({ - label: s, - value: s, - })) - ); - - const newStorage = { ...volumeConfiguration }; - newStorage.storage_class = res[0]; - - setVolumeConfiguration(newStorage); - }) - .catch((err: any) => { - console.log(err); - }); + setFilesBase64(copyCurrentList); }; const cancelButton = { @@ -386,16 +640,48 @@ const AddTenant = ({ setTenantName(e.target.value); clearValidationError("tenant-name"); }} - label="Tenant Name" + label="Name" value={tenantName} required error={validationErrors["tenant-name"] || ""} /> + + ) => { + setNamespace(e.target.value); + clearValidationError("namespace"); + }} + label="Namespace" + value={namespace} + error={validationErrors["namespace"] || ""} + required + /> + + + ) => { + console.log(e.target.value as string); + setSelectedStorageClass(e.target.value as string); + }} + label="Storage Class" + value={selectedStorageClass || ""} + options={storageClasses} + disabled={storageClasses.length < 1} + /> +
- Use Advanced mode to configure additional options in the tenant + Check 'Advanced Mode' for additional configuration options, such + as IDP, Disk Encryption, and customized TLS/SSL Certificates. +
+ Leave 'Advanced Mode' unchecked to use the secure default settings + for the tenant.


@@ -422,82 +708,32 @@ const AddTenant = ({ }, { label: "Configure", + advancedOnly: true, componentRender: (

Configure

Basic configurations for tenant management
+ { const targetD = e.target; const checked = targetD.checked; - setCustomACCK(checked); + setCustomDockerhub(checked); }} - label={"Use Custom Access Keys"} + label={"Use custom image"} /> - {customACCK && ( - - Please enter your access & secret keys - - ) => { - setAccessKey(e.target.value); - clearValidationError("access_key"); - }} - label="Access Key" - value={accessKey} - error={validationErrors["access_key"] || ""} - required - /> - - - ) => { - setSecretKey(e.target.value); - clearValidationError("secret_key"); - }} - label="Secret Key" - value={secretKey} - error={validationErrors["secret_key"] || ""} - required - /> - - - )} - - {advancedMode && ( - - { - const targetD = e.target; - const checked = targetD.checked; - - setCustomDockerhub(checked); - }} - label={"Use custom image"} - /> - - )} - {customDockerhub && ( - Please enter the MinIO image from dockerhub + Please enter the MinIO image from dockerhub to use + + ) => { + setConsoleImage(e.target.value); + clearValidationError("consoleImage"); + }} + label="Console's Image" + value={consoleImage} + error={validationErrors["consoleImage"] || ""} + placeholder="Eg. minio/console:v0.3.13" + required + /> + )} + + { + const targetD = e.target; + const checked = targetD.checked; + + setEnablePrometheus(checked); + }} + label={"Enable prometheus integration"} + /> +
), buttons: [ @@ -524,71 +790,185 @@ const AddTenant = ({ ], }, { - label: "Service Configuration", + label: "IDP", advancedOnly: true, componentRender: (
-

Service Configuration

-
- - ) => { - setServiceName(e.target.value); - clearValidationError("service_name"); - }} - label="Service Name" - value={serviceName} - error={validationErrors["service_name"] || ""} - /> - - - ) => { - setNamespace(e.target.value); - clearValidationError("namespace"); - }} - label="Namespace" - value={namespace} - error={validationErrors["namespace"] || ""} - /> - -
- ), - buttons: [ - cancelButton, - { label: "Back", type: "back", enabled: true }, - { label: "Next", type: "next", enabled: true }, - ], - }, - { - label: "Storage Class", - advancedOnly: true, - componentRender: ( - -
-

Choose your prefered Storage Class

+

IDP

- Review the storage classes available in the tenant and decide - which one to allocate the tenant to + Access to the tenant can be controlled via an external Identity + Manager.
- ) => { - setVolumeConfig("storage_class", e.target.value as string); + { + setIdpSelection(e.target.value); }} - label="Storage Class" - value={volumeConfiguration.storage_class} - options={storageClasses} + selectorOptions={[ + { label: "None", value: "none" }, + { label: "OpenID", value: "OpenID" }, + { label: "Active Directory", value: "AD" }, + ]} /> + MinIO supports both OpenID and Active Directory + + {idpSelection === "OpenID" && ( + + + ) => { + setOpenIDURL(e.target.value); + clearValidationError("openID_URL"); + }} + label="URL" + value={openIDURL} + error={validationErrors["openID_URL"] || ""} + required + /> + + + ) => { + setOpenIDClientID(e.target.value); + clearValidationError("openID_clientID"); + }} + label="Client ID" + value={openIDClientID} + error={validationErrors["openID_clientID"] || ""} + required + /> + + + ) => { + setOpenIDSecretID(e.target.value); + clearValidationError("openID_secretID"); + }} + label="Secret ID" + value={openIDSecretID} + error={validationErrors["openID_secretID"] || ""} + required + /> + + + )} + {idpSelection === "AD" && ( + + + ) => { + setADURL(e.target.value); + clearValidationError("AD_URL"); + }} + label="URL" + value={ADURL} + error={validationErrors["AD_URL"] || ""} + required + /> + + + { + const targetD = e.target; + const checked = targetD.checked; + + setADSkipTLS(checked); + }} + label={"Skip TLS Verification"} + /> + + + { + const targetD = e.target; + const checked = targetD.checked; + + setADServerInsecure(checked); + }} + label={"Server Insecure"} + /> + + + ) => { + setADUserNameFilter(e.target.value); + clearValidationError("ad_userNameFilter"); + }} + label="User Search Filter" + value={ADUserNameFilter} + error={validationErrors["ad_userNameFilter"] || ""} + required + /> + + + ) => { + setADGroupBaseDN(e.target.value); + clearValidationError("ad_groupBaseDN"); + }} + label="Group Search Base DN" + value={ADGroupBaseDN} + error={validationErrors["ad_groupBaseDN"] || ""} + required + /> + + + ) => { + setADGroupSearchFilter(e.target.value); + clearValidationError("ad_groupSearchFilter"); + }} + label="Group Search Filter" + value={ADGroupSearchFilter} + error={validationErrors["ad_groupSearchFilter"] || ""} + required + /> + + + ) => { + setADNameAttribute(e.target.value); + clearValidationError("ad_nameAttribute"); + }} + label="Group Name Attribute" + value={ADNameAttribute} + error={validationErrors["ad_nameAttribute"] || ""} + required + /> + + + )}
), buttons: [ @@ -598,59 +978,558 @@ const AddTenant = ({ ], }, { - label: "Server Configuration", + label: "Security", + advancedOnly: true, componentRender: (
-

Server Configuration

- Define the server configuration +

Security

- {advancedMode && ( - - ) => { - setMountPath(e.target.value); - }} - label="Mount Path" - value={mountPath} - /> - - )} + + { + const targetD = e.target; + const checked = targetD.checked; - {!advancedMode && ( - - ) => { - setServersSimple(e.target.value); - clearValidationError("servers"); - }} - label="Number of Servers" - value={zones.length > 0 ? zones[0].servers.toString(10) : "0"} - min="0" - required - error={validationErrors["servers"] || ""} - /> - + setEnableTLS(checked); + }} + label={"Enable TLS"} + /> + Enable TLS for the tenant, this is required for Encryption + Configuration + + {enableTLS && ( + + + { + setTLSType(e.target.value); + }} + selectorOptions={[ + { label: "Autocert", value: "autocert" }, + { label: "Custom Certificate", value: "customcert" }, + ]} + /> + + {tlsType !== "autocert" && ( + +
MinIO TLS Certs
+ + { + storeCertInObject("tlsKey", encodedValue); + }} + accept=".key,.pem" + id="tlsKey" + name="tlsKey" + label="Key" + required + /> + + + { + storeCertInObject("tlsCert", encodedValue); + }} + accept=".cer,.crt,.cert,.pem" + id="tlsCert" + name="tlsCert" + label="Cert" + required + /> + +
Console TLS Certs
+ + { + storeCertInObject("consoleKey", encodedValue); + }} + accept=".key,.pem" + id="consoleKey" + name="consoleKey" + label="Key" + required + /> + + + { + storeCertInObject("consoleCert", encodedValue); + }} + accept=".cer,.crt,.cert,.pem" + id="consoleCert" + name="consoleCert" + label="Cert" + required + /> + +
+ )} +
)} +
+ ), + buttons: [ + cancelButton, + { label: "Back", type: "back", enabled: true }, + { label: "Next", type: "next", enabled: true }, + ], + }, + { + label: "Encryption", + advancedOnly: true, + componentRender: ( + +
+

Encryption

+ How would you like to encrypt the information at rest. +
+ + { + const targetD = e.target; + const checked = targetD.checked; + + setEnableEncryption(checked); + }} + label={"Enable Server Side Encryption"} + disabled={!enableTLS} + /> + + {enableEncryption && ( + + + { + setEncryptionType(e.target.value); + }} + selectorOptions={[ + { label: "Vault", value: "vault" }, + { label: "AWS", value: "aws" }, + { label: "Gemalto", value: "gemalto" }, + ]} + /> + + + {enableTLS && tlsType !== "autocert" && ( + +
Server
+ + { + storeCertInObject("serverKey", encodedValue); + }} + accept=".key,.pem" + id="serverKey" + name="serverKey" + label="Key" + required + /> + + + { + storeCertInObject("serverCert", encodedValue); + }} + accept=".cer,.crt,.cert,.pem" + id="serverCert" + name="serverCert" + label="Cert" + required + /> + +
Client
+ + { + storeCertInObject("clientKey", encodedValue); + }} + accept=".key,.pem" + id="clientKey" + name="clientKey" + label="Key" + required + /> + + + { + storeCertInObject("clientCert", encodedValue); + }} + accept=".cer,.crt,.cert,.pem" + id="clientCert" + name="clientCert" + label="Cert" + required + /> + +
+ )} + + {encryptionType === "vault" && ( + + + ) => { + setVaultEndpoint(e.target.value); + clearValidationError("vault_endpoint"); + }} + label="Endpoint" + value={vaultEndpoint} + error={validationErrors["vault_endpoint"] || ""} + required + /> + + + ) => { + setVaultEngine(e.target.value); + }} + label="Engine" + value={vaultEngine} + required + /> + + + ) => { + setVaultNamespace(e.target.value); + }} + label="Namespace" + value={vaultNamespace} + /> + + + ) => { + setVaultPrefix(e.target.value); + }} + label="Prefix" + value={vaultPrefix} + /> + +
App Role
+ + ) => { + setVaultAppRoleEngine(e.target.value); + }} + label="Engine" + value={vaultAppRoleEngine} + /> + + + ) => { + setVaultId(e.target.value); + clearValidationError("vault_id"); + }} + label="Id" + value={vaultId} + error={validationErrors["vault_id"] || ""} + required + /> + + + ) => { + setVaultSecret(e.target.value); + clearValidationError("vault_secret"); + }} + label="Id" + value={vaultSecret} + error={validationErrors["vault_secret"] || ""} + required + /> + + + ) => { + setVaultRetry(e.target.value); + }} + label="Retry" + value={vaultRetry} + required + /> + +
TLS
+ + { + storeCertInObject("vaultKey", encodedValue); + }} + accept=".key,.pem" + id="vault_key" + name="vault_key" + label="Key" + required + /> + + + { + storeCertInObject("vaultCert", encodedValue); + }} + accept=".cer,.crt,.cert,.pem" + id="vault_cert" + name="vault_cert" + label="Cert" + required + /> + + + { + storeCertInObject("vaultCA", encodedValue); + }} + accept=".cer,.crt,.cert,.pem" + id="vault_ca" + name="vault_ca" + label="CA" + required + /> + +
Status
+ + ) => { + setVaultPing(e.target.value); + }} + label="Ping" + value={vaultPing} + required + /> + +
+ )} + {encryptionType === "aws" && ( + + + ) => { + setAWSEndpoint(e.target.value); + clearValidationError("aws_endpoint"); + }} + label="Endpoint" + value={awsEndpoint} + error={validationErrors["aws_endpoint"] || ""} + required + /> + + + ) => { + setAWSRegion(e.target.value); + clearValidationError("aws_region"); + }} + label="Region" + value={awsRegion} + error={validationErrors["aws_region"] || ""} + required + /> + + + ) => { + setAWSKMSKey(e.target.value); + }} + label="KMS Key" + value={awsKMSKey} + /> + +
Credentials
+ + ) => { + setAWSAccessKey(e.target.value); + clearValidationError("aws_accessKey"); + }} + label="Access Key" + value={awsAccessKey} + error={validationErrors["aws_accessKey"] || ""} + required + /> + + + ) => { + setAWSSecretKey(e.target.value); + clearValidationError("aws_secretKey"); + }} + label="Secret Key" + value={awsSecretKey} + error={validationErrors["aws_secretKey"] || ""} + required + /> + + + ) => { + setAWSToken(e.target.value); + }} + label="Secret Key" + value={awsToken} + /> + +
+ )} + {encryptionType === "gemalto" && ( + + + ) => { + setGemaltoEndpoint(e.target.value); + clearValidationError("gemalto_endpoint"); + }} + label="Endpoint" + value={gemaltoEndpoint} + error={validationErrors["gemalto_endpoint"] || ""} + required + /> + +
Credentials
+ + ) => { + setGemaltoToken(e.target.value); + clearValidationError("gemalto_endpoint"); + }} + label="Token" + value={gemaltoToken} + error={validationErrors["gemalto_endpoint"] || ""} + required + /> + + + ) => { + setGemaltoDomain(e.target.value); + clearValidationError("gemalto_domain"); + }} + label="Domain" + value={gemaltoDomain} + error={validationErrors["gemalto_domain"] || ""} + required + /> + + + ) => { + setGemaltoRetry(e.target.value); + }} + label="Domain" + value={gemaltoRetry} + error={validationErrors["gemalto_retry"] || ""} + /> + +
TLS
+ + { + storeCertInObject("gemaltoCA", encodedValue); + }} + accept=".cer,.crt,.cert,.pem" + id="gemalto_ca" + name="gemalto_ca" + label="CA" + required + /> + +
+ )} +
+ )} +
+ ), + buttons: [ + cancelButton, + { label: "Back", type: "back", enabled: true }, + { label: "Next", type: "next", enabled: true }, + ], + }, + { + label: "Tenant Size", + componentRender: ( + +
+

Tenant Size

+ Please select the desired capacity +
+ {distribution.error} ) => { - setVolumesPerServer(parseInt(e.target.value)); - clearValidationError("volumes_per_server"); + setNodes(e.target.value); + clearValidationError("nodes"); }} - label="Volumes per Server" - value={volumesPerServer.toString(10)} - min="0" + label="Number of Nodes" + value={nodes} + min="4" required - error={validationErrors["volumes_per_server"] || ""} + error={validationErrors["nodes"] || ""} /> @@ -661,11 +1540,11 @@ const AddTenant = ({ id="volume_size" name="volume_size" onChange={(e: React.ChangeEvent) => { - setVolumeConfig("size", e.target.value); + setVolumeSize(e.target.value); clearValidationError("volume_size"); }} label="Size" - value={volumeConfiguration.size.toString(10)} + value={volumeSize} required error={validationErrors["volume_size"] || ""} min="0" @@ -685,6 +1564,69 @@ const AddTenant = ({ + + ) => { + setMemoryNode(e.target.value); + clearValidationError("memory_per_node"); + }} + label="Memory per Node [Gi]" + value={memoryNode} + required + error={validationErrors["memory_per_node"] || ""} + min="2" + /> + + {advancedMode && ( + + ) => { + setECParity(e.target.value as string); + }} + label="Erasure Code Parity" + value={ecParity} + options={ecParityChoices} + /> + + Please select the desired parity. This setting will change the + max usable capacity in the cluster + + + )} +
Resource Allocation
+ + + + + Volumes per Node + + + {distribution ? distribution.disks : "-"} + + + + + Disk Size + + + {distribution ? niceBytes(distribution.pvSize) : "-"} + + + + + Total Number of Volumes + + + {distribution ? distribution.persistentVolumes : "-"} + + + +
), buttons: [ @@ -694,85 +1636,7 @@ const AddTenant = ({ ], }, { - label: "Zones Definition", - advancedOnly: true, - componentRender: ( - -
-

Zones Definition

- Define the size of the tenant by defining the zone size -
- -
- { - setZones(elements); - }} - elements={zones} - /> -
-
- {validationErrors["zones_selector"] || ""} -
-
-
- ), - buttons: [ - cancelButton, - { label: "Back", type: "back", enabled: true }, - { label: "Next", type: "next", enabled: zonesValid }, - ], - }, - { - label: "Extra Configurations", - advancedOnly: true, - componentRender: ( - -
-

Extra Configurations

-
- - { - const targetD = e.target; - const checked = targetD.checked; - - setEnableConsole(checked); - }} - label={"Enable Console"} - /> - - - { - const targetD = e.target; - const checked = targetD.checked; - - setEnableTLS(checked); - }} - label={"Enable TLS"} - /> - -
- ), - buttons: [ - cancelButton, - { label: "Back", type: "back", enabled: true }, - { label: "Next", type: "next", enabled: true }, - ], - }, - { - label: "Review", + label: "Preview Configuration", componentRender: (
@@ -799,22 +1663,6 @@ const AddTenant = ({ {tenantName} - {customACCK && ( - - - - Access Key - - {accessKey} - - - - Secret Key - - {secretKey} - - - )} {customDockerhub && ( @@ -825,15 +1673,6 @@ const AddTenant = ({ )} - {serviceName !== "" && ( - - - Service Name - - {serviceName} - - )} - {namespace !== "" && ( @@ -847,37 +1686,17 @@ const AddTenant = ({ Storage Class - {volumeConfiguration.storage_class} + {selectedStorageClass} - {mountPath !== "" && ( - - - Mount Path - - {mountPath} - - )} - - - Volumes per Server - - {volumesPerServer} - Volume Size - {volumeConfiguration.size} {sizeFactor} + {volumeSize} {sizeFactor} - - - Total Zones - - {zones.length} - {advancedMode && ( @@ -886,14 +1705,6 @@ const AddTenant = ({ {enableTLS ? "Enabled" : "Disabled"} - - - Enable Console - - - {enableConsole ? "Enabled" : "Disabled"} - - )} diff --git a/portal-ui/src/utils/validationFunctions.ts b/portal-ui/src/utils/validationFunctions.ts index d8f140114..0d1ddcaf2 100644 --- a/portal-ui/src/utils/validationFunctions.ts +++ b/portal-ui/src/utils/validationFunctions.ts @@ -19,6 +19,8 @@ export interface IValidation { required: boolean; pattern?: RegExp; customPatternMessage?: string; + customValidation?: boolean; + customValidationMessage?: string; value: string; } @@ -31,12 +33,18 @@ export const commonFormValidation = (fieldsValidate: IValidation[]) => { return; } + if (field.customValidation && field.customValidationMessage) { + returnErrors[field.fieldKey] = field.customValidationMessage; + return; + } + if (field.pattern && field.customPatternMessage) { const rgx = new RegExp(field.pattern, "g"); if (!field.value.match(rgx)) { returnErrors[field.fieldKey] = field.customPatternMessage; } + return; } });