Add support for edit/add/remove environment variables to MinIO tenant (#2331)
 Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com> Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user