Add Tenant page refactor (#617)

Refactored add tenant page to be working with reducers & settings styles
This commit is contained in:
Alex
2021-03-02 22:18:53 -06:00
committed by GitHub
parent 8958cbec69
commit 6b11d403a6
23 changed files with 5294 additions and 3340 deletions

View File

@@ -32,7 +32,6 @@ var (
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/account"
tenants = "/tenants"
addTenant = "/add-tenant"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
remoteBuckets = "/remote-buckets"
replication = "/replication"
@@ -282,7 +281,6 @@ var endpointRules = map[string]ConfigurationActionSet{
var operatorRules = map[string]ConfigurationActionSet{
tenants: tenantsActionSet,
tenantsDetail: tenantsActionSet,
addTenant: tenantsActionSet,
license: licenseActionSet,
}

View File

@@ -116,7 +116,7 @@ func TestOperatorOnlyEndpoints(t *testing.T) {
"admin:*",
},
},
want: 4,
want: 3,
},
{
name: "Operator Only - all s3 endpoints",
@@ -125,7 +125,7 @@ func TestOperatorOnlyEndpoints(t *testing.T) {
"s3:*",
},
},
want: 4,
want: 3,
},
{
name: "Operator Only - all admin and s3 endpoints",
@@ -135,14 +135,14 @@ func TestOperatorOnlyEndpoints(t *testing.T) {
"s3:*",
},
},
want: 4,
want: 3,
},
{
name: "Operator Only - default endpoints",
args: args{
[]string{},
},
want: 4,
want: 3,
},
}

File diff suppressed because one or more lines are too long

View File

@@ -452,3 +452,55 @@ export const snackBarCommon = {
maxWidth: "calc(100% - 140px)",
},
};
export const wizardCommon = {
multiContainer: {
display: "flex" as const,
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
alignSelf: "flex-start" as const,
},
headerElement: {
position: "sticky" as const,
top: 0,
paddingTop: 5,
marginBottom: 10,
zIndex: 500,
backgroundColor: "#fff",
},
tableTitle: {
fontWeight: 700,
width: "30%",
},
poolError: {
color: "#dc1f2e",
fontSize: "0.75rem",
paddingLeft: 120,
},
error: {
color: "#dc1f2e",
fontSize: "0.75rem",
},
h3Section: {
marginTop: 0,
},
descriptionText: {
fontSize: 13,
color: "#777777",
},
container: {
padding: "77px 0 0 0",
"& h6": {
color: "#777777",
fontSize: 14,
},
"& p": {
"& span:not(*[class*='smallUnit'])": {
fontSize: 16,
},
},
},
};

View File

@@ -18,23 +18,31 @@ import React, { useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { IWizardMain } from "./types";
import WizardPage from "./WizardPage";
import { Grid, Paper } from "@material-ui/core";
import { Grid } from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
wizardMain: {
display: "flex",
width: "100%",
height: "100%",
flexGrow: 1,
},
wizFromContainer: {
marginTop: "32px",
height: "calc(100vh - 365px)",
minHeight: 450,
padding: "0 30px",
},
wizardSteps: {
minWidth: 180,
marginRight: 10,
borderRight: "#eaeaea 1px solid",
display: "flex",
flexGrow: 1,
flexDirection: "column",
height: "100%",
"& ul": {
padding: "0px 15px 0 30px",
padding: "0 15px 0 40px",
marginTop: "0px",
"& li": {
@@ -56,15 +64,14 @@ const styles = (theme: Theme) =>
boxShadow: "none",
},
},
paddedGridItem: {
padding: "0px 10px 0px 10px",
paddedContentGrid: {
padding: "0 10px",
},
menuPaper: {
padding: "20px",
},
paperContainer: {
padding: "10px",
maxWidth: "900px",
stepsLabel: {
fontSize: 20,
color: "#393939",
fontWeight: 600,
margin: "15px 12px",
},
});
@@ -114,34 +121,25 @@ const GenericWizard = ({ classes, wizardSteps }: IWizardMain) => {
return (
<Grid container className={classes.wizFromContainer}>
<Grid
item
xs={12}
sm={3}
md={3}
lg={3}
xl={2}
className={classes.paddedGridItem}
>
<Paper className={classes.menuPaper}>
<div className={classes.wizardSteps}>
<ul>
{wizardSteps.map((step, index) => {
return (
<li key={`wizard-${index.toString()}`}>
<button
onClick={() => pageChange(index)}
disabled={index > currentStep}
className={classes.buttonList}
>
{step.label}
</button>
</li>
);
})}
</ul>
</div>
</Paper>
<Grid item xs={12} sm={3} md={3} lg={3} xl={2}>
<div className={classes.wizardSteps}>
<span className={classes.stepsLabel}>Steps</span>
<ul>
{wizardSteps.map((step, index) => {
return (
<li key={`wizard-${index.toString()}`}>
<button
onClick={() => pageChange(index)}
disabled={index > currentStep}
className={classes.buttonList}
>
{step.label}
</button>
</li>
);
})}
</ul>
</div>
</Grid>
<Grid
item
@@ -150,11 +148,9 @@ const GenericWizard = ({ classes, wizardSteps }: IWizardMain) => {
md={9}
lg={9}
xl={10}
className={classes.paddedGridItem}
className={classes.paddedContentGrid}
>
<Paper className={classes.paperContainer}>
<WizardPage page={wizardSteps[currentStep]} pageChange={pageChange} />
</Paper>
<WizardPage page={wizardSteps[currentStep]} pageChange={pageChange} />
</Grid>
</Grid>
);

View File

@@ -28,11 +28,14 @@ const styles = (theme: Theme) =>
wizardComponent: {
overflowY: "auto",
marginBottom: 10,
height: "calc(100vh - 435px)",
},
buttonsContainer: {
display: "flex",
flexDirection: "row",
justifyContent: "flex-end" as const,
padding: "10px 0",
borderTop: "#EAEAEA 1px solid",
"& button": {
marginLeft: 10,
},

View File

@@ -45,7 +45,7 @@ import Users from "./Users/Users";
import Groups from "./Groups/Groups";
import ConfigurationMain from "./Configurations/ConfigurationMain";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import ListTenants from "./Tenants/ListTenants/ListTenants";
import TenantsMain from "./Tenants/TenantsMain";
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
import ObjectBrowser from "./ObjectBrowser/ObjectBrowser";
import ObjectRouting from "./Buckets/ListBuckets/Objects/ListObjects/ObjectRouting";
@@ -55,7 +55,6 @@ import LogsMain from "./Logs/LogsMain";
import Heal from "./Heal/Heal";
import Watch from "./Watch/Watch";
import HealthInfo from "./HealthInfo/HealthInfo";
import AddTenant from "./Tenants/ListTenants/AddTenant";
const drawerWidth = 245;
@@ -291,13 +290,9 @@ const Console = ({
path: "/webhook/audit",
},
{
component: ListTenants,
component: TenantsMain,
path: "/tenants",
},
{
component: AddTenant,
path: "/add-tenant",
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName",

View File

@@ -0,0 +1,621 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, Fragment } from "react";
import { connect } from "react-redux";
import Grid from "@material-ui/core/Grid";
import { LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
modalBasic,
settingsCommon,
wizardCommon,
} from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
import { generatePoolName } from "../../../../common/utils";
import GenericWizard from "../../Common/GenericWizard/GenericWizard";
import { IWizardElement } from "../../Common/GenericWizard/types";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import { IAffinityModel, ITenantCreator } from "../../../../common/types";
import { KeyPair } from "../ListTenants/utils";
import { setModalErrorSnackMessage } from "../../../../actions";
import { getHardcodedAffinity } from "../TenantDetails/utils";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
import NameTenant from "./Steps/NameTenant";
import { AppState } from "../../../../store";
import { ICertificatesItems, IFieldStore } from "../types";
import { updateAddField } from "../actions";
import Configure from "./Steps/Configure";
import IdentityProvider from "./Steps/IdentityProvider";
import Security from "./Steps/Security";
import Encryption from "./Steps/Encryption";
import TenantSize from "./Steps/TenantSize";
import Preview from "./Steps/Preview";
interface IAddTenantProps {
closeAndRefresh: (reloadData: boolean) => any;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
updateAddField: typeof updateAddField;
fields: IFieldStore;
certificates: ICertificatesItems;
namespace: string;
validPages: string[];
advancedMode: boolean;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...wizardCommon,
...settingsCommon,
});
const AddTenant = ({
classes,
advancedMode,
fields,
certificates,
namespace,
validPages,
setModalErrorSnackMessage,
closeAndRefresh,
}: IAddTenantProps) => {
// Modals
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
const [
createdAccount,
setCreatedAccount,
] = useState<NewServiceAccount | null>(null);
// Fields
const [addSending, setAddSending] = useState<boolean>(false);
/* Send Information to backend */
useEffect(() => {
const tenantName = fields.nameTenant.tenantName;
const selectedStorageClass = fields.nameTenant.selectedStorageClass;
const imageName = fields.configure.imageName;
const consoleImage = fields.configure.consoleImage;
const customDockerhub = fields.configure.customDockerhub;
const imageRegistry = fields.configure.imageRegistry;
const imageRegistryUsername = fields.configure.imageRegistryUsername;
const imageRegistryPassword = fields.configure.imageRegistryPassword;
const exposeMinIO = fields.configure.exposeMinIO;
const exposeConsole = fields.configure.exposeConsole;
const idpSelection = fields.identityProvider.idpSelection;
const openIDURL = fields.identityProvider.openIDURL;
const openIDClientID = fields.identityProvider.openIDClientID;
const openIDSecretID = fields.identityProvider.openIDSecretID;
const ADURL = fields.identityProvider.ADURL;
const ADSkipTLS = fields.identityProvider.ADSkipTLS;
const ADServerInsecure = fields.identityProvider.ADServerInsecure;
const ADUserNameFilter = fields.identityProvider.ADUserNameFilter;
const ADGroupBaseDN = fields.identityProvider.ADGroupBaseDN;
const ADGroupSearchFilter = fields.identityProvider.ADGroupSearchFilter;
const ADNameAttribute = fields.identityProvider.ADNameAttribute;
const minioCertificates = certificates.minioCertificates;
const caCertificates = certificates.caCertificates;
const consoleCertificate = certificates.consoleCertificate;
const serverCertificate = certificates.serverCertificate;
const clientCertificate = certificates.clientCertificate;
const vaultCertificate = certificates.vaultCertificate;
const vaultCA = certificates.vaultCA;
const gemaltoCA = certificates.gemaltoCA;
const enableEncryption = fields.encryption.enableEncryption;
const encryptionType = fields.encryption.encryptionType;
const gemaltoEndpoint = fields.encryption.gemaltoEndpoint;
const gemaltoToken = fields.encryption.gemaltoToken;
const gemaltoDomain = fields.encryption.gemaltoDomain;
const gemaltoRetry = fields.encryption.gemaltoRetry;
const awsEndpoint = fields.encryption.awsEndpoint;
const awsRegion = fields.encryption.awsRegion;
const awsKMSKey = fields.encryption.awsKMSKey;
const awsAccessKey = fields.encryption.awsAccessKey;
const awsSecretKey = fields.encryption.awsSecretKey;
const awsToken = fields.encryption.awsToken;
const vaultEndpoint = fields.encryption.vaultEndpoint;
const vaultEngine = fields.encryption.vaultEngine;
const vaultNamespace = fields.encryption.vaultNamespace;
const vaultPrefix = fields.encryption.vaultPrefix;
const vaultAppRoleEngine = fields.encryption.vaultAppRoleEngine;
const vaultId = fields.encryption.vaultId;
const vaultSecret = fields.encryption.vaultSecret;
const vaultRetry = fields.encryption.vaultRetry;
const vaultPing = fields.encryption.vaultPing;
const gcpProjectID = fields.encryption.gcpProjectID;
const gcpEndpoint = fields.encryption.gcpEndpoint;
const gcpClientEmail = fields.encryption.gcpClientEmail;
const gcpClientID = fields.encryption.gcpClientID;
const gcpPrivateKeyID = fields.encryption.gcpPrivateKeyID;
const gcpPrivateKey = fields.encryption.gcpPrivateKey;
const enableAutoCert = fields.security.enableAutoCert;
const ecParity = fields.tenantSize.ecParity;
const distribution = fields.tenantSize.distribution;
const memorySize = fields.tenantSize.memorySize;
if (addSending) {
const poolName = generatePoolName([]);
const hardCodedAffinity: IAffinityModel = getHardcodedAffinity(
tenantName,
poolName
);
const erasureCode = ecParity.split(":")[1];
let dataSend: ITenantCreator = {
name: tenantName,
namespace: namespace,
access_key: "",
secret_key: "",
enable_tls: enableAutoCert,
enable_console: true,
enable_prometheus: true,
service_name: "",
image: imageName,
console_image: consoleImage,
expose_minio: exposeMinIO,
expose_console: exposeConsole,
pools: [
{
name: poolName,
servers: distribution.nodes,
volumes_per_server: distribution.disks,
volume_configuration: {
size: distribution.pvSize,
storage_class_name: selectedStorageClass,
},
resources: {
requests: {
memory: memorySize.request,
},
limits: {
memory: memorySize.limit,
},
},
affinity: hardCodedAffinity,
},
],
erasureCodingParity: parseInt(erasureCode, 10),
};
if (customDockerhub) {
dataSend = {
...dataSend,
image_registry: {
registry: imageRegistry,
username: imageRegistryUsername,
password: imageRegistryPassword,
},
};
}
let tenantCerts: any = null;
let consoleCerts: any = null;
let caCerts: any = null;
if (caCertificates.length > 0) {
caCerts = {
ca_certificates: caCertificates
.map((keyPair: KeyPair) => keyPair.encoded_cert)
.filter((keyPair) => keyPair),
};
}
if (minioCertificates.length > 0) {
tenantCerts = {
minio: minioCertificates
.map((keyPair: KeyPair) => ({
crt: keyPair.encoded_cert,
key: keyPair.encoded_key,
}))
.filter((keyPair) => keyPair.crt && keyPair.key),
};
}
if (
consoleCertificate.encoded_cert !== "" &&
consoleCertificate.encoded_key !== ""
) {
consoleCerts = {
console: {
crt: consoleCertificate.encoded_cert,
key: consoleCertificate.encoded_key,
},
};
}
if (tenantCerts || consoleCerts || caCerts) {
dataSend = {
...dataSend,
tls: {
...tenantCerts,
...consoleCerts,
...caCerts,
},
};
}
if (enableEncryption) {
let insertEncrypt = {};
switch (encryptionType) {
case "gemalto":
let gemaltoCAIntroduce = {};
if (gemaltoCA.encoded_cert !== "") {
gemaltoCAIntroduce = {
ca: gemaltoCA.encoded_cert,
};
}
insertEncrypt = {
gemalto: {
keysecure: {
endpoint: gemaltoEndpoint,
credentials: {
token: gemaltoToken,
domain: gemaltoDomain,
retry: parseInt(gemaltoRetry),
},
tls: {
...gemaltoCAIntroduce,
},
},
},
};
break;
case "aws":
insertEncrypt = {
aws: {
secretsmanager: {
endpoint: awsEndpoint,
region: awsRegion,
kmskey: awsKMSKey,
credentials: {
accesskey: awsAccessKey,
secretkey: awsSecretKey,
token: awsToken,
},
},
},
};
break;
case "gcp":
insertEncrypt = {
gcp: {
secretmanager: {
project_id: gcpProjectID,
endpoint: gcpEndpoint,
credentials: {
client_email: gcpClientEmail,
client_id: gcpClientID,
private_key_id: gcpPrivateKeyID,
private_key: gcpPrivateKey,
},
},
},
};
break;
case "vault":
let vaultKeyPair = null;
let vaultCAInsert = null;
if (
vaultCertificate.encoded_key !== "" &&
vaultCertificate.encoded_cert !== ""
) {
vaultKeyPair = {
key: vaultCertificate.encoded_key,
crt: vaultCertificate.encoded_cert,
};
}
if (vaultCA.encoded_cert !== "") {
vaultCAInsert = {
ca: vaultCA.encoded_cert,
};
}
let vaultTLS = null;
if (vaultKeyPair || vaultCA) {
vaultTLS = {
tls: {
...vaultKeyPair,
...vaultCAInsert,
},
};
}
insertEncrypt = {
vault: {
endpoint: vaultEndpoint,
engine: vaultEngine,
namespace: vaultNamespace,
prefix: vaultPrefix,
approle: {
engine: vaultAppRoleEngine,
id: vaultId,
secret: vaultSecret,
retry: parseInt(vaultRetry),
},
...vaultTLS,
status: {
ping: parseInt(vaultPing),
},
},
};
break;
}
let encryptionServerKeyPair: any = {};
let encryptionClientKeyPair: any = {};
if (
clientCertificate.encoded_key !== "" &&
clientCertificate.encoded_cert !== ""
) {
encryptionClientKeyPair = {
client: {
key: clientCertificate.encoded_key,
crt: clientCertificate.encoded_cert,
},
};
}
if (
serverCertificate.encoded_key !== "" &&
serverCertificate.encoded_cert !== ""
) {
encryptionServerKeyPair = {
server: {
key: serverCertificate.encoded_key,
crt: serverCertificate.encoded_cert,
},
};
}
dataSend = {
...dataSend,
encryption: {
...encryptionClientKeyPair,
...encryptionServerKeyPair,
...insertEncrypt,
},
};
}
if (idpSelection !== "Built-in") {
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 = {
console: {
accessKey: res.console.access_key,
secretKey: res.console.secret_key,
},
};
setAddSending(false);
setShowNewCredentials(true);
setCreatedAccount(newSrvAcc);
})
.catch((err) => {
setAddSending(false);
setModalErrorSnackMessage(err);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [addSending]);
const cancelButton = {
label: "Cancel",
type: "other",
enabled: true,
action: () => {
closeAndRefresh(false);
},
};
const wizardSteps: IWizardElement[] = [
{
label: "Name Tenant",
componentRender: <NameTenant />,
buttons: [
cancelButton,
{
label: "Next",
type: "next",
enabled: validPages.includes("nameTenant"),
},
],
},
{
label: "Configure",
advancedOnly: true,
componentRender: <Configure />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("configure"),
},
],
},
{
label: "Identity Provider",
advancedOnly: true,
componentRender: <IdentityProvider />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("identityProvider"),
},
],
},
{
label: "Security",
advancedOnly: true,
componentRender: <Security />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("security"),
},
],
},
{
label: "Encryption",
advancedOnly: true,
componentRender: <Encryption />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("encryption"),
},
],
},
{
label: "Tenant Size",
componentRender: <TenantSize />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("tenantSize"),
},
],
},
{
label: "Preview Configuration",
componentRender: <Preview />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Create",
type: "submit",
enabled: !addSending,
action: () => {
setAddSending(true);
},
},
],
},
];
let filteredWizardSteps = wizardSteps;
if (!advancedMode) {
filteredWizardSteps = wizardSteps.filter((step) => !step.advancedOnly);
}
const closeCredentialsModal = () => {
closeAndRefresh(true);
};
return (
<Fragment>
<Grid item xs={12} className={classes.customTitle}>
Create New Tenant
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={createdAccount}
open={showNewCredentials}
closeModal={() => {
closeCredentialsModal();
}}
entity="Tenant"
/>
)}
<Grid container>
<Grid item xs={12}>
<GenericWizard wizardSteps={filteredWizardSteps} />
</Grid>
</Grid>
</Fragment>
);
};
const mapState = (state: AppState) => ({
advancedMode: state.tenants.createTenant.advancedModeOn,
namespace: state.tenants.createTenant.fields.nameTenant.namespace,
validPages: state.tenants.createTenant.validPages,
fields: state.tenants.createTenant.fields,
certificates: state.tenants.createTenant.certificates,
});
const connector = connect(mapState, {
setModalErrorSnackMessage,
updateAddField,
});
export default withStyles(styles)(connector(AddTenant));

View File

@@ -0,0 +1,328 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, useCallback, Fragment } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid } from "@material-ui/core";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { updateAddField, isPageValid } 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";
interface IConfigureProps {
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
classes: any;
customImage: boolean;
imageName: string;
consoleImage: string;
customDockerhub: boolean;
imageRegistry: string;
imageRegistryUsername: string;
imageRegistryPassword: string;
exposeMinIO: boolean;
exposeConsole: boolean;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...wizardCommon,
});
const Configure = ({
classes,
customImage,
imageName,
consoleImage,
customDockerhub,
imageRegistry,
imageRegistryUsername,
imageRegistryPassword,
exposeMinIO,
exposeConsole,
updateAddField,
isPageValid,
}: IConfigureProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
// Common
const updateField = useCallback(
(field: string, value: any) => {
updateAddField("configure", field, value);
},
[updateAddField]
);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [];
if (customImage) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "image",
required: true,
value: imageName,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage: "Format must be of form: 'minio/minio:VERSION'",
},
{
fieldKey: "consoleImage",
required: true,
value: consoleImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'minio/console:VERSION'",
},
];
if (customDockerhub) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "registry",
required: true,
value: imageRegistry,
},
{
fieldKey: "registryUsername",
required: true,
value: imageRegistryUsername,
},
{
fieldKey: "registryPassword",
required: true,
value: imageRegistryPassword,
},
];
}
}
const commonVal = commonFormValidation(customAccountValidation);
isPageValid("configure", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [
customImage,
imageName,
consoleImage,
customDockerhub,
imageRegistry,
imageRegistryUsername,
imageRegistryPassword,
isPageValid,
]);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
return (
<Fragment>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Configure</h3>
<span className={classes.descriptionText}>
Basic configurations for tenant management
</span>
</div>
<Grid item xs={12}>
<FormSwitchWrapper
value="custom_image"
id="custom_image"
name="custom_image"
checked={customImage}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("customImage", checked);
}}
label={"Use custom image"}
/>
</Grid>
{customImage && (
<Fragment>
Please enter the MinIO image from dockerhub to use
<Grid item xs={12}>
<InputBoxWrapper
id="image"
name="image"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageName", e.target.value);
cleanValidation("image");
}}
label="MinIO's Image"
value={imageName}
error={validationErrors["image"] || ""}
placeholder="E.g. minio/minio:RELEASE.2020-05-08T02-40-49Z"
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="consoleImage"
name="consoleImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("consoleImage", e.target.value);
cleanValidation("consoleImage");
}}
label="Console's Image"
value={consoleImage}
error={validationErrors["consoleImage"] || ""}
placeholder="E.g. minio/console:v0.3.13"
required
/>
</Grid>
</Fragment>
)}
{customImage && (
<Fragment>
<Grid item xs={12}>
<FormSwitchWrapper
value="custom_docker_hub"
id="custom_docker_hub"
name="custom_docker_hub"
checked={customDockerhub}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("customDockerhub", checked);
}}
label={"Set/Update Image Registry"}
/>
</Grid>
</Fragment>
)}
{customDockerhub && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="registry"
name="registry"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageRegistry", e.target.value);
}}
label="Endpoint"
value={imageRegistry}
error={validationErrors["registry"] || ""}
placeholder="E.g. https://index.docker.io/v1/"
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="registryUsername"
name="registryUsername"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageRegistryUsername", e.target.value);
}}
label="Username"
value={imageRegistryUsername}
error={validationErrors["registryUsername"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="registryPassword"
name="registryPassword"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageRegistryPassword", e.target.value);
}}
label="Password"
value={imageRegistryPassword}
error={validationErrors["registryPassword"] || ""}
required
/>
</Grid>
</Fragment>
)}
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Expose Services</h3>
<span className={classes.descriptionText}>
Whether the tenant's services should request an external IP.
</span>
</div>
<Grid item xs={12}>
<FormSwitchWrapper
value="expose_minio"
id="expose_minio"
name="expose_minio"
checked={exposeMinIO}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("exposeMinIO", checked);
}}
label={"Expose MiniO Service"}
/>
</Grid>
<Grid item xs={12}>
<FormSwitchWrapper
value="expose_console"
id="expose_console"
name="expose_console"
checked={exposeConsole}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("exposeConsole", checked);
}}
label={"Expose Console Service"}
/>
</Grid>
</Fragment>
);
};
const mapState = (state: AppState) => ({
customImage: state.tenants.createTenant.fields.configure.customImage,
imageName: state.tenants.createTenant.fields.configure.imageName,
consoleImage: state.tenants.createTenant.fields.configure.consoleImage,
customDockerhub: state.tenants.createTenant.fields.configure.customDockerhub,
imageRegistry: state.tenants.createTenant.fields.configure.imageRegistry,
imageRegistryUsername:
state.tenants.createTenant.fields.configure.imageRegistryUsername,
imageRegistryPassword:
state.tenants.createTenant.fields.configure.imageRegistryPassword,
exposeMinIO: state.tenants.createTenant.fields.configure.exposeMinIO,
exposeConsole: state.tenants.createTenant.fields.configure.exposeConsole,
});
const connector = connect(mapState, {
updateAddField,
isPageValid,
});
export default withStyles(styles)(connector(Configure));

View File

@@ -0,0 +1,923 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, useState, useEffect, useCallback } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Typography } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import {
updateAddField,
isPageValid,
addFileServerCert,
addFileClientCert,
addFileVaultCert,
addFileVaultCa,
addFileGemaltoCa,
} from "../../actions";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { AppState } from "../../../../../store";
import { clearValidationError } from "../../utils";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import FileSelector from "../../../Common/FormComponents/FileSelector/FileSelector";
import RadioGroupSelector from "../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import { KeyPair } from "../../ListTenants/utils";
interface IEncryptionProps {
classes: any;
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
addFileServerCert: typeof addFileServerCert;
addFileClientCert: typeof addFileClientCert;
addFileVaultCert: typeof addFileVaultCert;
addFileVaultCa: typeof addFileVaultCa;
addFileGemaltoCa: typeof addFileGemaltoCa;
enableEncryption: boolean;
encryptionType: string;
gemaltoEndpoint: string;
gemaltoToken: string;
gemaltoDomain: string;
gemaltoRetry: string;
awsEndpoint: string;
awsRegion: string;
awsKMSKey: string;
awsAccessKey: string;
awsSecretKey: string;
awsToken: string;
vaultEndpoint: string;
vaultEngine: string;
vaultNamespace: string;
vaultPrefix: string;
vaultAppRoleEngine: string;
vaultId: string;
vaultSecret: string;
vaultRetry: string;
vaultPing: string;
gcpProjectID: string;
gcpEndpoint: string;
gcpClientEmail: string;
gcpClientID: string;
gcpPrivateKeyID: string;
gcpPrivateKey: string;
enableCustomCertsForKES: boolean;
enableAutoCert: boolean;
enableTLS: boolean;
enableCustomCerts: boolean;
minioCertificates: KeyPair[];
serverCertificate: KeyPair;
clientCertificate: KeyPair;
vaultCertificate: KeyPair;
vaultCA: KeyPair;
gemaltoCA: KeyPair;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...wizardCommon,
});
const Encryption = ({
classes,
updateAddField,
isPageValid,
addFileServerCert,
addFileClientCert,
addFileVaultCert,
addFileVaultCa,
addFileGemaltoCa,
enableEncryption,
enableCustomCerts,
encryptionType,
gemaltoEndpoint,
gemaltoToken,
gemaltoDomain,
gemaltoRetry,
awsEndpoint,
awsRegion,
awsKMSKey,
awsAccessKey,
awsSecretKey,
awsToken,
vaultEndpoint,
vaultEngine,
vaultNamespace,
vaultPrefix,
vaultAppRoleEngine,
vaultId,
vaultSecret,
vaultRetry,
vaultPing,
gcpProjectID,
gcpEndpoint,
gcpClientEmail,
gcpClientID,
gcpPrivateKeyID,
gcpPrivateKey,
enableCustomCertsForKES,
enableAutoCert,
enableTLS,
minioCertificates,
serverCertificate,
clientCertificate,
vaultCertificate,
vaultCA,
gemaltoCA,
}: IEncryptionProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
let encryptionAvailable = false;
if (
enableTLS &&
(enableAutoCert ||
(minioCertificates &&
minioCertificates.filter(
(item) => item.encoded_key && item.encoded_cert
).length > 0))
) {
encryptionAvailable = true;
}
// Common
const updateField = useCallback(
(field: string, value: any) => {
updateAddField("encryption", field, value);
},
[updateAddField]
);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
// Validation
useEffect(() => {
let encryptionValidation: IValidation[] = [];
if (enableEncryption) {
if (enableCustomCerts) {
encryptionValidation = [
...encryptionValidation,
{
fieldKey: "serverKey",
required: !enableAutoCert,
value: serverCertificate.encoded_key,
},
{
fieldKey: "serverCert",
required: !enableAutoCert,
value: serverCertificate.encoded_cert,
},
{
fieldKey: "clientKey",
required: !enableAutoCert,
value: clientCertificate.encoded_key,
},
{
fieldKey: "clientCert",
required: !enableAutoCert,
value: clientCertificate.encoded_cert,
},
];
}
if (encryptionType === "vault") {
encryptionValidation = [
...encryptionValidation,
{
fieldKey: "vault_endpoint",
required: true,
value: vaultEndpoint,
},
{
fieldKey: "vault_id",
required: true,
value: vaultId,
},
{
fieldKey: "vault_secret",
required: true,
value: vaultSecret,
},
{
fieldKey: "vault_ping",
required: false,
value: vaultPing,
customValidation: parseInt(vaultPing) < 0,
customValidationMessage: "Value needs to be 0 or greater",
},
{
fieldKey: "vault_retry",
required: false,
value: vaultRetry,
customValidation: parseInt(vaultRetry) < 0,
customValidationMessage: "Value needs to be 0 or greater",
},
];
}
if (encryptionType === "aws") {
encryptionValidation = [
...encryptionValidation,
{
fieldKey: "aws_endpoint",
required: true,
value: awsEndpoint,
},
{
fieldKey: "aws_region",
required: true,
value: awsRegion,
},
{
fieldKey: "aws_accessKey",
required: true,
value: awsAccessKey,
},
{
fieldKey: "aws_secretKey",
required: true,
value: awsSecretKey,
},
];
}
if (encryptionType === "gemalto") {
encryptionValidation = [
...encryptionValidation,
{
fieldKey: "gemalto_endpoint",
required: true,
value: gemaltoEndpoint,
},
{
fieldKey: "gemalto_token",
required: true,
value: gemaltoToken,
},
{
fieldKey: "gemalto_domain",
required: true,
value: gemaltoDomain,
},
{
fieldKey: "gemalto_retry",
required: false,
value: gemaltoRetry,
customValidation: parseInt(gemaltoRetry) < 0,
customValidationMessage: "Value needs to be 0 or greater",
},
];
}
}
const commonVal = commonFormValidation(encryptionValidation);
isPageValid("encryption", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [
enableEncryption,
encryptionType,
vaultEndpoint,
vaultEngine,
vaultId,
vaultSecret,
vaultPing,
vaultRetry,
awsEndpoint,
awsRegion,
awsSecretKey,
awsAccessKey,
gemaltoEndpoint,
gemaltoToken,
gemaltoDomain,
gemaltoRetry,
gcpProjectID,
isPageValid,
enableAutoCert,
enableCustomCerts,
serverCertificate.encoded_key,
serverCertificate.encoded_cert,
clientCertificate.encoded_key,
clientCertificate.encoded_cert,
]);
return (
<Fragment>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Encryption</h3>
<span className={classes.descriptionText}>
How would you like to encrypt the information at rest.
</span>
</div>
<Grid item xs={12}>
<FormSwitchWrapper
value="enableEncryption"
id="enableEncryption"
name="enableEncryption"
checked={enableEncryption}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("enableEncryption", checked);
}}
label={"Enable Server Side Encryption"}
disabled={!encryptionAvailable}
/>
</Grid>
{enableEncryption && (
<Fragment>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={encryptionType}
id="encryptionType"
name="encryptionType"
label="Encryption Options"
onChange={(e) => {
updateField("encryptionType", e.target.value);
}}
selectorOptions={[
{ label: "Vault", value: "vault" },
{ label: "AWS", value: "aws" },
{ label: "Gemalto", value: "gemalto" },
{ label: "GCP", value: "gcp" },
]}
/>
</Grid>
{encryptionType === "vault" && (
<Fragment>
<Grid item xs={12}>
<FormSwitchWrapper
value="enableCustomCertsForKES"
id="enableCustomCertsForKES"
name="enableCustomCertsForKES"
checked={enableCustomCertsForKES || !enableAutoCert}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("enableCustomCertsForKES", checked);
}}
label={"Custom Certificates"}
disabled={!enableAutoCert}
/>
</Grid>
{(enableCustomCertsForKES || !enableAutoCert) && (
<Fragment>
<Grid item xs={12}>
<Typography variant="overline" display="block" gutterBottom>
Encryption Service Certificates
</Typography>
</Grid>
<Grid container>
<Grid item xs={6}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileServerCert("key", fileName, encodedValue);
cleanValidation("serverKey");
}}
accept=".key,.pem"
id="serverKey"
name="serverKey"
label="Key"
error={validationErrors["serverKey"] || ""}
value={serverCertificate.key}
required={!enableAutoCert}
/>
</Grid>
<Grid item xs={6}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileServerCert("cert", fileName, encodedValue);
cleanValidation("serverCert");
}}
accept=".cer,.crt,.cert,.pem"
id="serverCert"
name="serverCert"
label="Cert"
error={validationErrors["serverCert"] || ""}
value={serverCertificate.cert}
required={!enableAutoCert}
/>
</Grid>
</Grid>
<Grid item xs={12}>
<Typography variant="overline" display="block" gutterBottom>
Mutual TLS authentication
</Typography>
</Grid>
<Grid container>
<Grid item xs={6}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileClientCert("key", fileName, encodedValue);
cleanValidation("clientKey");
}}
accept=".key,.pem"
id="clientKey"
name="clientKey"
label="Key"
error={validationErrors["clientKey"] || ""}
value={clientCertificate.key}
required={!enableAutoCert}
/>
</Grid>
<Grid item xs={6}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileClientCert("cert", fileName, encodedValue);
cleanValidation("clientCert");
}}
accept=".cer,.crt,.cert,.pem"
id="clientCert"
name="clientCert"
label="Cert"
error={validationErrors["clientCert"] || ""}
value={clientCertificate.cert}
required={!enableAutoCert}
/>
</Grid>
</Grid>
</Fragment>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="vault_endpoint"
name="vault_endpoint"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("vaultEndpoint", e.target.value);
cleanValidation("vault_endpoint");
}}
label="Endpoint"
value={vaultEndpoint}
error={validationErrors["vault_endpoint"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="vault_engine"
name="vault_engine"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("vaultEngine", e.target.value);
cleanValidation("vault_engine");
}}
label="Engine"
value={vaultEngine}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="vault_namespace"
name="vault_namespace"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("vaultNamespace", e.target.value);
}}
label="Namespace"
value={vaultNamespace}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="vault_prefix"
name="vault_prefix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("vaultPrefix", e.target.value);
}}
label="Prefix"
value={vaultPrefix}
/>
</Grid>
<h5>App Role</h5>
<Grid item xs={12}>
<InputBoxWrapper
id="vault_approle_engine"
name="vault_approle_engine"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("vaultAppRoleEngine", e.target.value);
}}
label="Engine"
value={vaultAppRoleEngine}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="vault_id"
name="vault_id"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("vaultId", e.target.value);
cleanValidation("vault_id");
}}
label="AppRole ID"
value={vaultId}
error={validationErrors["vault_id"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="vault_secret"
name="vault_secret"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("vaultSecret", e.target.value);
cleanValidation("vault_secret");
}}
label="AppRole Secret"
value={vaultSecret}
error={validationErrors["vault_secret"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
type="number"
min="0"
id="vault_retry"
name="vault_retry"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("vaultRetry", e.target.value);
cleanValidation("vault_retry");
}}
label="Retry (Seconds)"
value={vaultRetry}
error={validationErrors["vault_retry"] || ""}
/>
</Grid>
<h5>Mutual TLS authentication (optional)</h5>
<Grid container>
<Grid item xs={6}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileVaultCert("key", fileName, encodedValue);
cleanValidation("vault_key");
}}
accept=".key,.pem"
id="vault_key"
name="vault_key"
label="Key"
value={vaultCertificate.key}
/>
</Grid>
<Grid item xs={6}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileVaultCert("cert", fileName, encodedValue);
cleanValidation("vault_cert");
}}
accept=".cer,.crt,.cert,.pem"
id="vault_cert"
name="vault_cert"
label="Cert"
value={vaultCertificate.cert}
/>
</Grid>
</Grid>
<Grid item xs={12}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileVaultCa(fileName, encodedValue);
cleanValidation("vault_ca");
}}
accept=".cer,.crt,.cert,.pem"
id="vault_ca"
name="vault_ca"
label="CA"
value={vaultCA.cert}
/>
</Grid>
<h5>Status</h5>
<Grid item xs={12}>
<InputBoxWrapper
type="number"
min="0"
id="vault_ping"
name="vault_ping"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("vaultPing", e.target.value);
cleanValidation("vault_ping");
}}
label="Ping (Seconds)"
value={vaultPing}
error={validationErrors["vault_ping"] || ""}
/>
</Grid>
</Fragment>
)}
{encryptionType === "gcp" && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="gcp_project_id"
name="gcp_project_id"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gcpProjectID", e.target.value);
}}
label="Project ID"
value={gcpProjectID}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="gcp_endpoint"
name="gcp_endpoint"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gcpEndpoint", e.target.value);
}}
label="Endpoint"
value={gcpEndpoint}
/>
</Grid>
<h5>Credentials</h5>
<Grid item xs={12}>
<InputBoxWrapper
id="gcp_client_email"
name="gcp_client_email"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gcpClientEmail", e.target.value);
}}
label="Client Email"
value={gcpClientEmail}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="gcp_client_id"
name="gcp_client_id"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gcpClientID", e.target.value);
}}
label="Client ID"
value={gcpClientID}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="gcp_private_key_id"
name="gcp_private_key_id"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gcpPrivateKeyID", e.target.value);
}}
label="Private Key ID"
value={gcpPrivateKeyID}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="gcp_private_key"
name="gcp_private_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gcpPrivateKey", e.target.value);
}}
label="Private Key"
value={gcpPrivateKey}
/>
</Grid>
</Fragment>
)}
{encryptionType === "aws" && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="aws_endpoint"
name="aws_endpoint"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("awsEndpoint", e.target.value);
cleanValidation("aws_endpoint");
}}
label="Endpoint"
value={awsEndpoint}
error={validationErrors["aws_endpoint"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="aws_region"
name="aws_region"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("awsRegion", e.target.value);
cleanValidation("aws_region");
}}
label="Region"
value={awsRegion}
error={validationErrors["aws_region"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="aws_kmsKey"
name="aws_kmsKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("awsKMSKey", e.target.value);
}}
label="KMS Key"
value={awsKMSKey}
/>
</Grid>
<h5>Credentials</h5>
<Grid item xs={12}>
<InputBoxWrapper
id="aws_accessKey"
name="aws_accessKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("awsAccessKey", e.target.value);
cleanValidation("aws_accessKey");
}}
label="Access Key"
value={awsAccessKey}
error={validationErrors["aws_accessKey"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="aws_secretKey"
name="aws_secretKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("awsSecretKey", e.target.value);
cleanValidation("aws_secretKey");
}}
label="Secret Key"
value={awsSecretKey}
error={validationErrors["aws_secretKey"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="aws_token"
name="aws_token"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("awsToken", e.target.value);
}}
label="Token"
value={awsToken}
/>
</Grid>
</Fragment>
)}
{encryptionType === "gemalto" && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="gemalto_endpoint"
name="gemalto_endpoint"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gemaltoEndpoint", e.target.value);
cleanValidation("gemalto_endpoint");
}}
label="Endpoint"
value={gemaltoEndpoint}
error={validationErrors["gemalto_endpoint"] || ""}
required
/>
</Grid>
<h5>Credentials</h5>
<Grid item xs={12}>
<InputBoxWrapper
id="gemalto_token"
name="gemalto_token"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gemaltoToken", e.target.value);
cleanValidation("gemalto_token");
}}
label="Token"
value={gemaltoToken}
error={validationErrors["gemalto_token"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="gemalto_domain"
name="gemalto_domain"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gemaltoDomain", e.target.value);
cleanValidation("gemalto_domain");
}}
label="Domain"
value={gemaltoDomain}
error={validationErrors["gemalto_domain"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
type="number"
min="0"
id="gemalto_retry"
name="gemalto_retry"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("gemaltoRetry", e.target.value);
cleanValidation("gemalto_retry");
}}
label="Retry (seconds)"
value={gemaltoRetry}
error={validationErrors["gemalto_retry"] || ""}
/>
</Grid>
<h5>Custom CA Root certificate verification</h5>
<Grid item xs={12}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileGemaltoCa(fileName, encodedValue);
cleanValidation("gemalto_ca");
}}
accept=".cer,.crt,.cert,.pem"
id="gemalto_ca"
name="gemalto_ca"
label="CA"
value={gemaltoCA.cert}
/>
</Grid>
</Fragment>
)}
</Fragment>
)}
</Fragment>
);
};
const mapState = (state: AppState) => ({
enableEncryption:
state.tenants.createTenant.fields.encryption.enableEncryption,
encryptionType: state.tenants.createTenant.fields.encryption.encryptionType,
gemaltoEndpoint: state.tenants.createTenant.fields.encryption.gemaltoEndpoint,
gemaltoToken: state.tenants.createTenant.fields.encryption.gemaltoToken,
gemaltoDomain: state.tenants.createTenant.fields.encryption.gemaltoDomain,
gemaltoRetry: state.tenants.createTenant.fields.encryption.gemaltoRetry,
awsEndpoint: state.tenants.createTenant.fields.encryption.awsEndpoint,
awsRegion: state.tenants.createTenant.fields.encryption.awsRegion,
awsKMSKey: state.tenants.createTenant.fields.encryption.awsKMSKey,
awsAccessKey: state.tenants.createTenant.fields.encryption.awsAccessKey,
awsSecretKey: state.tenants.createTenant.fields.encryption.awsSecretKey,
awsToken: state.tenants.createTenant.fields.encryption.awsToken,
vaultEndpoint: state.tenants.createTenant.fields.encryption.vaultEndpoint,
vaultEngine: state.tenants.createTenant.fields.encryption.vaultEngine,
vaultNamespace: state.tenants.createTenant.fields.encryption.vaultNamespace,
vaultPrefix: state.tenants.createTenant.fields.encryption.vaultPrefix,
vaultAppRoleEngine:
state.tenants.createTenant.fields.encryption.vaultAppRoleEngine,
vaultId: state.tenants.createTenant.fields.encryption.vaultId,
vaultSecret: state.tenants.createTenant.fields.encryption.vaultSecret,
vaultRetry: state.tenants.createTenant.fields.encryption.vaultRetry,
vaultPing: state.tenants.createTenant.fields.encryption.vaultPing,
gcpProjectID: state.tenants.createTenant.fields.encryption.gcpProjectID,
gcpEndpoint: state.tenants.createTenant.fields.encryption.gcpEndpoint,
gcpClientEmail: state.tenants.createTenant.fields.encryption.gcpClientEmail,
gcpClientID: state.tenants.createTenant.fields.encryption.gcpClientID,
gcpPrivateKeyID: state.tenants.createTenant.fields.encryption.gcpPrivateKeyID,
gcpPrivateKey: state.tenants.createTenant.fields.encryption.gcpPrivateKey,
enableCustomCertsForKES:
state.tenants.createTenant.fields.encryption.enableCustomCertsForKES,
enableAutoCert: state.tenants.createTenant.fields.security.enableAutoCert,
enableTLS: state.tenants.createTenant.fields.security.enableTLS,
minioCertificates: state.tenants.createTenant.certificates.minioCertificates,
serverCertificate: state.tenants.createTenant.certificates.serverCertificate,
clientCertificate: state.tenants.createTenant.certificates.clientCertificate,
vaultCertificate: state.tenants.createTenant.certificates.vaultCertificate,
vaultCA: state.tenants.createTenant.certificates.vaultCA,
gemaltoCA: state.tenants.createTenant.certificates.gemaltoCA,
enableCustomCerts:
state.tenants.createTenant.fields.security.enableCustomCerts,
});
const connector = connect(mapState, {
updateAddField,
isPageValid,
addFileServerCert,
addFileClientCert,
addFileVaultCert,
addFileVaultCa,
addFileGemaltoCa,
});
export default withStyles(styles)(connector(Encryption));

View File

@@ -0,0 +1,394 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, useCallback, Fragment } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid, Typography } from "@material-ui/core";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { updateAddField, isPageValid } from "../../actions";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import { AppState } from "../../../../../store";
import { clearValidationError } from "../../utils";
import RadioGroupSelector from "../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
interface IIdentityProviderProps {
classes: any;
idpSelection: string;
openIDURL: string;
openIDClientID: string;
openIDSecretID: string;
ADURL: string;
ADSkipTLS: boolean;
ADServerInsecure: boolean;
ADUserNameFilter: string;
ADGroupBaseDN: string;
ADGroupSearchFilter: string;
ADNameAttribute: string;
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...wizardCommon,
});
const IdentityProvider = ({
classes,
idpSelection,
openIDURL,
openIDClientID,
openIDSecretID,
ADURL,
ADSkipTLS,
ADServerInsecure,
ADUserNameFilter,
ADGroupBaseDN,
ADGroupSearchFilter,
ADNameAttribute,
updateAddField,
isPageValid,
}: IIdentityProviderProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
// Common
const updateField = useCallback(
(field: string, value: any) => {
updateAddField("identityProvider", field, value);
},
[updateAddField]
);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
// Validation
useEffect(() => {
let customIDPValidation: IValidation[] = [];
if (idpSelection === "Built-in") {
isPageValid("identityProvider", true);
setValidationErrors({});
return;
}
if (idpSelection === "OpenID") {
customIDPValidation = [
...customIDPValidation,
{
fieldKey: "openID_URL",
required: true,
value: openIDURL,
},
{
fieldKey: "openID_clientID",
required: true,
value: openIDClientID,
},
{
fieldKey: "openID_secretID",
required: true,
value: openIDSecretID,
},
];
}
if (idpSelection === "AD") {
customIDPValidation = [
...customIDPValidation,
{
fieldKey: "AD_URL",
required: true,
value: ADURL,
},
{
fieldKey: "ad_userNameFilter",
required: true,
value: ADUserNameFilter,
},
{
fieldKey: "ad_groupBaseDN",
required: true,
value: ADGroupBaseDN,
},
{
fieldKey: "ad_groupSearchFilter",
required: true,
value: ADGroupSearchFilter,
},
{
fieldKey: "ad_nameAttribute",
required: true,
value: ADNameAttribute,
},
];
}
const commonVal = commonFormValidation(customIDPValidation);
isPageValid("identityProvider", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [
idpSelection,
openIDURL,
openIDClientID,
openIDSecretID,
ADURL,
ADUserNameFilter,
ADGroupBaseDN,
ADGroupSearchFilter,
ADNameAttribute,
isPageValid,
]);
return (
<Fragment>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Identity Provider</h3>
<span className={classes.descriptionText}>
Access to the tenant can be controlled via an external Identity
Manager.
</span>
</div>
<Grid item xs={12}>
<RadioGroupSelector
currentSelection={idpSelection}
id="idp-options"
name="idp-options"
label="Protocol"
onChange={(e) => {
updateField("idpSelection", e.target.value);
}}
selectorOptions={[
{ label: "Built-in", value: "Built-in" },
{ label: "OpenID", value: "OpenID" },
{ label: "Active Directory", value: "AD" },
]}
/>
MinIO supports both OpenID and Active Directory
</Grid>
{idpSelection === "OpenID" && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="openID_URL"
name="openID_URL"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("openIDURL", e.target.value);
cleanValidation("openID_URL");
}}
label="URL"
value={openIDURL}
error={validationErrors["openID_URL"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="openID_clientID"
name="openID_clientID"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("openIDClientID", e.target.value);
cleanValidation("openID_clientID");
}}
label="Client ID"
value={openIDClientID}
error={validationErrors["openID_clientID"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="openID_secretID"
name="openID_secretID"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("openIDSecretID", e.target.value);
cleanValidation("openID_secretID");
}}
label="Secret ID"
value={openIDSecretID}
error={validationErrors["openID_secretID"] || ""}
required
/>
</Grid>
</Fragment>
)}
{idpSelection === "AD" && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="AD_URL"
name="AD_URL"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("ADURL", e.target.value);
cleanValidation("AD_URL");
}}
label="URL"
value={ADURL}
error={validationErrors["AD_URL"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<FormSwitchWrapper
value="ad_skipTLS"
id="ad_skipTLS"
name="ad_skipTLS"
checked={ADSkipTLS}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("ADSkipTLS", checked);
}}
label={"Skip TLS Verification"}
/>
</Grid>
<Grid item xs={12}>
<FormSwitchWrapper
value="ad_serverInsecure"
id="ad_serverInsecure"
name="ad_serverInsecure"
checked={ADServerInsecure}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("ADServerInsecure", checked);
}}
label={"Server Insecure"}
/>
</Grid>
{ADServerInsecure ? (
<Grid item xs={12}>
<Typography
className={classes.error}
variant="caption"
display="block"
gutterBottom
>
Warning: All traffic with Active Directory will be unencrypted
</Typography>
<br />
</Grid>
) : null}
<Grid item xs={12}>
<InputBoxWrapper
id="ad_userNameFilter"
name="ad_userNameFilter"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("ADUserNameFilter", e.target.value);
cleanValidation("ad_userNameFilter");
}}
label="User Search Filter"
value={ADUserNameFilter}
error={validationErrors["ad_userNameFilter"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="ad_groupBaseDN"
name="ad_groupBaseDN"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("ADGroupBaseDN", e.target.value);
cleanValidation("ad_groupBaseDN");
}}
label="Group Search Base DN"
value={ADGroupBaseDN}
error={validationErrors["ad_groupBaseDN"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="ad_groupSearchFilter"
name="ad_groupSearchFilter"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("ADGroupSearchFilter", e.target.value);
cleanValidation("ad_groupSearchFilter");
}}
label="Group Search Filter"
value={ADGroupSearchFilter}
error={validationErrors["ad_groupSearchFilter"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="ad_nameAttribute"
name="ad_nameAttribute"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("ADNameAttribute", e.target.value);
cleanValidation("ad_nameAttribute");
}}
label="Group Name Attribute"
value={ADNameAttribute}
error={validationErrors["ad_nameAttribute"] || ""}
required
/>
</Grid>
</Fragment>
)}
</Fragment>
);
};
const mapState = (state: AppState) => ({
idpSelection: state.tenants.createTenant.fields.identityProvider.idpSelection,
openIDURL: state.tenants.createTenant.fields.identityProvider.openIDURL,
openIDClientID:
state.tenants.createTenant.fields.identityProvider.openIDClientID,
openIDSecretID:
state.tenants.createTenant.fields.identityProvider.openIDSecretID,
ADURL: state.tenants.createTenant.fields.identityProvider.ADURL,
ADSkipTLS: state.tenants.createTenant.fields.identityProvider.ADSkipTLS,
ADServerInsecure:
state.tenants.createTenant.fields.identityProvider.ADServerInsecure,
ADUserNameFilter:
state.tenants.createTenant.fields.identityProvider.ADUserNameFilter,
ADGroupBaseDN:
state.tenants.createTenant.fields.identityProvider.ADGroupBaseDN,
ADGroupSearchFilter:
state.tenants.createTenant.fields.identityProvider.ADGroupSearchFilter,
ADNameAttribute:
state.tenants.createTenant.fields.identityProvider.ADNameAttribute,
});
const connector = connect(mapState, {
updateAddField,
isPageValid,
});
export default withStyles(styles)(connector(IdentityProvider));

View File

@@ -0,0 +1,272 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, useMemo, useCallback } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import get from "lodash/get";
import debounce from "lodash/debounce";
import Grid from "@material-ui/core/Grid";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { setModalErrorSnackMessage } from "../../../../../actions";
import {
setAdvancedMode,
updateAddField,
isPageValid,
setStorageClassesList,
setLimitSize,
} from "../../actions";
import {
IQuotaElement,
IQuotas,
Opts,
getLimitSizes,
} from "../../ListTenants/utils";
import { AppState } from "../../../../../store";
import { commonFormValidation } from "../../../../../utils/validationFunctions";
import { clearValidationError } from "../../utils";
import api from "../../../../../common/api";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...wizardCommon,
});
interface INameTenantScreen {
classes: any;
storageClasses: Opts[];
setAdvancedMode: typeof setAdvancedMode;
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
setStorageClassesList: typeof setStorageClassesList;
setLimitSize: typeof setLimitSize;
tenantName: string;
namespace: string;
selectedStorageClass: string;
advancedMode: boolean;
}
const NameTenant = ({
classes,
storageClasses,
advancedMode,
tenantName,
namespace,
selectedStorageClass,
setAdvancedMode,
updateAddField,
setStorageClassesList,
setLimitSize,
isPageValid,
}: INameTenantScreen) => {
const [validationErrors, setValidationErrors] = useState<any>({});
// Common
const updateField = useCallback(
(field: string, value: string) => {
updateAddField("nameTenant", field, value);
},
[updateAddField]
);
// Storage classes retrieval
const getNamespaceInformation = useCallback(() => {
updateField("selectedStorageClass", "");
setStorageClassesList([]);
api
.invoke(
"GET",
`/api/v1/namespaces/${namespace}/resourcequotas/${namespace}-storagequota`
)
.then((res: IQuotas) => {
const elements: IQuotaElement[] = get(res, "elements", []);
setLimitSize(getLimitSizes(res));
const newStorage = elements.map((storageClass: any) => {
const name = get(storageClass, "name", "").split(
".storageclass.storage.k8s.io/requests.storage"
)[0];
return { label: name, value: name };
});
setStorageClassesList(newStorage);
if (newStorage.length > 0) {
updateField("selectedStorageClass", newStorage[0].value);
}
})
.catch((err: any) => {
console.error(err);
});
}, [namespace, setLimitSize, setStorageClassesList, updateField]);
const debounceNamespace = useMemo(
() => debounce(getNamespaceInformation, 500),
[getNamespaceInformation]
);
useEffect(() => {
if (namespace !== "") {
debounceNamespace();
// Cancel previous debounce calls during useEffect cleanup.
return debounceNamespace.cancel;
}
}, [debounceNamespace, namespace]);
// Validation
useEffect(() => {
const commonValidation = commonFormValidation([
{
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: "namespace",
required: true,
value: namespace,
customValidation: storageClasses.length < 1,
customValidationMessage: "Please enter a valid namespace",
},
]);
const isValid =
!("tenant-name" in commonValidation) &&
!("namespace" in commonValidation) &&
storageClasses.length > 0;
isPageValid("nameTenant", isValid);
setValidationErrors(commonValidation);
}, [storageClasses, namespace, tenantName, isPageValid]);
const frmValidationCleanup = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
return (
<React.Fragment>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Name Tenant</h3>
<span className={classes.descriptionText}>
How would you like to name this new tenant?
</span>
</div>
<Grid item xs={12}>
<InputBoxWrapper
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("tenantName", e.target.value);
frmValidationCleanup("tenant-name");
}}
label="Name"
value={tenantName}
required
error={validationErrors["tenant-name"] || ""}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="namespace"
name="namespace"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("namespace", e.target.value);
frmValidationCleanup("namespace");
}}
label="Namespace"
value={namespace}
error={validationErrors["namespace"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
updateField("selectedStorageClass", e.target.value as string);
}}
label="Storage Class"
value={selectedStorageClass}
options={storageClasses}
disabled={storageClasses.length < 1}
/>
</Grid>
<Grid item xs={12}>
<br />
<span className={classes.descriptionText}>
Check 'Advanced Mode' for additional configuration options, such as
configuring an Identity Provider, Encryption at rest, and customized
TLS/SSL Certificates.
<br />
Leave 'Advanced Mode' unchecked to use the secure default settings for
the tenant.
</span>
<br />
<br />
<FormSwitchWrapper
value="adv_mode"
id="adv_mode"
name="adv_mode"
checked={advancedMode}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setAdvancedMode(checked);
}}
label={"Advanced Mode"}
/>
</Grid>
</React.Fragment>
);
};
const mapState = (state: AppState) => ({
advancedMode: state.tenants.createTenant.advancedModeOn,
tenantName: state.tenants.createTenant.fields.nameTenant.tenantName,
namespace: state.tenants.createTenant.fields.nameTenant.namespace,
selectedStorageClass:
state.tenants.createTenant.fields.nameTenant.selectedStorageClass,
storageClasses: state.tenants.createTenant.storageClasses,
});
const connector = connect(mapState, {
setModalErrorSnackMessage,
setAdvancedMode,
updateAddField,
setStorageClassesList,
setLimitSize,
isPageValid,
});
export default withStyles(styles)(connector(NameTenant));

View File

@@ -0,0 +1,156 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import { AppState } from "../../../../../store";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
interface IPreviewProps {
classes: any;
tenantName: string;
customImage: boolean;
imageName: string;
consoleImage: string;
namespace: string;
selectedStorageClass: string;
volumeSize: string;
sizeFactor: string;
advancedMode: boolean;
enableTLS: boolean;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...wizardCommon,
});
const Preview = ({
classes,
tenantName,
customImage,
imageName,
consoleImage,
namespace,
selectedStorageClass,
volumeSize,
sizeFactor,
advancedMode,
enableTLS,
}: IPreviewProps) => {
return (
<Fragment>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Review</h3>
<span className={classes.descriptionText}>
Review the details of the new tenant
</span>
</div>
<Table size="small">
<TableBody>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Tenant Name
</TableCell>
<TableCell>{tenantName}</TableCell>
</TableRow>
{customImage && (
<Fragment>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
MinIO Image
</TableCell>
<TableCell>{imageName}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Console Image
</TableCell>
<TableCell>{consoleImage}</TableCell>
</TableRow>
</Fragment>
)}
{namespace !== "" && (
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Namespace
</TableCell>
<TableCell>{namespace}</TableCell>
</TableRow>
)}
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Storage Class
</TableCell>
<TableCell>{selectedStorageClass}</TableCell>
</TableRow>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Total Size
</TableCell>
<TableCell>
{volumeSize} {sizeFactor}
</TableCell>
</TableRow>
{advancedMode && (
<Fragment>
<TableRow>
<TableCell align="right" className={classes.tableTitle}>
Enable TLS
</TableCell>
<TableCell>{enableTLS ? "Enabled" : "Disabled"}</TableCell>
</TableRow>
</Fragment>
)}
</TableBody>
</Table>
</Fragment>
);
};
const mapState = (state: AppState) => ({
advancedMode: state.tenants.createTenant.advancedModeOn,
enableTLS: state.tenants.createTenant.fields.security.enableTLS,
tenantName: state.tenants.createTenant.fields.nameTenant.tenantName,
selectedStorageClass:
state.tenants.createTenant.fields.nameTenant.selectedStorageClass,
customImage: state.tenants.createTenant.fields.configure.customImage,
imageName: state.tenants.createTenant.fields.configure.imageName,
consoleImage: state.tenants.createTenant.fields.configure.consoleImage,
namespace: state.tenants.createTenant.fields.nameTenant.namespace,
volumeSize: state.tenants.createTenant.fields.tenantSize.volumeSize,
sizeFactor: state.tenants.createTenant.fields.tenantSize.sizeFactor,
});
const connector = connect(mapState, {});
export default withStyles(styles)(connector(Preview));

View File

@@ -0,0 +1,358 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, useCallback, Fragment } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, Divider, Grid, Typography } from "@material-ui/core";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import {
updateAddField,
isPageValid,
addFileToCaCertificates,
deleteCaCertificate,
addCaCertificate,
addKeyPair,
addFileToKeyPair,
deleteKeyPair,
addConsoleCertificate,
} from "../../actions";
import { AppState } from "../../../../../store";
import { KeyPair } from "../../ListTenants/utils";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import FileSelector from "../../../Common/FormComponents/FileSelector/FileSelector";
interface ISecurityProps {
classes: any;
enableTLS: boolean;
enableAutoCert: boolean;
enableCustomCerts: boolean;
minioCertificates: KeyPair[];
caCertificates: KeyPair[];
consoleCertificate: KeyPair;
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
addFileToCaCertificates: typeof addFileToCaCertificates;
deleteCaCertificate: typeof deleteCaCertificate;
addCaCertificate: typeof addCaCertificate;
addKeyPair: typeof addKeyPair;
addFileToKeyPair: typeof addFileToKeyPair;
deleteKeyPair: typeof deleteKeyPair;
addConsoleCertificate: typeof addConsoleCertificate;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...wizardCommon,
});
const Security = ({
classes,
enableTLS,
enableAutoCert,
enableCustomCerts,
minioCertificates,
caCertificates,
consoleCertificate,
updateAddField,
isPageValid,
addFileToCaCertificates,
deleteCaCertificate,
addCaCertificate,
addKeyPair,
addFileToKeyPair,
deleteKeyPair,
}: ISecurityProps) => {
// Common
const updateField = useCallback(
(field: string, value: any) => {
updateAddField("security", field, value);
},
[updateAddField]
);
// Validation
useEffect(() => {
if (!enableTLS) {
isPageValid("security", true);
return;
}
if (enableAutoCert) {
isPageValid("security", true);
return;
}
if (enableCustomCerts) {
isPageValid("security", true);
return;
}
isPageValid("security", false);
}, [enableTLS, enableAutoCert, enableCustomCerts, isPageValid]);
return (
<Fragment>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Security</h3>
</div>
<Grid item xs={12}>
<FormSwitchWrapper
value="enableTLS"
id="enableTLS"
name="enableTLS"
checked={enableTLS}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("enableTLS", checked);
}}
label={"Enable TLS"}
/>
Enable TLS for the tenant, this is required for Encryption Configuration
{enableTLS && (
<Fragment>
<br />
<br />
<Typography variant="caption" display="block" gutterBottom>
AutoCert: MinIO Operator will generate all TLS certificates
automatically
</Typography>
<Typography variant="caption" display="block" gutterBottom>
Custom certificates: Allow user to provide your own certificates
</Typography>
<br />
</Fragment>
)}
</Grid>
{enableTLS && (
<Fragment>
<Grid item xs={12}>
<FormSwitchWrapper
value="enableAutoCert"
id="enableAutoCert"
name="enableAutoCert"
checked={enableAutoCert}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("enableAutoCert", checked);
}}
label={"Enable AutoCert"}
/>
<FormSwitchWrapper
value="enableCustomCerts"
id="enableCustomCerts"
name="enableCustomCerts"
checked={enableCustomCerts}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("enableCustomCerts", checked);
}}
label={"Custom Certificates"}
/>
</Grid>
{enableCustomCerts && (
<Fragment>
<Grid container>
<Grid item xs={12}>
<Typography variant="overline" display="block" gutterBottom>
MinIO Certificates
</Typography>
</Grid>
{minioCertificates.map((keyPair: KeyPair) => (
<Fragment key={keyPair.id}>
<Grid item xs={5}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileToKeyPair(
keyPair.id,
"key",
fileName,
encodedValue
);
}}
accept=".key,.pem"
id="tlsKey"
name="tlsKey"
label="Key"
value={keyPair.key}
/>
</Grid>
<Grid item xs={5}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileToKeyPair(
keyPair.id,
"cert",
fileName,
encodedValue
);
}}
accept=".cer,.crt,.cert,.pem"
id="tlsCert"
name="tlsCert"
label="Cert"
value={keyPair.cert}
/>
</Grid>
<Grid item xs={1}>
<Button
onClick={() => {
deleteKeyPair(keyPair.id);
}}
color="secondary"
>
Remove
</Button>
</Grid>
</Fragment>
))}
<Grid item xs={12}>
<Button onClick={addKeyPair} color="primary">
Add More
</Button>
</Grid>
</Grid>
<Grid container>
<Grid item xs={12}>
<br />
<Divider />
<br />
</Grid>
</Grid>
<Grid container>
<Grid item xs={12}>
<Typography variant="overline" display="block" gutterBottom>
CA Certificates
</Typography>
</Grid>
{caCertificates.map((keyPair: KeyPair) => (
<Fragment key={keyPair.id}>
<Grid item xs={10}>
<FileSelector
onChange={(encodedValue, fileName) => {
addFileToCaCertificates(
keyPair.id,
"cert",
fileName,
encodedValue
);
}}
accept=".cer,.crt,.cert,.pem"
id="tlsCert"
name="tlsCert"
label="Cert"
value={keyPair.cert}
/>
</Grid>
<Grid item xs={1}>
<Button
onClick={() => {
deleteCaCertificate(keyPair.id);
}}
color="secondary"
>
Remove
</Button>
</Grid>
</Fragment>
))}
<Grid item xs={12}>
<Button onClick={addCaCertificate} color="primary">
Add More
</Button>
</Grid>
</Grid>
<Grid container>
<Grid item xs={12}>
<br />
<Divider />
<br />
</Grid>
</Grid>
<Grid container>
<Grid item xs={12}>
<Typography variant="overline" display="block" gutterBottom>
Console Certificates
</Typography>
</Grid>
<Grid item xs={6}>
<FileSelector
onChange={(encodedValue, fileName) => {
addConsoleCertificate("key", fileName, encodedValue);
}}
accept=".key,.pem"
id="consoleKey"
name="consoleKey"
label="Key"
value={consoleCertificate.key}
/>
</Grid>
<Grid item xs={6}>
<FileSelector
onChange={(encodedValue, fileName) => {
addConsoleCertificate("cert", fileName, encodedValue);
}}
accept=".cer,.crt,.cert,.pem"
id="consoleCert"
name="consoleCert"
label="Cert"
value={consoleCertificate.cert}
/>
</Grid>
</Grid>
</Fragment>
)}
</Fragment>
)}
</Fragment>
);
};
const mapState = (state: AppState) => ({
enableTLS: state.tenants.createTenant.fields.security.enableTLS,
enableAutoCert: state.tenants.createTenant.fields.security.enableAutoCert,
enableCustomCerts:
state.tenants.createTenant.fields.security.enableCustomCerts,
minioCertificates: state.tenants.createTenant.certificates.minioCertificates,
caCertificates: state.tenants.createTenant.certificates.caCertificates,
consoleCertificate:
state.tenants.createTenant.certificates.consoleCertificate,
});
const connector = connect(mapState, {
updateAddField,
isPageValid,
addFileToCaCertificates,
deleteCaCertificate,
addCaCertificate,
addKeyPair,
addFileToKeyPair,
deleteKeyPair,
addConsoleCertificate,
});
export default withStyles(styles)(connector(Security));

View File

@@ -0,0 +1,520 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, useState, useEffect, useCallback } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { AppState } from "../../../../../store";
import { updateAddField, isPageValid } from "../../actions";
import {
wizardCommon,
modalBasic,
} from "../../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import {
getBytes,
k8sfactorForDropdown,
niceBytes,
calculateDistribution,
erasureCodeCalc,
setMemoryResource,
} from "../../../../../common/utils";
import { clearValidationError } from "../../utils";
import { ecListTransform, Opts } from "../../ListTenants/utils";
import { IMemorySize } from "../../ListTenants/types";
import { ICapacity, IErasureCodeCalc } from "../../../../../common/types";
import api from "../../../../../common/api";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { commonFormValidation } from "../../../../../utils/validationFunctions";
interface ITenantSizeProps {
classes: any;
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
advancedMode: boolean;
volumeSize: string;
sizeFactor: string;
drivesPerServer: string;
nodes: string;
memoryNode: string;
ecParity: string;
ecParityChoices: Opts[];
cleanECChoices: string[];
maxAllocableMemo: number;
memorySize: IMemorySize;
distribution: any;
ecParityCalc: IErasureCodeCalc;
limitSize: any;
selectedStorageClass: string;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...wizardCommon,
});
const TenantSize = ({
classes,
updateAddField,
isPageValid,
advancedMode,
volumeSize,
sizeFactor,
drivesPerServer,
nodes,
memoryNode,
ecParity,
ecParityChoices,
cleanECChoices,
maxAllocableMemo,
memorySize,
distribution,
ecParityCalc,
limitSize,
selectedStorageClass,
}: ITenantSizeProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
const usableInformation = ecParityCalc.storageFactors.find(
(element) => element.erasureCode === ecParity
);
// Common
const updateField = useCallback(
(field: string, value: any) => {
updateAddField("tenantSize", field, value);
},
[updateAddField]
);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
/*Debounce functions*/
// Storage Quotas
const validateMemorySize = useCallback(() => {
const memSize = parseInt(memoryNode) || 0;
const clusterSize = volumeSize || 0;
const maxMemSize = maxAllocableMemo || 0;
const clusterSizeFactor = sizeFactor;
const clusterSizeBytes = getBytes(
clusterSize.toString(10),
clusterSizeFactor
);
const memoSize = setMemoryResource(memSize, clusterSizeBytes, maxMemSize);
updateField("memorySize", memoSize);
}, [maxAllocableMemo, memoryNode, sizeFactor, updateField, volumeSize]);
const getMaxAllocableMemory = (nodes: string) => {
if (nodes !== "" && !isNaN(parseInt(nodes))) {
api
.invoke(
"GET",
`/api/v1/cluster/max-allocatable-memory?num_nodes=${nodes}`
)
.then((res: { max_memory: number }) => {
const maxMemory = res.max_memory ? res.max_memory : 0;
updateField("maxAllocableMemo", maxMemory);
})
.catch((err: any) => {
updateField("maxAllocableMemo", 0);
console.error(err);
});
}
};
useEffect(() => {
validateMemorySize();
}, [memoryNode, validateMemorySize]);
useEffect(() => {
validateMemorySize();
}, [maxAllocableMemo, validateMemorySize]);
useEffect(() => {
if (ecParityChoices.length > 0 && distribution.error === "") {
const ecCodeValidated = erasureCodeCalc(
cleanECChoices,
distribution.persistentVolumes,
distribution.pvSize,
distribution.nodes
);
updateField("ecParityCalc", ecCodeValidated);
updateField("ecParity", ecCodeValidated.defaultEC);
}
}, [ecParityChoices.length, distribution, cleanECChoices, updateField]);
/*End debounce functions*/
/*Calculate Allocation*/
useEffect(() => {
validateClusterSize();
getECValue();
getMaxAllocableMemory(nodes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodes, volumeSize, sizeFactor, drivesPerServer]);
const validateClusterSize = () => {
const size = volumeSize;
const factor = sizeFactor;
const limitSize = getBytes("12", "Ti", true);
const clusterCapacity: ICapacity = {
unit: factor,
value: size.toString(),
};
const distrCalculate = calculateDistribution(
clusterCapacity,
parseInt(nodes),
parseInt(limitSize),
parseInt(drivesPerServer)
);
updateField("distribution", distrCalculate);
};
const getECValue = () => {
updateField("ecParity", "");
if (nodes.trim() !== "" && drivesPerServer.trim() !== "") {
api
.invoke("GET", `/api/v1/get-parity/${nodes}/${drivesPerServer}`)
.then((ecList: string[]) => {
updateField("ecParityChoices", ecListTransform(ecList));
updateField("cleanECChoices", ecList);
})
.catch((err: any) => {
updateField("ecparityChoices", []);
isPageValid("tenantSize", false);
updateField("ecParity", "");
});
}
};
/*Calculate Allocation End*/
/* Validations of pages */
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 ||
parseInt(parsedSize) > limitSize[selectedStorageClass],
customValidationMessage: `Volume size must be greater than 1Gi and less than ${niceBytes(
limitSize[selectedStorageClass],
true
)}`,
},
{
fieldKey: "memory_per_node",
required: true,
value: memoryNode,
customValidation: parseInt(memoryNode) < 2,
customValidationMessage: "Memory size must be greater than 2Gi",
},
{
fieldKey: "drivesps",
required: true,
value: drivesPerServer,
customValidation: parseInt(drivesPerServer) < 1,
customValidationMessage: "There must be at least one drive",
},
]);
isPageValid(
"tenantSize",
!("nodes" in commonValidation) &&
!("volume_size" in commonValidation) &&
!("memory_per_node" in commonValidation) &&
!("drivesps" in commonValidation) &&
distribution.error === "" &&
ecParityCalc.error === 0 &&
memorySize.error === ""
);
setValidationErrors(commonValidation);
}, [
nodes,
volumeSize,
sizeFactor,
memoryNode,
distribution,
drivesPerServer,
ecParityCalc,
memorySize,
limitSize,
selectedStorageClass,
]);
/* End Validation of pages */
return (
<Fragment>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Tenant Size</h3>
<span className={classes.descriptionText}>
Please select the desired capacity
</span>
</div>
<span className={classes.error}>{distribution.error}</span>
<span className={classes.error}>{memorySize.error}</span>
<Grid item xs={12}>
<InputBoxWrapper
id="nodes"
name="nodes"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("nodes", e.target.value);
cleanValidation("nodes");
}}
label="Number of Servers"
value={nodes}
min="4"
required
error={validationErrors["nodes"] || ""}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="drivesps"
name="drivesps"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("drivesPerServer", e.target.value);
cleanValidation("drivesps");
}}
label="Number of Drives per Server"
value={drivesPerServer}
min="1"
required
error={validationErrors["drivesps"] || ""}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<div>
<InputBoxWrapper
type="number"
id="volume_size"
name="volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("volumeSize", e.target.value);
cleanValidation("volume_size");
}}
label="Total Size"
value={volumeSize}
required
error={validationErrors["volume_size"] || ""}
min="0"
/>
</div>
<div className={classes.sizeFactorContainer}>
<SelectWrapper
label={"Unit"}
id="size_factor"
name="size_factor"
value={sizeFactor}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
updateField("sizeFactor", e.target.value as string);
}}
options={k8sfactorForDropdown()}
/>
</div>
</div>
</Grid>
{advancedMode && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
type="number"
id="memory_per_node"
name="memory_per_node"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("memoryNode", e.target.value);
cleanValidation("memory_per_node");
}}
label="Memory per Node [Gi]"
value={memoryNode}
required
error={validationErrors["memory_per_node"] || ""}
min="2"
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
id="ec_parity"
name="ec_parity"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
updateField("ecParity", e.target.value as string);
}}
label="Erasure Code Parity"
value={ecParity}
options={ecParityChoices}
/>
<span className={classes.descriptionText}>
Please select the desired parity. This setting will change the max
usable capacity in the cluster
</span>
</Grid>
</Fragment>
)}
<h4>Resource Allocation</h4>
<Table className={classes.table} aria-label="simple table">
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
Number of Servers
</TableCell>
<TableCell align="right">
{parseInt(nodes) > 0 ? nodes : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Drives per Server
</TableCell>
<TableCell align="right">
{distribution ? distribution.disks : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Drive Capacity
</TableCell>
<TableCell align="right">
{distribution ? niceBytes(distribution.pvSize) : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Total Number of Volumes
</TableCell>
<TableCell align="right">
{distribution ? distribution.persistentVolumes : "-"}
</TableCell>
</TableRow>
{!advancedMode && (
<TableRow>
<TableCell component="th" scope="row">
Memory per Node
</TableCell>
<TableCell align="right">{memoryNode} Gi</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{ecParityCalc.error === 0 && usableInformation && (
<Fragment>
<h4>Erasure Code Configuration</h4>
<Table className={classes.table} aria-label="simple table">
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
EC Parity
</TableCell>
<TableCell align="right">
{ecParity !== "" ? ecParity : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Raw Capacity
</TableCell>
<TableCell align="right">
{niceBytes(ecParityCalc.rawCapacity)}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Usable Capacity
</TableCell>
<TableCell align="right">
{niceBytes(usableInformation.maxCapacity)}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Number of server failures to tolerate
</TableCell>
<TableCell align="right">
{distribution
? Math.floor(
usableInformation.maxFailureTolerations /
distribution.disks
)
: "-"}
</TableCell>
</TableRow>
</TableBody>
</Table>
</Fragment>
)}
</Fragment>
);
};
const mapState = (state: AppState) => ({
advancedMode: state.tenants.createTenant.advancedModeOn,
volumeSize: state.tenants.createTenant.fields.tenantSize.volumeSize,
sizeFactor: state.tenants.createTenant.fields.tenantSize.sizeFactor,
drivesPerServer: state.tenants.createTenant.fields.tenantSize.drivesPerServer,
nodes: state.tenants.createTenant.fields.tenantSize.nodes,
memoryNode: state.tenants.createTenant.fields.tenantSize.memoryNode,
ecParity: state.tenants.createTenant.fields.tenantSize.ecParity,
ecParityChoices: state.tenants.createTenant.fields.tenantSize.ecParityChoices,
cleanECChoices: state.tenants.createTenant.fields.tenantSize.cleanECChoices,
maxAllocableMemo:
state.tenants.createTenant.fields.tenantSize.maxAllocableMemo,
memorySize: state.tenants.createTenant.fields.tenantSize.memorySize,
distribution: state.tenants.createTenant.fields.tenantSize.distribution,
ecParityCalc: state.tenants.createTenant.fields.tenantSize.ecParityCalc,
limitSize: state.tenants.createTenant.fields.tenantSize.limitSize,
selectedStorageClass:
state.tenants.createTenant.fields.nameTenant.selectedStorageClass,
});
const connector = connect(mapState, {
updateAddField,
isPageValid,
});
export default withStyles(styles)(connector(TenantSize));

File diff suppressed because it is too large Load Diff

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, { useEffect, useState } from "react";
import React, { useEffect, useState, Fragment } from "react";
import { connect } from "react-redux";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
@@ -27,28 +27,33 @@ import { niceBytes } from "../../../../common/utils";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import {
actionsTray,
containerForHeader,
searchField,
settingsCommon,
} from "../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../actions";
import { CreateIcon } from "../../../../icons";
import api from "../../../../common/api";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import DeleteTenant from "./DeleteTenant";
import AddTenant from "./AddTenant";
import AddTenant from "../AddTenant/AddTenant";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
import history from "../../../../history";
import RefreshIcon from "@material-ui/icons/Refresh";
import PageHeader from "../../Common/PageHeader/PageHeader";
import { Link } from "react-router-dom";
import SlideOptions from "../../Common/SlideOptions/SlideOptions";
import BackSettingsIcon from "../../../../icons/BackSettingsIcon";
import { resetAddTenantForm } from "../actions";
interface ITenantsList {
classes: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
resetAddTenantForm: typeof resetAddTenantForm;
}
const styles = (theme: Theme) =>
createStyles({
...actionsTray,
...searchField,
...settingsCommon,
seeMore: {
marginTop: theme.spacing(3),
},
@@ -74,13 +79,28 @@ const styles = (theme: Theme) =>
},
},
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
actionsTray: {
...actionsTray.actionsTray,
padding: "0 38px",
},
tenantsContainer: {
padding: "15px 0",
},
customConfigurationPage: {
height: "calc(100vh - 440px)",
scrollbarWidth: "none" as const,
"&::-webkit-scrollbar": {
display: "none",
},
},
});
const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
const [createTenantOpen, setCreateTenantOpen] = useState<boolean>(false);
const ListTenants = ({
classes,
setErrorSnackMessage,
resetAddTenantForm,
}: ITenantsList) => {
const [currentPanel, setCurrentPanel] = useState<number>(0);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedTenant, setSelectedTenant] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
@@ -92,16 +112,9 @@ const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
setCreatedAccount,
] = useState<NewServiceAccount | null>(null);
const closeAddModalAndRefresh = (
reloadData: boolean,
res: NewServiceAccount | null
) => {
setCreateTenantOpen(false);
if (res !== null) {
setShowNewCredentials(true);
setCreatedAccount(res);
}
const closeAddModalAndRefresh = (reloadData: boolean) => {
setCurrentPanel(0);
resetAddTenantForm();
if (reloadData) {
setIsLoading(true);
@@ -131,6 +144,11 @@ const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
setCreatedAccount(null);
};
const backClick = () => {
setCurrentPanel(currentPanel - 1);
resetAddTenantForm();
};
const tableActions = [
{ type: "view", onClick: redirectToTenantDetails },
{ type: "delete", onClick: confirmDeleteTenant },
@@ -183,14 +201,12 @@ const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
setIsLoading(true);
}, []);
const createTenant = () => {
setCurrentPanel(1);
};
return (
<React.Fragment>
{createTenantOpen && (
<AddTenant
open={createTenantOpen}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
)}
<Fragment>
{deleteOpen && (
<DeleteTenant
deleteOpen={deleteOpen}
@@ -208,74 +224,105 @@ const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
entity="Tenant"
/>
)}
<PageHeader label={"Tenants"} />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
component={Link}
to="/add-tenant"
>
Create Tenant
</Button>
</Grid>
<Grid item xs={12}>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Namespace", elementKey: "namespace" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Pools", elementKey: "pool_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tenants"
idField="name"
/>
<div className={classes.settingsOptionsContainer}>
<SlideOptions
slideOptions={[
<Fragment>
<Grid item xs={12} className={classes.customTitle}>
Tenants List
</Grid>
<Grid item xs={12} className={classes.tenantsContainer}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={createTenant}
>
Create Tenant
</Button>
</Grid>
<Grid item xs={12} className={classes.tenantsContainer}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Namespace", elementKey: "namespace" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Pools", elementKey: "pool_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tenants"
idField="name"
customPaperHeight={classes.customConfigurationPage}
noBackground
/>
</Grid>
</Grid>
</Fragment>,
<Fragment>
<Grid item xs={12} className={classes.backContainer}>
<button
onClick={backClick}
className={classes.backButton}
>
<BackSettingsIcon />
Back To Tenants List
</button>
</Grid>
<Grid item xs={12}>
{currentPanel === 1 && (
<AddTenant closeAndRefresh={closeAddModalAndRefresh} />
)}
</Grid>
</Fragment>,
]}
currentSlide={currentPanel}
/>
</div>
</Grid>
</Grid>
</Grid>
</React.Fragment>
</Fragment>
);
};
const connector = connect(null, {
setErrorSnackMessage,
resetAddTenantForm,
});
export default withStyles(styles)(connector(ListTenants));

View File

@@ -0,0 +1,59 @@
import React, { Fragment, useState } from "react";
import PageHeader from "../Common/PageHeader/PageHeader";
import { Grid } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { containerForHeader } from "../Common/FormComponents/common/styleLibrary";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
import ListTenants from "./ListTenants/ListTenants";
interface IConfigurationMain {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
headerLabel: {
fontSize: 22,
fontWeight: 600,
color: "#000",
marginTop: 4,
},
...containerForHeader(theme.spacing(4)),
});
const TenantsMain = ({ classes }: IConfigurationMain) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
return (
<Fragment>
<PageHeader label="Tenants" />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.headerLabel}>
Tenants Management
</Grid>
<Tabs
value={selectedTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="tenant-tabs"
>
<Tab label="Tenants" />
</Tabs>
<Grid item xs={12}>
{selectedTab === 0 && (
<Grid item xs={12}>
<ListTenants />
</Grid>
)}
</Grid>
</Grid>
</Grid>
</Fragment>
);
};
export default withStyles(styles)(TenantsMain);

View File

@@ -0,0 +1,222 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 { Opts } from "./ListTenants/utils";
import {
ADD_TENANT_SET_ADVANCED_MODE,
ADD_TENANT_SET_CURRENT_PAGE,
ADD_TENANT_UPDATE_FIELD,
ADD_TENANT_SET_PAGE_VALID,
ADD_TENANT_SET_STORAGE_CLASSES_LIST,
ADD_TENANT_SET_LIMIT_SIZE,
ADD_TENANT_ADD_CA_KEYPAIR,
ADD_TENANT_DELETE_CA_KEYPAIR,
ADD_TENANT_ADD_FILE_TO_CA_KEYPAIR,
ADD_TENANT_ADD_MINIO_KEYPAIR,
ADD_TENANT_DELETE_MINIO_KEYPAIR,
ADD_TENANT_ADD_FILE_TO_MINIO_KEYPAIR,
ADD_TENANT_ADD_CONSOLE_CERT,
ADD_TENANT_ENCRYPTION_SERVER_CERT,
ADD_TENANT_ENCRYPTION_CLIENT_CERT,
ADD_TENANT_ENCRYPTION_VAULT_CERT,
ADD_TENANT_ENCRYPTION_VAULT_CA,
ADD_TENANT_ENCRYPTION_GEMALTO_CA,
ADD_TENANT_RESET_FORM,
} from "./types";
// Basic actions
export const setWizardPage = (page: number) => {
return {
type: ADD_TENANT_SET_CURRENT_PAGE,
page,
};
};
export const setAdvancedMode = (state: boolean) => {
return {
type: ADD_TENANT_SET_ADVANCED_MODE,
state,
};
};
export const updateAddField = (
pageName: string,
fieldName: string,
value: any
) => {
return {
type: ADD_TENANT_UPDATE_FIELD,
pageName,
field: fieldName,
value,
};
};
export const isPageValid = (pageName: string, valid: boolean) => {
return {
type: ADD_TENANT_SET_PAGE_VALID,
pageName,
valid,
};
};
// Name Tenant actions
export const setStorageClassesList = (storageClasses: Opts[]) => {
return {
type: ADD_TENANT_SET_STORAGE_CLASSES_LIST,
storageClasses,
};
};
export const setLimitSize = (limitSize: any) => {
return {
type: ADD_TENANT_SET_LIMIT_SIZE,
limitSize,
};
};
// Security actions
export const addCaCertificate = () => {
return {
type: ADD_TENANT_ADD_CA_KEYPAIR,
};
};
export const deleteCaCertificate = (id: string) => {
return {
type: ADD_TENANT_DELETE_CA_KEYPAIR,
id,
};
};
export const addFileToCaCertificates = (
id: string,
key: string,
fileName: string,
value: string
) => {
return {
type: ADD_TENANT_ADD_FILE_TO_CA_KEYPAIR,
id,
key,
fileName,
value,
};
};
export const addKeyPair = () => {
return {
type: ADD_TENANT_ADD_MINIO_KEYPAIR,
};
};
export const deleteKeyPair = (id: string) => {
return {
type: ADD_TENANT_DELETE_MINIO_KEYPAIR,
id,
};
};
export const addFileToKeyPair = (
id: string,
key: string,
fileName: string,
value: string
) => {
return {
type: ADD_TENANT_ADD_FILE_TO_MINIO_KEYPAIR,
id,
key,
fileName,
value,
};
};
export const addConsoleCertificate = (
key: string,
fileName: string,
value: string
) => {
return {
type: ADD_TENANT_ADD_CONSOLE_CERT,
key,
fileName,
value,
};
};
export const addFileServerCert = (
key: string,
fileName: string,
value: string
) => {
return {
type: ADD_TENANT_ENCRYPTION_SERVER_CERT,
key,
fileName,
value,
};
};
export const addFileClientCert = (
key: string,
fileName: string,
value: string
) => {
return {
type: ADD_TENANT_ENCRYPTION_CLIENT_CERT,
key,
fileName,
value,
};
};
export const addFileVaultCert = (
key: string,
fileName: string,
value: string
) => {
return {
type: ADD_TENANT_ENCRYPTION_VAULT_CERT,
key,
fileName,
value,
};
};
export const addFileVaultCa = (fileName: string, value: string) => {
return {
type: ADD_TENANT_ENCRYPTION_VAULT_CA,
fileName,
value,
};
};
export const addFileGemaltoCa = (fileName: string, value: string) => {
return {
type: ADD_TENANT_ENCRYPTION_GEMALTO_CA,
fileName,
value,
};
};
export const resetAddTenantForm = () => {
return {
type: ADD_TENANT_RESET_FORM,
};
};

View File

@@ -0,0 +1,592 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 has from "lodash/has";
import get from "lodash/get";
import {
TenantsManagementTypes,
ITenantState,
ADD_TENANT_SET_CURRENT_PAGE,
ADD_TENANT_SET_ADVANCED_MODE,
ADD_TENANT_UPDATE_FIELD,
ADD_TENANT_SET_PAGE_VALID,
ADD_TENANT_SET_STORAGE_CLASSES_LIST,
ADD_TENANT_ADD_MINIO_KEYPAIR,
ADD_TENANT_DELETE_MINIO_KEYPAIR,
ADD_TENANT_ADD_CA_KEYPAIR,
ADD_TENANT_ADD_FILE_TO_CA_KEYPAIR,
ADD_TENANT_DELETE_CA_KEYPAIR,
ADD_TENANT_ADD_CONSOLE_CERT,
ADD_TENANT_ADD_FILE_TO_MINIO_KEYPAIR,
ADD_TENANT_ENCRYPTION_SERVER_CERT,
ADD_TENANT_ENCRYPTION_CLIENT_CERT,
ADD_TENANT_ENCRYPTION_VAULT_CERT,
ADD_TENANT_ENCRYPTION_VAULT_CA,
ADD_TENANT_ENCRYPTION_GEMALTO_CA,
ADD_TENANT_RESET_FORM,
} from "./types";
import { KeyPair } from "./ListTenants/utils";
const initialState: ITenantState = {
createTenant: {
page: 0,
validPages: [],
advancedModeOn: false,
storageClasses: [],
limitSize: {},
fields: {
nameTenant: {
tenantName: "",
namespace: "",
selectedStorageClass: "",
},
configure: {
customImage: false,
imageName: "",
consoleImage: "",
customDockerhub: false,
imageRegistry: "",
imageRegistryUsername: "",
imageRegistryPassword: "",
exposeMinIO: true,
exposeConsole: true,
},
identityProvider: {
idpSelection: "Built-in",
openIDURL: "",
openIDClientID: "",
openIDSecretID: "",
ADURL: "",
ADSkipTLS: false,
ADServerInsecure: false,
ADUserNameFilter: "",
ADGroupBaseDN: "",
ADGroupSearchFilter: "",
ADNameAttribute: "",
},
security: {
enableAutoCert: true,
enableCustomCerts: false,
enableTLS: true,
},
encryption: {
enableEncryption: false,
encryptionType: "vault",
gemaltoEndpoint: "",
gemaltoToken: "",
gemaltoDomain: "",
gemaltoRetry: "0",
awsEndpoint: "",
awsRegion: "",
awsKMSKey: "",
awsAccessKey: "",
awsSecretKey: "",
awsToken: "",
vaultEndpoint: "",
vaultEngine: "",
vaultNamespace: "",
vaultPrefix: "",
vaultAppRoleEngine: "",
vaultId: "",
vaultSecret: "",
vaultRetry: "0",
vaultPing: "0",
gcpProjectID: "",
gcpEndpoint: "",
gcpClientEmail: "",
gcpClientID: "",
gcpPrivateKeyID: "",
gcpPrivateKey: "",
enableCustomCertsForKES: false,
},
tenantSize: {
volumeSize: "100",
sizeFactor: "Gi",
drivesPerServer: "1",
nodes: "4",
memoryNode: "2",
ecParity: "",
ecParityChoices: [],
cleanECChoices: [],
maxAllocableMemo: 0,
memorySize: {
error: "",
limit: 0,
request: 0,
},
distribution: {
error: "",
nodes: 0,
persistentVolumes: 0,
disks: 0,
volumePerDisk: 0,
},
ecParityCalc: {
error: 0,
defaultEC: "",
erasureCodeSet: 0,
maxEC: "",
rawCapacity: "0",
storageFactors: [],
},
limitSize: {},
},
},
certificates: {
minioCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
caCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
consoleCertificate: {
id: "console_cert_pair",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
serverCertificate: {
id: "encryptionServerCertificate",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
clientCertificate: {
id: "encryptionClientCertificate",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
vaultCertificate: {
id: "encryptionVaultCertificate",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
vaultCA: {
id: "encryptionVaultCA",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
gemaltoCA: {
id: "encryptionGemaltoCA",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
},
},
};
export function tenantsReducer(
state = initialState,
action: TenantsManagementTypes
): ITenantState {
let newState: ITenantState = { ...state };
switch (action.type) {
case ADD_TENANT_SET_CURRENT_PAGE:
newState.createTenant.page = action.page;
return { ...newState };
case ADD_TENANT_SET_ADVANCED_MODE:
newState.createTenant.advancedModeOn = action.state;
return { ...newState };
case ADD_TENANT_UPDATE_FIELD:
if (
has(newState.createTenant.fields, `${action.pageName}.${action.field}`)
) {
const originPageNameItems = get(
newState.createTenant.fields,
`${action.pageName}`,
{}
);
let newValue: typeof originPageNameItems = {};
newValue[action.field] = action.value;
const joinValue = { ...originPageNameItems, ...newValue };
newState.createTenant.fields[action.pageName] = { ...joinValue };
return { ...newState };
}
return state;
case ADD_TENANT_SET_PAGE_VALID:
let originValidPages = state.createTenant.validPages;
if (action.valid) {
if (!originValidPages.includes(action.pageName)) {
originValidPages.push(action.pageName);
newState.createTenant.validPages = [...originValidPages];
}
} else {
const newSetOfPages = originValidPages.filter(
(elm) => elm !== action.pageName
);
newState.createTenant.validPages = [...newSetOfPages];
}
return { ...newState };
case ADD_TENANT_SET_STORAGE_CLASSES_LIST:
const changeCL = {
...state,
createTenant: {
...state.createTenant,
storageClasses: action.storageClasses,
},
};
return { ...changeCL };
case ADD_TENANT_ADD_MINIO_KEYPAIR:
const minioCerts = [
...state.createTenant.certificates.minioCertificates,
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
];
newState.createTenant.certificates.minioCertificates = [...minioCerts];
return { ...newState };
case ADD_TENANT_ADD_FILE_TO_MINIO_KEYPAIR:
const minioCertificates =
state.createTenant.certificates.minioCertificates;
const NCertList = minioCertificates.map((item: KeyPair) => {
if (item.id === action.id) {
return {
...item,
[action.key]: action.fileName,
[`encoded_${action.key}`]: action.value,
};
}
return item;
});
newState.createTenant.certificates.minioCertificates = [...NCertList];
return { ...newState };
case ADD_TENANT_DELETE_MINIO_KEYPAIR:
const minioCertsList = state.createTenant.certificates.minioCertificates;
if (minioCertsList.length > 1) {
const cleanMinioCertsList = minioCertsList.filter(
(item: KeyPair) => item.id !== action.id
);
newState.createTenant.certificates.minioCertificates = [
...cleanMinioCertsList,
];
return { ...newState };
}
return { ...state };
case ADD_TENANT_ADD_CA_KEYPAIR:
const CACerts = [
...state.createTenant.certificates.caCertificates,
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
];
newState.createTenant.certificates.caCertificates = [...CACerts];
return { ...newState };
case ADD_TENANT_ADD_FILE_TO_CA_KEYPAIR:
const caCertificates = state.createTenant.certificates.caCertificates;
const NACList = caCertificates.map((item: KeyPair) => {
if (item.id === action.id) {
return {
...item,
[action.key]: action.fileName,
[`encoded_${action.key}`]: action.value,
};
}
return item;
});
newState.createTenant.certificates.caCertificates = [...NACList];
return { ...newState };
case ADD_TENANT_DELETE_CA_KEYPAIR:
const CACertsList = state.createTenant.certificates.minioCertificates;
if (CACertsList.length > 1) {
const cleanMinioCertsList = CACertsList.filter(
(item: KeyPair) => item.id !== action.id
);
newState.createTenant.certificates.caCertificates = [
...cleanMinioCertsList,
];
return { ...newState };
}
return { ...state };
case ADD_TENANT_ADD_CONSOLE_CERT:
const consoleCert = state.createTenant.certificates.consoleCertificate;
newState.createTenant.certificates.consoleCertificate = {
...consoleCert,
[action.key]: action.fileName,
[`encoded_${action.key}`]: action.value,
};
return { ...newState };
case ADD_TENANT_ENCRYPTION_SERVER_CERT:
const encServerCert = state.createTenant.certificates.serverCertificate;
newState.createTenant.certificates.serverCertificate = {
...encServerCert,
[action.key]: action.fileName,
[`encoded_${action.key}`]: action.value,
};
return { ...newState };
case ADD_TENANT_ENCRYPTION_CLIENT_CERT:
const encClientCert = state.createTenant.certificates.clientCertificate;
newState.createTenant.certificates.clientCertificate = {
...encClientCert,
[action.key]: action.fileName,
[`encoded_${action.key}`]: action.value,
};
return { ...newState };
case ADD_TENANT_ENCRYPTION_VAULT_CERT:
const encVaultCert = state.createTenant.certificates.vaultCertificate;
newState.createTenant.certificates.vaultCertificate = {
...encVaultCert,
[action.key]: action.fileName,
[`encoded_${action.key}`]: action.value,
};
return { ...newState };
case ADD_TENANT_ENCRYPTION_VAULT_CA:
const encVaultCA = state.createTenant.certificates.vaultCA;
newState.createTenant.certificates.vaultCA = {
...encVaultCA,
cert: action.fileName,
encoded_cert: action.value,
};
return { ...newState };
case ADD_TENANT_ENCRYPTION_GEMALTO_CA:
const encGemaltoCA = state.createTenant.certificates.gemaltoCA;
newState.createTenant.certificates.gemaltoCA = {
...encGemaltoCA,
cert: action.fileName,
encoded_cert: action.value,
};
return { ...newState };
case ADD_TENANT_RESET_FORM:
return {
...state,
createTenant: {
page: 0,
validPages: [],
advancedModeOn: false,
storageClasses: [],
limitSize: {},
fields: {
nameTenant: {
tenantName: "",
namespace: "",
selectedStorageClass: "",
},
configure: {
customImage: false,
imageName: "",
consoleImage: "",
customDockerhub: false,
imageRegistry: "",
imageRegistryUsername: "",
imageRegistryPassword: "",
exposeMinIO: true,
exposeConsole: true,
},
identityProvider: {
idpSelection: "Built-in",
openIDURL: "",
openIDClientID: "",
openIDSecretID: "",
ADURL: "",
ADSkipTLS: false,
ADServerInsecure: false,
ADUserNameFilter: "",
ADGroupBaseDN: "",
ADGroupSearchFilter: "",
ADNameAttribute: "",
},
security: {
enableAutoCert: true,
enableCustomCerts: false,
enableTLS: true,
},
encryption: {
enableEncryption: false,
encryptionType: "vault",
gemaltoEndpoint: "",
gemaltoToken: "",
gemaltoDomain: "",
gemaltoRetry: "0",
awsEndpoint: "",
awsRegion: "",
awsKMSKey: "",
awsAccessKey: "",
awsSecretKey: "",
awsToken: "",
vaultEndpoint: "",
vaultEngine: "",
vaultNamespace: "",
vaultPrefix: "",
vaultAppRoleEngine: "",
vaultId: "",
vaultSecret: "",
vaultRetry: "0",
vaultPing: "0",
gcpProjectID: "",
gcpEndpoint: "",
gcpClientEmail: "",
gcpClientID: "",
gcpPrivateKeyID: "",
gcpPrivateKey: "",
enableCustomCertsForKES: false,
},
tenantSize: {
volumeSize: "100",
sizeFactor: "Gi",
drivesPerServer: "1",
nodes: "4",
memoryNode: "2",
ecParity: "",
ecParityChoices: [],
cleanECChoices: [],
maxAllocableMemo: 0,
memorySize: {
error: "",
limit: 0,
request: 0,
},
distribution: {
error: "",
nodes: 0,
persistentVolumes: 0,
disks: 0,
volumePerDisk: 0,
},
ecParityCalc: {
error: 0,
defaultEC: "",
erasureCodeSet: 0,
maxEC: "",
rawCapacity: "0",
storageFactors: [],
},
limitSize: {},
},
},
certificates: {
minioCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
caCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
consoleCertificate: {
id: "console_cert_pair",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
serverCertificate: {
id: "encryptionServerCertificate",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
clientCertificate: {
id: "encryptionClientCertificate",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
vaultCertificate: {
id: "encryptionVaultCertificate",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
vaultCA: {
id: "encryptionVaultCA",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
gemaltoCA: {
id: "encryptionGemaltoCA",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
},
},
};
default:
return state;
}
}

View File

@@ -0,0 +1,306 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 { IErasureCodeCalc } from "../../../common/types";
import { IMemorySize } from "./ListTenants/types";
import { KeyPair, Opts } from "./ListTenants/utils";
export const ADD_TENANT_SET_CURRENT_PAGE = "ADD_TENANT/SET_CURRENT_PAGE";
export const ADD_TENANT_SET_ADVANCED_MODE = "ADD_TENANT/SET_ADVANCED_MODE";
export const ADD_TENANT_UPDATE_FIELD = "ADD_TENANT/UPDATE_FIELD";
export const ADD_TENANT_SET_PAGE_VALID = "ADD_TENANT/SET_PAGE_VALID";
export const ADD_TENANT_RESET_FORM = "ADD_TENANT/RESET_FORM";
// Name Tenant
export const ADD_TENANT_SET_STORAGE_CLASSES_LIST =
"ADD_TENANT/SET_STORAGE_CLASSES_LIST";
export const ADD_TENANT_SET_LIMIT_SIZE = "ADD_TENANT/SET_LIMIT_SIZE";
// Security
export const ADD_TENANT_ADD_MINIO_KEYPAIR = "ADD_TENANT/ADD_MINIO_KEYPAIR";
export const ADD_TENANT_ADD_FILE_TO_MINIO_KEYPAIR =
"ADD_TENANT/ADD_FILE_MINIO_KEYPAIR";
export const ADD_TENANT_DELETE_MINIO_KEYPAIR =
"ADD_TENANT/DELETE_MINIO_KEYPAIR";
export const ADD_TENANT_ADD_CA_KEYPAIR = "ADD_TENANT/ADD_CA_KEYPAIR";
export const ADD_TENANT_ADD_FILE_TO_CA_KEYPAIR =
"ADD_TENANT/ADD_FILE_TO_CA_KEYPAIR";
export const ADD_TENANT_DELETE_CA_KEYPAIR = "ADD_TENANT/DELETE_CA_KEYPAIR";
export const ADD_TENANT_ADD_CONSOLE_CERT = "ADD_TENANT/ADD_CONSOLE_CERT";
// Encryption
export const ADD_TENANT_ENCRYPTION_SERVER_CERT =
"ADD_TENANT/ENCRYPTION_SERVER_CERT";
export const ADD_TENANT_ENCRYPTION_CLIENT_CERT =
"ADD_TENANT/ENCRYPTION_CLIENT_CERT";
export const ADD_TENANT_ENCRYPTION_VAULT_CERT =
"ADD_TENANT/ENCRYPTION_VAULT_CERT";
export const ADD_TENANT_ENCRYPTION_VAULT_CA = "ADD_TENANT/ENCRYPTION_VAULT_CA";
export const ADD_TENANT_ENCRYPTION_GEMALTO_CA =
"ADD_TENANT/ENCRYPTION_GEMALTO_CA";
export interface ICreateTenant {
page: number;
validPages: string[];
advancedModeOn: boolean;
storageClasses: Opts[];
limitSize: any;
fields: IFieldStore;
certificates: ICertificatesItems;
}
export interface ICertificatesItems {
minioCertificates: KeyPair[];
caCertificates: KeyPair[];
consoleCertificate: KeyPair;
serverCertificate: KeyPair;
clientCertificate: KeyPair;
vaultCertificate: KeyPair;
vaultCA: KeyPair;
gemaltoCA: KeyPair;
}
export interface IFieldStore {
nameTenant: INameTenantFields;
configure: IConfigureFields;
identityProvider: IIdentityProviderFields;
security: ISecurityFields;
encryption: IEncryptionFields;
tenantSize: ITenantSizeFields;
}
export interface INameTenantFields {
tenantName: string;
namespace: string;
selectedStorageClass: string;
}
export interface IConfigureFields {
customImage: boolean;
imageName: string;
consoleImage: string;
customDockerhub: boolean;
imageRegistry: string;
imageRegistryUsername: string;
imageRegistryPassword: string;
exposeMinIO: boolean;
exposeConsole: boolean;
}
export interface IIdentityProviderFields {
idpSelection: string;
openIDURL: string;
openIDClientID: string;
openIDSecretID: string;
ADURL: string;
ADSkipTLS: boolean;
ADServerInsecure: boolean;
ADUserNameFilter: string;
ADGroupBaseDN: string;
ADGroupSearchFilter: string;
ADNameAttribute: string;
}
export interface ISecurityFields {
enableTLS: boolean;
enableAutoCert: boolean;
enableCustomCerts: boolean;
}
export interface IEncryptionFields {
enableEncryption: boolean;
encryptionType: string;
gemaltoEndpoint: string;
gemaltoToken: string;
gemaltoDomain: string;
gemaltoRetry: string;
awsEndpoint: string;
awsRegion: string;
awsKMSKey: string;
awsAccessKey: string;
awsSecretKey: string;
awsToken: string;
vaultEndpoint: string;
vaultEngine: string;
vaultNamespace: string;
vaultPrefix: string;
vaultAppRoleEngine: string;
vaultId: string;
vaultSecret: string;
vaultRetry: string;
vaultPing: string;
gcpProjectID: string;
gcpEndpoint: string;
gcpClientEmail: string;
gcpClientID: string;
gcpPrivateKeyID: string;
gcpPrivateKey: string;
enableCustomCertsForKES: boolean;
}
export interface ITenantSizeFields {
volumeSize: string;
sizeFactor: string;
drivesPerServer: string;
nodes: string;
memoryNode: string;
ecParity: string;
ecParityChoices: Opts[];
cleanECChoices: string[];
maxAllocableMemo: number;
memorySize: IMemorySize;
distribution: any;
ecParityCalc: IErasureCodeCalc;
limitSize: any;
}
export interface ITenantState {
createTenant: ICreateTenant;
}
interface SetTenantWizardPage {
type: typeof ADD_TENANT_SET_CURRENT_PAGE;
page: number;
}
interface SetAdvancedMode {
type: typeof ADD_TENANT_SET_ADVANCED_MODE;
state: boolean;
}
interface UpdateATField {
type: typeof ADD_TENANT_UPDATE_FIELD;
pageName: keyof IFieldStore;
field: keyof FieldsToHandle;
value: any;
}
interface SetPageValid {
type: typeof ADD_TENANT_SET_PAGE_VALID;
pageName: keyof IFieldStore;
valid: boolean;
}
interface SetStorageClassesList {
type: typeof ADD_TENANT_SET_STORAGE_CLASSES_LIST;
storageClasses: Opts[];
}
interface SetLimitSize {
type: typeof ADD_TENANT_SET_LIMIT_SIZE;
limitSize: any;
}
interface AddMinioKeyPair {
type: typeof ADD_TENANT_ADD_MINIO_KEYPAIR;
}
interface AddFileToMinioKeyPair {
type: typeof ADD_TENANT_ADD_FILE_TO_MINIO_KEYPAIR;
id: string;
key: string;
fileName: string;
value: string;
}
interface DeleteMinioKeyPair {
type: typeof ADD_TENANT_DELETE_MINIO_KEYPAIR;
id: string;
}
interface AddCAKeyPair {
type: typeof ADD_TENANT_ADD_CA_KEYPAIR;
}
interface AddFileToCAKeyPair {
type: typeof ADD_TENANT_ADD_FILE_TO_CA_KEYPAIR;
id: string;
key: string;
fileName: string;
value: string;
}
interface DeleteCAKeyPair {
type: typeof ADD_TENANT_DELETE_CA_KEYPAIR;
id: string;
}
interface AddFileConsoleCert {
type: typeof ADD_TENANT_ADD_CONSOLE_CERT;
key: string;
fileName: string;
value: string;
}
// Encryption Certs
interface AddFileServerCert {
type: typeof ADD_TENANT_ENCRYPTION_SERVER_CERT;
key: string;
fileName: string;
value: string;
}
interface AddFileClientCert {
type: typeof ADD_TENANT_ENCRYPTION_CLIENT_CERT;
key: string;
fileName: string;
value: string;
}
interface AddFileVaultCert {
type: typeof ADD_TENANT_ENCRYPTION_VAULT_CERT;
key: string;
fileName: string;
value: string;
}
interface AddFileVaultCa {
type: typeof ADD_TENANT_ENCRYPTION_VAULT_CA;
fileName: string;
value: string;
}
interface AddFileGemaltoCa {
type: typeof ADD_TENANT_ENCRYPTION_GEMALTO_CA;
fileName: string;
value: string;
}
interface ResetForm {
type: typeof ADD_TENANT_RESET_FORM;
}
export type FieldsToHandle = INameTenantFields;
export type TenantsManagementTypes =
| SetTenantWizardPage
| SetAdvancedMode
| UpdateATField
| SetPageValid
| SetStorageClassesList
| SetLimitSize
| AddMinioKeyPair
| DeleteMinioKeyPair
| AddCAKeyPair
| DeleteCAKeyPair
| AddFileConsoleCert
| AddFileToMinioKeyPair
| AddFileToCAKeyPair
| AddFileServerCert
| AddFileClientCert
| AddFileVaultCert
| AddFileVaultCa
| AddFileGemaltoCa
| ResetForm;

View File

@@ -0,0 +1,25 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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/>.
export const clearValidationError = (
validationErrors: any,
fieldKey: string
) => {
const newValidationElement = { ...validationErrors };
delete newValidationElement[fieldKey];
return newValidationElement;
};

View File

@@ -24,6 +24,7 @@ import { watchReducer } from "./screens/Console/Watch/reducers";
import { consoleReducer } from "./screens/Console/reducer";
import { bucketsReducer } from "./screens/Console/Buckets/reducers";
import { objectBrowserReducer } from "./screens/Console/ObjectBrowser/reducers";
import { tenantsReducer } from "./screens/Console/Tenants/reducer";
const globalReducer = combineReducers({
system: systemReducer,
@@ -34,6 +35,7 @@ const globalReducer = combineReducers({
buckets: bucketsReducer,
objectBrowser: objectBrowserReducer,
healthInfo: healthInfoReducer,
tenants: tenantsReducer,
});
declare global {