feat: storage plugin list objects (#6723)

* feat(proto): add storage plugin proto definitions for session recording recovery

Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com>

Co-authored-by: Ryan Derr <ryan.derr@hashicorp.com>

* feat(gen): add generated storage plugin code for session recording recovery

Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com>

Co-authored-by: Manjeet Singh <manjeet.singh@hashicorp.com>

* feat(plugin): add plugin loopback support for session recording recovery

Co-authored-by: Ryan Derr <ryan.derr@hashicorp.com>

Co-authored-by: aditya2548 <aditya25dec2000@gmail.com>

---------

Co-authored-by: Ryan Derr <ryan.derr@hashicorp.com>
Co-authored-by: Manjeet Singh <manjeet.singh@hashicorp.com>
Co-authored-by: aditya2548 <aditya25dec2000@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-05-14 13:02:32 -05:00
1 parent 205644b091
commit cbb76ea7c8
9 files changed
+1771 -103

No files matched your search

+4
View File
@@ -91,6 +91,10 @@ func (tpc *WrappingPluginStorageClient) HeadObject(ctx context.Context, req *plg
return tpc.Server.HeadObject(ctx, req)
}
func (tpc *WrappingPluginStorageClient) ListObjects(ctx context.Context, req *plgpb.ListObjectsRequest, opts ...grpc.CallOption) (*plgpb.ListObjectsResponse, error) {
return tpc.Server.ListObjects(ctx, req)
}
func (tpc *WrappingPluginStorageClient) ValidatePermissions(ctx context.Context, req *plgpb.ValidatePermissionsRequest, opts ...grpc.CallOption) (*plgpb.ValidatePermissionsResponse, error) {
return tpc.Server.ValidatePermissions(ctx, req)
}
+1
View File
@@ -113,6 +113,7 @@ func NewLoopbackPlugin(opt ...TestOption) (*LoopbackPlugin, error) {
ret.GetObjectFn = ret.getObject
ret.PutObjectFn = ret.putObject
ret.DeleteObjectsFn = ret.deleteObjects
ret.ListObjectsFn = ret.listObjects
if len(opts.withMockBuckets) > 0 {
ret.buckets = opts.withMockBuckets
}
+52 -7
View File
@@ -4,6 +4,8 @@
package loopback
import (
"strings"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/grpc/codes"
@@ -39,6 +41,9 @@ const (
// Will return an error for the DeleteObjects method
DeleteObjects
// Will return an error for the ListObjects method
ListObjects
)
const (
@@ -47,12 +52,47 @@ const (
// PluginMockError is used to mock an error when interacting with an external object store.
type PluginMockError struct {
BucketName string
BucketPrefix string
ObjectKey string
ErrMsg string
ErrCode codes.Code
ErrMethod Method
// BucketName is the name of the bucket to match on.
BucketName string
// BucketPrefix is the prefix of the bucket to match on.
BucketPrefix string
// ObjectKey is the object key to match on. This is optional and may be empty.
// If empty, the error will match any object key, as long as the ErrPath is not set.
// This is because the ErrPath is a less specific match than the ObjectKey.
// The ObjectKey is ignored when the ErrPath is set.
// The ObjectKey is also ignored for the following plugin methods: onCreateStorageBucket,
// onUpdateStorageBucket, onDeleteStorageBucket as these methods do not provide an object key.
ObjectKey string
// ErrPath is the object key path prefix to match on. This is optional and may be empty.
// If empty, the error will match any object key, as long as the ObjectKey is not set.
// The ErrPath is ignored when the ObjectKey is set.
// The ErrPath is also ignored for the following plugin methods: onCreateStorageBucket,
// onUpdateStorageBucket, onDeleteStorageBucket as these methods do not provide an object key.
// The ErrPath is used to match on a prefix of the object key. For example, if the ErrPath
// is set to "path/to/obj", it will match any object key that starts with "path/to/obj",
// such as "path/to/obj1", "path/to/obj2/subobj", etc.
// This allows for more flexible error mocking based on object key prefixes.
// The ErrPath should always end with a "/" if it is intended to match a directory prefix.
// For example, to match all objects under "path/to/dir/", the ErrPath should be set to "path/to/dir/".
// This ensures that it does not unintentionally match objects like "path/to/dir_obj".
ErrPath string
// ErrMsg is the error message to return.
ErrMsg string
// ErrCode is the gRPC error code to return.
ErrCode codes.Code
// ErrMethod is the plugin method to match on. This is optional and may be set to Any.
// If set to Any, the error will be returned for any plugin method that matches the bucket
// and object key criteria. If set to a specific method, the error will only be returned
// for that method if the bucket and object key criteria also match.
ErrMethod Method
// StorageBucketCredentialState is the credential state to return.
StorageBucketCredentialState *plgpb.StorageBucketCredentialState
}
@@ -67,7 +107,8 @@ func (e PluginMockError) match(bucket *storagebuckets.StorageBucket, key string,
// the object key comparison is ignored when the given key is empty because the following
// plugin methods do not provide key values: onCreateStorageBucket, onUpdateStorageBucket,
// onDeleteStorageBucket
if key != "" && e.ObjectKey != key {
// if the mocked error object key is empty, it should match any key, as long as the ErrPath is not set.
if key != "" && e.ObjectKey != "" && e.ObjectKey != key {
return false
}
// if the mocked error bucket name does not match the request's bucket name, return false.
@@ -87,6 +128,10 @@ func (e PluginMockError) match(bucket *storagebuckets.StorageBucket, key string,
if e.ErrMethod != method {
return false
}
// if the mocked error path is not empty and the key does not have the mocked error path as a prefix, return false.
if e.ErrPath != "" && !strings.HasPrefix(key, e.ErrPath) {
return false
}
// all checks comparison checks passed, return true.
return true
}
+549
View File
@@ -0,0 +1,549 @@
// Copyright IBM Corp. 2024, 2026
// SPDX-License-Identifier: BUSL-1.1
package loopback
import (
"testing"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
plgpb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
)
func TestPluginMockError_match(t *testing.T) {
t.Parallel()
testBucket := &storagebuckets.StorageBucket{
BucketName: "test-bucket",
BucketPrefix: "test-prefix",
}
testBucketNoPrefix := &storagebuckets.StorageBucket{
BucketName: "test-bucket",
}
tests := []struct {
name string
mockError PluginMockError
bucket *storagebuckets.StorageBucket
key string
method Method
expected bool
}{
{
name: "exact match with all fields",
mockError: PluginMockError{
BucketName: "test-bucket",
BucketPrefix: "test-prefix",
ObjectKey: "test-key",
ErrMethod: PutObject,
},
bucket: testBucket,
key: "test-key",
method: PutObject,
expected: true,
},
{
name: "match with Any method",
mockError: PluginMockError{
BucketName: "test-bucket",
BucketPrefix: "test-prefix",
ObjectKey: "test-key",
ErrMethod: Any,
},
bucket: testBucket,
key: "test-key",
method: GetObject,
expected: true,
},
{
name: "match with empty key (create/update/delete operations)",
mockError: PluginMockError{
BucketName: "test-bucket",
BucketPrefix: "test-prefix",
ErrMethod: OnCreateStorageBucket,
},
bucket: testBucket,
key: "", // Empty key for bucket operations
method: OnCreateStorageBucket,
expected: true,
},
{
name: "match with path prefix",
mockError: PluginMockError{
BucketName: "test-bucket",
BucketPrefix: "test-prefix",
ObjectKey: "logs/session123/data.log", // Must match the key exactly
ErrPath: "logs/", // Path prefix also matches
ErrMethod: PutObject,
},
bucket: testBucket,
key: "logs/session123/data.log",
method: PutObject,
expected: true,
},
{
name: "no match - different bucket name",
mockError: PluginMockError{
BucketName: "different-bucket",
ErrMethod: PutObject,
},
bucket: testBucket,
key: "test-key",
method: PutObject,
expected: false,
},
{
name: "no match - different object key",
mockError: PluginMockError{
BucketName: "test-bucket",
ObjectKey: "different-key",
ErrMethod: PutObject,
},
bucket: testBucket,
key: "test-key",
method: PutObject,
expected: false,
},
{
name: "no match - different method",
mockError: PluginMockError{
BucketName: "test-bucket",
ObjectKey: "test-key",
ErrMethod: GetObject,
},
bucket: testBucket,
key: "test-key",
method: PutObject,
expected: false,
},
{
name: "no match - different bucket prefix",
mockError: PluginMockError{
BucketName: "test-bucket",
BucketPrefix: "different-prefix",
ErrMethod: PutObject,
},
bucket: testBucket,
key: "test-key",
method: PutObject,
expected: false,
},
{
name: "no match - path prefix doesn't match",
mockError: PluginMockError{
BucketName: "test-bucket",
ObjectKey: "", // ObjectKey should be empty when using ErrPath
ErrPath: "logs/",
ErrMethod: PutObject,
},
bucket: testBucketNoPrefix,
key: "data/session123/info.json",
method: PutObject,
expected: false,
},
{
name: "match - bucket with no prefix requirement",
mockError: PluginMockError{
BucketName: "test-bucket",
ObjectKey: "test-key",
ErrMethod: PutObject,
},
bucket: testBucketNoPrefix,
key: "test-key",
method: PutObject,
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.mockError.match(tt.bucket, tt.key, tt.method)
assert.Equal(t, tt.expected, result)
})
}
}
func TestPluginMockPutObjectResponse_match(t *testing.T) {
t.Parallel()
testBucket := &storagebuckets.StorageBucket{
BucketName: "test-bucket",
BucketPrefix: "test-prefix",
}
testBucketNoPrefix := &storagebuckets.StorageBucket{
BucketName: "test-bucket",
}
tests := []struct {
name string
mockResponse PluginMockPutObjectResponse
bucket *storagebuckets.StorageBucket
key string
expected bool
}{
{
name: "exact match with all fields",
mockResponse: PluginMockPutObjectResponse{
BucketName: "test-bucket",
BucketPrefix: "test-prefix",
ObjectKey: "test-key",
},
bucket: testBucket,
key: "test-key",
expected: true,
},
{
name: "no match - different bucket name",
mockResponse: PluginMockPutObjectResponse{
BucketName: "different-bucket",
ObjectKey: "test-key",
},
bucket: testBucket,
key: "test-key",
expected: false,
},
{
name: "no match - different object key",
mockResponse: PluginMockPutObjectResponse{
BucketName: "test-bucket",
ObjectKey: "different-key",
},
bucket: testBucket,
key: "test-key",
expected: false,
},
{
name: "no match - different bucket prefix",
mockResponse: PluginMockPutObjectResponse{
BucketName: "test-bucket",
BucketPrefix: "different-prefix",
ObjectKey: "test-key",
},
bucket: testBucket,
key: "test-key",
expected: false,
},
{
name: "match - bucket with no prefix requirement",
mockResponse: PluginMockPutObjectResponse{
BucketName: "test-bucket",
ObjectKey: "test-key",
},
bucket: testBucketNoPrefix,
key: "test-key",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.mockResponse.match(tt.bucket, tt.key)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetTestOpts(t *testing.T) {
t.Parallel()
t.Run("default options", func(t *testing.T) {
opts, err := getTestOpts()
require.NoError(t, err)
expected := TestOptions{
withChunkSize: 8,
}
assert.Equal(t, expected.withChunkSize, opts.withChunkSize)
assert.Nil(t, opts.withMockBuckets)
assert.Empty(t, opts.withMockError)
assert.Empty(t, opts.withMockPutObjectResponse)
})
t.Run("with mock buckets", func(t *testing.T) {
mockBuckets := map[BucketName]Bucket{
"test-bucket": make(Bucket),
}
opts, err := getTestOpts(WithMockBuckets(mockBuckets))
require.NoError(t, err)
assert.Equal(t, mockBuckets, opts.withMockBuckets)
})
t.Run("with mock error", func(t *testing.T) {
mockError := PluginMockError{
BucketName: "test-bucket",
ErrMsg: "test error",
ErrCode: codes.Internal,
ErrMethod: PutObject,
}
opts, err := getTestOpts(WithMockError(mockError))
require.NoError(t, err)
require.Len(t, opts.withMockError, 1)
assert.Equal(t, mockError, opts.withMockError[0])
})
t.Run("with multiple mock errors", func(t *testing.T) {
mockError1 := PluginMockError{
BucketName: "test-bucket-1",
ErrMsg: "test error 1",
ErrCode: codes.Internal,
ErrMethod: PutObject,
}
mockError2 := PluginMockError{
BucketName: "test-bucket-2",
ErrMsg: "test error 2",
ErrCode: codes.NotFound,
ErrMethod: GetObject,
}
opts, err := getTestOpts(
WithMockError(mockError1),
WithMockError(mockError2),
)
require.NoError(t, err)
require.Len(t, opts.withMockError, 2)
assert.Equal(t, mockError1, opts.withMockError[0])
assert.Equal(t, mockError2, opts.withMockError[1])
})
t.Run("with mock put object response", func(t *testing.T) {
mockResponse := PluginMockPutObjectResponse{
BucketName: "test-bucket",
ObjectKey: "test-key",
Response: &plgpb.PutObjectResponse{
ChecksumSha_256: []byte("test-checksum"),
},
}
opts, err := getTestOpts(WithMockPutObjectResponse(mockResponse))
require.NoError(t, err)
require.Len(t, opts.withMockPutObjectResponse, 1)
assert.Equal(t, mockResponse, opts.withMockPutObjectResponse[0])
})
t.Run("with custom chunk size", func(t *testing.T) {
customChunkSize := 16
opts, err := getTestOpts(WithChunkSize(customChunkSize))
require.NoError(t, err)
assert.Equal(t, customChunkSize, opts.withChunkSize)
})
t.Run("with all options combined", func(t *testing.T) {
mockBuckets := map[BucketName]Bucket{
"test-bucket": make(Bucket),
}
mockError := PluginMockError{
BucketName: "test-bucket",
ErrMsg: "test error",
ErrCode: codes.Internal,
ErrMethod: PutObject,
}
mockResponse := PluginMockPutObjectResponse{
BucketName: "test-bucket",
ObjectKey: "test-key",
Response: &plgpb.PutObjectResponse{
ChecksumSha_256: []byte("test-checksum"),
},
}
customChunkSize := 32
opts, err := getTestOpts(
WithMockBuckets(mockBuckets),
WithMockError(mockError),
WithMockPutObjectResponse(mockResponse),
WithChunkSize(customChunkSize),
)
require.NoError(t, err)
assert.Equal(t, mockBuckets, opts.withMockBuckets)
require.Len(t, opts.withMockError, 1)
assert.Equal(t, mockError, opts.withMockError[0])
require.Len(t, opts.withMockPutObjectResponse, 1)
assert.Equal(t, mockResponse, opts.withMockPutObjectResponse[0])
assert.Equal(t, customChunkSize, opts.withChunkSize)
})
}
func TestGetDefaultTestOptions(t *testing.T) {
t.Parallel()
opts := getDefaultTestOptions()
assert.Equal(t, 8, opts.withChunkSize)
assert.Nil(t, opts.withMockBuckets)
assert.Empty(t, opts.withMockError)
assert.Empty(t, opts.withMockPutObjectResponse)
}
func TestMethod_Constants(t *testing.T) {
t.Parallel()
// Test that Method constants have expected values
assert.Equal(t, Method(0), Any)
assert.Equal(t, Method(1), OnCreateStorageBucket)
assert.Equal(t, Method(2), OnUpdateStorageBucket)
assert.Equal(t, Method(3), OnDeleteStorageBucket)
assert.Equal(t, Method(4), ValidatePermissions)
assert.Equal(t, Method(5), HeadObject)
assert.Equal(t, Method(6), GetObject)
assert.Equal(t, Method(7), PutObject)
assert.Equal(t, Method(8), DeleteObjects)
}
func TestPluginMockError_ComplexScenarios(t *testing.T) {
t.Parallel()
t.Run("storage bucket credential state scenarios", func(t *testing.T) {
bucket := &storagebuckets.StorageBucket{
BucketName: "test-bucket",
}
credentialState := &plgpb.StorageBucketCredentialState{
State: &plgpb.Permissions{
Write: &plgpb.Permission{
State: plgpb.StateType_STATE_TYPE_ERROR,
},
},
}
mockError := PluginMockError{
BucketName: "test-bucket",
ErrMethod: ValidatePermissions,
StorageBucketCredentialState: credentialState,
}
result := mockError.match(bucket, "", ValidatePermissions)
assert.True(t, result)
})
t.Run("path-based error matching", func(t *testing.T) {
bucket := &storagebuckets.StorageBucket{
BucketName: "test-bucket",
}
tests := []struct {
name string
errPath string
objectKey string
key string
expected bool
}{
{
name: "exact path match",
errPath: "sessions/",
objectKey: "sessions/sr_123/data.log",
key: "sessions/sr_123/data.log",
expected: true,
},
{
name: "nested path match",
errPath: "sessions/sr_123/",
objectKey: "sessions/sr_123/connections/cr_456/data.log",
key: "sessions/sr_123/connections/cr_456/data.log",
expected: true,
},
{
name: "no path match",
errPath: "sessions/",
objectKey: "recordings/sr_123/data.log",
key: "recordings/sr_123/data.log",
expected: false,
},
{
name: "path match but different object key",
errPath: "sessions/",
objectKey: "sessions/file1.log",
key: "sessions/file2.log",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockError := PluginMockError{
BucketName: "test-bucket",
ObjectKey: tt.objectKey,
ErrPath: tt.errPath,
ErrMethod: PutObject,
}
result := mockError.match(bucket, tt.key, PutObject)
assert.Equal(t, tt.expected, result)
})
}
})
t.Run("path-based matching with empty key (bucket operations)", func(t *testing.T) {
bucket := &storagebuckets.StorageBucket{
BucketName: "test-bucket",
}
t.Run("empty path matches bucket operations", func(t *testing.T) {
mockError := PluginMockError{
BucketName: "test-bucket",
ObjectKey: "",
ErrPath: "", // Empty path works with empty key
ErrMethod: OnCreateStorageBucket,
}
// Empty key for bucket operations (create/update/delete)
result := mockError.match(bucket, "", OnCreateStorageBucket)
assert.True(t, result)
})
t.Run("non-empty path does not match empty key", func(t *testing.T) {
mockError := PluginMockError{
BucketName: "test-bucket",
ObjectKey: "",
ErrPath: "some/path/", // Non-empty path won't match empty key
ErrMethod: OnCreateStorageBucket,
}
// Empty key for bucket operations - should not match non-empty path
result := mockError.match(bucket, "", OnCreateStorageBucket)
assert.False(t, result)
})
})
}
func TestTestOptions_EdgeCases(t *testing.T) {
t.Parallel()
t.Run("zero chunk size", func(t *testing.T) {
opts, err := getTestOpts(WithChunkSize(0))
require.NoError(t, err)
assert.Equal(t, 0, opts.withChunkSize)
})
t.Run("negative chunk size", func(t *testing.T) {
opts, err := getTestOpts(WithChunkSize(-1))
require.NoError(t, err)
assert.Equal(t, -1, opts.withChunkSize)
})
t.Run("empty mock buckets map", func(t *testing.T) {
emptyBuckets := make(map[BucketName]Bucket)
opts, err := getTestOpts(WithMockBuckets(emptyBuckets))
require.NoError(t, err)
assert.NotNil(t, opts.withMockBuckets)
assert.Empty(t, opts.withMockBuckets)
})
t.Run("nil mock buckets map", func(t *testing.T) {
opts, err := getTestOpts(WithMockBuckets(nil))
require.NoError(t, err)
assert.Nil(t, opts.withMockBuckets)
})
}
+214
View File
@@ -37,6 +37,7 @@ type TestPluginStorageServer struct {
GetObjectFn func(*plgpb.GetObjectRequest, plgpb.StoragePluginService_GetObjectServer) error
PutObjectFn func(context.Context, *plgpb.PutObjectRequest) (*plgpb.PutObjectResponse, error)
DeleteObjectsFn func(context.Context, *plgpb.DeleteObjectsRequest) (*plgpb.DeleteObjectsResponse, error)
ListObjectsFn func(context.Context, *plgpb.ListObjectsRequest) (*plgpb.ListObjectsResponse, error)
plgpb.UnimplementedStoragePluginServiceServer
}
@@ -103,6 +104,13 @@ func (t TestPluginStorageServer) DeleteObjects(ctx context.Context, req *plgpb.D
return t.DeleteObjectsFn(ctx, req)
}
func (t TestPluginStorageServer) ListObjects(ctx context.Context, req *plgpb.ListObjectsRequest) (*plgpb.ListObjectsResponse, error) {
if t.ListObjectsFn == nil {
return t.UnimplementedStoragePluginServiceServer.ListObjects(ctx, req)
}
return t.ListObjectsFn(ctx, req)
}
type (
BucketName string
ObjectName string
@@ -157,9 +165,25 @@ func (l *LoopbackStorage) normalizeStorageBucketData(ctx context.Context, req *p
// ResetNormalizations sets the number of times that NormalizeStorageBucketData
// has been called to 0. Useful for unit tests.
func (l *LoopbackStorage) ResetNormalizations() {
l.m.Lock()
defer l.m.Unlock()
l.normalizations = 0
}
// ResetMockErrors removes all mock errors.
func (l *LoopbackStorage) ResetMockErrors() {
l.m.Lock()
defer l.m.Unlock()
l.errs = []PluginMockError{}
}
// SetMockErrors overrides the existing mock errors.
func (l *LoopbackStorage) SetMockErrors(mockErrs ...PluginMockError) {
l.m.Lock()
defer l.m.Unlock()
l.errs = mockErrs
}
// GetNormalizations returns the number of times that NormalizeStorageBucketData
// has been called via the loopback plugin. Useful for unit tests.
func (l *LoopbackStorage) GetNormalizations() int {
@@ -538,6 +562,53 @@ func (l *LoopbackStorage) deleteObjects(ctx context.Context, req *plgpb.DeleteOb
}, nil
}
func (l *LoopbackStorage) listObjects(ctx context.Context, req *plgpb.ListObjectsRequest) (*plgpb.ListObjectsResponse, error) {
const op = "loopback.(LoopbackStorage).listObjects"
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: request is nil", op)
}
if req.GetBucket() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing storage bucket", op)
}
if req.GetBucket().GetAttributes() == nil {
return nil, status.Errorf(codes.InvalidArgument, "%s: missing bucket attributes", op)
}
l.m.Lock()
defer l.m.Unlock()
bucket, ok := l.buckets[BucketName(req.GetBucket().GetBucketName())]
if !ok {
return nil, status.Errorf(codes.NotFound, "%s: bucket not found", op)
}
// return an expected mock error if one was provided
for _, err := range l.errs {
if err.match(req.GetBucket(), req.GetKeyPrefix(), ListObjects) {
if err.StorageBucketCredentialState != nil {
return nil, createErrorWithBucketCredentialState(err.ErrCode, fmt.Sprintf("%s: %s", op, err.ErrMsg), err.StorageBucketCredentialState)
}
return nil, status.Errorf(err.ErrCode, "%s: %s", op, err.ErrMsg)
}
}
searchPrefix := path.Join(req.GetBucket().GetBucketPrefix(), req.GetKeyPrefix())
// path.Join strips trailing slashes, but we need to preserve them for directory prefixes
// to ensure proper filtering behavior in listObjects functions
if strings.HasSuffix(req.GetKeyPrefix(), "/") || (req.GetKeyPrefix() == "" && strings.HasSuffix(req.GetBucket().GetBucketPrefix(), "/")) {
searchPrefix = searchPrefix + "/"
}
var objects []*plgpb.Object
if req.GetRecursive() {
objects = listObjectsRecursive(bucket, searchPrefix)
} else {
objects = listObjectsNonRecursive(bucket, searchPrefix)
}
return &plgpb.ListObjectsResponse{
Objects: objects,
}, nil
}
// CloneBucket returns a clone of the bucket.
// returns nil when the bucket is not found.
func (l *LoopbackStorage) CloneBucket(name string) Bucket {
@@ -612,3 +683,146 @@ func createErrorWithBucketCredentialState(errCode codes.Code, msg string, sbcSta
}
return st.Err()
}
// listObjectsRecursive returns recursive list of all files and directories found under a prefix from a provided bucket
func listObjectsRecursive(bucket Bucket, searchPrefix string) []*plgpb.Object {
var objects []*plgpb.Object
dirs := make(map[string]struct{})
seenKeys := make(map[string]struct{})
for objectName := range bucket {
key := string(objectName)
// Skip any objects that do not match the search prefix
if !strings.HasPrefix(key, searchPrefix) {
continue
}
// Skip any objects that have already been seen
if _, exists := seenKeys[key]; exists {
continue
}
isDirFile := strings.HasSuffix(key, "/")
// If the object is a directory and matches the search prefix, skip it
// This can happen if the prefix itself is a directory
// e.g. prefix = "foo/", object key = "foo/"
// In this case we don't want to return the directory itself as an object
if isDirFile && key == searchPrefix {
continue
}
dirPaths := buildRecursiveDirectoryPaths(key, searchPrefix)
for _, dirPath := range dirPaths {
dirs[dirPath] = struct{}{}
seenKeys[dirPath] = struct{}{}
}
// Only add the object if it hasn't already been added as a directory
// This can happen if the object key ends with a '/' and is added as a directory
// in the previous step
if _, exists := seenKeys[key]; !exists {
objects = append(objects, &plgpb.Object{
Key: key,
IsDir: isDirFile,
})
}
seenKeys[key] = struct{}{}
}
for dirKey := range dirs {
objects = append(objects, &plgpb.Object{
Key: dirKey,
IsDir: true,
})
}
return objects
}
// listObjectsNonRecursive returns only top-level files and directories within the prefix
func listObjectsNonRecursive(bucket Bucket, searchPrefix string) []*plgpb.Object {
var objects []*plgpb.Object
seenKeys := make(map[string]struct{})
for objectName := range bucket {
key := string(objectName)
// Skip any objects that do not match the search prefix
if !strings.HasPrefix(key, searchPrefix) {
continue
}
// Skip any objects that have already been seen
if _, exists := seenKeys[key]; exists {
continue
}
isDirFile := strings.HasSuffix(key, "/")
// If the object is a directory and matches the search prefix, skip it
// This can happen if the prefix itself is a directory
// e.g. prefix = "foo/", object key = "foo/"
// In this case we don't want to return the directory itself as an object
if isDirFile && key == searchPrefix {
continue
}
// Trim the prefix and and trailing '/' after which we split the path
// to determine if this object is in a subdirectory or a file at the root prefix
relativeFullPath := strings.TrimSuffix(strings.TrimPrefix(key, searchPrefix), "/")
split := strings.Split(relativeFullPath, "/")
// Adding this to guardrail against the case that the stdlib updates the behavior
// of strings.Split() causing it to not always return at least a 1 length slice
if len(split) == 0 {
continue
}
relativeParentPath := searchPrefix + split[0]
// If the original object key ends with a '/' we treat it as a directory
// even if it is at the root of the prefix.
// If the split contains more than one entry, it is also a directory as it has sub-paths
if len(split) > 1 || isDirFile {
relativeParentPath += "/"
}
// Skip any constructed parent paths that have already been seen
if _, exists := seenKeys[relativeParentPath]; exists {
continue
}
// If the split contains more than one entry, it is a directory as it has sub-paths
// If the original object key ends with a '/' we treat it as a directory
// even if it is at the root of the prefix.
if len(split) > 1 {
objects = append(objects, &plgpb.Object{
Key: relativeParentPath,
IsDir: true,
})
} else {
objects = append(objects, &plgpb.Object{
Key: relativeParentPath,
IsDir: strings.HasSuffix(relativeParentPath, "/"),
})
}
seenKeys[relativeParentPath] = struct{}{}
}
return objects
}
// buildDirectoryPaths collects all parent directory paths from an object key
// that are within the given prefix. This is used to simulate directory
// structures in object storage.
func buildRecursiveDirectoryPaths(objectKey, prefix string) []string {
var dirPaths []string
prefixDir := strings.TrimSuffix(prefix, "/")
pathParts := strings.Split(objectKey, "/")
// Build all possible parent directory paths from the split parts
for i := 1; i < len(pathParts); i++ {
dirPath := strings.Join(pathParts[:i], "/")
// Only include directories that are deeper than the search prefix and add a trailing slash to denote a directory
if len(dirPath) > len(prefixDir) && strings.HasPrefix(dirPath+"/", prefix) {
dirPaths = append(dirPaths, fmt.Sprintf("%s/", dirPath))
}
}
return dirPaths
}
+605
View File
@@ -1618,6 +1618,611 @@ func TestLoopbackDeleteObjects(t *testing.T) {
}
}
func TestLoopbackListObjects(t *testing.T) {
require, assert := tr.New(t), ta.New(t)
mockStorageMapData := map[BucketName]Bucket{
"aws_s3_mock": {
"test-file-1": MockObject([]Chunk{
[]byte("content of test-file-1"),
}),
"folder1/": MockObject(nil),
"test-file-2": MockObject([]Chunk{
[]byte("content of test-file-2"),
}),
"folder1/file-1": MockObject([]Chunk{
[]byte("content of folder1/file-1"),
}),
"folder1/subfolder/": MockObject(nil),
"folder1/subfolder/file-2": MockObject([]Chunk{
[]byte("content of folder1/subfolder/file-2"),
}),
"folder1/subfolder/nested/file-3": MockObject([]Chunk{
[]byte("content of folder1/subfolder/nested/file-3"),
}),
"folder1/subfolder2/": MockObject(nil),
"folder2/file-4": MockObject([]Chunk{
[]byte("content of folder2/file-4"),
}),
"folder3/": MockObject(nil),
},
"aws_s3_err": {
"test-file": MockObject([]Chunk{
[]byte("test content"),
}),
},
}
plg, err := NewLoopbackPlugin(
WithMockBuckets(mockStorageMapData),
WithMockError(PluginMockError{
BucketName: "aws_s3_err",
ErrMsg: "invalid credentials",
ErrCode: codes.PermissionDenied,
}),
)
assert.NoError(err)
client := NewWrappingPluginStorageClient(plg)
secrets := &structpb.Struct{
Fields: map[string]*structpb.Value{
"AWS_ACCESS_KEY_ID": structpb.NewStringValue("access_key_id"),
"AWS_SECRET_ACCESS_KEY": structpb.NewStringValue("secret_access_key"),
},
}
tests := []struct {
name string
request *plgpb.ListObjectsRequest
expectedErr codes.Code
expectedObjects []*plgpb.Object
}{
{
name: "missing request",
expectedErr: codes.InvalidArgument,
},
{
name: "missing storage bucket",
request: &plgpb.ListObjectsRequest{},
expectedErr: codes.InvalidArgument,
},
{
name: "missing attributes",
request: &plgpb.ListObjectsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
},
},
expectedErr: codes.InvalidArgument,
},
{
name: "bucket not found",
request: &plgpb.ListObjectsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "invalid_bucket",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedErr: codes.NotFound,
},
{
name: "mocked error",
request: &plgpb.ListObjectsRequest{
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_err",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedErr: codes.PermissionDenied,
},
{
name: "list all objects without prefix recursively",
request: &plgpb.ListObjectsRequest{
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "test-file-1", IsDir: false},
{Key: "test-file-2", IsDir: false},
{Key: "folder1/", IsDir: true},
{Key: "folder1/file-1", IsDir: false},
{Key: "folder1/subfolder/", IsDir: true},
{Key: "folder1/subfolder2/", IsDir: true},
{Key: "folder1/subfolder/file-2", IsDir: false},
{Key: "folder1/subfolder/nested/", IsDir: true},
{Key: "folder1/subfolder/nested/file-3", IsDir: false},
{Key: "folder2/", IsDir: true},
{Key: "folder2/file-4", IsDir: false},
{Key: "folder3/", IsDir: true},
},
},
{
name: "list all objects without prefix non-recursively",
request: &plgpb.ListObjectsRequest{
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "test-file-1", IsDir: false},
{Key: "test-file-2", IsDir: false},
{Key: "folder1/", IsDir: true},
{Key: "folder2/", IsDir: true},
{Key: "folder3/", IsDir: true},
},
},
{
name: "list objects with prefix folder1/ recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder1/",
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/file-1", IsDir: false},
{Key: "folder1/subfolder/", IsDir: true},
{Key: "folder1/subfolder2/", IsDir: true},
{Key: "folder1/subfolder/file-2", IsDir: false},
{Key: "folder1/subfolder/nested/", IsDir: true},
{Key: "folder1/subfolder/nested/file-3", IsDir: false},
},
},
{
name: "list objects with prefix folder1/ non-recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder1/",
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/file-1", IsDir: false},
{Key: "folder1/subfolder/", IsDir: true},
{Key: "folder1/subfolder2/", IsDir: true},
},
},
{
name: "list objects with prefix folder1/sub recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder1/sub",
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/subfolder/", IsDir: true},
{Key: "folder1/subfolder2/", IsDir: true},
{Key: "folder1/subfolder/file-2", IsDir: false},
{Key: "folder1/subfolder/nested/", IsDir: true},
{Key: "folder1/subfolder/nested/file-3", IsDir: false},
},
},
{
name: "list objects with prefix folder1/sub non-recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder1/sub",
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/subfolder/", IsDir: true},
{Key: "folder1/subfolder2/", IsDir: true},
},
},
{
name: "list objects with prefix folder3/ recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder3/",
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{},
},
{
name: "list objects with prefix folder3/ non-recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder3/",
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{},
},
{
name: "list objects with prefix folder1/subfolder2 recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder1/subfolder2",
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/subfolder2/", IsDir: true},
},
},
{
name: "list objects with prefix folder1/subfolder2 non-recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder1/subfolder2",
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/subfolder2/", IsDir: true},
},
},
{
name: "list objects with prefix folder1/subfolder2/ recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder1/subfolder2/",
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{},
},
{
name: "list objects with prefix folder1/subfolder2/ non-recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder1/subfolder2/",
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{},
},
{
name: "list objects with non-existent prefix recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "nonexistent/",
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{},
},
{
name: "list objects with non-existent prefix non-recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "nonexistent/",
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{},
},
{
name: "list objects without secrets non-recursively",
request: &plgpb.ListObjectsRequest{
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "test-file-1", IsDir: false},
{Key: "test-file-2", IsDir: false},
{Key: "folder1/", IsDir: true},
{Key: "folder2/", IsDir: true},
{Key: "folder3/", IsDir: true},
},
},
{
name: "list objects without secrets recursively",
request: &plgpb.ListObjectsRequest{
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "test-file-1", IsDir: false},
{Key: "test-file-2", IsDir: false},
{Key: "folder1/", IsDir: true},
{Key: "folder1/file-1", IsDir: false},
{Key: "folder1/subfolder/", IsDir: true},
{Key: "folder1/subfolder2/", IsDir: true},
{Key: "folder1/subfolder/file-2", IsDir: false},
{Key: "folder1/subfolder/nested/", IsDir: true},
{Key: "folder1/subfolder/nested/file-3", IsDir: false},
{Key: "folder2/", IsDir: true},
{Key: "folder2/file-4", IsDir: false},
{Key: "folder3/", IsDir: true},
},
},
{
name: "list objects with bucket prefix and no key prefix recursively",
request: &plgpb.ListObjectsRequest{
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
BucketPrefix: "folder1/",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/file-1", IsDir: false},
{Key: "folder1/subfolder/", IsDir: true},
{Key: "folder1/subfolder2/", IsDir: true},
{Key: "folder1/subfolder/file-2", IsDir: false},
{Key: "folder1/subfolder/nested/", IsDir: true},
{Key: "folder1/subfolder/nested/file-3", IsDir: false},
},
},
{
name: "list objects with bucket prefix and no key prefix non-recursively",
request: &plgpb.ListObjectsRequest{
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
BucketPrefix: "folder1/",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/file-1", IsDir: false},
{Key: "folder1/subfolder/", IsDir: true},
{Key: "folder1/subfolder2/", IsDir: true},
},
},
{
name: "list objects with bucket prefix and key prefix recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "subfolder/",
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
BucketPrefix: "folder1/",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/subfolder/file-2", IsDir: false},
{Key: "folder1/subfolder/nested/", IsDir: true},
{Key: "folder1/subfolder/nested/file-3", IsDir: false},
},
},
{
name: "list objects with bucket prefix and key prefix non-recursively",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "subfolder/",
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
BucketPrefix: "folder1/",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/subfolder/file-2", IsDir: false},
{Key: "folder1/subfolder/nested/", IsDir: true},
},
},
{
name: "list objects with bucket prefix no trailing slash and key prefix with trailing slash",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "subfolder/",
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
BucketPrefix: "folder1",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/subfolder/file-2", IsDir: false},
{Key: "folder1/subfolder/nested/", IsDir: true},
{Key: "folder1/subfolder/nested/file-3", IsDir: false},
},
},
{
name: "list objects with bucket prefix empty and key prefix folder1/",
request: &plgpb.ListObjectsRequest{
KeyPrefix: "folder1/",
Recursive: false,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
BucketPrefix: "",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{
{Key: "folder1/file-1", IsDir: false},
{Key: "folder1/subfolder/", IsDir: true},
{Key: "folder1/subfolder2/", IsDir: true},
},
},
{
name: "list objects with bucket prefix filter matching nothing",
request: &plgpb.ListObjectsRequest{
Recursive: true,
Bucket: &storagebuckets.StorageBucket{
BucketName: "aws_s3_mock",
BucketPrefix: "nonexistent/",
Secrets: secrets,
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"endpoint": structpb.NewStringValue("0.0.0.0"),
},
},
},
},
expectedObjects: []*plgpb.Object{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := client.ListObjects(context.Background(), tt.request)
if tt.expectedErr != codes.OK {
assert.Error(err)
assert.Nil(resp)
s, ok := status.FromError(err)
require.True(ok, "invalid error type")
assert.Equal(tt.expectedErr, s.Code())
return
}
assert.NoError(err)
require.NotNil(resp)
require.Equal(len(tt.expectedObjects), len(resp.Objects), "object count mismatch")
assert.ElementsMatch(tt.expectedObjects, resp.Objects, "object list mismatch")
})
}
}
func TestLoopbackStoragePlugin(t *testing.T) {
td := t.TempDir()
@@ -45,17 +45,20 @@ service StoragePluginService {
// deleted.
rpc OnDeleteStorageBucket(OnDeleteStorageBucketRequest) returns (OnDeleteStorageBucketResponse);
// ValidatePermissions is a hook that checks if the secrets associated with
// ValidatePermissions checks if the secrets associated with
// the storage bucket meet the requirements of the plugin.
rpc ValidatePermissions(ValidatePermissionsRequest) returns (ValidatePermissionsResponse);
// HeadObject is a hook that retrieves metadata about an object.
// HeadObject returns metadata about an object.
rpc HeadObject(HeadObjectRequest) returns (HeadObjectResponse);
// GetObject is a hook that retrieves objects.
// GetObject returns object requested.
rpc GetObject(GetObjectRequest) returns (stream GetObjectResponse);
// PutObject is a hook that reads a file stored on local disk and
// ListObjects returns a list of objects within a storage bucket that matches the provided prefix.
rpc ListObjects(ListObjectsRequest) returns (ListObjectsResponse);
// PutObject reads a file stored on local disk and
// stores it to an external object store.
rpc PutObject(PutObjectRequest) returns (PutObjectResponse);
@@ -157,6 +160,30 @@ message GetObjectResponse {
bytes file_chunk = 10;
}
message ListObjectsRequest {
// Required. The existing state of the storage bucket.
controller.api.resources.storagebuckets.v1.StorageBucket bucket = 10;
// Optional. The key prefix used to scope the object search.
string key_prefix = 20;
// Optional. Flag to recursively list all objects under a given prefix.
bool recursive = 30;
}
message Object {
// The object key defined within the object storage.
string key = 10;
// Informs whether or not the object is a directory.
bool is_dir = 20;
}
message ListObjectsResponse {
// A list of objects found under a provided prefix.
repeated Object objects = 10;
}
message PutObjectRequest {
// Required. The existing state of the storage bucket.
controller.api.resources.storagebuckets.v1.StorageBucket bucket = 10;
+267 -84
View File
@@ -769,6 +769,168 @@ func (x *GetObjectResponse) GetFileChunk() []byte {
return nil
}
type ListObjectsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The existing state of the storage bucket.
Bucket *storagebuckets.StorageBucket `protobuf:"bytes,10,opt,name=bucket,proto3" json:"bucket,omitempty"`
// Optional. The key prefix used to scope the object search.
KeyPrefix string `protobuf:"bytes,20,opt,name=key_prefix,json=keyPrefix,proto3" json:"key_prefix,omitempty"`
// Optional. Flag to recursively list all objects under a given prefix.
Recursive bool `protobuf:"varint,30,opt,name=recursive,proto3" json:"recursive,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListObjectsRequest) Reset() {
*x = ListObjectsRequest{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListObjectsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListObjectsRequest) ProtoMessage() {}
func (x *ListObjectsRequest) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListObjectsRequest.ProtoReflect.Descriptor instead.
func (*ListObjectsRequest) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{14}
}
func (x *ListObjectsRequest) GetBucket() *storagebuckets.StorageBucket {
if x != nil {
return x.Bucket
}
return nil
}
func (x *ListObjectsRequest) GetKeyPrefix() string {
if x != nil {
return x.KeyPrefix
}
return ""
}
func (x *ListObjectsRequest) GetRecursive() bool {
if x != nil {
return x.Recursive
}
return false
}
type Object struct {
state protoimpl.MessageState `protogen:"open.v1"`
// The object key defined within the object storage.
Key string `protobuf:"bytes,10,opt,name=key,proto3" json:"key,omitempty"`
// Informs whether or not the object is a directory.
IsDir bool `protobuf:"varint,20,opt,name=is_dir,json=isDir,proto3" json:"is_dir,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Object) Reset() {
*x = Object{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Object) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Object) ProtoMessage() {}
func (x *Object) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Object.ProtoReflect.Descriptor instead.
func (*Object) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{15}
}
func (x *Object) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Object) GetIsDir() bool {
if x != nil {
return x.IsDir
}
return false
}
type ListObjectsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// A list of objects found under a provided prefix.
Objects []*Object `protobuf:"bytes,10,rep,name=objects,proto3" json:"objects,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListObjectsResponse) Reset() {
*x = ListObjectsResponse{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListObjectsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListObjectsResponse) ProtoMessage() {}
func (x *ListObjectsResponse) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListObjectsResponse.ProtoReflect.Descriptor instead.
func (*ListObjectsResponse) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{16}
}
func (x *ListObjectsResponse) GetObjects() []*Object {
if x != nil {
return x.Objects
}
return nil
}
type PutObjectRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Required. The existing state of the storage bucket.
@@ -783,7 +945,7 @@ type PutObjectRequest struct {
func (x *PutObjectRequest) Reset() {
*x = PutObjectRequest{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[14]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -795,7 +957,7 @@ func (x *PutObjectRequest) String() string {
func (*PutObjectRequest) ProtoMessage() {}
func (x *PutObjectRequest) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[14]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -808,7 +970,7 @@ func (x *PutObjectRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use PutObjectRequest.ProtoReflect.Descriptor instead.
func (*PutObjectRequest) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{14}
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{17}
}
func (x *PutObjectRequest) GetBucket() *storagebuckets.StorageBucket {
@@ -842,7 +1004,7 @@ type PutObjectResponse struct {
func (x *PutObjectResponse) Reset() {
*x = PutObjectResponse{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[15]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -854,7 +1016,7 @@ func (x *PutObjectResponse) String() string {
func (*PutObjectResponse) ProtoMessage() {}
func (x *PutObjectResponse) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[15]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -867,7 +1029,7 @@ func (x *PutObjectResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use PutObjectResponse.ProtoReflect.Descriptor instead.
func (*PutObjectResponse) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{15}
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{18}
}
func (x *PutObjectResponse) GetChecksumSha_256() []byte {
@@ -901,7 +1063,7 @@ type DeleteObjectsRequest struct {
func (x *DeleteObjectsRequest) Reset() {
*x = DeleteObjectsRequest{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[16]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -913,7 +1075,7 @@ func (x *DeleteObjectsRequest) String() string {
func (*DeleteObjectsRequest) ProtoMessage() {}
func (x *DeleteObjectsRequest) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[16]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -926,7 +1088,7 @@ func (x *DeleteObjectsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteObjectsRequest.ProtoReflect.Descriptor instead.
func (*DeleteObjectsRequest) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{16}
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{19}
}
func (x *DeleteObjectsRequest) GetBucket() *storagebuckets.StorageBucket {
@@ -964,7 +1126,7 @@ type DeleteObjectsResponse struct {
func (x *DeleteObjectsResponse) Reset() {
*x = DeleteObjectsResponse{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[17]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -976,7 +1138,7 @@ func (x *DeleteObjectsResponse) String() string {
func (*DeleteObjectsResponse) ProtoMessage() {}
func (x *DeleteObjectsResponse) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[17]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -989,7 +1151,7 @@ func (x *DeleteObjectsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use DeleteObjectsResponse.ProtoReflect.Descriptor instead.
func (*DeleteObjectsResponse) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{17}
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{20}
}
func (x *DeleteObjectsResponse) GetObjectsDeleted() uint32 {
@@ -1013,7 +1175,7 @@ type Permission struct {
func (x *Permission) Reset() {
*x = Permission{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[18]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1025,7 +1187,7 @@ func (x *Permission) String() string {
func (*Permission) ProtoMessage() {}
func (x *Permission) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[18]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1038,7 +1200,7 @@ func (x *Permission) ProtoReflect() protoreflect.Message {
// Deprecated: Use Permission.ProtoReflect.Descriptor instead.
func (*Permission) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{18}
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{21}
}
func (x *Permission) GetState() StateType {
@@ -1076,7 +1238,7 @@ type Permissions struct {
func (x *Permissions) Reset() {
*x = Permissions{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[19]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1088,7 +1250,7 @@ func (x *Permissions) String() string {
func (*Permissions) ProtoMessage() {}
func (x *Permissions) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[19]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1101,7 +1263,7 @@ func (x *Permissions) ProtoReflect() protoreflect.Message {
// Deprecated: Use Permissions.ProtoReflect.Descriptor instead.
func (*Permissions) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{19}
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{22}
}
func (x *Permissions) GetWrite() *Permission {
@@ -1137,7 +1299,7 @@ type StorageBucketCredentialState struct {
func (x *StorageBucketCredentialState) Reset() {
*x = StorageBucketCredentialState{}
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[20]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1149,7 +1311,7 @@ func (x *StorageBucketCredentialState) String() string {
func (*StorageBucketCredentialState) ProtoMessage() {}
func (x *StorageBucketCredentialState) ProtoReflect() protoreflect.Message {
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[20]
mi := &file_plugin_v1_storage_plugin_service_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1162,7 +1324,7 @@ func (x *StorageBucketCredentialState) ProtoReflect() protoreflect.Message {
// Deprecated: Use StorageBucketCredentialState.ProtoReflect.Descriptor instead.
func (*StorageBucketCredentialState) Descriptor() ([]byte, []int) {
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{20}
return file_plugin_v1_storage_plugin_service_proto_rawDescGZIP(), []int{23}
}
func (x *StorageBucketCredentialState) GetState() *Permissions {
@@ -1236,7 +1398,20 @@ const file_plugin_v1_storage_plugin_service_proto_rawDesc = "" +
"\x11GetObjectResponse\x12\x1d\n" +
"\n" +
"file_chunk\x18\n" +
" \x01(\fR\tfileChunk\"\x8b\x01\n" +
" \x01(\fR\tfileChunk\"\xa4\x01\n" +
"\x12ListObjectsRequest\x12Q\n" +
"\x06bucket\x18\n" +
" \x01(\v29.controller.api.resources.storagebuckets.v1.StorageBucketR\x06bucket\x12\x1d\n" +
"\n" +
"key_prefix\x18\x14 \x01(\tR\tkeyPrefix\x12\x1c\n" +
"\trecursive\x18\x1e \x01(\bR\trecursive\"1\n" +
"\x06Object\x12\x10\n" +
"\x03key\x18\n" +
" \x01(\tR\x03key\x12\x15\n" +
"\x06is_dir\x18\x14 \x01(\bR\x05isDir\"B\n" +
"\x13ListObjectsResponse\x12+\n" +
"\aobjects\x18\n" +
" \x03(\v2\x11.plugin.v1.ObjectR\aobjects\"\x8b\x01\n" +
"\x10PutObjectRequest\x12Q\n" +
"\x06bucket\x18\n" +
" \x01(\v29.controller.api.resources.storagebuckets.v1.StorageBucketR\x06bucket\x12\x10\n" +
@@ -1274,7 +1449,7 @@ const file_plugin_v1_storage_plugin_service_proto_rawDesc = "" +
"\x16STATE_TYPE_UNSPECIFIED\x10\x00\x12\x11\n" +
"\rSTATE_TYPE_OK\x10\x01\x12\x14\n" +
"\x10STATE_TYPE_ERROR\x10\x02\x12\x16\n" +
"\x12STATE_TYPE_UNKNOWN\x10\x032\xec\x06\n" +
"\x12STATE_TYPE_UNKNOWN\x10\x032\xba\a\n" +
"\x14StoragePluginService\x12y\n" +
"\x1aNormalizeStorageBucketData\x12,.plugin.v1.NormalizeStorageBucketDataRequest\x1a-.plugin.v1.NormalizeStorageBucketDataResponse\x12j\n" +
"\x15OnCreateStorageBucket\x12'.plugin.v1.OnCreateStorageBucketRequest\x1a(.plugin.v1.OnCreateStorageBucketResponse\x12j\n" +
@@ -1283,7 +1458,8 @@ const file_plugin_v1_storage_plugin_service_proto_rawDesc = "" +
"\x13ValidatePermissions\x12%.plugin.v1.ValidatePermissionsRequest\x1a&.plugin.v1.ValidatePermissionsResponse\x12I\n" +
"\n" +
"HeadObject\x12\x1c.plugin.v1.HeadObjectRequest\x1a\x1d.plugin.v1.HeadObjectResponse\x12H\n" +
"\tGetObject\x12\x1b.plugin.v1.GetObjectRequest\x1a\x1c.plugin.v1.GetObjectResponse0\x01\x12F\n" +
"\tGetObject\x12\x1b.plugin.v1.GetObjectRequest\x1a\x1c.plugin.v1.GetObjectResponse0\x01\x12L\n" +
"\vListObjects\x12\x1d.plugin.v1.ListObjectsRequest\x1a\x1e.plugin.v1.ListObjectsResponse\x12F\n" +
"\tPutObject\x12\x1b.plugin.v1.PutObjectRequest\x1a\x1c.plugin.v1.PutObjectResponse\x12R\n" +
"\rDeleteObjects\x12\x1f.plugin.v1.DeleteObjectsRequest\x1a .plugin.v1.DeleteObjectsResponseB5Z3github.com/hashicorp/boundary/sdk/pbs/plugin;pluginb\x06proto3"
@@ -1300,7 +1476,7 @@ func file_plugin_v1_storage_plugin_service_proto_rawDescGZIP() []byte {
}
var file_plugin_v1_storage_plugin_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_plugin_v1_storage_plugin_service_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
var file_plugin_v1_storage_plugin_service_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
var file_plugin_v1_storage_plugin_service_proto_goTypes = []any{
(StateType)(0), // 0: plugin.v1.StateType
(*NormalizeStorageBucketDataRequest)(nil), // 1: plugin.v1.NormalizeStorageBucketDataRequest
@@ -1317,66 +1493,73 @@ var file_plugin_v1_storage_plugin_service_proto_goTypes = []any{
(*HeadObjectResponse)(nil), // 12: plugin.v1.HeadObjectResponse
(*GetObjectRequest)(nil), // 13: plugin.v1.GetObjectRequest
(*GetObjectResponse)(nil), // 14: plugin.v1.GetObjectResponse
(*PutObjectRequest)(nil), // 15: plugin.v1.PutObjectRequest
(*PutObjectResponse)(nil), // 16: plugin.v1.PutObjectResponse
(*DeleteObjectsRequest)(nil), // 17: plugin.v1.DeleteObjectsRequest
(*DeleteObjectsResponse)(nil), // 18: plugin.v1.DeleteObjectsResponse
(*Permission)(nil), // 19: plugin.v1.Permission
(*Permissions)(nil), // 20: plugin.v1.Permissions
(*StorageBucketCredentialState)(nil), // 21: plugin.v1.StorageBucketCredentialState
(*structpb.Struct)(nil), // 22: google.protobuf.Struct
(*plugins.PluginInfo)(nil), // 23: controller.api.resources.plugins.v1.PluginInfo
(*storagebuckets.StorageBucket)(nil), // 24: controller.api.resources.storagebuckets.v1.StorageBucket
(*storagebuckets.StorageBucketPersisted)(nil), // 25: controller.api.resources.storagebuckets.v1.StorageBucketPersisted
(*timestamppb.Timestamp)(nil), // 26: google.protobuf.Timestamp
(*ListObjectsRequest)(nil), // 15: plugin.v1.ListObjectsRequest
(*Object)(nil), // 16: plugin.v1.Object
(*ListObjectsResponse)(nil), // 17: plugin.v1.ListObjectsResponse
(*PutObjectRequest)(nil), // 18: plugin.v1.PutObjectRequest
(*PutObjectResponse)(nil), // 19: plugin.v1.PutObjectResponse
(*DeleteObjectsRequest)(nil), // 20: plugin.v1.DeleteObjectsRequest
(*DeleteObjectsResponse)(nil), // 21: plugin.v1.DeleteObjectsResponse
(*Permission)(nil), // 22: plugin.v1.Permission
(*Permissions)(nil), // 23: plugin.v1.Permissions
(*StorageBucketCredentialState)(nil), // 24: plugin.v1.StorageBucketCredentialState
(*structpb.Struct)(nil), // 25: google.protobuf.Struct
(*plugins.PluginInfo)(nil), // 26: controller.api.resources.plugins.v1.PluginInfo
(*storagebuckets.StorageBucket)(nil), // 27: controller.api.resources.storagebuckets.v1.StorageBucket
(*storagebuckets.StorageBucketPersisted)(nil), // 28: controller.api.resources.storagebuckets.v1.StorageBucketPersisted
(*timestamppb.Timestamp)(nil), // 29: google.protobuf.Timestamp
}
var file_plugin_v1_storage_plugin_service_proto_depIdxs = []int32{
22, // 0: plugin.v1.NormalizeStorageBucketDataRequest.attributes:type_name -> google.protobuf.Struct
23, // 1: plugin.v1.NormalizeStorageBucketDataRequest.plugin:type_name -> controller.api.resources.plugins.v1.PluginInfo
22, // 2: plugin.v1.NormalizeStorageBucketDataResponse.attributes:type_name -> google.protobuf.Struct
24, // 3: plugin.v1.OnCreateStorageBucketRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
25, // 4: plugin.v1.OnCreateStorageBucketResponse.persisted:type_name -> controller.api.resources.storagebuckets.v1.StorageBucketPersisted
24, // 5: plugin.v1.OnUpdateStorageBucketRequest.current_bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
24, // 6: plugin.v1.OnUpdateStorageBucketRequest.new_bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
25, // 7: plugin.v1.OnUpdateStorageBucketRequest.persisted:type_name -> controller.api.resources.storagebuckets.v1.StorageBucketPersisted
25, // 8: plugin.v1.OnUpdateStorageBucketResponse.persisted:type_name -> controller.api.resources.storagebuckets.v1.StorageBucketPersisted
24, // 9: plugin.v1.OnDeleteStorageBucketRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
25, // 10: plugin.v1.OnDeleteStorageBucketRequest.persisted:type_name -> controller.api.resources.storagebuckets.v1.StorageBucketPersisted
24, // 11: plugin.v1.ValidatePermissionsRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
24, // 12: plugin.v1.HeadObjectRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
26, // 13: plugin.v1.HeadObjectResponse.last_modified:type_name -> google.protobuf.Timestamp
24, // 14: plugin.v1.GetObjectRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
24, // 15: plugin.v1.PutObjectRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
24, // 16: plugin.v1.DeleteObjectsRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
0, // 17: plugin.v1.Permission.state:type_name -> plugin.v1.StateType
26, // 18: plugin.v1.Permission.checked_at:type_name -> google.protobuf.Timestamp
19, // 19: plugin.v1.Permissions.write:type_name -> plugin.v1.Permission
19, // 20: plugin.v1.Permissions.read:type_name -> plugin.v1.Permission
19, // 21: plugin.v1.Permissions.delete:type_name -> plugin.v1.Permission
20, // 22: plugin.v1.StorageBucketCredentialState.state:type_name -> plugin.v1.Permissions
1, // 23: plugin.v1.StoragePluginService.NormalizeStorageBucketData:input_type -> plugin.v1.NormalizeStorageBucketDataRequest
3, // 24: plugin.v1.StoragePluginService.OnCreateStorageBucket:input_type -> plugin.v1.OnCreateStorageBucketRequest
5, // 25: plugin.v1.StoragePluginService.OnUpdateStorageBucket:input_type -> plugin.v1.OnUpdateStorageBucketRequest
7, // 26: plugin.v1.StoragePluginService.OnDeleteStorageBucket:input_type -> plugin.v1.OnDeleteStorageBucketRequest
9, // 27: plugin.v1.StoragePluginService.ValidatePermissions:input_type -> plugin.v1.ValidatePermissionsRequest
11, // 28: plugin.v1.StoragePluginService.HeadObject:input_type -> plugin.v1.HeadObjectRequest
13, // 29: plugin.v1.StoragePluginService.GetObject:input_type -> plugin.v1.GetObjectRequest
15, // 30: plugin.v1.StoragePluginService.PutObject:input_type -> plugin.v1.PutObjectRequest
17, // 31: plugin.v1.StoragePluginService.DeleteObjects:input_type -> plugin.v1.DeleteObjectsRequest
2, // 32: plugin.v1.StoragePluginService.NormalizeStorageBucketData:output_type -> plugin.v1.NormalizeStorageBucketDataResponse
4, // 33: plugin.v1.StoragePluginService.OnCreateStorageBucket:output_type -> plugin.v1.OnCreateStorageBucketResponse
6, // 34: plugin.v1.StoragePluginService.OnUpdateStorageBucket:output_type -> plugin.v1.OnUpdateStorageBucketResponse
8, // 35: plugin.v1.StoragePluginService.OnDeleteStorageBucket:output_type -> plugin.v1.OnDeleteStorageBucketResponse
10, // 36: plugin.v1.StoragePluginService.ValidatePermissions:output_type -> plugin.v1.ValidatePermissionsResponse
12, // 37: plugin.v1.StoragePluginService.HeadObject:output_type -> plugin.v1.HeadObjectResponse
14, // 38: plugin.v1.StoragePluginService.GetObject:output_type -> plugin.v1.GetObjectResponse
16, // 39: plugin.v1.StoragePluginService.PutObject:output_type -> plugin.v1.PutObjectResponse
18, // 40: plugin.v1.StoragePluginService.DeleteObjects:output_type -> plugin.v1.DeleteObjectsResponse
32, // [32:41] is the sub-list for method output_type
23, // [23:32] is the sub-list for method input_type
23, // [23:23] is the sub-list for extension type_name
23, // [23:23] is the sub-list for extension extendee
0, // [0:23] is the sub-list for field type_name
25, // 0: plugin.v1.NormalizeStorageBucketDataRequest.attributes:type_name -> google.protobuf.Struct
26, // 1: plugin.v1.NormalizeStorageBucketDataRequest.plugin:type_name -> controller.api.resources.plugins.v1.PluginInfo
25, // 2: plugin.v1.NormalizeStorageBucketDataResponse.attributes:type_name -> google.protobuf.Struct
27, // 3: plugin.v1.OnCreateStorageBucketRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
28, // 4: plugin.v1.OnCreateStorageBucketResponse.persisted:type_name -> controller.api.resources.storagebuckets.v1.StorageBucketPersisted
27, // 5: plugin.v1.OnUpdateStorageBucketRequest.current_bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
27, // 6: plugin.v1.OnUpdateStorageBucketRequest.new_bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
28, // 7: plugin.v1.OnUpdateStorageBucketRequest.persisted:type_name -> controller.api.resources.storagebuckets.v1.StorageBucketPersisted
28, // 8: plugin.v1.OnUpdateStorageBucketResponse.persisted:type_name -> controller.api.resources.storagebuckets.v1.StorageBucketPersisted
27, // 9: plugin.v1.OnDeleteStorageBucketRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
28, // 10: plugin.v1.OnDeleteStorageBucketRequest.persisted:type_name -> controller.api.resources.storagebuckets.v1.StorageBucketPersisted
27, // 11: plugin.v1.ValidatePermissionsRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
27, // 12: plugin.v1.HeadObjectRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
29, // 13: plugin.v1.HeadObjectResponse.last_modified:type_name -> google.protobuf.Timestamp
27, // 14: plugin.v1.GetObjectRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
27, // 15: plugin.v1.ListObjectsRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
16, // 16: plugin.v1.ListObjectsResponse.objects:type_name -> plugin.v1.Object
27, // 17: plugin.v1.PutObjectRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
27, // 18: plugin.v1.DeleteObjectsRequest.bucket:type_name -> controller.api.resources.storagebuckets.v1.StorageBucket
0, // 19: plugin.v1.Permission.state:type_name -> plugin.v1.StateType
29, // 20: plugin.v1.Permission.checked_at:type_name -> google.protobuf.Timestamp
22, // 21: plugin.v1.Permissions.write:type_name -> plugin.v1.Permission
22, // 22: plugin.v1.Permissions.read:type_name -> plugin.v1.Permission
22, // 23: plugin.v1.Permissions.delete:type_name -> plugin.v1.Permission
23, // 24: plugin.v1.StorageBucketCredentialState.state:type_name -> plugin.v1.Permissions
1, // 25: plugin.v1.StoragePluginService.NormalizeStorageBucketData:input_type -> plugin.v1.NormalizeStorageBucketDataRequest
3, // 26: plugin.v1.StoragePluginService.OnCreateStorageBucket:input_type -> plugin.v1.OnCreateStorageBucketRequest
5, // 27: plugin.v1.StoragePluginService.OnUpdateStorageBucket:input_type -> plugin.v1.OnUpdateStorageBucketRequest
7, // 28: plugin.v1.StoragePluginService.OnDeleteStorageBucket:input_type -> plugin.v1.OnDeleteStorageBucketRequest
9, // 29: plugin.v1.StoragePluginService.ValidatePermissions:input_type -> plugin.v1.ValidatePermissionsRequest
11, // 30: plugin.v1.StoragePluginService.HeadObject:input_type -> plugin.v1.HeadObjectRequest
13, // 31: plugin.v1.StoragePluginService.GetObject:input_type -> plugin.v1.GetObjectRequest
15, // 32: plugin.v1.StoragePluginService.ListObjects:input_type -> plugin.v1.ListObjectsRequest
18, // 33: plugin.v1.StoragePluginService.PutObject:input_type -> plugin.v1.PutObjectRequest
20, // 34: plugin.v1.StoragePluginService.DeleteObjects:input_type -> plugin.v1.DeleteObjectsRequest
2, // 35: plugin.v1.StoragePluginService.NormalizeStorageBucketData:output_type -> plugin.v1.NormalizeStorageBucketDataResponse
4, // 36: plugin.v1.StoragePluginService.OnCreateStorageBucket:output_type -> plugin.v1.OnCreateStorageBucketResponse
6, // 37: plugin.v1.StoragePluginService.OnUpdateStorageBucket:output_type -> plugin.v1.OnUpdateStorageBucketResponse
8, // 38: plugin.v1.StoragePluginService.OnDeleteStorageBucket:output_type -> plugin.v1.OnDeleteStorageBucketResponse
10, // 39: plugin.v1.StoragePluginService.ValidatePermissions:output_type -> plugin.v1.ValidatePermissionsResponse
12, // 40: plugin.v1.StoragePluginService.HeadObject:output_type -> plugin.v1.HeadObjectResponse
14, // 41: plugin.v1.StoragePluginService.GetObject:output_type -> plugin.v1.GetObjectResponse
17, // 42: plugin.v1.StoragePluginService.ListObjects:output_type -> plugin.v1.ListObjectsResponse
19, // 43: plugin.v1.StoragePluginService.PutObject:output_type -> plugin.v1.PutObjectResponse
21, // 44: plugin.v1.StoragePluginService.DeleteObjects:output_type -> plugin.v1.DeleteObjectsResponse
35, // [35:45] is the sub-list for method output_type
25, // [25:35] is the sub-list for method input_type
25, // [25:25] is the sub-list for extension type_name
25, // [25:25] is the sub-list for extension extendee
0, // [0:25] is the sub-list for field type_name
}
func init() { file_plugin_v1_storage_plugin_service_proto_init() }
@@ -1390,7 +1573,7 @@ func file_plugin_v1_storage_plugin_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_plugin_v1_storage_plugin_service_proto_rawDesc), len(file_plugin_v1_storage_plugin_service_proto_rawDesc)),
NumEnums: 1,
NumMessages: 21,
NumMessages: 24,
NumExtensions: 0,
NumServices: 1,
},
@@ -29,6 +29,7 @@ const (
StoragePluginService_ValidatePermissions_FullMethodName = "/plugin.v1.StoragePluginService/ValidatePermissions"
StoragePluginService_HeadObject_FullMethodName = "/plugin.v1.StoragePluginService/HeadObject"
StoragePluginService_GetObject_FullMethodName = "/plugin.v1.StoragePluginService/GetObject"
StoragePluginService_ListObjects_FullMethodName = "/plugin.v1.StoragePluginService/ListObjects"
StoragePluginService_PutObject_FullMethodName = "/plugin.v1.StoragePluginService/PutObject"
StoragePluginService_DeleteObjects_FullMethodName = "/plugin.v1.StoragePluginService/DeleteObjects"
)
@@ -66,14 +67,16 @@ type StoragePluginServiceClient interface {
// OnDeleteStorageBucket is a hook that runs when a storage bucket is
// deleted.
OnDeleteStorageBucket(ctx context.Context, in *OnDeleteStorageBucketRequest, opts ...grpc.CallOption) (*OnDeleteStorageBucketResponse, error)
// ValidatePermissions is a hook that checks if the secrets associated with
// ValidatePermissions checks if the secrets associated with
// the storage bucket meet the requirements of the plugin.
ValidatePermissions(ctx context.Context, in *ValidatePermissionsRequest, opts ...grpc.CallOption) (*ValidatePermissionsResponse, error)
// HeadObject is a hook that retrieves metadata about an object.
// HeadObject returns metadata about an object.
HeadObject(ctx context.Context, in *HeadObjectRequest, opts ...grpc.CallOption) (*HeadObjectResponse, error)
// GetObject is a hook that retrieves objects.
// GetObject returns object requested.
GetObject(ctx context.Context, in *GetObjectRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetObjectResponse], error)
// PutObject is a hook that reads a file stored on local disk and
// ListObjects returns a list of objects within a storage bucket that matches the provided prefix.
ListObjects(ctx context.Context, in *ListObjectsRequest, opts ...grpc.CallOption) (*ListObjectsResponse, error)
// PutObject reads a file stored on local disk and
// stores it to an external object store.
PutObject(ctx context.Context, in *PutObjectRequest, opts ...grpc.CallOption) (*PutObjectResponse, error)
// DeleteObjects deletes one or many files in an external object store
@@ -168,6 +171,16 @@ func (c *storagePluginServiceClient) GetObject(ctx context.Context, in *GetObjec
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StoragePluginService_GetObjectClient = grpc.ServerStreamingClient[GetObjectResponse]
func (c *storagePluginServiceClient) ListObjects(ctx context.Context, in *ListObjectsRequest, opts ...grpc.CallOption) (*ListObjectsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListObjectsResponse)
err := c.cc.Invoke(ctx, StoragePluginService_ListObjects_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *storagePluginServiceClient) PutObject(ctx context.Context, in *PutObjectRequest, opts ...grpc.CallOption) (*PutObjectResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(PutObjectResponse)
@@ -221,14 +234,16 @@ type StoragePluginServiceServer interface {
// OnDeleteStorageBucket is a hook that runs when a storage bucket is
// deleted.
OnDeleteStorageBucket(context.Context, *OnDeleteStorageBucketRequest) (*OnDeleteStorageBucketResponse, error)
// ValidatePermissions is a hook that checks if the secrets associated with
// ValidatePermissions checks if the secrets associated with
// the storage bucket meet the requirements of the plugin.
ValidatePermissions(context.Context, *ValidatePermissionsRequest) (*ValidatePermissionsResponse, error)
// HeadObject is a hook that retrieves metadata about an object.
// HeadObject returns metadata about an object.
HeadObject(context.Context, *HeadObjectRequest) (*HeadObjectResponse, error)
// GetObject is a hook that retrieves objects.
// GetObject returns object requested.
GetObject(*GetObjectRequest, grpc.ServerStreamingServer[GetObjectResponse]) error
// PutObject is a hook that reads a file stored on local disk and
// ListObjects returns a list of objects within a storage bucket that matches the provided prefix.
ListObjects(context.Context, *ListObjectsRequest) (*ListObjectsResponse, error)
// PutObject reads a file stored on local disk and
// stores it to an external object store.
PutObject(context.Context, *PutObjectRequest) (*PutObjectResponse, error)
// DeleteObjects deletes one or many files in an external object store
@@ -265,6 +280,9 @@ func (UnimplementedStoragePluginServiceServer) HeadObject(context.Context, *Head
func (UnimplementedStoragePluginServiceServer) GetObject(*GetObjectRequest, grpc.ServerStreamingServer[GetObjectResponse]) error {
return status.Error(codes.Unimplemented, "method GetObject not implemented")
}
func (UnimplementedStoragePluginServiceServer) ListObjects(context.Context, *ListObjectsRequest) (*ListObjectsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListObjects not implemented")
}
func (UnimplementedStoragePluginServiceServer) PutObject(context.Context, *PutObjectRequest) (*PutObjectResponse, error) {
return nil, status.Error(codes.Unimplemented, "method PutObject not implemented")
}
@@ -411,6 +429,24 @@ func _StoragePluginService_GetObject_Handler(srv interface{}, stream grpc.Server
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StoragePluginService_GetObjectServer = grpc.ServerStreamingServer[GetObjectResponse]
func _StoragePluginService_ListObjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListObjectsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StoragePluginServiceServer).ListObjects(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StoragePluginService_ListObjects_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StoragePluginServiceServer).ListObjects(ctx, req.(*ListObjectsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StoragePluginService_PutObject_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(PutObjectRequest)
if err := dec(in); err != nil {
@@ -478,6 +514,10 @@ var StoragePluginService_ServiceDesc = grpc.ServiceDesc{
MethodName: "HeadObject",
Handler: _StoragePluginService_HeadObject_Handler,
},
{
MethodName: "ListObjects",
Handler: _StoragePluginService_ListObjects_Handler,
},
{
MethodName: "PutObject",
Handler: _StoragePluginService_PutObject_Handler,