Add Watch api and UI integration (#120)

Uses a similar approach as Trace and Console Logs by using
websockets. It also includes the integration with the UI which
needs 3 input fields that are sent as query parameters.
This commit is contained in:
César Nieto
2020-05-15 14:24:29 -07:00
committed by GitHub
parent acf480fd25
commit 6fef30f29d
23 changed files with 1169 additions and 236 deletions

4
go.mod
View File

@@ -17,9 +17,9 @@ require (
github.com/jessevdk/go-flags v1.4.0 github.com/jessevdk/go-flags v1.4.0
github.com/json-iterator/go v1.1.9 github.com/json-iterator/go v1.1.9
github.com/minio/cli v1.22.0 github.com/minio/cli v1.22.0
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0 github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22 github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1

10
go.sum
View File

@@ -31,7 +31,6 @@ github.com/alecthomas/participle v0.2.1 h1:4AVLj1viSGa4LG5HDXKXrm5xRx19SB/rS/skP
github.com/alecthomas/participle v0.2.1/go.mod h1:SW6HZGeZgSIpcUWX3fXpfZhuaWHnmoD5KCVaqSaNTkk= github.com/alecthomas/participle v0.2.1/go.mod h1:SW6HZGeZgSIpcUWX3fXpfZhuaWHnmoD5KCVaqSaNTkk=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM=
@@ -42,7 +41,6 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
github.com/aws/aws-sdk-go v1.20.21/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.20.21/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2 h1:M+TYzBcNIRyzPRg66ndEqUMd7oWDmhvdQmaPC6EZNwM= github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2 h1:M+TYzBcNIRyzPRg66ndEqUMd7oWDmhvdQmaPC6EZNwM=
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2/go.mod h1:RDu/qcrnpEdJC/p8tx34+YBFqqX71lB7dOX9QE+ZC4M= github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2/go.mod h1:RDu/qcrnpEdJC/p8tx34+YBFqqX71lB7dOX9QE+ZC4M=
github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw= github.com/beevik/ntp v0.2.0 h1:sGsd+kAXzT0bfVfzJfce04g+dSRfrs+tbQW8lweuYgw=
@@ -401,15 +399,17 @@ github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2
github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc=
github.com/minio/lsync v1.0.1 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs= github.com/minio/lsync v1.0.1 h1:AVvILxA976xc27hstd1oR+X9PQG0sPSom1MNb1ImfUs=
github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA= github.com/minio/lsync v1.0.1/go.mod h1:tCFzfo0dlvdGl70IT4IAK/5Wtgb0/BrTmo/jE8pArKA=
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c h1:JLr0fYpCleodj9nGB5hfsJU2zPdnNQKqa2bYsIvPhVw= github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c h1:G4ZTNwiiJ73folxqNXZpWQofxus2fGJG7hKxYNrtvRM=
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c/go.mod h1:l9PuOY62zT7AQJqopDjfo/T22AIBJSb2yXPVZf4RlhM= github.com/minio/mc v0.0.0-20200515191050-09c31c4ab28c/go.mod h1:U3Jgk0bcSjn+QPUMisrS6nxCWOoQ6rYWSvLCB30apuU=
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis= github.com/minio/minio v0.0.0-20200421050159-282c9f790a03/go.mod h1:zBua5AiljGs1Irdl2XEyiJjvZVCVDIG8gjozzRBcVlw=
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0 h1:QxIz36O01LbKqJiz6HKeKCOC2afgydspkpahQ807msY= github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0 h1:QxIz36O01LbKqJiz6HKeKCOC2afgydspkpahQ807msY=
github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0/go.mod h1:Vhlqz7Se0EgpgFiVxpvzF4Zz/h2LMx+EPKH96Aera5U= github.com/minio/minio v0.0.0-20200501193630-d1c8e9f31ba0/go.mod h1:Vhlqz7Se0EgpgFiVxpvzF4Zz/h2LMx+EPKH96Aera5U=
github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI= github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI=
github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI= github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22 h1:nZEve4vdUhwHBoV18zRvPDgjL6NYyDJE5QJvz3l9bRs= github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22 h1:nZEve4vdUhwHBoV18zRvPDgjL6NYyDJE5QJvz3l9bRs=
github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI= github.com/minio/minio-go/v6 v6.0.55-0.20200424204115-7506d2996b22/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f h1:ifHrI8+exqLi5RztIWWKS5k+Wu+W7DJisVXwNaCH2zs=
github.com/minio/minio-go/v6 v6.0.56-0.20200502013257-a81c8c13cc3f/go.mod h1:KQMM+/44DSlSGSQWSfRrAZ12FVMmpWNuX37i2AX0jfI=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA= github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61/go.mod h1:4trzEJ7N1nBTd5Tt7OCZT5SEin+WiAXpdJ/WgPkESA8= github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61/go.mod h1:4trzEJ7N1nBTd5Tt7OCZT5SEin+WiAXpdJ/WgPkESA8=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=

View File

@@ -22,13 +22,14 @@ import (
"strings" "strings"
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
"github.com/minio/mcs/pkg/auth" "github.com/go-openapi/swag"
) )
// Authenticate validates websocket header and returns mcs jwt claims // GetTokenFromRequest returns a token from a http Request
// either defined on a cookie `token` or on Authorization header.
// //
// Authorization Header needs to be like "Authorization Bearer <jwt_token>" // Authorization Header needs to be like "Authorization Bearer <jwt_token>"
func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) { func GetTokenFromRequest(r *http.Request) (*string, error) {
// Get Auth token // Get Auth token
var reqToken string var reqToken string
@@ -46,11 +47,5 @@ func Authenticate(r *http.Request) (*auth.DecryptedClaims, error) {
} else { } else {
reqToken = strings.TrimSpace(tokenCookie.Value) reqToken = strings.TrimSpace(tokenCookie.Value)
} }
return swag.String(reqToken), nil
// Perform authentication before upgrading to a Websocket Connection
claims, err := auth.JWTAuthenticate(reqToken)
if err != nil {
return nil, errors.New(http.StatusUnauthorized, err.Error())
}
return claims, nil
} }

File diff suppressed because one or more lines are too long

View File

@@ -38,3 +38,12 @@ export const setCookie = (name: string, val: string) => {
document.cookie = document.cookie =
name + "=" + value + "; expires=" + date.toUTCString() + "; path=/"; name + "=" + value + "; expires=" + date.toUTCString() + "; path=/";
}; };
// timeFromdate gets time string from date input
export const timeFromDate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};

View File

@@ -63,6 +63,7 @@ import { Button, LinearProgress } from "@material-ui/core";
import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel"; import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace"; import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs"; import Logs from "./Logs/Logs";
import Watch from "./Watch/Watch";
function Copyright() { function Copyright() {
return ( return (
@@ -196,7 +197,7 @@ interface IConsoleProps {
class Console extends React.Component< class Console extends React.Component<
IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps IConsoleProps & RouteComponentProps & StyledProps & ThemedComponentProps
> { > {
componentDidMount(): void { componentDidMount(): void {
api api
.invoke("GET", `/api/v1/session`) .invoke("GET", `/api/v1/session`)
@@ -261,20 +262,20 @@ class Console extends React.Component<
<LinearProgress /> <LinearProgress />
</React.Fragment> </React.Fragment>
) : ( ) : (
<React.Fragment> <React.Fragment>
The instance needs to be restarted for configuration changes The instance needs to be restarted for configuration changes
to take effect.{" "} to take effect.{" "}
<Button <Button
color="secondary" color="secondary"
size="small" size="small"
onClick={() => { onClick={() => {
this.restartServer(); this.restartServer();
}} }}
> >
Restart Restart
</Button> </Button>
</React.Fragment> </React.Fragment>
)} )}
</div> </div>
)} )}
<div className={classes.appBarSpacer} /> <div className={classes.appBarSpacer} />
@@ -306,6 +307,7 @@ class Console extends React.Component<
<Route exact path="/webhook/audit" component={WebhookPanel} /> <Route exact path="/webhook/audit" component={WebhookPanel} />
<Route exct path="/trace" component={Trace} /> <Route exct path="/trace" component={Trace} />
<Route exct path="/logs" component={Logs} /> <Route exct path="/logs" component={Logs} />
<Route exct path="/watch" component={Watch} />
<Route exact path="/"> <Route exact path="/">
<Redirect to="/dashboard" /> <Redirect to="/dashboard" />
</Route> </Route>

View File

@@ -15,15 +15,13 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket"; import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import storage from "local-storage-fallback";
import { AppState } from "../../../store"; import { AppState } from "../../../store";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { logMessageReceived, logResetMessages } from "./actions"; import { logMessageReceived, logResetMessages } from "./actions";
import { LogMessage } from "./types"; import { LogMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes } from "../../../common/utils"; import { timeFromDate } from "../../../common/utils";
import Ansi from "ansi-to-react"; import { isNullOrUndefined } from "util";
import { isNull, isNullOrUndefined } from "util";
import { wsProtocol } from "../../../utils/wsUtils"; import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@@ -111,14 +109,6 @@ const Logs = ({
} }
}, [logMessageReceived]); }, [logMessageReceived]);
const timeFromdate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};
// replaces a character of a string with other at a given index // replaces a character of a string with other at a given index
const replaceWeirdChar = ( const replaceWeirdChar = (
origString: string, origString: string,
@@ -146,7 +136,7 @@ const Logs = ({
errorElems.push( errorElems.push(
<li key={`time-${logElement.key}`}> <li key={`time-${logElement.key}`}>
<span className={classes.logerror}> <span className={classes.logerror}>
Time: {timeFromdate(logElement.time)} Time: {timeFromDate(logElement.time)}
</span> </span>
</li> </li>
); );

View File

@@ -18,6 +18,7 @@ import React from "react";
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemIcon from "@material-ui/core/ListItemIcon";
import WebAssetIcon from "@material-ui/icons/WebAsset"; import WebAssetIcon from "@material-ui/icons/WebAsset";
import CenterFocusWeakIcon from '@material-ui/icons/CenterFocusWeak';
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { Divider, Typography, withStyles } from "@material-ui/core"; import { Divider, Typography, withStyles } from "@material-ui/core";
@@ -132,6 +133,12 @@ class Menu extends React.Component<MenuProps> {
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Buckets" /> <ListItemText primary="Buckets" />
</ListItem> </ListItem>
<ListItem button component={NavLink} to="/watch">
<ListItemIcon>
<CenterFocusWeakIcon />
</ListItemIcon>
<ListItemText primary="Watch" />
</ListItem>
<Divider /> <Divider />
<ListItem component={Typography}>Admin</ListItem> <ListItem component={Typography}>Admin</ListItem>
<ListItem button component={NavLink} to="/users"> <ListItem button component={NavLink} to="/users">

View File

@@ -15,13 +15,12 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket"; import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import storage from "local-storage-fallback";
import { AppState } from "../../../store"; import { AppState } from "../../../store";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { traceMessageReceived, traceResetMessages } from "./actions"; import { traceMessageReceived, traceResetMessages } from "./actions";
import { TraceMessage } from "./types"; import { TraceMessage } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes } from "../../../common/utils"; import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils"; import { wsProtocol } from "../../../utils/wsUtils";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@@ -92,14 +91,6 @@ const Trace = ({
} }
}, [traceMessageReceived]); }, [traceMessageReceived]);
const timeFromdate = (d: Date) => {
let h = d.getHours() < 10 ? `0${d.getHours()}` : `${d.getHours()}`;
let m = d.getMinutes() < 10 ? `0${d.getMinutes()}` : `${d.getMinutes()}`;
let s = d.getSeconds() < 10 ? `0${d.getSeconds()}` : `${d.getSeconds()}`;
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};
return ( return (
<div> <div>
<h1>Trace</h1> <h1>Trace</h1>
@@ -108,7 +99,7 @@ const Trace = ({
{messages.map(m => { {messages.map(m => {
return ( return (
<li key={m.key}> <li key={m.key}>
{timeFromdate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "} {timeFromDate(m.time)} - {m.api}[{m.statusCode} {m.statusMsg}]{" "}
{m.api} {m.host} {m.client} {m.callStats.duration} {" "} {m.api} {m.host} {m.client} {m.callStats.duration} {" "}
{niceBytes(m.callStats.rx + "")} {" "} {niceBytes(m.callStats.rx + "")} {" "}
{niceBytes(m.callStats.tx + "")} {niceBytes(m.callStats.tx + "")}

View File

@@ -0,0 +1,259 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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, { useEffect, useState } from "react";
import {
Button,
Grid,
Typography,
TextField
} from "@material-ui/core";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { AppState } from "../../../store";
import { connect } from "react-redux";
import { watchMessageReceived, watchResetMessages } from "./actions";
import { EventInfo, BucketList, Bucket } from "./types";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { niceBytes, timeFromDate } from "../../../common/utils";
import { wsProtocol } from "../../../utils/wsUtils";
import api from "../../../common/api";
import {
FormControl,
MenuItem,
Select,
} from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
watchList: {
background: "white",
maxHeight: "400px",
overflow: "auto",
"& ul": {
margin: "4px",
padding: "0px"
},
"& ul li": {
listStyle: "none",
margin: "0px",
padding: "0px",
borderBottom: "1px solid #dedede"
}
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
}
},
inputField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
marginLeft: 10,
boxShadow: "0px 3px 6px #00000012"
},
fieldContainer: {
background: "#FFFFFF",
padding: 0,
borderRadius: 5,
marginLeft: 10,
textAlign: "left",
minWidth: "206px",
boxShadow: "0px 3px 6px #00000012"
}
});
interface IWatch {
classes: any;
watchMessageReceived: typeof watchMessageReceived;
watchResetMessages: typeof watchResetMessages;
messages: EventInfo[];
}
const Watch = ({
classes,
watchMessageReceived,
watchResetMessages,
messages
}: IWatch) => {
const [start, setStart] = useState(false);
const [bucketName, setBucketName] = useState("Select Bucket");
const [prefix, setPrefix] = useState("");
const [suffix, setSuffix] = useState("");
const [bucketList, setBucketList] = useState<Bucket[]>([]);
const fetchBucketList = () => {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
let buckets: Bucket[] = [];
if (res.buckets !== null) {
buckets = res.buckets;
}
setBucketList(buckets);
})
.catch((err: any) => {
console.log(err);
});
}
useEffect(() => {
fetchBucketList();
}, []);
useEffect(() => {
watchResetMessages();
// begin watch if bucketName in bucketList and start pressed
if (start && bucketList.some(bucket => bucket.name === bucketName)) {
const url = new URL(window.location.toString());
const isDev = process.env.NODE_ENV === "development";
const port = isDev ? "9090" : url.port;
const wsProt = wsProtocol(url.protocol);
const c = new W3CWebSocket(`${wsProt}://${url.hostname}:${port}/ws/watch/${bucketName}?prefix=${prefix}&suffix=${suffix}`);
let interval: any | null = null;
if (c !== null) {
c.onopen = () => {
console.log("WebSocket Client Connected");
c.send("ok");
interval = setInterval(() => {
c.send("ok");
}, 10 * 1000);
};
c.onmessage = (message: IMessageEvent) => {
let m: EventInfo = JSON.parse(message.data.toString());
m.Time = new Date(m.Time.toString());
m.key = Math.random();
watchMessageReceived(m);
};
c.onclose = () => {
clearInterval(interval);
console.log("connection closed by server");
};
return () => {
// close websocket on useEffect cleanup
c.close(1000);
clearInterval(interval);
console.log("closing websockets");
};
}
} else {
// reset start status
setStart(false);
}
}, [watchMessageReceived, start]);
const bucketNames = bucketList.map(bucketName => ({
label: bucketName.name,
value: bucketName.name
}));
return (
<React.Fragment>
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Watch</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<FormControl variant="outlined">
<Select
id="bucket-name"
name="bucket-name"
value={bucketName}
onChange={(e) => { setBucketName(e.target.value as string) }}
className={classes.fieldContainer}
disabled={start}
>
<MenuItem
value={bucketName}
key={`select-bucket-name-default`}
disabled={true}
>
Select Bucket
</MenuItem>
{bucketNames.map(option => (
<MenuItem
value={option.value}
key={`select-bucket-name-${option.label}`}
>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
placeholder="Prefix"
className={classes.inputField}
id="prefix-resource"
label=""
disabled={start}
InputProps={{
disableUnderline: true,
}}
onChange={(e) => { setPrefix(e.target.value) }}
/>
<TextField
placeholder="Suffix"
className={classes.inputField}
id="suffix-resource"
label=""
disabled={start}
InputProps={{
disableUnderline: true,
}}
onChange={(e) => { setSuffix(e.target.value) }}
/>
<Button
type="submit"
variant="contained"
color="primary"
disabled={start}
onClick={() => setStart(true)}
>
Start
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
</Grid>
<div className={classes.watchList}>
<ul>
{messages.map(m => {
return (
<li key={m.key}>
{timeFromDate(m.Time)} - {niceBytes(m.Size + "")} - {m.Type} - {m.Path}
</li>
);
})}
</ul>
</div>
</React.Fragment>
);
};
const mapState = (state: AppState) => ({
messages: state.watch.messages
});
const connector = connect(mapState, {
watchMessageReceived: watchMessageReceived,
watchResetMessages: watchResetMessages
});
export default connector(withStyles(styles)(Watch));

View File

@@ -0,0 +1,47 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 { EventInfo } from "./types";
export const WATCH_MESSAGE_RECEIVED = "WATCH_MESSAGE_RECEIVED";
export const WATCH_RESET_MESSAGES = "WATCH_RESET_MESSAGES";
interface WatchMessageReceivedAction {
type: typeof WATCH_MESSAGE_RECEIVED;
message: EventInfo;
}
interface WatchResetMessagesAction {
type: typeof WATCH_RESET_MESSAGES;
}
export type WatchActionTypes =
| WatchMessageReceivedAction
| WatchResetMessagesAction;
export function watchMessageReceived(message: EventInfo) {
return {
type: WATCH_MESSAGE_RECEIVED,
message: message
};
}
export function watchResetMessages() {
return {
type: WATCH_RESET_MESSAGES
};
}

View File

@@ -0,0 +1,50 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 {
WATCH_MESSAGE_RECEIVED,
WATCH_RESET_MESSAGES,
WatchActionTypes
} from "./actions";
import { EventInfo } from "./types";
export interface WatchState {
messages: EventInfo[];
}
const initialState: WatchState = {
messages: []
};
export function watchReducer(
state = initialState,
action: WatchActionTypes
): WatchState {
switch (action.type) {
case WATCH_MESSAGE_RECEIVED:
return {
...state,
messages: [...state.messages, action.message]
};
case WATCH_RESET_MESSAGES:
return {
...state,
messages: []
};
default:
return state;
}
}

View File

@@ -0,0 +1,36 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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/>.
export interface EventInfo {
Time: Date;
Size: number;
UserMetadata: Map<string, string>;
Path: string;
Type: string;
Host: string;
Port: string;
UserAgent: string;
key: number;
}
export interface Bucket {
name: string;
}
export interface BucketList {
buckets: Bucket[];
total: number;
}

View File

@@ -19,11 +19,13 @@ import thunk from "redux-thunk";
import { systemReducer } from "./reducer"; import { systemReducer } from "./reducer";
import { traceReducer } from "./screens/Console/Trace/reducers"; import { traceReducer } from "./screens/Console/Trace/reducers";
import { logReducer } from "./screens/Console/Logs/reducers"; import { logReducer } from "./screens/Console/Logs/reducers";
import { watchReducer } from "./screens/Console/Watch/reducers";
const globalReducer = combineReducers({ const globalReducer = combineReducers({
system: systemReducer, system: systemReducer,
trace: traceReducer, trace: traceReducer,
logs: logReducer logs: logReducer,
watch: watchReducer,
}); });
declare global { declare global {

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
export const wsProtocol = (protocol: string): string => { export const wsProtocol = (protocol: string): string => {
let wsProtocol = "ws"; let wsProtocol = "ws";
if (protocol == "https:") { if (protocol === "https:") {
wsProtocol = "wss"; wsProtocol = "wss";
} }
return wsProtocol; return wsProtocol;

View File

@@ -39,7 +39,6 @@ func TestAdminConsoleLog(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
adminClient := adminClientMock{} adminClient := adminClientMock{}
mockWSConn := mockConn{} mockWSConn := mockConn{}
wsClientMock := wsClientMock{madmin: adminClient}
function := "startConsoleLog()" function := "startConsoleLog()"
testReceiver := make(chan madmin.LogInfo, 5) testReceiver := make(chan madmin.LogInfo, 5)
@@ -83,7 +82,7 @@ func TestAdminConsoleLog(t *testing.T) {
writesCount++ writesCount++
return nil return nil
} }
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil { if err := startConsoleLog(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} }
// check that the TestReceiver got the same number of data from Console. // check that the TestReceiver got the same number of data from Console.
@@ -95,7 +94,7 @@ func TestAdminConsoleLog(t *testing.T) {
connWriteMessageMock = func(messageType int, data []byte) error { connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write") return fmt.Errorf("error on write")
} }
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) { if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on write", err.Error()) assert.Equal("error on write", err.Error())
} }
@@ -107,7 +106,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""} return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
} }
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) { if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error()) assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
} }
@@ -116,7 +115,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""} return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
} }
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil { if err := startConsoleLog(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} }
@@ -125,7 +124,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""} return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
} }
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); err != nil { if err := startConsoleLog(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} }
@@ -134,7 +133,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, fmt.Errorf("error on read") return 0, []byte{}, fmt.Errorf("error on read")
} }
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) { if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on read", err.Error()) assert.Equal("error on read", err.Error())
} }
@@ -162,7 +161,7 @@ func TestAdminConsoleLog(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil return 0, []byte{}, nil
} }
if err := startConsoleLog(mockWSConn, wsClientMock.madmin); assert.Error(err) { if err := startConsoleLog(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on Console", err.Error()) assert.Equal("error on Console", err.Error())
} }
} }

View File

@@ -40,7 +40,6 @@ func TestAdminTrace(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
adminClient := adminClientMock{} adminClient := adminClientMock{}
mockWSConn := mockConn{} mockWSConn := mockConn{}
wsClientMock := wsClientMock{madmin: adminClient}
function := "startTraceInfo()" function := "startTraceInfo()"
testReceiver := make(chan shortTraceMsg, 5) testReceiver := make(chan shortTraceMsg, 5)
@@ -84,7 +83,7 @@ func TestAdminTrace(t *testing.T) {
writesCount++ writesCount++
return nil return nil
} }
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil { if err := startTraceInfo(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} }
// check that the TestReceiver got the same number of data from trace. // check that the TestReceiver got the same number of data from trace.
@@ -96,7 +95,7 @@ func TestAdminTrace(t *testing.T) {
connWriteMessageMock = func(messageType int, data []byte) error { connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write") return fmt.Errorf("error on write")
} }
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) { if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on write", err.Error()) assert.Equal("error on write", err.Error())
} }
@@ -108,7 +107,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""} return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
} }
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) { if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error()) assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
} }
@@ -117,7 +116,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""} return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
} }
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil { if err := startTraceInfo(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} }
@@ -126,7 +125,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""} return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
} }
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); err != nil { if err := startTraceInfo(mockWSConn, adminClient); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
} }
@@ -135,7 +134,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, fmt.Errorf("error on read") return 0, []byte{}, fmt.Errorf("error on read")
} }
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) { if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on read", err.Error()) assert.Equal("error on read", err.Error())
} }
@@ -163,7 +162,7 @@ func TestAdminTrace(t *testing.T) {
connReadMessageMock = func() (messageType int, p []byte, err error) { connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil return 0, []byte{}, nil
} }
if err := startTraceInfo(mockWSConn, wsClientMock.madmin); assert.Error(err) { if err := startTraceInfo(mockWSConn, adminClient); assert.Error(err) {
assert.Equal("error on trace", err.Error()) assert.Equal("error on trace", err.Error())
} }
} }

View File

@@ -19,6 +19,7 @@ package restapi
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"errors" "errors"
@@ -93,6 +94,7 @@ func (c minioClient) getBucketPolicy(bucketName string) (string, error) {
type MCS3Client interface { type MCS3Client interface {
addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error addNotificationConfig(arn string, events []string, prefix, suffix string, ignoreExisting bool) *probe.Error
removeNotificationConfig(arn string, event string, prefix string, suffix string) *probe.Error removeNotificationConfig(arn string, event string, prefix string, suffix string) *probe.Error
watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
} }
// Interface implementation // Interface implementation
@@ -113,6 +115,10 @@ func (c mcS3Client) removeNotificationConfig(arn string, event string, prefix st
return c.client.RemoveNotificationConfig(arn, event, prefix, suffix) return c.client.RemoveNotificationConfig(arn, event, prefix, suffix)
} }
func (c mcS3Client) watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
return c.client.Watch(options)
}
// MCSCredentials interface with all functions to be implemented // MCSCredentials interface with all functions to be implemented
// by mock when testing, it should include all needed minioCredentials.Credentials api calls // by mock when testing, it should include all needed minioCredentials.Credentials api calls
// that are used within this project. // that are used within this project.
@@ -224,19 +230,23 @@ func newMinioClient(jwt string) (*minio.Client, error) {
} }
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket // newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
func newS3BucketClient(bucketName *string) (*mc.S3Client, error) { func newS3BucketClient(jwt string, bucketName string) (*mc.S3Client, error) {
endpoint := getMinIOServer() endpoint := getMinIOServer()
accessKeyID := getAccessKey()
secretAccessKey := getSecretKey()
useSSL := getMinIOEndpointIsSecure() useSSL := getMinIOEndpointIsSecure()
if bucketName != nil { claims, err := auth.JWTAuthenticate(jwt)
endpoint += fmt.Sprintf("/%s", *bucketName)
}
s3Config := newS3Config(endpoint, accessKeyID, secretAccessKey, !useSSL)
client, err := mc.S3New(s3Config)
if err != nil { if err != nil {
return nil, err.Cause return nil, err
}
if strings.TrimSpace(bucketName) != "" {
endpoint += fmt.Sprintf("/%s", bucketName)
}
s3Config := newS3Config(endpoint, claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken, !useSSL)
client, pErr := mc.S3New(s3Config)
if pErr != nil {
return nil, pErr.Cause
} }
s3Client, ok := client.(*mc.S3Client) s3Client, ok := client.(*mc.S3Client)
if !ok { if !ok {
@@ -248,7 +258,7 @@ func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
// newS3Config simply creates a new Config struct using the passed // newS3Config simply creates a new Config struct using the passed
// parameters. // parameters.
func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Config { func newS3Config(endpoint, accessKey, secretKey, sessionToken string, isSecure bool) *mc.Config {
// We have a valid alias and hostConfig. We populate the // We have a valid alias and hostConfig. We populate the
// minioCredentials from the match found in the config file. // minioCredentials from the match found in the config file.
s3Config := new(mc.Config) s3Config := new(mc.Config)
@@ -262,6 +272,7 @@ func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Confi
s3Config.HostURL = endpoint s3Config.HostURL = endpoint
s3Config.AccessKey = accessKey s3Config.AccessKey = accessKey
s3Config.SecretKey = secretKey s3Config.SecretKey = secretKey
s3Config.SessionToken = sessionToken
s3Config.Signature = "S3v4" s3Config.Signature = "S3v4"
return s3Config return s3Config
} }

View File

@@ -40,14 +40,16 @@ func registerBucketEventsHandlers(api *operations.McsAPI) {
}) })
// create bucket event // create bucket event
api.UserAPICreateBucketEventHandler = user_api.CreateBucketEventHandlerFunc(func(params user_api.CreateBucketEventParams, principal *models.Principal) middleware.Responder { api.UserAPICreateBucketEventHandler = user_api.CreateBucketEventHandlerFunc(func(params user_api.CreateBucketEventParams, principal *models.Principal) middleware.Responder {
if err := getCreateBucketEventsResponse(params.BucketName, params.Body); err != nil { sessionID := string(*principal)
if err := getCreateBucketEventsResponse(sessionID, params.BucketName, params.Body); err != nil {
return user_api.NewCreateBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) return user_api.NewCreateBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
} }
return user_api.NewCreateBucketEventCreated() return user_api.NewCreateBucketEventCreated()
}) })
// delete bucket event // delete bucket event
api.UserAPIDeleteBucketEventHandler = user_api.DeleteBucketEventHandlerFunc(func(params user_api.DeleteBucketEventParams, principal *models.Principal) middleware.Responder { api.UserAPIDeleteBucketEventHandler = user_api.DeleteBucketEventHandlerFunc(func(params user_api.DeleteBucketEventParams, principal *models.Principal) middleware.Responder {
if err := getDeleteBucketEventsResponse(params.BucketName, params.Arn, params.Body.Events, params.Body.Prefix, params.Body.Suffix); err != nil { sessionID := string(*principal)
if err := getDeleteBucketEventsResponse(sessionID, params.BucketName, params.Arn, params.Body.Events, params.Body.Prefix, params.Body.Suffix); err != nil {
return user_api.NewDeleteBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())}) return user_api.NewDeleteBucketEventDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
} }
return user_api.NewDeleteBucketEventNoContent() return user_api.NewDeleteBucketEventNoContent()
@@ -178,8 +180,8 @@ func createBucketEvent(client MCS3Client, arn string, notificationEvents []model
} }
// getCreateBucketEventsResponse calls createBucketEvent to add a bucket event notification // getCreateBucketEventsResponse calls createBucketEvent to add a bucket event notification
func getCreateBucketEventsResponse(bucketName string, eventReq *models.BucketEventRequest) error { func getCreateBucketEventsResponse(sessionID, bucketName string, eventReq *models.BucketEventRequest) error {
s3Client, err := newS3BucketClient(swag.String(bucketName)) s3Client, err := newS3BucketClient(sessionID, bucketName)
if err != nil { if err != nil {
log.Println("error creating S3Client:", err) log.Println("error creating S3Client:", err)
return err return err
@@ -214,8 +216,8 @@ func joinNotificationEvents(events []models.NotificationEventType) string {
} }
// getDeleteBucketEventsResponse calls deleteBucketEventNotification() to delete a bucket event notification // getDeleteBucketEventsResponse calls deleteBucketEventNotification() to delete a bucket event notification
func getDeleteBucketEventsResponse(bucketName string, arn string, events []models.NotificationEventType, prefix, suffix *string) error { func getDeleteBucketEventsResponse(sessionID, bucketName string, arn string, events []models.NotificationEventType, prefix, suffix *string) error {
s3Client, err := newS3BucketClient(swag.String(bucketName)) s3Client, err := newS3BucketClient(sessionID, bucketName)
if err != nil { if err != nil {
log.Println("error creating S3Client:", err) log.Println("error creating S3Client:", err)
return err return err

165
restapi/user_watch.go Normal file
View File

@@ -0,0 +1,165 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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"
"encoding/json"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"sync"
"github.com/gorilla/websocket"
mc "github.com/minio/mc/cmd"
)
type watchOptions struct {
BucketName string
mc.WatchOptions
}
// startWatch starts by setting a websocket reader that
// will check for a heartbeat.
//
// A WaitGroup is used to handle goroutines and to ensure
// all finish in the proper order. If any, sendWatchInfo()
// or wsReadCheck() returns, watch should end.
func startWatch(conn WSConn, client MCS3Client, options watchOptions) (mError error) {
// a WaitGroup waits for a collection of goroutines to finish
wg := sync.WaitGroup{}
// a cancel context is needed to end all goroutines used
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Set number of goroutines to wait. wg.Wait()
// waits until counter is zero (all are done)
wg.Add(3)
// start go routine for reading websocket heartbeat
readErr := wsReadCheck(ctx, &wg, conn)
// send Stream of watch events to the ws c.connection
ch := sendWatchInfo(ctx, &wg, conn, client, options)
// If wsReadCheck returns it means that it is not possible to check
// ws heartbeat anymore so we stop from doing Watch, cancel context
// for all goroutines.
go func(wg *sync.WaitGroup) {
defer wg.Done()
if err := <-readErr; err != nil {
log.Println("error on wsReadCheck:", err)
mError = err
}
// cancel context for all goroutines.
cancel()
}(&wg)
if err := <-ch; err != nil {
mError = err
}
// if ch closes for any reason,
// cancel context for all goroutines
cancel()
// wait all goroutines to finish
wg.Wait()
return mError
}
// sendWatchInfo sends stream of Watch Event to the ws connection
func sendWatchInfo(ctx context.Context, wg *sync.WaitGroup, conn WSConn, wsc MCS3Client, options watchOptions) <-chan error {
// decrements the WaitGroup counter
// by one when the function returns
defer wg.Done()
ch := make(chan error)
go func(ch chan<- error) {
defer close(ch)
wo, pErr := wsc.watch(options.WatchOptions)
if pErr != nil {
fmt.Println("error initializing watch:", pErr.Cause)
ch <- pErr.Cause
return
}
for {
select {
case <-ctx.Done():
close(wo.DoneChan)
return
case events, ok := <-wo.Events():
// zero value returned because the channel is closed and empty
if !ok {
return
}
for _, event := range events {
// Serialize message to be sent
bytes, err := json.Marshal(event)
if err != nil {
fmt.Println("error on json.Marshal:", err)
ch <- err
return
}
// Send Message through websocket connection
err = conn.writeMessage(websocket.TextMessage, bytes)
if err != nil {
log.Println("error writeMessage:", err)
ch <- err
return
}
}
case pErr, ok := <-wo.Errors():
// zero value returned because the channel is closed and empty
if !ok {
return
}
if pErr != nil {
log.Println("error on watch:", pErr.Cause)
ch <- pErr.Cause
return
}
}
}
}(ch)
return ch
}
// getOptionsFromReq gets bucket name, events, prefix, suffix from a websocket
// watch path if defined.
// path come as : `/watch/bucket1` and query params come on request form
func getOptionsFromReq(req *http.Request) watchOptions {
wOptions := watchOptions{}
// Default Events if not defined
wOptions.Events = []string{"put", "get", "delete"}
re := regexp.MustCompile(`(/watch/)(.*?$)`)
matches := re.FindAllSubmatch([]byte(req.URL.Path), -1)
// len matches is always 3
// matches comes as e.g.
// [["...", "/watch/" "bucket1"]]
// [["/watch/" "/watch/" ""]]
// bucket name is on the second group, third position
wOptions.BucketName = strings.TrimSpace(string(matches[0][2]))
events := req.FormValue("events")
if strings.TrimSpace(events) != "" {
wOptions.Events = strings.Split(events, ",")
}
wOptions.Prefix = req.FormValue("prefix")
wOptions.Suffix = req.FormValue("suffix")
return wOptions
}

291
restapi/user_watch_test.go Normal file
View File

@@ -0,0 +1,291 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 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 (
"encoding/json"
"fmt"
"net/http"
"net/url"
"testing"
"github.com/gorilla/websocket"
mc "github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
"github.com/stretchr/testify/assert"
)
// assigning mock at runtime instead of compile time
var mcWatchMock func(options mc.WatchOptions) (*mc.WatchObject, *probe.Error)
// implements mc.S3Client.Watch()
func (c s3ClientMock) watch(options mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
return mcWatchMock(options)
}
func TestWatch(t *testing.T) {
assert := assert.New(t)
client := s3ClientMock{}
mockWSConn := mockConn{}
function := "startWatch()"
testStreamSize := 5
testReceiver := make(chan []mc.EventInfo, testStreamSize)
textToReceive := "test message"
testOptions := watchOptions{}
testOptions.BucketName = "bucktest"
testOptions.Prefix = "file/"
testOptions.Suffix = ".png"
// Test-1: Serve Watch with no errors until Watch finishes sending
// define mock function behavior
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
wo := &mc.WatchObject{
EventInfoChan: make(chan []mc.EventInfo),
ErrorChan: make(chan *probe.Error),
DoneChan: make(chan struct{}),
}
// Only success, start a routine to start reading line by line.
go func(wo *mc.WatchObject) {
defer wo.Close()
lines := make([]int, testStreamSize)
// mocking sending 5 lines of info
for range lines {
info := []mc.EventInfo{
mc.EventInfo{
UserAgent: textToReceive,
},
}
wo.Events() <- info
}
}(wo)
return wo, nil
}
// mock function of conn.ReadMessage(), no error on read
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil
}
writesCount := 1
// mock connection WriteMessage() no error
connWriteMessageMock = func(messageType int, data []byte) error {
// emulate that receiver gets the message written
var t []mc.EventInfo
_ = json.Unmarshal(data, &t)
if writesCount == testStreamSize {
// for testing we need to close the receiver channel
close(testReceiver)
return nil
}
testReceiver <- t
writesCount++
return nil
}
if err := startWatch(mockWSConn, client, testOptions); 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.
for i := range testReceiver {
for _, val := range i {
assert.Equal(textToReceive, val.UserAgent)
}
}
// Test-2: if error happens while writing, return error
connWriteMessageMock = func(messageType int, data []byte) error {
return fmt.Errorf("error on write")
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("error on write", err.Error())
}
// Test-3: error happens while reading, unexpected Close Error should return error.
connWriteMessageMock = func(messageType int, data []byte) error {
return nil
}
// mock function of conn.ReadMessage(), returns unexpected Close Error CloseAbnormalClosure
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseAbnormalClosure, Text: ""}
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("websocket: close 1006 (abnormal closure)", err.Error())
}
// Test-4: error happens while reading, expected Close Error NormalClosure
// expected Close Error should not return an error, just end Console.
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: ""}
}
if err := startWatch(mockWSConn, client, testOptions); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-5: error happens while reading, expected Close Error CloseGoingAway
// expected Close Error should not return an error, just return.
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, &websocket.CloseError{Code: websocket.CloseGoingAway, Text: ""}
}
if err := startWatch(mockWSConn, client, testOptions); err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
// Test-6: error happens while reading, non Close Error Type should be returned as
// error
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, fmt.Errorf("error on read")
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("error on read", err.Error())
}
// Test-7: error happens on Watch, watch should stop
// and error shall be returned.
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
wo := &mc.WatchObject{
EventInfoChan: make(chan []mc.EventInfo),
ErrorChan: make(chan *probe.Error),
DoneChan: make(chan struct{}),
}
// Only success, start a routine to start reading line by line.
go func(wo *mc.WatchObject) {
defer wo.Close()
lines := make([]int, testStreamSize)
// mocking sending 5 lines of info
for range lines {
info := []mc.EventInfo{
mc.EventInfo{
UserAgent: textToReceive,
},
}
wo.Events() <- info
}
wo.Errors() <- &probe.Error{Cause: fmt.Errorf("error on Watch")}
}(wo)
return wo, nil
}
// mock function of conn.ReadMessage(), no error on read, should stay unless
// context is done.
connReadMessageMock = func() (messageType int, p []byte, err error) {
return 0, []byte{}, nil
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("error on Watch", err.Error())
}
// Test-8: error happens on Watch, watch should stop
// and error shall be returned.
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
return nil, &probe.Error{Cause: fmt.Errorf("error on Watch")}
}
if err := startWatch(mockWSConn, client, testOptions); assert.Error(err) {
assert.Equal("error on Watch", err.Error())
}
// Test-9: return nil on error on Watch
mcWatchMock = func(params mc.WatchOptions) (*mc.WatchObject, *probe.Error) {
wo := &mc.WatchObject{
EventInfoChan: make(chan []mc.EventInfo),
ErrorChan: make(chan *probe.Error),
DoneChan: make(chan struct{}),
}
// Only success, start a routine to start reading line by line.
go func(wo *mc.WatchObject) {
defer wo.Close()
lines := make([]int, testStreamSize)
// mocking sending 5 lines of info
for range lines {
info := []mc.EventInfo{
mc.EventInfo{
UserAgent: textToReceive,
},
}
wo.Events() <- info
}
wo.Events() <- nil
wo.Errors() <- nil
}(wo)
return wo, nil
}
if err := startWatch(mockWSConn, client, testOptions); 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.
for i := range testReceiver {
for _, val := range i {
assert.Equal(textToReceive, val.UserAgent)
}
}
// Test-9: getOptionsFromReq return parameters from path
u, err := url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=put,get")
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
}
req := &http.Request{
URL: u,
}
opts := getOptionsFromReq(req)
expectedOptions := watchOptions{
BucketName: "bucket1",
}
expectedOptions.Prefix = ""
expectedOptions.Suffix = ".jpg"
expectedOptions.Events = []string{"put", "get"}
assert.Equal(expectedOptions.BucketName, opts.BucketName)
assert.Equal(expectedOptions.Prefix, opts.Prefix)
assert.Equal(expectedOptions.Suffix, opts.Suffix)
assert.Equal(expectedOptions.Events, opts.Events)
// Test-9: getOptionsFromReq return default events if not defined
u, err = url.Parse("http://localhost/api/v1/watch/bucket1?prefix=&suffix=.jpg&events=")
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
}
req = &http.Request{
URL: u,
}
opts = getOptionsFromReq(req)
expectedOptions = watchOptions{
BucketName: "bucket1",
}
expectedOptions.Prefix = ""
expectedOptions.Suffix = ".jpg"
expectedOptions.Events = []string{"put", "get", "delete"}
assert.Equal(expectedOptions.BucketName, opts.BucketName)
assert.Equal(expectedOptions.Prefix, opts.Prefix)
assert.Equal(expectedOptions.Suffix, opts.Suffix)
assert.Equal(expectedOptions.Events, opts.Events)
// Test-10: getOptionsFromReq return default events if not defined
u, err = url.Parse("http://localhost/api/v1/watch/bucket2?prefix=&suffix=")
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", "url.Parse()", err.Error())
}
req = &http.Request{
URL: u,
}
opts = getOptionsFromReq(req)
expectedOptions = watchOptions{
BucketName: "bucket2",
}
expectedOptions.Events = []string{"put", "get", "delete"}
assert.Equal(expectedOptions.BucketName, opts.BucketName)
assert.Equal(expectedOptions.Prefix, opts.Prefix)
assert.Equal(expectedOptions.Suffix, opts.Suffix)
assert.Equal(expectedOptions.Events, opts.Events)
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/minio/mcs/pkg/auth"
"github.com/minio/mcs/pkg/ws" "github.com/minio/mcs/pkg/ws"
) )
@@ -46,17 +47,29 @@ const (
maxMessageSize = 512 maxMessageSize = 512
) )
// MCSWebsocket interface of a Websocket Client // MCSWebsocketAdmin interface of a Websocket Client
type MCSWebsocket interface { type MCSWebsocketAdmin interface {
// start trace info from servers
trace() trace()
console()
} }
type wsClient struct { type wsAdminClient struct {
// websocket connection. // websocket connection.
conn wsConn conn wsConn
// MinIO admin Client // MinIO admin Client
madmin MinioAdmin client MinioAdmin
}
// MCSWebsocket interface of a Websocket Client
type MCSWebsocket interface {
watch(options watchOptions)
}
type wsS3Client struct {
// websocket connection.
conn wsConn
// mcS3Client
client MCS3Client
} }
// WSConn interface with all functions to be implemented // WSConn interface with all functions to be implemented
@@ -106,14 +119,18 @@ func (c wsConn) readMessage() (messageType int, p []byte, err error) {
// Websocket communication will be done depending // Websocket communication will be done depending
// on the path. // on the path.
// Request should come like ws://<host>:<port>/ws/<api> // Request should come like ws://<host>:<port>/ws/<api>
//
// TODO: Enable CORS
func serveWS(w http.ResponseWriter, req *http.Request) { func serveWS(w http.ResponseWriter, req *http.Request) {
sessionID, err := ws.GetTokenFromRequest(req)
if err != nil {
errors.ServeError(w, req, err)
return
}
// Perform authentication before upgrading to a Websocket Connection
// authenticate WS connection with MCS // authenticate WS connection with MCS
claims, err := ws.Authenticate(req) claims, err := auth.JWTAuthenticate(*sessionID)
if err != nil { if err != nil {
log.Print("error on ws authentication: ", err) log.Print("error on ws authentication: ", err)
errors.ServeError(w, req, err) errors.ServeError(w, req, errors.New(http.StatusUnauthorized, err.Error()))
return return
} }
@@ -125,28 +142,30 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
return return
} }
// Only start Websocket Interaction after user has been wsPath := strings.TrimPrefix(req.URL.Path, wsBasePath)
// authenticated with MinIO switch {
mAdmin, err := newAdminFromClaims(claims) case wsPath == "/trace":
if err != nil { wsAdminClient, err := newWebSocketAdminClient(conn, claims)
log.Println("error creating Madmin Client:", err) if err != nil {
errors.ServeError(w, req, err) errors.ServeError(w, req, err)
return return
} }
// create a minioClient interface implementation go wsAdminClient.trace()
// defining the client to be used case wsPath == "/console":
adminClient := adminClient{client: mAdmin} wsAdminClient, err := newWebSocketAdminClient(conn, claims)
// create a websocket connection interface implementation if err != nil {
// defining the connection to be used errors.ServeError(w, req, err)
wsConnection := wsConn{conn: conn} return
}
// create websocket client and handle request go wsAdminClient.console()
wsClient := &wsClient{conn: wsConnection, madmin: adminClient} case strings.HasPrefix(wsPath, `/watch`):
switch strings.TrimPrefix(req.URL.Path, wsBasePath) { wOptions := getOptionsFromReq(req)
case "/trace": wsS3Client, err := newWebSocketS3Client(conn, *sessionID, wOptions.BucketName)
go wsClient.trace() if err != nil {
case "/console": errors.ServeError(w, req, err)
go wsClient.console() return
}
go wsS3Client.watch(wOptions)
default: default:
// path not found // path not found
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
@@ -154,6 +173,51 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
} }
} }
// newWebSocketAdminClient returns a wsAdminClient authenticated as an admin user
func newWebSocketAdminClient(conn *websocket.Conn, autClaims *auth.DecryptedClaims) (*wsAdminClient, error) {
// Only start Websocket Interaction after user has been
// authenticated with MinIO
mAdmin, err := newAdminFromClaims(autClaims)
if err != nil {
log.Println("error creating Madmin Client:", err)
// close connection
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
conn.Close()
return nil, err
}
// create a websocket connection interface implementation
// defining the connection to be used
wsConnection := wsConn{conn: conn}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
// create websocket client and handle request
wsAdminClient := &wsAdminClient{conn: wsConnection, client: adminClient}
return wsAdminClient, nil
}
// newWebSocketS3Client returns a wsAdminClient authenticated as MCS admin
func newWebSocketS3Client(conn *websocket.Conn, jwt, bucketName string) (*wsS3Client, error) {
// Only start Websocket Interaction after user has been
// authenticated with MinIO
s3Client, err := newS3BucketClient(jwt, bucketName)
if err != nil {
log.Println("error creating S3Client:", err)
conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
conn.Close()
return nil, err
}
// create a websocket connection interface implementation
// defining the connection to be used
wsConnection := wsConn{conn: conn}
// create a s3Client interface implementation
// defining the client to be used
mcS3C := mcS3Client{client: s3Client}
// create websocket client and handle request
wsS3Client := &wsS3Client{conn: wsConnection, client: mcS3C}
return wsS3Client, nil
}
// wsReadCheck ensures that the client is sending a message // wsReadCheck ensures that the client is sending a message
// every `pingWait` seconds. If deadline exceeded or an error // every `pingWait` seconds. If deadline exceeded or an error
// happened this will return, meaning it won't be able to ensure // happened this will return, meaning it won't be able to ensure
@@ -206,7 +270,7 @@ func wsReadCheck(ctx context.Context, wg *sync.WaitGroup, conn WSConn) chan erro
// trace serves madmin.ServiceTraceInfo // trace serves madmin.ServiceTraceInfo
// on a Websocket connection. // on a Websocket connection.
func (wsc *wsClient) trace() { func (wsc *wsAdminClient) trace() {
defer func() { defer func() {
log.Println("trace stopped") log.Println("trace stopped")
// close connection after return // close connection after return
@@ -214,7 +278,7 @@ func (wsc *wsClient) trace() {
}() }()
log.Println("trace started") log.Println("trace started")
err := startTraceInfo(wsc.conn, wsc.madmin) err := startTraceInfo(wsc.conn, wsc.client)
// Send Connection Close Message indicating the Status Code // Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45 // see https://tools.ietf.org/html/rfc6455#page-45
if err != nil { if err != nil {
@@ -237,7 +301,7 @@ func (wsc *wsClient) trace() {
// console serves madmin.GetLogs // console serves madmin.GetLogs
// on a Websocket connection. // on a Websocket connection.
func (wsc *wsClient) console() { func (wsc *wsAdminClient) console() {
defer func() { defer func() {
log.Println("console logs stopped") log.Println("console logs stopped")
// close connection after return // close connection after return
@@ -245,7 +309,36 @@ func (wsc *wsClient) console() {
}() }()
log.Println("console logs started") log.Println("console logs started")
err := startConsoleLog(wsc.conn, wsc.madmin) err := startConsoleLog(wsc.conn, wsc.client)
// Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45
if err != nil {
// If connection exceeded read deadline send Close
// Message Policy Violation code since we don't want
// to let the receiver figure out the read deadline.
// This is a generic code designed if there is a
// need to hide specific details about the policy.
if nErr, ok := err.(net.Error); ok && nErr.Timeout() {
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.ClosePolicyViolation, ""))
return
}
// else, internal server error
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseInternalServerErr, err.Error()))
return
}
// normal closure
wsc.conn.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
}
func (wsc *wsS3Client) watch(params watchOptions) {
defer func() {
log.Println("watch stopped")
// close connection after return
wsc.conn.close()
}()
log.Println("watch started")
err := startWatch(wsc.conn, wsc.client, params)
// Send Connection Close Message indicating the Status Code // Send Connection Close Message indicating the Status Code
// see https://tools.ietf.org/html/rfc6455#page-45 // see https://tools.ietf.org/html/rfc6455#page-45
if err != nil { if err != nil {

View File

@@ -51,18 +51,3 @@ func (c mockConn) setReadDeadline(t time.Time) error {
} }
func (c mockConn) setPongHandler(h func(appData string) error) { func (c mockConn) setPongHandler(h func(appData string) error) {
} }
// Common mocks for MCSWebsocket interface
// assigning mock at runtime instead of compile time
var wsTraceMock func()
// Define a mock struct of wsClient interface implementation
type wsClientMock struct {
// MinIO admin Client
madmin MinioAdmin
}
// mock function of wsc.trace()
func (wsc wsClientMock) trace() {
wsTraceMock()
}