From dd197c5f688518c779b3d676450aeec57e4d69c0 Mon Sep 17 00:00:00 2001 From: Ryotaro Banno Date: Wed, 13 Nov 2024 07:27:09 +0000 Subject: [PATCH] add internal/controller/internal/objectstorage Signed-off-by: Ryotaro Banno --- Makefile | 1 + go.mod | 18 ++++ go.sum | 36 ++++++++ .../internal/objectstorage/objectstorage.go | 10 ++ .../objectstorage/objectstorage_mock.go | 70 ++++++++++++++ .../controller/internal/objectstorage/s3.go | 92 +++++++++++++++++++ 6 files changed, 227 insertions(+) create mode 100644 internal/controller/internal/objectstorage/objectstorage.go create mode 100644 internal/controller/internal/objectstorage/objectstorage_mock.go create mode 100644 internal/controller/internal/objectstorage/s3.go diff --git a/Makefile b/Makefile index d7c98759..bd016fa9 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,7 @@ vet: ## Run go vet against code. mock: mockgen $(MOCKGEN) -source=internal/ceph/command.go -destination=internal/ceph/command_mock.go -package=ceph $(MOCKGEN) -source=pkg/controller/proto/controller_grpc.pb.go -destination=pkg/controller/proto/controller_grpc.pb_mock.go -package=proto + $(MOCKGEN) -source=internal/controller/internal/objectstorage/objectstorage.go -destination=internal/controller/internal/objectstorage/objectstorage_mock.go -package=objectstorage .PHONY: test test: manifests generate fmt vet envtest mock ## Run tests. diff --git a/go.mod b/go.mod index d7318f2d..27908bf4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/cybozu-go/mantle go 1.22 require ( + github.com/aws/aws-sdk-go-v2 v1.32.4 + github.com/aws/aws-sdk-go-v2/config v1.28.3 + github.com/aws/aws-sdk-go-v2/service/s3 v1.66.3 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.35.1 @@ -25,6 +28,21 @@ require ( github.com/Masterminds/sprig v2.15.0+incompatible // indirect github.com/aokoli/goutils v1.0.1 // indirect github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.44 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 // indirect + github.com/aws/smithy-go v1.22.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 871be6f9..fa8f792d 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,42 @@ github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE= +github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA= +github.com/aws/aws-sdk-go-v2/config v1.28.3 h1:kL5uAptPcPKaJ4q0sDUjUIdueO18Q7JDzl64GpVwdOM= +github.com/aws/aws-sdk-go-v2/config v1.28.3/go.mod h1:SPEn1KA8YbgQnwiJ/OISU4fz7+F6Fe309Jf0QTsRCl4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.44 h1:qqfs5kulLUHUEXlHEZXLJkgGoF3kkUeFUTVA585cFpU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.44/go.mod h1:0Lm2YJ8etJdEdw23s+q/9wTpOeo2HhNE97XcRa7T8MA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 h1:1SZBDiRzzs3sNhOMVApyWPduWYGAX0imGy06XiBnCAM= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23/go.mod h1:i9TkxgbZmHVh2S0La6CAXtnyFhlCX/pJ0JsOvBAS6Mk= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 h1:aaPpoG15S2qHkWm4KlEyF01zovK1nW4BBbyXuHNSE90= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4/go.mod h1:eD9gS2EARTKgGr/W5xwgY/ik9z/zqpW+m/xOQbVxrMk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 h1:E5ZAVOmI2apR8ADb72Q63KqwwwdW1XcMeXIlrZ1Psjg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4/go.mod h1:wezzqVUOVVdk+2Z/JzQT4NxAU0NbhRe5W8pIE72jsWI= +github.com/aws/aws-sdk-go-v2/service/s3 v1.66.3 h1:neNOYJl72bHrz9ikAEED4VqWyND/Po0DnEx64RW6YM4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.66.3/go.mod h1:TMhLIyRIyoGVlaEMAt+ITMbwskSTpcGsCPDq91/ihY0= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.4 h1:yDxvkz3/uOKfxnv8YhzOi9m+2OGIxF+on3KOISbK5IU= +github.com/aws/aws-sdk-go-v2/service/sts v1.32.4/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= diff --git a/internal/controller/internal/objectstorage/objectstorage.go b/internal/controller/internal/objectstorage/objectstorage.go new file mode 100644 index 00000000..e5022271 --- /dev/null +++ b/internal/controller/internal/objectstorage/objectstorage.go @@ -0,0 +1,10 @@ +package objectstorage + +import "context" + +type Bucket interface { + Exists(ctx context.Context, path string) (bool, error) + + // Delete deletes the specified object. Delete will return nil if the object is not found. + Delete(ctx context.Context, path string) error +} diff --git a/internal/controller/internal/objectstorage/objectstorage_mock.go b/internal/controller/internal/objectstorage/objectstorage_mock.go new file mode 100644 index 00000000..2375d08b --- /dev/null +++ b/internal/controller/internal/objectstorage/objectstorage_mock.go @@ -0,0 +1,70 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/controller/internal/objectstorage/objectstorage.go +// +// Generated by this command: +// +// mockgen -source=internal/controller/internal/objectstorage/objectstorage.go -destination=internal/controller/internal/objectstorage/objectstorage_mock.go -package=objectstorage +// + +// Package objectstorage is a generated GoMock package. +package objectstorage + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockBucket is a mock of Bucket interface. +type MockBucket struct { + ctrl *gomock.Controller + recorder *MockBucketMockRecorder + isgomock struct{} +} + +// MockBucketMockRecorder is the mock recorder for MockBucket. +type MockBucketMockRecorder struct { + mock *MockBucket +} + +// NewMockBucket creates a new mock instance. +func NewMockBucket(ctrl *gomock.Controller) *MockBucket { + mock := &MockBucket{ctrl: ctrl} + mock.recorder = &MockBucketMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBucket) EXPECT() *MockBucketMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockBucket) Delete(ctx context.Context, path string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, path) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockBucketMockRecorder) Delete(ctx, path any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockBucket)(nil).Delete), ctx, path) +} + +// Exists mocks base method. +func (m *MockBucket) Exists(ctx context.Context, path string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Exists", ctx, path) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Exists indicates an expected call of Exists. +func (mr *MockBucketMockRecorder) Exists(ctx, path any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockBucket)(nil).Exists), ctx, path) +} diff --git a/internal/controller/internal/objectstorage/s3.go b/internal/controller/internal/objectstorage/s3.go new file mode 100644 index 00000000..4b8019fd --- /dev/null +++ b/internal/controller/internal/objectstorage/s3.go @@ -0,0 +1,92 @@ +package objectstorage + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net/http" + + "github.com/aws/aws-sdk-go-v2/aws" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" +) + +type S3Bucket struct { + caPEMCerts []byte + bucketName, endpoint string + s3Client *s3.Client +} + +var _ Bucket = &S3Bucket{} + +func NewS3Bucket(ctx context.Context, bucketName, endpoint, accessKeyID, secretAccessKey string, caPEMCerts []byte) (*S3Bucket, error) { + var httpClient config.HTTPClient + if caPEMCerts != nil { + certPool := x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM(caPEMCerts); !ok { + return nil, errors.New("failed to append certs to pool") + } + httpClient = awshttp.NewBuildableClient().WithTransportOptions(func(tr *http.Transport) { + if tr.TLSClientConfig == nil { + tr.TLSClientConfig = &tls.Config{} + } + tr.TLSClientConfig.RootCAs = certPool + }) + } + + sdkConfig, err := config.LoadDefaultConfig( + ctx, + config.WithHTTPClient(httpClient), + config.WithRegion("ceph"), + config.WithCredentialsProvider( + aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) { + return aws.Credentials{ + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + }, nil + }), + ), + ) + if err != nil { + return nil, fmt.Errorf("failed to load default config: %w", err) + } + s3Client := s3.NewFromConfig(sdkConfig, func(o *s3.Options) { + o.BaseEndpoint = &endpoint + o.UsePathStyle = true + }) + + return &S3Bucket{caPEMCerts, bucketName, endpoint, s3Client}, nil +} + +func (b *S3Bucket) Exists(ctx context.Context, key string) (bool, error) { + if _, err := b.s3Client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &b.bucketName, + Key: &key, + }); err != nil { + var notFound *types.NotFound + if errors.As(err, ¬Found) { + return false, nil + } + return false, fmt.Errorf("HeadObject failed: %s: %s: %s: %w", b.endpoint, b.bucketName, key, err) + } + + return true, nil +} + +func (b *S3Bucket) Delete(ctx context.Context, key string) error { + if _, err := b.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &b.bucketName, + Key: &key, + }); err != nil { + var notFound *types.NotFound + if errors.As(err, ¬Found) { + return nil + } + return fmt.Errorf("Delete failed: %s: %s: %s: %w", b.endpoint, b.bucketName, key, err) + } + return nil +}