Different gateway implementations due to different backend API errors, might return different unsupported errors at our handler layer. Current code posed a problem for us because this information was lost and we would convert it to InternalError in this situation all S3 clients end up retrying the request. To avoid this unexpected situation implement a way to support this cleanly such that the underlying information is not lost which is returned by gateway.
969 lines
25 KiB
Go
969 lines
25 KiB
Go
/*
|
|
* Minio Cloud Storage, (C) 2016, 2017, 2018 Minio, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/minio/minio/pkg/auth"
|
|
"github.com/minio/minio/pkg/madmin"
|
|
)
|
|
|
|
var (
|
|
configJSON = []byte(`{
|
|
"version": "33",
|
|
"credential": {
|
|
"accessKey": "minio",
|
|
"secretKey": "minio123"
|
|
},
|
|
"region": "us-east-1",
|
|
"worm": "off",
|
|
"storageclass": {
|
|
"standard": "",
|
|
"rrs": ""
|
|
},
|
|
"cache": {
|
|
"drives": [],
|
|
"expiry": 90,
|
|
"maxuse": 80,
|
|
"exclude": []
|
|
},
|
|
"kms": {
|
|
"vault": {
|
|
"endpoint": "",
|
|
"auth": {
|
|
"type": "",
|
|
"approle": {
|
|
"id": "",
|
|
"secret": ""
|
|
}
|
|
},
|
|
"key-id": {
|
|
"name": "",
|
|
"version": 0
|
|
}
|
|
}
|
|
},
|
|
"notify": {
|
|
"amqp": {
|
|
"1": {
|
|
"enable": false,
|
|
"url": "",
|
|
"exchange": "",
|
|
"routingKey": "",
|
|
"exchangeType": "",
|
|
"deliveryMode": 0,
|
|
"mandatory": false,
|
|
"immediate": false,
|
|
"durable": false,
|
|
"internal": false,
|
|
"noWait": false,
|
|
"autoDeleted": false
|
|
}
|
|
},
|
|
"elasticsearch": {
|
|
"1": {
|
|
"enable": false,
|
|
"format": "namespace",
|
|
"url": "",
|
|
"index": ""
|
|
}
|
|
},
|
|
"kafka": {
|
|
"1": {
|
|
"enable": false,
|
|
"brokers": null,
|
|
"topic": "",
|
|
"tls": {
|
|
"enable": false,
|
|
"skipVerify": false,
|
|
"clientAuth": 0
|
|
},
|
|
"sasl": {
|
|
"enable": false,
|
|
"username": "",
|
|
"password": ""
|
|
}
|
|
}
|
|
},
|
|
"mqtt": {
|
|
"1": {
|
|
"enable": false,
|
|
"broker": "",
|
|
"topic": "",
|
|
"qos": 0,
|
|
"username": "",
|
|
"password": "",
|
|
"reconnectInterval": 0,
|
|
"keepAliveInterval": 0,
|
|
"queueDir": ""
|
|
}
|
|
},
|
|
"mysql": {
|
|
"1": {
|
|
"enable": false,
|
|
"format": "namespace",
|
|
"dsnString": "",
|
|
"table": "",
|
|
"host": "",
|
|
"port": "",
|
|
"user": "",
|
|
"password": "",
|
|
"database": ""
|
|
}
|
|
},
|
|
"nats": {
|
|
"1": {
|
|
"enable": false,
|
|
"address": "",
|
|
"subject": "",
|
|
"username": "",
|
|
"password": "",
|
|
"token": "",
|
|
"secure": false,
|
|
"pingInterval": 0,
|
|
"streaming": {
|
|
"enable": false,
|
|
"clusterID": "",
|
|
"async": false,
|
|
"maxPubAcksInflight": 0
|
|
}
|
|
}
|
|
},
|
|
"nsq": {
|
|
"1": {
|
|
"enable": false,
|
|
"nsqdAddress": "",
|
|
"topic": "",
|
|
"tls": {
|
|
"enable": false,
|
|
"skipVerify": false
|
|
}
|
|
}
|
|
},
|
|
"postgresql": {
|
|
"1": {
|
|
"enable": false,
|
|
"format": "namespace",
|
|
"connectionString": "",
|
|
"table": "",
|
|
"host": "",
|
|
"port": "",
|
|
"user": "",
|
|
"password": "",
|
|
"database": ""
|
|
}
|
|
},
|
|
"redis": {
|
|
"1": {
|
|
"enable": false,
|
|
"format": "namespace",
|
|
"address": "",
|
|
"password": "",
|
|
"key": ""
|
|
}
|
|
},
|
|
"webhook": {
|
|
"1": {
|
|
"enable": false,
|
|
"endpoint": ""
|
|
}
|
|
}
|
|
},
|
|
"logger": {
|
|
"console": {
|
|
"enabled": true
|
|
},
|
|
"http": {
|
|
"1": {
|
|
"enabled": false,
|
|
"endpoint": "https://username:password@example.com/api"
|
|
}
|
|
}
|
|
},
|
|
"compress": {
|
|
"enabled": false,
|
|
"extensions":[".txt",".log",".csv",".json"],
|
|
"mime-types":["text/csv","text/plain","application/json"]
|
|
},
|
|
"openid": {
|
|
"jwks": {
|
|
"url": ""
|
|
}
|
|
},
|
|
"policy": {
|
|
"opa": {
|
|
"url": "",
|
|
"authToken": ""
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
)
|
|
|
|
// adminXLTestBed - encapsulates subsystems that need to be setup for
|
|
// admin-handler unit tests.
|
|
type adminXLTestBed struct {
|
|
configPath string
|
|
xlDirs []string
|
|
objLayer ObjectLayer
|
|
router *mux.Router
|
|
}
|
|
|
|
// prepareAdminXLTestBed - helper function that setups a single-node
|
|
// XL backend for admin-handler tests.
|
|
func prepareAdminXLTestBed() (*adminXLTestBed, error) {
|
|
// reset global variables to start afresh.
|
|
resetTestGlobals()
|
|
|
|
// Initializing objectLayer for HealFormatHandler.
|
|
objLayer, xlDirs, xlErr := initTestXLObjLayer()
|
|
if xlErr != nil {
|
|
return nil, xlErr
|
|
}
|
|
|
|
// Initialize minio server config.
|
|
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Initialize boot time
|
|
globalBootTime = UTCNow()
|
|
|
|
globalEndpoints = mustGetNewEndpointList(xlDirs...)
|
|
|
|
// Set globalIsXL to indicate that the setup uses an erasure
|
|
// code backend.
|
|
globalIsXL = true
|
|
|
|
// initialize NSLock.
|
|
isDistXL := false
|
|
initNSLock(isDistXL)
|
|
|
|
// Init global heal state
|
|
initAllHealState(globalIsXL)
|
|
|
|
globalConfigSys = NewConfigSys()
|
|
|
|
globalIAMSys = NewIAMSys()
|
|
globalIAMSys.Init(objLayer)
|
|
|
|
globalPolicySys = NewPolicySys()
|
|
globalPolicySys.Init(objLayer)
|
|
|
|
globalNotificationSys = NewNotificationSys(globalServerConfig, globalEndpoints)
|
|
globalNotificationSys.Init(objLayer)
|
|
|
|
// Setup admin mgmt REST API handlers.
|
|
adminRouter := mux.NewRouter()
|
|
registerAdminRouter(adminRouter, true, true)
|
|
|
|
return &adminXLTestBed{
|
|
xlDirs: xlDirs,
|
|
objLayer: objLayer,
|
|
router: adminRouter,
|
|
}, nil
|
|
}
|
|
|
|
// TearDown - method that resets the test bed for subsequent unit
|
|
// tests to start afresh.
|
|
func (atb *adminXLTestBed) TearDown() {
|
|
removeRoots(atb.xlDirs)
|
|
resetTestGlobals()
|
|
}
|
|
|
|
func (atb *adminXLTestBed) GenerateHealTestData(t *testing.T) {
|
|
// Create an object myobject under bucket mybucket.
|
|
bucketName := "mybucket"
|
|
err := atb.objLayer.MakeBucketWithLocation(context.Background(), bucketName, "")
|
|
if err != nil {
|
|
t.Fatalf("Failed to make bucket %s - %v", bucketName,
|
|
err)
|
|
}
|
|
|
|
// create some objects
|
|
{
|
|
objName := "myobject"
|
|
for i := 0; i < 10; i++ {
|
|
objectName := fmt.Sprintf("%s-%d", objName, i)
|
|
_, err = atb.objLayer.PutObject(context.Background(), bucketName, objectName,
|
|
mustGetPutObjReader(t, bytes.NewReader([]byte("hello")),
|
|
int64(len("hello")), "", ""), ObjectOptions{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create %s - %v", objectName,
|
|
err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// create a multipart upload (incomplete)
|
|
{
|
|
objName := "mpObject"
|
|
uploadID, err := atb.objLayer.NewMultipartUpload(context.Background(), bucketName,
|
|
objName, ObjectOptions{})
|
|
if err != nil {
|
|
t.Fatalf("mp new error: %v", err)
|
|
}
|
|
|
|
_, err = atb.objLayer.PutObjectPart(context.Background(), bucketName, objName,
|
|
uploadID, 3, mustGetPutObjReader(t, bytes.NewReader(
|
|
[]byte("hello")), int64(len("hello")), "", ""), ObjectOptions{})
|
|
if err != nil {
|
|
t.Fatalf("mp put error: %v", err)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
func (atb *adminXLTestBed) CleanupHealTestData(t *testing.T) {
|
|
bucketName := "mybucket"
|
|
objName := "myobject"
|
|
for i := 0; i < 10; i++ {
|
|
atb.objLayer.DeleteObject(context.Background(), bucketName,
|
|
fmt.Sprintf("%s-%d", objName, i))
|
|
}
|
|
|
|
atb.objLayer.DeleteBucket(context.Background(), bucketName)
|
|
}
|
|
|
|
// initTestObjLayer - Helper function to initialize an XL-based object
|
|
// layer and set globalObjectAPI.
|
|
func initTestXLObjLayer() (ObjectLayer, []string, error) {
|
|
xlDirs, err := getRandomDisks(16)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
endpoints := mustGetNewEndpointList(xlDirs...)
|
|
format, err := waitForFormatXL(context.Background(), true, endpoints, 1, 16)
|
|
if err != nil {
|
|
removeRoots(xlDirs)
|
|
return nil, nil, err
|
|
}
|
|
|
|
globalPolicySys = NewPolicySys()
|
|
objLayer, err := newXLSets(endpoints, format, 1, 16)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Make objLayer available to all internal services via globalObjectAPI.
|
|
globalObjLayerMutex.Lock()
|
|
globalObjectAPI = objLayer
|
|
globalObjLayerMutex.Unlock()
|
|
return objLayer, xlDirs, nil
|
|
}
|
|
|
|
func TestAdminVersionHandler(t *testing.T) {
|
|
adminTestBed, err := prepareAdminXLTestBed()
|
|
if err != nil {
|
|
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
|
}
|
|
defer adminTestBed.TearDown()
|
|
|
|
req, err := newTestRequest("GET", "/minio/admin/version", 0, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to construct request - %v", err)
|
|
}
|
|
cred := globalServerConfig.GetCredential()
|
|
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to sign request - %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
if http.StatusOK != rec.Code {
|
|
t.Errorf("Unexpected status code - got %d but expected %d",
|
|
rec.Code, http.StatusOK)
|
|
}
|
|
|
|
var result madmin.AdminAPIVersionInfo
|
|
err = json.NewDecoder(rec.Body).Decode(&result)
|
|
if err != nil {
|
|
t.Errorf("json parse err: %v", err)
|
|
}
|
|
|
|
if result != adminAPIVersionInfo {
|
|
t.Errorf("unexpected version: %v", result)
|
|
}
|
|
}
|
|
|
|
// cmdType - Represents different service subcomands like status, stop
|
|
// and restart.
|
|
type cmdType int
|
|
|
|
const (
|
|
statusCmd cmdType = iota
|
|
restartCmd
|
|
stopCmd
|
|
setCreds
|
|
)
|
|
|
|
// String - String representation for cmdType
|
|
func (c cmdType) String() string {
|
|
switch c {
|
|
case statusCmd:
|
|
return "status"
|
|
case restartCmd:
|
|
return "restart"
|
|
case stopCmd:
|
|
return "stop"
|
|
case setCreds:
|
|
return "set-credentials"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// apiMethod - Returns the HTTP method corresponding to the admin REST
|
|
// API for a given cmdType value.
|
|
func (c cmdType) apiMethod() string {
|
|
switch c {
|
|
case statusCmd:
|
|
return "GET"
|
|
case restartCmd:
|
|
return "POST"
|
|
case stopCmd:
|
|
return "POST"
|
|
case setCreds:
|
|
return "PUT"
|
|
}
|
|
return "GET"
|
|
}
|
|
|
|
// apiEndpoint - Return endpoint for each admin REST API mapped to a
|
|
// command here.
|
|
func (c cmdType) apiEndpoint() string {
|
|
switch c {
|
|
case statusCmd, restartCmd, stopCmd:
|
|
return "/minio/admin/v1/service"
|
|
case setCreds:
|
|
return "/minio/admin/v1/config/credential"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// toServiceSignal - Helper function that translates a given cmdType
|
|
// value to its corresponding serviceSignal value.
|
|
func (c cmdType) toServiceSignal() serviceSignal {
|
|
switch c {
|
|
case statusCmd:
|
|
return serviceStatus
|
|
case restartCmd:
|
|
return serviceRestart
|
|
case stopCmd:
|
|
return serviceStop
|
|
}
|
|
return serviceStatus
|
|
}
|
|
|
|
func (c cmdType) toServiceActionValue() madmin.ServiceActionValue {
|
|
switch c {
|
|
case restartCmd:
|
|
return madmin.ServiceActionValueRestart
|
|
case stopCmd:
|
|
return madmin.ServiceActionValueStop
|
|
}
|
|
return madmin.ServiceActionValueStop
|
|
}
|
|
|
|
// testServiceSignalReceiver - Helper function that simulates a
|
|
// go-routine waiting on service signal.
|
|
func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
|
|
expectedCmd := cmd.toServiceSignal()
|
|
serviceCmd := <-globalServiceSignalCh
|
|
if serviceCmd != expectedCmd {
|
|
t.Errorf("Expected service command %v but received %v", expectedCmd, serviceCmd)
|
|
}
|
|
}
|
|
|
|
// getServiceCmdRequest - Constructs a management REST API request for service
|
|
// subcommands for a given cmdType value.
|
|
func getServiceCmdRequest(cmd cmdType, cred auth.Credentials, body []byte) (*http.Request, error) {
|
|
req, err := newTestRequest(cmd.apiMethod(), cmd.apiEndpoint(), 0, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set body
|
|
req.Body = ioutil.NopCloser(bytes.NewReader(body))
|
|
req.ContentLength = int64(len(body))
|
|
|
|
// Set sha-sum header
|
|
req.Header.Set("X-Amz-Content-Sha256", getSHA256Hash(body))
|
|
|
|
// management REST API uses signature V4 for authentication.
|
|
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
// testServicesCmdHandler - parametrizes service subcommand tests on
|
|
// cmdType value.
|
|
func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
|
adminTestBed, err := prepareAdminXLTestBed()
|
|
if err != nil {
|
|
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
|
}
|
|
defer adminTestBed.TearDown()
|
|
|
|
// Initialize admin peers to make admin RPC calls. Note: In a
|
|
// single node setup, this degenerates to a simple function
|
|
// call under the hood.
|
|
globalMinioAddr = "127.0.0.1:9000"
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Setting up a go routine to simulate ServerRouter's
|
|
// handleServiceSignals for stop and restart commands.
|
|
if cmd == restartCmd {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
testServiceSignalReceiver(cmd, t)
|
|
}()
|
|
}
|
|
credentials := globalServerConfig.GetCredential()
|
|
|
|
body, err := json.Marshal(madmin.ServiceAction{
|
|
Action: cmd.toServiceActionValue()})
|
|
if err != nil {
|
|
t.Fatalf("JSONify error: %v", err)
|
|
}
|
|
|
|
req, err := getServiceCmdRequest(cmd, credentials, body)
|
|
if err != nil {
|
|
t.Fatalf("Failed to build service status request %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
|
|
if cmd == statusCmd {
|
|
expectedInfo := madmin.ServiceStatus{
|
|
ServerVersion: madmin.ServerVersion{Version: Version, CommitID: CommitID},
|
|
}
|
|
receivedInfo := madmin.ServiceStatus{}
|
|
if jsonErr := json.Unmarshal(rec.Body.Bytes(), &receivedInfo); jsonErr != nil {
|
|
t.Errorf("Failed to unmarshal StorageInfo - %v", jsonErr)
|
|
}
|
|
if expectedInfo.ServerVersion != receivedInfo.ServerVersion {
|
|
t.Errorf("Expected storage info and received storage info differ, %v %v", expectedInfo, receivedInfo)
|
|
}
|
|
}
|
|
|
|
if rec.Code != http.StatusOK {
|
|
resp, _ := ioutil.ReadAll(rec.Body)
|
|
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
|
|
http.StatusOK, rec.Code, string(resp))
|
|
}
|
|
|
|
// Wait until testServiceSignalReceiver() called in a goroutine quits.
|
|
wg.Wait()
|
|
}
|
|
|
|
// Test for service status management REST API.
|
|
func TestServiceStatusHandler(t *testing.T) {
|
|
testServicesCmdHandler(statusCmd, t)
|
|
}
|
|
|
|
// Test for service restart management REST API.
|
|
func TestServiceRestartHandler(t *testing.T) {
|
|
testServicesCmdHandler(restartCmd, t)
|
|
}
|
|
|
|
// Test for service set creds management REST API.
|
|
func TestServiceSetCreds(t *testing.T) {
|
|
adminTestBed, err := prepareAdminXLTestBed()
|
|
if err != nil {
|
|
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
|
}
|
|
defer adminTestBed.TearDown()
|
|
|
|
// Initialize admin peers to make admin RPC calls. Note: In a
|
|
// single node setup, this degenerates to a simple function
|
|
// call under the hood.
|
|
globalMinioAddr = "127.0.0.1:9000"
|
|
|
|
credentials := globalServerConfig.GetCredential()
|
|
|
|
testCases := []struct {
|
|
AccessKey string
|
|
SecretKey string
|
|
EnvKeysSet bool
|
|
ExpectedStatusCode int
|
|
}{
|
|
// Bad secret key
|
|
{"minio", "minio", false, http.StatusBadRequest},
|
|
// Bad secret key set from the env
|
|
{"minio", "minio", true, http.StatusMethodNotAllowed},
|
|
// Good keys set from the env
|
|
{"minio", "minio123", true, http.StatusMethodNotAllowed},
|
|
// Successful operation should be the last one to
|
|
// not change server credentials during tests.
|
|
{"minio", "minio123", false, http.StatusOK},
|
|
}
|
|
for i, testCase := range testCases {
|
|
// Set or unset environement keys
|
|
globalIsEnvCreds = testCase.EnvKeysSet
|
|
|
|
// Construct setCreds request body
|
|
body, err := json.Marshal(madmin.SetCredsReq{
|
|
AccessKey: testCase.AccessKey,
|
|
SecretKey: testCase.SecretKey})
|
|
if err != nil {
|
|
t.Fatalf("JSONify err: %v", err)
|
|
}
|
|
|
|
ebody, err := madmin.EncryptData(credentials.SecretKey, body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Construct setCreds request
|
|
req, err := getServiceCmdRequest(setCreds, credentials, ebody)
|
|
if err != nil {
|
|
t.Fatalf("Failed to build service status request %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
// Execute request
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
|
|
// Check if the http code response is expected
|
|
if rec.Code != testCase.ExpectedStatusCode {
|
|
t.Errorf("Test %d: Wrong status code, expected = %d, found = %d", i+1, testCase.ExpectedStatusCode, rec.Code)
|
|
resp, _ := ioutil.ReadAll(rec.Body)
|
|
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
|
|
http.StatusOK, rec.Code, string(resp))
|
|
}
|
|
|
|
// If we got 200 OK, check if new credentials are really set
|
|
if rec.Code == http.StatusOK {
|
|
cred := globalServerConfig.GetCredential()
|
|
if cred.AccessKey != testCase.AccessKey {
|
|
t.Errorf("Test %d: Wrong access key, expected = %s, found = %s", i+1, testCase.AccessKey, cred.AccessKey)
|
|
}
|
|
if cred.SecretKey != testCase.SecretKey {
|
|
t.Errorf("Test %d: Wrong secret key, expected = %s, found = %s", i+1, testCase.SecretKey, cred.SecretKey)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// buildAdminRequest - helper function to build an admin API request.
|
|
func buildAdminRequest(queryVal url.Values, method, path string,
|
|
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) {
|
|
|
|
req, err := newTestRequest(method,
|
|
"/minio/admin/v1"+path+"?"+queryVal.Encode(),
|
|
contentLength, bodySeeker)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cred := globalServerConfig.GetCredential()
|
|
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
// TestGetConfigHandler - test for GetConfigHandler.
|
|
func TestGetConfigHandler(t *testing.T) {
|
|
adminTestBed, err := prepareAdminXLTestBed()
|
|
if err != nil {
|
|
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
|
}
|
|
defer adminTestBed.TearDown()
|
|
|
|
// Initialize admin peers to make admin RPC calls.
|
|
globalMinioAddr = "127.0.0.1:9000"
|
|
|
|
// Prepare query params for get-config mgmt REST API.
|
|
queryVal := url.Values{}
|
|
queryVal.Set("config", "")
|
|
|
|
req, err := buildAdminRequest(queryVal, http.MethodGet, "/config", 0, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to construct get-config object request - %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
|
}
|
|
|
|
}
|
|
|
|
// TestSetConfigHandler - test for SetConfigHandler.
|
|
func TestSetConfigHandler(t *testing.T) {
|
|
adminTestBed, err := prepareAdminXLTestBed()
|
|
if err != nil {
|
|
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
|
}
|
|
defer adminTestBed.TearDown()
|
|
|
|
// Initialize admin peers to make admin RPC calls.
|
|
globalMinioAddr = "127.0.0.1:9000"
|
|
|
|
// Prepare query params for set-config mgmt REST API.
|
|
queryVal := url.Values{}
|
|
queryVal.Set("config", "")
|
|
|
|
password := globalServerConfig.GetCredential().SecretKey
|
|
econfigJSON, err := madmin.EncryptData(password, configJSON)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
|
int64(len(econfigJSON)), bytes.NewReader(econfigJSON))
|
|
if err != nil {
|
|
t.Fatalf("Failed to construct set-config object request - %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("Expected to succeed but failed with %d, body: %s", rec.Code, rec.Body)
|
|
}
|
|
|
|
// Check that a very large config file returns an error.
|
|
{
|
|
// Make a large enough config string
|
|
invalidCfg := []byte(strings.Repeat("A", maxEConfigJSONSize+1))
|
|
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
|
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
|
if err != nil {
|
|
t.Fatalf("Failed to construct set-config object request - %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
respBody := string(rec.Body.Bytes())
|
|
if rec.Code != http.StatusBadRequest ||
|
|
!strings.Contains(respBody, "Configuration data provided exceeds the allowed maximum of") {
|
|
t.Errorf("Got unexpected response code or body %d - %s", rec.Code, respBody)
|
|
}
|
|
}
|
|
|
|
// Check that a config with duplicate keys in an object return
|
|
// error.
|
|
{
|
|
invalidCfg := append(econfigJSON[:len(econfigJSON)-1], []byte(`, "version": "15"}`)...)
|
|
req, err := buildAdminRequest(queryVal, http.MethodPut, "/config",
|
|
int64(len(invalidCfg)), bytes.NewReader(invalidCfg))
|
|
if err != nil {
|
|
t.Fatalf("Failed to construct set-config object request - %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
respBody := string(rec.Body.Bytes())
|
|
if rec.Code != http.StatusBadRequest ||
|
|
!strings.Contains(respBody, "JSON configuration provided is of incorrect format") {
|
|
t.Errorf("Got unexpected response code or body %d - %s", rec.Code, respBody)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAdminServerInfo(t *testing.T) {
|
|
adminTestBed, err := prepareAdminXLTestBed()
|
|
if err != nil {
|
|
t.Fatal("Failed to initialize a single node XL backend for admin handler tests.")
|
|
}
|
|
defer adminTestBed.TearDown()
|
|
|
|
// Initialize admin peers to make admin RPC calls.
|
|
globalMinioAddr = "127.0.0.1:9000"
|
|
|
|
// Prepare query params for set-config mgmt REST API.
|
|
queryVal := url.Values{}
|
|
queryVal.Set("info", "")
|
|
|
|
req, err := buildAdminRequest(queryVal, http.MethodGet, "/info", 0, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to construct get-config object request - %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
|
}
|
|
|
|
results := []ServerInfo{}
|
|
err = json.NewDecoder(rec.Body).Decode(&results)
|
|
if err != nil {
|
|
t.Fatalf("Failed to decode set config result json %v", err)
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
t.Error("Expected at least one server info result")
|
|
}
|
|
|
|
for _, serverInfo := range results {
|
|
if len(serverInfo.Addr) == 0 {
|
|
t.Error("Expected server address to be non empty")
|
|
}
|
|
if serverInfo.Error != "" {
|
|
t.Errorf("Unexpected error = %v\n", serverInfo.Error)
|
|
}
|
|
if serverInfo.Data.Properties.Region != globalMinioDefaultRegion {
|
|
t.Errorf("Expected %s, got %s", globalMinioDefaultRegion, serverInfo.Data.Properties.Region)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestToAdminAPIErrCode - test for toAdminAPIErrCode helper function.
|
|
func TestToAdminAPIErrCode(t *testing.T) {
|
|
testCases := []struct {
|
|
err error
|
|
expectedAPIErr APIErrorCode
|
|
}{
|
|
// 1. Server not in quorum.
|
|
{
|
|
err: errXLWriteQuorum,
|
|
expectedAPIErr: ErrAdminConfigNoQuorum,
|
|
},
|
|
// 2. No error.
|
|
{
|
|
err: nil,
|
|
expectedAPIErr: ErrNone,
|
|
},
|
|
// 3. Non-admin API specific error.
|
|
{
|
|
err: errDiskNotFound,
|
|
expectedAPIErr: toAPIErrorCode(context.Background(), errDiskNotFound),
|
|
},
|
|
}
|
|
|
|
for i, test := range testCases {
|
|
actualErr := toAdminAPIErrCode(context.Background(), test.err)
|
|
if actualErr != test.expectedAPIErr {
|
|
t.Errorf("Test %d: Expected %v but received %v",
|
|
i+1, test.expectedAPIErr, actualErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mkHealStartReq(t *testing.T, bucket, prefix string,
|
|
opts madmin.HealOpts) *http.Request {
|
|
|
|
body, err := json.Marshal(opts)
|
|
if err != nil {
|
|
t.Fatalf("Unable marshal heal opts")
|
|
}
|
|
|
|
path := fmt.Sprintf("/minio/admin/v1/heal/%s", bucket)
|
|
if bucket != "" && prefix != "" {
|
|
path += "/" + prefix
|
|
}
|
|
|
|
req, err := newTestRequest("POST", path,
|
|
int64(len(body)), bytes.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("Failed to construct request - %v", err)
|
|
}
|
|
cred := globalServerConfig.GetCredential()
|
|
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to sign request - %v", err)
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
func mkHealStatusReq(t *testing.T, bucket, prefix,
|
|
clientToken string) *http.Request {
|
|
|
|
path := fmt.Sprintf("/minio/admin/v1/heal/%s", bucket)
|
|
if bucket != "" && prefix != "" {
|
|
path += "/" + prefix
|
|
}
|
|
path += fmt.Sprintf("?clientToken=%s", clientToken)
|
|
|
|
req, err := newTestRequest("POST", path, 0, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to construct request - %v", err)
|
|
}
|
|
cred := globalServerConfig.GetCredential()
|
|
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to sign request - %v", err)
|
|
}
|
|
|
|
return req
|
|
}
|
|
|
|
func collectHealResults(t *testing.T, adminTestBed *adminXLTestBed, bucket,
|
|
prefix, clientToken string, timeLimitSecs int) madmin.HealTaskStatus {
|
|
|
|
var res, cur madmin.HealTaskStatus
|
|
|
|
// loop and fetch heal status. have a time-limit to loop over
|
|
// all statuses.
|
|
timeLimit := UTCNow().Add(time.Second * time.Duration(timeLimitSecs))
|
|
for cur.Summary != healStoppedStatus && cur.Summary != healFinishedStatus {
|
|
if UTCNow().After(timeLimit) {
|
|
t.Fatalf("heal-status loop took too long - clientToken: %s", clientToken)
|
|
}
|
|
req := mkHealStatusReq(t, bucket, prefix, clientToken)
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
if http.StatusOK != rec.Code {
|
|
t.Errorf("Unexpected status code - got %d but expected %d",
|
|
rec.Code, http.StatusOK)
|
|
break
|
|
}
|
|
err := json.NewDecoder(rec.Body).Decode(&cur)
|
|
if err != nil {
|
|
t.Errorf("unable to unmarshal resp: %v", err)
|
|
break
|
|
}
|
|
|
|
// all results are accumulated into a slice
|
|
// and returned to caller in the end
|
|
allItems := append(res.Items, cur.Items...)
|
|
res = cur
|
|
res.Items = allItems
|
|
|
|
time.Sleep(time.Millisecond * 200)
|
|
}
|
|
|
|
return res
|
|
}
|