Site replication status (#1834)

Site replication status UI
Site replication status ui-test
Address review comment by Alex
Add functional test for API
Add integration tests for status API
This commit is contained in:
Prakash Senthil Vel
2022-04-14 07:21:43 +00:00
committed by GitHub
parent 4541b4de03
commit d1d3d91fc1
29 changed files with 2365 additions and 110 deletions

View File

@@ -0,0 +1,97 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// SiteReplicationStatusResponse site replication status response
//
// swagger:model siteReplicationStatusResponse
type SiteReplicationStatusResponse struct {
// bucket stats
BucketStats interface{} `json:"bucketStats,omitempty"`
// enabled
Enabled bool `json:"enabled,omitempty"`
// group stats
GroupStats interface{} `json:"groupStats,omitempty"`
// max buckets
MaxBuckets int64 `json:"maxBuckets,omitempty"`
// max groups
MaxGroups int64 `json:"maxGroups,omitempty"`
// max policies
MaxPolicies int64 `json:"maxPolicies,omitempty"`
// max users
MaxUsers int64 `json:"maxUsers,omitempty"`
// policy stats
PolicyStats interface{} `json:"policyStats,omitempty"`
// sites
Sites interface{} `json:"sites,omitempty"`
// stats summary
StatsSummary interface{} `json:"statsSummary,omitempty"`
// user stats
UserStats interface{} `json:"userStats,omitempty"`
}
// Validate validates this site replication status response
func (m *SiteReplicationStatusResponse) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this site replication status response based on context it is used
func (m *SiteReplicationStatusResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *SiteReplicationStatusResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *SiteReplicationStatusResponse) UnmarshalBinary(b []byte) error {
var res SiteReplicationStatusResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -162,6 +162,7 @@ export const IAM_PAGES = {
TIERS_ADD: "/settings/tiers/add",
TIERS_ADD_SERVICE: "/settings/tiers/add/:service",
SITE_REPLICATION: "/settings/site-replication",
SITE_REPLICATION_STATUS: "/settings/site-replication/status",
/* Operator */
TENANTS: "/tenants",
@@ -185,7 +186,7 @@ export const IAM_PAGES = {
NAMESPACE_TENANT_POOLS_ADD:
"/namespaces/:tenantNamespace/tenants/:tenantName/add-pool",
NAMESPACE_TENANT_POOLS_EDIT:
"/namespaces/:tenantNamespace/tenants/:tenantName/edit-pool",
"/namespaces/:tenantNamespace/tenants/:tenantName/edit-pool",
NAMESPACE_TENANT_VOLUMES:
"/namespaces/:tenantNamespace/tenants/:tenantName/volumes",
NAMESPACE_TENANT_LICENSE:
@@ -384,7 +385,11 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
};
[IAM_PAGES.SITE_REPLICATION_STATUS]: [
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
}
export const S3_ALL_RESOURCES = "arn:aws:s3:::*";
export const CONSOLE_UI_RESOURCE = "console-ui";

View File

@@ -28,6 +28,7 @@ type DeleteButtonProps = {
tooltip?: string;
classes?: any;
icon?: React.ReactNode;
showLabelAlways?:boolean;
[x: string]: any;
};
@@ -80,6 +81,7 @@ const RBIconButton = (props: RBIconProps) => {
tooltip,
icon = null,
className = "",
showLabelAlways=false,
...restProps
} = props;
@@ -98,7 +100,8 @@ const RBIconButton = (props: RBIconProps) => {
"& span": {
fontSize: 14,
"@media (max-width: 900px)": {
display: "none",
display: showLabelAlways?"inline":"none",
marginRight:showLabelAlways?"7px":""
},
},
}}

View File

@@ -0,0 +1,210 @@
// 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, { useState } from "react";
import { Box, Grid } from "@mui/material";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import { ClustersIcon } from "../../../../icons";
import useApi from "../../Common/Hooks/useApi";
import { StatsResponseType } from "./SiteReplicationStatus";
import BucketEntityStatus from "./LookupStatus/BucketEntityStatus";
import Loader from "../../Common/Loader/Loader";
import PolicyEntityStatus from "./LookupStatus/PolicyEntityStatus";
import GroupEntityStatus from "./LookupStatus/GroupEntityStatus";
import UserEntityStatus from "./LookupStatus/UserEntityStatus";
const EntityReplicationLookup = () => {
const [entityType, setEntityType] = useState<string>("bucket");
const [entityValue, setEntityValue] = useState<string>("");
const [stats, setStats] = useState<StatsResponseType>({});
const [statsLoaded, setStatsLoaded] = useState<boolean>(false);
const [isStatsLoading, invokeSiteStatsApi] = useApi(
(res: any) => {
setStats(res);
setStatsLoaded(true);
},
(err: any) => {
setStats({});
setStatsLoaded(true);
}
);
const {
bucketStats = {},
sites = {},
userStats = {},
policyStats = {},
groupStats = {},
} = stats || {};
const getStats = (entityType: string = "", entityValue: string = "") => {
setStatsLoaded(false);
if (entityType && entityValue) {
let url = `api/v1/admin/site-replication/status?buckets=false&entityType=${entityType}&entityValue=${entityValue}&groups=false&policies=false&users=false`;
invokeSiteStatsApi("GET", url);
}
};
return (
<Box>
<Box
sx={{
display: "grid",
alignItems: "center",
gridTemplateColumns: {
md: ".7fr .9fr 1.2fr .3fr",
sm: "1.2fr .7fr .7fr .3fr",
xs: "1fr",
},
gap: "15px",
}}
>
<Box sx={{ width: "240px", flexGrow: "0" }}>
View Replication Status for a:
</Box>
<Box
sx={{
marginLeft: {
md: "-25px",
xs: "0px",
},
}}
>
<SelectWrapper
id="replicationEntityLookup"
name="replicationEntityLookup"
onChange={(e) => {
setEntityType(e.target.value);
setStatsLoaded(false);
}}
label=""
value={entityType}
options={[
{
label: "Bucket",
value: "bucket",
},
{
label: "User",
value: "user",
},
{
label: "Group",
value: "group",
},
{
label: "Policy",
value: "policy",
},
]}
disabled={false}
/>
</Box>
<Box
sx={{
flex: 2,
}}
>
<InputBoxWrapper
id="replicationLookupEntityValue"
name="replicationLookupEntityValue"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setEntityValue(e.target.value);
setStatsLoaded(false);
}}
placeholder={`test-${entityType}`}
label=""
value={entityValue}
/>
</Box>
<Box
sx={{
maxWidth: "80px",
}}
>
<RBIconButton
type={"button"}
onClick={() => {
getStats(entityType, entityValue);
}}
text={`View `}
tooltip={"View across sites"}
icon={<ClustersIcon />}
color={"primary"}
showLabelAlways
disabled={!entityValue || !entityType}
/>
</Box>
</Box>
{isStatsLoading ? (
<Grid
item
xs={12}
display={"flex"}
alignItems={"center"}
justifyContent={"center"}
marginTop={"45px"}
>
<Loader style={{ width: 25, height: 25 }} />
</Grid>
) : null}
{statsLoaded ? (
<Box>
{!isStatsLoading && entityType === "bucket" && entityValue ? (
<BucketEntityStatus
bucketStats={bucketStats}
sites={sites}
lookupValue={entityValue}
/>
) : null}
{!isStatsLoading && entityType === "user" && entityValue ? (
<UserEntityStatus
userStats={userStats}
sites={sites}
lookupValue={entityValue}
/>
) : null}
{!isStatsLoading && entityType === "group" && entityValue ? (
<GroupEntityStatus
groupStats={groupStats}
sites={sites}
lookupValue={entityValue}
/>
) : null}
{!isStatsLoading && entityType === "policy" && entityValue ? (
<PolicyEntityStatus
policyStats={policyStats}
sites={sites}
lookupValue={entityValue}
/>
) : null}
</Box>
) : null}
</Box>
);
};
export default EntityReplicationLookup;

View File

@@ -0,0 +1,129 @@
// 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 from "react";
import { StatsResponseType } from "../SiteReplicationStatus";
import LookupStatusTable from "./LookupStatusTable";
import { EntityNotFound, isEntityNotFound, syncStatus } from "./Utils";
type BucketEntityStatusProps = Partial<StatsResponseType> & {
lookupValue?: string;
};
const BucketEntityStatus = ({
bucketStats = {},
sites = {},
lookupValue = "",
}: BucketEntityStatusProps) => {
const rowsForStatus = [
"Tags",
"Policy",
"Quota",
"Retention",
"Encryption",
"Replication",
];
const bucketSites: Record<string, any> = bucketStats[lookupValue] || {};
if (!lookupValue) return null;
const siteKeys = Object.keys(sites);
const notFound = isEntityNotFound(sites, bucketSites, "HasBucket");
const resultMatrix: any = [];
if (notFound) {
return <EntityNotFound entityType={"Bucket"} entityValue={lookupValue} />;
} else {
const row = [];
for (let sCol = 0; sCol < siteKeys.length; sCol++) {
if (sCol === 0) {
row.push("");
}
/**
* ----------------------------------
* | <blank cell> | sit-0 | site-1 |
* -----------------------------------
*/
row.push(sites[siteKeys[sCol]].name);
}
resultMatrix.push(row);
for (let fi = 0; fi < rowsForStatus.length; fi++) {
/**
* -------------------------------------------------
* | Feature Name | site-0-status | site-1-status |
* --------------------------------------------------
*/
const sfRow = [];
const feature = rowsForStatus[fi];
let sbStatus: string | boolean = "";
for (let si = 0; si < siteKeys.length; si++) {
const bucketSiteDeploymentId = sites[siteKeys[si]].deploymentID;
const rSite = bucketSites[bucketSiteDeploymentId];
if (si === 0) {
sfRow.push(feature);
}
switch (fi) {
case 0:
sbStatus = syncStatus(rSite.TagMismatch, rSite.HasTagsSet);
sfRow.push(sbStatus);
break;
case 1:
sbStatus = syncStatus(rSite.PolicyMismatch, rSite.HasPolicySet);
sfRow.push(sbStatus);
break;
case 2:
sbStatus = syncStatus(rSite.QuotaCfgMismatch, rSite.HasQuotaCfgSet);
sfRow.push(sbStatus);
break;
case 3:
sbStatus = syncStatus(
rSite.OLockConfigMismatch,
rSite.HasOLockConfigSet
);
sfRow.push(sbStatus);
break;
case 4:
sbStatus = syncStatus(rSite.SSEConfigMismatch, rSite.HasSSECfgSet);
sfRow.push(sbStatus);
break;
case 5:
sbStatus = syncStatus(
rSite.ReplicationCfgMismatch,
rSite.HasReplicationCfg
);
sfRow.push(sbStatus);
break;
}
}
resultMatrix.push(sfRow);
}
}
return (
<LookupStatusTable
matrixData={resultMatrix}
entityName={lookupValue}
entityType={"Bucket"}
/>
);
};
export default BucketEntityStatus;

View File

@@ -0,0 +1,29 @@
import React from "react";
import { Box } from "@mui/material";
const EntityNotFound = ({
entityType,
entityValue,
}: {
entityType: string;
entityValue: string;
}) => {
return (
<Box
sx={{
marginTop: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{entityType}:{" "}
<Box sx={{ marginLeft: "5px", marginRight: "5px", fontWeight: 600 }}>
{entityValue}
</Box>{" "}
not found.
</Box>
);
};
export default EntityNotFound;

View File

@@ -0,0 +1,99 @@
// 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 from "react";
import { StatsResponseType } from "../SiteReplicationStatus";
import LookupStatusTable from "./LookupStatusTable";
import { EntityNotFound, isEntityNotFound, syncStatus } from "./Utils";
type GroupEntityStatusProps = Partial<StatsResponseType> & {
lookupValue?: string;
};
const UserEntityStatus = ({
groupStats = {},
sites = {},
lookupValue = "",
}: GroupEntityStatusProps) => {
const rowsForStatus = ["Info", "Policy mapping"];
const groupSites: Record<string, any> = groupStats[lookupValue] || {};
if (!lookupValue) return null;
const siteKeys = Object.keys(sites);
const notFound = isEntityNotFound(sites, groupSites, "HasGroup");
const resultMatrix: any = [];
if (notFound) {
return <EntityNotFound entityType={"Group"} entityValue={lookupValue} />;
} else {
const row = [];
for (let sCol = 0; sCol < siteKeys.length; sCol++) {
if (sCol === 0) {
row.push("");
}
/**
* ----------------------------------
* | <blank cell> | sit-0 | site-1 |
* -----------------------------------
*/
row.push(sites[siteKeys[sCol]].name);
}
resultMatrix.push(row);
for (let fi = 0; fi < rowsForStatus.length; fi++) {
/**
* -------------------------------------------------
* | Feature Name | site-0-status | site-1-status |
* --------------------------------------------------
*/
const sfRow = [];
const feature = rowsForStatus[fi];
let sbStatus: string | boolean = "";
for (let si = 0; si < siteKeys.length; si++) {
const bucketSiteDeploymentId = sites[siteKeys[si]].deploymentID;
const rSite = groupSites[bucketSiteDeploymentId];
if (si === 0) {
sfRow.push(feature);
}
switch (fi) {
case 0:
sbStatus = syncStatus(rSite.GroupDescMismatch, rSite.HasGroup);
sfRow.push(sbStatus);
break;
case 1:
sbStatus = syncStatus(rSite.PolicyMismatch, rSite.HasPolicyMapping);
sfRow.push(sbStatus);
break;
}
}
resultMatrix.push(sfRow);
}
}
return (
<LookupStatusTable
matrixData={resultMatrix}
entityName={lookupValue}
entityType={"Group"}
/>
);
};
export default UserEntityStatus;

View File

@@ -0,0 +1,142 @@
// 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 from "react";
import { Box } from "@mui/material";
import { CircleIcon } from "../../../../../icons";
const LookupStatusTable = ({
matrixData = [],
entityName = "",
entityType = "",
}: {
matrixData: any;
entityName: string;
entityType: string;
}) => {
//Assumes 1st row should be a header row.
const [header = [], ...rows] = matrixData;
const tableHeader = header.map((hC: string, hcIdx: number) => {
return (
<td className="header-cell" key={`${0}${hcIdx}`}>
{hC}
</td>
);
});
const tableRowsToRender = rows.map((r: any, rIdx: number) => {
return (
<tr key={`r-${rIdx + 1}`}>
{r.map((v: any, cIdx: number) => {
let indicator = null;
if (cIdx === 0) {
indicator = v;
} else if (v === "") {
indicator = "";
}
if (v === true) {
indicator = (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
"& .min-icon": {
fill: "#4CCB92",
height: "15px",
width: "15px",
},
}}
>
<CircleIcon />
</Box>
);
} else if (v === false) {
indicator = (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
"& .min-icon": {
fill: "#C83B51",
height: "15px",
width: "15px",
},
}}
>
<CircleIcon />
</Box>
);
}
return (
<td
key={`${rIdx + 1}${cIdx}`}
className={cIdx === 0 ? "feature-cell" : "status-cell"}
>
{indicator}
</td>
);
})}
</tr>
);
});
return (
<Box
sx={{
marginTop: "15px",
table: {
width: "100%",
borderCollapse: "collapse",
"& .feature-cell": {
fontWeight: 600,
fontSize: "14px",
paddingLeft: "15px",
},
"& .status-cell": {
textAlign: "center",
},
"& .header-cell": {
textAlign: "center",
},
"& tr": {
height: "38px",
},
"tr td ": {
border: "1px solid #f1f1f1",
},
},
}}
>
<Box sx={{ marginTop: "15px", marginBottom: "15px" }}>
Replication status for {entityType}: <strong>{entityName}</strong>.
</Box>
<table>
<thead>
<tr>{tableHeader}</tr>
</thead>
<tbody>{tableRowsToRender}</tbody>
</table>
</Box>
);
};
export default LookupStatusTable;

View File

@@ -0,0 +1,85 @@
// 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 from "react";
import { StatsResponseType } from "../SiteReplicationStatus";
import LookupStatusTable from "./LookupStatusTable";
import { EntityNotFound, isEntityNotFound, syncStatus } from "./Utils";
type PolicyEntityStatusProps = Partial<StatsResponseType> & {
lookupValue?: string;
};
const PolicyEntityStatus = ({
policyStats = {},
sites = {},
lookupValue = "",
}: PolicyEntityStatusProps) => {
const rowsForStatus = ["Policy"];
const policySites: Record<string, any> = policyStats[lookupValue] || {};
if (!lookupValue) return null;
const siteKeys = Object.keys(sites);
const notFound = isEntityNotFound(sites, policySites, "HasPolicy");
const resultMatrix: any = [];
if (notFound) {
return <EntityNotFound entityType={"Policy"} entityValue={lookupValue} />;
} else {
const row = [];
for (let sCol = 0; sCol < siteKeys.length; sCol++) {
if (sCol === 0) {
row.push("");
}
row.push(sites[siteKeys[sCol]].name);
}
resultMatrix.push(row);
for (let fi = 0; fi < rowsForStatus.length; fi++) {
const sfRow = [];
const feature = rowsForStatus[fi];
let sbStatus: string | boolean = "";
for (let si = 0; si < siteKeys.length; si++) {
const bucketSiteDeploymentId = sites[siteKeys[si]].deploymentID;
const rSite = policySites[bucketSiteDeploymentId];
if (si === 0) {
sfRow.push(feature);
}
switch (fi) {
case 0:
sbStatus = syncStatus(rSite.PolicyMismatch, rSite.HasPolicy);
sfRow.push(sbStatus);
break;
}
}
resultMatrix.push(sfRow);
}
}
return (
<LookupStatusTable
matrixData={resultMatrix}
entityName={lookupValue}
entityType={"Policy"}
/>
);
};
export default PolicyEntityStatus;

View File

@@ -0,0 +1,91 @@
// 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 from "react";
import { StatsResponseType } from "../SiteReplicationStatus";
import LookupStatusTable from "./LookupStatusTable";
import { EntityNotFound, isEntityNotFound, syncStatus } from "./Utils";
type PolicyEntityStatusProps = Partial<StatsResponseType> & {
lookupValue?: string;
};
const UserEntityStatus = ({
userStats = {},
sites = {},
lookupValue = "",
}: PolicyEntityStatusProps) => {
const rowsForStatus = ["Info", "Policy mapping"];
const userSites: Record<string, any> = userStats[lookupValue] || {};
if (!lookupValue) return null;
const siteKeys = Object.keys(sites);
const notFound = isEntityNotFound(sites, userSites, "HasUser");
const resultMatrix: any = [];
if (notFound) {
return <EntityNotFound entityType={"User"} entityValue={lookupValue} />;
} else {
const row = [];
for (let sCol = 0; sCol < siteKeys.length; sCol++) {
if (sCol === 0) {
row.push("");
}
row.push(sites[siteKeys[sCol]].name);
}
resultMatrix.push(row);
for (let fi = 0; fi < rowsForStatus.length; fi++) {
const sfRow = [];
const feature = rowsForStatus[fi];
let sbStatus: string | boolean = "";
for (let si = 0; si < siteKeys.length; si++) {
const bucketSiteDeploymentId = sites[siteKeys[si]].deploymentID;
const rSite = userSites[bucketSiteDeploymentId];
if (si === 0) {
sfRow.push(feature);
}
switch (fi) {
case 0:
sbStatus = syncStatus(rSite.UserInfoMismatch, rSite.HasUser);
sfRow.push(sbStatus);
break;
case 1:
sbStatus = syncStatus(rSite.PolicyMismatch, rSite.HasPolicyMapping);
sfRow.push(sbStatus);
break;
}
}
resultMatrix.push(sfRow);
}
}
return (
<LookupStatusTable
matrixData={resultMatrix}
entityName={lookupValue}
entityType={"User"}
/>
);
};
export default UserEntityStatus;

View File

@@ -0,0 +1,49 @@
import { Box } from "@mui/material";
import React from "react";
import { StatsResponseType } from "../SiteReplicationStatus";
export function syncStatus(mismatch: boolean, set: boolean): string | boolean {
if (!set) {
return "";
}
return !mismatch;
}
export function isEntityNotFound(
sites: Partial<StatsResponseType>,
lookupList: Partial<StatsResponseType>,
lookupKey: string
) {
const siteKeys: string[] = Object.keys(sites);
return siteKeys.find((sk: string) => {
// there is no way to find the type of this ! as it is an entry in the structure itself.
// @ts-ignore
const result: Record<string, any> = lookupList[sk] || {};
return !result[lookupKey];
});
}
export const EntityNotFound = ({
entityType,
entityValue,
}: {
entityType: string;
entityValue: string;
}) => {
return (
<Box
sx={{
marginTop: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{entityType}:{" "}
<Box sx={{ marginLeft: "5px", marginRight: "5px", fontWeight: 600 }}>
{entityValue}
</Box>{" "}
not found.
</Box>
);
};

View File

@@ -15,15 +15,9 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState } from "react";
import ListSubheader from "@mui/material/ListSubheader";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import Collapse from "@mui/material/Collapse";
import { Box, Button, DialogContentText, Tooltip } from "@mui/material";
import {
MenuCollapsedIcon,
MenuExpandedIcon,
} from "../../../../icons/SidebarMenus";
import { ReplicationSite } from "./SiteReplication";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import TrashIcon from "../../../../icons/TrashIcon";
@@ -67,16 +61,10 @@ const ReplicationSites = ({
onRefresh: () => void;
classes: any;
}) => {
const [expanded, setExpanded] = React.useState<string>("");
const [deleteSiteKey, setIsDeleteSiteKey] = useState<string>("");
const [editSite, setEditSite] = useState<any>(null);
const [editEndPointName, setEditEndPointName] = useState<string>("");
const handleClick = (key: string) => {
setExpanded(key);
};
const [isEditing, invokeSiteEditApi] = useApi(
(res: any) => {
if (res.success) {
@@ -103,7 +91,6 @@ const ReplicationSites = ({
});
};
const hasExpand = false; //siteInfo.isCurrent to b
let isValidEndPointUrl = false;
try {
@@ -139,20 +126,11 @@ const ReplicationSites = ({
</Box>
{sites.map((siteInfo, index) => {
const key = `${siteInfo.name}`;
const isExpanded = expanded === siteInfo.name;
const handleToggle = () => {
if (!isExpanded) {
handleClick(key);
} else {
handleClick("");
}
};
return (
<React.Fragment key={key}>
<ListItemButton
disableRipple
className={isExpanded ? "expanded" : ""}
sx={{
display: "flex",
alignItems: "center",
@@ -274,89 +252,7 @@ const ReplicationSites = ({
}}
/>
</Box>
{hasExpand ? (
<Box
sx={{
height: "25px",
width: "25px",
background: "#FBFAFA",
borderRadius: "2px",
border: "1px solid #eaeaea",
"&:hover": {
background: "#fafafa",
},
display: {
md: "block",
xs: "none",
},
"& .collapse-icon": {
fill: "#494949",
"& g rect": {
fill: "#ffffff",
},
},
"& .expand-icon": {
fill: "#494949",
"& rect": {
fill: "#ffffff",
},
},
}}
onClick={handleToggle}
>
{isExpanded ? (
<MenuCollapsedIcon className="collapse-icon" />
) : (
<MenuExpandedIcon className="expand-icon" />
)}
</Box>
) : (
<Box
sx={{
height: "25px",
width: "25px",
}}
/>
)}
</ListItemButton>
{isExpanded ? (
<Box
key={`${siteInfo.name}`}
sx={{
border: "1px solid #f1f1f1",
borderLeft: "0",
borderRight: "0",
borderTop: "0",
}}
>
<ListSubheader
key={`${index}-drive-details`}
component="div"
sx={{ paddingLeft: "30px" }}
>
Replication status
</ListSubheader>
<Collapse
in={isExpanded}
timeout="auto"
unmountOnExit
sx={{
width: "100%",
flex: 1,
display: "flex",
padding: { md: "15px 30px", xs: "10px 10px" },
"& .MuiCollapse-wrapperInner": {
display: "flex",
flexFlow: "column",
gap: "15px",
},
}}
>
Status info
</Collapse>
</Box>
) : null}
{deleteSiteKey === key ? (
<ConfirmDialog

View File

@@ -30,6 +30,8 @@ import { setErrorSnackMessage, setSnackBarMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import HelpBox from "../../../../common/HelpBox";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import history from "../../../../history";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
export type ReplicationSite = {
deploymentID: string;
@@ -125,6 +127,17 @@ const SiteReplication = ({
setIsDeleteAll(true);
}}
/>
<RBIconButton
tooltip={"Replication Status"}
text={"Replication Status"}
variant="outlined"
color="primary"
icon={<RecoverIcon />}
onClick={(e) => {
e.preventDefault();
history.push(IAM_PAGES.SITE_REPLICATION_STATUS);
}}
/>
</Box>
) : null}
<RBIconButton

View File

@@ -0,0 +1,215 @@
// 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, { Fragment, useEffect, useState } from "react";
import PageHeader from "../../Common/PageHeader/PageHeader";
import PageLayout from "../../Common/Layout/PageLayout";
import { Box, Grid } from "@mui/material";
import useApi from "../../Common/Hooks/useApi";
import BackLink from "../../../../common/BackLink";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import ScreenTitle from "../../Common/ScreenTitle/ScreenTitle";
import StatusCountCard from "../../Dashboard/BasicDashboard/StatusCountCard";
import {
BucketsIcon,
GroupsIcon,
IAMPoliciesIcon,
RefreshIcon,
UsersIcon,
} from "../../../../icons";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import EntityReplicationLookup from "./EntityReplicationLookup";
import Loader from "../../Common/Loader/Loader";
export type StatsResponseType = {
maxBuckets?: number;
bucketStats?: Record<string, any>;
maxGroups?: number;
groupStats?: Record<string, any>;
maxUsers?: number;
userStats?: Record<string, any>;
maxPolicies?: number;
policyStats?: Record<string, any>;
sites?: Record<string, any>;
};
const SREntityStatus = ({
maxValue = 0,
entityStatObj = {},
entityTextPlural = "",
icon = null,
}: {
maxValue: number;
entityStatObj: Record<string, any>;
entityTextPlural: string;
icon?: React.ReactNode;
}) => {
const statEntityLen = Object.keys(entityStatObj || {})?.length;
return (
<Box
sx={{
border: "1px solid #f1f1f1",
padding: "25px",
maxWidth: {
sm: "100%",
},
}}
>
<StatusCountCard
icon={icon}
onlineCount={maxValue}
offlineCount={statEntityLen}
okStatusText={"Synced"}
notOkStatusText={"Failed"}
label={entityTextPlural}
/>
</Box>
);
};
const SiteReplicationStatus = () => {
const [stats, setStats] = useState<StatsResponseType>({});
const [isStatsLoading, invokeSiteStatsApi] = useApi(
(res: any) => {
setStats(res);
},
(err: any) => {
setStats({});
}
);
const {
maxBuckets = 0,
bucketStats = {},
maxGroups = 0,
groupStats = {},
maxUsers = 0,
userStats = {},
maxPolicies = 0,
policyStats = {},
} = stats || {};
const getStats = () => {
let url = `api/v1/admin/site-replication/status?buckets=true&groups=true&policies=true&users=true`;
invokeSiteStatsApi("GET", url);
};
useEffect(() => {
getStats();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Fragment>
<PageHeader
label={
<BackLink
to={IAM_PAGES.SITE_REPLICATION}
label={"Site Replication"}
/>
}
/>
<PageLayout>
<ScreenTitle
title={"Replication status from all Sites"}
actions={
<Fragment>
<RBIconButton
onClick={() => {
getStats();
}}
tooltip={"Refresh"}
text={"Refresh"}
showLabelAlways
icon={<RefreshIcon />}
color="primary"
variant={"contained"}
/>
</Fragment>
}
/>
{!isStatsLoading ? (
<Box
sx={{
display: "grid",
marginTop: "25px",
gridTemplateColumns: {
md: "1fr 1fr 1fr 1fr",
sm: "1fr 1fr",
xs: "1fr",
},
gap: "30px",
}}
>
<SREntityStatus
entityStatObj={bucketStats}
entityTextPlural={"Buckets"}
maxValue={maxBuckets}
icon={<BucketsIcon />}
/>
<SREntityStatus
entityStatObj={userStats}
entityTextPlural={"Users"}
maxValue={maxUsers}
icon={<UsersIcon />}
/>
<SREntityStatus
entityStatObj={groupStats}
entityTextPlural={"Groups"}
maxValue={maxGroups}
icon={<GroupsIcon />}
/>
<SREntityStatus
entityStatObj={policyStats}
entityTextPlural={"Policies"}
maxValue={maxPolicies}
icon={<IAMPoliciesIcon />}
/>
</Box>
) : (
<Grid
item
xs={12}
display={"flex"}
alignItems={"center"}
justifyContent={"center"}
marginTop={"45px"}
>
<Loader style={{ width: 25, height: 25 }} />
</Grid>
)}
<Box
sx={{
border: "1px solid #eaeaea",
minHeight: {
sm: "450px",
xs: "250px",
},
marginTop: "25px",
padding: "25px",
}}
>
<EntityReplicationLookup />
</Box>
</PageLayout>
</Fragment>
);
};
export default SiteReplicationStatus;

View File

@@ -121,6 +121,7 @@ const AddPool = React.lazy(
const SiteReplication = React.lazy(
() => import("./Configurations/SiteReplication/SiteReplication")
);
const SiteReplicationStatus = React.lazy(() => import("./Configurations/SiteReplication/SiteReplicationStatus"));
const styles = (theme: Theme) =>
createStyles({
root: {
@@ -373,6 +374,10 @@ const Console = ({
component: SiteReplication,
path: IAM_PAGES.SITE_REPLICATION,
},
{
component:SiteReplicationStatus,
path:IAM_PAGES.SITE_REPLICATION_STATUS,
},
{
component: Account,
path: IAM_PAGES.ACCOUNT,

View File

@@ -23,11 +23,15 @@ export const StatusCountCard = ({
offlineCount = 0,
icon = null,
label = "",
okStatusText = "Online",
notOkStatusText = "Offline",
}: {
icon: any;
onlineCount: number;
offlineCount: number;
label: string;
okStatusText?: string;
notOkStatusText?: string;
}) => {
return (
<Box
@@ -115,7 +119,7 @@ export const StatusCountCard = ({
},
}}
>
<CircleIcon /> <div className="stat-text">Online</div>
<CircleIcon /> <div className="stat-text">{okStatusText}</div>
</Box>
</Box>
@@ -130,7 +134,8 @@ export const StatusCountCard = ({
},
}}
>
<CircleIcon /> <div className="stat-text">Offline</div>
<CircleIcon />{" "}
<div className="stat-text">{notOkStatusText}</div>
</Box>
</Box>
</Box>

View File

@@ -29,7 +29,7 @@ export const addSitesBtn = Selector("button").withText("Add Sites");
// Command to invoke the test locally: testcafe chrome tests/permissions/site-replication.ts
/* End Local Testing config block */
fixture("Site Replication for user with Admin permissions")
fixture("Site Replication Status for user with Admin permissions")
.page(testDomainUrl)
.beforeEach(async (t) => {
await t.useRole(roles.settings);

View File

@@ -203,3 +203,64 @@ func TestDeleteSiteReplicationInfo(t *testing.T) {
fmt.Println("TestDeleteReplicationInfo: ", response.StatusCode)
}
// Status API
func makeStatusExecuteReq(method string, url string) (*http.Response, error) {
client := &http.Client{
Timeout: 10 * time.Second,
}
request, err := http.NewRequest(
method,
url,
nil,
)
if err != nil {
return nil, err
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
return nil, err
}
return response, nil
}
func TestGetSiteReplicationStatus(t *testing.T) {
assert := assert.New(t)
reqUrls := [5]string{
//default
"http://localhost:9090/api/v1/admin/site-replication/status?users=true&groups=true&buckets=true&policies=true",
//specific bucket lookup
"http://localhost:9090/api/v1/admin/site-replication/status?users=false&groups=false&buckets=false&policies=false&entityValue=test-bucket&entityType=bucket",
//specific-user lookup
"http://localhost:9090/api/v1/admin/site-replication/status?users=false&groups=false&buckets=false&policies=false&entityValue=test-user&entityType=user",
//specific-group lookup
"http://localhost:9090/api/v1/admin/site-replication/status?users=false&groups=false&buckets=false&policies=false&entityValue=test-group&entityType=group",
//specific-policy lookup
"http://localhost:9090/api/v1/admin/site-replication/status?users=false&groups=false&buckets=false&policies=false&entityValue=test-policies&entityType=policiy",
}
for i, url := range reqUrls {
response, err := makeStatusExecuteReq("GET", url)
tgt := &models.SiteReplicationStatusResponse{}
json.NewDecoder(response.Body).Decode(tgt)
if err != nil {
log.Println(err)
return
}
if response != nil {
assert.Equal(200, response.StatusCode, "Status Code for", i)
}
}
}

View File

@@ -0,0 +1,92 @@
// 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/>.
package restapi
import (
"context"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/restapi/operations"
"github.com/minio/console/restapi/operations/admin_api"
"github.com/minio/madmin-go"
)
func registerSiteReplicationStatusHandler(api *operations.ConsoleAPI) {
api.AdminAPIGetSiteReplicationStatusHandler = admin_api.GetSiteReplicationStatusHandlerFunc(func(params admin_api.GetSiteReplicationStatusParams, session *models.Principal) middleware.Responder {
rInfo, err := getSRStatusResponse(session, params)
if err != nil {
return admin_api.NewGetSiteReplicationStatusDefault(500).WithPayload(prepareError(err))
}
return admin_api.NewGetSiteReplicationStatusOK().WithPayload(rInfo)
})
}
func getSRStatusResponse(session *models.Principal, params admin_api.GetSiteReplicationStatusParams) (info *models.SiteReplicationStatusResponse, err error) {
mAdmin, err := NewMinioAdminClient(session)
if err != nil {
return nil, err
}
adminClient := AdminClient{Client: mAdmin}
ctx := context.Background()
res, err := getSRStats(ctx, adminClient, params)
if err != nil {
return nil, err
}
return res, nil
}
func getSRStats(ctx context.Context, client MinioAdmin, params admin_api.GetSiteReplicationStatusParams) (info *models.SiteReplicationStatusResponse, err error) {
srParams := madmin.SRStatusOptions{
Buckets: *params.Buckets,
Policies: *params.Policies,
Users: *params.Users,
Groups: *params.Groups,
}
if params.EntityType != nil && params.EntityValue != nil {
srParams.Entity = madmin.GetSREntityType(*params.EntityType)
srParams.EntityValue = *params.EntityValue
}
srInfo, err := client.getSiteReplicationStatus(ctx, srParams)
retInfo := models.SiteReplicationStatusResponse{
BucketStats: &srInfo.BucketStats,
Enabled: srInfo.Enabled,
GroupStats: srInfo.GroupStats,
MaxBuckets: int64(srInfo.MaxBuckets),
MaxGroups: int64(srInfo.MaxGroups),
MaxPolicies: int64(srInfo.MaxPolicies),
MaxUsers: int64(srInfo.MaxUsers),
PolicyStats: &srInfo.PolicyStats,
Sites: &srInfo.Sites,
StatsSummary: srInfo.StatsSummary,
UserStats: &srInfo.UserStats,
}
if err != nil {
return nil, err
}
return &retInfo, nil
}

View File

@@ -52,6 +52,12 @@ func (ac adminClientMock) deleteSiteReplicationInfo(ctx context.Context, removeR
return deleteSiteReplicationInfoMock(ctx, removeReq)
}
var getSiteReplicationStatus func(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error)
func (ac adminClientMock) getSiteReplicationStatus(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error) {
return getSiteReplicationStatus(ctx, params)
}
func TestGetSiteReplicationInfo(t *testing.T) {
assert := assert.New(t)
// mock minIO client
@@ -243,3 +249,60 @@ func TestDeleteSiteReplicationInfo(t *testing.T) {
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: length of lists is not the same", function))
}
func TestSiteReplicationStatus(t *testing.T) {
assert := assert.New(t)
// mock minIO client
adminClient := adminClientMock{}
function := "getSiteReplicationStatus()"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
retValueMock := madmin.SRStatusInfo{
Enabled: true,
MaxBuckets: 0,
MaxUsers: 0,
MaxGroups: 0,
MaxPolicies: 0,
Sites: nil,
StatsSummary: nil,
BucketStats: nil,
PolicyStats: nil,
UserStats: nil,
GroupStats: nil,
}
expValueMock := &madmin.SRStatusInfo{
Enabled: true,
MaxBuckets: 0,
MaxUsers: 0,
MaxGroups: 0,
MaxPolicies: 0,
Sites: nil,
StatsSummary: nil,
BucketStats: nil,
PolicyStats: nil,
UserStats: nil,
GroupStats: nil,
}
getSiteReplicationStatus = func(ctx context.Context, params madmin.SRStatusOptions) (info *madmin.SRStatusInfo, err error) {
return &retValueMock, nil
}
reqValues := madmin.SRStatusOptions{
Buckets: true,
Policies: true,
Users: true,
Groups: true,
}
srInfo, err := adminClient.getSiteReplicationStatus(ctx, reqValues)
if err != nil {
assert.Error(err)
}
assert.Equal(expValueMock, srInfo, fmt.Sprintf("Failed on %s: expected result is not same", function))
}

View File

@@ -119,6 +119,9 @@ type MinioAdmin interface {
addSiteReplicationInfo(ctx context.Context, sites []madmin.PeerSite) (*madmin.ReplicateAddStatus, error)
editSiteReplicationInfo(ctx context.Context, site madmin.PeerInfo) (*madmin.ReplicateEditStatus, error)
deleteSiteReplicationInfo(ctx context.Context, removeReq madmin.SRRemoveReq) (*madmin.ReplicateRemoveStatus, error)
//Replication status
getSiteReplicationStatus(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error)
}
// Interface implementation
@@ -547,3 +550,12 @@ func (ac AdminClient) deleteSiteReplicationInfo(ctx context.Context, removeReq m
ErrDetail: res.ErrDetail,
}, nil
}
func (ac AdminClient) getSiteReplicationStatus(ctx context.Context, params madmin.SRStatusOptions) (*madmin.SRStatusInfo, error) {
res, err := ac.Client.SRStatusInfo(ctx, params)
if err != nil {
return nil, err
}
return &res, nil
}

View File

@@ -132,6 +132,7 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
registerNodesHandler(api)
registerSiteReplicationHandler(api)
registerSiteReplicationStatusHandler(api)
// Operator Console

View File

@@ -429,6 +429,71 @@ func init() {
}
}
},
"/admin/site-replication/status": {
"get": {
"tags": [
"AdminAPI"
],
"summary": "Display overall site replication status",
"operationId": "GetSiteReplicationStatus",
"parameters": [
{
"type": "boolean",
"default": true,
"description": "Include Bucket stats",
"name": "buckets",
"in": "query"
},
{
"type": "boolean",
"default": true,
"description": "Include Group stats",
"name": "groups",
"in": "query"
},
{
"type": "boolean",
"default": true,
"description": "Include Policies stats",
"name": "policies",
"in": "query"
},
{
"type": "boolean",
"default": true,
"description": "Include Policies stats",
"name": "users",
"in": "query"
},
{
"type": "string",
"description": "Entity Type to lookup",
"name": "entityType",
"in": "query"
},
{
"type": "string",
"description": "Entity Value to lookup",
"name": "entityValue",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/siteReplicationStatusResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/admin/tiers": {
"get": {
"tags": [
@@ -6311,6 +6376,44 @@ func init() {
}
}
},
"siteReplicationStatusResponse": {
"type": "object",
"properties": {
"bucketStats": {
"type": "object"
},
"enabled": {
"type": "boolean"
},
"groupStats": {
"type": "object"
},
"maxBuckets": {
"type": "integer"
},
"maxGroups": {
"type": "integer"
},
"maxPolicies": {
"type": "integer"
},
"maxUsers": {
"type": "integer"
},
"policyStats": {
"type": "object"
},
"sites": {
"type": "object"
},
"statsSummary": {
"type": "object"
},
"userStats": {
"type": "object"
}
}
},
"startProfilingItem": {
"type": "object",
"properties": {
@@ -7250,6 +7353,71 @@ func init() {
}
}
},
"/admin/site-replication/status": {
"get": {
"tags": [
"AdminAPI"
],
"summary": "Display overall site replication status",
"operationId": "GetSiteReplicationStatus",
"parameters": [
{
"type": "boolean",
"default": true,
"description": "Include Bucket stats",
"name": "buckets",
"in": "query"
},
{
"type": "boolean",
"default": true,
"description": "Include Group stats",
"name": "groups",
"in": "query"
},
{
"type": "boolean",
"default": true,
"description": "Include Policies stats",
"name": "policies",
"in": "query"
},
{
"type": "boolean",
"default": true,
"description": "Include Policies stats",
"name": "users",
"in": "query"
},
{
"type": "string",
"description": "Entity Type to lookup",
"name": "entityType",
"in": "query"
},
{
"type": "string",
"description": "Entity Value to lookup",
"name": "entityValue",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/siteReplicationStatusResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/admin/tiers": {
"get": {
"tags": [
@@ -13258,6 +13426,44 @@ func init() {
}
}
},
"siteReplicationStatusResponse": {
"type": "object",
"properties": {
"bucketStats": {
"type": "object"
},
"enabled": {
"type": "boolean"
},
"groupStats": {
"type": "object"
},
"maxBuckets": {
"type": "integer"
},
"maxGroups": {
"type": "integer"
},
"maxPolicies": {
"type": "integer"
},
"maxUsers": {
"type": "integer"
},
"policyStats": {
"type": "object"
},
"sites": {
"type": "object"
},
"statsSummary": {
"type": "object"
},
"userStats": {
"type": "object"
}
}
},
"startProfilingItem": {
"type": "object",
"properties": {

View File

@@ -0,0 +1,88 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// GetSiteReplicationStatusHandlerFunc turns a function with the right signature into a get site replication status handler
type GetSiteReplicationStatusHandlerFunc func(GetSiteReplicationStatusParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn GetSiteReplicationStatusHandlerFunc) Handle(params GetSiteReplicationStatusParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// GetSiteReplicationStatusHandler interface for that can handle valid get site replication status params
type GetSiteReplicationStatusHandler interface {
Handle(GetSiteReplicationStatusParams, *models.Principal) middleware.Responder
}
// NewGetSiteReplicationStatus creates a new http.Handler for the get site replication status operation
func NewGetSiteReplicationStatus(ctx *middleware.Context, handler GetSiteReplicationStatusHandler) *GetSiteReplicationStatus {
return &GetSiteReplicationStatus{Context: ctx, Handler: handler}
}
/* GetSiteReplicationStatus swagger:route GET /admin/site-replication/status AdminAPI getSiteReplicationStatus
Display overall site replication status
*/
type GetSiteReplicationStatus struct {
Context *middleware.Context
Handler GetSiteReplicationStatusHandler
}
func (o *GetSiteReplicationStatus) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewGetSiteReplicationStatusParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
*r = *aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,275 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// NewGetSiteReplicationStatusParams creates a new GetSiteReplicationStatusParams object
// with the default values initialized.
func NewGetSiteReplicationStatusParams() GetSiteReplicationStatusParams {
var (
// initialize parameters with default values
bucketsDefault = bool(true)
groupsDefault = bool(true)
policiesDefault = bool(true)
usersDefault = bool(true)
)
return GetSiteReplicationStatusParams{
Buckets: &bucketsDefault,
Groups: &groupsDefault,
Policies: &policiesDefault,
Users: &usersDefault,
}
}
// GetSiteReplicationStatusParams contains all the bound params for the get site replication status operation
// typically these are obtained from a http.Request
//
// swagger:parameters GetSiteReplicationStatus
type GetSiteReplicationStatusParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*Include Bucket stats
In: query
Default: true
*/
Buckets *bool
/*Entity Type to lookup
In: query
*/
EntityType *string
/*Entity Value to lookup
In: query
*/
EntityValue *string
/*Include Group stats
In: query
Default: true
*/
Groups *bool
/*Include Policies stats
In: query
Default: true
*/
Policies *bool
/*Include Policies stats
In: query
Default: true
*/
Users *bool
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewGetSiteReplicationStatusParams() beforehand.
func (o *GetSiteReplicationStatusParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
qBuckets, qhkBuckets, _ := qs.GetOK("buckets")
if err := o.bindBuckets(qBuckets, qhkBuckets, route.Formats); err != nil {
res = append(res, err)
}
qEntityType, qhkEntityType, _ := qs.GetOK("entityType")
if err := o.bindEntityType(qEntityType, qhkEntityType, route.Formats); err != nil {
res = append(res, err)
}
qEntityValue, qhkEntityValue, _ := qs.GetOK("entityValue")
if err := o.bindEntityValue(qEntityValue, qhkEntityValue, route.Formats); err != nil {
res = append(res, err)
}
qGroups, qhkGroups, _ := qs.GetOK("groups")
if err := o.bindGroups(qGroups, qhkGroups, route.Formats); err != nil {
res = append(res, err)
}
qPolicies, qhkPolicies, _ := qs.GetOK("policies")
if err := o.bindPolicies(qPolicies, qhkPolicies, route.Formats); err != nil {
res = append(res, err)
}
qUsers, qhkUsers, _ := qs.GetOK("users")
if err := o.bindUsers(qUsers, qhkUsers, route.Formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// bindBuckets binds and validates parameter Buckets from query.
func (o *GetSiteReplicationStatusParams) bindBuckets(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewGetSiteReplicationStatusParams()
return nil
}
value, err := swag.ConvertBool(raw)
if err != nil {
return errors.InvalidType("buckets", "query", "bool", raw)
}
o.Buckets = &value
return nil
}
// bindEntityType binds and validates parameter EntityType from query.
func (o *GetSiteReplicationStatusParams) bindEntityType(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
o.EntityType = &raw
return nil
}
// bindEntityValue binds and validates parameter EntityValue from query.
func (o *GetSiteReplicationStatusParams) bindEntityValue(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
return nil
}
o.EntityValue = &raw
return nil
}
// bindGroups binds and validates parameter Groups from query.
func (o *GetSiteReplicationStatusParams) bindGroups(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewGetSiteReplicationStatusParams()
return nil
}
value, err := swag.ConvertBool(raw)
if err != nil {
return errors.InvalidType("groups", "query", "bool", raw)
}
o.Groups = &value
return nil
}
// bindPolicies binds and validates parameter Policies from query.
func (o *GetSiteReplicationStatusParams) bindPolicies(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewGetSiteReplicationStatusParams()
return nil
}
value, err := swag.ConvertBool(raw)
if err != nil {
return errors.InvalidType("policies", "query", "bool", raw)
}
o.Policies = &value
return nil
}
// bindUsers binds and validates parameter Users from query.
func (o *GetSiteReplicationStatusParams) bindUsers(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string
if len(rawData) > 0 {
raw = rawData[len(rawData)-1]
}
// Required: false
// AllowEmptyValue: false
if raw == "" { // empty values pass all other validations
// Default values have been previously initialized by NewGetSiteReplicationStatusParams()
return nil
}
value, err := swag.ConvertBool(raw)
if err != nil {
return errors.InvalidType("users", "query", "bool", raw)
}
o.Users = &value
return nil
}

View File

@@ -0,0 +1,133 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/console/models"
)
// GetSiteReplicationStatusOKCode is the HTTP code returned for type GetSiteReplicationStatusOK
const GetSiteReplicationStatusOKCode int = 200
/*GetSiteReplicationStatusOK A successful response.
swagger:response getSiteReplicationStatusOK
*/
type GetSiteReplicationStatusOK struct {
/*
In: Body
*/
Payload *models.SiteReplicationStatusResponse `json:"body,omitempty"`
}
// NewGetSiteReplicationStatusOK creates GetSiteReplicationStatusOK with default headers values
func NewGetSiteReplicationStatusOK() *GetSiteReplicationStatusOK {
return &GetSiteReplicationStatusOK{}
}
// WithPayload adds the payload to the get site replication status o k response
func (o *GetSiteReplicationStatusOK) WithPayload(payload *models.SiteReplicationStatusResponse) *GetSiteReplicationStatusOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the get site replication status o k response
func (o *GetSiteReplicationStatusOK) SetPayload(payload *models.SiteReplicationStatusResponse) {
o.Payload = payload
}
// WriteResponse to the client
func (o *GetSiteReplicationStatusOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}
/*GetSiteReplicationStatusDefault Generic error response.
swagger:response getSiteReplicationStatusDefault
*/
type GetSiteReplicationStatusDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewGetSiteReplicationStatusDefault creates GetSiteReplicationStatusDefault with default headers values
func NewGetSiteReplicationStatusDefault(code int) *GetSiteReplicationStatusDefault {
if code <= 0 {
code = 500
}
return &GetSiteReplicationStatusDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the get site replication status default response
func (o *GetSiteReplicationStatusDefault) WithStatusCode(code int) *GetSiteReplicationStatusDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the get site replication status default response
func (o *GetSiteReplicationStatusDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the get site replication status default response
func (o *GetSiteReplicationStatusDefault) WithPayload(payload *models.Error) *GetSiteReplicationStatusDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the get site replication status default response
func (o *GetSiteReplicationStatusDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *GetSiteReplicationStatusDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,167 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package admin_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
"github.com/go-openapi/swag"
)
// GetSiteReplicationStatusURL generates an URL for the get site replication status operation
type GetSiteReplicationStatusURL struct {
Buckets *bool
EntityType *string
EntityValue *string
Groups *bool
Policies *bool
Users *bool
_basePath string
// avoid unkeyed usage
_ struct{}
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *GetSiteReplicationStatusURL) WithBasePath(bp string) *GetSiteReplicationStatusURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *GetSiteReplicationStatusURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *GetSiteReplicationStatusURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/admin/site-replication/status"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
var bucketsQ string
if o.Buckets != nil {
bucketsQ = swag.FormatBool(*o.Buckets)
}
if bucketsQ != "" {
qs.Set("buckets", bucketsQ)
}
var entityTypeQ string
if o.EntityType != nil {
entityTypeQ = *o.EntityType
}
if entityTypeQ != "" {
qs.Set("entityType", entityTypeQ)
}
var entityValueQ string
if o.EntityValue != nil {
entityValueQ = *o.EntityValue
}
if entityValueQ != "" {
qs.Set("entityValue", entityValueQ)
}
var groupsQ string
if o.Groups != nil {
groupsQ = swag.FormatBool(*o.Groups)
}
if groupsQ != "" {
qs.Set("groups", groupsQ)
}
var policiesQ string
if o.Policies != nil {
policiesQ = swag.FormatBool(*o.Policies)
}
if policiesQ != "" {
qs.Set("policies", policiesQ)
}
var usersQ string
if o.Users != nil {
usersQ = swag.FormatBool(*o.Users)
}
if usersQ != "" {
qs.Set("users", usersQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *GetSiteReplicationStatusURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *GetSiteReplicationStatusURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *GetSiteReplicationStatusURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on GetSiteReplicationStatusURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on GetSiteReplicationStatusURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *GetSiteReplicationStatusURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -225,6 +225,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
AdminAPIGetSiteReplicationInfoHandler: admin_api.GetSiteReplicationInfoHandlerFunc(func(params admin_api.GetSiteReplicationInfoParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.GetSiteReplicationInfo has not yet been implemented")
}),
AdminAPIGetSiteReplicationStatusHandler: admin_api.GetSiteReplicationStatusHandlerFunc(func(params admin_api.GetSiteReplicationStatusParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.GetSiteReplicationStatus has not yet been implemented")
}),
AdminAPIGetTierHandler: admin_api.GetTierHandlerFunc(func(params admin_api.GetTierParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.GetTier has not yet been implemented")
}),
@@ -591,6 +594,8 @@ type ConsoleAPI struct {
UserAPIGetServiceAccountPolicyHandler user_api.GetServiceAccountPolicyHandler
// AdminAPIGetSiteReplicationInfoHandler sets the operation handler for the get site replication info operation
AdminAPIGetSiteReplicationInfoHandler admin_api.GetSiteReplicationInfoHandler
// AdminAPIGetSiteReplicationStatusHandler sets the operation handler for the get site replication status operation
AdminAPIGetSiteReplicationStatusHandler admin_api.GetSiteReplicationStatusHandler
// AdminAPIGetTierHandler sets the operation handler for the get tier operation
AdminAPIGetTierHandler admin_api.GetTierHandler
// AdminAPIGetUserInfoHandler sets the operation handler for the get user info operation
@@ -973,6 +978,9 @@ func (o *ConsoleAPI) Validate() error {
if o.AdminAPIGetSiteReplicationInfoHandler == nil {
unregistered = append(unregistered, "admin_api.GetSiteReplicationInfoHandler")
}
if o.AdminAPIGetSiteReplicationStatusHandler == nil {
unregistered = append(unregistered, "admin_api.GetSiteReplicationStatusHandler")
}
if o.AdminAPIGetTierHandler == nil {
unregistered = append(unregistered, "admin_api.GetTierHandler")
}
@@ -1492,6 +1500,10 @@ func (o *ConsoleAPI) initHandlerCache() {
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/admin/site-replication/status"] = admin_api.NewGetSiteReplicationStatus(o.context, o.AdminAPIGetSiteReplicationStatusHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/admin/tiers/{type}/{name}"] = admin_api.NewGetTier(o.context, o.AdminAPIGetTierHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)

View File

@@ -2504,6 +2504,52 @@ paths:
tags:
- AdminAPI
/admin/site-replication/status:
get:
summary: Display overall site replication status
operationId: GetSiteReplicationStatus
parameters:
- name: buckets
description: Include Bucket stats
in: query
type: boolean
default: true
- name: groups
description: Include Group stats
in: query
type: boolean
default: true
- name: policies
description: Include Policies stats
in: query
type: boolean
default: true
- name: users
description: Include Policies stats
in: query
type: boolean
default: true
- name: entityType
description: Entity Type to lookup
in: query
type: string
required: false
- name: entityValue
description: Entity Value to lookup
in: query
type: string
required: false
responses:
200:
description: A successful response.
schema:
$ref: "#/definitions/siteReplicationStatusResponse"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- AdminAPI
/admin/tiers:
get:
@@ -3923,6 +3969,32 @@ definitions:
serviceAccountAccessKey:
type: string
siteReplicationStatusResponse:
type: object
properties:
enabled:
type: boolean
maxBuckets:
type: integer
maxUsers:
type: integer
maxGroups:
type: integer
maxPolicies:
type: integer
sites:
type: object
statsSummary:
type: object
bucketStats:
type: object
policyStats:
type: object
userStats:
type: object
groupStats:
type: object
updateUser:
type: object
required: