feat(plugin): Implement HeadObject

This commit is contained in:
Hugo committed 2024-03-12 18:47:26 +00:00
1 parent e0da13c89a
commit a2235d93ce
2 files changed
+228 -2

No files matched your search

+42 -1
View File
@@ -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.
+186 -1
View File
@@ -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) {}