Add support for edit/add/remove environment variables to MinIO tenant (#2331)

![image](https://user-images.githubusercontent.com/1795553/191574784-69d55ca6-0a8c-41f3-b7f5-8526854cc8d2.png)


Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
Lenin Alevski
2022-09-29 20:50:35 -07:00
committed by GitHub
parent 73a687376a
commit a3b88567cc
21 changed files with 2741 additions and 7 deletions

View File

@@ -733,6 +733,26 @@ export const getClientOS = (): string => {
return getPlatform;
};
export const MinIOEnvVarsSettings: any = {
MINIO_ACCESS_KEY: { secret: true },
MINIO_ACCESS_KEY_OLD: { secret: true },
MINIO_AUDIT_WEBHOOK_AUTH_TOKEN: { secret: true },
MINIO_IDENTITY_LDAP_LOOKUP_BIND_PASSWORD: { secret: true },
MINIO_IDENTITY_OPENID_CLIENT_SECRET: { secret: true },
MINIO_KMS_SECRET_KEY: { secret: true },
MINIO_LOGGER_WEBHOOK_AUTH_TOKEN: { secret: true },
MINIO_NOTIFY_ELASTICSEARCH_PASSWORD: { secret: true },
MINIO_NOTIFY_KAFKA_SASL_PASSWORD: { secret: true },
MINIO_NOTIFY_MQTT_PASSWORD: { secret: true },
MINIO_NOTIFY_NATS_PASSWORD: { secret: true },
MINIO_NOTIFY_NATS_TOKEN: { secret: true },
MINIO_NOTIFY_REDIS_PASSWORD: { secret: true },
MINIO_NOTIFY_WEBHOOK_AUTH_TOKEN: { secret: true },
MINIO_ROOT_PASSWORD: { secret: true },
MINIO_SECRET_KEY: { secret: true },
MINIO_SECRET_KEY_OLD: { secret: true },
};
export const MinIOEnvironmentVariables = [
"MINIO_ACCESS_KEY",
"MINIO_ACCESS_KEY_OLD",

View File

@@ -0,0 +1,321 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useCallback, useEffect, useState } from "react";
import { connect, useSelector } from "react-redux";
import { Theme } from "@mui/material/styles";
import { DialogContentText, IconButton } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "../../../../icons/RemoveIcon";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import {
ITenantConfigurationRequest,
ITenantConfigurationResponse,
LabelKeyPair,
} from "../types";
import {
containerForHeader,
createTenantCommon,
formFieldStyles,
modalBasic,
spacingUtils,
tenantDetailsStyles,
wizardCommon,
} from "../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { AppState, useAppDispatch } from "../../../../store";
import { ErrorResponseHandler } from "../../../../common/types";
import { ConfirmModalIcon } from "../../../../icons";
import { setErrorSnackMessage } from "../../../../systemSlice";
import api from "../../../../common/api";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import Loader from "../../Common/Loader/Loader";
import { Button } from "mds";
import { MinIOEnvVarsSettings } from "../../../../common/utils";
interface ITenantConfiguration {
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
...spacingUtils,
envVarRow: {
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
"&:last-child": {
borderBottom: 0,
},
"@media (max-width: 900px)": {
flex: 1,
"& div label": {
minWidth: 50,
},
},
},
rowActions: {
display: "flex",
justifyContent: "flex-end",
"@media (max-width: 900px)": {
flex: 1,
},
},
overlayAction: {
marginLeft: 10,
"& svg": {
width: 15,
height: 15,
maxWidth: 15,
maxHeight: 15,
},
"& button": {
background: "#EAEAEA",
},
},
loaderAlign: {
textAlign: "center",
},
bold: { fontWeight: "bold" },
italic: { fontStyle: "italic" },
fileItem: {
marginRight: 10,
display: "flex",
"& div label": {
minWidth: 50,
},
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
...containerForHeader(theme.spacing(4)),
...createTenantCommon,
...formFieldStyles,
...modalBasic,
...wizardCommon,
});
const TenantConfiguration = ({ classes }: ITenantConfiguration) => {
const dispatch = useAppDispatch();
const tenant = useSelector((state: AppState) => state.tenants.tenantInfo);
const loadingTenant = useSelector(
(state: AppState) => state.tenants.loadingTenant
);
const [isSending, setIsSending] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [envVars, setEnvVars] = useState<LabelKeyPair[]>([]);
const [envVarsToBeDeleted, setEnvVarsToBeDeleted] = useState<string[]>([]);
const getTenantConfigurationInfo = useCallback(() => {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenant?.namespace}/tenants/${tenant?.name}/configuration`
)
.then((res: ITenantConfigurationResponse) => {
if (res.environmentVariables) {
setEnvVars(res.environmentVariables);
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
});
}, [tenant, dispatch]);
useEffect(() => {
if (tenant) {
getTenantConfigurationInfo();
}
}, [tenant, getTenantConfigurationInfo]);
const updateTenantConfiguration = () => {
setIsSending(true);
let payload: ITenantConfigurationRequest = {
environmentVariables: envVars.filter((env) => env.key !== ""),
keysToBeDeleted: envVarsToBeDeleted,
};
api
.invoke(
"PATCH",
`/api/v1/namespaces/${tenant?.namespace}/tenants/${tenant?.name}/configuration`,
payload
)
.then(() => {
setIsSending(false);
setDialogOpen(false);
getTenantConfigurationInfo();
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setIsSending(false);
});
};
return (
<React.Fragment>
<ConfirmDialog
title={"Save and Restart"}
confirmText={"Restart"}
cancelText="Cancel"
titleIcon={<ConfirmModalIcon />}
isLoading={isSending}
onClose={() => setDialogOpen(false)}
isOpen={dialogOpen}
onConfirm={updateTenantConfiguration}
confirmationContent={
<DialogContentText>
Are you sure you want to save the changes and restart the service?
</DialogContentText>
}
/>
{loadingTenant ? (
<div className={classes.loaderAlign}>
<Loader />
</div>
) : (
<Grid container spacing={1}>
<Grid item xs={12}>
<h1 className={classes.sectionTitle}>Configuration</h1>
<hr className={classes.hrClass} />
</Grid>
<Grid container spacing={1}>
{envVars.map((envVar, index) => (
<Grid
item
xs={12}
className={`${classes.formFieldRow} ${classes.envVarRow}`}
key={`tenant-envVar-${index.toString()}`}
>
<Grid item xs={5} className={classes.fileItem}>
<InputBoxWrapper
id="env_var_key"
name="env_var_key"
label="Key"
value={envVar.key}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const existingEnvVars = [...envVars];
setEnvVars(
existingEnvVars.map((keyPair, i) =>
i === index
? { key: e.target.value, value: keyPair.value }
: keyPair
)
);
}}
index={index}
key={`env_var_key_${index.toString()}`}
/>
</Grid>
<Grid item xs={5} className={classes.fileItem}>
<InputBoxWrapper
id="env_var_value"
name="env_var_value"
label="Value"
value={envVar.value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const existingEnvVars = [...envVars];
setEnvVars(
existingEnvVars.map((keyPair, i) =>
i === index
? { key: keyPair.key, value: e.target.value }
: keyPair
)
);
}}
index={index}
key={`env_var_value_${index.toString()}`}
type={
MinIOEnvVarsSettings[envVar.key] &&
MinIOEnvVarsSettings[envVar.key].secret
? "password"
: "text"
}
/>
</Grid>
<Grid item xs={2} className={classes.rowActions}>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => {
const existingEnvVars = [...envVars];
existingEnvVars.push({ key: "", value: "" });
setEnvVars(existingEnvVars);
}}
disabled={index !== envVars.length - 1}
>
<AddIcon />
</IconButton>
</div>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => {
const existingEnvVars = envVars.filter(
(item, fIndex) => fIndex !== index
);
setEnvVars(existingEnvVars);
setEnvVarsToBeDeleted([
...envVarsToBeDeleted,
envVar.key,
]);
}}
disabled={envVars.length <= 1}
>
<RemoveIcon />
</IconButton>
</div>
</Grid>
</Grid>
))}
</Grid>
<Grid
item
xs={12}
sx={{ display: "flex", justifyContent: "flex-end" }}
>
<Button
id={"save-environment-variables"}
type="submit"
variant="callAction"
disabled={dialogOpen || isSending}
onClick={() => setDialogOpen(true)}
label={"Save"}
/>
</Grid>
</Grid>
)}
</React.Fragment>
);
};
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.loadingTenant,
selectedTenant: state.tenants.currentTenant,
tenant: state.tenants.tenantInfo,
});
const connector = connect(mapState, null);
export default withStyles(styles)(connector(TenantConfiguration));

View File

@@ -89,6 +89,9 @@ const PodDetails = withSuspense(React.lazy(() => import("./pods/PodDetails")));
const EditTenantMonitoringScreen = withSuspense(
React.lazy(() => import("./EditTenantMonitoringScreen"))
);
const TenantConfiguration = withSuspense(
React.lazy(() => import("./TenantConfiguration"))
);
interface ITenantDetailsProps {
classes: any;
@@ -366,6 +369,10 @@ const TenantDetails = ({ classes }: ITenantDetailsProps) => {
<div className={classes.contentSpacer}>
<Routes>
<Route path={"summary"} element={<TenantSummary />} />
<Route
path={"configuration"}
element={<TenantConfiguration />}
/>
<Route path={`summary/yaml`} element={<TenantYAML />} />
<Route path={"metrics"} element={<TenantMetrics />} />
<Route path={"trace"} element={<TenantTrace />} />
@@ -408,6 +415,14 @@ const TenantDetails = ({ classes }: ITenantDetailsProps) => {
to: getRoutePath("summary"),
},
}}
{{
tabConfig: {
label: "Configuration",
value: "configuration",
component: Link,
to: getRoutePath("configuration"),
},
}}
{{
tabConfig: {
label: "Metrics",

View File

@@ -40,6 +40,15 @@ export interface ICustomCertificates {
consoleCAs: ICertificateInfo[];
}
export interface ITenantConfigurationResponse {
environmentVariables: LabelKeyPair[];
}
export interface ITenantConfigurationRequest {
environmentVariables: LabelKeyPair[];
keysToBeDeleted: string[];
}
export interface ITenantSecurityResponse {
autoCert: boolean;
customCertificates: ICustomCertificates;