Move Register Component to Redux (#2630)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2023-02-06 13:51:39 -08:00
committed by GitHub
parent 900b8d9f06
commit 24fdf3487b
13 changed files with 1042 additions and 615 deletions

View File

@@ -30,11 +30,12 @@ import { spacingUtils } from "../Common/FormComponents/common/styleLibrary";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { useNavigate } from "react-router-dom";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
interface IApiKeyRegister {
classes: any;
registerEndpoint: string;
afterRegister: () => void;
}
const styles = (theme: Theme) =>
@@ -45,11 +46,9 @@ const styles = (theme: Theme) =>
...spacingUtils,
});
const ApiKeyRegister = ({
classes,
registerEndpoint,
afterRegister,
}: IApiKeyRegister) => {
const ApiKeyRegister = ({ classes, registerEndpoint }: IApiKeyRegister) => {
const navigate = useNavigate();
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
const [apiKey, setApiKey] = useState("");
const [loading, setLoading] = useState(false);
@@ -67,8 +66,7 @@ const ApiKeyRegister = ({
.then((resp: SubnetLoginResponse) => {
setLoading(false);
if (resp && resp.registered) {
reset();
afterRegister();
navigate(IAM_PAGES.LICENSE);
}
})
.catch((err: ErrorResponseHandler) => {
@@ -76,7 +74,7 @@ const ApiKeyRegister = ({
setLoading(false);
reset();
});
}, [afterRegister, apiKey, dispatch, loading, registerEndpoint]);
}, [apiKey, dispatch, loading, registerEndpoint, navigate]);
useEffect(() => {
if (fromModal) {

View File

@@ -0,0 +1,116 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 from "react";
import { Box } from "@mui/material";
import { FormTitle } from "./utils";
import SelectWrapper from "../Common/FormComponents/SelectWrapper/SelectWrapper";
import { setLoading, setSelectedSubnetOrganization } from "./registerSlice";
import { Button } from "mds";
import RegisterHelpBox from "./RegisterHelpBox";
import { useSelector } from "react-redux";
import { AppState, useAppDispatch } from "../../../store";
import { callRegister } from "./registerThunks";
const ClusterRegistrationForm = () => {
const dispatch = useAppDispatch();
const subnetAccessToken = useSelector(
(state: AppState) => state.register.subnetAccessToken
);
const selectedSubnetOrganization = useSelector(
(state: AppState) => state.register.selectedSubnetOrganization
);
const subnetOrganizations = useSelector(
(state: AppState) => state.register.subnetOrganizations
);
const loading = useSelector((state: AppState) => state.register.loading);
return (
<Box
sx={{
display: "flex",
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
}}
>
<Box
sx={{
marginTop: "15px",
marginBottom: "15px",
"& .title-text": {
marginLeft: "0px",
},
}}
>
<FormTitle title={`Register MinIO cluster`} />
</Box>
<Box>
<SelectWrapper
id="subnet-organization"
name="subnet-organization"
onChange={(e) =>
dispatch(setSelectedSubnetOrganization(e.target.value as string))
}
label="Select an organization"
value={selectedSubnetOrganization}
options={subnetOrganizations.map((organization) => ({
label: organization.company,
value: organization.accountId.toString(),
}))}
/>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "15px",
}}
>
<Button
id={"register-cluster"}
onClick={() => () => {
if (loading) {
return;
}
dispatch(setLoading(true));
if (subnetAccessToken && selectedSubnetOrganization) {
dispatch(
callRegister({
token: subnetAccessToken,
account_id: selectedSubnetOrganization,
})
);
}
}}
disabled={loading || subnetAccessToken.trim().length === 0}
variant="callAction"
label={"Register"}
/>
</Box>
</Box>
</Box>
<RegisterHelpBox />
</Box>
);
};
export default ClusterRegistrationForm;

View File

@@ -0,0 +1,159 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 { Box, Link } from "@mui/material";
import { ClusterRegistered, FormTitle } from "./utils";
import { Button, CopyIcon, OfflineRegistrationIcon } from "mds";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import CopyToClipboard from "react-copy-to-clipboard";
import RegisterHelpBox from "./RegisterHelpBox";
import { AppState } from "../../../store";
import { useSelector } from "react-redux";
const OfflineRegistration = () => {
const subnetRegToken = useSelector(
(state: AppState) => state.register.subnetRegToken
);
const clusterRegistered = useSelector(
(state: AppState) => state.register.clusterRegistered
);
const licenseInfo = useSelector(
(state: AppState) => state.register.licenseInfo
);
const offlineRegUrl = `https://subnet.min.io/cluster/register?token=${subnetRegToken}`;
return (
<Fragment>
<Box
sx={{
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "43px",
}}
>
{clusterRegistered && licenseInfo ? (
<ClusterRegistered email={licenseInfo.email} />
) : null}
<Box
sx={{
"& .title-text": {
marginLeft: "27px",
fontWeight: 600,
},
}}
>
<FormTitle
icon={<OfflineRegistrationIcon />}
title={`Register cluster in an Air-gap environment`}
/>
</Box>
<Box
sx={{
display: "flex",
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
marginTop: "15px",
"& .step-number": {
color: "#ffffff",
height: "25px",
width: "25px",
background: "#081C42",
marginRight: "10px",
textAlign: "center",
fontWeight: 600,
borderRadius: "50%",
},
"& .step-row": {
fontSize: "16px",
display: "flex",
marginTop: "15px",
marginBottom: "15px",
},
}}
>
<Box>
<Box className="step-row">
<div className="step-text">
Click on the link to register this cluster in SUBNET
</div>
</Box>
<Box
sx={{
flex: "1",
display: "flex",
alignItems: "center",
gap: 3,
}}
>
<Link
style={{
color: "#2781B0",
cursor: "pointer",
}}
color="inherit"
href={offlineRegUrl}
target="_blank"
>
https://subnet.min.io/cluster/register
</Link>
<TooltipWrapper tooltip={"Copy to Clipboard"}>
<CopyToClipboard text={offlineRegUrl}>
<Button
type={"button"}
id={"copy-ult-to-clip-board"}
icon={<CopyIcon />}
color={"primary"}
variant={"regular"}
/>
</CopyToClipboard>
</TooltipWrapper>
</Box>
<div
style={{
marginTop: "25px",
fontSize: "14px",
fontStyle: "italic",
color: "#5E5E5E",
}}
>
If this machine does not have internet connection, Copy paste
the following URL in a browser where you access SUBNET and
follow the instructions to complete the registration
</div>
</Box>
</Box>
<RegisterHelpBox />
</Box>
</Box>
</Fragment>
);
};
export default OfflineRegistration;

View File

@@ -0,0 +1,190 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 { Box } from "@mui/material";
import { FormTitle } from "./utils";
import { Button, OnlineRegistrationIcon, UsersIcon } from "mds";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye";
import RegisterHelpBox from "./RegisterHelpBox";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { spacingUtils } from "../Common/FormComponents/common/styleLibrary";
import makeStyles from "@mui/styles/makeStyles";
import { useSelector } from "react-redux";
import { selOpMode } from "../../../systemSlice";
import { AppState, useAppDispatch } from "../../../store";
import {
setShowPassword,
setSubnetEmail,
setSubnetPassword,
} from "./registerSlice";
import { subnetLogin } from "./registerThunks";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
sizedLabel: {
minWidth: "75px",
},
...spacingUtils,
})
);
const OnlineRegistration = () => {
const classes = useStyles();
const dispatch = useAppDispatch();
const operatorMode = useSelector(selOpMode);
const subnetPassword = useSelector(
(state: AppState) => state.register.subnetPassword
);
const subnetEmail = useSelector(
(state: AppState) => state.register.subnetEmail
);
const showPassword = useSelector(
(state: AppState) => state.register.showPassword
);
const loading = useSelector((state: AppState) => state.register.loading);
return (
<Fragment>
<Box
sx={{
"& .title-text": {
marginLeft: "27px",
fontWeight: 600,
},
}}
>
<FormTitle
icon={<OnlineRegistrationIcon />}
title={`Online activation of MinIO Subscription Network License`}
/>
</Box>
<Box
sx={{
display: "flex",
flexFlow: {
xs: "column",
md: "row",
},
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
}}
>
<Box
sx={{
fontSize: "16px",
display: "flex",
flexFlow: "column",
marginTop: "30px",
marginBottom: "30px",
}}
>
Use your MinIO Subscription Network login credentials to register
this cluster.
</Box>
<Box
sx={{
flex: "1",
}}
>
<InputBoxWrapper
className={classes.spacerBottom}
classes={{
inputLabel: classes.sizedLabel,
}}
id="subnet-email"
name="subnet-email"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSubnetEmail(event.target.value))
}
label="Email"
value={subnetEmail}
overlayIcon={<UsersIcon />}
/>
<InputBoxWrapper
className={classes.spacerBottom}
classes={{
inputLabel: classes.sizedLabel,
}}
id="subnet-password"
name="subnet-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSubnetPassword(event.target.value))
}
label="Password"
type={showPassword ? "text" : "password"}
value={subnetPassword}
overlayIcon={
showPassword ? <VisibilityOffIcon /> : <RemoveRedEyeIcon />
}
overlayAction={() => dispatch(setShowPassword(!showPassword))}
/>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
"& button": {
marginLeft: "8px",
},
}}
>
<Button
id={"sign-up"}
type="submit"
className={classes.spacerRight}
variant="regular"
onClick={(e) => {
e.preventDefault();
window.open(
`https://min.io/signup?ref=${operatorMode ? "op" : "con"}`,
"_blank"
);
}}
label={"Sign up"}
/>
<Button
id={"register-credentials"}
type="submit"
variant="callAction"
disabled={
loading ||
subnetEmail.trim().length === 0 ||
subnetPassword.trim().length === 0
}
onClick={() => dispatch(subnetLogin())}
label={"Register"}
/>
</Box>
</Box>
</Box>
<RegisterHelpBox />
</Box>
</Fragment>
);
};
export default OnlineRegistration;

View File

@@ -14,61 +14,37 @@
// 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, useCallback, useEffect, useState } from "react";
import React, { Fragment, useEffect, useState } from "react";
import { Theme } from "@mui/material/styles";
import {
Button,
CopyIcon,
OfflineRegistrationIcon,
OnlineRegistrationIcon,
PageHeader,
UsersIcon,
} from "mds";
import { PageHeader } from "mds";
import createStyles from "@mui/styles/createStyles";
import {
actionsTray,
containerForHeader,
searchField,
spacingUtils,
} from "../Common/FormComponents/common/styleLibrary";
import { spacingUtils } from "../Common/FormComponents/common/styleLibrary";
import withStyles from "@mui/styles/withStyles";
import { Box, Link } from "@mui/material";
import { Box } from "@mui/material";
import PageLayout from "../Common/Layout/PageLayout";
import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import api from "../../../common/api";
import {
SubnetInfo,
SubnetLoginRequest,
SubnetLoginResponse,
SubnetLoginWithMFARequest,
SubnetOrganization,
SubnetRegisterRequest,
SubnetRegTokenResponse,
} from "../License/types";
import { SubnetRegTokenResponse } from "../License/types";
import { ErrorResponseHandler } from "../../../common/types";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import SelectWrapper from "../Common/FormComponents/SelectWrapper/SelectWrapper";
import { hasPermission } from "../../../common/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_PAGES_PERMISSIONS,
} from "../../../common/SecureComponent/permissions";
import { useSelector } from "react-redux";
import RegisterHelpBox from "./RegisterHelpBox";
import { selOpMode, setErrorSnackMessage } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import { setErrorSnackMessage } from "../../../systemSlice";
import { AppState, useAppDispatch } from "../../../store";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import { TabPanel } from "../../shared/tabs";
import { ClusterRegistered, FormTitle, ProxyConfiguration } from "./utils";
import { ClusterRegistered, ProxyConfiguration } from "./utils";
import ApiKeyRegister from "./ApiKeyRegister";
import CopyToClipboard from "react-copy-to-clipboard";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
import { fetchLicenseInfo } from "./registerThunks";
import {
resetRegisterForm,
setCurTab,
setLoading,
setSubnetRegToken,
} from "./registerSlice";
import OfflineRegistration from "./OfflineRegistration";
import SubnetMFAToken from "./SubnetMFAToken";
import ClusterRegistrationForm from "./ClusterRegistrationForm";
import OnlineRegistration from "./OnlineRegistration";
interface IRegister {
classes: any;
@@ -79,469 +55,83 @@ const styles = (theme: Theme) =>
sizedLabel: {
minWidth: "75px",
},
...actionsTray,
...searchField,
...spacingUtils,
...containerForHeader,
});
const Register = ({ classes }: IRegister) => {
const dispatch = useAppDispatch();
const operatorMode = useSelector(selOpMode);
const [license, setLicense] = useState<string>("");
const [subnetPassword, setSubnetPassword] = useState<string>("");
const [subnetEmail, setSubnetEmail] = useState<string>("");
const [subnetMFAToken, setSubnetMFAToken] = useState<string>("");
const [subnetOTP, setSubnetOTP] = useState<string>("");
const [subnetAccessToken, setSubnetAccessToken] = useState<string>("");
const [selectedSubnetOrganization, setSelectedSubnetOrganization] =
useState<string>("");
const [subnetRegToken, setSubnetRegToken] = useState<string>("");
const [subnetOrganizations, setSubnetOrganizations] = useState<
SubnetOrganization[]
>([]);
const [showPassword, setShowPassword] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [loadingLicenseInfo, setLoadingLicenseInfo] = useState<boolean>(false);
const [clusterRegistered, setClusterRegistered] = useState<boolean>(false);
const [licenseInfo, setLicenseInfo] = useState<SubnetInfo | undefined>();
const [curTab, setCurTab] = useState<number>(0);
const subnetMFAToken = useSelector(
(state: AppState) => state.register.subnetMFAToken
);
const subnetAccessToken = useSelector(
(state: AppState) => state.register.subnetAccessToken
);
const subnetRegToken = useSelector(
(state: AppState) => state.register.subnetRegToken
);
const subnetOrganizations = useSelector(
(state: AppState) => state.register.subnetOrganizations
);
const loading = useSelector((state: AppState) => state.register.loading);
const loadingLicenseInfo = useSelector(
(state: AppState) => state.register.loadingLicenseInfo
);
const clusterRegistered = useSelector(
(state: AppState) => state.register.clusterRegistered
);
const licenseInfo = useSelector(
(state: AppState) => state.register.licenseInfo
);
const curTab = useSelector((state: AppState) => state.register.curTab);
const [initialLicenseLoading, setInitialLicenseLoading] =
useState<boolean>(true);
const clearForm = () => {
setSubnetAccessToken("");
setSelectedSubnetOrganization("");
setSubnetRegToken("");
setShowPassword(false);
setSubnetOrganizations([]);
setLicense("");
setSubnetPassword("");
setSubnetEmail("");
setSubnetMFAToken("");
setSubnetOTP("");
};
const getSubnetInfo = hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.LICENSE],
true
);
const fetchLicenseInfo = useCallback(() => {
if (loadingLicenseInfo) {
return;
}
if (getSubnetInfo) {
setLoadingLicenseInfo(true);
api
.invoke("GET", `/api/v1/subnet/info`)
.then((res: SubnetInfo) => {
setLicenseInfo(res);
setClusterRegistered(true);
setLoadingLicenseInfo(false);
})
.catch((err: ErrorResponseHandler) => {
if (
err.detailedError.toLowerCase() !==
"License is not present".toLowerCase()
) {
dispatch(setErrorSnackMessage(err));
}
setClusterRegistered(false);
setLoadingLicenseInfo(false);
});
} else {
setLoadingLicenseInfo(false);
}
}, [loadingLicenseInfo, getSubnetInfo, dispatch]);
useEffect(() => {
// when unmounted, reset
return () => {
dispatch(resetRegisterForm());
};
}, [dispatch]);
const fetchSubnetRegToken = () => {
if (loading || subnetRegToken) {
return;
}
setLoading(true);
dispatch(setLoading(true));
api
.invoke("GET", "/api/v1/subnet/registration-token")
.then((resp: SubnetRegTokenResponse) => {
setLoading(false);
dispatch(setLoading(false));
if (resp && resp.regToken) {
setSubnetRegToken(resp.regToken);
dispatch(setSubnetRegToken(resp.regToken));
}
})
.catch((err: ErrorResponseHandler) => {
console.error(err);
dispatch(setErrorSnackMessage(err));
setLoading(false);
});
};
const callRegister = (token: string, account_id: string) => {
const request: SubnetRegisterRequest = {
token: token,
account_id: account_id,
};
api
.invoke("POST", "/api/v1/subnet/register", request)
.then(() => {
setLoading(false);
clearForm();
fetchLicenseInfo();
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoading(false);
});
};
const subnetRegister = () => {
if (loading) {
return;
}
setLoading(true);
if (subnetAccessToken && selectedSubnetOrganization) {
callRegister(subnetAccessToken, selectedSubnetOrganization);
}
};
const subnetLoginWithMFA = () => {
if (loading) {
return;
}
setLoading(true);
const request: SubnetLoginWithMFARequest = {
username: subnetEmail,
otp: subnetOTP,
mfa_token: subnetMFAToken,
};
api
.invoke("POST", "/api/v1/subnet/login/mfa", request)
.then((resp: SubnetLoginResponse) => {
setLoading(false);
if (resp && resp.access_token && resp.organizations.length > 0) {
if (resp.organizations.length === 1) {
callRegister(
resp.access_token,
resp.organizations[0].accountId.toString()
);
} else {
setSubnetAccessToken(resp.access_token);
setSubnetOrganizations(resp.organizations);
setSelectedSubnetOrganization(
resp.organizations[0].accountId.toString()
);
}
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoading(false);
setSubnetOTP("");
});
};
const subnetLogin = () => {
if (loading) {
return;
}
setLoading(true);
let request: SubnetLoginRequest = {
username: subnetEmail,
password: subnetPassword,
apiKey: license,
};
api
.invoke("POST", "/api/v1/subnet/login", request)
.then((resp: SubnetLoginResponse) => {
setLoading(false);
if (resp && resp.registered) {
clearForm();
fetchLicenseInfo();
} else if (resp && resp.mfa_token) {
setSubnetMFAToken(resp.mfa_token);
} else if (resp && resp.access_token && resp.organizations.length > 0) {
setSubnetAccessToken(resp.access_token);
setSubnetOrganizations(resp.organizations);
setSelectedSubnetOrganization(
resp.organizations[0].accountId.toString()
);
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoading(false);
clearForm();
dispatch(setLoading(false));
});
};
useEffect(() => {
if (initialLicenseLoading) {
fetchLicenseInfo();
dispatch(fetchLicenseInfo());
setInitialLicenseLoading(false);
}
}, [fetchLicenseInfo, initialLicenseLoading, setInitialLicenseLoading]);
}, [initialLicenseLoading, setInitialLicenseLoading, dispatch]);
let clusterRegistrationForm: JSX.Element = <Fragment />;
if (subnetAccessToken && subnetOrganizations.length > 0) {
clusterRegistrationForm = (
<Box
sx={{
display: "flex",
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
}}
>
<Box
sx={{
marginTop: "15px",
marginBottom: "15px",
"& .title-text": {
marginLeft: "0px",
},
}}
>
<FormTitle title={`Register MinIO cluster`} />
</Box>
<Box>
<SelectWrapper
id="subnet-organization"
name="subnet-organization"
onChange={(e) =>
setSelectedSubnetOrganization(e.target.value as string)
}
label="Select an organization"
value={selectedSubnetOrganization}
options={subnetOrganizations.map((organization) => ({
label: organization.company,
value: organization.accountId.toString(),
}))}
/>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "15px",
}}
>
<Button
id={"register-cluster"}
onClick={() => subnetRegister()}
disabled={loading || subnetAccessToken.trim().length === 0}
variant="callAction"
label={"Register"}
/>
</Box>
</Box>
</Box>
<RegisterHelpBox />
</Box>
);
clusterRegistrationForm = <ClusterRegistrationForm />;
} else if (subnetMFAToken) {
clusterRegistrationForm = (
<Box
sx={{
display: "flex",
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
}}
>
<Box
sx={{
fontSize: "16px",
display: "flex",
flexFlow: "column",
marginTop: "30px",
marginBottom: "30px",
}}
>
Two-Factor Authentication
</Box>
<Box>
Please enter the 6-digit verification code that was sent to your
email address. This code will be valid for 5 minutes.
</Box>
<Box
sx={{
flex: "1",
marginTop: "30px",
}}
>
<InputBoxWrapper
overlayIcon={<LockOutlinedIcon />}
id="subnet-otp"
name="subnet-otp"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSubnetOTP(event.target.value)
}
placeholder=""
label=""
value={subnetOTP}
/>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<Button
id={"verify"}
onClick={() => subnetLoginWithMFA()}
disabled={
loading ||
subnetOTP.trim().length === 0 ||
subnetMFAToken.trim().length === 0
}
variant="callAction"
label={"Verify"}
/>
</Box>
</Box>
<RegisterHelpBox />
</Box>
);
clusterRegistrationForm = <SubnetMFAToken />;
} else {
clusterRegistrationForm = (
<Fragment>
<Box
sx={{
"& .title-text": {
marginLeft: "27px",
fontWeight: 600,
},
}}
>
<FormTitle
icon={<OnlineRegistrationIcon />}
title={`Online activation of MinIO Subscription Network License`}
/>
</Box>
<Box
sx={{
display: "flex",
flexFlow: {
xs: "column",
md: "row",
},
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
}}
>
<Box
sx={{
fontSize: "16px",
display: "flex",
flexFlow: "column",
marginTop: "30px",
marginBottom: "30px",
}}
>
Use your MinIO Subscription Network login credentials to register
this cluster.
</Box>
<Box
sx={{
flex: "1",
}}
>
<InputBoxWrapper
className={classes.spacerBottom}
classes={{
inputLabel: classes.sizedLabel,
}}
id="subnet-email"
name="subnet-email"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSubnetEmail(event.target.value)
}
label="Email"
value={subnetEmail}
overlayIcon={<UsersIcon />}
/>
<InputBoxWrapper
className={classes.spacerBottom}
classes={{
inputLabel: classes.sizedLabel,
}}
id="subnet-password"
name="subnet-password"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSubnetPassword(event.target.value)
}
label="Password"
type={showPassword ? "text" : "password"}
value={subnetPassword}
overlayIcon={
showPassword ? <VisibilityOffIcon /> : <RemoveRedEyeIcon />
}
overlayAction={() => setShowPassword(!showPassword)}
/>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
"& button": {
marginLeft: "8px",
},
}}
>
<Button
id={"sign-up"}
type="submit"
className={classes.spacerRight}
variant="regular"
onClick={(e) => {
e.preventDefault();
window.open(
`https://min.io/signup?ref=${
operatorMode ? "op" : "con"
}`,
"_blank"
);
}}
label={"Sign up"}
/>
<Button
id={"register-credentials"}
type="submit"
variant="callAction"
disabled={
loading ||
subnetEmail.trim().length === 0 ||
subnetPassword.trim().length === 0
}
onClick={() => subnetLogin()}
label={"Register"}
/>
</Box>
</Box>
</Box>
<RegisterHelpBox />
</Box>
</Fragment>
);
clusterRegistrationForm = <OnlineRegistration />;
}
const apiKeyRegistration = (
@@ -558,131 +148,14 @@ const Register = ({ classes }: IRegister) => {
{clusterRegistered && licenseInfo ? (
<ClusterRegistered email={licenseInfo.email} />
) : (
<ApiKeyRegister
afterRegister={fetchLicenseInfo}
registerEndpoint={"/api/v1/subnet/login"}
/>
<ApiKeyRegister registerEndpoint={"/api/v1/subnet/login"} />
)}
</Box>
<ProxyConfiguration linkClass={classes.link} />
<ProxyConfiguration />
</Fragment>
);
const offlineRegUrl = `https://subnet.min.io/cluster/register?token=${subnetRegToken}`;
const offlineRegistration = (
<Fragment>
<Box
sx={{
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "43px",
}}
>
{clusterRegistered && licenseInfo ? (
<ClusterRegistered email={licenseInfo.email} />
) : null}
<Box
sx={{
"& .title-text": {
marginLeft: "27px",
fontWeight: 600,
},
}}
>
<FormTitle
icon={<OfflineRegistrationIcon />}
title={`Register cluster in an Airgap environment`}
/>
</Box>
<Box
sx={{
display: "flex",
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
marginTop: "15px",
"& .step-number": {
color: "#ffffff",
height: "25px",
width: "25px",
background: "#081C42",
marginRight: "10px",
textAlign: "center",
fontWeight: 600,
borderRadius: "50%",
},
"& .step-row": {
fontSize: "16px",
display: "flex",
marginTop: "15px",
marginBottom: "15px",
},
}}
>
<Box>
<Box className="step-row">
<div className="step-text">
Click on the link to register this cluster in SUBNET
</div>
</Box>
<Box
sx={{
flex: "1",
display: "flex",
alignItems: "center",
gap: 3,
}}
>
<Link
className={classes.link}
color="inherit"
href={offlineRegUrl}
target="_blank"
>
https://subnet.min.io/cluster/register
</Link>
<TooltipWrapper tooltip={"Copy to Clipboard"}>
<CopyToClipboard text={offlineRegUrl}>
<Button
type={"button"}
id={"copy-ult-to-clip-board"}
icon={<CopyIcon />}
color={"primary"}
variant={"regular"}
/>
</CopyToClipboard>
</TooltipWrapper>
</Box>
<div
style={{
marginTop: "25px",
fontSize: "14px",
fontStyle: "italic",
color: "#5E5E5E",
}}
>
If this machine does not have internet connection, Copy paste
the following URL in a browser where you access SUBNET and
follow the instructions to complete the registration
</div>
</Box>
</Box>
<RegisterHelpBox />
</Box>
</Box>
</Fragment>
);
const offlineRegistration = <OfflineRegistration />;
const regUi = (
<Fragment>
@@ -702,7 +175,7 @@ const Register = ({ classes }: IRegister) => {
)}
</Box>
{!clusterRegistered && <ProxyConfiguration linkClass={classes.link} />}
{!clusterRegistered && <ProxyConfiguration />}
</Fragment>
);
@@ -720,7 +193,7 @@ const Register = ({ classes }: IRegister) => {
<Tabs
value={curTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
setCurTab(newValue);
dispatch(setCurTab(newValue));
}}
indicatorColor="primary"
textColor="primary"
@@ -731,17 +204,17 @@ const Register = ({ classes }: IRegister) => {
<Tab
label="Credentials"
id="simple-tab-0"
aria-controls="simple-tabpanel-0"
aria-controls="simple-tab-panel-0"
/>
<Tab
label="API Key"
id="simple-tab-1"
aria-controls="simple-tabpanel-1"
aria-controls="simple-tab-panel-1"
/>
<Tab
label="Airgap"
label="Air-Gap"
id="simple-tab-2"
aria-controls="simple-tabpanel-2"
aria-controls="simple-tab-panel-2"
onClick={() => fetchSubnetRegToken()}
/>
</Tabs>

View File

@@ -82,10 +82,7 @@ const RegisterOperator = ({ classes }: IRegister) => {
{apiKeyRegistered ? (
<ClusterRegistered email={"Operator"} />
) : (
<ApiKeyRegister
registerEndpoint={"/api/v1/subnet/apikey/register"}
afterRegister={fetchAPIKeyInfo}
/>
<ApiKeyRegister registerEndpoint={"/api/v1/subnet/apikey/register"} />
)}
</Box>
</Fragment>

View File

@@ -78,7 +78,10 @@ function RegisterStatus({ classes, showHelp }: IRegisterStatus) {
<Link
href="https://subnet.min.io"
target="_blank"
className={classes.link}
style={{
color: "#2781B0",
cursor: "pointer",
}}
>
SUBNET
</Link>{" "}

View File

@@ -0,0 +1,110 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 from "react";
import { Box } from "@mui/material";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
import { setSubnetOTP } from "./registerSlice";
import { Button } from "mds";
import RegisterHelpBox from "./RegisterHelpBox";
import { AppState, useAppDispatch } from "../../../store";
import { useSelector } from "react-redux";
import { subnetLoginWithMFA } from "./registerThunks";
const SubnetMFAToken = () => {
const dispatch = useAppDispatch();
const subnetMFAToken = useSelector(
(state: AppState) => state.register.subnetMFAToken
);
const subnetOTP = useSelector((state: AppState) => state.register.subnetOTP);
const loading = useSelector((state: AppState) => state.register.loading);
return (
<Box
sx={{
display: "flex",
}}
>
<Box
sx={{
display: "flex",
flexFlow: "column",
flex: "2",
}}
>
<Box
sx={{
fontSize: "16px",
display: "flex",
flexFlow: "column",
marginTop: "30px",
marginBottom: "30px",
}}
>
Two-Factor Authentication
</Box>
<Box>
Please enter the 6-digit verification code that was sent to your email
address. This code will be valid for 5 minutes.
</Box>
<Box
sx={{
flex: "1",
marginTop: "30px",
}}
>
<InputBoxWrapper
overlayIcon={<LockOutlinedIcon />}
id="subnet-otp"
name="subnet-otp"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSubnetOTP(event.target.value))
}
placeholder=""
label=""
value={subnetOTP}
/>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
<Button
id={"verify"}
onClick={() => dispatch(subnetLoginWithMFA())}
disabled={
loading ||
subnetOTP.trim().length === 0 ||
subnetMFAToken.trim().length === 0
}
variant="callAction"
label={"Verify"}
/>
</Box>
</Box>
<RegisterHelpBox />
</Box>
);
};
export default SubnetMFAToken;

View File

@@ -0,0 +1,132 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SubnetInfo, SubnetOrganization } from "../License/types";
export interface RegisterState {
license: string;
subnetPassword: string;
subnetEmail: string;
subnetMFAToken: string;
subnetOTP: string;
subnetAccessToken: string;
selectedSubnetOrganization: string;
subnetRegToken: string;
subnetOrganizations: SubnetOrganization[];
showPassword: boolean;
loading: boolean;
loadingLicenseInfo: boolean;
clusterRegistered: boolean;
licenseInfo: SubnetInfo | undefined;
curTab: number;
}
const initialState: RegisterState = {
license: "",
subnetPassword: "",
subnetEmail: "",
subnetMFAToken: "",
subnetOTP: "",
subnetAccessToken: "",
selectedSubnetOrganization: "",
subnetRegToken: "",
subnetOrganizations: [],
showPassword: false,
loading: false,
loadingLicenseInfo: false,
clusterRegistered: false,
licenseInfo: undefined,
curTab: 0,
};
export const registerSlice = createSlice({
name: "register",
initialState,
reducers: {
setLicense: (state, action: PayloadAction<string>) => {
state.license = action.payload;
},
setSubnetPassword: (state, action: PayloadAction<string>) => {
state.subnetPassword = action.payload;
},
setSubnetEmail: (state, action: PayloadAction<string>) => {
state.subnetEmail = action.payload;
},
setSubnetMFAToken: (state, action: PayloadAction<string>) => {
state.subnetMFAToken = action.payload;
},
setSubnetOTP: (state, action: PayloadAction<string>) => {
state.subnetOTP = action.payload;
},
setSubnetAccessToken: (state, action: PayloadAction<string>) => {
state.subnetAccessToken = action.payload;
},
setSelectedSubnetOrganization: (state, action: PayloadAction<string>) => {
state.selectedSubnetOrganization = action.payload;
},
setSubnetRegToken: (state, action: PayloadAction<string>) => {
state.subnetRegToken = action.payload;
},
setSubnetOrganizations: (
state,
action: PayloadAction<SubnetOrganization[]>
) => {
state.subnetOrganizations = action.payload;
},
setShowPassword: (state, action: PayloadAction<boolean>) => {
state.showPassword = action.payload;
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setLoadingLicenseInfo: (state, action: PayloadAction<boolean>) => {
state.loadingLicenseInfo = action.payload;
},
setClusterRegistered: (state, action: PayloadAction<boolean>) => {
state.clusterRegistered = action.payload;
},
setLicenseInfo: (state, action: PayloadAction<SubnetInfo | undefined>) => {
state.licenseInfo = action.payload;
},
setCurTab: (state, action: PayloadAction<number>) => {
state.curTab = action.payload;
},
resetRegisterForm: () => initialState,
},
});
// Action creators are generated for each case reducer function
export const {
setLicense,
setSubnetPassword,
setSubnetEmail,
setSubnetMFAToken,
setSubnetOTP,
setSubnetAccessToken,
setSelectedSubnetOrganization,
setSubnetRegToken,
setSubnetOrganizations,
setShowPassword,
setLoading,
setLoadingLicenseInfo,
setClusterRegistered,
setLicenseInfo,
setCurTab,
resetRegisterForm,
} = registerSlice.actions;
export default registerSlice.reducer;

View File

@@ -0,0 +1,211 @@
// This file is part of MinIO Console Server
// Copyright (c) 2023 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 {
resetRegisterForm,
setClusterRegistered,
setLicenseInfo,
setLoading,
setLoadingLicenseInfo,
setSelectedSubnetOrganization,
setSubnetAccessToken,
setSubnetMFAToken,
setSubnetOrganizations,
setSubnetOTP,
} from "./registerSlice";
import api from "../../../common/api";
import {
SubnetInfo,
SubnetLoginRequest,
SubnetLoginResponse,
SubnetLoginWithMFARequest,
SubnetRegisterRequest,
} from "../License/types";
import { ErrorResponseHandler } from "../../../common/types";
import { setErrorSnackMessage } from "../../../systemSlice";
import { createAsyncThunk } from "@reduxjs/toolkit";
import { AppState } from "../../../store";
import { hasPermission } from "../../../common/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_PAGES_PERMISSIONS,
} from "../../../common/SecureComponent/permissions";
export const fetchLicenseInfo = createAsyncThunk(
"register/fetchLicenseInfo",
async (_, { getState, dispatch }) => {
const state = getState() as AppState;
const getSubnetInfo = hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.LICENSE],
true
);
const loadingLicenseInfo = state.register.loadingLicenseInfo;
if (loadingLicenseInfo) {
return;
}
if (getSubnetInfo) {
dispatch(setLoadingLicenseInfo(true));
api
.invoke("GET", `/api/v1/subnet/info`)
.then((res: SubnetInfo) => {
dispatch(setLicenseInfo(res));
dispatch(setClusterRegistered(true));
dispatch(setLoadingLicenseInfo(false));
})
.catch((err: ErrorResponseHandler) => {
if (
err.detailedError.toLowerCase() !==
"License is not present".toLowerCase() &&
err.detailedError.toLowerCase() !==
"license not found".toLowerCase()
) {
dispatch(setErrorSnackMessage(err));
}
dispatch(setClusterRegistered(false));
dispatch(setLoadingLicenseInfo(false));
});
} else {
dispatch(setLoadingLicenseInfo(false));
}
}
);
export interface ClassRegisterArgs {
token: string;
account_id: string;
}
export const callRegister = createAsyncThunk(
"register/callRegister",
async (args: ClassRegisterArgs, { dispatch }) => {
const request: SubnetRegisterRequest = {
token: args.token,
account_id: args.account_id,
};
api
.invoke("POST", "/api/v1/subnet/register", request)
.then(() => {
dispatch(setLoading(false));
dispatch(resetRegisterForm());
dispatch(fetchLicenseInfo());
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
dispatch(setLoading(false));
});
}
);
export const subnetLoginWithMFA = createAsyncThunk(
"register/subnetLoginWithMFA",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const subnetEmail = state.register.subnetEmail;
const subnetMFAToken = state.register.subnetMFAToken;
const subnetOTP = state.register.subnetOTP;
const loading = state.register.loading;
if (loading) {
return;
}
dispatch(setLoading(true));
const request: SubnetLoginWithMFARequest = {
username: subnetEmail,
otp: subnetOTP,
mfa_token: subnetMFAToken,
};
api
.invoke("POST", "/api/v1/subnet/login/mfa", request)
.then((resp: SubnetLoginResponse) => {
dispatch(setLoading(false));
if (resp && resp.access_token && resp.organizations.length > 0) {
if (resp.organizations.length === 1) {
dispatch(
callRegister({
token: resp.access_token,
account_id: resp.organizations[0].accountId.toString(),
})
);
} else {
dispatch(setSubnetAccessToken(resp.access_token));
dispatch(setSubnetOrganizations(resp.organizations));
dispatch(
setSelectedSubnetOrganization(
resp.organizations[0].accountId.toString()
)
);
}
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
dispatch(setLoading(false));
dispatch(setSubnetOTP(""));
});
}
);
export const subnetLogin = createAsyncThunk(
"register/subnetLogin",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const license = state.register.license;
const subnetPassword = state.register.subnetPassword;
const subnetEmail = state.register.subnetEmail;
const loading = state.register.loading;
if (loading) {
return;
}
dispatch(setLoading(true));
let request: SubnetLoginRequest = {
username: subnetEmail,
password: subnetPassword,
apiKey: license,
};
api
.invoke("POST", "/api/v1/subnet/login", request)
.then((resp: SubnetLoginResponse) => {
dispatch(setLoading(false));
if (resp && resp.registered) {
dispatch(resetRegisterForm());
dispatch(fetchLicenseInfo());
} else if (resp && resp.mfa_token) {
dispatch(setSubnetMFAToken(resp.mfa_token));
} else if (resp && resp.access_token && resp.organizations.length > 0) {
dispatch(setSubnetAccessToken(resp.access_token));
dispatch(setSubnetOrganizations(resp.organizations));
dispatch(
setSelectedSubnetOrganization(
resp.organizations[0].accountId.toString()
)
);
}
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
dispatch(setLoading(false));
dispatch(resetRegisterForm());
});
}
);

View File

@@ -58,7 +58,7 @@ export const ClusterRegistered = ({ email }: { email: string }) => {
);
};
export const ProxyConfiguration = ({ linkClass }: { linkClass: string }) => {
export const ProxyConfiguration = () => {
const proxyConfigurationCommand =
"mc admin config set {alias} subnet proxy={proxy}";
const [displaySubnetProxy, setDisplaySubnetProxy] = useState(false);
@@ -104,7 +104,10 @@ export const ProxyConfiguration = ({ linkClass }: { linkClass: string }) => {
>
For airgap/firewalled environments it is possible to{" "}
<Link
className={linkClass}
style={{
color: "#2781B0",
cursor: "pointer",
}}
href="https://min.io/docs/minio/linux/reference/minio-mc-admin/mc-admin-config.html?ref=con"
target="_blank"
>

View File

@@ -37,6 +37,7 @@ import editTenantAuditLoggingReducer from "./screens/Console/Tenants/TenantDetai
import editTenantSecurityContextReducer from "./screens/Console/Tenants/tenantSecurityContextSlice";
import directPVReducer from "./screens/Console/DirectPV/directPVSlice";
import licenseReducer from "./screens/Console/License/licenseSlice";
import registerReducer from "./screens/Console/Support/registerSlice";
const rootReducer = combineReducers({
system: systemReducer,
@@ -50,6 +51,7 @@ const rootReducer = combineReducers({
objectBrowser: objectBrowserReducer,
healthInfo: healthInfoReducer,
dashboard: dashboardReducer,
register: registerReducer,
// Operator Reducers
tenants: tenantsReducer,
createTenant: createTenantReducer,

View File

@@ -62,7 +62,7 @@ func registerSubnetHandlers(api *operations.ConsoleAPI) {
})
// Get subnet info
api.SubnetSubnetInfoHandler = subnetApi.SubnetInfoHandlerFunc(func(params subnetApi.SubnetInfoParams, session *models.Principal) middleware.Responder {
resp, err := GetSubnetInfoResponse(params)
resp, err := GetSubnetInfoResponse(session, params)
if err != nil {
return subnetApi.NewSubnetInfoDefault(int(err.Code)).WithPayload(err)
}
@@ -318,14 +318,47 @@ func subnetRegisterResponse(ctx context.Context, minioClient MinioAdmin, params
return nil
}
func GetSubnetInfoResponse(params subnetApi.SubnetInfoParams) (*models.License, *models.Error) {
var ErrSubnetLicenseNotFound = errors.New("license not found")
func GetSubnetInfoResponse(session *models.Principal, params subnetApi.SubnetInfoParams) (*models.License, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
client := &xhttp.Client{
Client: GetConsoleHTTPClient(""),
}
// license gets seeded to us by MinIO
seededLicense := os.Getenv(EnvSubnetLicense)
// if it's missing, we will gracefully fallback to attempt to fetch it from MinIO
if seededLicense == "" {
mAdmin, err := NewMinioAdminClient(session)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
adminClient := AdminClient{Client: mAdmin}
licenseInfo, err := subnet.ParseLicense(client, os.Getenv(EnvSubnetLicense))
configBytes, err := adminClient.getConfigKV(params.HTTPRequest.Context(), "subnet")
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
subSysConfigs, err := madmin.ParseServerConfigOutput(string(configBytes))
if err != nil {
return nil, ErrorWithContext(ctx, err)
}
// search for licese
for _, v := range subSysConfigs {
for _, sv := range v.KV {
if sv.Key == "license" {
seededLicense = sv.Value
}
}
}
}
// still empty means not found
if seededLicense == "" {
return nil, ErrorWithContext(ctx, ErrSubnetLicenseNotFound)
}
licenseInfo, err := subnet.ParseLicense(client, seededLicense)
if err != nil {
return nil, ErrorWithContext(ctx, err)
}