diff --git a/integration/admin_api_integration_test.go b/integration/admin_api_integration_test.go index fb327ce96..6dadda260 100644 --- a/integration/admin_api_integration_test.go +++ b/integration/admin_api_integration_test.go @@ -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, + ) + } + +} diff --git a/portal-ui/src/screens/Console/Logs/ErrorLogs/ErrorLogs.tsx b/portal-ui/src/screens/Console/Logs/ErrorLogs/ErrorLogs.tsx index c0e31a6b9..91965600a 100644 --- a/portal-ui/src/screens/Console/Logs/ErrorLogs/ErrorLogs.tsx +++ b/portal-ui/src/screens/Console/Logs/ErrorLogs/ErrorLogs.tsx @@ -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(""); + const [nodes, setNodes] = useState([""]); + const [selectedNode, setSelectedNode] = useState("all"); + const [selectedUserAgent, setSelectedUserAgent] = + useState("Select user agent"); + const [userAgents, setUserAgents] = useState(["All User Agents"]); + const [logType, setLogType] = useState("all"); + const [loadingNodes, setLoadingNodes] = useState(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 ( - + + + {!loadingNodes ? ( + + + + ) : ( +

Loading nodes

+ )} +
+ + + + + + + + {userAgents.length > 1 && ( + + + + )} + + + {!logsStarted && ( + + )} + {logsStarted && ( + + )} + - - {filteredMessages.length ? ( -
- - - - {filteredMessages.map((m) => { - return ; - })} - -
-
-
- ) : ( - - {filter.trim().length - ? `No matching logs for "${filter.trim()}".` - : "No logs to display."} - - )} + +
+ + + + {filteredMessages.map((m) => { + return ; + })} + +
+ {filteredMessages.length === 0 && ( +
+ No logs to display +
+ )} +
+
@@ -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)); diff --git a/portal-ui/src/screens/Console/Logs/ErrorLogs/LogLine.tsx b/portal-ui/src/screens/Console/Logs/ErrorLogs/LogLine.tsx index 266f9d2f7..e31acd01f 100644 --- a/portal-ui/src/screens/Console/Logs/ErrorLogs/LogLine.tsx +++ b/portal-ui/src/screens/Console/Logs/ErrorLogs/LogLine.tsx @@ -177,6 +177,20 @@ const LogLine = (props: { log: LogMessage }) => { {dateStr} + setOpen(!open)} + style={{ width: 200, color: "#989898", fontSize: 12 }} + > + + {log.errKind} + + setOpen(!open)}>
{
Log Details
- + 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; diff --git a/portal-ui/src/screens/Console/Logs/types.ts b/portal-ui/src/screens/Console/Logs/types.ts index 4994ced8d..c979297f0 100644 --- a/portal-ui/src/screens/Console/Logs/types.ts +++ b/portal-ui/src/screens/Console/Logs/types.ts @@ -41,4 +41,5 @@ export interface LogMessage { error: logError; ConsoleMsg: string; key: number; + errKind: string; } diff --git a/portal-ui/tests/permissions-3/logs.ts b/portal-ui/tests/permissions-3/logs.ts index f78f29d35..460c1044c 100644 --- a/portal-ui/tests/permissions-3/logs.ts +++ b/portal-ui/tests/permissions-3/logs.ts @@ -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(); +}); diff --git a/portal-ui/tests/utils/elements.ts b/portal-ui/tests/utils/elements.ts index 27bd312d7..40244f09f 100644 --- a/portal-ui/tests/utils/elements.ts +++ b/portal-ui/tests/utils/elements.ts @@ -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"]'); diff --git a/restapi/admin_console.go b/restapi/admin_console.go index b161d9ae6..0de60d70c 100644 --- a/restapi/admin_console.go +++ b/restapi/admin_console.go @@ -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 { diff --git a/restapi/admin_console_test.go b/restapi/admin_console_test.go index 3fa7f0ca0..10ceae3e8 100644 --- a/restapi/admin_console_test.go +++ b/restapi/admin_console_test.go @@ -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()) } } diff --git a/restapi/admin_nodes.go b/restapi/admin_nodes.go new file mode 100644 index 000000000..7f89a9ef3 --- /dev/null +++ b/restapi/admin_nodes.go @@ -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 . + +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 +} diff --git a/restapi/client-admin.go b/restapi/client-admin.go index 254dc1fec..d1403a020 100644 --- a/restapi/client-admin.go +++ b/restapi/client-admin.go @@ -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) } diff --git a/restapi/configure_console.go b/restapi/configure_console.go index 8f74eb4d2..1182215bf 100644 --- a/restapi/configure_console.go +++ b/restapi/configure_console.go @@ -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) diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 10a49d2b3..911c4391e 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -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": [ diff --git a/restapi/operations/admin_api/list_nodes.go b/restapi/operations/admin_api/list_nodes.go new file mode 100644 index 000000000..1d9527577 --- /dev/null +++ b/restapi/operations/admin_api/list_nodes.go @@ -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 . +// + +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) + +} diff --git a/restapi/operations/admin_api/list_nodes_parameters.go b/restapi/operations/admin_api/list_nodes_parameters.go new file mode 100644 index 000000000..f6d325252 --- /dev/null +++ b/restapi/operations/admin_api/list_nodes_parameters.go @@ -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 . +// + +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 +} diff --git a/restapi/operations/admin_api/list_nodes_responses.go b/restapi/operations/admin_api/list_nodes_responses.go new file mode 100644 index 000000000..eaf4fd278 --- /dev/null +++ b/restapi/operations/admin_api/list_nodes_responses.go @@ -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 . +// + +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 + } + } +} diff --git a/restapi/operations/admin_api/list_nodes_urlbuilder.go b/restapi/operations/admin_api/list_nodes_urlbuilder.go new file mode 100644 index 000000000..501e11a4c --- /dev/null +++ b/restapi/operations/admin_api/list_nodes_urlbuilder.go @@ -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 . +// + +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() +} diff --git a/restapi/operations/console_api.go b/restapi/operations/console_api.go index ff46f6bf3..4b8f1e9cb 100644 --- a/restapi/operations/console_api.go +++ b/restapi/operations/console_api.go @@ -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) diff --git a/restapi/ws_handle.go b/restapi/ws_handle.go index 4886c930b..b6fd5a62a 100644 --- a/restapi/ws_handle.go +++ b/restapi/ws_handle.go @@ -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) } diff --git a/swagger-console.yml b/swagger-console.yml index d43ee9c94..555e9127a 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -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: