diff --git a/.github/workflows/jobs.yaml b/.github/workflows/jobs.yaml index 2a74ff30f9..2c231f1fd4 100644 --- a/.github/workflows/jobs.yaml +++ b/.github/workflows/jobs.yaml @@ -1298,7 +1298,7 @@ jobs: go tool cover -func=all.out | grep total > tmp2 result=`cat tmp2 | awk 'END {print $3}'` result=${result%\%} - threshold=69.4 + threshold=71.4 echo "Result:" echo "$result%" if (( $(echo "$result >= $threshold" |bc -l) )); then diff --git a/restapi/admin_client_mock.go b/restapi/admin_client_mock.go index 435feb25a7..571e27abe3 100644 --- a/restapi/admin_client_mock.go +++ b/restapi/admin_client_mock.go @@ -86,26 +86,30 @@ var ( minioInfoServiceAccountMock func(ctx context.Context, serviceAccount string) (madmin.InfoServiceAccountResp, error) minioUpdateServiceAccountMock func(ctx context.Context, serviceAccount string, opts madmin.UpdateServiceAccountReq) error minioGetLDAPPolicyEntitiesMock func(ctx context.Context, query madmin.PolicyEntitiesQuery) (madmin.PolicyEntitiesResult, error) + + minioListRemoteBucketsMock func(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error) + minioGetRemoteBucketMock func(ctx context.Context, bucket, arnType string) (targets *madmin.BucketTarget, err error) + minioAddRemoteBucketMock func(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error) ) func (ac AdminClientMock) serverInfo(ctx context.Context) (madmin.InfoMessage, error) { return MinioServerInfoMock(ctx) } -func (ac AdminClientMock) listRemoteBuckets(_ context.Context, _, _ string) (targets []madmin.BucketTarget, err error) { - return nil, nil +func (ac AdminClientMock) listRemoteBuckets(ctx context.Context, bucket, arnType string) (targets []madmin.BucketTarget, err error) { + return minioListRemoteBucketsMock(ctx, bucket, arnType) } -func (ac AdminClientMock) getRemoteBucket(_ context.Context, _, _ string) (targets *madmin.BucketTarget, err error) { - return nil, nil +func (ac AdminClientMock) getRemoteBucket(ctx context.Context, bucket, arnType string) (targets *madmin.BucketTarget, err error) { + return minioGetRemoteBucketMock(ctx, bucket, arnType) } func (ac AdminClientMock) removeRemoteBucket(_ context.Context, _, _ string) error { return nil } -func (ac AdminClientMock) addRemoteBucket(_ context.Context, _ string, _ *madmin.BucketTarget) (string, error) { - return "", nil +func (ac AdminClientMock) addRemoteBucket(ctx context.Context, bucket string, target *madmin.BucketTarget) (string, error) { + return minioAddRemoteBucketMock(ctx, bucket, target) } func (ac AdminClientMock) changePassword(ctx context.Context, accessKey, secretKey string) error { diff --git a/restapi/admin_remote_buckets.go b/restapi/admin_remote_buckets.go index 5c7895820a..7a7add8f00 100644 --- a/restapi/admin_remote_buckets.go +++ b/restapi/admin_remote_buckets.go @@ -150,14 +150,7 @@ func getListRemoteBucketsResponse(session *models.Principal, params bucketApi.Li return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err)) } adminClient := AdminClient{Client: mAdmin} - buckets, err := listRemoteBuckets(ctx, adminClient) - if err != nil { - return nil, ErrorWithContext(ctx, fmt.Errorf("error listing remote buckets: %v", err)) - } - return &models.ListRemoteBucketsResponse{ - Buckets: buckets, - Total: int64(len(buckets)), - }, nil + return listRemoteBuckets(ctx, adminClient) } func getRemoteBucketDetailsResponse(session *models.Principal, params bucketApi.RemoteBucketDetailsParams) (*models.RemoteBucket, *models.Error) { @@ -168,11 +161,7 @@ func getRemoteBucketDetailsResponse(session *models.Principal, params bucketApi. return nil, ErrorWithContext(ctx, fmt.Errorf("error creating Madmin Client: %v", err)) } adminClient := AdminClient{Client: mAdmin} - bucket, err := getRemoteBucket(ctx, adminClient, params.Name) - if err != nil { - return nil, ErrorWithContext(ctx, fmt.Errorf("error getting remote bucket details: %v", err)) - } - return bucket, nil + return getRemoteBucket(ctx, adminClient, params.Name) } func getDeleteRemoteBucketResponse(session *models.Principal, params bucketApi.DeleteRemoteBucketParams) *models.Error { @@ -205,11 +194,11 @@ func getAddRemoteBucketResponse(session *models.Principal, params bucketApi.AddR return nil } -func listRemoteBuckets(ctx context.Context, client MinioAdmin) ([]*models.RemoteBucket, error) { +func listRemoteBuckets(ctx context.Context, client MinioAdmin) (*models.ListRemoteBucketsResponse, *models.Error) { var remoteBuckets []*models.RemoteBucket buckets, err := client.listRemoteBuckets(ctx, "", "") if err != nil { - return nil, err + return nil, ErrorWithContext(ctx, fmt.Errorf("error listing remote buckets: %v", err)) } for _, bucket := range buckets { remoteBucket := &models.RemoteBucket{ @@ -230,16 +219,20 @@ func listRemoteBuckets(ctx context.Context, client MinioAdmin) ([]*models.Remote } remoteBuckets = append(remoteBuckets, remoteBucket) } - return remoteBuckets, nil + + return &models.ListRemoteBucketsResponse{ + Buckets: remoteBuckets, + Total: int64(len(remoteBuckets)), + }, nil } -func getRemoteBucket(ctx context.Context, client MinioAdmin, name string) (*models.RemoteBucket, error) { +func getRemoteBucket(ctx context.Context, client MinioAdmin, name string) (*models.RemoteBucket, *models.Error) { remoteBucket, err := client.getRemoteBucket(ctx, name, "") if err != nil { - return nil, err + return nil, ErrorWithContext(ctx, fmt.Errorf("error getting remote bucket details: %v", err)) } if remoteBucket == nil { - return nil, errors.New("bucket not found") + return nil, ErrorWithContext(ctx, "error getting remote bucket details: bucket not found") } return &models.RemoteBucket{ AccessKey: &remoteBucket.Credentials.AccessKey, @@ -556,20 +549,19 @@ func listExternalBucketsResponse(params bucketApi.ListExternalBucketsParams) (*m if err != nil { return nil, ErrorWithContext(ctx, err) } - // create a minioClient interface implementation - // defining the client to be used - remoteClient := AdminClient{Client: remoteAdmin} - buckets, err := getAccountBuckets(ctx, remoteClient) + return listExternalBuckets(ctx, AdminClient{Client: remoteAdmin}) +} + +func listExternalBuckets(ctx context.Context, client MinioAdmin) (*models.ListBucketsResponse, *models.Error) { + buckets, err := getAccountBuckets(ctx, client) if err != nil { return nil, ErrorWithContext(ctx, err) } - // serialize output - listBucketsResponse := &models.ListBucketsResponse{ + return &models.ListBucketsResponse{ Buckets: buckets, Total: int64(len(buckets)), - } - return listBucketsResponse, nil + }, nil } func getARNFromID(conf *replication.Config, rule string) string { @@ -674,7 +666,7 @@ func deleteAllReplicationRules(ctx context.Context, session *models.Principal, b err2 := mcClient.deleteAllReplicationRules(ctx) if err2 != nil { - return err + return err2.ToGoError() } for i := range cfg.Rules { diff --git a/restapi/admin_remote_buckets_test.go b/restapi/admin_remote_buckets_test.go new file mode 100644 index 0000000000..fa0998df6e --- /dev/null +++ b/restapi/admin_remote_buckets_test.go @@ -0,0 +1,381 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2023 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 . + +package restapi + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/go-openapi/swag" + "github.com/minio/console/models" + "github.com/minio/console/restapi/operations" + bucketApi "github.com/minio/console/restapi/operations/bucket" + "github.com/minio/madmin-go/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type RemoteBucketsTestSuite struct { + suite.Suite + assert *assert.Assertions + currentServer string + isServerSet bool + server *httptest.Server + adminClient AdminClientMock + minioClient minioClientMock + mockRemoteBucket *models.RemoteBucket + mockBucketTarget *madmin.BucketTarget + mockListBuckets *models.ListBucketsResponse +} + +func (suite *RemoteBucketsTestSuite) SetupSuite() { + suite.assert = assert.New(suite.T()) + suite.adminClient = AdminClientMock{} + suite.minioClient = minioClientMock{} + suite.mockObjects() +} + +func (suite *RemoteBucketsTestSuite) mockObjects() { + suite.mockListBuckets = &models.ListBucketsResponse{ + Buckets: []*models.Bucket{}, + Total: 0, + } + suite.mockRemoteBucket = &models.RemoteBucket{ + AccessKey: swag.String("accessKey"), + SecretKey: "secretKey", + RemoteARN: swag.String("remoteARN"), + Service: "replication", + SourceBucket: swag.String("sourceBucket"), + TargetBucket: "targetBucket", + TargetURL: "targetURL", + Status: "", + } + suite.mockBucketTarget = &madmin.BucketTarget{ + Credentials: &madmin.Credentials{ + AccessKey: *suite.mockRemoteBucket.AccessKey, + SecretKey: suite.mockRemoteBucket.SecretKey, + }, + Arn: *suite.mockRemoteBucket.RemoteARN, + SourceBucket: *suite.mockRemoteBucket.SourceBucket, + TargetBucket: suite.mockRemoteBucket.TargetBucket, + Endpoint: suite.mockRemoteBucket.TargetURL, + } +} + +func (suite *RemoteBucketsTestSuite) SetupTest() { + suite.server = httptest.NewServer(http.HandlerFunc(suite.serverHandler)) + suite.currentServer, suite.isServerSet = os.LookupEnv(ConsoleMinIOServer) + os.Setenv(ConsoleMinIOServer, suite.server.URL) +} + +func (suite *RemoteBucketsTestSuite) serverHandler(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(400) +} + +func (suite *RemoteBucketsTestSuite) TearDownSuite() { +} + +func (suite *RemoteBucketsTestSuite) TearDownTest() { + if suite.isServerSet { + os.Setenv(ConsoleMinIOServer, suite.currentServer) + } else { + os.Unsetenv(ConsoleMinIOServer) + } +} + +func (suite *RemoteBucketsTestSuite) TestRegisterRemoteBucketsHandlers() { + api := &operations.ConsoleAPI{} + suite.assertHandlersAreNil(api) + registerAdminBucketRemoteHandlers(api) + suite.assertHandlersAreNotNil(api) +} + +func (suite *RemoteBucketsTestSuite) assertHandlersAreNil(api *operations.ConsoleAPI) { + suite.assert.Nil(api.BucketListRemoteBucketsHandler) + suite.assert.Nil(api.BucketRemoteBucketDetailsHandler) + suite.assert.Nil(api.BucketDeleteRemoteBucketHandler) + suite.assert.Nil(api.BucketAddRemoteBucketHandler) + suite.assert.Nil(api.BucketSetMultiBucketReplicationHandler) + suite.assert.Nil(api.BucketListExternalBucketsHandler) + suite.assert.Nil(api.BucketDeleteBucketReplicationRuleHandler) + suite.assert.Nil(api.BucketDeleteAllReplicationRulesHandler) + suite.assert.Nil(api.BucketDeleteSelectedReplicationRulesHandler) + suite.assert.Nil(api.BucketUpdateMultiBucketReplicationHandler) +} + +func (suite *RemoteBucketsTestSuite) assertHandlersAreNotNil(api *operations.ConsoleAPI) { + suite.assert.NotNil(api.BucketListRemoteBucketsHandler) + suite.assert.NotNil(api.BucketRemoteBucketDetailsHandler) + suite.assert.NotNil(api.BucketDeleteRemoteBucketHandler) + suite.assert.NotNil(api.BucketAddRemoteBucketHandler) + suite.assert.NotNil(api.BucketSetMultiBucketReplicationHandler) + suite.assert.NotNil(api.BucketListExternalBucketsHandler) + suite.assert.NotNil(api.BucketDeleteBucketReplicationRuleHandler) + suite.assert.NotNil(api.BucketDeleteAllReplicationRulesHandler) + suite.assert.NotNil(api.BucketDeleteSelectedReplicationRulesHandler) + suite.assert.NotNil(api.BucketUpdateMultiBucketReplicationHandler) +} + +func (suite *RemoteBucketsTestSuite) TestListRemoteBucketsHandlerWithError() { + params, api := suite.initListRemoteBucketsRequest() + response := api.BucketListRemoteBucketsHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.ListRemoteBucketsDefault) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initListRemoteBucketsRequest() (params bucketApi.ListRemoteBucketsParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + params.HTTPRequest = &http.Request{} + return params, api +} + +func (suite *RemoteBucketsTestSuite) TestListRemoteBucketsWithoutError() { + ctx := context.Background() + minioListRemoteBucketsMock = func(_ context.Context, _, _ string) (targets []madmin.BucketTarget, err error) { + return []madmin.BucketTarget{{ + Credentials: &madmin.Credentials{ + AccessKey: "accessKey", + SecretKey: "secretKey", + }, + }}, nil + } + res, err := listRemoteBuckets(ctx, &suite.adminClient) + suite.assert.NotNil(res) + suite.assert.Nil(err) +} + +func (suite *RemoteBucketsTestSuite) TestRemoteBucketDetailsHandlerWithError() { + params, api := suite.initRemoteBucketDetailsRequest() + response := api.BucketRemoteBucketDetailsHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.RemoteBucketDetailsDefault) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initRemoteBucketDetailsRequest() (params bucketApi.RemoteBucketDetailsParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + params.HTTPRequest = &http.Request{} + return params, api +} + +func (suite *RemoteBucketsTestSuite) TestGetRemoteBucketWithoutError() { + ctx := context.Background() + minioGetRemoteBucketMock = func(_ context.Context, _, _ string) (targets *madmin.BucketTarget, err error) { + return suite.mockBucketTarget, nil + } + res, err := getRemoteBucket(ctx, &suite.adminClient, "bucketName") + suite.assert.Nil(err) + suite.assert.NotNil(res) + suite.assert.Equal(suite.mockRemoteBucket, res) +} + +func (suite *RemoteBucketsTestSuite) TestDeleteRemoteBucketHandlerWithError() { + params, api := suite.initDeleteRemoteBucketRequest() + response := api.BucketDeleteRemoteBucketHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.DeleteRemoteBucketDefault) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initDeleteRemoteBucketRequest() (params bucketApi.DeleteRemoteBucketParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + params.HTTPRequest = &http.Request{} + return params, api +} + +func (suite *RemoteBucketsTestSuite) TestAddRemoteBucketHandlerWithError() { + params, api := suite.initAddRemoteBucketRequest() + response := api.BucketAddRemoteBucketHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.AddRemoteBucketDefault) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initAddRemoteBucketRequest() (params bucketApi.AddRemoteBucketParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + url := "^&*&^%^" + accessKey := "accessKey" + secretKey := "secretKey" + params.HTTPRequest = &http.Request{} + params.Body = &models.CreateRemoteBucket{ + TargetURL: &url, + AccessKey: &accessKey, + SecretKey: &secretKey, + } + return params, api +} + +func (suite *RemoteBucketsTestSuite) TestAddRemoteBucketWithoutError() { + ctx := context.Background() + minioAddRemoteBucketMock = func(_ context.Context, _ string, _ *madmin.BucketTarget) (string, error) { + return "bucketName", nil + } + url := "https://localhost" + accessKey := "accessKey" + secretKey := "secretKey" + targetBucket := "targetBucket" + syncMode := "async" + sourceBucket := "sourceBucket" + data := models.CreateRemoteBucket{ + TargetURL: &url, + TargetBucket: &targetBucket, + AccessKey: &accessKey, + SecretKey: &secretKey, + SyncMode: &syncMode, + HealthCheckPeriod: 10, + SourceBucket: &sourceBucket, + } + res, err := addRemoteBucket(ctx, &suite.adminClient, data) + suite.assert.NotNil(res) + suite.assert.Nil(err) +} + +func (suite *RemoteBucketsTestSuite) TestSetMultiBucketReplicationHandlerWithError() { + params, api := suite.initSetMultiBucketReplicationRequest() + response := api.BucketSetMultiBucketReplicationHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.SetMultiBucketReplicationOK) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initSetMultiBucketReplicationRequest() (params bucketApi.SetMultiBucketReplicationParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + accessKey := "accessKey" + secretKey := "secretKey" + targetURL := "https://localhost" + syncMode := "async" + params.HTTPRequest = &http.Request{} + params.Body = &models.MultiBucketReplication{ + BucketsRelation: []*models.MultiBucketsRelation{{}}, + AccessKey: &accessKey, + SecretKey: &secretKey, + Region: "region", + TargetURL: &targetURL, + SyncMode: &syncMode, + Bandwidth: 10, + HealthCheckPeriod: 10, + } + return params, api +} + +func (suite *RemoteBucketsTestSuite) TestListExternalBucketsHandlerWithError() { + params, api := suite.initListExternalBucketsRequest() + response := api.BucketListExternalBucketsHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.ListExternalBucketsDefault) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initListExternalBucketsRequest() (params bucketApi.ListExternalBucketsParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + url := "http://localhost:9000" + accessKey := "accessKey" + secretKey := "secretKey" + tls := false + params.HTTPRequest = &http.Request{} + params.Body = &models.ListExternalBucketsParams{ + TargetURL: &url, + AccessKey: &accessKey, + SecretKey: &secretKey, + UseTLS: &tls, + } + return params, api +} + +func (suite *RemoteBucketsTestSuite) TestListExternalBucketsWithError() { + ctx := context.Background() + minioAccountInfoMock = func(ctx context.Context) (madmin.AccountInfo, error) { + return madmin.AccountInfo{}, errors.New("error") + } + res, err := listExternalBuckets(ctx, &suite.adminClient) + suite.assert.NotNil(err) + suite.assert.Nil(res) +} + +func (suite *RemoteBucketsTestSuite) TestListExternalBucketsWithoutError() { + ctx := context.Background() + minioAccountInfoMock = func(ctx context.Context) (madmin.AccountInfo, error) { + return madmin.AccountInfo{ + Buckets: []madmin.BucketAccessInfo{}, + }, nil + } + res, err := listExternalBuckets(ctx, &suite.adminClient) + suite.assert.Nil(err) + suite.assert.NotNil(res) + suite.assert.Equal(suite.mockListBuckets, res) +} + +func (suite *RemoteBucketsTestSuite) TestDeleteBucketReplicationRuleHandlerWithError() { + params, api := suite.initDeleteBucketReplicationRuleRequest() + response := api.BucketDeleteBucketReplicationRuleHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.DeleteBucketReplicationRuleDefault) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initDeleteBucketReplicationRuleRequest() (params bucketApi.DeleteBucketReplicationRuleParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + params.HTTPRequest = &http.Request{} + return params, api +} + +func (suite *RemoteBucketsTestSuite) TestDeleteAllReplicationRulesHandlerWithError() { + params, api := suite.initDeleteAllReplicationRulesRequest() + response := api.BucketDeleteAllReplicationRulesHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.DeleteAllReplicationRulesDefault) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initDeleteAllReplicationRulesRequest() (params bucketApi.DeleteAllReplicationRulesParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + params.HTTPRequest = &http.Request{} + return params, api +} + +func (suite *RemoteBucketsTestSuite) TestDeleteSelectedReplicationRulesHandlerWithError() { + params, api := suite.initDeleteSelectedReplicationRulesRequest() + response := api.BucketDeleteSelectedReplicationRulesHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.DeleteSelectedReplicationRulesDefault) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initDeleteSelectedReplicationRulesRequest() (params bucketApi.DeleteSelectedReplicationRulesParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + params.HTTPRequest = &http.Request{} + params.BucketName = "bucketName" + params.Rules = &models.BucketReplicationRuleList{ + Rules: []string{"rule1", "rule2"}, + } + + return params, api +} + +func (suite *RemoteBucketsTestSuite) TestUpdateMultiBucketReplicationHandlerWithError() { + params, api := suite.initUpdateMultiBucketReplicationRequest() + response := api.BucketUpdateMultiBucketReplicationHandler.Handle(params, &models.Principal{}) + _, ok := response.(*bucketApi.UpdateMultiBucketReplicationDefault) + suite.assert.True(ok) +} + +func (suite *RemoteBucketsTestSuite) initUpdateMultiBucketReplicationRequest() (params bucketApi.UpdateMultiBucketReplicationParams, api operations.ConsoleAPI) { + registerAdminBucketRemoteHandlers(&api) + params.HTTPRequest = &http.Request{} + params.Body = &models.MultiBucketReplicationEdit{} + return params, api +} + +func TestRemoteBuckets(t *testing.T) { + suite.Run(t, new(RemoteBucketsTestSuite)) +} diff --git a/restapi/user_buckets.go b/restapi/user_buckets.go index a49df4b196..55a0c6d13c 100644 --- a/restapi/user_buckets.go +++ b/restapi/user_buckets.go @@ -381,7 +381,7 @@ func getAccountBuckets(ctx context.Context, client MinioAdmin) ([]*models.Bucket if err != nil { return []*models.Bucket{}, err } - var bucketInfos []*models.Bucket + bucketInfos := []*models.Bucket{} for _, bucket := range info.Buckets { bucketElem := &models.Bucket{ CreationDate: bucket.Created.Format(time.RFC3339),