Repository: dragonflyoss/Dragonfly2
Pull request #1369 (2022-06-09 14:28:56)

feat: dynconfig add object storage
Signed-off-by: Gaius <gaius.qi@gmail.com>

<!--- Provide a general summary of your changes in the Title above -->

## Description
- Dynconfig add object storage.
- Manager add `GetObjectStorage` and `ListBuckets` grpc interface.
<!--- Describe your changes in detail -->

## Related Issue

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation Update (if none of the other choices apply)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I have read the **CONTRIBUTING** document.
- [ ] I have added tests to cover my changes.


codecov-commenter: # [Codecov](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1369?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) Report
> Merging [#1369](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1369?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) (4730ce0) into [main](https://codecov.io/gh/dragonflyoss/Dragonfly2/commit/0e07a28705e775fdda9f0c40e4b64d267a54c06c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) (0e07a28) will **increase** coverage by `0.08%`.
> The diff coverage is `87.50%`.

[![Impacted file tree graph](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1369/graphs/tree.svg?width=650&height=150&src=pr&token=PUICJYSAT7&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss)](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1369?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss)

```diff
@@            Coverage Diff             @@
##             main    #1369      +/-   ##
==========================================
+ Coverage   45.45%   45.53%   +0.08%     
==========================================
  Files         140      140              
  Lines       12000    12030      +30     
==========================================
+ Hits         5454     5478      +24     
- Misses       5925     5927       +2     
- Partials      621      625       +4     
```

| Flag | Coverage Δ | |
|---|---|---|
| Object-compatibility-e2etests | `?` | |
| e2etests | `?` | |
| unittests | `45.53% <87.50%> (+0.08%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1369?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss) | Coverage Δ | |
|---|---|---|
| [client/config/dynconfig.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2NvbmZpZy9keW5jb25maWcuZ28=) | `67.39% <87.50%> (+9.32%)` | :arrow_up: |
| [...ent/daemon/peer/peertask\_piecetask\_synchronizer.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2RhZW1vbi9wZWVyL3BlZXJ0YXNrX3BpZWNldGFza19zeW5jaHJvbml6ZXIuZ28=) | `47.61% <0.00%> (-1.30%)` | :arrow_down: |
| [client/daemon/peer/peertask\_conductor.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2RhZW1vbi9wZWVyL3BlZXJ0YXNrX2NvbmR1Y3Rvci5nbw==) | `57.14% <0.00%> (-0.21%)` | :arrow_down: |
| [client/daemon/rpcserver/subscriber.go](https://codecov.io/gh/dragonflyoss/Dragonfly2/pull/1369/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=dragonflyoss#diff-Y2xpZW50L2RhZW1vbi9ycGNzZXJ2ZXIvc3Vic2NyaWJlci5nbw==) | `51.14% <0.00%> (+2.29%)` | :arrow_up: |


Patch:
diff --git a/client/config/dynconfig.go b/client/config/dynconfig.go
index 2cd2ab2b..75ade12f 100644
--- a/client/config/dynconfig.go
+++ b/client/config/dynconfig.go
@@ -21,6 +21,9 @@ import (
 	"path/filepath"
 	"time"
 
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
 	logger "d7y.io/dragonfly/v2/internal/dflog"
 	internaldynconfig "d7y.io/dragonfly/v2/internal/dynconfig"
 	"d7y.io/dragonfly/v2/manager/searcher"
@@ -37,13 +40,21 @@ var (
 )
 
 type DynconfigData struct {
-	Schedulers []*manager.Scheduler
+	Schedulers    []*manager.Scheduler
+	ObjectStorage *manager.ObjectStorage
+	Buckets       []*manager.Bucket
 }
 
 type Dynconfig interface {
-	// Get the dynamic config from manager.
+	// Get the dynamic schedulers config from manager.
 	GetSchedulers() ([]*manager.Scheduler, error)
 
+	// Get the dynamic object storage config from manager.
+	GetObjectStorage() (*manager.ObjectStorage, error)
+
+	// Get the dynamic buckets config from manager.
+	GetBuckets() ([]*manager.Bucket, error)
+
 	// Get the dynamic config from manager.
 	Get() (*DynconfigData, error)
 
@@ -104,6 +115,24 @@ func (d *dynconfig) GetSchedulers() ([]*manager.Scheduler, error) {
 	return data.Schedulers, nil
 }
 
+func (d *dynconfig) GetObjectStorage() (*manager.ObjectStorage, error) {
+	data, err := d.Get()
+	if err != nil {
+		return nil, err
+	}
+
+	return data.ObjectStorage, nil
+}
+
+func (d *dynconfig) GetBuckets() ([]*manager.Bucket, error) {
+	data, err := d.Get()
+	if err != nil {
+		return nil, err
+	}
+
+	return data.Buckets, nil
+}
+
 func (d *dynconfig) Get() (*DynconfigData, error) {
 	var data DynconfigData
 	if err := d.Unmarshal(&data); err != nil {
@@ -182,7 +211,7 @@ func newManagerClient(client managerclient.Client, hostOption HostOption) intern
 }
 
 func (mc *managerClient) Get() (interface{}, error) {
-	schedulers, err := mc.ListSchedulers(&manager.ListSchedulersRequest{
+	listSchedulersResp, err := mc.ListSchedulers(&manager.ListSchedulersRequest{
 		SourceType: manager.SourceType_PEER_SOURCE,
 		HostName:   mc.hostOption.Hostname,
 		Ip:         mc.hostOption.AdvertiseIP,
@@ -197,5 +226,34 @@ func (mc *managerClient) Get() (interface{}, error) {
 		return nil, err
 	}
 
-	return schedulers, nil
+	getObjectStorageResp, err := mc.GetObjectStorage(&manager.GetObjectStorageRequest{
+		SourceType: manager.SourceType_PEER_SOURCE,
+		HostName:   mc.hostOption.Hostname,
+		Ip:         mc.hostOption.AdvertiseIP,
+	})
+	if err != nil {
+		if s, ok := status.FromError(err); ok &&
+			(s.Code() == codes.NotFound || s.Code() == codes.Unimplemented) {
+			return DynconfigData{
+				Schedulers: listSchedulersResp.Schedulers,
+			}, nil
+		}
+
+		return nil, err
+	}
+
+	listBucketsResp, err := mc.ListBuckets(&manager.ListBucketsRequest{
+		SourceType: manager.SourceType_PEER_SOURCE,
+		HostName:   mc.hostOption.Hostname,
+		Ip:         mc.hostOption.AdvertiseIP,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return DynconfigData{
+		Schedulers:    listSchedulersResp.Schedulers,
+		ObjectStorage: getObjectStorageResp,
+		Buckets:       listBucketsResp.Buckets,
+	}, nil
 }
diff --git a/client/config/mocks/manager_client_mock.go b/client/config/mocks/manager_client_mock.go
deleted file mode 100644
index 0187ed7e..00000000
--- a/client/config/mocks/manager_client_mock.go
+++ /dev/null
@@ -1,122 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: pkg/rpc/manager/client/client.go
-
-// Package mocks is a generated GoMock package.
-package mocks
-
-import (
-	reflect "reflect"
-	time "time"
-
-	manager "d7y.io/dragonfly/v2/pkg/rpc/manager"
-	gomock "github.com/golang/mock/gomock"
-)
-
-// MockClient is a mock of Client interface.
-type MockClient struct {
-	ctrl     *gomock.Controller
-	recorder *MockClientMockRecorder
-}
-
-// MockClientMockRecorder is the mock recorder for MockClient.
-type MockClientMockRecorder struct {
-	mock *MockClient
-}
-
-// NewMockClient creates a new mock instance.
-func NewMockClient(ctrl *gomock.Controller) *MockClient {
-	mock := &MockClient{ctrl: ctrl}
-	mock.recorder = &MockClientMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockClient) EXPECT() *MockClientMockRecorder {
-	return m.recorder
-}
-
-// Close mocks base method.
-func (m *MockClient) Close() error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Close")
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// Close indicates an expected call of Close.
-func (mr *MockClientMockRecorder) Close() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockClient)(nil).Close))
-}
-
-// GetScheduler mocks base method.
-func (m *MockClient) GetScheduler(arg0 *manager.GetSchedulerRequest) (*manager.Scheduler, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetScheduler", arg0)
-	ret0, _ := ret[0].(*manager.Scheduler)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// GetScheduler indicates an expected call of GetScheduler.
-func (mr *MockClientMockRecorder) GetScheduler(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScheduler", reflect.TypeOf((*MockClient)(nil).GetScheduler), arg0)
-}
-
-// KeepAlive mocks base method.
-func (m *MockClient) KeepAlive(arg0 time.Duration, arg1 *manager.KeepAliveRequest) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "KeepAlive", arg0, arg1)
-}
-
-// KeepAlive indicates an expected call of KeepAlive.
-func (mr *MockClientMockRecorder) KeepAlive(arg0, arg1 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeepAlive", reflect.TypeOf((*MockClient)(nil).KeepAlive), arg0, arg1)
-}
-
-// ListSchedulers mocks base method.
-func (m *MockClient) ListSchedulers(arg0 *manager.ListSchedulersRequest) (*manager.ListSchedulersResponse, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ListSchedulers", arg0)
-	ret0, _ := ret[0].(*manager.ListSchedulersResponse)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// ListSchedulers indicates an expected call of ListSchedulers.
-func (mr *MockClientMockRecorder) ListSchedulers(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSchedulers", reflect.TypeOf((*MockClient)(nil).ListSchedulers), arg0)
-}
-
-// UpdateScheduler mocks base method.
-func (m *MockClient) UpdateScheduler(arg0 *manager.UpdateSchedulerRequest) (*manager.Scheduler, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UpdateScheduler", arg0)
-	ret0, _ := ret[0].(*manager.Scheduler)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// UpdateScheduler indicates an expected call of UpdateScheduler.
-func (mr *MockClientMockRecorder) UpdateScheduler(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateScheduler", reflect.TypeOf((*MockClient)(nil).UpdateScheduler), arg0)
-}
-
-// UpdateSeedPeer mocks base method.
-func (m *MockClient) UpdateSeedPeer(arg0 *manager.UpdateSeedPeerRequest) (*manager.SeedPeer, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "UpdateSeedPeer", arg0)
-	ret0, _ := ret[0].(*manager.SeedPeer)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// UpdateSeedPeer indicates an expected call of UpdateSeedPeer.
-func (mr *MockClientMockRecorder) UpdateSeedPeer(arg0 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSeedPeer", reflect.TypeOf((*MockClient)(nil).UpdateSeedPeer), arg0)
-}
diff --git a/manager/cache/cache.go b/manager/cache/cache.go
index 4b7814f5..82a3772b 100644
--- a/manager/cache/cache.go
+++ b/manager/cache/cache.go
@@ -38,10 +38,13 @@ const (
 
 	// Schedulers prefix of cache key.
 	SchedulersNamespace = "schedulers"
+
+	// Buckets prefix of cache key.
+	BucketsNamespace = "buckets"
 )
 
 const (
-	// PeerCacheTTL is the ttl for peer cache
+	// PeerCacheTTL is the ttl for peer cache.
 	PeerCacheTTL = 30 * time.Minute
 )
 
@@ -74,27 +77,32 @@ func New(cfg *config.Config) (*Cache, error) {
 	}, nil
 }
 
-// Make cache key
+// Make cache key.
 func MakeCacheKey(namespace string, id string) string {
 	return fmt.Sprintf("manager:%s:%s", namespace, id)
 }
 
-// Make cache key for seed peer
+// Make cache key for seed peer.
 func MakeSeedPeerCacheKey(hostname string, clusterID uint) string {
 	return MakeCacheKey(SeedPeerNamespace, fmt.Sprintf("%s-%d", hostname, clusterID))
 }
 
-// Make cache key for peer
+// Make cache key for peer.
 func MakePeerCacheKey(hostname, ip string) string {
 	return MakeCacheKey(PeerNamespace, fmt.Sprintf("%s-%s", hostname, ip))
 }
 
-// Make cache key for scheduler
+// Make cache key for scheduler.
 func MakeSchedulerCacheKey(hostname string, clusterID uint) string {
 	return MakeCacheKey(SchedulerNamespace, fmt.Sprintf("%s-%d", hostname, clusterID))
 }
 
-// Make cache key for schedulers
+// Make cache key for schedulers.
 func MakeSchedulersCacheKey(hostname, ip string) string {
 	return MakeCacheKey(SchedulersNamespace, fmt.Sprintf("%s-%s", hostname, ip))
 }
+
+// Make cache key for buckets.
+func MakeBucketsCacheKey(name string) string {
+	return MakeCacheKey(BucketsNamespace, name)
+}
diff --git a/manager/database/database.go b/manager/database/database.go
index 5450944d..dc26024b 100644
--- a/manager/database/database.go
+++ b/manager/database/database.go
@@ -29,7 +29,7 @@ type Database struct {
 }
 
 func New(cfg *config.Config) (*Database, error) {
-	db, err := newMyqsl(cfg.Database.Mysql)
+	db, err := newMyqsl(cfg)
 	if err != nil {
 		return nil, err
 	}
diff --git a/manager/database/mysql.go b/manager/database/mysql.go
index 346942a1..bbc8eb5c 100644
--- a/manager/database/mysql.go
+++ b/manager/database/mysql.go
@@ -23,18 +23,22 @@ import (
 	"github.com/go-sql-driver/mysql"
 	drivermysql "gorm.io/driver/mysql"
 	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
 	"gorm.io/gorm/schema"
 	"moul.io/zapgorm2"
 
 	logger "d7y.io/dragonfly/v2/internal/dflog"
 	"d7y.io/dragonfly/v2/manager/config"
 	"d7y.io/dragonfly/v2/manager/model"
+	"d7y.io/dragonfly/v2/manager/types"
 	schedulerconfig "d7y.io/dragonfly/v2/scheduler/config"
 )
 
-func newMyqsl(cfg *config.MysqlConfig) (*gorm.DB, error) {
+func newMyqsl(cfg *config.Config) (*gorm.DB, error) {
+	mysql := cfg.Database.Mysql
+
 	// Format dsn string
-	dsn, err := formatDSN(cfg)
+	dsn, err := formatDSN(mysql)
 	if err != nil {
 		return nil, err
 	}
@@ -52,14 +56,14 @@ func newMyqsl(cfg *config.MysqlConfig) (*gorm.DB, error) {
 	}
 
 	// Run migration
-	if cfg.Migrate {
+	if mysql.Migrate {
 		if err := migrate(db); err != nil {
 			return nil, err
 		}
 	}
 
 	// Run seed
-	if err := seed(db); err != nil {
+	if err := seed(cfg, db); err != nil {
 		return nil, err
 	}
 
@@ -111,11 +115,12 @@ func migrate(db *gorm.DB) error {
 	)
 }
 
-func seed(db *gorm.DB) error {
+func seed(cfg *config.Config, db *gorm.DB) error {
 	var schedulerClusterCount int64
 	if err := db.Model(model.SchedulerCluster{}).Count(&schedulerClusterCount).Error; err != nil {
 		return err
 	}
+
 	if schedulerClusterCount <= 0 {
 		if err := db.Create(&model.SchedulerCluster{
 			Model: model.Model{
@@ -140,6 +145,7 @@ func seed(db *gorm.DB) error {
 	if err := db.Model(model.SeedPeerCluster{}).Count(&seedPeerClusterCount).Error; err != nil {
 		return err
 	}
+
 	if seedPeerClusterCount <= 0 {
 		if err := db.Create(&model.SeedPeerCluster{
 			Model: model.Model{
@@ -169,5 +175,17 @@ func seed(db *gorm.DB) error {
 		}
 	}
 
+	if cfg.ObjectStorage.Enable {
+		if err := db.Clauses(clause.OnConflict{
+			Columns:   []clause.Column{{Name: types.ObjectStorageIsEnabledConfigName}},
+			DoUpdates: clause.AssignmentColumns([]string{"name", "value"}),
+		}).Create(&model.Config{
+			Name:  types.ObjectStorageIsEnabledConfigName,
+			Value: "true",
+		}).Error; err != nil {
+			return err
+		}
+	}
+
 	return nil
 }
diff --git a/manager/manager.go b/manager/manager.go
index 22006241..c8678b83 100644
--- a/manager/manager.go
+++ b/manager/manager.go
@@ -91,9 +91,9 @@ func New(cfg *config.Config, d dfpath.Dfpath) (*Server, error) {
 	}
 
 	// Initialize object storage
-	var serviceOptions []service.Option
+	var objectStorage objectstorage.ObjectStorage
 	if cfg.ObjectStorage.Enable {
-		objectStorage, err := objectstorage.New(
+		objectStorage, err = objectstorage.New(
 			cfg.ObjectStorage.Name,
 			cfg.ObjectStorage.Region,
 			cfg.ObjectStorage.Endpoint,
@@ -103,12 +103,10 @@ func New(cfg *config.Config, d dfpath.Dfpath) (*Server, error) {
 		if err != nil {
 			return nil, err
 		}
-
-		serviceOptions = append(serviceOptions, service.WithObjectStorage(objectStorage))
 	}
 
 	// Initialize REST server
-	restService := service.New(db, cache, job, enforcer, serviceOptions...)
+	restService := service.New(db, cache, job, enforcer, objectStorage)
 	router, err := router.Init(cfg, d.LogDir(), restService, enforcer)
 	if err != nil {
 		return nil, err
@@ -125,14 +123,14 @@ func New(cfg *config.Config, d dfpath.Dfpath) (*Server, error) {
 	}
 
 	// Initialize GRPC server
-	var options []grpc.ServerOption
+	var grpcOptions []grpc.ServerOption
 	if s.config.Options.Telemetry.Jaeger != "" {
-		options = []grpc.ServerOption{
+		grpcOptions = []grpc.ServerOption{
 			grpc.ChainUnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
 			grpc.ChainStreamInterceptor(otelgrpc.StreamServerInterceptor()),
 		}
 	}
-	grpcServer := rpcserver.New(db, cache, searcher, options...)
+	grpcServer := rpcserver.New(db, cache, searcher, objectStorage, cfg.ObjectStorage, grpcOptions...)
 	s.grpcServer = grpcServer
 
 	// Initialize prometheus
diff --git a/manager/router/router.go b/manager/router/router.go
index aa15eabd..8d2c9d27 100644
--- a/manager/router/router.go
+++ b/manager/router/router.go
@@ -196,13 +196,11 @@ func Init(cfg *config.Config, logDir string, service service.Service, enforcer *
 	sg.DELETE(":id/security-rules/:security_rule_id", h.DestroySecurityRuleToSecurityGroup)
 
 	// Bucket
-	if cfg.ObjectStorage.Enable {
-		bucket := apiv1.Group("/buckets")
-		bucket.POST("", h.CreateBucket, jwt.MiddlewareFunc(), rbac)
-		bucket.DELETE(":id", h.DestroyBucket, jwt.MiddlewareFunc(), rbac)
-		bucket.GET(":id", h.GetBucket, jwt.MiddlewareFunc(), rbac)
-		bucket.GET("", h.GetBuckets)
-	}
+	bucket := apiv1.Group("/buckets")
+	bucket.POST("", h.CreateBucket, jwt.MiddlewareFunc(), rbac)
+	bucket.DELETE(":id", h.DestroyBucket, jwt.MiddlewareFunc(), rbac)
+	bucket.GET(":id", h.GetBucket, jwt.MiddlewareFunc(), rbac)
+	bucket.GET("", h.GetBuckets)
 
 	// Config
 	config := apiv1.Group("/configs")
diff --git a/manager/rpcserver/rpcserver.go b/manager/rpcserver/rpcserver.go
index 87433b1c..548ed531 100644
--- a/manager/rpcserver/rpcserver.go
+++ b/manager/rpcserver/rpcserver.go
@@ -37,11 +37,13 @@ import (
 
 	logger "d7y.io/dragonfly/v2/internal/dflog"
 	"d7y.io/dragonfly/v2/manager/cache"
+	"d7y.io/dragonfly/v2/manager/config"
 	"d7y.io/dragonfly/v2/manager/database"
 	"d7y.io/dragonfly/v2/manager/metrics"
 	"d7y.io/dragonfly/v2/manager/model"
 	"d7y.io/dragonfly/v2/manager/searcher"
 	"d7y.io/dragonfly/v2/manager/types"
+	"d7y.io/dragonfly/v2/pkg/objectstorage"
 	"d7y.io/dragonfly/v2/pkg/rpc/manager"
 )
 
@@ -77,15 +79,24 @@ type Server struct {
 	searcher searcher.Searcher
 	// Manager grpc interface.
 	manager.UnimplementedManagerServer
+	// Object storage interface.
+	objectStorage objectstorage.ObjectStorage
+	// Object storage configuration.
+	objectStorageConfig *config.ObjectStorageConfig
 }
 
 // New returns a new manager server from the given options.
-func New(database *database.Database, cache *cache.Cache, searcher searcher.Searcher, opts ...grpc.ServerOption) *grpc.Server {
+func New(
+	database *database.Database, cache *cache.Cache, searcher searcher.Searcher,
+	objectStorage objectstorage.ObjectStorage, objectStorageConfig *config.ObjectStorageConfig, opts ...grpc.ServerOption,
+) *grpc.Server {
 	server := &Server{
-		db:       database.DB,
-		rdb:      database.RDB,
-		cache:    cache,
-		searcher: searcher,
+		db:                  database.DB,
+		rdb:                 database.RDB,
+		cache:               cache,
+		searcher:            searcher,
+		objectStorage:       objectStorage,
+		objectStorageConfig: objectStorageConfig,
 	}
 
 	grpcServer := grpc.NewServer(append([]grpc.ServerOption{
@@ -533,6 +544,72 @@ func (s *Server) getPeerCount(ctx context.Context, req *manager.ListSchedulersRe
 	return len(val), nil
 }
 
+// Get object storage configuration.
+func (s *Server) GetObjectStorage(ctx context.Context, req *manager.GetObjectStorageRequest) (*manager.ObjectStorage, error) {
+	log := logger.WithHostnameAndIP(req.HostName, req.Ip)
+
+	if !s.objectStorageConfig.Enable {
+		msg := "object storage is disabled"
+		log.Debug(msg)
+		return nil, status.Error(codes.NotFound, msg)
+	}
+
+	return &manager.ObjectStorage{
+		Name:      s.objectStorageConfig.Name,
+		Region:    s.objectStorageConfig.Region,
+		Endpoint:  s.objectStorageConfig.Endpoint,
+		AccessKey: s.objectStorageConfig.AccessKey,
+		SecretKey: s.objectStorageConfig.SecretKey,
+	}, nil
+}
+
+// List buckets configuration.
+func (s *Server) ListBuckets(ctx context.Context, req *manager.ListBucketsRequest) (*manager.ListBucketsResponse, error) {
+	log := logger.WithHostnameAndIP(req.HostName, req.Ip)
+
+	if !s.objectStorageConfig.Enable {
+		msg := "object storage is disabled"
+		log.Debug(msg)
+		return nil, status.Error(codes.NotFound, msg)
+	}
+
+	var pbListBucketsResponse manager.ListBucketsResponse
+	cacheKey := cache.MakeBucketsCacheKey(s.objectStorageConfig.Name)
+
+	// Cache hit.
+	if err := s.cache.Get(ctx, cacheKey, &pbListBucketsResponse); err == nil {
+		log.Infof("%s cache hit", cacheKey)
+		return &pbListBucketsResponse, nil
+	}
+
+	// Cache miss.
+	log.Infof("%s cache miss", cacheKey)
+	buckets, err := s.objectStorage.ListBucketMetadatas(ctx)
+	if err != nil {
+		log.Errorf("list bucket metadatas failed: %s", err.Error())
+		return nil, status.Error(codes.Unknown, err.Error())
+	}
+
+	// Construct schedulers.
+	for _, bucket := range buckets {
+		pbListBucketsResponse.Buckets = append(pbListBucketsResponse.Buckets, &manager.Bucket{
+			Name: bucket.Name,
+		})
+	}
+
+	// Cache data.
+	if err := s.cache.Once(&cachev8.Item{
+		Ctx:   ctx,
+		Key:   cacheKey,
+		Value: &pbListBucketsResponse,
+		TTL:   s.cache.TTL,
+	}); err != nil {
+		log.Warnf("storage cache failed: %v", err)
+	}
+
+	return &pbListBucketsResponse, nil
+}
+
 // KeepAlive with manager.
 func (s *Server) KeepAlive(stream manager.Manager_KeepAliveServer) error {
 	req, err := stream.Recv()
diff --git a/manager/service/bucket.go b/manager/service/bucket.go
index d4a6834e..2c74149f 100644
--- a/manager/service/bucket.go
+++ b/manager/service/bucket.go
@@ -18,23 +18,42 @@ package service
 
 import (
 	"context"
+	"errors"
 
 	"d7y.io/dragonfly/v2/manager/types"
 	"d7y.io/dragonfly/v2/pkg/objectstorage"
 )
 
+var ErrObjectStorageDisabled = errors.New("object storage is disabled")
+
 func (s *service) CreateBucket(ctx context.Context, json types.CreateBucketRequest) error {
+	if s.objectStorage == nil {
+		return ErrObjectStorageDisabled
+	}
+
 	return s.objectStorage.CreateBucket(ctx, json.Name)
 }
 
 func (s *service) DestroyBucket(ctx context.Context, id string) error {
+	if s.objectStorage == nil {
+		return ErrObjectStorageDisabled
+	}
+
 	return s.objectStorage.DeleteBucket(ctx, id)
 }
 
 func (s *service) GetBucket(ctx context.Context, id string) (*objectstorage.BucketMetadata, error) {
+	if s.objectStorage == nil {
+		return nil, ErrObjectStorageDisabled
+	}
+
 	return s.objectStorage.GetBucketMetadata(ctx, id)
 }
 
 func (s *service) GetBuckets(ctx context.Context) ([]*objectstorage.BucketMetadata, error) {
+	if s.objectStorage == nil {
+		return nil, ErrObjectStorageDisabled
+	}
+
 	return s.objectStorage.ListBucketMetadatas(ctx)
 }
diff --git a/manager/service/service.go b/manager/service/service.go
index 27eea88f..f253dda3 100644
--- a/manager/service/service.go
+++ b/manager/service/service.go
@@ -146,30 +146,14 @@ type service struct {
 	objectStorage objectstorage.ObjectStorage
 }
 
-// HostOption is a functional option for configuring the host.
-type Option func(s *service) *service
-
-// WithObjectStorage sets service's object storage client.
-func WithObjectStorage(objectStorage objectstorage.ObjectStorage) Option {
-	return func(s *service) *service {
-		s.objectStorage = objectStorage
-		return s
-	}
-}
-
 // NewREST returns a new REST instence
-func New(database *database.Database, cache *cache.Cache, job *job.Job, enforcer *casbin.Enforcer, options ...Option) Service {
-	s := &service{
-		db:       database.DB,
-		rdb:      database.RDB,
-		cache:    cache,
-		job:      job,
-		enforcer: enforcer,
+func New(database *database.Database, cache *cache.Cache, job *job.Job, enforcer *casbin.Enforcer, objectStorage objectstorage.ObjectStorage) Service {
+	return &service{
+		db:            database.DB,
+		rdb:           database.RDB,
+		cache:         cache,
+		job:           job,
+		enforcer:      enforcer,
+		objectStorage: objectStorage,
 	}
-
-	for _, opt := range options {
-		opt(s)
-	}
-
-	return s
 }
diff --git a/manager/types/config.go b/manager/types/config.go
index 3568cc77..3c6084fb 100644
--- a/manager/types/config.go
+++ b/manager/types/config.go
@@ -17,8 +17,11 @@
 package types
 
 const (
-	// Server is boot
+	// Server is boot.
 	IsBootConfigName = "is_boot"
+
+	// Object storage is enabled.
+	ObjectStorageIsEnabledConfigName = "object_storage_is_enabled"
 )
 
 type ConfigParams struct {
diff --git a/pkg/objectstorage/mocks/objectstorage_mock.go b/pkg/objectstorage/mocks/objectstorage_mock.go
index a8805b7d..7acbfb2d 100644
--- a/pkg/objectstorage/mocks/objectstorage_mock.go
+++ b/pkg/objectstorage/mocks/objectstorage_mock.go
@@ -5,6 +5,7 @@
 package mocks
 
 import (
+	context "context"
 	io "io"
 	reflect "reflect"
 
@@ -36,132 +37,132 @@ func (m *MockObjectStorage) EXPECT() *MockObjectStorageMockRecorder {
 }
 
 // CreateBucket mocks base method.
-func (m *MockObjectStorage) CreateBucket(bucketName string) error {
+func (m *MockObjectStorage) CreateBucket(ctx context.Context, bucketName string) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateBucket", bucketName)
+	ret := m.ctrl.Call(m, "CreateBucket", ctx, bucketName)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // CreateBucket indicates an expected call of CreateBucket.
-func (mr *MockObjectStorageMockRecorder) CreateBucket(bucketName interface{}) *gomock.Call {
+func (mr *MockObjectStorageMockRecorder) CreateBucket(ctx, bucketName interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*MockObjectStorage)(nil).CreateBucket), bucketName)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*MockObjectStorage)(nil).CreateBucket), ctx, bucketName)
 }
 
 // CreateObject mocks base method.
-func (m *MockObjectStorage) CreateObject(bucketName, objectKey string, reader io.Reader) error {
+func (m *MockObjectStorage) CreateObject(ctx context.Context, bucketName, objectKey string, reader io.Reader) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "CreateObject", bucketName, objectKey, reader)
+	ret := m.ctrl.Call(m, "CreateObject", ctx, bucketName, objectKey, reader)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // CreateObject indicates an expected call of CreateObject.
-func (mr *MockObjectStorageMockRecorder) CreateObject(bucketName, objectKey, reader interface{}) *gomock.Call {
+func (mr *MockObjectStorageMockRecorder) CreateObject(ctx, bucketName, objectKey, reader interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateObject", reflect.TypeOf((*MockObjectStorage)(nil).CreateObject), bucketName, objectKey, reader)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateObject", reflect.TypeOf((*MockObjectStorage)(nil).CreateObject), ctx, bucketName, objectKey, reader)
 }
 
 // DeleteBucket mocks base method.
-func (m *MockObjectStorage) DeleteBucket(bucketName string) error {
+func (m *MockObjectStorage) DeleteBucket(ctx context.Context, bucketName string) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "DeleteBucket", bucketName)
+	ret := m.ctrl.Call(m, "DeleteBucket", ctx, bucketName)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // DeleteBucket indicates an expected call of DeleteBucket.
-func (mr *MockObjectStorageMockRecorder) DeleteBucket(bucketName interface{}) *gomock.Call {
+func (mr *MockObjectStorageMockRecorder) DeleteBucket(ctx, bucketName interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucket", reflect.TypeOf((*MockObjectStorage)(nil).DeleteBucket), bucketName)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucket", reflect.TypeOf((*MockObjectStorage)(nil).DeleteBucket), ctx, bucketName)
 }
 
 // DeleteObject mocks base method.
-func (m *MockObjectStorage) DeleteObject(bucketName, objectKey string) error {
+func (m *MockObjectStorage) DeleteObject(ctx context.Context, bucketName, objectKey string) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "DeleteObject", bucketName, objectKey)
+	ret := m.ctrl.Call(m, "DeleteObject", ctx, bucketName, objectKey)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
 // DeleteObject indicates an expected call of DeleteObject.
-func (mr *MockObjectStorageMockRecorder) DeleteObject(bucketName, objectKey interface{}) *gomock.Call {
+func (mr *MockObjectStorageMockRecorder) DeleteObject(ctx, bucketName, objectKey interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObject", reflect.TypeOf((*MockObjectStorage)(nil).DeleteObject), bucketName, objectKey)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObject", reflect.TypeOf((*MockObjectStorage)(nil).DeleteObject), ctx, bucketName, objectKey)
 }
 
 // GetBucketMetadata mocks base method.
-func (m *MockObjectStorage) GetBucketMetadata(bucketName string) (*objectstorage.BucketMetadata, error) {
+func (m *MockObjectStorage) GetBucketMetadata(ctx context.Context, bucketName string) (*objectstorage.BucketMetadata, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetBucketMetadata", bucketName)
+	ret := m.ctrl.Call(m, "GetBucketMetadata", ctx, bucketName)
 	ret0, _ := ret[0].(*objectstorage.BucketMetadata)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // GetBucketMetadata indicates an expected call of GetBucketMetadata.
-func (mr *MockObjectStorageMockRecorder) GetBucketMetadata(bucketName interface{}) *gomock.Call {
+func (mr *MockObjectStorageMockRecorder) GetBucketMetadata(ctx, bucketName interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketMetadata", reflect.TypeOf((*MockObjectStorage)(nil).GetBucketMetadata), bucketName)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketMetadata", reflect.TypeOf((*MockObjectStorage)(nil).GetBucketMetadata), ctx, bucketName)
 }
 
 // GetObjectMetadata mocks base method.
-func (m *MockObjectStorage) GetObjectMetadata(bucketName, objectKey string) (*objectstorage.ObjectMetadata, error) {
+func (m *MockObjectStorage) GetObjectMetadata(ctx context.Context, bucketName, objectKey string) (*objectstorage.ObjectMetadata, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetObjectMetadata", bucketName, objectKey)
+	ret := m.ctrl.Call(m, "GetObjectMetadata", ctx, bucketName, objectKey)
 	ret0, _ := ret[0].(*objectstorage.ObjectMetadata)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // GetObjectMetadata indicates an expected call of GetObjectMetadata.
-func (mr *MockObjectStorageMockRecorder) GetObjectMetadata(bucketName, objectKey interface{}) *gomock.Call {
+func (mr *MockObjectStorageMockRecorder) GetObjectMetadata(ctx, bucketName, objectKey interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectMetadata", reflect.TypeOf((*MockObjectStorage)(nil).GetObjectMetadata), bucketName, objectKey)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectMetadata", reflect.TypeOf((*MockObjectStorage)(nil).GetObjectMetadata), ctx, bucketName, objectKey)
 }
 
 // GetOject mocks base method.
-func (m *MockObjectStorage) GetOject(bucketName, objectKey string) (io.ReadCloser, error) {
+func (m *MockObjectStorage) GetOject(ctx context.Context, bucketName, objectKey string) (io.ReadCloser, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "GetOject", bucketName, objectKey)
+	ret := m.ctrl.Call(m, "GetOject", ctx, bucketName, objectKey)
 	ret0, _ := ret[0].(io.ReadCloser)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // GetOject indicates an expected call of GetOject.
-func (mr *MockObjectStorageMockRecorder) GetOject(bucketName, objectKey interface{}) *gomock.Call {
+func (mr *MockObjectStorageMockRecorder) GetOject(ctx, bucketName, objectKey interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOject", reflect.TypeOf((*MockObjectStorage)(nil).GetOject), bucketName, objectKey)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOject", reflect.TypeOf((*MockObjectStorage)(nil).GetOject), ctx, bucketName, objectKey)
 }
 
 // ListBucketMetadatas mocks base method.
-func (m *MockObjectStorage) ListBucketMetadatas() ([]*objectstorage.BucketMetadata, error) {
+func (m *MockObjectStorage) ListBucketMetadatas(ctx context.Context) ([]*objectstorage.BucketMetadata, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ListBucketMetadatas")
+	ret := m.ctrl.Call(m, "ListBucketMetadatas", ctx)
 	ret0, _ := ret[0].([]*objectstorage.BucketMetadata)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // ListBucketMetadatas indicates an expected call of ListBucketMetadatas.
-func (mr *MockObjectStorageMockRecorder) ListBucketMetadatas() *gomock.Call {
+func (mr *MockObjectStorageMockRecorder) ListBucketMetadatas(ctx interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketMetadatas", reflect.TypeOf((*MockObjectStorage)(nil).ListBucketMetadatas))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketMetadatas", reflect.TypeOf((*MockObjectStorage)(nil).ListBucketMetadatas), ctx)
 }
 
 // ListObjectMetadatas mocks base method.
-func (m *MockObjectStorage) ListObjectMetadatas(bucketName, prefix, marker string, limit int64) ([]*objectstorage.ObjectMetadata, error) {
+func (m *MockObjectStorage) ListObjectMetadatas(ctx context.Context, bucketName, prefix, marker string, limit int64) ([]*objectstorage.ObjectMetadata, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ListObjectMetadatas", bucketName, prefix, marker, limit)
+	ret := m.ctrl.Call(m, "ListObjectMetadatas", ctx, bucketName, prefix, marker, limit)
 	ret0, _ := ret[0].([]*objectstorage.ObjectMetadata)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // ListObjectMetadatas indicates an expected call of ListObjectMetadatas.
-func (mr *MockObjectStorageMockRecorder) ListObjectMetadatas(bucketName, prefix, marker, limit interface{}) *gomock.Call {
+func (mr *MockObjectStorageMockRecorder) ListObjectMetadatas(ctx, bucketName, prefix, marker, limit interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectMetadatas", reflect.TypeOf((*MockObjectStorage)(nil).ListObjectMetadatas), bucketName, prefix, marker, limit)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectMetadatas", reflect.TypeOf((*MockObjectStorage)(nil).ListObjectMetadatas), ctx, bucketName, prefix, marker, limit)
 }
diff --git a/pkg/rpc/manager/client/client.go b/pkg/rpc/manager/client/client.go
index cbbaf7c0..4c09e01d 100644
--- a/pkg/rpc/manager/client/client.go
+++ b/pkg/rpc/manager/client/client.go
@@ -60,6 +60,12 @@ type Client interface {
 	// List acitve schedulers configuration.
 	ListSchedulers(*manager.ListSchedulersRequest) (*manager.ListSchedulersResponse, error)
 
+	// Get object storage configuration.
+	GetObjectStorage(*manager.GetObjectStorageRequest) (*manager.ObjectStorage, error)
+
+	// List buckets configuration.
+	ListBuckets(*manager.ListBucketsRequest) (*manager.ListBucketsResponse, error)
+
 	// KeepAlive with manager.
 	KeepAlive(time.Duration, *manager.KeepAliveRequest)
 
@@ -148,6 +154,22 @@ func (c *client) ListSchedulers(req *manager.ListSchedulersRequest) (*manager.Li
 	return c.ManagerClient.ListSchedulers(ctx, req)
 }
 
+// Get object storage configuration.
+func (c *client) GetObjectStorage(req *manager.GetObjectStorageRequest) (*manager.ObjectStorage, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
+	defer cancel()
+
+	return c.ManagerClient.GetObjectStorage(ctx, req)
+}
+
+// List buckets configuration.
+func (c *client) ListBuckets(req *manager.ListBucketsRequest) (*manager.ListBucketsResponse, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
+	defer cancel()
+
+	return c.ManagerClient.ListBuckets(ctx, req)
+}
+
 // List acitve schedulers configuration.
 func (c *client) KeepAlive(interval time.Duration, keepalive *manager.KeepAliveRequest) {
 retry:
diff --git a/pkg/rpc/manager/client/mocks/client_mock.go b/pkg/rpc/manager/client/mocks/client_mock.go
index 4f4d0c7d..8344932e 100644
--- a/pkg/rpc/manager/client/mocks/client_mock.go
+++ b/pkg/rpc/manager/client/mocks/client_mock.go
@@ -49,6 +49,21 @@ func (mr *MockClientMockRecorder) Close() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockClient)(nil).Close))
 }
 
+// GetObjectStorage mocks base method.
+func (m *MockClient) GetObjectStorage(arg0 *manager.GetObjectStorageRequest) (*manager.ObjectStorage, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetObjectStorage", arg0)
+	ret0, _ := ret[0].(*manager.ObjectStorage)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetObjectStorage indicates an expected call of GetObjectStorage.
+func (mr *MockClientMockRecorder) GetObjectStorage(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectStorage", reflect.TypeOf((*MockClient)(nil).GetObjectStorage), arg0)
+}
+
 // GetScheduler mocks base method.
 func (m *MockClient) GetScheduler(arg0 *manager.GetSchedulerRequest) (*manager.Scheduler, error) {
 	m.ctrl.T.Helper()
@@ -76,6 +91,21 @@ func (mr *MockClientMockRecorder) KeepAlive(arg0, arg1 interface{}) *gomock.Call
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeepAlive", reflect.TypeOf((*MockClient)(nil).KeepAlive), arg0, arg1)
 }
 
+// ListBuckets mocks base method.
+func (m *MockClient) ListBuckets(arg0 *manager.ListBucketsRequest) (*manager.ListBucketsResponse, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ListBuckets", arg0)
+	ret0, _ := ret[0].(*manager.ListBucketsResponse)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// ListBuckets indicates an expected call of ListBuckets.
+func (mr *MockClientMockRecorder) ListBuckets(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuckets", reflect.TypeOf((*MockClient)(nil).ListBuckets), arg0)
+}
+
 // ListSchedulers mocks base method.
 func (m *MockClient) ListSchedulers(arg0 *manager.ListSchedulersRequest) (*manager.ListSchedulersResponse, error) {
 	m.ctrl.T.Helper()
diff --git a/pkg/rpc/manager/manager.pb.go b/pkg/rpc/manager/manager.pb.go
index da1743c4..1a533037 100644
--- a/pkg/rpc/manager/manager.pb.go
+++ b/pkg/rpc/manager/manager.pb.go
@@ -1227,6 +1227,323 @@ func (x *ListSchedulersResponse) GetSchedulers() []*Scheduler {
 	return nil
 }
 
+// ObjectStorage represents config of object storage.
+type ObjectStorage struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Object storage name of type.
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	// Storage region.
+	Region string `protobuf:"bytes,2,opt,name=region,proto3" json:"region,omitempty"`
+	// Datacenter endpoint.
+	Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
+	// Access key id.
+	AccessKey string `protobuf:"bytes,4,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"`
+	// Access key secret.
+	SecretKey string `protobuf:"bytes,5,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"`
+}
+
+func (x *ObjectStorage) Reset() {
+	*x = ObjectStorage{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_rpc_manager_manager_proto_msgTypes[11]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ObjectStorage) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ObjectStorage) ProtoMessage() {}
+
+func (x *ObjectStorage) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_rpc_manager_manager_proto_msgTypes[11]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ObjectStorage.ProtoReflect.Descriptor instead.
+func (*ObjectStorage) Descriptor() ([]byte, []int) {
+	return file_pkg_rpc_manager_manager_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *ObjectStorage) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *ObjectStorage) GetRegion() string {
+	if x != nil {
+		return x.Region
+	}
+	return ""
+}
+
+func (x *ObjectStorage) GetEndpoint() string {
+	if x != nil {
+		return x.Endpoint
+	}
+	return ""
+}
+
+func (x *ObjectStorage) GetAccessKey() string {
+	if x != nil {
+		return x.AccessKey
+	}
+	return ""
+}
+
+func (x *ObjectStorage) GetSecretKey() string {
+	if x != nil {
+		return x.SecretKey
+	}
+	return ""
+}
+
+// GetObjectStorageRequest represents request of GetObjectStorage.
+type GetObjectStorageRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Request source type.
+	SourceType SourceType `protobuf:"varint,1,opt,name=source_type,json=sourceType,proto3,enum=manager.SourceType" json:"source_type,omitempty"`
+	// Source service hostname.
+	HostName string `protobuf:"bytes,2,opt,name=host_name,json=hostName,proto3" json:"host_name,omitempty"`
+	// Source service ip.
+	Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"`
+}
+
+func (x *GetObjectStorageRequest) Reset() {
+	*x = GetObjectStorageRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_rpc_manager_manager_proto_msgTypes[12]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetObjectStorageRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetObjectStorageRequest) ProtoMessage() {}
+
+func (x *GetObjectStorageRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_rpc_manager_manager_proto_msgTypes[12]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetObjectStorageRequest.ProtoReflect.Descriptor instead.
+func (*GetObjectStorageRequest) Descriptor() ([]byte, []int) {
+	return file_pkg_rpc_manager_manager_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *GetObjectStorageRequest) GetSourceType() SourceType {
+	if x != nil {
+		return x.SourceType
+	}
+	return SourceType_SCHEDULER_SOURCE
+}
+
+func (x *GetObjectStorageRequest) GetHostName() string {
+	if x != nil {
+		return x.HostName
+	}
+	return ""
+}
+
+func (x *GetObjectStorageRequest) GetIp() string {
+	if x != nil {
+		return x.Ip
+	}
+	return ""
+}
+
+// Bucket represents config of bucket.
+type Bucket struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Bucket name.
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *Bucket) Reset() {
+	*x = Bucket{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_rpc_manager_manager_proto_msgTypes[13]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Bucket) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Bucket) ProtoMessage() {}
+
+func (x *Bucket) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_rpc_manager_manager_proto_msgTypes[13]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Bucket.ProtoReflect.Descriptor instead.
+func (*Bucket) Descriptor() ([]byte, []int) {
+	return file_pkg_rpc_manager_manager_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *Bucket) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+// ListSchedulersRequest represents request of ListBuckets.
+type ListBucketsRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Request source type.
+	SourceType SourceType `protobuf:"varint,1,opt,name=source_type,json=sourceType,proto3,enum=manager.SourceType" json:"source_type,omitempty"`
+	// Source service hostname.
+	HostName string `protobuf:"bytes,2,opt,name=host_name,json=hostName,proto3" json:"host_name,omitempty"`
+	// Source service ip.
+	Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"`
+}
+
+func (x *ListBucketsRequest) Reset() {
+	*x = ListBucketsRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_rpc_manager_manager_proto_msgTypes[14]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListBucketsRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListBucketsRequest) ProtoMessage() {}
+
+func (x *ListBucketsRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_rpc_manager_manager_proto_msgTypes[14]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListBucketsRequest.ProtoReflect.Descriptor instead.
+func (*ListBucketsRequest) Descriptor() ([]byte, []int) {
+	return file_pkg_rpc_manager_manager_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *ListBucketsRequest) GetSourceType() SourceType {
+	if x != nil {
+		return x.SourceType
+	}
+	return SourceType_SCHEDULER_SOURCE
+}
+
+func (x *ListBucketsRequest) GetHostName() string {
+	if x != nil {
+		return x.HostName
+	}
+	return ""
+}
+
+func (x *ListBucketsRequest) GetIp() string {
+	if x != nil {
+		return x.Ip
+	}
+	return ""
+}
+
+// ListBucketsResponse represents response of ListBuckets.
+type ListBucketsResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Bucket configs.
+	Buckets []*Bucket `protobuf:"bytes,1,rep,name=buckets,proto3" json:"buckets,omitempty"`
+}
+
+func (x *ListBucketsResponse) Reset() {
+	*x = ListBucketsResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_pkg_rpc_manager_manager_proto_msgTypes[15]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ListBucketsResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListBucketsResponse) ProtoMessage() {}
+
+func (x *ListBucketsResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_pkg_rpc_manager_manager_proto_msgTypes[15]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListBucketsResponse.ProtoReflect.Descriptor instead.
+func (*ListBucketsResponse) Descriptor() ([]byte, []int) {
+	return file_pkg_rpc_manager_manager_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *ListBucketsResponse) GetBuckets() []*Bucket {
+	if x != nil {
+		return x.Buckets
+	}
+	return nil
+}
+
 // KeepAliveRequest represents request of KeepAlive.
 type KeepAliveRequest struct {
 	state         protoimpl.MessageState
@@ -1244,7 +1561,7 @@ type KeepAliveRequest struct {
 func (x *KeepAliveRequest) Reset() {
 	*x = KeepAliveRequest{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_pkg_rpc_manager_manager_proto_msgTypes[11]
+		mi := &file_pkg_rpc_manager_manager_proto_msgTypes[16]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -1257,7 +1574,7 @@ func (x *KeepAliveRequest) String() string {
 func (*KeepAliveRequest) ProtoMessage() {}
 
 func (x *KeepAliveRequest) ProtoReflect() protoreflect.Message {
-	mi := &file_pkg_rpc_manager_manager_proto_msgTypes[11]
+	mi := &file_pkg_rpc_manager_manager_proto_msgTypes[16]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -1270,7 +1587,7 @@ func (x *KeepAliveRequest) ProtoReflect() protoreflect.Message {
 
 // Deprecated: Use KeepAliveRequest.ProtoReflect.Descriptor instead.
 func (*KeepAliveRequest) Descriptor() ([]byte, []int) {
-	return file_pkg_rpc_manager_manager_proto_rawDescGZIP(), []int{11}
+	return file_pkg_rpc_manager_manager_proto_rawDescGZIP(), []int{16}
 }
 
 func (x *KeepAliveRequest) GetSourceType() SourceType {
@@ -1496,51 +1813,101 @@ var file_pkg_rpc_manager_manager_proto_rawDesc = []byte{
 	0x12, 0x32, 0x0a, 0x0a, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x73, 0x18, 0x01,
 	0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53,
 	0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x52, 0x0a, 0x73, 0x63, 0x68, 0x65, 0x64, 0x75,
-	0x6c, 0x65, 0x72, 0x73, 0x22, 0xa0, 0x01, 0x0a, 0x10, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69,
-	0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x0b, 0x73, 0x6f, 0x75,
-	0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13,
-	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54,
-	0x79, 0x70, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x73,
-	0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x09, 0x68, 0x6f, 0x73,
-	0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42,
-	0x04, 0x72, 0x02, 0x68, 0x01, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12,
-	0x26, 0x0a, 0x0a, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20,
-	0x01, 0x28, 0x04, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x32, 0x02, 0x28, 0x01, 0x52, 0x09, 0x63, 0x6c,
-	0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x64, 0x2a, 0x49, 0x0a, 0x0a, 0x53, 0x6f, 0x75, 0x72, 0x63,
-	0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c,
-	0x45, 0x52, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x50,
-	0x45, 0x45, 0x52, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10,
-	0x53, 0x45, 0x45, 0x44, 0x5f, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45,
-	0x10, 0x02, 0x32, 0xac, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x3d,
-	0x0a, 0x0b, 0x47, 0x65, 0x74, 0x53, 0x65, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1b, 0x2e,
-	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x65, 0x64, 0x50,
-	0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e,
-	0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, 0x43, 0x0a,
-	0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12,
-	0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
-	0x53, 0x65, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
-	0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x65, 0x64, 0x50, 0x65,
-	0x65, 0x72, 0x12, 0x40, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c,
-	0x65, 0x72, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74,
-	0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
-	0x1a, 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64,
-	0x75, 0x6c, 0x65, 0x72, 0x12, 0x46, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63,
-	0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
-	0x72, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65,
-	0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
-	0x65, 0x72, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x12, 0x51, 0x0a, 0x0e,
-	0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x73, 0x12, 0x1e,
-	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x68,
-	0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f,
-	0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x68,
-	0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
-	0x40, 0x0a, 0x09, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x12, 0x19, 0x2e, 0x6d,
-	0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65,
-	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
-	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28,
-	0x01, 0x42, 0x25, 0x5a, 0x23, 0x64, 0x37, 0x79, 0x2e, 0x69, 0x6f, 0x2f, 0x64, 0x72, 0x61, 0x67,
-	0x6f, 0x6e, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x72, 0x70, 0x63,
-	0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x6c, 0x65, 0x72, 0x73, 0x22, 0xdd, 0x01, 0x0a, 0x0d, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53,
+	0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01, 0x18, 0x80, 0x08,
+	0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0d, 0xfa, 0x42, 0x0a, 0x72, 0x08, 0x10, 0x01, 0x18,
+	0x80, 0x08, 0xd0, 0x01, 0x01, 0x52, 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a,
+	0x08, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42,
+	0x0d, 0xfa, 0x42, 0x0a, 0x72, 0x08, 0x10, 0x01, 0x18, 0x80, 0x08, 0xd0, 0x01, 0x01, 0x52, 0x08,
+	0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x65,
+	0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0d, 0xfa, 0x42,
+	0x0a, 0x72, 0x08, 0x10, 0x01, 0x18, 0x80, 0x08, 0xd0, 0x01, 0x01, 0x52, 0x09, 0x61, 0x63, 0x63,
+	0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
+	0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0d, 0xfa, 0x42, 0x0a, 0x72,
+	0x08, 0x10, 0x01, 0x18, 0x80, 0x08, 0xd0, 0x01, 0x01, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65,
+	0x74, 0x4b, 0x65, 0x79, 0x22, 0x98, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4f, 0x62, 0x6a, 0x65,
+	0x63, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x12, 0x3e, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e,
+	0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x82,
+	0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65,
+	0x12, 0x24, 0x0a, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x68, 0x01, 0x52, 0x08, 0x68, 0x6f,
+	0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x70, 0x01, 0x52, 0x02, 0x69, 0x70, 0x22,
+	0x28, 0x0a, 0x06, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1e, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
+	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x72, 0x05, 0x10, 0x01,
+	0x18, 0x80, 0x08, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x93, 0x01, 0x0a, 0x12, 0x4c, 0x69,
+	0x73, 0x74, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x12, 0x3e, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e,
+	0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x82,
+	0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65,
+	0x12, 0x24, 0x0a, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x68, 0x01, 0x52, 0x08, 0x68, 0x6f,
+	0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02, 0x70, 0x01, 0x52, 0x02, 0x69, 0x70, 0x22,
+	0x40, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x07, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74,
+	0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
+	0x72, 0x2e, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x07, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74,
+	0x73, 0x22, 0xa0, 0x01, 0x0a, 0x10, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76, 0x65, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3e, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+	0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x6d, 0x61,
+	0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65,
+	0x42, 0x08, 0xfa, 0x42, 0x05, 0x82, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72,
+	0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x6e,
+	0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x72, 0x02,
+	0x68, 0x01, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0a,
+	0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04,
+	0x42, 0x07, 0xfa, 0x42, 0x04, 0x32, 0x02, 0x28, 0x01, 0x52, 0x09, 0x63, 0x6c, 0x75, 0x73, 0x74,
+	0x65, 0x72, 0x49, 0x64, 0x2a, 0x49, 0x0a, 0x0a, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79,
+	0x70, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x43, 0x48, 0x45, 0x44, 0x55, 0x4c, 0x45, 0x52, 0x5f,
+	0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x45, 0x45, 0x52,
+	0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x45, 0x45,
+	0x44, 0x5f, 0x50, 0x45, 0x45, 0x52, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x10, 0x02, 0x32,
+	0xc4, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x0b, 0x47,
+	0x65, 0x74, 0x53, 0x65, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1b, 0x2e, 0x6d, 0x61, 0x6e,
+	0x61, 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x65, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
+	0x72, 0x2e, 0x53, 0x65, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, 0x43, 0x0a, 0x0e, 0x55, 0x70,
+	0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12, 0x1e, 0x2e, 0x6d,
+	0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x65, 0x65,
+	0x64, 0x50, 0x65, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x6d,
+	0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x12,
+	0x40, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x12,
+	0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68,
+	0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e,
+	0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65,
+	0x72, 0x12, 0x46, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x64,
+	0x75, 0x6c, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x55,
+	0x70, 0x64, 0x61, 0x74, 0x65, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e,
+	0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x12, 0x51, 0x0a, 0x0e, 0x4c, 0x69, 0x73,
+	0x74, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x72, 0x73, 0x12, 0x1e, 0x2e, 0x6d, 0x61,
+	0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75,
+	0x6c, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6d, 0x61,
+	0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75,
+	0x6c, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x10,
+	0x47, 0x65, 0x74, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+	0x12, 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x62,
+	0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x1a, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x4f, 0x62, 0x6a,
+	0x65, 0x63, 0x74, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x48, 0x0a, 0x0b, 0x4c, 0x69,
+	0x73, 0x74, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
+	0x67, 0x65, 0x72, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x52,
+	0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72,
+	0x2e, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x4b, 0x65, 0x65, 0x70, 0x41, 0x6c, 0x69, 0x76,
+	0x65, 0x12, 0x19, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x4b, 0x65, 0x65, 0x70,
+	0x41, 0x6c, 0x69, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45,
+	0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x42, 0x25, 0x5a, 0x23, 0x64, 0x37, 0x79, 0x2e, 0x69, 0x6f,
+	0x2f, 0x64, 0x72, 0x61, 0x67, 0x6f, 0x6e, 0x66, 0x6c, 0x79, 0x2f, 0x76, 0x32, 0x2f, 0x70, 0x6b,
+	0x67, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x62, 0x06, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -1556,23 +1923,28 @@ func file_pkg_rpc_manager_manager_proto_rawDescGZIP() []byte {
 }
 
 var file_pkg_rpc_manager_manager_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_pkg_rpc_manager_manager_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
+var file_pkg_rpc_manager_manager_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
 var file_pkg_rpc_manager_manager_proto_goTypes = []interface{}{
-	(SourceType)(0),                // 0: manager.SourceType
-	(*SecurityGroup)(nil),          // 1: manager.SecurityGroup
-	(*SeedPeerCluster)(nil),        // 2: manager.SeedPeerCluster
-	(*SeedPeer)(nil),               // 3: manager.SeedPeer
-	(*GetSeedPeerRequest)(nil),     // 4: manager.GetSeedPeerRequest
-	(*UpdateSeedPeerRequest)(nil),  // 5: manager.UpdateSeedPeerRequest
-	(*SchedulerCluster)(nil),       // 6: manager.SchedulerCluster
-	(*Scheduler)(nil),              // 7: manager.Scheduler
-	(*GetSchedulerRequest)(nil),    // 8: manager.GetSchedulerRequest
-	(*UpdateSchedulerRequest)(nil), // 9: manager.UpdateSchedulerRequest
-	(*ListSchedulersRequest)(nil),  // 10: manager.ListSchedulersRequest
-	(*ListSchedulersResponse)(nil), // 11: manager.ListSchedulersResponse
-	(*KeepAliveRequest)(nil),       // 12: manager.KeepAliveRequest
-	nil,                            // 13: manager.ListSchedulersRequest.HostInfoEntry
-	(*emptypb.Empty)(nil),          // 14: google.protobuf.Empty
+	(SourceType)(0),                 // 0: manager.SourceType
+	(*SecurityGroup)(nil),           // 1: manager.SecurityGroup
+	(*SeedPeerCluster)(nil),         // 2: manager.SeedPeerCluster
+	(*SeedPeer)(nil),                // 3: manager.SeedPeer
+	(*GetSeedPeerRequest)(nil),      // 4: manager.GetSeedPeerRequest
+	(*UpdateSeedPeerRequest)(nil),   // 5: manager.UpdateSeedPeerRequest
+	(*SchedulerCluster)(nil),        // 6: manager.SchedulerCluster
+	(*Scheduler)(nil),               // 7: manager.Scheduler
+	(*GetSchedulerRequest)(nil),     // 8: manager.GetSchedulerRequest
+	(*UpdateSchedulerRequest)(nil),  // 9: manager.UpdateSchedulerRequest
+	(*ListSchedulersRequest)(nil),   // 10: manager.ListSchedulersRequest
+	(*ListSchedulersResponse)(nil),  // 11: manager.ListSchedulersResponse
+	(*ObjectStorage)(nil),           // 12: manager.ObjectStorage
+	(*GetObjectStorageRequest)(nil), // 13: manager.GetObjectStorageRequest
+	(*Bucket)(nil),                  // 14: manager.Bucket
+	(*ListBucketsRequest)(nil),      // 15: manager.ListBucketsRequest
+	(*ListBucketsResponse)(nil),     // 16: manager.ListBucketsResponse
+	(*KeepAliveRequest)(nil),        // 17: manager.KeepAliveRequest
+	nil,                             // 18: manager.ListSchedulersRequest.HostInfoEntry
+	(*emptypb.Empty)(nil),           // 19: google.protobuf.Empty
 }
 var file_pkg_rpc_manager_manager_proto_depIdxs = []int32{
 	1,  // 0: manager.SeedPeerCluster.security_group:type_name -> manager.SecurityGroup
@@ -1586,26 +1958,33 @@ var file_pkg_rpc_manager_manager_proto_depIdxs = []int32{
 	0,  // 8: manager.GetSchedulerRequest.source_type:type_name -> manager.SourceType
 	0,  // 9: manager.UpdateSchedulerRequest.source_type:type_name -> manager.SourceType
 	0,  // 10: manager.ListSchedulersRequest.source_type:type_name -> manager.SourceType
-	13, // 11: manager.ListSchedulersRequest.host_info:type_name -> manager.ListSchedulersRequest.HostInfoEntry
+	18, // 11: manager.ListSchedulersRequest.host_info:type_name -> manager.ListSchedulersRequest.HostInfoEntry
 	7,  // 12: manager.ListSchedulersResponse.schedulers:type_name -> manager.Scheduler
-	0,  // 13: manager.KeepAliveRequest.source_type:type_name -> manager.SourceType
-	4,  // 14: manager.Manager.GetSeedPeer:input_type -> manager.GetSeedPeerRequest
-	5,  // 15: manager.Manager.UpdateSeedPeer:input_type -> manager.UpdateSeedPeerRequest
-	8,  // 16: manager.Manager.GetScheduler:input_type -> manager.GetSchedulerRequest
-	9,  // 17: manager.Manager.UpdateScheduler:input_type -> manager.UpdateSchedulerRequest
-	10, // 18: manager.Manager.ListSchedulers:input_type -> manager.ListSchedulersRequest
-	12, // 19: manager.Manager.KeepAlive:input_type -> manager.KeepAliveRequest
-	3,  // 20: manager.Manager.GetSeedPeer:output_type -> manager.SeedPeer
-	3,  // 21: manager.Manager.UpdateSeedPeer:output_type -> manager.SeedPeer
-	7,  // 22: manager.Manager.GetScheduler:output_type -> manager.Scheduler
-	7,  // 23: manager.Manager.UpdateScheduler:output_type -> manager.Scheduler
-	11, // 24: manager.Manager.ListSchedulers:output_type -> manager.ListSchedulersResponse
-	14, // 25: manager.Manager.KeepAlive:output_type -> google.protobuf.Empty
-	20, // [20:26] is the sub-list for method output_type
-	14, // [14:20] is the sub-list for method input_type
-	14, // [14:14] is the sub-list for extension type_name
-	14, // [14:14] is the sub-list for extension extendee
-	0,  // [0:14] is the sub-list for field type_name
+	0,  // 13: manager.GetObjectStorageRequest.source_type:type_name -> manager.SourceType
+	0,  // 14: manager.ListBucketsRequest.source_type:type_name -> manager.SourceType
+	14, // 15: manager.ListBucketsResponse.buckets:type_name -> manager.Bucket
+	0,  // 16: manager.KeepAliveRequest.source_type:type_name -> manager.SourceType
+	4,  // 17: manager.Manager.GetSeedPeer:input_type -> manager.GetSeedPeerRequest
+	5,  // 18: manager.Manager.UpdateSeedPeer:input_type -> manager.UpdateSeedPeerRequest
+	8,  // 19: manager.Manager.GetScheduler:input_type -> manager.GetSchedulerRequest
+	9,  // 20: manager.Manager.UpdateScheduler:input_type -> manager.UpdateSchedulerRequest
+	10, // 21: manager.Manager.ListSchedulers:input_type -> manager.ListSchedulersRequest
+	13, // 22: manager.Manager.GetObjectStorage:input_type -> manager.GetObjectStorageRequest
+	15, // 23: manager.Manager.ListBuckets:input_type -> manager.ListBucketsRequest
+	17, // 24: manager.Manager.KeepAlive:input_type -> manager.KeepAliveRequest
+	3,  // 25: manager.Manager.GetSeedPeer:output_type -> manager.SeedPeer
+	3,  // 26: manager.Manager.UpdateSeedPeer:output_type -> manager.SeedPeer
+	7,  // 27: manager.Manager.GetScheduler:output_type -> manager.Scheduler
+	7,  // 28: manager.Manager.UpdateScheduler:output_type -> manager.Scheduler
+	11, // 29: manager.Manager.ListSchedulers:output_type -> manager.ListSchedulersResponse
+	12, // 30: manager.Manager.GetObjectStorage:output_type -> manager.ObjectStorage
+	16, // 31: manager.Manager.ListBuckets:output_type -> manager.ListBucketsResponse
+	19, // 32: manager.Manager.KeepAlive:output_type -> google.protobuf.Empty
+	25, // [25:33] is the sub-list for method output_type
+	17, // [17:25] is the sub-list for method input_type
+	17, // [17:17] is the sub-list for extension type_name
+	17, // [17:17] is the sub-list for extension extendee
+	0,  // [0:17] is the sub-list for field type_name
 }
 
 func init() { file_pkg_rpc_manager_manager_proto_init() }
@@ -1747,6 +2126,66 @@ func file_pkg_rpc_manager_manager_proto_init() {
 			}
 		}
 		file_pkg_rpc_manager_manager_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ObjectStorage); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_pkg_rpc_manager_manager_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetObjectStorageRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_pkg_rpc_manager_manager_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Bucket); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_pkg_rpc_manager_manager_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListBucketsRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_pkg_rpc_manager_manager_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ListBucketsResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_pkg_rpc_manager_manager_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*KeepAliveRequest); i {
 			case 0:
 				return &v.state
@@ -1765,7 +2204,7 @@ func file_pkg_rpc_manager_manager_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_pkg_rpc_manager_manager_proto_rawDesc,
 			NumEnums:      1,
-			NumMessages:   13,
+			NumMessages:   18,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
@@ -1802,6 +2241,10 @@ type ManagerClient interface {
 	UpdateScheduler(ctx context.Context, in *UpdateSchedulerRequest, opts ...grpc.CallOption) (*Scheduler, error)
 	// List acitve schedulers configuration.
 	ListSchedulers(ctx context.Context, in *ListSchedulersRequest, opts ...grpc.CallOption) (*ListSchedulersResponse, error)
+	// Get ObjectStorage configuration.
+	GetObjectStorage(ctx context.Context, in *GetObjectStorageRequest, opts ...grpc.CallOption) (*ObjectStorage, error)
+	// List buckets configuration.
+	ListBuckets(ctx context.Context, in *ListBucketsRequest, opts ...grpc.CallOption) (*ListBucketsResponse, error)
 	// KeepAlive with manager.
 	KeepAlive(ctx context.Context, opts ...grpc.CallOption) (Manager_KeepAliveClient, error)
 }
@@ -1859,6 +2302,24 @@ func (c *managerClient) ListSchedulers(ctx context.Context, in *ListSchedulersRe
 	return out, nil
 }
 
+func (c *managerClient) GetObjectStorage(ctx context.Context, in *GetObjectStorageRequest, opts ...grpc.CallOption) (*ObjectStorage, error) {
+	out := new(ObjectStorage)
+	err := c.cc.Invoke(ctx, "/manager.Manager/GetObjectStorage", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *managerClient) ListBuckets(ctx context.Context, in *ListBucketsRequest, opts ...grpc.CallOption) (*ListBucketsResponse, error) {
+	out := new(ListBucketsResponse)
+	err := c.cc.Invoke(ctx, "/manager.Manager/ListBuckets", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 func (c *managerClient) KeepAlive(ctx context.Context, opts ...grpc.CallOption) (Manager_KeepAliveClient, error) {
 	stream, err := c.cc.NewStream(ctx, &_Manager_serviceDesc.Streams[0], "/manager.Manager/KeepAlive", opts...)
 	if err != nil {
@@ -1905,6 +2366,10 @@ type ManagerServer interface {
 	UpdateScheduler(context.Context, *UpdateSchedulerRequest) (*Scheduler, error)
 	// List acitve schedulers configuration.
 	ListSchedulers(context.Context, *ListSchedulersRequest) (*ListSchedulersResponse, error)
+	// Get ObjectStorage configuration.
+	GetObjectStorage(context.Context, *GetObjectStorageRequest) (*ObjectStorage, error)
+	// List buckets configuration.
+	ListBuckets(context.Context, *ListBucketsRequest) (*ListBucketsResponse, error)
 	// KeepAlive with manager.
 	KeepAlive(Manager_KeepAliveServer) error
 }
@@ -1928,6 +2393,12 @@ func (*UnimplementedManagerServer) UpdateScheduler(context.Context, *UpdateSched
 func (*UnimplementedManagerServer) ListSchedulers(context.Context, *ListSchedulersRequest) (*ListSchedulersResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method ListSchedulers not implemented")
 }
+func (*UnimplementedManagerServer) GetObjectStorage(context.Context, *GetObjectStorageRequest) (*ObjectStorage, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetObjectStorage not implemented")
+}
+func (*UnimplementedManagerServer) ListBuckets(context.Context, *ListBucketsRequest) (*ListBucketsResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method ListBuckets not implemented")
+}
 func (*UnimplementedManagerServer) KeepAlive(Manager_KeepAliveServer) error {
 	return status.Errorf(codes.Unimplemented, "method KeepAlive not implemented")
 }
@@ -2026,6 +2497,42 @@ func _Manager_ListSchedulers_Handler(srv interface{}, ctx context.Context, dec f
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Manager_GetObjectStorage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetObjectStorageRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ManagerServer).GetObjectStorage(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/manager.Manager/GetObjectStorage",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ManagerServer).GetObjectStorage(ctx, req.(*GetObjectStorageRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Manager_ListBuckets_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(ListBucketsRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ManagerServer).ListBuckets(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/manager.Manager/ListBuckets",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ManagerServer).ListBuckets(ctx, req.(*ListBucketsRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 func _Manager_KeepAlive_Handler(srv interface{}, stream grpc.ServerStream) error {
 	return srv.(ManagerServer).KeepAlive(&managerKeepAliveServer{stream})
 }
@@ -2076,6 +2583,14 @@ var _Manager_serviceDesc = grpc.ServiceDesc{
 			MethodName: "ListSchedulers",
 			Handler:    _Manager_ListSchedulers_Handler,
 		},
+		{
+			MethodName: "GetObjectStorage",
+			Handler:    _Manager_GetObjectStorage_Handler,
+		},
+		{
+			MethodName: "ListBuckets",
+			Handler:    _Manager_ListBuckets_Handler,
+		},
 	},
 	Streams: []grpc.StreamDesc{
 		{
diff --git a/pkg/rpc/manager/manager.pb.validate.go b/pkg/rpc/manager/manager.pb.validate.go
index 5968b672..5814e214 100644
--- a/pkg/rpc/manager/manager.pb.validate.go
+++ b/pkg/rpc/manager/manager.pb.validate.go
@@ -1323,6 +1323,513 @@ var _ interface {
 	ErrorName() string
 } = ListSchedulersResponseValidationError{}
 
+// Validate checks the field values on ObjectStorage with the rules defined in
+// the proto definition for this message. If any rules are violated, an error
+// is returned.
+func (m *ObjectStorage) Validate() error {
+	if m == nil {
+		return nil
+	}
+
+	if l := utf8.RuneCountInString(m.GetName()); l < 1 || l > 1024 {
+		return ObjectStorageValidationError{
+			field:  "Name",
+			reason: "value length must be between 1 and 1024 runes, inclusive",
+		}
+	}
+
+	if m.GetRegion() != "" {
+
+		if l := utf8.RuneCountInString(m.GetRegion()); l < 1 || l > 1024 {
+			return ObjectStorageValidationError{
+				field:  "Region",
+				reason: "value length must be between 1 and 1024 runes, inclusive",
+			}
+		}
+
+	}
+
+	if m.GetEndpoint() != "" {
+
+		if l := utf8.RuneCountInString(m.GetEndpoint()); l < 1 || l > 1024 {
+			return ObjectStorageValidationError{
+				field:  "Endpoint",
+				reason: "value length must be between 1 and 1024 runes, inclusive",
+			}
+		}
+
+	}
+
+	if m.GetAccessKey() != "" {
+
+		if l := utf8.RuneCountInString(m.GetAccessKey()); l < 1 || l > 1024 {
+			return ObjectStorageValidationError{
+				field:  "AccessKey",
+				reason: "value length must be between 1 and 1024 runes, inclusive",
+			}
+		}
+
+	}
+
+	if m.GetSecretKey() != "" {
+
+		if l := utf8.RuneCountInString(m.GetSecretKey()); l < 1 || l > 1024 {
+			return ObjectStorageValidationError{
+				field:  "SecretKey",
+				reason: "value length must be between 1 and 1024 runes, inclusive",
+			}
+		}
+
+	}
+
+	return nil
+}
+
+// ObjectStorageValidationError is the validation error returned by
+// ObjectStorage.Validate if the designated constraints aren't met.
+type ObjectStorageValidationError struct {
+	field  string
+	reason string
+	cause  error
+	key    bool
+}
+
+// Field function returns field value.
+func (e ObjectStorageValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e ObjectStorageValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e ObjectStorageValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e ObjectStorageValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e ObjectStorageValidationError) ErrorName() string { return "ObjectStorageValidationError" }
+
+// Error satisfies the builtin error interface
+func (e ObjectStorageValidationError) Error() string {
+	cause := ""
+	if e.cause != nil {
+		cause = fmt.Sprintf(" | caused by: %v", e.cause)
+	}
+
+	key := ""
+	if e.key {
+		key = "key for "
+	}
+
+	return fmt.Sprintf(
+		"invalid %sObjectStorage.%s: %s%s",
+		key,
+		e.field,
+		e.reason,
+		cause)
+}
+
+var _ error = ObjectStorageValidationError{}
+
+var _ interface {
+	Field() string
+	Reason() string
+	Key() bool
+	Cause() error
+	ErrorName() string
+} = ObjectStorageValidationError{}
+
+// Validate checks the field values on GetObjectStorageRequest with the rules
+// defined in the proto definition for this message. If any rules are
+// violated, an error is returned.
+func (m *GetObjectStorageRequest) Validate() error {
+	if m == nil {
+		return nil
+	}
+
+	if _, ok := SourceType_name[int32(m.GetSourceType())]; !ok {
+		return GetObjectStorageRequestValidationError{
+			field:  "SourceType",
+			reason: "value must be one of the defined enum values",
+		}
+	}
+
+	if err := m._validateHostname(m.GetHostName()); err != nil {
+		return GetObjectStorageRequestValidationError{
+			field:  "HostName",
+			reason: "value must be a valid hostname",
+			cause:  err,
+		}
+	}
+
+	if ip := net.ParseIP(m.GetIp()); ip == nil {
+		return GetObjectStorageRequestValidationError{
+			field:  "Ip",
+			reason: "value must be a valid IP address",
+		}
+	}
+
+	return nil
+}
+
+func (m *GetObjectStorageRequest) _validateHostname(host string) error {
+	s := strings.ToLower(strings.TrimSuffix(host, "."))
+
+	if len(host) > 253 {
+		return errors.New("hostname cannot exceed 253 characters")
+	}
+
+	for _, part := range strings.Split(s, ".") {
+		if l := len(part); l == 0 || l > 63 {
+			return errors.New("hostname part must be non-empty and cannot exceed 63 characters")
+		}
+
+		if part[0] == '-' {
+			return errors.New("hostname parts cannot begin with hyphens")
+		}
+
+		if part[len(part)-1] == '-' {
+			return errors.New("hostname parts cannot end with hyphens")
+		}
+
+		for _, r := range part {
+			if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' {
+				return fmt.Errorf("hostname parts can only contain alphanumeric characters or hyphens, got %q", string(r))
+			}
+		}
+	}
+
+	return nil
+}
+
+// GetObjectStorageRequestValidationError is the validation error returned by
+// GetObjectStorageRequest.Validate if the designated constraints aren't met.
+type GetObjectStorageRequestValidationError struct {
+	field  string
+	reason string
+	cause  error
+	key    bool
+}
+
+// Field function returns field value.
+func (e GetObjectStorageRequestValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e GetObjectStorageRequestValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e GetObjectStorageRequestValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e GetObjectStorageRequestValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e GetObjectStorageRequestValidationError) ErrorName() string {
+	return "GetObjectStorageRequestValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e GetObjectStorageRequestValidationError) Error() string {
+	cause := ""
+	if e.cause != nil {
+		cause = fmt.Sprintf(" | caused by: %v", e.cause)
+	}
+
+	key := ""
+	if e.key {
+		key = "key for "
+	}
+
+	return fmt.Sprintf(
+		"invalid %sGetObjectStorageRequest.%s: %s%s",
+		key,
+		e.field,
+		e.reason,
+		cause)
+}
+
+var _ error = GetObjectStorageRequestValidationError{}
+
+var _ interface {
+	Field() string
+	Reason() string
+	Key() bool
+	Cause() error
+	ErrorName() string
+} = GetObjectStorageRequestValidationError{}
+
+// Validate checks the field values on Bucket with the rules defined in the
+// proto definition for this message. If any rules are violated, an error is returned.
+func (m *Bucket) Validate() error {
+	if m == nil {
+		return nil
+	}
+
+	if l := utf8.RuneCountInString(m.GetName()); l < 1 || l > 1024 {
+		return BucketValidationError{
+			field:  "Name",
+			reason: "value length must be between 1 and 1024 runes, inclusive",
+		}
+	}
+
+	return nil
+}
+
+// BucketValidationError is the validation error returned by Bucket.Validate if
+// the designated constraints aren't met.
+type BucketValidationError struct {
+	field  string
+	reason string
+	cause  error
+	key    bool
+}
+
+// Field function returns field value.
+func (e BucketValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e BucketValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e BucketValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e BucketValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e BucketValidationError) ErrorName() string { return "BucketValidationError" }
+
+// Error satisfies the builtin error interface
+func (e BucketValidationError) Error() string {
+	cause := ""
+	if e.cause != nil {
+		cause = fmt.Sprintf(" | caused by: %v", e.cause)
+	}
+
+	key := ""
+	if e.key {
+		key = "key for "
+	}
+
+	return fmt.Sprintf(
+		"invalid %sBucket.%s: %s%s",
+		key,
+		e.field,
+		e.reason,
+		cause)
+}
+
+var _ error = BucketValidationError{}
+
+var _ interface {
+	Field() string
+	Reason() string
+	Key() bool
+	Cause() error
+	ErrorName() string
+} = BucketValidationError{}
+
+// Validate checks the field values on ListBucketsRequest with the rules
+// defined in the proto definition for this message. If any rules are
+// violated, an error is returned.
+func (m *ListBucketsRequest) Validate() error {
+	if m == nil {
+		return nil
+	}
+
+	if _, ok := SourceType_name[int32(m.GetSourceType())]; !ok {
+		return ListBucketsRequestValidationError{
+			field:  "SourceType",
+			reason: "value must be one of the defined enum values",
+		}
+	}
+
+	if err := m._validateHostname(m.GetHostName()); err != nil {
+		return ListBucketsRequestValidationError{
+			field:  "HostName",
+			reason: "value must be a valid hostname",
+			cause:  err,
+		}
+	}
+
+	if ip := net.ParseIP(m.GetIp()); ip == nil {
+		return ListBucketsRequestValidationError{
+			field:  "Ip",
+			reason: "value must be a valid IP address",
+		}
+	}
+
+	return nil
+}
+
+func (m *ListBucketsRequest) _validateHostname(host string) error {
+	s := strings.ToLower(strings.TrimSuffix(host, "."))
+
+	if len(host) > 253 {
+		return errors.New("hostname cannot exceed 253 characters")
+	}
+
+	for _, part := range strings.Split(s, ".") {
+		if l := len(part); l == 0 || l > 63 {
+			return errors.New("hostname part must be non-empty and cannot exceed 63 characters")
+		}
+
+		if part[0] == '-' {
+			return errors.New("hostname parts cannot begin with hyphens")
+		}
+
+		if part[len(part)-1] == '-' {
+			return errors.New("hostname parts cannot end with hyphens")
+		}
+
+		for _, r := range part {
+			if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' {
+				return fmt.Errorf("hostname parts can only contain alphanumeric characters or hyphens, got %q", string(r))
+			}
+		}
+	}
+
+	return nil
+}
+
+// ListBucketsRequestValidationError is the validation error returned by
+// ListBucketsRequest.Validate if the designated constraints aren't met.
+type ListBucketsRequestValidationError struct {
+	field  string
+	reason string
+	cause  error
+	key    bool
+}
+
+// Field function returns field value.
+func (e ListBucketsRequestValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e ListBucketsRequestValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e ListBucketsRequestValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e ListBucketsRequestValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e ListBucketsRequestValidationError) ErrorName() string {
+	return "ListBucketsRequestValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e ListBucketsRequestValidationError) Error() string {
+	cause := ""
+	if e.cause != nil {
+		cause = fmt.Sprintf(" | caused by: %v", e.cause)
+	}
+
+	key := ""
+	if e.key {
+		key = "key for "
+	}
+
+	return fmt.Sprintf(
+		"invalid %sListBucketsRequest.%s: %s%s",
+		key,
+		e.field,
+		e.reason,
+		cause)
+}
+
+var _ error = ListBucketsRequestValidationError{}
+
+var _ interface {
+	Field() string
+	Reason() string
+	Key() bool
+	Cause() error
+	ErrorName() string
+} = ListBucketsRequestValidationError{}
+
+// Validate checks the field values on ListBucketsResponse with the rules
+// defined in the proto definition for this message. If any rules are
+// violated, an error is returned.
+func (m *ListBucketsResponse) Validate() error {
+	if m == nil {
+		return nil
+	}
+
+	for idx, item := range m.GetBuckets() {
+		_, _ = idx, item
+
+		if v, ok := interface{}(item).(interface{ Validate() error }); ok {
+			if err := v.Validate(); err != nil {
+				return ListBucketsResponseValidationError{
+					field:  fmt.Sprintf("Buckets[%v]", idx),
+					reason: "embedded message failed validation",
+					cause:  err,
+				}
+			}
+		}
+
+	}
+
+	return nil
+}
+
+// ListBucketsResponseValidationError is the validation error returned by
+// ListBucketsResponse.Validate if the designated constraints aren't met.
+type ListBucketsResponseValidationError struct {
+	field  string
+	reason string
+	cause  error
+	key    bool
+}
+
+// Field function returns field value.
+func (e ListBucketsResponseValidationError) Field() string { return e.field }
+
+// Reason function returns reason value.
+func (e ListBucketsResponseValidationError) Reason() string { return e.reason }
+
+// Cause function returns cause value.
+func (e ListBucketsResponseValidationError) Cause() error { return e.cause }
+
+// Key function returns key value.
+func (e ListBucketsResponseValidationError) Key() bool { return e.key }
+
+// ErrorName returns error name.
+func (e ListBucketsResponseValidationError) ErrorName() string {
+	return "ListBucketsResponseValidationError"
+}
+
+// Error satisfies the builtin error interface
+func (e ListBucketsResponseValidationError) Error() string {
+	cause := ""
+	if e.cause != nil {
+		cause = fmt.Sprintf(" | caused by: %v", e.cause)
+	}
+
+	key := ""
+	if e.key {
+		key = "key for "
+	}
+
+	return fmt.Sprintf(
+		"invalid %sListBucketsResponse.%s: %s%s",
+		key,
+		e.field,
+		e.reason,
+		cause)
+}
+
+var _ error = ListBucketsResponseValidationError{}
+
+var _ interface {
+	Field() string
+	Reason() string
+	Key() bool
+	Cause() error
+	ErrorName() string
+} = ListBucketsResponseValidationError{}
+
 // Validate checks the field values on KeepAliveRequest with the rules defined
 // in the proto definition for this message. If any rules are violated, an
 // error is returned.
diff --git a/pkg/rpc/manager/manager.proto b/pkg/rpc/manager/manager.proto
index 7558aed0..8cadd582 100644
--- a/pkg/rpc/manager/manager.proto
+++ b/pkg/rpc/manager/manager.proto
@@ -231,6 +231,52 @@ message ListSchedulersResponse {
   repeated Scheduler schedulers = 1;
 }
 
+// ObjectStorage represents config of object storage.
+message ObjectStorage {
+  // Object storage name of type.
+  string name = 1 [(validate.rules).string = {min_len: 1, max_len: 1024}];
+  // Storage region.
+  string region = 2 [(validate.rules).string = {min_len: 1, max_len: 1024, ignore_empty: true}];
+  // Datacenter endpoint.
+  string endpoint = 3 [(validate.rules).string = {min_len: 1, max_len: 1024, ignore_empty: true}];
+  // Access key id.
+  string access_key = 4 [(validate.rules).string = {min_len: 1, max_len: 1024, ignore_empty: true}];
+  // Access key secret.
+  string secret_key = 5 [(validate.rules).string = {min_len: 1, max_len: 1024, ignore_empty: true}];
+}
+
+// GetObjectStorageRequest represents request of GetObjectStorage.
+message GetObjectStorageRequest {
+  // Request source type.
+  SourceType source_type = 1 [(validate.rules).enum.defined_only = true];
+  // Source service hostname.
+  string host_name = 2 [(validate.rules).string.hostname = true];
+  // Source service ip.
+  string ip = 3 [(validate.rules).string.ip = true];
+}
+
+// Bucket represents config of bucket.
+message Bucket {
+  // Bucket name.
+  string name = 1 [(validate.rules).string = {min_len: 1, max_len: 1024}];
+}
+
+// ListSchedulersRequest represents request of ListBuckets.
+message ListBucketsRequest {
+  // Request source type.
+  SourceType source_type = 1 [(validate.rules).enum.defined_only = true];
+  // Source service hostname.
+  string host_name = 2 [(validate.rules).string.hostname = true];
+  // Source service ip.
+  string ip = 3 [(validate.rules).string.ip = true];
+}
+
+// ListBucketsResponse represents response of ListBuckets.
+message ListBucketsResponse {
+  // Bucket configs.
+  repeated Bucket buckets = 1;
+}
+
 // KeepAliveRequest represents request of KeepAlive.
 message KeepAliveRequest {
   // Request source type.
@@ -253,6 +299,10 @@ service Manager {
   rpc UpdateScheduler(UpdateSchedulerRequest) returns(Scheduler);
   // List acitve schedulers configuration.
   rpc ListSchedulers(ListSchedulersRequest)returns(ListSchedulersResponse);
+  // Get ObjectStorage configuration.
+  rpc GetObjectStorage(GetObjectStorageRequest) returns(ObjectStorage);
+  // List buckets configuration.
+  rpc ListBuckets(ListBucketsRequest)returns(ListBucketsResponse);
   // KeepAlive with manager.
   rpc KeepAlive(stream KeepAliveRequest)returns(google.protobuf.Empty);
 }


Test patch:
diff --git a/client/config/dynconfig_test.go b/client/config/dynconfig_test.go
index 00453b6c..adf0eb51 100644
--- a/client/config/dynconfig_test.go
+++ b/client/config/dynconfig_test.go
@@ -25,9 +25,11 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
 
-	"d7y.io/dragonfly/v2/client/config/mocks"
 	"d7y.io/dragonfly/v2/pkg/rpc/manager"
+	"d7y.io/dragonfly/v2/pkg/rpc/manager/client/mocks"
 )
 
 func TestDynconfigNewDynconfig(t *testing.T) {
@@ -54,7 +56,11 @@ func TestDynconfigNewDynconfig(t *testing.T) {
 				}
 			},
 			mock: func(m *mocks.MockClientMockRecorder) {
-				m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1)
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+				)
 			},
 			expect: func(t *testing.T, err error) {
 				assert := assert.New(t)
@@ -62,7 +68,7 @@ func TestDynconfigNewDynconfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "new dynconfig without empty host option",
+			name:       "new dynconfig and host option is empty",
 			expire:     10 * time.Millisecond,
 			hostOption: HostOption{},
 			cleanFileCache: func(t *testing.T) {
@@ -71,7 +77,11 @@ func TestDynconfigNewDynconfig(t *testing.T) {
 				}
 			},
 			mock: func(m *mocks.MockClientMockRecorder) {
-				m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1)
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+				)
 			},
 			expect: func(t *testing.T, err error) {
 				assert := assert.New(t)
@@ -79,7 +89,7 @@ func TestDynconfigNewDynconfig(t *testing.T) {
 			},
 		},
 		{
-			name:           "new dynconfig with list scheduler error",
+			name:           "new dynconfig and list scheduler error",
 			expire:         10 * time.Millisecond,
 			hostOption:     HostOption{},
 			cleanFileCache: func(t *testing.T) {},
@@ -92,7 +102,60 @@ func TestDynconfigNewDynconfig(t *testing.T) {
 			},
 		},
 		{
-			name:   "new dynconfig without expire time",
+			name:           "new dynconfig and get object storage error",
+			expire:         10 * time.Millisecond,
+			hostOption:     HostOption{},
+			cleanFileCache: func(t *testing.T) {},
+			mock: func(m *mocks.MockClientMockRecorder) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(nil, errors.New("foo")).Times(1),
+				)
+			},
+			expect: func(t *testing.T, err error) {
+				assert := assert.New(t)
+				assert.Errorf(err, "foo")
+			},
+		},
+		{
+			name:       "new dynconfig and object storage is not found",
+			expire:     10 * time.Millisecond,
+			hostOption: HostOption{},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(nil, status.Error(codes.NotFound, "")).Times(1),
+				)
+			},
+			expect: func(t *testing.T, err error) {
+				assert := assert.New(t)
+				assert.NoError(err)
+			},
+		},
+		{
+			name:           "new dynconfig and list buckets error",
+			expire:         10 * time.Millisecond,
+			hostOption:     HostOption{},
+			cleanFileCache: func(t *testing.T) {},
+			mock: func(m *mocks.MockClientMockRecorder) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(nil, errors.New("foo")).Times(1),
+				)
+			},
+			expect: func(t *testing.T, err error) {
+				assert := assert.New(t)
+				assert.Errorf(err, "foo")
+			},
+		},
+		{
+			name:   "new dynconfig and expire time is empty",
 			expire: 0,
 			hostOption: HostOption{
 				Hostname: "foo",
@@ -146,6 +209,14 @@ func TestDynconfigGet(t *testing.T) {
 						HostName: "foo",
 					},
 				},
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+				Buckets: []*manager.Bucket{
+					{
+						Name: "foo",
+					},
+				},
 			},
 			sleep: func() {},
 			cleanFileCache: func(t *testing.T) {
@@ -162,6 +233,16 @@ func TestDynconfigGet(t *testing.T) {
 							},
 						},
 					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{
+						Buckets: []*manager.Bucket{
+							{
+								Name: data.ObjectStorage.Name,
+							},
+						},
+					}, nil).Times(1),
 				)
 			},
 			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
@@ -183,6 +264,14 @@ func TestDynconfigGet(t *testing.T) {
 						HostName: "foo",
 					},
 				},
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+				Buckets: []*manager.Bucket{
+					{
+						Name: "foo",
+					},
+				},
 			},
 			sleep: func() {
 				time.Sleep(100 * time.Millisecond)
@@ -195,6 +284,8 @@ func TestDynconfigGet(t *testing.T) {
 			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
 				gomock.InOrder(
 					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
 					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{
 						Schedulers: []*manager.Scheduler{
 							{
@@ -202,6 +293,16 @@ func TestDynconfigGet(t *testing.T) {
 							},
 						},
 					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{
+						Buckets: []*manager.Bucket{
+							{
+								Name: data.ObjectStorage.Name,
+							},
+						},
+					}, nil).Times(1),
 				)
 			},
 			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
@@ -223,6 +324,14 @@ func TestDynconfigGet(t *testing.T) {
 						HostName: "foo",
 					},
 				},
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+				Buckets: []*manager.Bucket{
+					{
+						Name: "foo",
+					},
+				},
 			},
 			sleep: func() {
 				time.Sleep(100 * time.Millisecond)
@@ -241,6 +350,16 @@ func TestDynconfigGet(t *testing.T) {
 							},
 						},
 					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{
+						Buckets: []*manager.Bucket{
+							{
+								Name: data.Buckets[0].Name,
+							},
+						},
+					}, nil).Times(1),
 					m.ListSchedulers(gomock.Any()).Return(nil, errors.New("foo")).Times(1),
 				)
 			},
@@ -252,13 +371,205 @@ func TestDynconfigGet(t *testing.T) {
 			},
 		},
 		{
-			name:   "list schedulers empty",
+			name:   "get object storage error",
 			expire: 10 * time.Millisecond,
 			hostOption: HostOption{
 				Hostname: "foo",
 			},
 			data: &DynconfigData{
-				Schedulers: []*manager.Scheduler(nil),
+				Schedulers: []*manager.Scheduler{
+					{
+						HostName: "foo",
+					},
+				},
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+				Buckets: []*manager.Bucket{
+					{
+						Name: "foo",
+					},
+				},
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{
+						Schedulers: []*manager.Scheduler{
+							{
+								HostName: data.Schedulers[0].HostName,
+							},
+						},
+					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{
+						Buckets: []*manager.Bucket{
+							{
+								Name: data.Buckets[0].Name,
+							},
+						},
+					}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(nil, errors.New("foo")).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.Get()
+				assert.NoError(err)
+				assert.EqualValues(result, data)
+			},
+		},
+		{
+			name:   "object storage is not found",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				Schedulers: []*manager.Scheduler{
+					{
+						HostName: "foo",
+					},
+				},
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+				Buckets: []*manager.Bucket{
+					{
+						Name: "foo",
+					},
+				},
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{
+						Schedulers: []*manager.Scheduler{
+							{
+								HostName: data.Schedulers[0].HostName,
+							},
+						},
+					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{
+						Buckets: []*manager.Bucket{
+							{
+								Name: data.Buckets[0].Name,
+							},
+						},
+					}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{
+						Schedulers: []*manager.Scheduler{
+							{
+								HostName: data.Schedulers[0].HostName,
+							},
+						},
+					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(nil, status.Error(codes.NotFound, "")).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.Get()
+				assert.NoError(err)
+				assert.EqualValues(result, &DynconfigData{
+					Schedulers: []*manager.Scheduler{
+						{
+							HostName: data.Schedulers[0].HostName,
+						},
+					},
+				})
+			},
+		},
+		{
+			name:   "list buckets error",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				Schedulers: []*manager.Scheduler{
+					{
+						HostName: "foo",
+					},
+				},
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+				Buckets: []*manager.Bucket{
+					{
+						Name: "foo",
+					},
+				},
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{
+						Schedulers: []*manager.Scheduler{
+							{
+								HostName: data.Schedulers[0].HostName,
+							},
+						},
+					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{
+						Buckets: []*manager.Bucket{
+							{
+								Name: data.Buckets[0].Name,
+							},
+						},
+					}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(nil, errors.New("foo")).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.Get()
+				assert.NoError(err)
+				assert.EqualValues(result, data)
+			},
+		},
+		{
+			name:   "resource is empty",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				Schedulers:    []*manager.Scheduler(nil),
+				ObjectStorage: &manager.ObjectStorage{},
+				Buckets:       []*manager.Bucket(nil),
 			},
 			sleep: func() {
 				time.Sleep(100 * time.Millisecond)
@@ -271,7 +582,11 @@ func TestDynconfigGet(t *testing.T) {
 			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
 				gomock.InOrder(
 					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
 					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
 				)
 			},
 			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
@@ -344,6 +659,8 @@ func TestDynconfigGetSchedulers(t *testing.T) {
 							},
 						},
 					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
 				)
 			},
 			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
@@ -377,6 +694,8 @@ func TestDynconfigGetSchedulers(t *testing.T) {
 			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
 				gomock.InOrder(
 					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
 					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{
 						Schedulers: []*manager.Scheduler{
 							{
@@ -384,6 +703,8 @@ func TestDynconfigGetSchedulers(t *testing.T) {
 							},
 						},
 					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
 				)
 			},
 			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
@@ -423,6 +744,8 @@ func TestDynconfigGetSchedulers(t *testing.T) {
 							},
 						},
 					}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
 					m.ListSchedulers(gomock.Any()).Return(nil, errors.New("foo")).Times(1),
 				)
 			},
@@ -434,7 +757,7 @@ func TestDynconfigGetSchedulers(t *testing.T) {
 			},
 		},
 		{
-			name:   "list schedulers empty",
+			name:   "schedulers is empty",
 			expire: 10 * time.Millisecond,
 			hostOption: HostOption{
 				Hostname: "foo",
@@ -453,7 +776,11 @@ func TestDynconfigGetSchedulers(t *testing.T) {
 			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
 				gomock.InOrder(
 					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
 					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
 				)
 			},
 			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
@@ -483,3 +810,413 @@ func TestDynconfigGetSchedulers(t *testing.T) {
 		})
 	}
 }
+
+func TestDynconfigGetObjectStorage(t *testing.T) {
+	mockCacheDir := t.TempDir()
+
+	mockCachePath := filepath.Join(mockCacheDir, cacheFileName)
+	tests := []struct {
+		name           string
+		expire         time.Duration
+		hostOption     HostOption
+		data           *DynconfigData
+		sleep          func()
+		cleanFileCache func(t *testing.T)
+		mock           func(m *mocks.MockClientMockRecorder, data *DynconfigData)
+		expect         func(t *testing.T, dynconfig Dynconfig, data *DynconfigData)
+	}{
+		{
+			name:   "get cache object storage",
+			expire: 10 * time.Second,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+			},
+			sleep: func() {},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.GetObjectStorage()
+				assert.NoError(err)
+				assert.EqualValues(result, data.ObjectStorage)
+			},
+		},
+		{
+			name:   "get object storage",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.GetObjectStorage()
+				assert.NoError(err)
+				assert.EqualValues(result, data.ObjectStorage)
+			},
+		},
+		{
+			name:   "get object storage error",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(nil, errors.New("foo")).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.GetObjectStorage()
+				assert.NoError(err)
+				assert.EqualValues(result, data.ObjectStorage)
+			},
+		},
+		{
+			name:   "object storage is not found",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				ObjectStorage: &manager.ObjectStorage{
+					Name: "foo",
+				},
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{
+						Name: data.ObjectStorage.Name,
+					}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(nil, status.Error(codes.NotFound, "")).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.GetObjectStorage()
+				assert.NoError(err)
+				assert.EqualValues(result, (*manager.ObjectStorage)(nil))
+			},
+		},
+		{
+			name:   "object storage is empty",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				ObjectStorage: &manager.ObjectStorage{},
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.GetObjectStorage()
+				assert.NoError(err)
+				assert.EqualValues(result, data.ObjectStorage)
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			ctl := gomock.NewController(t)
+			defer ctl.Finish()
+
+			mockManagerClient := mocks.NewMockClient(ctl)
+			tc.mock(mockManagerClient.EXPECT(), tc.data)
+			dynconfig, err := NewDynconfig(mockManagerClient, mockCacheDir, tc.hostOption, tc.expire)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			tc.sleep()
+			tc.expect(t, dynconfig, tc.data)
+			tc.cleanFileCache(t)
+		})
+	}
+}
+
+func TestDynconfigGetBuckets(t *testing.T) {
+	mockCacheDir := t.TempDir()
+
+	mockCachePath := filepath.Join(mockCacheDir, cacheFileName)
+	tests := []struct {
+		name           string
+		expire         time.Duration
+		hostOption     HostOption
+		data           *DynconfigData
+		sleep          func()
+		cleanFileCache func(t *testing.T)
+		mock           func(m *mocks.MockClientMockRecorder, data *DynconfigData)
+		expect         func(t *testing.T, dynconfig Dynconfig, data *DynconfigData)
+	}{
+		{
+			name:   "get cache buckets",
+			expire: 10 * time.Second,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				Buckets: []*manager.Bucket{
+					{
+						Name: "foo",
+					},
+				},
+			},
+			sleep: func() {},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{
+						Buckets: []*manager.Bucket{
+							{
+								Name: data.Buckets[0].Name,
+							},
+						},
+					}, nil).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.GetBuckets()
+				assert.NoError(err)
+				assert.EqualValues(result, data.Buckets)
+			},
+		},
+		{
+			name:   "get buckets",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				Buckets: []*manager.Bucket{
+					{
+						Name: "foo",
+					},
+				},
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{
+						Buckets: []*manager.Bucket{
+							{
+								Name: data.Buckets[0].Name,
+							},
+						},
+					}, nil).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.GetBuckets()
+				assert.NoError(err)
+				assert.EqualValues(result, data.Buckets)
+			},
+		},
+		{
+			name:   "list buckets error",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				Buckets: []*manager.Bucket{
+					{
+						Name: "foo",
+					},
+				},
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{
+						Buckets: []*manager.Bucket{
+							{
+								Name: data.Buckets[0].Name,
+							},
+						},
+					}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(nil, errors.New("foo")).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.GetBuckets()
+				assert.NoError(err)
+				assert.EqualValues(result, data.Buckets)
+			},
+		},
+		{
+			name:   "buckets is empty",
+			expire: 10 * time.Millisecond,
+			hostOption: HostOption{
+				Hostname: "foo",
+			},
+			data: &DynconfigData{
+				Buckets: []*manager.Bucket(nil),
+			},
+			sleep: func() {
+				time.Sleep(100 * time.Millisecond)
+			},
+			cleanFileCache: func(t *testing.T) {
+				if err := os.Remove(mockCachePath); err != nil {
+					t.Fatal(err)
+				}
+			},
+			mock: func(m *mocks.MockClientMockRecorder, data *DynconfigData) {
+				gomock.InOrder(
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+					m.ListSchedulers(gomock.Any()).Return(&manager.ListSchedulersResponse{}, nil).Times(1),
+					m.GetObjectStorage(gomock.Any()).Return(&manager.ObjectStorage{}, nil).Times(1),
+					m.ListBuckets(gomock.Any()).Return(&manager.ListBucketsResponse{}, nil).Times(1),
+				)
+			},
+			expect: func(t *testing.T, dynconfig Dynconfig, data *DynconfigData) {
+				assert := assert.New(t)
+				result, err := dynconfig.GetBuckets()
+				assert.NoError(err)
+				assert.EqualValues(result, data.Buckets)
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			ctl := gomock.NewController(t)
+			defer ctl.Finish()
+
+			mockManagerClient := mocks.NewMockClient(ctl)
+			tc.mock(mockManagerClient.EXPECT(), tc.data)
+			dynconfig, err := NewDynconfig(mockManagerClient, mockCacheDir, tc.hostOption, tc.expire)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			tc.sleep()
+			tc.expect(t, dynconfig, tc.data)
+			tc.cleanFileCache(t)
+		})
+	}
+}