Tenant health info upload (#2606)

For registered clusters, after generating the Health Info report, Health Info is uploaded to Subnet, and latest metrics are visible in Subnet.
Authored-by: Jillian Inapurapu <jillii@Jillians-MBP.attlocal.net>
This commit is contained in:
jinapurapu
2023-03-17 09:42:01 -07:00
committed by GitHub
parent 8b1b2b1e2d
commit c9ac525358
7 changed files with 174 additions and 40 deletions

View File

@@ -18,11 +18,13 @@ package subnet
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
xhttp "github.com/minio/console/pkg/http"
@@ -64,6 +66,62 @@ func LogWebhookURL() string {
return subnetBaseURL() + "/api/logs"
}
func UploadURL(uploadType string, filename string) string {
return fmt.Sprintf("%s/api/%s/upload?filename=%s", subnetBaseURL(), uploadType, filename)
}
func UploadAuthHeaders(apiKey string) map[string]string {
return map[string]string{"x-subnet-api-key": apiKey}
}
func UploadFileToSubnet(info interface{}, client *xhttp.Client, filename string, reqURL string, headers map[string]string) (string, error) {
req, e := subnetUploadReq(info, reqURL, filename)
if e != nil {
return "", e
}
resp, e := subnetReqDo(client, req, headers)
return resp, e
}
func subnetUploadReq(info interface{}, url string, filename string) (*http.Request, error) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
zipWriter := gzip.NewWriter(&body)
version := "3"
enc := json.NewEncoder(zipWriter)
header := struct {
Version string `json:"version"`
}{Version: version}
if e := enc.Encode(header); e != nil {
return nil, e
}
if e := enc.Encode(info); e != nil {
return nil, e
}
zipWriter.Close()
temp := body
part, e := writer.CreateFormFile("file", filename)
if e != nil {
return nil, e
}
if _, e = io.Copy(part, &temp); e != nil {
return nil, e
}
writer.Close()
r, e := http.NewRequest(http.MethodPost, url, &body)
if e != nil {
return nil, e
}
r.Header.Add("Content-Type", writer.FormDataContentType())
return r, nil
}
func GenerateRegToken(clusterRegInfo mc.ClusterRegistrationInfo) (string, error) {
token, e := json.Marshal(clusterRegInfo)
if e != nil {

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import clsx from "clsx";
import {
ICloseEvent,
IMessageEvent,
@@ -72,7 +72,7 @@ const styles = (theme: Theme) =>
color: "#07193E",
fontWeight: "bold",
textAlign: "center",
marginBottom: 10,
marginBottom: 20,
},
progressResult: {
textAlign: "center",
@@ -94,8 +94,6 @@ const styles = (theme: Theme) =>
interface IHealthInfo {
classes: any;
namespace: string;
tenant: string;
}
const HealthInfo = ({ classes }: IHealthInfo) => {
@@ -104,22 +102,19 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
const message = useSelector((state: AppState) => state.healthInfo.message);
const clusterRegistered = registeredCluster();
const serverDiagnosticStatus = useSelector(
(state: AppState) => state.system.serverDiagnosticStatus
);
const [startDiagnostic, setStartDiagnostic] = useState(false);
const [downloadDisabled, setDownloadDisabled] = useState(true);
const [localMessage, setMessage] = useState<string>("");
const [buttonStartText, setButtonStartText] =
useState<string>("Start Diagnostic");
const [title, setTitle] = useState<string>("New Diagnostic");
const [diagFileContent, setDiagFileContent] = useState<string>("");
const isDiagnosticComplete =
serverDiagnosticStatus === DiagStatSuccess ||
serverDiagnosticStatus === DiagStatError;
const [subnetResponse, setSubnetResponse] = useState<string>("");
const clusterRegistered = registeredCluster();
const download = () => {
let element = document.createElement("a");
@@ -195,7 +190,6 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
const c = new W3CWebSocket(
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/health-info?deadline=1h`
);
let interval: any | null = null;
if (c !== null) {
c.onopen = () => {
@@ -220,6 +214,9 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
if (m.encoded !== "") {
setDiagFileContent(m.encoded);
}
if (m.subnetResponse) {
setSubnetResponse(m.subnetResponse);
}
};
c.onerror = (error: Error) => {
console.log("error closing websocket:", error.message);
@@ -275,38 +272,70 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
className={classes.progressResult}
>
<div className={classes.localMessage}>{localMessage}</div>
<div className={classes.progressResult}>
{" "}
{subnetResponse !== "" &&
!subnetResponse.toLowerCase().includes("error") && (
<Grid item xs={12} className={classes.serversData}>
<strong>
Health report uploaded to Subnet successfully!
</strong>
&nbsp;{" "}
<strong>
See the results on your{" "}
<a href={subnetResponse}>Subnet Dashboard</a>{" "}
</strong>
</Grid>
)}
{(subnetResponse === "" ||
subnetResponse.toLowerCase().includes("error")) &&
serverDiagnosticStatus === DiagStatSuccess && (
<Grid item xs={12} className={classes.serversData}>
<strong>
Something went wrong uploading your Health report to
Subnet.
</strong>
&nbsp;{" "}
<strong>
Log into your{" "}
<a href="https://subnet.min.io">Subnet Account</a> to
manually upload your Health report.
</strong>
</Grid>
)}
</div>
{serverDiagnosticStatus === DiagStatInProgress ? (
<div className={classes.loading}>
<Loader style={{ width: 25, height: 25 }} />
</div>
) : (
<Fragment>
{serverDiagnosticStatus !== DiagStatError &&
!downloadDisabled && (
<Grid container justifyItems={"flex-start"}>
<Grid item xs={6}>
{serverDiagnosticStatus !== DiagStatError &&
!downloadDisabled && (
<Button
id={"download"}
type="submit"
variant="callAction"
onClick={() => download()}
disabled={downloadDisabled}
label={"Download"}
/>
)}
</Grid>
<Grid item xs={6}>
<Button
id={"download"}
id="start-new-diagnostic"
type="submit"
variant="callAction"
onClick={() => download()}
disabled={downloadDisabled}
label={"Download"}
variant={
!clusterRegistered ? "regular" : "callAction"
}
disabled={startDiagnostic}
onClick={startDiagnosticAction}
label={buttonStartText}
/>
)}
<Grid
item
xs={12}
className={clsx(classes.startDiagnostic, {
[classes.startDiagnosticCenter]: !isDiagnosticComplete,
})}
>
<Button
id="start-new-diagnostic"
type="submit"
variant={!clusterRegistered ? "regular" : "callAction"}
disabled={startDiagnostic}
onClick={startDiagnosticAction}
label={buttonStartText}
/>
</Grid>
</Grid>
</Fragment>
)}
@@ -322,7 +351,12 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
"During the health diagnostics run, all production traffic will be suspended."
}
iconComponent={<WarnIcon />}
help={<Fragment />}
help={
<Fragment>
Cluster Health Report will be uploaded to Subnet, and is
viewable from your Subnet Diagnostics dashboard.
</Fragment>
}
/>
</Fragment>
)}

View File

@@ -29,6 +29,7 @@ export interface HealthInfoMessage {
export interface ReportMessage {
encoded: string;
serverHealthInfo: HealthInfoMessage;
subnetResponse: string;
}
export interface perfInfo {

View File

@@ -147,7 +147,7 @@ export const systemSlice = createSlice({
setSiteReplicationInfo: (state, action: PayloadAction<SRInfoStateType>) => {
state.siteReplicationInfo = action.payload;
},
setLicenseInfo: (state, action: PayloadAction<SubnetInfo | null>) => {
setSystemLicenseInfo: (state, action: PayloadAction<SubnetInfo | null>) => {
state.licenseInfo = action.payload;
},
setOverrideStyles: (
@@ -181,7 +181,7 @@ export const {
setServerDiagStat,
globalSetDistributedSetup,
setSiteReplicationInfo,
setLicenseInfo,
setSystemLicenseInfo,
setOverrideStyles,
setAnonymousMode,
resetSystem,

View File

@@ -22,11 +22,14 @@ import (
b64 "encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/klauspost/compress/gzip"
xhttp "github.com/minio/console/pkg/http"
subnet "github.com/minio/console/pkg/subnet"
"github.com/minio/madmin-go/v2"
"github.com/minio/websocket"
)
@@ -51,7 +54,6 @@ func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadli
madmin.HealthDataTypeSysNet,
madmin.HealthDataTypeSysProcess,
}
var err error
// Fetch info of all servers (cluster or single server)
healthInfo, version, err := client.serverHealthInfo(ctx, healthDataTypes, *deadline)
@@ -68,12 +70,19 @@ func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadli
type messageReport struct {
Encoded string `json:"encoded"`
ServerHealthInfo interface{} `json:"serverHealthInfo"`
SubnetResponse string `json:"subnetResponse"`
}
subnetResp, err := sendHealthInfoToSubnet(ctx, healthInfo, client)
report := messageReport{
Encoded: encodedDiag,
ServerHealthInfo: healthInfo,
SubnetResponse: subnetResp,
}
if err != nil {
report.SubnetResponse = fmt.Sprintf("Error: %s", err.Error())
}
message, err := json.Marshal(report)
if err != nil {
return err
@@ -117,3 +126,35 @@ func getHealthInfoOptionsFromReq(req *http.Request) (*time.Duration, error) {
}
return &deadlineDuration, nil
}
func sendHealthInfoToSubnet(ctx context.Context, healthInfo interface{}, client MinioAdmin) (string, error) {
filename := fmt.Sprintf("health_%d.json", time.Now().Unix())
subnetUploadURL := subnet.UploadURL("health", filename)
subnetHTTPClient := &xhttp.Client{Client: GetConsoleHTTPClient("")}
subnetTokenConfig, e := GetSubnetKeyFromMinIOConfig(ctx, client)
if e != nil {
return "", e
}
apiKey := subnetTokenConfig.APIKey
headers := subnet.UploadAuthHeaders(apiKey)
resp, e := subnet.UploadFileToSubnet(healthInfo, subnetHTTPClient, filename, subnetUploadURL, headers)
if e != nil {
return "", e
}
type SubnetResponse struct {
ClusterURL string `json:"cluster_url,omitempty"`
}
var subnetResp SubnetResponse
e = json.Unmarshal([]byte(resp), &subnetResp)
if e != nil {
return "", e
}
if len(subnetResp.ClusterURL) != 0 {
subnetClusterURL := strings.ReplaceAll(subnetResp.ClusterURL, "%2f", "/")
return subnetClusterURL, nil
}
return "", ErrSubnetUploadFail
}

View File

@@ -71,6 +71,7 @@ var (
ErrEncryptionConfigNotFound = errors.New("encryption configuration not found")
ErrPolicyNotFound = errors.New("policy does not exist")
ErrLoginNotAllowed = errors.New("login not allowed")
ErrSubnetUploadFail = errors.New("Subnet upload failed")
)
// ErrorWithContext :

View File

@@ -502,7 +502,6 @@ func (wsc *wsAdminClient) healthInfo(ctx context.Context, deadline *time.Duratio
LogInfo("health info started")
ctx = wsReadClientCtx(ctx, wsc.conn)
err := startHealthInfo(ctx, wsc.conn, wsc.client, deadline)
sendWsCloseMessage(wsc.conn, err)