Skip to content

Commit c9ac525

Browse files
authored
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>
1 parent 8b1b2b1 commit c9ac525

File tree

7 files changed

+174
-40
lines changed

7 files changed

+174
-40
lines changed

pkg/subnet/utils.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package subnet
1818

1919
import (
2020
"bytes"
21+
"compress/gzip"
2122
"encoding/base64"
2223
"encoding/json"
2324
"fmt"
2425
"io"
2526
"io/ioutil"
27+
"mime/multipart"
2628
"net/http"
2729

2830
xhttp "github.com/minio/console/pkg/http"
@@ -64,6 +66,62 @@ func LogWebhookURL() string {
6466
return subnetBaseURL() + "/api/logs"
6567
}
6668

69+
func UploadURL(uploadType string, filename string) string {
70+
return fmt.Sprintf("%s/api/%s/upload?filename=%s", subnetBaseURL(), uploadType, filename)
71+
}
72+
73+
func UploadAuthHeaders(apiKey string) map[string]string {
74+
return map[string]string{"x-subnet-api-key": apiKey}
75+
}
76+
77+
func UploadFileToSubnet(info interface{}, client *xhttp.Client, filename string, reqURL string, headers map[string]string) (string, error) {
78+
req, e := subnetUploadReq(info, reqURL, filename)
79+
if e != nil {
80+
return "", e
81+
}
82+
resp, e := subnetReqDo(client, req, headers)
83+
return resp, e
84+
}
85+
86+
func subnetUploadReq(info interface{}, url string, filename string) (*http.Request, error) {
87+
var body bytes.Buffer
88+
writer := multipart.NewWriter(&body)
89+
zipWriter := gzip.NewWriter(&body)
90+
version := "3"
91+
enc := json.NewEncoder(zipWriter)
92+
93+
header := struct {
94+
Version string `json:"version"`
95+
}{Version: version}
96+
97+
if e := enc.Encode(header); e != nil {
98+
return nil, e
99+
}
100+
101+
if e := enc.Encode(info); e != nil {
102+
return nil, e
103+
}
104+
zipWriter.Close()
105+
temp := body
106+
part, e := writer.CreateFormFile("file", filename)
107+
if e != nil {
108+
return nil, e
109+
}
110+
if _, e = io.Copy(part, &temp); e != nil {
111+
return nil, e
112+
}
113+
114+
writer.Close()
115+
116+
r, e := http.NewRequest(http.MethodPost, url, &body)
117+
if e != nil {
118+
return nil, e
119+
}
120+
r.Header.Add("Content-Type", writer.FormDataContentType())
121+
122+
return r, nil
123+
}
124+
67125
func GenerateRegToken(clusterRegInfo mc.ClusterRegistrationInfo) (string, error) {
68126
token, e := json.Marshal(clusterRegInfo)
69127
if e != nil {

portal-ui/src/screens/Console/HealthInfo/HealthInfo.tsx

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// You should have received a copy of the GNU Affero General Public License
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
import React, { Fragment, useEffect, useState } from "react";
17-
import clsx from "clsx";
17+
1818
import {
1919
ICloseEvent,
2020
IMessageEvent,
@@ -72,7 +72,7 @@ const styles = (theme: Theme) =>
7272
color: "#07193E",
7373
fontWeight: "bold",
7474
textAlign: "center",
75-
marginBottom: 10,
75+
marginBottom: 20,
7676
},
7777
progressResult: {
7878
textAlign: "center",
@@ -94,8 +94,6 @@ const styles = (theme: Theme) =>
9494

9595
interface IHealthInfo {
9696
classes: any;
97-
namespace: string;
98-
tenant: string;
9997
}
10098

10199
const HealthInfo = ({ classes }: IHealthInfo) => {
@@ -104,22 +102,19 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
104102

105103
const message = useSelector((state: AppState) => state.healthInfo.message);
106104

107-
const clusterRegistered = registeredCluster();
108-
109105
const serverDiagnosticStatus = useSelector(
110106
(state: AppState) => state.system.serverDiagnosticStatus
111107
);
112108
const [startDiagnostic, setStartDiagnostic] = useState(false);
109+
113110
const [downloadDisabled, setDownloadDisabled] = useState(true);
114111
const [localMessage, setMessage] = useState<string>("");
115112
const [buttonStartText, setButtonStartText] =
116113
useState<string>("Start Diagnostic");
117114
const [title, setTitle] = useState<string>("New Diagnostic");
118115
const [diagFileContent, setDiagFileContent] = useState<string>("");
119-
120-
const isDiagnosticComplete =
121-
serverDiagnosticStatus === DiagStatSuccess ||
122-
serverDiagnosticStatus === DiagStatError;
116+
const [subnetResponse, setSubnetResponse] = useState<string>("");
117+
const clusterRegistered = registeredCluster();
123118

124119
const download = () => {
125120
let element = document.createElement("a");
@@ -195,7 +190,6 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
195190
const c = new W3CWebSocket(
196191
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/health-info?deadline=1h`
197192
);
198-
199193
let interval: any | null = null;
200194
if (c !== null) {
201195
c.onopen = () => {
@@ -220,6 +214,9 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
220214
if (m.encoded !== "") {
221215
setDiagFileContent(m.encoded);
222216
}
217+
if (m.subnetResponse) {
218+
setSubnetResponse(m.subnetResponse);
219+
}
223220
};
224221
c.onerror = (error: Error) => {
225222
console.log("error closing websocket:", error.message);
@@ -275,38 +272,70 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
275272
className={classes.progressResult}
276273
>
277274
<div className={classes.localMessage}>{localMessage}</div>
275+
<div className={classes.progressResult}>
276+
{" "}
277+
{subnetResponse !== "" &&
278+
!subnetResponse.toLowerCase().includes("error") && (
279+
<Grid item xs={12} className={classes.serversData}>
280+
<strong>
281+
Health report uploaded to Subnet successfully!
282+
</strong>
283+
&nbsp;{" "}
284+
<strong>
285+
See the results on your{" "}
286+
<a href={subnetResponse}>Subnet Dashboard</a>{" "}
287+
</strong>
288+
</Grid>
289+
)}
290+
{(subnetResponse === "" ||
291+
subnetResponse.toLowerCase().includes("error")) &&
292+
serverDiagnosticStatus === DiagStatSuccess && (
293+
<Grid item xs={12} className={classes.serversData}>
294+
<strong>
295+
Something went wrong uploading your Health report to
296+
Subnet.
297+
</strong>
298+
&nbsp;{" "}
299+
<strong>
300+
Log into your{" "}
301+
<a href="https://subnet.min.io">Subnet Account</a> to
302+
manually upload your Health report.
303+
</strong>
304+
</Grid>
305+
)}
306+
</div>
278307
{serverDiagnosticStatus === DiagStatInProgress ? (
279308
<div className={classes.loading}>
280309
<Loader style={{ width: 25, height: 25 }} />
281310
</div>
282311
) : (
283312
<Fragment>
284-
{serverDiagnosticStatus !== DiagStatError &&
285-
!downloadDisabled && (
313+
<Grid container justifyItems={"flex-start"}>
314+
<Grid item xs={6}>
315+
{serverDiagnosticStatus !== DiagStatError &&
316+
!downloadDisabled && (
317+
<Button
318+
id={"download"}
319+
type="submit"
320+
variant="callAction"
321+
onClick={() => download()}
322+
disabled={downloadDisabled}
323+
label={"Download"}
324+
/>
325+
)}
326+
</Grid>
327+
<Grid item xs={6}>
286328
<Button
287-
id={"download"}
329+
id="start-new-diagnostic"
288330
type="submit"
289-
variant="callAction"
290-
onClick={() => download()}
291-
disabled={downloadDisabled}
292-
label={"Download"}
331+
variant={
332+
!clusterRegistered ? "regular" : "callAction"
333+
}
334+
disabled={startDiagnostic}
335+
onClick={startDiagnosticAction}
336+
label={buttonStartText}
293337
/>
294-
)}
295-
<Grid
296-
item
297-
xs={12}
298-
className={clsx(classes.startDiagnostic, {
299-
[classes.startDiagnosticCenter]: !isDiagnosticComplete,
300-
})}
301-
>
302-
<Button
303-
id="start-new-diagnostic"
304-
type="submit"
305-
variant={!clusterRegistered ? "regular" : "callAction"}
306-
disabled={startDiagnostic}
307-
onClick={startDiagnosticAction}
308-
label={buttonStartText}
309-
/>
338+
</Grid>
310339
</Grid>
311340
</Fragment>
312341
)}
@@ -322,7 +351,12 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
322351
"During the health diagnostics run, all production traffic will be suspended."
323352
}
324353
iconComponent={<WarnIcon />}
325-
help={<Fragment />}
354+
help={
355+
<Fragment>
356+
Cluster Health Report will be uploaded to Subnet, and is
357+
viewable from your Subnet Diagnostics dashboard.
358+
</Fragment>
359+
}
326360
/>
327361
</Fragment>
328362
)}

portal-ui/src/screens/Console/HealthInfo/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface HealthInfoMessage {
2929
export interface ReportMessage {
3030
encoded: string;
3131
serverHealthInfo: HealthInfoMessage;
32+
subnetResponse: string;
3233
}
3334

3435
export interface perfInfo {

portal-ui/src/systemSlice.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export const systemSlice = createSlice({
147147
setSiteReplicationInfo: (state, action: PayloadAction<SRInfoStateType>) => {
148148
state.siteReplicationInfo = action.payload;
149149
},
150-
setLicenseInfo: (state, action: PayloadAction<SubnetInfo | null>) => {
150+
setSystemLicenseInfo: (state, action: PayloadAction<SubnetInfo | null>) => {
151151
state.licenseInfo = action.payload;
152152
},
153153
setOverrideStyles: (
@@ -181,7 +181,7 @@ export const {
181181
setServerDiagStat,
182182
globalSetDistributedSetup,
183183
setSiteReplicationInfo,
184-
setLicenseInfo,
184+
setSystemLicenseInfo,
185185
setOverrideStyles,
186186
setAnonymousMode,
187187
resetSystem,

restapi/admin_health_info.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ import (
2222
b64 "encoding/base64"
2323
"encoding/json"
2424
"errors"
25+
"fmt"
2526
"net/http"
27+
"strings"
2628
"time"
2729

2830
"github.com/klauspost/compress/gzip"
29-
31+
xhttp "github.com/minio/console/pkg/http"
32+
subnet "github.com/minio/console/pkg/subnet"
3033
"github.com/minio/madmin-go/v2"
3134
"github.com/minio/websocket"
3235
)
@@ -51,7 +54,6 @@ func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadli
5154
madmin.HealthDataTypeSysNet,
5255
madmin.HealthDataTypeSysProcess,
5356
}
54-
5557
var err error
5658
// Fetch info of all servers (cluster or single server)
5759
healthInfo, version, err := client.serverHealthInfo(ctx, healthDataTypes, *deadline)
@@ -68,12 +70,19 @@ func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadli
6870
type messageReport struct {
6971
Encoded string `json:"encoded"`
7072
ServerHealthInfo interface{} `json:"serverHealthInfo"`
73+
SubnetResponse string `json:"subnetResponse"`
7174
}
7275

76+
subnetResp, err := sendHealthInfoToSubnet(ctx, healthInfo, client)
7377
report := messageReport{
7478
Encoded: encodedDiag,
7579
ServerHealthInfo: healthInfo,
80+
SubnetResponse: subnetResp,
81+
}
82+
if err != nil {
83+
report.SubnetResponse = fmt.Sprintf("Error: %s", err.Error())
7684
}
85+
7786
message, err := json.Marshal(report)
7887
if err != nil {
7988
return err
@@ -117,3 +126,35 @@ func getHealthInfoOptionsFromReq(req *http.Request) (*time.Duration, error) {
117126
}
118127
return &deadlineDuration, nil
119128
}
129+
130+
func sendHealthInfoToSubnet(ctx context.Context, healthInfo interface{}, client MinioAdmin) (string, error) {
131+
filename := fmt.Sprintf("health_%d.json", time.Now().Unix())
132+
133+
subnetUploadURL := subnet.UploadURL("health", filename)
134+
subnetHTTPClient := &xhttp.Client{Client: GetConsoleHTTPClient("")}
135+
subnetTokenConfig, e := GetSubnetKeyFromMinIOConfig(ctx, client)
136+
if e != nil {
137+
return "", e
138+
}
139+
apiKey := subnetTokenConfig.APIKey
140+
headers := subnet.UploadAuthHeaders(apiKey)
141+
resp, e := subnet.UploadFileToSubnet(healthInfo, subnetHTTPClient, filename, subnetUploadURL, headers)
142+
if e != nil {
143+
return "", e
144+
}
145+
type SubnetResponse struct {
146+
ClusterURL string `json:"cluster_url,omitempty"`
147+
}
148+
149+
var subnetResp SubnetResponse
150+
e = json.Unmarshal([]byte(resp), &subnetResp)
151+
if e != nil {
152+
return "", e
153+
}
154+
if len(subnetResp.ClusterURL) != 0 {
155+
subnetClusterURL := strings.ReplaceAll(subnetResp.ClusterURL, "%2f", "/")
156+
return subnetClusterURL, nil
157+
}
158+
159+
return "", ErrSubnetUploadFail
160+
}

restapi/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ var (
7171
ErrEncryptionConfigNotFound = errors.New("encryption configuration not found")
7272
ErrPolicyNotFound = errors.New("policy does not exist")
7373
ErrLoginNotAllowed = errors.New("login not allowed")
74+
ErrSubnetUploadFail = errors.New("Subnet upload failed")
7475
)
7576

7677
// ErrorWithContext :

restapi/ws_handle.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,6 @@ func (wsc *wsAdminClient) healthInfo(ctx context.Context, deadline *time.Duratio
502502
LogInfo("health info started")
503503

504504
ctx = wsReadClientCtx(ctx, wsc.conn)
505-
506505
err := startHealthInfo(ctx, wsc.conn, wsc.client, deadline)
507506

508507
sendWsCloseMessage(wsc.conn, err)

0 commit comments

Comments
 (0)