Added node and type selector UI to Error Logs screen (#1715)

This commit is contained in:
jinapurapu
2022-04-04 11:54:03 -07:00
committed by GitHub
parent 6f5cb4d1a5
commit 1d362aceaf
21 changed files with 951 additions and 91 deletions

View File

@@ -55,6 +55,29 @@ func RestartService() (*http.Response, error) {
return response, err
}
func GetNodes() (*http.Response, error) {
/*
Helper function to get nodes
HTTP Verb: GET
URL: /api/v1/nodes
*/
request, err := http.NewRequest(
"GET",
"http://localhost:9090/api/v1/nodes",
nil,
)
if err != nil {
log.Println(err)
}
request.Header.Add("Cookie", fmt.Sprintf("token=%s", token))
request.Header.Add("Content-Type", "application/json")
client := &http.Client{
Timeout: 2000 * time.Second, // increased timeout since restart takes time, more than other APIs.
}
response, err := client.Do(request)
return response, err
}
func NotifyPostgres() (*http.Response, error) {
/*
Helper function to add Postgres Notification
@@ -257,3 +280,23 @@ func TestListUsersWithAccessToBucket(t *testing.T) {
}
}
func TestGetNodes(t *testing.T) {
assert := assert.New(t)
getNodesResponse, getNodesError := GetNodes()
assert.Nil(getNodesError)
if getNodesError != nil {
log.Println(getNodesError)
return
}
addObjRsp := inspectHTTPResponse(getNodesResponse)
if getNodesResponse != nil {
assert.Equal(
200,
getNodesResponse.StatusCode,
addObjRsp,
)
}
}

View File

@@ -19,10 +19,24 @@ import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { connect } from "react-redux";
import { Box, Grid } from "@mui/material";
import {
Grid,
FormControl,
MenuItem,
Select,
InputBase,
Button,
} from "@mui/material";
import moment from "moment/moment";
import { ErrorResponseHandler } from "../../../../../src/common/types";
import api from "../../../../../src/common/api";
import { AppState } from "../../../../store";
import { logMessageReceived, logResetMessages } from "../actions";
import {
logMessageReceived,
logResetMessages,
setLogsStarted,
} from "../actions";
import { LogMessage } from "../types";
import { wsProtocol } from "../../../../utils/wsUtils";
import {
@@ -30,6 +44,7 @@ import {
containerForHeader,
logsCommon,
searchField,
inlineCheckboxes,
} from "../../Common/FormComponents/common/styleLibrary";
import PageHeader from "../../Common/PageHeader/PageHeader";
import PageLayout from "../../Common/Layout/PageLayout";
@@ -60,36 +75,83 @@ const styles = (theme: Theme) =>
color: "#A52A2A",
paddingLeft: 25,
},
nodeField: {
width: "100%",
},
ansidefault: {
color: "#000",
},
midColumnCheckboxes: {
display: "flex",
},
checkBoxLabel: {
marginTop: 10,
fontSize: 16,
fontWeight: 500,
},
highlight: {
"& span": {
backgroundColor: "#082F5238",
},
},
...actionsTray,
actionsTray: {
...actionsTray.actionsTray,
marginBottom: 0,
},
...searchField,
...logsCommon,
...inlineCheckboxes,
...containerForHeader(theme.spacing(4)),
});
const SelectStyled = withStyles((theme: Theme) =>
createStyles({
root: {
lineHeight: "50px",
"label + &": {
marginTop: theme.spacing(3),
},
"& .MuiSelect-select:focus": {
backgroundColor: "transparent",
},
},
input: {
height: 50,
fontSize: 13,
lineHeight: "50px",
},
})
)(InputBase);
interface ILogs {
classes: any;
logMessageReceived: typeof logMessageReceived;
logResetMessages: typeof logResetMessages;
setLogsStarted: typeof setLogsStarted;
messages: LogMessage[];
logsStarted: boolean;
}
var c: any = null;
const ErrorLogs = ({
classes,
logMessageReceived,
logResetMessages,
setLogsStarted,
messages,
logsStarted,
}: ILogs) => {
const [filter, setFilter] = useState<string>("");
const [nodes, setNodes] = useState<string[]>([""]);
const [selectedNode, setSelectedNode] = useState<string>("all");
const [selectedUserAgent, setSelectedUserAgent] =
useState<string>("Select user agent");
const [userAgents, setUserAgents] = useState<string[]>(["All User Agents"]);
const [logType, setLogType] = useState<string>("all");
const [loadingNodes, setLoadingNodes] = useState<boolean>(false);
useEffect(() => {
const startLogs = () => {
logResetMessages();
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
@@ -97,14 +159,18 @@ const ErrorLogs = ({
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}/ws/console`
c = new W3CWebSocket(
`${wsProt}://${
url.hostname
}:${port}/ws/console/?logType=${logType}&node=${
selectedNode === "Select node" ? "" : selectedNode
}`
);
let interval: any | null = null;
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
setLogsStarted(true);
c.send("ok");
interval = setInterval(() => {
c.send("ok");
@@ -113,54 +179,205 @@ const ErrorLogs = ({
c.onmessage = (message: IMessageEvent) => {
// console.log(message.data.toString())
// FORMAT: 00:35:17 UTC 01/01/2021
let m: LogMessage = JSON.parse(message.data.toString());
m.time = moment(m.time, "HH:mm:s UTC MM/DD/YYYY").toDate();
m.key = Math.random();
if (userAgents.indexOf(m.userAgent) < 0 && m.userAgent !== undefined) {
userAgents.push(m.userAgent);
setUserAgents(userAgents);
}
logMessageReceived(m);
};
c.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
setLogsStarted(false);
};
return () => {
c.close(1000);
clearInterval(interval);
console.log("closing websockets");
setLogsStarted(false);
};
}
}, [logMessageReceived, logResetMessages]);
};
const stopLogs = () => {
if (c !== null && c !== undefined) {
c.close(1000);
setLogsStarted(false);
}
};
const filtLow = filter.toLowerCase();
let filteredMessages = messages.filter((m) => {
if (filter !== "") {
if (m.ConsoleMsg.toLowerCase().indexOf(filtLow) >= 0) {
return true;
} else if (
m.error &&
m.error.source &&
m.error.source.filter((x) => {
return x.toLowerCase().indexOf(filtLow) >= 0;
}).length > 0
) {
return true;
} else if (
m.error &&
m.error.message.toLowerCase().indexOf(filtLow) >= 0
) {
return true;
} else if (m.api && m.api.name.toLowerCase().indexOf(filtLow) >= 0) {
return true;
if (
m.userAgent === selectedUserAgent ||
selectedUserAgent === "All User Agents" ||
selectedUserAgent === "Select user agent"
) {
if (filter !== "") {
if (m.ConsoleMsg.toLowerCase().indexOf(filtLow) >= 0) {
return true;
} else if (
m.error &&
m.error.source &&
m.error.source.filter((x) => {
return x.toLowerCase().indexOf(filtLow) >= 0;
}).length > 0
) {
return true;
} else if (
m.error &&
m.error.message.toLowerCase().indexOf(filtLow) >= 0
) {
return true;
} else if (m.api && m.api.name.toLowerCase().indexOf(filtLow) >= 0) {
return true;
}
return false;
}
return false;
}
return true;
return true;
} else return false;
});
useEffect(() => {
setLoadingNodes(true);
api
.invoke("GET", `/api/v1/nodes`)
.then((res: string[]) => {
setNodes(res);
// if (res.length > 0) {
// setSelectedNode(res[0]);
// }
setLoadingNodes(false);
})
.catch((err: ErrorResponseHandler) => {
setLoadingNodes(false);
});
}, []);
return (
<Fragment>
<PageHeader label="Logs" />
<PageLayout>
<Grid container>
<Grid container spacing={1}>
<Grid item xs={4}>
{!loadingNodes ? (
<FormControl variant="outlined" className={classes.nodeField}>
<Select
id="node"
name="node"
data-test-id="node-selector"
value={selectedNode}
onChange={(e) => {
setSelectedNode(e.target.value as string);
}}
className={classes.searchField}
disabled={loadingNodes || logsStarted}
input={<SelectStyled />}
placeholder={"Select Node"}
>
<MenuItem value={"all"} key={`select-node-all`}>
All Nodes
</MenuItem>
{nodes.map((aNode) => (
<MenuItem value={aNode} key={`select-node-name-${aNode}`}>
{aNode}
</MenuItem>
))}
</Select>
</FormControl>
) : (
<h3> Loading nodes</h3>
)}
</Grid>
<Grid item xs={3}>
<FormControl variant="outlined" className={classes.nodeField}>
<Select
id="logType"
name="logType"
data-test-id="log-type"
value={logType}
onChange={(e) => {
setLogType(e.target.value as string);
}}
className={classes.searchField}
disabled={loadingNodes || logsStarted}
input={<SelectStyled />}
placeholder={"Select Log Type"}
>
<MenuItem value="all" key="all-log-types">
All Log Types
</MenuItem>
<MenuItem value="minio" key="minio-log-type">
MinIO
</MenuItem>
<MenuItem value="application" key="app-log-type">
Application
</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={3}>
{userAgents.length > 1 && (
<FormControl variant="outlined" className={classes.nodeField}>
<Select
id="userAgent"
name="userAgent"
data-test-id="user-agent"
value={selectedUserAgent}
onChange={(e) => {
setSelectedUserAgent(e.target.value as string);
}}
className={classes.searchField}
disabled={userAgents.length < 1 || logsStarted}
input={<SelectStyled />}
>
<MenuItem
value={selectedUserAgent}
key={`select-user-agent-default`}
disabled={true}
>
Select User Agent
</MenuItem>
{userAgents.map((anAgent) => (
<MenuItem
value={anAgent}
key={`select-user-agent-${anAgent}`}
>
{anAgent}
</MenuItem>
))}
</Select>
</FormControl>
)}
</Grid>
<Grid item xs={2} textAlign={"right"}>
{!logsStarted && (
<Button
type="submit"
variant="contained"
color="primary"
disabled={false}
onClick={startLogs}
>
Start Logs
</Button>
)}
{logsStarted && (
<Button
type="button"
variant="contained"
color="primary"
onClick={stopLogs}
>
Stop Logs
</Button>
)}
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<SearchBox
placeholder="Filter"
@@ -170,38 +387,27 @@ const ErrorLogs = ({
value={filter}
/>
</Grid>
<Grid item xs={12} data-test-id={"logs-list-container"}>
{filteredMessages.length ? (
<div id="logs-container" className={classes.logList}>
<TableContainer
component={Paper}
sx={{
borderBottom: "0px",
}}
>
<Table aria-label="collapsible table">
<TableBody>
{filteredMessages.map((m) => {
return <LogLine key={m.key} log={m} />;
})}
</TableBody>
</Table>
</TableContainer>
</div>
) : (
<Box
sx={{
padding: "15px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{filter.trim().length
? `No matching logs for "${filter.trim()}".`
: "No logs to display."}
</Box>
)}
<Grid item xs={12}>
<div
id="logs-container"
className={classes.logList}
data-test-id="logs-list-container"
>
<TableContainer component={Paper}>
<Table aria-label="collapsible table">
<TableBody>
{filteredMessages.map((m) => {
return <LogLine log={m} />;
})}
</TableBody>
</Table>
{filteredMessages.length === 0 && (
<div style={{ padding: 20, textAlign: "center" }}>
No logs to display
</div>
)}
</TableContainer>
</div>
</Grid>
</Grid>
</PageLayout>
@@ -210,12 +416,15 @@ const ErrorLogs = ({
};
const mapState = (state: AppState) => ({
messages: state.logs.messages,
messages: state.logs.logMessages,
logsStarted: state.logs.logsStarted,
});
const connector = connect(mapState, {
logMessageReceived: logMessageReceived,
logResetMessages: logResetMessages,
setLogsStarted,
});
export default withStyles(styles)(connector(ErrorLogs));
//export default withStyles(styles)(connector(ErrorLogs));
export default connector(withStyles(styles)(ErrorLogs));

View File

@@ -177,6 +177,20 @@ const LogLine = (props: { log: LogMessage }) => {
{dateStr}
</Box>
</TableCell>
<TableCell
onClick={() => setOpen(!open)}
style={{ width: 200, color: "#989898", fontSize: 12 }}
>
<Box
sx={{
"& .min-icon": { width: 12, marginRight: 1 },
fontWeight: "bold",
lineHeight: 1,
}}
>
{log.errKind}
</Box>
</TableCell>
<TableCell onClick={() => setOpen(!open)}>
<div
style={{
@@ -235,7 +249,7 @@ const LogLine = (props: { log: LogMessage }) => {
<div style={{ marginTop: 10 }}>Log Details</div>
</Collapse>
</TableCell>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }}>
<TableCell colSpan={2} style={{ paddingBottom: 0, paddingTop: 0 }}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1 }}>
<Typography

View File

@@ -18,6 +18,7 @@ import { LogMessage } from "./types";
export const LOG_MESSAGE_RECEIVED = "LOG_MESSAGE_RECEIVED";
export const LOG_RESET_MESSAGES = "LOG_RESET_MESSAGES";
export const LOG_SET_STARTED = "LOG_SET_STARTED";
interface LogMessageReceivedAction {
type: typeof LOG_MESSAGE_RECEIVED;
@@ -28,7 +29,15 @@ interface LogResetMessagesAction {
type: typeof LOG_RESET_MESSAGES;
}
export type LogActionTypes = LogMessageReceivedAction | LogResetMessagesAction;
interface LogSetStarted {
type: typeof LOG_SET_STARTED;
status: boolean;
}
export type LogActionTypes =
| LogMessageReceivedAction
| LogResetMessagesAction
| LogSetStarted;
export function logMessageReceived(message: LogMessage) {
return {
@@ -42,3 +51,10 @@ export function logResetMessages() {
type: LOG_RESET_MESSAGES,
};
}
export function setLogsStarted(status: boolean) {
return {
type: LOG_SET_STARTED,
status,
};
}

View File

@@ -17,16 +17,19 @@
import {
LOG_MESSAGE_RECEIVED,
LOG_RESET_MESSAGES,
LOG_SET_STARTED,
LogActionTypes,
} from "./actions";
import { LogMessage } from "./types";
export interface LogState {
messages: LogMessage[];
logMessages: LogMessage[];
logsStarted: boolean;
}
const initialState: LogState = {
messages: [],
logMessages: [],
logsStarted: false,
};
export function logReducer(
@@ -37,7 +40,7 @@ export function logReducer(
case LOG_MESSAGE_RECEIVED:
// if it's a simple ConsoleMsg, append it to the current ConsoleMsg in the
// state if any
let msgs = [...state.messages];
let msgs = [...state.logMessages];
if (
msgs.length > 0 &&
@@ -57,12 +60,17 @@ export function logReducer(
return {
...state,
messages: msgs,
logMessages: msgs,
};
case LOG_RESET_MESSAGES:
return {
...state,
messages: [],
logMessages: [],
};
case LOG_SET_STARTED:
return {
...state,
logsStarted: action.status,
};
default:
return state;

View File

@@ -41,4 +41,5 @@ export interface LogMessage {
error: logError;
ConsoleMsg: string;
key: number;
errKind: string;
}

View File

@@ -52,3 +52,11 @@ test("Log window exists in Logs page", async (t) => {
.expect(logWindowExists)
.ok();
});
test("Node selector exists in Logs page", async (t) => {
const nodeSelectorExists = elements.nodeSelector.exists;
await t
.navigateTo("http://localhost:9090/tools/logs")
.expect(nodeSelectorExists)
.ok();
});

View File

@@ -207,3 +207,5 @@ export const settingsAuditWebhookTab = Selector(".MuiTab-root").withAttribute(
// Log window
//----------------------------------------------------
export const logWindow = Selector('[data-test-id="logs-list-container"]');
//Node selector
export const nodeSelector = Selector('[data-test-id="node-selector"]');

View File

@@ -29,24 +29,38 @@ import (
const logTimeFormat string = "15:04:05 MST 01/02/2006"
// startConsoleLog starts log of the servers
func startConsoleLog(ctx context.Context, conn WSConn, client MinioAdmin) error {
// TODO: accept parameters as variables
func startConsoleLog(ctx context.Context, conn WSConn, client MinioAdmin, logRequest LogRequest) error {
var node string
// name of node, default = "" (all)
node := ""
if logRequest.node == "all" {
node = ""
} else {
node = logRequest.node
}
trimNode := strings.Split(node, ":")
// number of log lines
lineCount := 100
// type of logs "minio"|"application"|"all" default = "all"
logKind := "all"
var logKind string
if logRequest.logType == "minio" || logRequest.logType == "application" || logRequest.logType == "all" {
logKind = logRequest.logType
} else {
logKind = "all"
}
// Start listening on all Console Log activity.
logCh := client.getLogs(ctx, node, lineCount, logKind)
logCh := client.getLogs(ctx, trimNode[0], lineCount, logKind)
for {
select {
case <-ctx.Done():
return nil
case logInfo, ok := <-logCh:
// zero value returned because the channel is closed and empty
if !ok {
return nil
}
if logInfo.Err != nil {

View File

@@ -81,7 +81,7 @@ func TestAdminConsoleLog(t *testing.T) {
writesCount++
return nil
}
if err := startConsoleLog(ctx, mockWSConn, adminClient); err != nil {
if err := startConsoleLog(ctx, mockWSConn, adminClient, LogRequest{node: "", logType: "all"}); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// check that the TestReceiver got the same number of data from Console.
@@ -93,7 +93,7 @@ func TestAdminConsoleLog(t *testing.T) {
connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write")
}
if err := startConsoleLog(ctx, mockWSConn, adminClient); assert.Error(err) {
if err := startConsoleLog(ctx, mockWSConn, adminClient, LogRequest{node: "", logType: "all"}); assert.Error(err) {
assert.Equal("error on write", err.Error())
}
@@ -119,7 +119,7 @@ func TestAdminConsoleLog(t *testing.T) {
connWriteMessageMock = func(messageType int, data []byte) error {
return nil
}
if err := startConsoleLog(ctx, mockWSConn, adminClient); assert.Error(err) {
if err := startConsoleLog(ctx, mockWSConn, adminClient, LogRequest{node: "", logType: "all"}); assert.Error(err) {
assert.Equal("error on Console", err.Error())
}
}

55
restapi/admin_nodes.go Normal file
View File

@@ -0,0 +1,55 @@
// 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"
)
func registerNodesHandler(api *operations.ConsoleAPI) {
api.AdminAPIListNodesHandler = admin_api.ListNodesHandlerFunc(func(params admin_api.ListNodesParams, session *models.Principal) middleware.Responder {
listNodesResponse, err := getListNodesResponse(session)
if err != nil {
return admin_api.NewListNodesDefault(int(err.Code)).WithPayload(err)
}
return admin_api.NewListNodesOK().WithPayload(listNodesResponse)
})
}
// getListNodesResponse returns a list of available node endpoints .
func getListNodesResponse(session *models.Principal) ([]string, *models.Error) {
ctx := context.Background()
mAdmin, err := NewMinioAdminClient(session)
if err != nil {
return nil, prepareError(err)
}
var nodeList []string
adminResources, _ := mAdmin.ServerInfo(ctx)
for _, n := range adminResources.Servers {
nodeList = append(nodeList, n.Endpoint)
}
return nodeList, nil
}

View File

@@ -280,6 +280,7 @@ func (ac AdminClient) serviceTrace(ctx context.Context, threshold int64, s3, int
// implements madmin.GetLogs()
func (ac AdminClient) getLogs(ctx context.Context, node string, lineCnt int, logKind string) <-chan madmin.LogInfo {
return ac.Client.GetLogs(ctx, node, lineCnt, logKind)
}

View File

@@ -128,6 +128,8 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
registerAdminTiersHandlers(api)
//Register Inspect Handler
registerInspectHandler(api)
// Register nodes handlers
registerNodesHandler(api)
registerSiteReplicationHandler(api)

View File

@@ -2912,6 +2912,32 @@ func init() {
}
}
},
"/nodes": {
"get": {
"tags": [
"AdminAPI"
],
"summary": "Lists Nodes",
"operationId": "ListNodes",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/policies": {
"get": {
"tags": [
@@ -9680,6 +9706,32 @@ func init() {
}
}
},
"/nodes": {
"get": {
"tags": [
"AdminAPI"
],
"summary": "Lists Nodes",
"operationId": "ListNodes",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/policies": {
"get": {
"tags": [

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"
)
// ListNodesHandlerFunc turns a function with the right signature into a list nodes handler
type ListNodesHandlerFunc func(ListNodesParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn ListNodesHandlerFunc) Handle(params ListNodesParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// ListNodesHandler interface for that can handle valid list nodes params
type ListNodesHandler interface {
Handle(ListNodesParams, *models.Principal) middleware.Responder
}
// NewListNodes creates a new http.Handler for the list nodes operation
func NewListNodes(ctx *middleware.Context, handler ListNodesHandler) *ListNodes {
return &ListNodes{Context: ctx, Handler: handler}
}
/* ListNodes swagger:route GET /nodes AdminAPI listNodes
Lists Nodes
*/
type ListNodes struct {
Context *middleware.Context
Handler ListNodesHandler
}
func (o *ListNodes) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
*r = *rCtx
}
var Params = NewListNodesParams()
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,63 @@
// 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/middleware"
)
// NewListNodesParams creates a new ListNodesParams object
//
// There are no default values defined in the spec.
func NewListNodesParams() ListNodesParams {
return ListNodesParams{}
}
// ListNodesParams contains all the bound params for the list nodes operation
// typically these are obtained from a http.Request
//
// swagger:parameters ListNodes
type ListNodesParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
}
// 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 NewListNodesParams() beforehand.
func (o *ListNodesParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -0,0 +1,136 @@
// 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"
)
// ListNodesOKCode is the HTTP code returned for type ListNodesOK
const ListNodesOKCode int = 200
/*ListNodesOK A successful response.
swagger:response listNodesOK
*/
type ListNodesOK struct {
/*
In: Body
*/
Payload []string `json:"body,omitempty"`
}
// NewListNodesOK creates ListNodesOK with default headers values
func NewListNodesOK() *ListNodesOK {
return &ListNodesOK{}
}
// WithPayload adds the payload to the list nodes o k response
func (o *ListNodesOK) WithPayload(payload []string) *ListNodesOK {
o.Payload = payload
return o
}
// SetPayload sets the payload to the list nodes o k response
func (o *ListNodesOK) SetPayload(payload []string) {
o.Payload = payload
}
// WriteResponse to the client
func (o *ListNodesOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(200)
payload := o.Payload
if payload == nil {
// return empty array
payload = make([]string, 0, 50)
}
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
/*ListNodesDefault Generic error response.
swagger:response listNodesDefault
*/
type ListNodesDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewListNodesDefault creates ListNodesDefault with default headers values
func NewListNodesDefault(code int) *ListNodesDefault {
if code <= 0 {
code = 500
}
return &ListNodesDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the list nodes default response
func (o *ListNodesDefault) WithStatusCode(code int) *ListNodesDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the list nodes default response
func (o *ListNodesDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the list nodes default response
func (o *ListNodesDefault) WithPayload(payload *models.Error) *ListNodesDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the list nodes default response
func (o *ListNodesDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *ListNodesDefault) 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,104 @@
// 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"
)
// ListNodesURL generates an URL for the list nodes operation
type ListNodesURL struct {
_basePath string
}
// 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 *ListNodesURL) WithBasePath(bp string) *ListNodesURL {
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 *ListNodesURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *ListNodesURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/nodes"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *ListNodesURL) 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 *ListNodesURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *ListNodesURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on ListNodesURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on ListNodesURL")
}
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 *ListNodesURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -261,6 +261,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
AdminAPIListGroupsForPolicyHandler: admin_api.ListGroupsForPolicyHandlerFunc(func(params admin_api.ListGroupsForPolicyParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.ListGroupsForPolicy has not yet been implemented")
}),
AdminAPIListNodesHandler: admin_api.ListNodesHandlerFunc(func(params admin_api.ListNodesParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.ListNodes has not yet been implemented")
}),
UserAPIListObjectsHandler: user_api.ListObjectsHandlerFunc(func(params user_api.ListObjectsParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.ListObjects has not yet been implemented")
}),
@@ -612,6 +615,8 @@ type ConsoleAPI struct {
AdminAPIListGroupsHandler admin_api.ListGroupsHandler
// AdminAPIListGroupsForPolicyHandler sets the operation handler for the list groups for policy operation
AdminAPIListGroupsForPolicyHandler admin_api.ListGroupsForPolicyHandler
// AdminAPIListNodesHandler sets the operation handler for the list nodes operation
AdminAPIListNodesHandler admin_api.ListNodesHandler
// UserAPIListObjectsHandler sets the operation handler for the list objects operation
UserAPIListObjectsHandler user_api.ListObjectsHandler
// AdminAPIListPoliciesHandler sets the operation handler for the list policies operation
@@ -1004,6 +1009,9 @@ func (o *ConsoleAPI) Validate() error {
if o.AdminAPIListGroupsForPolicyHandler == nil {
unregistered = append(unregistered, "admin_api.ListGroupsForPolicyHandler")
}
if o.AdminAPIListNodesHandler == nil {
unregistered = append(unregistered, "admin_api.ListNodesHandler")
}
if o.UserAPIListObjectsHandler == nil {
unregistered = append(unregistered, "user_api.ListObjectsHandler")
}
@@ -1532,6 +1540,10 @@ func (o *ConsoleAPI) initHandlerCache() {
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/nodes"] = admin_api.NewListNodes(o.context, o.AdminAPIListNodesHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/buckets/{bucket_name}/objects"] = user_api.NewListObjects(o.context, o.UserAPIListObjectsHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)

View File

@@ -97,6 +97,12 @@ type TraceRequest struct {
path string
}
//Type for log requests. This allows for filtering by node and kind
type LogRequest struct {
node string
logType string
}
func (c wsConn) writeMessage(messageType int, data []byte) error {
return c.conn.WriteMessage(messageType, data)
}
@@ -168,12 +174,20 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
go wsAdminClient.trace(traceRequestItem)
case strings.HasPrefix(wsPath, `/console`):
wsAdminClient, err := newWebSocketAdminClient(conn, session)
if err != nil {
closeWsConn(conn)
return
}
go wsAdminClient.console()
node := req.URL.Query().Get("node")
logType := req.URL.Query().Get("logType")
logRequestItem := LogRequest{
node: node,
logType: logType,
}
go wsAdminClient.console(logRequestItem)
case strings.HasPrefix(wsPath, `/health-info`):
deadline, err := getHealthInfoOptionsFromReq(req)
if err != nil {
@@ -330,7 +344,7 @@ func (wsc *wsAdminClient) trace(traceRequestItem TraceRequest) {
// console serves madmin.GetLogs
// on a Websocket connection.
func (wsc *wsAdminClient) console() {
func (wsc *wsAdminClient) console(logRequestItem LogRequest) {
defer func() {
LogInfo("console logs stopped")
// close connection after return
@@ -340,7 +354,7 @@ func (wsc *wsAdminClient) console() {
ctx := wsReadClientCtx(wsc.conn)
err := startConsoleLog(ctx, wsc.conn, wsc.client)
err := startConsoleLog(ctx, wsc.conn, wsc.client, logRequestItem)
sendWsCloseMessage(wsc.conn, err)
}

View File

@@ -19,7 +19,7 @@ securityDefinitions:
tokenUrl: http://min.io
# Apply the key security definition to all APIs
security:
- key: [ ]
- key: []
paths:
/login:
get:
@@ -35,7 +35,7 @@ paths:
schema:
$ref: "#/definitions/error"
# Exclude this API from the authentication requirement
security: [ ]
security: []
tags:
- UserAPI
post:
@@ -55,7 +55,7 @@ paths:
schema:
$ref: "#/definitions/error"
# Exclude this API from the authentication requirement
security: [ ]
security: []
tags:
- UserAPI
/login/oauth2/auth:
@@ -75,7 +75,7 @@ paths:
description: Generic error response.
schema:
$ref: "#/definitions/error"
security: [ ]
security: []
tags:
- UserAPI
@@ -122,7 +122,7 @@ paths:
description: Generic error response.
schema:
$ref: "#/definitions/error"
security: [ ]
security: []
tags:
- UserAPI
@@ -2600,6 +2600,24 @@ paths:
tags:
- AdminAPI
/nodes:
get:
summary: Lists Nodes
operationId: ListNodes
responses:
200:
description: A successful response.
schema:
type: array
items:
type: string
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- AdminAPI
/remote-buckets:
get:
summary: List Remote Buckets
@@ -2701,7 +2719,7 @@ paths:
- name: order
in: query
type: string
enum: [ timeDesc, timeAsc ]
enum: [timeDesc, timeAsc]
default: timeDesc
- name: timeStart
in: query
@@ -3515,7 +3533,7 @@ definitions:
properties:
loginStrategy:
type: string
enum: [ form, redirect, service-account ]
enum: [form, redirect, service-account]
redirect:
type: string
loginOauth2AuthRequest:
@@ -3598,7 +3616,7 @@ definitions:
type: string
status:
type: string
enum: [ ok ]
enum: [ok]
operator:
type: boolean
distributedMode:
@@ -3619,7 +3637,7 @@ definitions:
type: string
values:
type: array
items: { }
items: {}
resultTarget:
type: object
properties:
@@ -3985,7 +4003,7 @@ definitions:
type: string
service:
type: string
enum: [ replication ]
enum: [replication]
syncMode:
type: string
bandwidth: