Add bucket replication screen (#3040)

This commit is contained in:
jinapurapu
2024-02-26 13:44:55 -08:00
committed by GitHub
parent 31056e12ba
commit 54c0b4b8a2
6 changed files with 584 additions and 7 deletions

View File

@@ -1743,10 +1743,9 @@ export class HttpClient<SecurityDataType = unknown> {
? { "Content-Type": type }
: {}),
},
signal:
(cancelToken
? this.createAbortSignal(cancelToken)
: requestParams.signal) || null,
signal: cancelToken
? this.createAbortSignal(cancelToken)
: requestParams.signal,
body:
typeof body === "undefined" || body === null
? null

View File

@@ -139,6 +139,7 @@ export const IAM_PAGES = {
/* Buckets */
BUCKETS: "/buckets",
ADD_BUCKETS: "add-bucket",
BUCKETS_ADD_REPLICATION: "/buckets/add-replication",
BUCKETS_ADMIN_VIEW: ":bucketName/admin/*",
BUCKETS_EDIT_REPLICATION: "/buckets/edit-replication",
/* Object Browser */
@@ -296,6 +297,9 @@ export const IAM_PAGES_PERMISSIONS = {
[IAM_PAGES.BUCKETS_EDIT_REPLICATION]: [
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // edit bucket replication bucket page
],
[IAM_PAGES.BUCKETS_ADD_REPLICATION]: [
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // add bucket replication rule
],
[IAM_PAGES.BUCKETS_ADMIN_VIEW]: [
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // bucket admin page
],

View File

@@ -0,0 +1,473 @@
// 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, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
BackLink,
Box,
BucketReplicationIcon,
Button,
FormLayout,
Grid,
HelpBox,
InputBox,
PageLayout,
Select,
Switch,
} from "mds";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
import HelpMenu from "../../HelpMenu";
import { api } from "api";
import { errorToHandler } from "api/errors";
import QueryMultiSelector from "screens/Console/Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
import { getBytes, k8sScalarUnitsExcluding } from "common/utils";
import get from "lodash/get";
import InputUnitMenu from "screens/Console/Common/FormComponents/InputUnitMenu/InputUnitMenu";
const AddBucketReplication = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
let params = new URLSearchParams(document.location.search);
const bucketName = params.get("bucketName") || "";
const nextPriority = params.get("nextPriority") || "1";
const [addLoading, setAddLoading] = useState<boolean>(false);
const [priority, setPriority] = useState<string>(nextPriority);
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [targetURL, setTargetURL] = useState<string>("");
const [targetStorageClass, setTargetStorageClass] = useState<string>("");
const [prefix, setPrefix] = useState<string>("");
const [targetBucket, setTargetBucket] = useState<string>("");
const [region, setRegion] = useState<string>("");
const [useTLS, setUseTLS] = useState<boolean>(true);
const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(true);
const [repDelete, setRepDelete] = useState<boolean>(true);
const [metadataSync, setMetadataSync] = useState<boolean>(true);
const [repExisting, setRepExisting] = useState<boolean>(false);
const [tags, setTags] = useState<string>("");
const [replicationMode, setReplicationMode] = useState<"async" | "sync">(
"async",
);
const [bandwidthScalar, setBandwidthScalar] = useState<string>("100");
const [bandwidthUnit, setBandwidthUnit] = useState<string>("Gi");
const [healthCheck, setHealthCheck] = useState<string>("60");
const [validated, setValidated] = useState<boolean>(false);
const backLink = IAM_PAGES.BUCKETS + `/${bucketName}/admin/replication`;
useEffect(() => {
dispatch(setHelpName("bucket-replication-add"));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const addRecord = () => {
const replicate = [
{
originBucket: bucketName,
destinationBucket: targetBucket,
},
];
const hc = parseInt(healthCheck);
const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`;
const remoteBucketsInfo = {
accessKey: accessKey,
secretKey: secretKey,
targetURL: endURL,
region: region,
bucketsRelation: replicate,
syncMode: replicationMode,
bandwidth:
replicationMode === "async"
? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true))
: 0,
healthCheckPeriod: hc,
prefix: prefix,
tags: tags,
replicateDeleteMarkers: repDeleteMarker,
replicateDeletes: repDelete,
replicateExistingObjects: repExisting,
priority: parseInt(priority),
storageClass: targetStorageClass,
replicateMetadata: metadataSync,
};
api.bucketsReplication
.setMultiBucketReplication(remoteBucketsInfo)
.then((res) => {
setAddLoading(false);
const states = get(res.data, "replicationState", []);
if (states.length > 0) {
const itemVal = states[0];
setAddLoading(false);
if (itemVal.errorString && itemVal.errorString !== "") {
dispatch(
setErrorSnackMessage({
errorMessage: itemVal.errorString,
detailedError: "There was an error",
}),
);
// navigate(backLink);
return;
}
navigate(backLink);
return;
}
dispatch(
setErrorSnackMessage({
errorMessage: "No changes applied",
detailedError: "",
}),
);
})
.catch((err) => {
console.log("this is an error!");
setAddLoading(false);
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
});
};
useEffect(() => {
!validated &&
accessKey.length >= 3 &&
secretKey.length >= 8 &&
targetBucket.length >= 3 &&
targetURL.length > 0 &&
setValidated(true);
}, [targetURL, accessKey, secretKey, targetBucket, validated]);
useEffect(() => {
if (
validated &&
(accessKey.length < 3 ||
secretKey.length < 8 ||
targetBucket.length < 3 ||
targetURL.length < 1)
) {
setValidated(false);
}
}, [targetURL, accessKey, secretKey, targetBucket, validated]);
return (
<Fragment>
<PageHeaderWrapper
label={
<BackLink
label={"Add Bucket Replication Rule - " + bucketName}
onClick={() => navigate(backLink)}
/>
}
actions={<HelpMenu />}
/>
<PageLayout>
<FormLayout
title="Add Replication Rule"
icon={<BucketReplicationIcon />}
helpBox={
<HelpBox
iconComponent={<BucketReplicationIcon />}
title="Bucket Replication Configuration"
help={
<Fragment>
<Box sx={{ paddconngTop: "10px" }}>
The bucket selected in this deployment acts as the source
while the configured remote deployment acts as the target.
</Box>
<Box sx={{ paddingTop: "10px" }}>
For each write operation to this "source" bucket, MinIO
checks all configured replication rules and applies the
matching rule with highest configured priority.
</Box>
<Box sx={{ paddingTop: "10px" }}>
MinIO supports automatically replicating existing objects in
a bucket, however it does not enable existing object
replication by default. Objects created before replication
was configured or while replication is disabled are not
synchronized to the target deployment unless replication of
existing objects is enabled.
</Box>
<Box sx={{ paddingTop: "10px" }}>
MinIO supports replicating delete operations, where MinIO
synchronizes deleting specific object versions and new
delete markers. Delete operation replication uses the same
replication process as all other replication operations.
</Box>{" "}
</Fragment>
}
/>
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddLoading(true);
addRecord();
}}
>
<InputBox
id="priority"
name="priority"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setPriority(e.target.value);
}
}}
label="Priority"
value={priority}
pattern={"[0-9]*"}
/>
<InputBox
id="targetURL"
name="targetURL"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetURL(e.target.value);
}}
placeholder="play.min.io"
label="Target URL"
value={targetURL}
/>
<Switch
checked={useTLS}
id="useTLS"
name="useTLS"
label="Use TLS"
onChange={(e) => {
setUseTLS(e.target.checked);
}}
value="yes"
/>
<InputBox
id="accessKey"
name="accessKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
<InputBox
id="secretKey"
name="secretKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
<InputBox
id="targetBucket"
name="targetBucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetBucket(e.target.value);
}}
label="Target Bucket"
value={targetBucket}
/>
<InputBox
id="region"
name="region"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setRegion(e.target.value);
}}
label="Region"
value={region}
/>
<Select
id="replication_mode"
name="replication_mode"
onChange={(value) => {
setReplicationMode(value as "async" | "sync");
}}
label="Replication Mode"
value={replicationMode}
options={[
{ label: "Asynchronous", value: "async" },
{ label: "Synchronous", value: "sync" },
]}
/>
{replicationMode === "async" && (
<Box className={"inputItem"}>
<InputBox
type="number"
id="bandwidth_scalar"
name="bandwidth_scalar"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.validity.valid) {
setBandwidthScalar(e.target.value as string);
}
}}
label="Bandwidth"
value={bandwidthScalar}
min="0"
pattern={"[0-9]*"}
overlayObject={
<InputUnitMenu
id={"quota_unit"}
onUnitChange={(newValue) => {
setBandwidthUnit(newValue);
}}
unitSelected={bandwidthUnit}
unitsList={k8sScalarUnitsExcluding(["Ki"])}
disabled={false}
/>
}
/>
</Box>
)}
<InputBox
id="healthCheck"
name="healthCheck"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setHealthCheck(e.target.value as string);
}}
label="Health Check Duration"
value={healthCheck}
/>
<InputBox
id="storageClass"
name="storageClass"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setTargetStorageClass(e.target.value);
}}
placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc"
label="Storage Class"
value={targetStorageClass}
/>
<fieldset className={"inputItem"}>
<legend>Object Filters</legend>
<InputBox
id="prefix"
name="prefix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setPrefix(e.target.value);
}}
placeholder="prefix"
label="Prefix"
value={prefix}
/>
<QueryMultiSelector
name="tags"
label="Tags"
elements={""}
onChange={(vl: string) => {
setTags(vl);
}}
keyPlaceholder="Tag Key"
valuePlaceholder="Tag Value"
withBorder
/>
</fieldset>
<fieldset className={"inputItem"}>
<legend>Replication Options</legend>
<Switch
checked={repExisting}
id="repExisting"
name="repExisting"
label="Existing Objects"
onChange={(e) => {
setRepExisting(e.target.checked);
}}
description={"Replicate existing objects"}
/>
<Switch
checked={metadataSync}
id="metadatataSync"
name="metadatataSync"
label="Metadata Sync"
onChange={(e) => {
setMetadataSync(e.target.checked);
}}
description={"Metadata Sync"}
/>
<Switch
checked={repDeleteMarker}
id="deleteMarker"
name="deleteMarker"
label="Delete Marker"
onChange={(e) => {
setRepDeleteMarker(e.target.checked);
}}
description={"Replicate soft deletes"}
/>
<Switch
checked={repDelete}
id="repDelete"
name="repDelete"
label="Deletes"
onChange={(e) => {
setRepDelete(e.target.checked);
}}
description={"Replicate versioned deletes"}
/>
</fieldset>
<Grid
item
xs={12}
sx={{
display: "flex",
flexDirection: "row",
justifyContent: "end",
gap: 10,
paddingTop: 10,
}}
>
<Button
id={"cancel"}
type="button"
variant="regular"
disabled={addLoading}
onClick={() => {
navigate(backLink);
}}
label={"Cancel"}
/>
<Button
id={"submit"}
type="submit"
variant="callAction"
color="primary"
disabled={addLoading || !validated}
label={"Save"}
/>
</Grid>
</form>
</FormLayout>
</PageLayout>
</Fragment>
);
};
export default AddBucketReplication;

View File

@@ -40,7 +40,10 @@ import {
hasPermission,
SecureComponent,
} from "../../../../common/SecureComponent";
import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions";
import {
IAM_PAGES,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
import { selBucketDetailsLoading } from "./bucketDetailsSlice";
import { useAppDispatch } from "../../../../store";
@@ -83,7 +86,6 @@ const BucketReplicationPanel = () => {
IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
IAM_SCOPES.S3_GET_ACTIONS,
]);
useEffect(() => {
dispatch(setHelpName("bucket_detail_replication"));
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -293,7 +295,12 @@ const BucketReplicationPanel = () => {
<Button
id={"add-bucket-replication-rule"}
onClick={() => {
setOpenReplicationOpen(true);
navigate(
IAM_PAGES.BUCKETS_ADD_REPLICATION +
`?bucketName=${bucketName}&nextPriority=${
replicationRules.length + 1
}`,
);
}}
label={"Add Replication Rule"}
icon={<AddIcon />}

View File

@@ -95,6 +95,9 @@ const Buckets = React.lazy(() => import("./Buckets/Buckets"));
const EditBucketReplication = React.lazy(
() => import("./Buckets/BucketDetails/EditBucketReplication"),
);
const AddBucketReplication = React.lazy(
() => import("./Buckets/BucketDetails/AddBucketReplication"),
);
const Policies = React.lazy(() => import("./Policies/Policies"));
const AddPolicyScreen = React.lazy(() => import("./Policies/AddPolicyScreen"));
@@ -239,6 +242,16 @@ const Console = () => {
return hasPermission("*", IAM_PAGES_PERMISSIONS[IAM_PAGES.ADD_BUCKETS]);
},
},
{
component: AddBucketReplication,
path: IAM_PAGES.BUCKETS_ADD_REPLICATION,
customPermissionFnc: () => {
return hasPermission(
"*",
IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_ADD_REPLICATION],
);
},
},
{
component: EditBucketReplication,
path: IAM_PAGES.BUCKETS_EDIT_REPLICATION,

View File

@@ -5104,5 +5104,86 @@
}
]
}
},
"bucket-replication-add": {
"docs": {
"header": null,
"links": [
{
"img": "https://min.io/resources/img/logo/MINIO_wordmark.png",
"title": "Bucket Replication Requirements",
"url": "https://min.io/docs/minio/kubernetes/upstream/administration/bucket-replication/bucket-replication-requirements.html",
"body": "Check here to ensure you meet the prerequisites before setting up any replication configurations."
},
{
"img": "https://min.io/resources/img/logo/MINIO_wordmark.png",
"title": "One-way Bucket Replication",
"url": " https://min.io/docs/minio/kubernetes/upstream/administration/bucket-replication/enable-server-side-one-way-bucket-replication.html#",
"body": "Guidance on configuring one-way Bucket replication using MinIO Console."
},
{
"img": "https://min.io/resources/img/logo/MINIO_wordmark.png",
"title": "Two-way Bucket Replication",
"url": " https://min.io/docs/minio/kubernetes/upstream/administration/bucket-replication/enable-server-side-two-way-bucket-replication.html#minio-bucket-replication-serverside-twoway",
"body": "Guidance on configuring two-way (active-active) Bucket replication using MinIO Console."
},
{
"img": "https://min.io/resources/img/logo/MINIO_wordmark.png",
"title": "Bucket Replication",
"url": "https://min.io/docs/minio/kubernetes/upstream/administration/bucket-replication.html#minio-bucket-replication-serverside",
"body": "MinIO server-side bucket replication is an automatic bucket-level configuration that synchronizes objects between a source and destination bucket."
},
{
"img": "https://min.io/resources/img/logo/MINIO_wordmark.png",
"title": "Replication Internals",
"url": "https://min.io/docs/minio/windows/administration/bucket-replication.html#minio-replication-process",
"body": "Learn details of the MinIO replication process."
},
{
"img": "https://min.io/resources/img/logo/MINIO_wordmark.png",
"title": "SUBNET Registration and Support",
"url": "https://min.io/docs/minio/linux/administration/console/subnet-registration.html",
"body": "Learn how to register your MinIO deployment and access our SUBNET support system"
},
{
"img": "https://blog.min.io/content/images/size/w1000/2020/12/pay_banner-01-01-01-01-01.png",
"title": "Troubleshooting",
"url": "https://min.io/docs/minio/linux/operations/troubleshooting.html",
"body": "Need more help? Check out additional Troubleshooting options"
}
]
},
"video": {
"header": null,
"links": [
{
"img": "https://i.ytimg.com/vi/89vnToCcoAw/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAxsMWCpBYqwqEy0xULLLdcIVKkeA",
"title": "Replication Lab I",
"url": "https://www.youtube.com/watch?v=89vnToCcoAw",
"body": "Demonstrates bucket replication concepts using MinIO Client, including active-passive and active-active replication."
},
{
"img": "https://i.ytimg.com/vi/G4wQZEsIxcU/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBTPmOjC6YVBAmuyHwPK1OMfTrXoA",
"title": "Bucket Replication Overview",
"url": "https://www.youtube.com/watch?v=G4wQZEsIxcU",
"body": "In this video, we will cover bucket level replication, both active-passive and active-active."
}
]
},
"blog": {
"header": null,
"links": [
{
"img": "https://blog.min.io/content/images/size/w1000/2022/12/replication-bestpractices.jpg",
"title": "Replication Best Practices",
"url": "https://blog.min.io/minio-replication-best-practices/",
"body": "A detailed tutorial guiding you through setting up MinIO with a well-configured replication architecture."
}
]
}
}
}