feat(plugin): add support for storage service
This commit is contained in:
1 parent
46c53fb94a
commit
010b9df595
16 files changed
+3589
-14
No files matched your search
@@ -0,0 +1,300 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/hashicorp/boundary-plugin-host-aws/internal/credential"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/storagebuckets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/hashicorp/go-secure-stdlib/awsutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
const (
|
||||
testOptionErr = "test option error"
|
||||
testGetUserErr = "test error for GetUser"
|
||||
testGetCallerIdentityErr = "test error for GetCallerIdentity"
|
||||
testDeleteAccessKeyErr = "test error for DeleteAccessKey"
|
||||
testGetObjectErr = "test error for GetObject"
|
||||
testPutObjectErr = "test error for PutObject"
|
||||
testHeadObjectErr = "test error for HeadObject"
|
||||
)
|
||||
|
||||
// throttleErr is a mocked error used for testing the aws s3 client
|
||||
type throttleErr struct{}
|
||||
|
||||
func (e *throttleErr) ErrorCode() string {
|
||||
return "ThrottlingException"
|
||||
}
|
||||
|
||||
func (e *throttleErr) Error() string {
|
||||
return "ThrottlingException"
|
||||
}
|
||||
|
||||
type testMockS3State struct {
|
||||
GetObjectCalled bool
|
||||
GetObjectInputParams *s3.GetObjectInput
|
||||
|
||||
PutObjectCalled bool
|
||||
PutObjectInputParams *s3.PutObjectInput
|
||||
PutObjectBody []byte
|
||||
|
||||
HeadObjectCalled bool
|
||||
HeadObjectInputParams *s3.HeadObjectInput
|
||||
}
|
||||
|
||||
func (s *testMockS3State) Reset() {
|
||||
s.GetObjectCalled = false
|
||||
s.GetObjectInputParams = nil
|
||||
s.PutObjectCalled = false
|
||||
s.PutObjectInputParams = nil
|
||||
s.PutObjectBody = nil
|
||||
s.HeadObjectCalled = false
|
||||
s.HeadObjectInputParams = nil
|
||||
}
|
||||
|
||||
type testMockS3 struct {
|
||||
S3API
|
||||
|
||||
State *testMockS3State
|
||||
Region string
|
||||
Endpoint aws.EndpointResolverWithOptions
|
||||
|
||||
// mocked responses for getObject
|
||||
GetObjectOutput *s3.GetObjectOutput
|
||||
GetObjectErr error
|
||||
|
||||
// mocked responses for putObject
|
||||
PutObjectOutput *s3.PutObjectOutput
|
||||
PutObjectErr error
|
||||
|
||||
// mocked responses for headObject
|
||||
HeadObjectOutput *s3.HeadObjectOutput
|
||||
HeadObjectErr error
|
||||
}
|
||||
|
||||
type testMockS3Option func(m *testMockS3) error
|
||||
|
||||
func testMockS3WithPutObjectOutput(o *s3.PutObjectOutput) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.PutObjectOutput = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithPutObjectError(e error) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.PutObjectErr = e
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithGetObjectOutput(o *s3.GetObjectOutput) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.GetObjectOutput = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithGetObjectError(e error) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.GetObjectErr = e
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithHeadObjectOutput(o *s3.HeadObjectOutput) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.HeadObjectOutput = o
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testMockS3WithHeadObjectError(e error) testMockS3Option {
|
||||
return func(m *testMockS3) error {
|
||||
m.HeadObjectErr = e
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func newTestMockS3(state *testMockS3State, opts ...testMockS3Option) s3APIFunc {
|
||||
return func(cfgs ...aws.Config) (S3API, error) {
|
||||
m := &testMockS3{
|
||||
State: state,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, cfg := range cfgs {
|
||||
// Last region takes precedence
|
||||
if cfg.Region != "" {
|
||||
m.Region = cfg.Region
|
||||
}
|
||||
if cfg.EndpointResolverWithOptions != nil {
|
||||
m.Endpoint = cfg.EndpointResolverWithOptions
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *testMockS3) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
|
||||
if m.State != nil {
|
||||
m.State.GetObjectCalled = true
|
||||
m.State.GetObjectInputParams = params
|
||||
}
|
||||
|
||||
if m.GetObjectErr != nil {
|
||||
return nil, m.GetObjectErr
|
||||
}
|
||||
|
||||
return m.GetObjectOutput, nil
|
||||
}
|
||||
|
||||
func (m *testMockS3) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
|
||||
if m.State != nil {
|
||||
m.State.PutObjectCalled = true
|
||||
m.State.PutObjectInputParams = params
|
||||
}
|
||||
|
||||
if m.PutObjectErr != nil {
|
||||
return nil, m.PutObjectErr
|
||||
}
|
||||
|
||||
// Reads and Writes on the pipe are matched one to one except when multiple Reads are needed to consume a single Write.
|
||||
// Therefore, we need to read the entire body to ensure that the pipe is not blocked.
|
||||
if params.Body != nil {
|
||||
data, err := io.ReadAll(params.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m.State == nil || m.State.PutObjectBody == nil {
|
||||
return m.PutObjectOutput, nil
|
||||
}
|
||||
if !bytes.Equal(data, m.State.PutObjectBody) {
|
||||
return nil, fmt.Errorf("expected body %q, got %q", m.State.PutObjectBody, data)
|
||||
}
|
||||
}
|
||||
|
||||
return m.PutObjectOutput, nil
|
||||
}
|
||||
|
||||
func (m *testMockS3) DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
|
||||
return &s3.DeleteObjectOutput{}, nil
|
||||
}
|
||||
|
||||
func (m *testMockS3) HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
|
||||
if m.State != nil {
|
||||
m.State.HeadObjectCalled = true
|
||||
m.State.HeadObjectInputParams = params
|
||||
}
|
||||
|
||||
if m.HeadObjectErr != nil {
|
||||
return nil, m.HeadObjectErr
|
||||
}
|
||||
|
||||
return m.HeadObjectOutput, nil
|
||||
}
|
||||
|
||||
type getObjectStreamMock struct {
|
||||
grpc.ServerStream
|
||||
ctx context.Context
|
||||
err error
|
||||
data *pb.GetObjectResponse
|
||||
}
|
||||
|
||||
func (s *getObjectStreamMock) Context() context.Context {
|
||||
return s.ctx
|
||||
}
|
||||
|
||||
// Send streams a response message to the client.
|
||||
// This function should only be called on the server side.
|
||||
func (g *getObjectStreamMock) Send(req *pb.GetObjectResponse) error {
|
||||
if req == nil {
|
||||
return fmt.Errorf(`parameter arg "resp GetObjectResponse" cannot be nil.`)
|
||||
}
|
||||
g.data = req
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recv streams a response message from the server.
|
||||
// This function should only be called on the client side.
|
||||
func (g *getObjectStreamMock) Recv() (*pb.GetObjectResponse, error) {
|
||||
if g.data == nil {
|
||||
if g.err != nil {
|
||||
return nil, g.err
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
resp := &pb.GetObjectResponse{
|
||||
FileChunk: append([]byte{}, g.data.FileChunk...),
|
||||
}
|
||||
g.data = nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func newGetObjectStreamMock() *getObjectStreamMock {
|
||||
return &getObjectStreamMock{
|
||||
ctx: context.Background(),
|
||||
}
|
||||
}
|
||||
|
||||
func createTime(t *testing.T, timestamp string) time.Time {
|
||||
require := require.New(t)
|
||||
testTime, err := time.Parse(time.RFC3339, timestamp)
|
||||
require.NoError(err)
|
||||
return testTime
|
||||
}
|
||||
|
||||
func deepCopyPutObjectRequest(v *pb.PutObjectRequest) *pb.PutObjectRequest {
|
||||
deepCopy := &pb.PutObjectRequest{
|
||||
Key: v.Key,
|
||||
Path: v.Path,
|
||||
}
|
||||
if v.Bucket != nil {
|
||||
deepCopy.Bucket = &storagebuckets.StorageBucket{
|
||||
BucketName: v.Bucket.BucketName,
|
||||
BucketPrefix: v.Bucket.BucketPrefix,
|
||||
}
|
||||
if v.Bucket.Attributes != nil {
|
||||
attrs, _ := structpb.NewStruct(v.Bucket.Attributes.AsMap())
|
||||
deepCopy.Bucket.Attributes = attrs
|
||||
}
|
||||
if v.Bucket.Secrets != nil {
|
||||
secrets, _ := structpb.NewStruct(v.Bucket.Secrets.AsMap())
|
||||
deepCopy.Bucket.Secrets = secrets
|
||||
}
|
||||
}
|
||||
return deepCopy
|
||||
}
|
||||
|
||||
func validSTSMock() []credential.AwsCredentialPersistedStateOption {
|
||||
return []credential.AwsCredentialPersistedStateOption{
|
||||
credential.WithStateTestOpts([]awsutil.Option{
|
||||
awsutil.WithSTSAPIFunc(
|
||||
awsutil.NewMockSTS(
|
||||
awsutil.WithGetCallerIdentityOutput(&sts.GetCallerIdentityOutput{
|
||||
Account: aws.String("0123456789"),
|
||||
Arn: aws.String("arn:aws:iam::0123456789:user/test"),
|
||||
UserId: aws.String("test"),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user