From a2235d93ce8ca05e24ab5c5e856742534872fb94 Mon Sep 17 00:00:00 2001 From: Hugo <10965479+hugoghx@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:50:17 +0000 Subject: [PATCH] feat(plugin): Implement HeadObject --- plugin/service/storage/plugin.go | 43 +++++- plugin/service/storage/plugin_test.go | 187 +++++++++++++++++++++++++- 2 files changed, 228 insertions(+), 2 deletions(-) diff --git a/plugin/service/storage/plugin.go b/plugin/service/storage/plugin.go index 911c06a..258ccf7 100644 --- a/plugin/service/storage/plugin.go +++ b/plugin/service/storage/plugin.go @@ -18,6 +18,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" ) var _ pb.StoragePluginServiceServer = (*StoragePlugin)(nil) @@ -196,7 +197,47 @@ func (sp *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.Valida // HeadObject is a hook that retrieves metadata about an object. func (sp *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectRequest) (*pb.HeadObjectResponse, error) { - return nil, fmt.Errorf("unimplemented") + bucket := req.GetBucket() + if bucket == nil { + return nil, status.Error(codes.InvalidArgument, "no storage bucket information found") + } + if bucket.GetBucketName() == "" { + return nil, status.Error(codes.InvalidArgument, "storage bucket name is required") + } + + if req.GetKey() == "" { + return nil, status.Error(codes.InvalidArgument, "object key is required") + } + + sa, err := getStorageAttributes(bucket.GetAttributes()) + if err != nil { + return nil, err // getStorageAttributes already returns a "status"-type error. + } + + sec, err := getStorageSecrets(bucket.GetSecrets()) + if err != nil { + return nil, err + } + + cl, err := minio.New(sa.EndpointUrl, &minio.Options{ + Creds: credentials.NewStaticV4(sec.AccessKeyId, sec.SecretAccessKey, ""), + Secure: sa.UseSSL, + Region: sa.Region, + }) + if err != nil { + return nil, status.Errorf(codes.Unknown, "failed to create minio sdk client: %v", err) + } + + objKey := path.Join(bucket.GetBucketPrefix(), req.GetKey()) + oi, err := cl.StatObject(ctx, bucket.GetBucketName(), objKey, minio.StatObjectOptions{}) + if err != nil { + return nil, status.Errorf(codes.Unknown, "failed to stat object: %v", err) + } + + return &pb.HeadObjectResponse{ + ContentLength: oi.Size, + LastModified: timestamppb.New(oi.LastModified), + }, nil } // GetObject is a hook that retrieves objects. diff --git a/plugin/service/storage/plugin_test.go b/plugin/service/storage/plugin_test.go index 686b5db..43ac7f5 100644 --- a/plugin/service/storage/plugin_test.go +++ b/plugin/service/storage/plugin_test.go @@ -4,10 +4,14 @@ package storage import ( + "bytes" "context" + "fmt" + "path" "testing" "github.com/google/go-cmp/cmp" + "github.com/google/uuid" internaltest "github.com/hashicorp/boundary-plugin-minio/internal/testing" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets" "github.com/hashicorp/boundary/sdk/pbs/plugin" @@ -802,7 +806,188 @@ func TestValidatePermissions(t *testing.T) { } } -func TestHeadObject(t *testing.T) {} +func TestHeadObject(t *testing.T) { + ctx := context.Background() + server := internaltest.NewMinioServer(t) + + bucketName := "test-bucket" + err := server.Client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}) + require.NoError(t, err) + + objectKey := fmt.Sprintf("test-head-object-%s", uuid.New().String()) + rd := bytes.NewReader([]byte("test-head-object-contents")) + _, err = server.Client.PutObject(ctx, bucketName, objectKey, rd, rd.Size(), minio.PutObjectOptions{}) + require.NoError(t, err) + + bucketPrefix := "prefix" + rd2 := bytes.NewReader([]byte("test-head-object-contents")) + _, err = server.Client.PutObject(ctx, bucketName, path.Join(bucketPrefix, objectKey), rd2, rd2.Size(), minio.PutObjectOptions{}) + require.NoError(t, err) + + tests := []struct { + name string + req *plugin.HeadObjectRequest + expErrMsg string + }{ + { + name: "nilBucket", + req: &plugin.HeadObjectRequest{Bucket: nil}, + expErrMsg: "no storage bucket information found", + }, + { + name: "emptyBucket", + req: &plugin.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{}, + }, + expErrMsg: "storage bucket name is required", + }, + { + name: "emptyObjectKey", + req: &plugin.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + }, + Key: "", + }, + expErrMsg: "object key is required", + }, + { + name: "badStorageAttributes", + req: &plugin.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "endpoint_url": structpb.NewStringValue("foo"), + }, + }, + }, + Key: "object-key", + }, + expErrMsg: "attributes.endpoint_url.format: unknown protocol, should be http:// or https://", + }, + { + name: "badStorageSecrets", + req: &plugin.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "endpoint_url": structpb.NewStringValue("http://foo"), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "access_key_id": structpb.NewStringValue("foo"), + }, + }, + }, + Key: "object-key", + }, + expErrMsg: "secrets.secret_access_key: missing required value \"secret_access_key\"", + }, + { + name: "statObjectFail", + req: &plugin.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://localhost:1"), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue("foo"), + ConstSecretAccessKey: structpb.NewStringValue("bar"), + }, + }, + }, + Key: "object-key", + }, + expErrMsg: "failed to stat object", + }, + { + name: "nonExistantKey", + req: &plugin.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId), + ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey), + }, + }, + }, + Key: "doesnt-exist", + }, + expErrMsg: "failed to stat object: The specified key does not exist.", + }, + { + name: "success", + req: &plugin.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId), + ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey), + }, + }, + }, + Key: objectKey, + }, + }, + { + name: "successWithBucketPrefix", + req: &plugin.HeadObjectRequest{ + Bucket: &storagebuckets.StorageBucket{ + BucketName: bucketName, + BucketPrefix: bucketPrefix, + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstEndpointUrl: structpb.NewStringValue("http://" + server.ApiAddr), + }, + }, + Secrets: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + ConstAccessKeyId: structpb.NewStringValue(server.ServiceAccountAccessKeyId), + ConstSecretAccessKey: structpb.NewStringValue(server.ServiceAccountSecretAccessKey), + }, + }, + }, + Key: objectKey, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sp := new(StoragePlugin) + rsp, err := sp.HeadObject(ctx, tt.req) + if tt.expErrMsg != "" { + require.ErrorContains(t, err, tt.expErrMsg) + require.Nil(t, rsp) + return + } + + require.NoError(t, err) + require.NotNil(t, rsp) + require.Equal(t, rd.Size(), rsp.GetContentLength()) + require.NotEmpty(t, rsp.GetLastModified().AsTime()) + }) + } +} func TestGetObject(t *testing.T) {}