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:
4
go.mod
4
go.mod
@@ -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
10
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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
@@ -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()}`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 + "")}
|
||||||
|
|||||||
259
portal-ui/src/screens/Console/Watch/Watch.tsx
Normal file
259
portal-ui/src/screens/Console/Watch/Watch.tsx
Normal 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));
|
||||||
47
portal-ui/src/screens/Console/Watch/actions.ts
Normal file
47
portal-ui/src/screens/Console/Watch/actions.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
50
portal-ui/src/screens/Console/Watch/reducers.ts
Normal file
50
portal-ui/src/screens/Console/Watch/reducers.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
portal-ui/src/screens/Console/Watch/types.ts
Normal file
36
portal-ui/src/screens/Console/Watch/types.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
165
restapi/user_watch.go
Normal 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
291
restapi/user_watch_test.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user