mirror of
				https://github.com/minio/minio.git
				synced 2025-11-04 10:11:09 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			880 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			880 lines
		
	
	
		
			22 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"
 | 
						|
 | 
						|
	"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 {
 | 
						|
	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 := rec.Body.String()
 | 
						|
		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 := rec.Body.String()
 | 
						|
		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)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |