652 lines
21 KiB
Go
652 lines
21 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package storage
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
madmin "github.com/minio/madmin-go/v3"
|
|
"github.com/minio/minio-go/v7"
|
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
"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)
|
|
|
|
const (
|
|
defaultGetObjectChunkSize = uint32(65536)
|
|
)
|
|
|
|
// StoragePlugin implements the StoragePluginServiceServer interface for the
|
|
// MinIO storage service plugin.
|
|
type StoragePlugin struct {
|
|
pb.UnimplementedStoragePluginServiceServer
|
|
}
|
|
|
|
// OnCreateStorageBucket is a hook that runs when a storage bucket is created.
|
|
func (sp *StoragePlugin) OnCreateStorageBucket(ctx context.Context, req *pb.OnCreateStorageBucketRequest) (*pb.OnCreateStorageBucketResponse, error) {
|
|
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")
|
|
}
|
|
|
|
sa, err := getStorageAttributes(bucket.GetAttributes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sec, err := getStorageSecrets(bucket.GetSecrets())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
deleteOldCredsFn := func() error { return nil }
|
|
if !sa.DisableCredentialRotation {
|
|
newSec, fn, err := rotateCredentials(ctx, bucket.GetBucketName(), sa, sec)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "failed to rotate minio credentials: %v", err)
|
|
}
|
|
sec = newSec
|
|
deleteOldCredsFn = fn
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
err = dryRun(ctx, cl, bucket)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Unknown, "failed to verify provided minio environment: %v", err)
|
|
}
|
|
|
|
persistedData, err := structpb.NewStruct(sec.AsMap())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to convert storage attributes to protobuf struct: %v", err)
|
|
}
|
|
|
|
// Given that at this point everything is OK with the bucket, we shouldn't
|
|
// let a failure to delete the old credentials fail the entire process. If
|
|
// we ever establish a way to log non-critical errors in the plugin, log
|
|
// this.
|
|
_ = deleteOldCredsFn()
|
|
|
|
return &pb.OnCreateStorageBucketResponse{
|
|
Persisted: &storagebuckets.StorageBucketPersisted{Data: persistedData},
|
|
}, nil
|
|
}
|
|
|
|
// OnUpdateStorageBucket is a hook that runs when a storage bucket is updated.
|
|
func (sp *StoragePlugin) OnUpdateStorageBucket(ctx context.Context, req *pb.OnUpdateStorageBucketRequest) (*pb.OnUpdateStorageBucketResponse, error) {
|
|
newBucket := req.GetNewBucket()
|
|
if newBucket == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "new bucket is required")
|
|
}
|
|
if newBucket.BucketName == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "new bucketName is required")
|
|
}
|
|
|
|
oldBucket := req.GetCurrentBucket()
|
|
if oldBucket == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "current bucket is required")
|
|
}
|
|
osa, err := getStorageAttributes(oldBucket.GetAttributes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
nsa, err := getStorageAttributes(newBucket.GetAttributes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sec, err := getStorageSecrets(req.GetPersisted().GetData())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch {
|
|
case newBucket.GetBucketName() != oldBucket.GetBucketName():
|
|
return nil, status.Errorf(codes.InvalidArgument, "cannot update attribute BucketName")
|
|
case newBucket.GetBucketPrefix() != oldBucket.GetBucketPrefix():
|
|
return nil, status.Errorf(codes.InvalidArgument, "cannot update attribute BucketPrefix")
|
|
case nsa.Region != osa.Region:
|
|
return nil, status.Errorf(codes.InvalidArgument, "cannot update attribute Region")
|
|
case nsa.EndpointUrl != osa.EndpointUrl:
|
|
return nil, status.Errorf(codes.InvalidArgument, "cannot update attribute EndpointUrl")
|
|
}
|
|
|
|
if nsa.DisableCredentialRotation && newBucket.GetSecrets() == nil {
|
|
if !sec.LastRotatedTime.IsZero() {
|
|
return nil, status.Error(codes.InvalidArgument, "cannot disable rotation for already-rotated credentials")
|
|
}
|
|
}
|
|
|
|
deleteRotatedCredsFn := func() error { return nil }
|
|
deleteOldPersistedCredsFn := func() error { return nil }
|
|
if newBucket.GetSecrets() != nil {
|
|
// Preemptively check if we're managing the current persisted
|
|
// credentials and schedule them for later deletion if we are, since in
|
|
// both rotation and non-rotation cases below, we'll be replacing these.
|
|
if !sec.LastRotatedTime.IsZero() {
|
|
sc := sec.Clone()
|
|
deleteOldPersistedCredsFn = sync.OnceValue(func() error {
|
|
cl, err := newMadminClient(nsa, sc)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create new minio admin client: %w", err)
|
|
}
|
|
err = cl.DeleteServiceAccount(ctx, sc.AccessKeyId)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete minio service account: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
newSec, err := getStorageSecrets(newBucket.GetSecrets())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !nsa.DisableCredentialRotation {
|
|
// We have new incoming credentials and we're also rotating,
|
|
// therefore we use incoming credentials to create a new service
|
|
// account, then delete the incoming credentials. The existing
|
|
// persisted credentials have already been scheduled for deletion
|
|
// above (if applicable), so we don't need to do it here.
|
|
rotatedSec, fn, err := rotateCredentials(ctx, newBucket.GetBucketName(), nsa, newSec)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "failed to rotate minio credentials: %v", err)
|
|
}
|
|
sec = rotatedSec
|
|
deleteRotatedCredsFn = fn
|
|
} else {
|
|
// We have new incoming credentials but we're not rotating. Set them
|
|
// directly for validation below. The existing persisted credentials
|
|
// have already been scheduled for deletion above (if applicable),
|
|
// so we don't need to do it here.
|
|
sec = newSec
|
|
}
|
|
} else {
|
|
if osa.DisableCredentialRotation && !nsa.DisableCredentialRotation {
|
|
// Handle the case where the user enables credential rotation but
|
|
// provides no new credentials. This is an error.
|
|
return nil, status.Errorf(codes.InvalidArgument, "credential rotation enabled with no new credentials provided")
|
|
}
|
|
}
|
|
|
|
cl, err := minio.New(nsa.EndpointUrl, &minio.Options{
|
|
Creds: credentials.NewStaticV4(sec.AccessKeyId, sec.SecretAccessKey, ""),
|
|
Secure: nsa.UseSSL,
|
|
Region: nsa.Region,
|
|
})
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "failed to create minio sdk client: %s", err)
|
|
}
|
|
|
|
if err = dryRun(ctx, cl, newBucket); err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "failed to verify provided minio environment: %s", err)
|
|
}
|
|
|
|
persisted, err := structpb.NewStruct(sec.AsMap())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "failed to convert storage attributes to protobuf struct: %s", err)
|
|
}
|
|
|
|
// Given that at this point everything is OK with the bucket, we shouldn't
|
|
// let a failure to delete credentials fail the entire process. If we ever
|
|
// establish a way to log non-critical errors in the plugin, log this.
|
|
_ = deleteRotatedCredsFn()
|
|
_ = deleteOldPersistedCredsFn()
|
|
|
|
return &pb.OnUpdateStorageBucketResponse{
|
|
Persisted: &storagebuckets.StorageBucketPersisted{Data: persisted},
|
|
}, nil
|
|
}
|
|
|
|
// OnDeleteStorageBucket is a hook that runs when a storage bucket is deleted.
|
|
func (sp *StoragePlugin) OnDeleteStorageBucket(ctx context.Context, req *pb.OnDeleteStorageBucketRequest) (*pb.OnDeleteStorageBucketResponse, error) {
|
|
bucket := req.GetBucket()
|
|
if bucket == nil {
|
|
return nil, status.Error(codes.InvalidArgument, "no storage bucket information found")
|
|
}
|
|
|
|
sec, err := getStorageSecrets(req.GetPersisted().GetData())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !sec.LastRotatedTime.IsZero() {
|
|
sa, err := getStorageAttributes(bucket.GetAttributes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ac, err := newMadminClient(sa, sec)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "failed to create minio admin client: %v", err)
|
|
}
|
|
|
|
err = ac.DeleteServiceAccount(ctx, sec.AccessKeyId)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Unknown, "failed to delete minio service account: %v", err)
|
|
}
|
|
}
|
|
|
|
return &pb.OnDeleteStorageBucketResponse{}, nil
|
|
}
|
|
|
|
// ValidatePermissions is a hook that checks if the secrets associated with the
|
|
// storage bucket meet the requirements of the plugin.
|
|
func (sp *StoragePlugin) ValidatePermissions(ctx context.Context, req *pb.ValidatePermissionsRequest) (*pb.ValidatePermissionsResponse, error) {
|
|
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")
|
|
}
|
|
|
|
sa, err := getStorageAttributes(bucket.GetAttributes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sec, err := getStorageSecrets(bucket.GetSecrets())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !sec.LastRotatedTime.IsZero() {
|
|
// since these are existing secrets, we only want to validate the
|
|
// service account if the credentials have been rotated previously
|
|
err = ensureServiceAccount(ctx, sa, sec)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "failed to ensure service account: %v", 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)
|
|
}
|
|
|
|
err = dryRun(ctx, cl, bucket)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Unknown, "failed to verify provided minio environment: %v", err)
|
|
}
|
|
|
|
return &pb.ValidatePermissionsResponse{}, nil
|
|
}
|
|
|
|
// HeadObject is a hook that retrieves metadata about an object.
|
|
func (sp *StoragePlugin) HeadObject(ctx context.Context, req *pb.HeadObjectRequest) (*pb.HeadObjectResponse, error) {
|
|
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
|
|
}
|
|
|
|
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.
|
|
func (sp *StoragePlugin) GetObject(req *pb.GetObjectRequest, objServer pb.StoragePluginService_GetObjectServer) error {
|
|
bucket := req.GetBucket()
|
|
if bucket == nil {
|
|
return status.Error(codes.InvalidArgument, "no storage bucket information found")
|
|
}
|
|
if bucket.GetBucketName() == "" {
|
|
return status.Error(codes.InvalidArgument, "storage bucket name is required")
|
|
}
|
|
|
|
if req.GetKey() == "" {
|
|
return status.Error(codes.InvalidArgument, "object key is required")
|
|
}
|
|
|
|
sa, err := getStorageAttributes(bucket.GetAttributes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sec, err := getStorageSecrets(bucket.GetSecrets())
|
|
if err != nil {
|
|
return 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 status.Errorf(codes.Unknown, "failed to create minio sdk client: %v", err)
|
|
}
|
|
|
|
objKey := path.Join(bucket.GetBucketPrefix(), req.GetKey())
|
|
obj, err := cl.GetObject(objServer.Context(), bucket.GetBucketName(), objKey, minio.GetObjectOptions{})
|
|
if err != nil {
|
|
return status.Errorf(codes.Unknown, "failed to get object: %v", err)
|
|
}
|
|
defer obj.Close()
|
|
|
|
chunkSize := req.GetChunkSize()
|
|
if chunkSize == 0 {
|
|
chunkSize = defaultGetObjectChunkSize
|
|
}
|
|
|
|
rd := bufio.NewReader(obj)
|
|
for {
|
|
select {
|
|
case <-objServer.Context().Done():
|
|
return status.Errorf(codes.Canceled, "server context done while streaming: %v", objServer.Context().Err())
|
|
default:
|
|
buffer := make([]byte, chunkSize)
|
|
n, err := rd.Read(buffer)
|
|
if err != nil && err != io.EOF {
|
|
return status.Errorf(codes.Unknown, "failed to read chunk from minio object: %v", err)
|
|
}
|
|
if n > 0 {
|
|
err := objServer.Send(&pb.GetObjectResponse{FileChunk: buffer[:n]})
|
|
if err != nil {
|
|
return status.Errorf(codes.Unknown, "failed to send chunk to client: %v", err)
|
|
}
|
|
}
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// PutObject is a hook that reads a file stored on local disk and stores it to
|
|
// an external object store.
|
|
func (sp *StoragePlugin) PutObject(ctx context.Context, req *pb.PutObjectRequest) (*pb.PutObjectResponse, error) {
|
|
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")
|
|
}
|
|
if req.GetPath() == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "path is required")
|
|
}
|
|
|
|
sa, err := getStorageAttributes(bucket.GetAttributes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sec, err := getStorageSecrets(bucket.GetSecrets())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
file, err := os.Open(req.GetPath())
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to open file: %v", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to read file info: %v", err)
|
|
}
|
|
if info.IsDir() {
|
|
return nil, status.Error(codes.InvalidArgument, "path is not a file")
|
|
}
|
|
if info.Size() == 0 {
|
|
return nil, status.Error(codes.InvalidArgument, "file is empty")
|
|
}
|
|
|
|
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.Internal, "failed to create minio sdk client: %v", err)
|
|
}
|
|
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, file); err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to calculate hash: %v", err)
|
|
}
|
|
checksum := base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
|
|
|
if _, err = file.Seek(0, io.SeekStart); err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to rewind file pointer: %v", err)
|
|
}
|
|
|
|
key := path.Join(bucket.GetBucketPrefix(), req.GetKey())
|
|
res, err := cl.PutObject(ctx, bucket.GetBucketName(), key, file, info.Size(), minio.PutObjectOptions{
|
|
UserMetadata: map[string]string{
|
|
"x-amz-checksum-algorithm": "SHA256",
|
|
"x-amz-checksum-sha256": checksum,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to put object into minio: %v", err)
|
|
}
|
|
|
|
if res.ChecksumSHA256 == "" {
|
|
return nil, status.Error(codes.Internal, "missing checksum response from minio")
|
|
}
|
|
if checksum != res.ChecksumSHA256 {
|
|
return nil, status.Error(codes.Internal, "mismatched checksum")
|
|
}
|
|
decodedChecksum, err := base64.StdEncoding.DecodeString(res.ChecksumSHA256)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to decode checksum value from minio: %v", err)
|
|
}
|
|
return &pb.PutObjectResponse{
|
|
ChecksumSha_256: decodedChecksum,
|
|
}, nil
|
|
}
|
|
|
|
// DeleteObjects deletes one or many files in an external object store via a
|
|
// provided key prefix.
|
|
func (sp *StoragePlugin) DeleteObjects(ctx context.Context, req *pb.DeleteObjectsRequest) (*pb.DeleteObjectsResponse, error) {
|
|
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.GetKeyPrefix() == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "key prefix is required")
|
|
}
|
|
|
|
sa, err := getStorageAttributes(bucket.GetAttributes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
prefix := path.Join(bucket.GetBucketPrefix(), req.GetKeyPrefix())
|
|
if strings.HasSuffix(req.GetKeyPrefix(), "/") {
|
|
// path.Join ends by "cleaning" the path, including removing a trailing slash, if
|
|
// it exists. given that a slash is used to denote a folder, it is required here.
|
|
prefix += "/"
|
|
}
|
|
|
|
if !req.Recursive {
|
|
if err := cl.RemoveObject(ctx, bucket.GetBucketName(), prefix, minio.RemoveObjectOptions{}); err != nil {
|
|
return nil, status.Errorf(codes.Unknown, "error deleting minio object: %v", err)
|
|
}
|
|
return &pb.DeleteObjectsResponse{
|
|
ObjectsDeleted: uint32(1),
|
|
}, nil
|
|
}
|
|
|
|
objects := []minio.ObjectInfo{}
|
|
for obj := range cl.ListObjects(ctx, bucket.GetBucketName(), minio.ListObjectsOptions{
|
|
Prefix: prefix,
|
|
Recursive: true,
|
|
}) {
|
|
if obj.Err != nil {
|
|
return nil, status.Errorf(codes.Unknown, "error iterating minio bucket contents: %v", err)
|
|
}
|
|
objects = append(objects, obj)
|
|
}
|
|
|
|
emitter := make(chan minio.ObjectInfo)
|
|
go func() {
|
|
defer close(emitter)
|
|
// we could loop the listobjects respose here, but we want to check all the errs ahead of time
|
|
for _, obj := range objects {
|
|
emitter <- obj
|
|
}
|
|
}()
|
|
|
|
removed := 0
|
|
for res := range cl.RemoveObjectsWithResult(ctx, bucket.GetBucketName(), emitter, minio.RemoveObjectsOptions{}) {
|
|
if res.Err != nil {
|
|
return nil, status.Errorf(codes.Unknown, "error deleting minio object(s): %v", err)
|
|
}
|
|
removed++
|
|
}
|
|
|
|
return &pb.DeleteObjectsResponse{
|
|
ObjectsDeleted: uint32(removed),
|
|
}, nil
|
|
}
|
|
|
|
func dryRun(ctx context.Context, cl *minio.Client, bucket *storagebuckets.StorageBucket) error {
|
|
objectKey := path.Join(bucket.GetBucketPrefix(), uuid.New().String())
|
|
rd := bytes.NewReader([]byte("hashicorp boundary minio plugin access test"))
|
|
_, err := cl.PutObject(ctx, bucket.GetBucketName(), objectKey, rd, rd.Size(), minio.PutObjectOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to put object at %q: %w", objectKey, err)
|
|
}
|
|
|
|
_, err = cl.StatObject(ctx, bucket.GetBucketName(), objectKey, minio.StatObjectOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to stat object at %q: %w", objectKey, err)
|
|
}
|
|
|
|
_, err = cl.GetObject(ctx, bucket.GetBucketName(), objectKey, minio.GetObjectOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get object at %q: %w", objectKey, err)
|
|
}
|
|
|
|
oiCh := cl.ListObjects(ctx, bucket.GetBucketName(), minio.ListObjectsOptions{Recursive: true})
|
|
for oi := range oiCh {
|
|
if oi.Err != nil {
|
|
return fmt.Errorf("failed to list objects in bucket %q: %w", bucket.GetBucketName(), oi.Err)
|
|
}
|
|
}
|
|
|
|
err = cl.RemoveObject(ctx, bucket.GetBucketName(), objectKey, minio.RemoveObjectOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to remove object at %q: %w", objectKey, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func newMadminClient(sa *StorageAttributes, sec *StorageSecrets) (*madmin.AdminClient, error) {
|
|
return madmin.NewWithOptions(sa.EndpointUrl, &madmin.Options{
|
|
Creds: credentials.NewStaticV4(sec.AccessKeyId, sec.SecretAccessKey, ""),
|
|
Secure: sa.UseSSL,
|
|
})
|
|
}
|
|
|
|
// ensureServiceAccount ensures the credentials we received belong to a MinIO
|
|
// service account. This plugin does not support using user credentials for its
|
|
// configuration.
|
|
func ensureServiceAccount(ctx context.Context, sa *StorageAttributes, sec *StorageSecrets) error {
|
|
cl, err := newMadminClient(sa, sec)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create madmin client: %w", err)
|
|
}
|
|
|
|
if _, err = cl.InfoServiceAccount(ctx, sec.AccessKeyId); err != nil {
|
|
return fmt.Errorf("failed to obtain service account info: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|