1
0
mirror of https://github.com/minio/mc.git synced 2025-12-10 10:22:47 +03:00

Add command for uploading file to SUBNET issue (#4854)

mc support upload --issue <issueNum> --comment <msg> <alias> </path/to/file>

Will upload the given file to the given SUBNET issue
This commit is contained in:
Shireesh Anjal
2024-03-07 05:59:26 +05:30
committed by GitHub
parent 98af07b69c
commit daf5b18e18
8 changed files with 239 additions and 21 deletions

View File

@@ -488,6 +488,7 @@ var completeCmds = map[string]complete.Predictor{
"/support/top/drive": aliasCompleter,
"/support/top/disk": aliasCompleter,
"/support/top/net": aliasCompleter,
"/support/upload": aliasCompleter,
"/license/register": aliasCompleter,
"/license/info": aliasCompleter,

View File

@@ -35,6 +35,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/klauspost/compress/zstd"
"github.com/minio/cli"
"github.com/minio/madmin-go/v3"
"github.com/minio/mc/pkg/probe"
@@ -61,12 +62,16 @@ func subnetBaseURL() string {
return subnet.BaseURL(globalDevMode)
}
func subnetIssueURL(issueNum int) string {
return fmt.Sprintf("%s/issues/%d", subnetBaseURL(), issueNum)
}
func subnetLogWebhookURL() string {
return subnetBaseURL() + "/api/logs"
}
func subnetUploadURL(uploadType, filename string) string {
return fmt.Sprintf("%s/api/%s/upload?filename=%s", subnetBaseURL(), uploadType, filename)
func subnetUploadURL(uploadType string) string {
return fmt.Sprintf("%s/api/%s/upload", subnetBaseURL(), uploadType)
}
func subnetRegisterURL() string {
@@ -708,28 +713,61 @@ func prepareSubnetUploadURL(uploadURL, alias, apiKey string) (string, map[string
return reqURL, headers
}
func uploadFileToSubnet(alias, filename, reqURL string, headers map[string]string) (string, error) {
req, e := subnetUploadReq(reqURL, filename)
type subnetFileUploader struct {
alias string
filePath string
filename string
reqURL string
headers subnetHeaders
autoCompress bool
deleteAfterUpload bool
params url.Values
}
func (i *subnetFileUploader) uploadFileToSubnet() (string, error) {
req, e := i.subnetUploadReq()
if e != nil {
return "", e
}
resp, e := subnetReqDo(req, headers)
resp, e := subnetReqDo(req, i.headers)
if e != nil {
return "", e
}
// Delete the file after successful upload
os.Remove(filename)
if i.deleteAfterUpload {
os.Remove(i.filePath)
}
// ensure that both api-key and license from
// SUBNET response are saved in the config
extractAndSaveSubnetCreds(alias, resp)
extractAndSaveSubnetCreds(i.alias, resp)
return resp, e
}
func subnetUploadReq(url, filename string) (*http.Request, error) {
func (i *subnetFileUploader) updateParams() {
if i.params == nil {
i.params = url.Values{}
}
if i.filename == "" {
i.filename = filepath.Base(i.filePath)
}
i.autoCompress = i.autoCompress && !strings.HasSuffix(strings.ToLower(i.filePath), ".zst")
if i.autoCompress {
i.filename += ".zst"
i.params.Add("auto-compression", "zstd")
}
i.params.Add("filename", i.filename)
i.reqURL += "?" + i.params.Encode()
}
func (i *subnetFileUploader) subnetUploadReq() (*http.Request, error) {
i.updateParams()
r, w := io.Pipe()
mwriter := multipart.NewWriter(w)
contentType := mwriter.FormDataContentType()
@@ -744,21 +782,27 @@ func subnetUploadReq(url, filename string) (*http.Request, error) {
w.CloseWithError(e)
}()
part, e = mwriter.CreateFormFile("file", filepath.Base(filename))
part, e = mwriter.CreateFormFile("file", i.filename)
if e != nil {
return
}
file, e := os.Open(filename)
file, e := os.Open(i.filePath)
if e != nil {
return
}
defer file.Close()
if i.autoCompress {
z, _ := zstd.NewWriter(part, zstd.WithEncoderConcurrency(2))
defer z.Close()
_, e = z.ReadFrom(file)
} else {
_, e = io.Copy(part, file)
}
}()
req, e := http.NewRequest(http.MethodPost, url, r)
req, e := http.NewRequest(http.MethodPost, i.reqURL, r)
if e != nil {
return nil, e
}

View File

@@ -205,7 +205,7 @@ func execSupportDiag(ctx *cli.Context, client *madmin.AdminClient, alias, apiKey
if !globalAirgapped {
// Retrieve subnet credentials (login/license) beforehand as
// it can take a long time to fetch the health information
uploadURL := subnetUploadURL("health", filename)
uploadURL := subnetUploadURL("health")
reqURL, headers = prepareSubnetUploadURL(uploadURL, alias, apiKey)
}
@@ -228,7 +228,13 @@ func execSupportDiag(ctx *cli.Context, client *madmin.AdminClient, alias, apiKey
fatalIf(probe.NewError(e), "Unable to save MinIO diagnostics report")
if !globalAirgapped {
_, e := uploadFileToSubnet(alias, filename, reqURL, headers)
_, e := (&subnetFileUploader{
alias: alias,
filePath: filename,
reqURL: reqURL,
headers: headers,
deleteAfterUpload: true,
}).uploadFileToSubnet()
fatalIf(probe.NewError(e), "Unable to upload MinIO diagnostics report to SUBNET portal")
printMsg(supportDiagMessage{})

View File

@@ -196,10 +196,18 @@ func mainSupportInspect(ctx *cli.Context) error {
return nil
}
uploadURL := subnetUploadURL("inspect", inspectOutputFilename)
uploadURL := subnetUploadURL("inspect")
reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey)
_, e = uploadFileToSubnet(alias, tmpFile.Name(), reqURL, headers)
tmpFileName := tmpFile.Name()
_, e = (&subnetFileUploader{
alias: alias,
filePath: tmpFileName,
filename: inspectOutputFilename,
reqURL: reqURL,
headers: headers,
deleteAfterUpload: true,
}).uploadFileToSubnet()
if e != nil {
console.Errorln("Unable to upload inspect data to SUBNET portal: " + e.Error())
saveInspectDataFile(key, tmpFile)

View File

@@ -490,10 +490,16 @@ func execSupportPerf(ctx *cli.Context, aliasedURL, perfType string) {
return
}
uploadURL := subnetUploadURL("perf", tmpFileName)
uploadURL := subnetUploadURL("perf")
reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey)
_, e = uploadFileToSubnet(alias, tmpFileName, reqURL, headers)
_, e = (&subnetFileUploader{
alias: alias,
filePath: tmpFileName,
reqURL: reqURL,
headers: headers,
deleteAfterUpload: true,
}).uploadFileToSubnet()
if e != nil {
errorIf(probe.NewError(e), "Unable to upload performance results to SUBNET portal")
savePerfResultFile(tmpFileName, resultFileNamePfx)

View File

@@ -226,7 +226,7 @@ func execSupportProfile(ctx *cli.Context, client *madmin.AdminClient, alias, api
if !globalAirgapped {
// Retrieve subnet credentials (login/license) beforehand as
// it can take a long time to fetch the profile data
uploadURL := subnetUploadURL("profile", profileFile)
uploadURL := subnetUploadURL("profile")
reqURL, headers = prepareSubnetUploadURL(uploadURL, alias, apiKey)
}
@@ -239,7 +239,13 @@ func execSupportProfile(ctx *cli.Context, client *madmin.AdminClient, alias, api
saveProfileFile(data)
if !globalAirgapped {
_, e = uploadFileToSubnet(alias, profileFile, reqURL, headers)
_, e = (&subnetFileUploader{
alias: alias,
filePath: profileFile,
reqURL: reqURL,
headers: headers,
deleteAfterUpload: true,
}).uploadFileToSubnet()
if e != nil {
printMsg(supportProfileMessage{
Status: "error",

146
cmd/support-upload.go Normal file
View File

@@ -0,0 +1,146 @@
// Copyright (c) 2015-2024 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// 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 cmd
import (
"fmt"
"net/url"
"github.com/minio/cli"
"github.com/minio/mc/pkg/probe"
"github.com/minio/pkg/v2/console"
)
// profile command flags.
var (
uploadFlags = append(globalFlags,
cli.IntFlag{
Name: "issue",
Usage: "SUBNET issue number to which the file is to be uploaded",
},
cli.StringFlag{
Name: "comment",
Usage: "comment to be posted on the issue along with the file",
},
cli.BoolFlag{
Name: "dev",
Usage: "Development mode",
Hidden: true,
},
)
)
type supportUploadMessage struct {
Status string `json:"status"`
IssueNum int `json:"-"`
IssueURL string `json:"issueUrl"`
}
// String colorized upload message
func (s supportUploadMessage) String() string {
msg := fmt.Sprintf("File uploaded to SUBNET successfully. Click here to visit the issue: %s", subnetIssueURL(s.IssueNum))
return console.Colorize(supportSuccessMsgTag, msg)
}
// JSON jsonified upload message
func (s supportUploadMessage) JSON() string {
return toJSON(s)
}
var supportUploadCmd = cli.Command{
Name: "upload",
Usage: "upload file to a SUBNET issue",
Action: mainSupportUpload,
OnUsageError: onUsageError,
Before: setGlobalsFromContext,
Flags: uploadFlags,
HideHelpCommand: true,
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} [FLAGS] ALIAS FILE
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Upload file './trace.log' for cluster 'myminio' to SUBNET issue number 10
{{.Prompt}} {{.HelpName}} --issue 10 myminio ./trace.log
2. Upload file './trace.log' for cluster 'myminio' to SUBNET issue number 10 with comment 'here is the trace log'
{{.Prompt}} {{.HelpName}} --issue 10 --comment "here is the trace log" myminio ./trace.log
`,
}
func checkSupportUploadSyntax(ctx *cli.Context) {
if len(ctx.Args()) != 2 {
showCommandHelpAndExit(ctx, 1) // last argument is exit code
}
if ctx.Int("issue") <= 0 {
fatal(errDummy().Trace(), "Invalid issue number")
}
}
// mainSupportUpload is the handle for "mc support upload" command.
func mainSupportUpload(ctx *cli.Context) error {
// Check for command syntax
checkSupportUploadSyntax(ctx)
setSuccessMessageColor()
// Get the alias parameter from cli
aliasedURL := ctx.Args().Get(0)
alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true)
if len(apiKey) == 0 {
// api key not passed as flag. Check that the cluster is registered.
apiKey = validateClusterRegistered(alias, true)
}
// Main execution
execSupportUpload(ctx, alias, apiKey)
return nil
}
func execSupportUpload(ctx *cli.Context, alias, apiKey string) {
filePath := ctx.Args().Get(1)
issueNum := ctx.Int("issue")
msg := ctx.String("comment")
params := url.Values{}
params.Add("issueNumber", fmt.Sprintf("%d", issueNum))
if len(msg) > 0 {
params.Add("message", msg)
}
uploadURL := subnetUploadURL("attachment")
reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey)
_, e := (&subnetFileUploader{
alias: alias,
filePath: filePath,
reqURL: reqURL,
headers: headers,
autoCompress: true,
params: params,
}).uploadFileToSubnet()
if e != nil {
fatalIf(probe.NewError(e), "Unable to upload file to SUBNET")
}
printMsg(supportUploadMessage{IssueNum: issueNum, Status: "success", IssueURL: subnetIssueURL(issueNum)})
}

View File

@@ -55,6 +55,7 @@ var supportSubcommands = []cli.Command{
supportProfileCmd,
supportTopCmd,
supportProxyCmd,
supportUploadCmd,
}
var supportCmd = cli.Command{