Files
object-browser/restapi/admin_tenants_test.go
Daniel Valdivia bdfa6dc9bf Support Usage API talk to MinIO over TLS with Insecure (#241)
* Support Usage API talk to MinIO over TLS with Insecure

Right now if MinIO is running  with TLS, and the certificate is not trusted by console, we fail usage requests. We need to leverage the support for insecure connections so we can read Health Checks and Usage information.

* Remove unusd import
2020-08-09 14:36:55 -07:00

723 lines
24 KiB
Go

// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// 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 restapi
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"reflect"
"testing"
"github.com/go-openapi/swag"
"github.com/minio/console/cluster"
"github.com/minio/console/models"
"github.com/minio/console/restapi/operations/admin_api"
operator "github.com/minio/operator/pkg/apis/minio.min.io/v1"
v1 "github.com/minio/operator/pkg/apis/minio.min.io/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
types "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/fake"
)
var opClientTenantDeleteMock func(ctx context.Context, namespace string, tenantName string, options metav1.DeleteOptions) error
var opClientTenantGetMock func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error)
var opClientTenantPatchMock func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error)
var opClientTenantListMock func(ctx context.Context, namespace string, opts metav1.ListOptions) (*v1.TenantList, error)
var httpClientGetMock func(url string) (resp *http.Response, err error)
var k8sclientGetSecretMock func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error)
var k8sclientGetServiceMock func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error)
// mock function of TenantDelete()
func (ac opClientMock) TenantDelete(ctx context.Context, namespace string, tenantName string, options metav1.DeleteOptions) error {
return opClientTenantDeleteMock(ctx, namespace, tenantName, options)
}
// mock function of TenantGet()
func (ac opClientMock) TenantGet(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return opClientTenantGetMock(ctx, namespace, tenantName, options)
}
// mock function of TenantPatch()
func (ac opClientMock) TenantPatch(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return opClientTenantPatchMock(ctx, namespace, tenantName, pt, data, options)
}
// mock function of TenantList()
func (ac opClientMock) TenantList(ctx context.Context, namespace string, opts metav1.ListOptions) (*v1.TenantList, error) {
return opClientTenantListMock(ctx, namespace, opts)
}
// mock function of get()
func (h httpClientMock) Get(url string) (resp *http.Response, err error) {
return httpClientGetMock(url)
}
func (c k8sClientMock) getSecret(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return k8sclientGetSecretMock(ctx, namespace, secretName, opts)
}
func (c k8sClientMock) getService(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error) {
return k8sclientGetServiceMock(ctx, namespace, serviceName, opts)
}
func Test_TenantInfoTenantAdminClient(t *testing.T) {
ctx := context.Background()
kClient := k8sClientMock{}
type args struct {
ctx context.Context
client K8sClient
namespace string
tenantName string
serviceName string
scheme string
insecure bool
}
tests := []struct {
name string
args args
wantErr bool
mockGetSecret func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error)
mockGetService func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error)
}{
{
name: "Return Tenant Admin, no errors",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
vals["secretkey"] = []byte("secret")
vals["accesskey"] = []byte("access")
sec := &corev1.Secret{
Data: vals,
}
return sec, nil
},
mockGetService: func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error) {
serv := &corev1.Service{
Spec: corev1.ServiceSpec{
ClusterIP: "10.1.1.2",
},
}
return serv, nil
},
wantErr: false,
},
{
name: "Access key not stored on secrets",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
vals["secretkey"] = []byte("secret")
sec := &corev1.Secret{
Data: vals,
}
return sec, nil
},
mockGetService: func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error) {
serv := &corev1.Service{
Spec: corev1.ServiceSpec{
ClusterIP: "10.1.1.2",
},
}
return serv, nil
},
wantErr: true,
},
{
name: "Secret key not stored on secrets",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
vals["accesskey"] = []byte("access")
sec := &corev1.Secret{
Data: vals,
}
return sec, nil
},
mockGetService: func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error) {
serv := &corev1.Service{
Spec: corev1.ServiceSpec{
ClusterIP: "10.1.1.2",
},
}
return serv, nil
},
wantErr: true,
},
{
name: "Handle error on getService",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
vals := make(map[string][]byte)
vals["accesskey"] = []byte("access")
vals["secretkey"] = []byte("secret")
sec := &corev1.Secret{
Data: vals,
}
return sec, nil
},
mockGetService: func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error) {
return nil, errors.New("error")
},
wantErr: true,
},
{
name: "Handle error on getSecret",
args: args{
ctx: ctx,
client: kClient,
namespace: "default",
tenantName: "tenant-1",
serviceName: "service-1",
scheme: "http",
},
mockGetSecret: func(ctx context.Context, namespace, secretName string, opts metav1.GetOptions) (*corev1.Secret, error) {
return nil, errors.New("error")
},
mockGetService: func(ctx context.Context, namespace, serviceName string, opts metav1.GetOptions) (*corev1.Service, error) {
serv := &corev1.Service{
Spec: corev1.ServiceSpec{
ClusterIP: "10.1.1.2",
},
}
return serv, nil
},
wantErr: true,
},
}
for _, tt := range tests {
k8sclientGetSecretMock = tt.mockGetSecret
k8sclientGetServiceMock = tt.mockGetService
t.Run(tt.name, func(t *testing.T) {
got, err := getTenantAdminClient(tt.args.ctx, tt.args.client, tt.args.namespace, tt.args.tenantName, tt.args.serviceName, tt.args.scheme, tt.args.insecure)
if err != nil {
if tt.wantErr {
return
}
t.Errorf("getTenantAdminClient() error = %v, wantErr %v", err, tt.wantErr)
}
if got == nil {
t.Errorf("getTenantAdminClient() expected type: *madmin.AdminClient, got: nil")
}
})
}
}
func Test_TenantInfo(t *testing.T) {
testTimeStamp := metav1.Now()
type args struct {
minioTenant *operator.Tenant
tenantInfo *usageInfo
}
tests := []struct {
name string
args args
want *models.Tenant
}{
{
name: "Get tenant Info",
args: args{
minioTenant: &operator.Tenant{
ObjectMeta: metav1.ObjectMeta{
CreationTimestamp: testTimeStamp,
Name: "tenant1",
Namespace: "minio-ns",
},
Spec: operator.TenantSpec{
Zones: []operator.Zone{
{
Name: "zone1",
Servers: int32(2),
VolumesPerServer: 4,
VolumeClaimTemplate: &corev1.PersistentVolumeClaim{
Spec: corev1.PersistentVolumeClaimSpec{
Resources: corev1.ResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceStorage: resource.MustParse("1Mi"),
},
},
StorageClassName: swag.String("standard"),
},
},
},
},
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
},
Status: operator.TenantStatus{
CurrentState: "ready",
},
},
tenantInfo: &usageInfo{
DisksUsage: 1024,
},
},
want: &models.Tenant{
CreationDate: testTimeStamp.String(),
Name: "tenant1",
TotalSize: int64(8388608),
CurrentState: "ready",
Zones: []*models.Zone{
{
Name: "zone1",
Servers: swag.Int64(int64(2)),
VolumesPerServer: swag.Int32(4),
VolumeConfiguration: &models.ZoneVolumeConfiguration{
StorageClassName: "standard",
Size: swag.Int64(1024 * 1024),
},
},
},
Namespace: "minio-ns",
Image: "minio/minio:RELEASE.2020-06-14T18-32-17Z",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getTenantInfo(tt.args.minioTenant)
if !reflect.DeepEqual(got, tt.want) {
ji, _ := json.Marshal(got)
vi, _ := json.Marshal(tt.want)
t.Errorf("got %s want %s", ji, vi)
}
})
}
}
func Test_deleteTenantAction(t *testing.T) {
opClient := opClientMock{}
type args struct {
ctx context.Context
operatorClient OperatorClient
nameSpace string
tenantName string
mockTenantDelete func(ctx context.Context, namespace string, tenantName string, options metav1.DeleteOptions) error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Success",
args: args{
ctx: context.Background(),
operatorClient: opClient,
nameSpace: "default",
tenantName: "minio-tenant",
mockTenantDelete: func(ctx context.Context, namespace string, tenantName string, options metav1.DeleteOptions) error {
return nil
},
},
wantErr: false,
},
{
name: "Error",
args: args{
ctx: context.Background(),
operatorClient: opClient,
nameSpace: "default",
tenantName: "minio-tenant",
mockTenantDelete: func(ctx context.Context, namespace string, tenantName string, options metav1.DeleteOptions) error {
return errors.New("something happened")
},
},
wantErr: true,
},
}
for _, tt := range tests {
opClientTenantDeleteMock = tt.args.mockTenantDelete
t.Run(tt.name, func(t *testing.T) {
if err := deleteTenantAction(tt.args.ctx, tt.args.operatorClient, tt.args.nameSpace, tt.args.tenantName); (err != nil) != tt.wantErr {
t.Errorf("deleteTenantAction() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_TenantAddZone(t *testing.T) {
opClient := opClientMock{}
type args struct {
ctx context.Context
operatorClient OperatorClient
nameSpace string
mockTenantPatch func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error)
mockTenantGet func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error)
params admin_api.TenantAddZoneParams
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Add zone, no errors",
args: args{
ctx: context.Background(),
operatorClient: opClient,
nameSpace: "default",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
params: admin_api.TenantAddZoneParams{
Body: &models.Zone{
Name: "zone-1",
Servers: swag.Int64(int64(4)),
VolumeConfiguration: &models.ZoneVolumeConfiguration{
Size: swag.Int64(2147483648),
StorageClassName: "standard",
},
VolumesPerServer: swag.Int32(4),
},
},
},
wantErr: false,
}, {
name: "Add zone, error size",
args: args{
ctx: context.Background(),
operatorClient: opClient,
nameSpace: "default",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
params: admin_api.TenantAddZoneParams{
Body: &models.Zone{
Name: "zone-1",
Servers: swag.Int64(int64(4)),
VolumeConfiguration: &models.ZoneVolumeConfiguration{
Size: swag.Int64(0),
StorageClassName: "standard",
},
VolumesPerServer: swag.Int32(4),
},
},
},
wantErr: true,
},
{
name: "Add zone, error servers negative",
args: args{
ctx: context.Background(),
operatorClient: opClient,
nameSpace: "default",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
params: admin_api.TenantAddZoneParams{
Body: &models.Zone{
Name: "zone-1",
Servers: swag.Int64(int64(-1)),
VolumeConfiguration: &models.ZoneVolumeConfiguration{
Size: swag.Int64(2147483648),
StorageClassName: "standard",
},
VolumesPerServer: swag.Int32(4),
},
},
},
wantErr: true,
},
{
name: "Add zone, error volumes per server negative",
args: args{
ctx: context.Background(),
operatorClient: opClient,
nameSpace: "default",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
params: admin_api.TenantAddZoneParams{
Body: &models.Zone{
Name: "zone-1",
Servers: swag.Int64(int64(4)),
VolumeConfiguration: &models.ZoneVolumeConfiguration{
Size: swag.Int64(2147483648),
StorageClassName: "standard",
},
VolumesPerServer: swag.Int32(-1),
},
},
},
wantErr: true,
},
{
name: "Error on patch, handle error",
args: args{
ctx: context.Background(),
operatorClient: opClient,
nameSpace: "default",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return nil, errors.New("errors")
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
params: admin_api.TenantAddZoneParams{
Body: &models.Zone{
Name: "zone-1",
Servers: swag.Int64(int64(4)),
},
},
},
wantErr: true,
},
{
name: "Error on get, handle error",
args: args{
ctx: context.Background(),
operatorClient: opClient,
nameSpace: "default",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return nil, errors.New("errors")
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return nil, errors.New("errors")
},
params: admin_api.TenantAddZoneParams{
Body: &models.Zone{
Name: "zone-1",
Servers: swag.Int64(int64(4)),
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
opClientTenantGetMock = tt.args.mockTenantGet
opClientTenantPatchMock = tt.args.mockTenantPatch
t.Run(tt.name, func(t *testing.T) {
if err := addTenantZone(tt.args.ctx, tt.args.operatorClient, tt.args.params); (err != nil) != tt.wantErr {
t.Errorf("addTenantZone() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_UpdateTenantAction(t *testing.T) {
opClient := opClientMock{}
httpClientM := httpClientMock{}
type args struct {
ctx context.Context
operatorClient OperatorClient
httpCl cluster.HTTPClientI
nameSpace string
tenantName string
mockTenantPatch func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error)
mockTenantGet func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error)
mockHTTPClientGet func(url string) (resp *http.Response, err error)
params admin_api.UpdateTenantParams
}
tests := []struct {
name string
args args
objs []runtime.Object
wantErr bool
}{
{
name: "Update minio version no errors",
args: args{
ctx: context.Background(),
operatorClient: opClient,
httpCl: httpClientM,
nameSpace: "default",
tenantName: "minio-tenant",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockHTTPClientGet: func(url string) (resp *http.Response, err error) {
return &http.Response{}, nil
},
params: admin_api.UpdateTenantParams{
Body: &models.UpdateTenantRequest{
Image: "minio/minio:RELEASE.2020-06-03T22-13-49Z",
},
},
},
wantErr: false,
},
{
name: "Error occurs getting minioTenant",
args: args{
ctx: context.Background(),
operatorClient: opClient,
httpCl: httpClientM,
nameSpace: "default",
tenantName: "minio-tenant",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return nil, errors.New("error-get")
},
mockHTTPClientGet: func(url string) (resp *http.Response, err error) {
return &http.Response{}, nil
},
params: admin_api.UpdateTenantParams{
Body: &models.UpdateTenantRequest{
Image: "minio/minio:RELEASE.2020-06-03T22-13-49Z",
},
},
},
wantErr: true,
},
{
name: "Error occurs patching minioTenant",
args: args{
ctx: context.Background(),
operatorClient: opClient,
httpCl: httpClientM,
nameSpace: "default",
tenantName: "minio-tenant",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return nil, errors.New("error-get")
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockHTTPClientGet: func(url string) (resp *http.Response, err error) {
return &http.Response{}, nil
},
params: admin_api.UpdateTenantParams{
Body: &models.UpdateTenantRequest{
Image: "minio/minio:RELEASE.2020-06-03T22-13-49Z",
},
},
},
wantErr: true,
},
{
name: "Empty image should patch correctly with latest image",
args: args{
ctx: context.Background(),
operatorClient: opClient,
httpCl: httpClientM,
nameSpace: "default",
tenantName: "minio-tenant",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockHTTPClientGet: func(url string) (resp *http.Response, err error) {
r := ioutil.NopCloser(bytes.NewReader([]byte(`./minio.RELEASE.2020-06-18T02-23-35Z"`)))
return &http.Response{
Body: r,
}, nil
},
params: admin_api.UpdateTenantParams{
Body: &models.UpdateTenantRequest{
Image: "",
},
},
},
wantErr: false,
},
{
name: "Empty image input Error retrieving latest image",
args: args{
ctx: context.Background(),
operatorClient: opClient,
httpCl: httpClientM,
nameSpace: "default",
tenantName: "minio-tenant",
mockTenantPatch: func(ctx context.Context, namespace string, tenantName string, pt types.PatchType, data []byte, options metav1.PatchOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockTenantGet: func(ctx context.Context, namespace string, tenantName string, options metav1.GetOptions) (*v1.Tenant, error) {
return &v1.Tenant{}, nil
},
mockHTTPClientGet: func(url string) (resp *http.Response, err error) {
return nil, errors.New("error")
},
params: admin_api.UpdateTenantParams{
Body: &models.UpdateTenantRequest{
Image: "",
},
},
},
wantErr: true,
},
}
for _, tt := range tests {
opClientTenantGetMock = tt.args.mockTenantGet
opClientTenantPatchMock = tt.args.mockTenantPatch
httpClientGetMock = tt.args.mockHTTPClientGet
cnsClient := fake.NewSimpleClientset(tt.objs...)
t.Run(tt.name, func(t *testing.T) {
if err := updateTenantAction(tt.args.ctx, tt.args.operatorClient, cnsClient.CoreV1(), tt.args.httpCl, tt.args.nameSpace, tt.args.params); (err != nil) != tt.wantErr {
t.Errorf("deleteTenantAction() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}