Move Create Tenant to Thunk (#2043)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2022-05-26 16:47:39 -07:00
committed by GitHub
parent 04adf25e65
commit 9c5d4aaf11
16 changed files with 1271 additions and 1125 deletions

View File

@@ -103,6 +103,9 @@ const WizardPage = ({
>
<div className={classes.buttonInnerContainer}>
{page.buttons.map((btn) => {
if (btn.componentRender) {
return btn.componentRender;
}
return (
<Button
id={"wizard-button-" + btn.label}

View File

@@ -15,11 +15,12 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export interface IWizardButton {
label: string;
type: string;
label?: string;
type?: string;
action?: (nextFunction: (to: string | number) => void) => void;
enabled?: boolean;
toPage?: number;
componentRender?: React.ReactNode;
}
export interface IWizardElement {

View File

@@ -27,15 +27,11 @@ import {
settingsCommon,
wizardCommon,
} from "../../Common/FormComponents/common/styleLibrary";
import api from "../../../../common/api";
import { generatePoolName, getBytes } from "../../../../common/utils";
import GenericWizard from "../../Common/GenericWizard/GenericWizard";
import { IWizardElement } from "../../Common/GenericWizard/types";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import { ErrorResponseHandler, ITenantCreator } from "../../../../common/types";
import { KeyPair } from "../ListTenants/utils";
import { getDefaultAffinity, getNodeSelector } from "../TenantDetails/utils";
import {
IWizardButton,
IWizardElement,
} from "../../Common/GenericWizard/types";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
import { AppState } from "../../../../store";
import Configure from "./Steps/Configure";
@@ -57,10 +53,10 @@ import {
} from "./Steps/TenantResources/utils";
import HelpBox from "../../../../common/HelpBox";
import { StorageIcon } from "../../../../icons";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { selFeatures } from "../../consoleSlice";
import makeStyles from "@mui/styles/makeStyles";
import { resetAddTenantForm } from "./createTenantSlice";
import CreateTenantButton from "./CreateTenantButton";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -77,32 +73,20 @@ const AddTenant = () => {
const dispatch = useDispatch();
const classes = useStyles();
const namespace = useSelector(
(state: AppState) => state.createTenant.fields.nameTenant.namespace
);
const validPages = useSelector(
(state: AppState) => state.createTenant.validPages
);
const fields = useSelector((state: AppState) => state.createTenant.fields);
const certificates = useSelector(
(state: AppState) => state.createTenant.certificates
);
const selectedStorageClass = useSelector(
(state: AppState) =>
state.createTenant.fields.nameTenant.selectedStorageClass
);
const features = useSelector(selFeatures);
const tolerations = useSelector(
(state: AppState) => state.createTenant.tolerations
);
// Modals
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
const [createdAccount, setCreatedAccount] =
useState<NewServiceAccount | null>(null);
const showNewCredentials = useSelector(
(state: AppState) => state.createTenant.showNewCredentials
);
const createdAccount = useSelector(
(state: AppState) => state.createTenant.createdAccount
);
// Fields
const [addSending, setAddSending] = useState<boolean>(false);
const addSending = useSelector(
(state: AppState) => state.createTenant.addingTenant
);
const [formRender, setFormRender] = useState<IMkEnvs | null>(null);
useEffect(() => {
@@ -125,596 +109,6 @@ const AddTenant = () => {
setFormRender(setConfiguration);
}, [features]);
/* Send Information to backend */
useEffect(() => {
const tenantName = fields.nameTenant.tenantName;
const selectedStorageClass = fields.nameTenant.selectedStorageClass;
const imageName = fields.configure.imageName;
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 openIDConfigurationURL =
fields.identityProvider.openIDConfigurationURL;
const openIDClientID = fields.identityProvider.openIDClientID;
const openIDClaimName = fields.identityProvider.openIDClaimName;
const openIDCallbackURL = fields.identityProvider.openIDCallbackURL;
const openIDScopes = fields.identityProvider.openIDScopes;
const openIDSecretID = fields.identityProvider.openIDSecretID;
const ADURL = fields.identityProvider.ADURL;
const ADSkipTLS = fields.identityProvider.ADSkipTLS;
const ADServerInsecure = fields.identityProvider.ADServerInsecure;
const ADGroupSearchBaseDN = fields.identityProvider.ADGroupSearchBaseDN;
const ADGroupSearchFilter = fields.identityProvider.ADGroupSearchFilter;
const ADUserDNs = fields.identityProvider.ADUserDNs;
const ADLookupBindDN = fields.identityProvider.ADLookupBindDN;
const ADLookupBindPassword = fields.identityProvider.ADLookupBindPassword;
const ADUserDNSearchBaseDN = fields.identityProvider.ADUserDNSearchBaseDN;
const ADUserDNSearchFilter = fields.identityProvider.ADUserDNSearchFilter;
const ADServerStartTLS = fields.identityProvider.ADServerStartTLS;
const accessKeys = fields.identityProvider.accessKeys;
const secretKeys = fields.identityProvider.secretKeys;
const minioCertificates = certificates.minioCertificates;
const caCertificates = certificates.caCertificates;
const consoleCaCertificates = certificates.consoleCaCertificates;
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 azureEndpoint = fields.encryption.azureEndpoint;
const azureTenantID = fields.encryption.azureTenantID;
const azureClientID = fields.encryption.azureClientID;
const azureClientSecret = fields.encryption.azureClientSecret;
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 enableTLS = fields.security.enableTLS;
const ecParity = fields.tenantSize.ecParity;
const distribution = fields.tenantSize.distribution;
const tenantCustom = fields.configure.tenantCustom;
const logSearchEnabled = fields.configure.logSearchEnabled;
const prometheusEnabled = fields.configure.prometheusEnabled;
const logSearchVolumeSize = fields.configure.logSearchVolumeSize;
const logSearchSelectedStorageClass =
fields.configure.logSearchSelectedStorageClass;
const logSearchImage = fields.configure.logSearchImage;
const kesImage = fields.configure.kesImage;
const logSearchPostgresImage = fields.configure.logSearchPostgresImage;
const logSearchPostgresInitImage =
fields.configure.logSearchPostgresInitImage;
const prometheusImage = fields.configure.prometheusImage;
const prometheusSidecarImage = fields.configure.prometheusSidecarImage;
const prometheusInitImage = fields.configure.prometheusInitImage;
const prometheusSelectedStorageClass =
fields.configure.prometheusSelectedStorageClass;
const prometheusVolumeSize = fields.configure.prometheusVolumeSize;
const affinityType = fields.affinity.podAffinity;
const nodeSelectorLabels = fields.affinity.nodeSelectorLabels;
const withPodAntiAffinity = fields.affinity.withPodAntiAffinity;
const tenantSecurityContext = fields.configure.tenantSecurityContext;
const logSearchSecurityContext = fields.configure.logSearchSecurityContext;
const logSearchPostgresSecurityContext =
fields.configure.logSearchPostgresSecurityContext;
const prometheusSecurityContext =
fields.configure.prometheusSecurityContext;
const kesSecurityContext = fields.encryption.kesSecurityContext;
const kesReplicas = fields.encryption.replicas;
const setDomains = fields.configure.setDomains;
const minioDomains = fields.configure.minioDomains;
const consoleDomain = fields.configure.consoleDomain;
if (addSending) {
const tolerationValues = tolerations.filter(
(toleration) => toleration.key.trim() !== ""
);
const poolName = generatePoolName([]);
let affinityObject = {};
switch (affinityType) {
case "default":
affinityObject = {
affinity: getDefaultAffinity(tenantName, poolName),
};
break;
case "nodeSelector":
affinityObject = {
affinity: getNodeSelector(
nodeSelectorLabels,
withPodAntiAffinity,
tenantName,
poolName
),
};
break;
}
const erasureCode = ecParity.split(":")[1];
let dataSend: ITenantCreator = {
name: tenantName,
namespace: namespace,
access_key: "",
secret_key: "",
access_keys: [],
secret_keys: [],
enable_tls: enableTLS && enableAutoCert,
enable_console: true,
enable_prometheus: true,
service_name: "",
image: imageName,
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,
},
securityContext: tenantCustom ? tenantSecurityContext : null,
...affinityObject,
tolerations: tolerationValues,
},
],
erasureCodingParity: parseInt(erasureCode, 10),
};
// Set Resources
if (
fields.tenantSize.resourcesCPURequest !== "" ||
fields.tenantSize.resourcesCPULimit !== "" ||
fields.tenantSize.resourcesMemoryRequest !== "" ||
fields.tenantSize.resourcesMemoryLimit !== ""
) {
dataSend.pools[0].resources = {};
// requests
if (
fields.tenantSize.resourcesCPURequest !== "" ||
fields.tenantSize.resourcesMemoryRequest !== ""
) {
dataSend.pools[0].resources.requests = {};
if (fields.tenantSize.resourcesCPURequest !== "") {
dataSend.pools[0].resources.requests.cpu = parseInt(
fields.tenantSize.resourcesCPURequest
);
}
if (fields.tenantSize.resourcesMemoryRequest !== "") {
dataSend.pools[0].resources.requests.memory = parseInt(
getBytes(fields.tenantSize.resourcesMemoryRequest, "Gi", true)
);
}
}
// limits
if (
fields.tenantSize.resourcesCPULimit !== "" ||
fields.tenantSize.resourcesMemoryLimit !== ""
) {
dataSend.pools[0].resources.limits = {};
if (fields.tenantSize.resourcesCPULimit !== "") {
dataSend.pools[0].resources.limits.cpu = parseInt(
fields.tenantSize.resourcesCPULimit
);
}
if (fields.tenantSize.resourcesMemoryLimit !== "") {
dataSend.pools[0].resources.limits.memory = parseInt(
getBytes(fields.tenantSize.resourcesMemoryLimit, "Gi", true)
);
}
}
}
if (customDockerhub) {
dataSend = {
...dataSend,
image_registry: {
registry: imageRegistry,
username: imageRegistryUsername,
password: imageRegistryPassword,
},
};
}
if (logSearchEnabled) {
dataSend = {
...dataSend,
logSearchConfiguration: {
storageClass:
logSearchSelectedStorageClass === "default"
? ""
: logSearchSelectedStorageClass,
storageSize: parseInt(logSearchVolumeSize),
image: logSearchImage,
postgres_image: logSearchPostgresImage,
postgres_init_image: logSearchPostgresInitImage,
securityContext: logSearchSecurityContext,
postgres_securityContext: logSearchPostgresSecurityContext,
},
};
}
if (prometheusEnabled) {
dataSend = {
...dataSend,
prometheusConfiguration: {
storageClass:
prometheusSelectedStorageClass === "default"
? ""
: prometheusSelectedStorageClass,
storageSize: parseInt(prometheusVolumeSize),
image: prometheusImage,
sidecar_image: prometheusSidecarImage,
init_image: prometheusInitImage,
securityContext: prometheusSecurityContext,
},
};
}
let tenantCerts: any = null;
let consoleCerts: any = null;
let caCerts: any = null;
let consoleCaCerts: any = null;
if (caCertificates.length > 0) {
caCerts = {
ca_certificates: caCertificates
.map((keyPair: KeyPair) => keyPair.encoded_cert)
.filter((keyPair) => keyPair),
};
}
if (consoleCaCertificates.length > 0) {
consoleCaCerts = {
console_ca_certificates: consoleCaCertificates
.map((keyPair: KeyPair) => keyPair.encoded_cert)
.filter((keyPair) => keyPair),
};
}
if (enableTLS && minioCertificates.length > 0) {
tenantCerts = {
minio: minioCertificates
.map((keyPair: KeyPair) => ({
crt: keyPair.encoded_cert,
key: keyPair.encoded_key,
}))
.filter((keyPair) => keyPair.crt && keyPair.key),
};
}
if (
enableTLS &&
consoleCertificate.encoded_cert !== "" &&
consoleCertificate.encoded_key !== ""
) {
consoleCerts = {
console: {
crt: consoleCertificate.encoded_cert,
key: consoleCertificate.encoded_key,
},
};
}
if (tenantCerts || consoleCerts || caCerts || consoleCaCerts) {
dataSend = {
...dataSend,
tls: {
...tenantCerts,
...consoleCerts,
...caCerts,
...consoleCaCerts,
},
};
}
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 "azure":
insertEncrypt = {
azure: {
keyvault: {
endpoint: azureEndpoint,
credentials: {
tenant_id: azureTenantID,
client_id: azureClientID,
client_secret: azureClientSecret,
},
},
},
};
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 || vaultCAInsert) {
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: {
replicas: kesReplicas,
securityContext: kesSecurityContext,
image: kesImage,
...encryptionClientKeyPair,
...encryptionServerKeyPair,
...insertEncrypt,
},
};
}
let dataIDP: any = {};
switch (idpSelection) {
case "Built-in":
let keyarray = [];
for (let i = 0; i < accessKeys.length; i++) {
keyarray.push({
access_key: accessKeys[i],
secret_key: secretKeys[i],
});
}
dataIDP = {
keys: keyarray,
};
break;
case "OpenID":
dataIDP = {
oidc: {
configuration_url: openIDConfigurationURL,
client_id: openIDClientID,
secret_id: openIDSecretID,
claim_name: openIDClaimName,
callback_url: openIDCallbackURL,
scopes: openIDScopes,
},
};
break;
case "AD":
dataIDP = {
active_directory: {
url: ADURL,
skip_tls_verification: ADSkipTLS,
server_insecure: ADServerInsecure,
group_search_base_dn: ADGroupSearchBaseDN,
group_search_filter: ADGroupSearchFilter,
user_dns: ADUserDNs,
lookup_bind_dn: ADLookupBindDN,
lookup_bind_password: ADLookupBindPassword,
user_dn_search_base_dn: ADUserDNSearchBaseDN,
user_dn_search_filter: ADUserDNSearchFilter,
server_start_tls: ADServerStartTLS,
},
};
break;
}
let domains: any = {};
let sendDomain: any = {};
if (setDomains) {
if (consoleDomain !== "") {
domains.console = consoleDomain;
}
const filteredDomains = minioDomains.filter((dom) => dom.trim() !== "");
if (filteredDomains.length > 0) {
domains.minio = filteredDomains;
}
if (Object.keys(domains).length > 0) {
sendDomain.domains = domains;
}
}
dataSend = {
...dataSend,
...sendDomain,
idp: { ...dataIDP },
};
api
.invoke("POST", `/api/v1/tenants`, dataSend)
.then((res) => {
const consoleSAList = get(res, "console", []);
let newSrvAcc: NewServiceAccount = {
idp: get(res, "externalIDP", false),
console: [],
};
if (consoleSAList) {
if (Array.isArray(consoleSAList)) {
const consoleItem = consoleSAList.map((consoleKey) => {
return {
accessKey: consoleKey.access_key,
secretKey: consoleKey.secret_key,
api: "s3v4",
path: "auto",
url: consoleKey.url,
};
});
newSrvAcc.console = consoleItem;
} else {
newSrvAcc = {
console: {
accessKey: res.console.access_key,
secretKey: res.console.secret_key,
url: res.console.url,
},
};
}
}
setAddSending(false);
setShowNewCredentials(true);
setCreatedAccount(newSrvAcc);
})
.catch((err: ErrorResponseHandler) => {
setAddSending(false);
dispatch(setErrorSnackMessage(err));
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [addSending]);
const cancelButton = {
label: "Cancel",
type: "other",
@@ -724,25 +118,9 @@ const AddTenant = () => {
history.push("/tenants");
},
};
const requiredPages = [
"nameTenant",
"tenantSize",
"configure",
"affinity",
"identityProvider",
"security",
"encryption",
];
const createButton = {
label: "Create",
type: "submit",
enabled:
!addSending &&
selectedStorageClass !== "" &&
requiredPages.every((v) => validPages.includes(v)),
action: () => {
setAddSending(true);
},
const createButton: IWizardButton = {
componentRender: <CreateTenantButton />,
};
const wizardSteps: IWizardElement[] = [

View File

@@ -0,0 +1,62 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Button } from "@mui/material";
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "../../../../store";
import { requiredPages } from "./common";
import { createTenantAsync } from "./thunks/createTenantThunk";
const CreateTenantButton = () => {
const dispatch = useDispatch();
const addSending = useSelector(
(state: AppState) => state.createTenant.addingTenant
);
const validPages = useSelector(
(state: AppState) => state.createTenant.validPages
);
const selectedStorageClass = useSelector(
(state: AppState) =>
state.createTenant.fields.nameTenant.selectedStorageClass
);
const enabled =
!addSending &&
selectedStorageClass !== "" &&
requiredPages.every((v) => validPages.includes(v));
return (
<Button
id={"wizard-button-Create"}
variant="contained"
color="primary"
size="small"
onClick={() => {
dispatch(createTenantAsync());
}}
disabled={!enabled}
key={`button-AddTenant-Create`}
>
Create
</Button>
);
};
export default CreateTenantButton;

View File

@@ -48,12 +48,6 @@ const AWSKMSAdd = () => {
const dispatch = useDispatch();
const classes = useStyles();
const enableEncryption = useSelector(
(state: AppState) => state.createTenant.fields.encryption.enableEncryption
);
const encryptionType = useSelector(
(state: AppState) => state.createTenant.fields.encryption.encryptionType
);
const awsEndpoint = useSelector(
(state: AppState) => state.createTenant.fields.encryption.awsEndpoint
);
@@ -78,33 +72,29 @@ const AWSKMSAdd = () => {
useEffect(() => {
let encryptionValidation: IValidation[] = [];
if (enableEncryption) {
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,
},
];
}
}
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,
},
];
const commonVal = commonFormValidation(encryptionValidation);
@@ -116,15 +106,7 @@ const AWSKMSAdd = () => {
);
setValidationErrors(commonVal);
}, [
enableEncryption,
encryptionType,
awsEndpoint,
awsRegion,
awsSecretKey,
awsAccessKey,
dispatch,
]);
}, [awsEndpoint, awsRegion, awsSecretKey, awsAccessKey, dispatch]);
// Common
const updateField = useCallback(

View File

@@ -48,13 +48,6 @@ const AzureKMSAdd = () => {
const dispatch = useDispatch();
const classes = useStyles();
const enableEncryption = useSelector(
(state: AppState) => state.createTenant.fields.encryption.enableEncryption
);
const encryptionType = useSelector(
(state: AppState) => state.createTenant.fields.encryption.encryptionType
);
const azureEndpoint = useSelector(
(state: AppState) => state.createTenant.fields.encryption.azureEndpoint
);
@@ -74,33 +67,29 @@ const AzureKMSAdd = () => {
useEffect(() => {
let encryptionValidation: IValidation[] = [];
if (enableEncryption) {
if (encryptionType === "azure") {
encryptionValidation = [
...encryptionValidation,
{
fieldKey: "azure_endpoint",
required: true,
value: azureEndpoint,
},
{
fieldKey: "azure_tenant_id",
required: true,
value: azureTenantID,
},
{
fieldKey: "azure_client_id",
required: true,
value: azureClientID,
},
{
fieldKey: "azure_client_secret",
required: true,
value: azureClientSecret,
},
];
}
}
encryptionValidation = [
...encryptionValidation,
{
fieldKey: "azure_endpoint",
required: true,
value: azureEndpoint,
},
{
fieldKey: "azure_tenant_id",
required: true,
value: azureTenantID,
},
{
fieldKey: "azure_client_id",
required: true,
value: azureClientID,
},
{
fieldKey: "azure_client_secret",
required: true,
value: azureClientSecret,
},
];
const commonVal = commonFormValidation(encryptionValidation);
@@ -113,8 +102,6 @@ const AzureKMSAdd = () => {
setValidationErrors(commonVal);
}, [
enableEncryption,
encryptionType,
azureEndpoint,
azureTenantID,
azureClientID,

View File

@@ -53,12 +53,6 @@ const GemaltoKMSAdd = () => {
const dispatch = useDispatch();
const classes = useStyles();
const enableEncryption = useSelector(
(state: AppState) => state.createTenant.fields.encryption.enableEncryption
);
const encryptionType = useSelector(
(state: AppState) => state.createTenant.fields.encryption.encryptionType
);
const gemaltoCA = useSelector(
(state: AppState) => state.createTenant.certificates.gemaltoCA
);
@@ -81,35 +75,31 @@ const GemaltoKMSAdd = () => {
useEffect(() => {
let encryptionValidation: IValidation[] = [];
if (enableEncryption) {
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",
},
];
}
}
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);
@@ -121,15 +111,7 @@ const GemaltoKMSAdd = () => {
);
setValidationErrors(commonVal);
}, [
enableEncryption,
encryptionType,
gemaltoEndpoint,
gemaltoToken,
gemaltoDomain,
gemaltoRetry,
dispatch,
]);
}, [gemaltoEndpoint, gemaltoToken, gemaltoDomain, gemaltoRetry, dispatch]);
// Common
const updateField = useCallback(

View File

@@ -55,13 +55,6 @@ const VaultKMSAdd = () => {
const dispatch = useDispatch();
const classes = useStyles();
const enableEncryption = useSelector(
(state: AppState) => state.createTenant.fields.encryption.enableEncryption
);
const encryptionType = useSelector(
(state: AppState) => state.createTenant.fields.encryption.encryptionType
);
const vaultEndpoint = useSelector(
(state: AppState) => state.createTenant.fields.encryption.vaultEndpoint
);
@@ -102,42 +95,38 @@ const VaultKMSAdd = () => {
useEffect(() => {
let encryptionValidation: IValidation[] = [];
if (enableEncryption) {
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",
},
];
}
}
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",
},
];
const commonVal = commonFormValidation(encryptionValidation);
@@ -150,8 +139,6 @@ const VaultKMSAdd = () => {
setValidationErrors(commonVal);
}, [
enableEncryption,
encryptionType,
vaultEndpoint,
vaultEngine,
vaultId,

View File

@@ -58,6 +58,7 @@ import {
setLimitSize,
setStorageClassesList,
setStorageType,
setTenantName,
updateAddField,
} from "../../createTenantSlice";
import { selFeatures } from "../../../../consoleSlice";
@@ -76,6 +77,31 @@ const styles = (theme: Theme) =>
...wizardCommon,
});
const NameTenantField = () => {
const dispatch = useDispatch();
const tenantName = useSelector(
(state: AppState) => state.createTenant.fields.nameTenant.tenantName
);
const tenantNameError = useSelector(
(state: AppState) => state.createTenant.validationErrors["tenant-name"]
);
return (
<InputBoxWrapper
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setTenantName(e.target.value));
}}
label="Name"
value={tenantName}
required
error={tenantNameError || ""}
/>
);
};
interface INameTenantMainScreen {
classes: any;
formToRender?: IMkEnvs;
@@ -84,9 +110,6 @@ interface INameTenantMainScreen {
const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
const dispatch = useDispatch();
const tenantName = useSelector(
(state: AppState) => state.createTenant.fields.nameTenant.tenantName
);
const namespace = useSelector(
(state: AppState) => state.createTenant.fields.nameTenant.namespace
);
@@ -219,14 +242,6 @@ const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
}
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,
@@ -237,7 +252,6 @@ const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
]);
const isValid =
!("tenant-name" in commonValidation) &&
!("namespace" in commonValidation) &&
((formToRender === IMkEnvs.default && storageClasses.length > 0) ||
(formToRender !== IMkEnvs.default && selectedStorageType !== ""));
@@ -248,7 +262,6 @@ const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
}, [
storageClasses,
namespace,
tenantName,
dispatch,
emptyNamespace,
loadingNamespaceInfo,
@@ -293,18 +306,7 @@ const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
</span>
</div>
<div className={classes.formFieldRow}>
<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"] || ""}
/>
<NameTenantField />
</div>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>

View File

@@ -0,0 +1,24 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export const requiredPages = [
"nameTenant",
"tenantSize",
"configure",
"affinity",
"identityProvider",
"security",
"encryption",
];

View File

@@ -0,0 +1,61 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import api from "../../../../common/api";
import get from "lodash/get";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import { ErrorResponseHandler, ITenantCreator } from "../../../../common/types";
export const createTenantCall = (dataSend: ITenantCreator) => {
return new Promise<NewServiceAccount>((resolve, reject) => {
api
.invoke("POST", `/api/v1/tenants`, dataSend)
.then((res) => {
const consoleSAList = get(res, "console", []);
let newSrvAcc: NewServiceAccount = {
idp: get(res, "externalIDP", false),
console: [],
};
if (consoleSAList) {
if (Array.isArray(consoleSAList)) {
newSrvAcc.console = consoleSAList.map((consoleKey) => {
return {
accessKey: consoleKey.access_key,
secretKey: consoleKey.secret_key,
api: "s3v4",
path: "auto",
url: consoleKey.url,
};
});
} else {
newSrvAcc = {
console: {
accessKey: res.console.access_key,
secretKey: res.console.secret_key,
url: res.console.url,
},
};
}
}
resolve(newSrvAcc);
})
.catch((err: ErrorResponseHandler) => {
reject(err);
});
});
};

View File

@@ -33,19 +33,29 @@ import {
} from "./Steps/TenantResources/utils";
import { getBytesNumber } from "../../../../common/utils";
import { CertificateFile, FileValue, KeyFileValue } from "../tenantsSlice";
import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import { createTenantAsync } from "./thunks/createTenantThunk";
import { commonFormValidation } from "../../../../utils/validationFunctions";
import { flipValidPageInState } from "./sliceUtils";
export interface ICreateTenant {
addingTenant: boolean;
page: number;
validPages: string[];
validationErrors: { [key: string]: string };
storageClasses: Opts[];
limitSize: any;
fields: IFieldStore;
certificates: ICertificatesItems;
nodeSelectorPairs: LabelKeyPair[];
tolerations: ITolerationModel[];
// after creation states
createdAccount: NewServiceAccount | null;
showNewCredentials: boolean;
}
const initialState: ICreateTenant = {
addingTenant: false,
page: 0,
// We can assume all the other pages are valid with default configuration except for 'nameTenant'
// because the user still have to choose a namespace and a name for the tenant
@@ -57,6 +67,7 @@ const initialState: ICreateTenant = {
"security",
"encryption",
],
validationErrors: {},
storageClasses: [],
limitSize: {},
fields: {
@@ -340,6 +351,8 @@ const initialState: ICreateTenant = {
operator: ITolerationOperator.Equal,
},
],
createdAccount: null,
showNewCredentials: false,
};
export const createTenantSlice = createSlice({
@@ -382,7 +395,6 @@ export const createTenantSlice = createSlice({
}>
) => {
let originValidPages = state.validPages;
if (action.payload.valid) {
if (!originValidPages.includes(action.payload.pageName)) {
originValidPages.push(action.payload.pageName);
@@ -393,7 +405,6 @@ export const createTenantSlice = createSlice({
const newSetOfPages = originValidPages.filter(
(elm) => elm !== action.payload.pageName
);
state.validPages = [...newSetOfPages];
}
},
@@ -658,302 +669,304 @@ export const createTenantSlice = createSlice({
};
},
resetAddTenantForm: (state) => {
state = {
page: 0,
// We can assume all the other pages are valid with default configuration except for 'nameTenant'
// because the user still have to choose a namespace and a name for the tenant
validPages: [
"tenantSize",
"configure",
"affinity",
"identityProvider",
"security",
"encryption",
],
storageClasses: [],
limitSize: {},
fields: {
nameTenant: {
tenantName: "",
namespace: "",
selectedStorageClass: "",
selectedStorageType: "",
state.addingTenant = false;
state.page = 0;
// We can assume all the other pages are valid with default configuration except for 'nameTenant'
// because the user still have to choose a namespace and a name for the tenant
state.validPages = [
"tenantSize",
"configure",
"affinity",
"identityProvider",
"security",
"encryption",
];
state.validationErrors = {};
state.storageClasses = [];
state.limitSize = {};
state.fields = {
nameTenant: {
tenantName: "",
namespace: "",
selectedStorageClass: "",
selectedStorageType: "",
},
configure: {
customImage: false,
imageName: "",
customDockerhub: false,
imageRegistry: "",
imageRegistryUsername: "",
imageRegistryPassword: "",
exposeMinIO: true,
exposeConsole: true,
tenantCustom: false,
logSearchEnabled: true,
prometheusEnabled: true,
logSearchVolumeSize: "5",
logSearchSizeFactor: "Gi",
logSearchSelectedStorageClass: "default",
logSearchImage: "",
kesImage: "",
logSearchPostgresImage: "",
logSearchPostgresInitImage: "",
prometheusVolumeSize: "5",
prometheusSizeFactor: "Gi",
prometheusSelectedStorageClass: "default",
prometheusImage: "",
prometheusSidecarImage: "",
prometheusInitImage: "",
setDomains: false,
consoleDomain: "",
minioDomains: [""],
tenantSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
configure: {
customImage: false,
imageName: "",
customDockerhub: false,
imageRegistry: "",
imageRegistryUsername: "",
imageRegistryPassword: "",
exposeMinIO: true,
exposeConsole: true,
tenantCustom: false,
logSearchEnabled: true,
prometheusEnabled: true,
logSearchVolumeSize: "5",
logSearchSizeFactor: "Gi",
logSearchSelectedStorageClass: "default",
logSearchImage: "",
kesImage: "",
logSearchPostgresImage: "",
logSearchPostgresInitImage: "",
prometheusVolumeSize: "5",
prometheusSizeFactor: "Gi",
prometheusSelectedStorageClass: "default",
prometheusImage: "",
prometheusSidecarImage: "",
prometheusInitImage: "",
setDomains: false,
consoleDomain: "",
minioDomains: [""],
tenantSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
logSearchSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
logSearchPostgresSecurityContext: {
runAsUser: "999",
runAsGroup: "999",
fsGroup: "999",
runAsNonRoot: true,
},
prometheusSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
logSearchSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
identityProvider: {
idpSelection: "Built-in",
accessKeys: [getRandomString(16)],
secretKeys: [getRandomString(32)],
openIDConfigurationURL: "",
openIDClientID: "",
openIDSecretID: "",
openIDCallbackURL: "",
openIDClaimName: "",
openIDScopes: "",
ADURL: "",
ADSkipTLS: false,
ADServerInsecure: false,
ADGroupSearchBaseDN: "",
ADGroupSearchFilter: "",
ADUserDNs: [""],
ADLookupBindDN: "",
ADLookupBindPassword: "",
ADUserDNSearchBaseDN: "",
ADUserDNSearchFilter: "",
ADServerStartTLS: false,
logSearchPostgresSecurityContext: {
runAsUser: "999",
runAsGroup: "999",
fsGroup: "999",
runAsNonRoot: true,
},
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",
azureEndpoint: "",
azureTenantID: "",
azureClientID: "",
azureClientSecret: "",
gcpProjectID: "",
gcpEndpoint: "",
gcpClientEmail: "",
gcpClientID: "",
gcpPrivateKeyID: "",
gcpPrivateKey: "",
enableCustomCertsForKES: false,
replicas: "1",
kesSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
},
tenantSize: {
volumeSize: "1024",
sizeFactor: "Gi",
drivesPerServer: "4",
nodes: "4",
memoryNode: "2",
ecParity: "",
ecParityChoices: [],
cleanECChoices: [],
untouchedECField: true,
distribution: {
error: "",
nodes: 0,
persistentVolumes: 0,
disks: 0,
},
ecParityCalc: {
error: 0,
defaultEC: "",
erasureCodeSet: 0,
maxEC: "",
rawCapacity: "0",
storageFactors: [],
},
limitSize: {},
cpuToUse: "0",
// resource request
resourcesSpecifyLimit: false,
resourcesCPURequestError: "",
resourcesCPURequest: "",
resourcesCPULimitError: "",
resourcesCPULimit: "",
resourcesMemoryRequestError: "",
resourcesMemoryRequest: "",
resourcesMemoryLimitError: "",
resourcesMemoryLimit: "",
resourcesSize: {
error: "",
memoryRequest: 0,
memoryLimit: 0,
cpuRequest: 0,
cpuLimit: 0,
},
maxAllocatableResources: {
min_allocatable_mem: 0,
min_allocatable_cpu: 0,
cpu_priority: {
max_allocatable_cpu: 0,
max_allocatable_mem: 0,
},
mem_priority: {
max_allocatable_cpu: 0,
max_allocatable_mem: 0,
},
},
maxCPUsUse: "0",
maxMemorySize: "0",
integrationSelection: {
driveSize: { driveSize: "0", sizeUnit: "B" },
CPU: 0,
typeSelection: "",
memory: 0,
drivesPerServer: 0,
storageClass: "",
},
},
affinity: {
nodeSelectorLabels: "",
podAffinity: "default",
withPodAntiAffinity: true,
prometheusSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
},
certificates: {
minioCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
caCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
consoleCaCertificates: [
{
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: "",
identityProvider: {
idpSelection: "Built-in",
accessKeys: [getRandomString(16)],
secretKeys: [getRandomString(32)],
openIDConfigurationURL: "",
openIDClientID: "",
openIDSecretID: "",
openIDCallbackURL: "",
openIDClaimName: "",
openIDScopes: "",
ADURL: "",
ADSkipTLS: false,
ADServerInsecure: false,
ADGroupSearchBaseDN: "",
ADGroupSearchFilter: "",
ADUserDNs: [""],
ADLookupBindDN: "",
ADLookupBindPassword: "",
ADUserDNSearchBaseDN: "",
ADUserDNSearchFilter: "",
ADServerStartTLS: false,
},
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",
azureEndpoint: "",
azureTenantID: "",
azureClientID: "",
azureClientSecret: "",
gcpProjectID: "",
gcpEndpoint: "",
gcpClientEmail: "",
gcpClientID: "",
gcpPrivateKeyID: "",
gcpPrivateKey: "",
enableCustomCertsForKES: false,
replicas: "1",
kesSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
},
nodeSelectorPairs: [{ key: "", value: "" }],
tolerations: [
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
tenantSize: {
volumeSize: "1024",
sizeFactor: "Gi",
drivesPerServer: "4",
nodes: "4",
memoryNode: "2",
ecParity: "",
ecParityChoices: [],
cleanECChoices: [],
untouchedECField: true,
distribution: {
error: "",
nodes: 0,
persistentVolumes: 0,
disks: 0,
},
],
ecParityCalc: {
error: 0,
defaultEC: "",
erasureCodeSet: 0,
maxEC: "",
rawCapacity: "0",
storageFactors: [],
},
limitSize: {},
cpuToUse: "0",
// resource request
resourcesSpecifyLimit: false,
resourcesCPURequestError: "",
resourcesCPURequest: "",
resourcesCPULimitError: "",
resourcesCPULimit: "",
resourcesMemoryRequestError: "",
resourcesMemoryRequest: "",
resourcesMemoryLimitError: "",
resourcesMemoryLimit: "",
resourcesSize: {
error: "",
memoryRequest: 0,
memoryLimit: 0,
cpuRequest: 0,
cpuLimit: 0,
},
maxAllocatableResources: {
min_allocatable_mem: 0,
min_allocatable_cpu: 0,
cpu_priority: {
max_allocatable_cpu: 0,
max_allocatable_mem: 0,
},
mem_priority: {
max_allocatable_cpu: 0,
max_allocatable_mem: 0,
},
},
maxCPUsUse: "0",
maxMemorySize: "0",
integrationSelection: {
driveSize: { driveSize: "0", sizeUnit: "B" },
CPU: 0,
typeSelection: "",
memory: 0,
drivesPerServer: 0,
storageClass: "",
},
},
affinity: {
nodeSelectorLabels: "",
podAffinity: "default",
withPodAntiAffinity: true,
},
};
state.certificates = {
minioCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
caCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
consoleCaCertificates: [
{
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: "",
},
};
state.nodeSelectorPairs = [{ key: "", value: "" }];
state.tolerations = [
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
},
];
state.createdAccount = null;
state.showNewCredentials = false;
},
setKeyValuePairs: (state, action: PayloadAction<LabelKeyPair[]>) => {
state.nodeSelectorPairs = action.payload;
@@ -1057,6 +1070,45 @@ export const createTenantSlice = createSlice({
setIDP: (state, action: PayloadAction<string>) => {
state.fields.identityProvider.idpSelection = action.payload;
},
setTenantName: (state, action: PayloadAction<string>) => {
state.fields.nameTenant.tenantName = action.payload;
delete state.validationErrors["tenant-name"];
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: action.payload,
},
]);
let isValid = false;
if ("tenant-name" in commonValidation) {
isValid = true;
state.validationErrors["tenant-name"] = commonValidation["tenant-name"];
}
flipValidPageInState(state, "nameTenant", isValid);
},
},
extraReducers: (builder) => {
builder
.addCase(createTenantAsync.pending, (state, action) => {
state.addingTenant = true;
state.createdAccount = null;
state.showNewCredentials = false;
})
.addCase(createTenantAsync.rejected, (state, action) => {
state.addingTenant = false;
})
.addCase(createTenantAsync.fulfilled, (state, action) => {
state.addingTenant = false;
state.createdAccount = action.payload;
state.showNewCredentials = true;
});
},
});
@@ -1097,6 +1149,7 @@ export const {
addIDPADUsrAtIndex,
removeIDPADUsrAtIndex,
setIDP,
setTenantName,
} = createTenantSlice.actions;
export default createTenantSlice.reducer;

View File

@@ -0,0 +1,36 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { ICreateTenant } from "./createTenantSlice";
import { Draft } from "@reduxjs/toolkit";
export const flipValidPageInState = (
state: Draft<ICreateTenant>,
pageName: string,
valid: boolean
) => {
let originValidPages = state.validPages;
if (valid) {
if (!originValidPages.includes(pageName)) {
originValidPages.push(pageName);
state.validPages = [...originValidPages];
}
} else {
const newSetOfPages = originValidPages.filter((elm) => elm !== pageName);
state.validPages = [...newSetOfPages];
}
};

View File

@@ -0,0 +1,590 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createAsyncThunk } from "@reduxjs/toolkit";
import { AppState } from "../../../../../store";
import { generatePoolName, getBytes } from "../../../../../common/utils";
import { getDefaultAffinity, getNodeSelector } from "../../TenantDetails/utils";
import { ITenantCreator } from "../../../../../common/types";
import { KeyPair } from "../../ListTenants/utils";
import { createTenantCall } from "../createTenantAPI";
import { setErrorSnackMessage } from "../../../../../systemSlice";
export const createTenantAsync = createAsyncThunk(
"createTenant/createTenantAsync",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
let fields = state.createTenant.fields;
let certificates = state.createTenant.certificates;
const tenantName = fields.nameTenant.tenantName;
const selectedStorageClass = fields.nameTenant.selectedStorageClass;
const imageName = fields.configure.imageName;
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 openIDConfigurationURL =
fields.identityProvider.openIDConfigurationURL;
const openIDClientID = fields.identityProvider.openIDClientID;
const openIDClaimName = fields.identityProvider.openIDClaimName;
const openIDCallbackURL = fields.identityProvider.openIDCallbackURL;
const openIDScopes = fields.identityProvider.openIDScopes;
const openIDSecretID = fields.identityProvider.openIDSecretID;
const ADURL = fields.identityProvider.ADURL;
const ADSkipTLS = fields.identityProvider.ADSkipTLS;
const ADServerInsecure = fields.identityProvider.ADServerInsecure;
const ADGroupSearchBaseDN = fields.identityProvider.ADGroupSearchBaseDN;
const ADGroupSearchFilter = fields.identityProvider.ADGroupSearchFilter;
const ADUserDNs = fields.identityProvider.ADUserDNs;
const ADLookupBindDN = fields.identityProvider.ADLookupBindDN;
const ADLookupBindPassword = fields.identityProvider.ADLookupBindPassword;
const ADUserDNSearchBaseDN = fields.identityProvider.ADUserDNSearchBaseDN;
const ADUserDNSearchFilter = fields.identityProvider.ADUserDNSearchFilter;
const ADServerStartTLS = fields.identityProvider.ADServerStartTLS;
const accessKeys = fields.identityProvider.accessKeys;
const secretKeys = fields.identityProvider.secretKeys;
const minioCertificates = certificates.minioCertificates;
const caCertificates = certificates.caCertificates;
const consoleCaCertificates = certificates.consoleCaCertificates;
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 azureEndpoint = fields.encryption.azureEndpoint;
const azureTenantID = fields.encryption.azureTenantID;
const azureClientID = fields.encryption.azureClientID;
const azureClientSecret = fields.encryption.azureClientSecret;
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 enableTLS = fields.security.enableTLS;
const ecParity = fields.tenantSize.ecParity;
const distribution = fields.tenantSize.distribution;
const tenantCustom = fields.configure.tenantCustom;
const logSearchEnabled = fields.configure.logSearchEnabled;
const prometheusEnabled = fields.configure.prometheusEnabled;
const logSearchVolumeSize = fields.configure.logSearchVolumeSize;
const logSearchSelectedStorageClass =
fields.configure.logSearchSelectedStorageClass;
const logSearchImage = fields.configure.logSearchImage;
const kesImage = fields.configure.kesImage;
const logSearchPostgresImage = fields.configure.logSearchPostgresImage;
const logSearchPostgresInitImage =
fields.configure.logSearchPostgresInitImage;
const prometheusImage = fields.configure.prometheusImage;
const prometheusSidecarImage = fields.configure.prometheusSidecarImage;
const prometheusInitImage = fields.configure.prometheusInitImage;
const prometheusSelectedStorageClass =
fields.configure.prometheusSelectedStorageClass;
const prometheusVolumeSize = fields.configure.prometheusVolumeSize;
const affinityType = fields.affinity.podAffinity;
const nodeSelectorLabels = fields.affinity.nodeSelectorLabels;
const withPodAntiAffinity = fields.affinity.withPodAntiAffinity;
const tenantSecurityContext = fields.configure.tenantSecurityContext;
const logSearchSecurityContext = fields.configure.logSearchSecurityContext;
const logSearchPostgresSecurityContext =
fields.configure.logSearchPostgresSecurityContext;
const prometheusSecurityContext =
fields.configure.prometheusSecurityContext;
const kesSecurityContext = fields.encryption.kesSecurityContext;
const kesReplicas = fields.encryption.replicas;
const setDomains = fields.configure.setDomains;
const minioDomains = fields.configure.minioDomains;
const consoleDomain = fields.configure.consoleDomain;
let tolerations = state.createTenant.tolerations;
let namespace = state.createTenant.fields.nameTenant.namespace;
const tolerationValues = tolerations.filter(
(toleration) => toleration.key.trim() !== ""
);
const poolName = generatePoolName([]);
let affinityObject = {};
switch (affinityType) {
case "default":
affinityObject = {
affinity: getDefaultAffinity(tenantName, poolName),
};
break;
case "nodeSelector":
affinityObject = {
affinity: getNodeSelector(
nodeSelectorLabels,
withPodAntiAffinity,
tenantName,
poolName
),
};
break;
}
const erasureCode = ecParity.split(":")[1];
let dataSend: ITenantCreator = {
name: tenantName,
namespace: namespace,
access_key: "",
secret_key: "",
access_keys: [],
secret_keys: [],
enable_tls: enableTLS && enableAutoCert,
enable_console: true,
enable_prometheus: true,
service_name: "",
image: imageName,
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,
},
securityContext: tenantCustom ? tenantSecurityContext : null,
...affinityObject,
tolerations: tolerationValues,
},
],
erasureCodingParity: parseInt(erasureCode, 10),
};
// Set Resources
if (
fields.tenantSize.resourcesCPURequest !== "" ||
fields.tenantSize.resourcesCPULimit !== "" ||
fields.tenantSize.resourcesMemoryRequest !== "" ||
fields.tenantSize.resourcesMemoryLimit !== ""
) {
dataSend.pools[0].resources = {};
// requests
if (
fields.tenantSize.resourcesCPURequest !== "" ||
fields.tenantSize.resourcesMemoryRequest !== ""
) {
dataSend.pools[0].resources.requests = {};
if (fields.tenantSize.resourcesCPURequest !== "") {
dataSend.pools[0].resources.requests.cpu = parseInt(
fields.tenantSize.resourcesCPURequest
);
}
if (fields.tenantSize.resourcesMemoryRequest !== "") {
dataSend.pools[0].resources.requests.memory = parseInt(
getBytes(fields.tenantSize.resourcesMemoryRequest, "Gi", true)
);
}
}
// limits
if (
fields.tenantSize.resourcesCPULimit !== "" ||
fields.tenantSize.resourcesMemoryLimit !== ""
) {
dataSend.pools[0].resources.limits = {};
if (fields.tenantSize.resourcesCPULimit !== "") {
dataSend.pools[0].resources.limits.cpu = parseInt(
fields.tenantSize.resourcesCPULimit
);
}
if (fields.tenantSize.resourcesMemoryLimit !== "") {
dataSend.pools[0].resources.limits.memory = parseInt(
getBytes(fields.tenantSize.resourcesMemoryLimit, "Gi", true)
);
}
}
}
if (customDockerhub) {
dataSend = {
...dataSend,
image_registry: {
registry: imageRegistry,
username: imageRegistryUsername,
password: imageRegistryPassword,
},
};
}
if (logSearchEnabled) {
dataSend = {
...dataSend,
logSearchConfiguration: {
storageClass:
logSearchSelectedStorageClass === "default"
? ""
: logSearchSelectedStorageClass,
storageSize: parseInt(logSearchVolumeSize),
image: logSearchImage,
postgres_image: logSearchPostgresImage,
postgres_init_image: logSearchPostgresInitImage,
securityContext: logSearchSecurityContext,
postgres_securityContext: logSearchPostgresSecurityContext,
},
};
}
if (prometheusEnabled) {
dataSend = {
...dataSend,
prometheusConfiguration: {
storageClass:
prometheusSelectedStorageClass === "default"
? ""
: prometheusSelectedStorageClass,
storageSize: parseInt(prometheusVolumeSize),
image: prometheusImage,
sidecar_image: prometheusSidecarImage,
init_image: prometheusInitImage,
securityContext: prometheusSecurityContext,
},
};
}
let tenantCerts: any = null;
let consoleCerts: any = null;
let caCerts: any = null;
let consoleCaCerts: any = null;
if (caCertificates.length > 0) {
caCerts = {
ca_certificates: caCertificates
.map((keyPair: KeyPair) => keyPair.encoded_cert)
.filter((keyPair) => keyPair),
};
}
if (consoleCaCertificates.length > 0) {
consoleCaCerts = {
console_ca_certificates: consoleCaCertificates
.map((keyPair: KeyPair) => keyPair.encoded_cert)
.filter((keyPair) => keyPair),
};
}
if (enableTLS && minioCertificates.length > 0) {
tenantCerts = {
minio: minioCertificates
.map((keyPair: KeyPair) => ({
crt: keyPair.encoded_cert,
key: keyPair.encoded_key,
}))
.filter((keyPair) => keyPair.crt && keyPair.key),
};
}
if (
enableTLS &&
consoleCertificate.encoded_cert !== "" &&
consoleCertificate.encoded_key !== ""
) {
consoleCerts = {
console: {
crt: consoleCertificate.encoded_cert,
key: consoleCertificate.encoded_key,
},
};
}
if (tenantCerts || consoleCerts || caCerts || consoleCaCerts) {
dataSend = {
...dataSend,
tls: {
...tenantCerts,
...consoleCerts,
...caCerts,
...consoleCaCerts,
},
};
}
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 "azure":
insertEncrypt = {
azure: {
keyvault: {
endpoint: azureEndpoint,
credentials: {
tenant_id: azureTenantID,
client_id: azureClientID,
client_secret: azureClientSecret,
},
},
},
};
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 || vaultCAInsert) {
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: {
replicas: kesReplicas,
securityContext: kesSecurityContext,
image: kesImage,
...encryptionClientKeyPair,
...encryptionServerKeyPair,
...insertEncrypt,
},
};
}
let dataIDP: any = {};
switch (idpSelection) {
case "Built-in":
let keyarray = [];
for (let i = 0; i < accessKeys.length; i++) {
keyarray.push({
access_key: accessKeys[i],
secret_key: secretKeys[i],
});
}
dataIDP = {
keys: keyarray,
};
break;
case "OpenID":
dataIDP = {
oidc: {
configuration_url: openIDConfigurationURL,
client_id: openIDClientID,
secret_id: openIDSecretID,
claim_name: openIDClaimName,
callback_url: openIDCallbackURL,
scopes: openIDScopes,
},
};
break;
case "AD":
dataIDP = {
active_directory: {
url: ADURL,
skip_tls_verification: ADSkipTLS,
server_insecure: ADServerInsecure,
group_search_base_dn: ADGroupSearchBaseDN,
group_search_filter: ADGroupSearchFilter,
user_dns: ADUserDNs,
lookup_bind_dn: ADLookupBindDN,
lookup_bind_password: ADLookupBindPassword,
user_dn_search_base_dn: ADUserDNSearchBaseDN,
user_dn_search_filter: ADUserDNSearchFilter,
server_start_tls: ADServerStartTLS,
},
};
break;
}
let domains: any = {};
let sendDomain: any = {};
if (setDomains) {
if (consoleDomain !== "") {
domains.console = consoleDomain;
}
const filteredDomains = minioDomains.filter((dom) => dom.trim() !== "");
if (filteredDomains.length > 0) {
domains.minio = filteredDomains;
}
if (Object.keys(domains).length > 0) {
sendDomain.domains = domains;
}
}
dataSend = {
...dataSend,
...sendDomain,
idp: { ...dataIDP },
};
const response = createTenantCall(dataSend)
.then((resp) => {
return resp;
})
.catch((err) => {
dispatch(setErrorSnackMessage(err));
return rejectWithValue(err);
});
return response;
}
);

View File

@@ -384,5 +384,3 @@ export interface ITenantIdentityProviderResponse {
user_dn_search_filter: string;
};
}
export type FieldsToHandle = INameTenantFields;

View File

@@ -27,7 +27,7 @@ import (
)
var (
ErrDefault = errors.New("an errors occurred, please try again")
ErrDefault = errors.New("an error occurred, please try again")
ErrInvalidLogin = errors.New("invalid Login")
ErrForbidden = errors.New("403 Forbidden")
ErrFileTooLarge = errors.New("413 File too Large")