diff --git a/backend/backend.go b/backend/backend.go index 1b0fecd..1630b65 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -100,6 +100,10 @@ type Backend interface { ListBucketsAndOwners(context.Context) ([]s3response.Bucket, error) } +// InterfaceVersion tracks changes to the Backend interface for plugins. +// Increment this when the Backend interface changes. +const InterfaceVersion = 1 + type BackendUnsupported struct{} var _ Backend = &BackendUnsupported{} diff --git a/backend/plugin/plugin.go b/backend/plugin/plugin.go new file mode 100644 index 0000000..4788853 --- /dev/null +++ b/backend/plugin/plugin.go @@ -0,0 +1,516 @@ +// Copyright 2025 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package vgwplugin + +import ( + "bufio" + "context" + "fmt" + "plugin" + "reflect" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/versity/versitygw/backend" + "github.com/versity/versitygw/s3err" + "github.com/versity/versitygw/s3response" +) + +// The plugin backend is used to dynamically load a Go plugin at runtime. +// It loads the plugin and calls the InitPlugin function to initialize it. +// A config string option is passed to init the plugin, it is expected that the +// plugin will handle its own configuration and initialization from this. +// If the plugin cannot be loaded or initialized, it returns an error. +// The InitPlugin function should be defined in the plugin and should have +// the signature func(configfile string) (version int, err error). +// The plugin should also implement the backend.Backend interface functions. +// However, the plugin does not need to implement all functions of the +// backend.Backend interface. It can implement only the functions it needs. +// Any non-implemented functions will return an error indicating that +// the function is not implemented. +// The plugin file should be compiled with the same Go version as the +// application using it. The plugin file should be built with the +// -buildmode=plugin flag. +// Example: go build -buildmode=plugin -o myplugin.so myplugin.go +// See the following for caveats and details: +// https://pkg.go.dev/plugin#hdr-Warnings + +// PluginBackend implements the backend.Backend interface using Go plugins. +type PluginBackend struct { + p *plugin.Plugin +} + +// NewPluginBackend creates a new PluginBackend. The path parameter should +// point to the compiled plugin file (e.g., .so file). +func NewPluginBackend(path, config string) (*PluginBackend, error) { + p, err := plugin.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open plugin: %w", err) + } + + initSymbol, err := p.Lookup("InitPlugin") + if err != nil { + return nil, fmt.Errorf("failed to lookup InitPlugin symbol: %w", err) + } + + initFunc, ok := initSymbol.(func(string) (int, error)) + if !ok { + return nil, fmt.Errorf("InitPlugin symbol is not a func() (int, error)") + } + + version, err := initFunc(config) + if err != nil { + return nil, fmt.Errorf("InitPlugin failed: %w", err) + } + + if version != backend.InterfaceVersion { + return nil, fmt.Errorf("plugin interface version mismatch: gateway %v, plugin %v", + backend.InterfaceVersion, version) + } + + return &PluginBackend{p: p}, nil +} + +func (p *PluginBackend) callPluginFunc(name string, args []any) ([]reflect.Value, error) { + symbol, err := p.p.Lookup(name) + if err != nil { + return nil, s3err.GetAPIError(s3err.ErrNotImplemented) + } + + symbolValue := reflect.ValueOf(symbol) + if symbolValue.Kind() != reflect.Func { + return nil, fmt.Errorf("symbol %s is not a function", name) + } + + numIn := symbolValue.Type().NumIn() + if len(args) != numIn { + return nil, fmt.Errorf("incorrect number of arguments for function %s, expected %d, got %d", name, numIn, len(args)) + } + + in := make([]reflect.Value, len(args)) + for i := range args { + in[i] = reflect.ValueOf(args[i]) + } + + return symbolValue.Call(in), nil +} + +func (p *PluginBackend) String() string { return "Plugin Gateway" } +func (p *PluginBackend) Shutdown() {} + +func (p *PluginBackend) ListBuckets(ctx context.Context, input s3response.ListBucketsInput) (s3response.ListAllMyBucketsResult, error) { + results, err := p.callPluginFunc("ListBuckets", []any{ctx, input}) + if err != nil { + return s3response.ListAllMyBucketsResult{}, err + } + + return results[0].Interface().(s3response.ListAllMyBucketsResult), convertError(results[1]) +} + +func (p *PluginBackend) HeadBucket(ctx context.Context, input *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) { + results, err := p.callPluginFunc("HeadBucket", []any{ctx, input}) + if err != nil { + return nil, err + } + + return results[0].Interface().(*s3.HeadBucketOutput), convertError(results[1]) +} + +func (p *PluginBackend) GetBucketAcl(ctx context.Context, input *s3.GetBucketAclInput) ([]byte, error) { + results, err := p.callPluginFunc("GetBucketAcl", []any{ctx, input}) + if err != nil { + return nil, err + } + + return results[0].Interface().([]byte), convertError(results[1]) +} + +func (p *PluginBackend) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, defaultACL []byte) error { + _, err := p.callPluginFunc("CreateBucket", []any{ctx, input, defaultACL}) + return err +} + +func (p *PluginBackend) PutBucketAcl(ctx context.Context, bucket string, data []byte) error { + _, err := p.callPluginFunc("PutBucketAcl", []any{ctx, bucket, data}) + return err +} + +func (p *PluginBackend) DeleteBucket(ctx context.Context, bucket string) error { + _, err := p.callPluginFunc("DeleteBucket", []any{ctx, bucket}) + return err +} + +func (p *PluginBackend) PutBucketVersioning(ctx context.Context, bucket string, status types.BucketVersioningStatus) error { + _, err := p.callPluginFunc("PutBucketVersioning", []any{ctx, bucket, status}) + return err +} + +func (p *PluginBackend) GetBucketVersioning(ctx context.Context, bucket string) (s3response.GetBucketVersioningOutput, error) { + results, err := p.callPluginFunc("GetBucketVersioning", []any{ctx, bucket}) + if err != nil { + return s3response.GetBucketVersioningOutput{}, err + } + + return results[0].Interface().(s3response.GetBucketVersioningOutput), convertError(results[1]) +} + +func (p *PluginBackend) PutBucketPolicy(ctx context.Context, bucket string, policy []byte) error { + _, err := p.callPluginFunc("PutBucketPolicy", []any{ctx, bucket, policy}) + return err +} + +func (p *PluginBackend) GetBucketPolicy(ctx context.Context, bucket string) ([]byte, error) { + results, err := p.callPluginFunc("GetBucketPolicy", []any{ctx, bucket}) + if err != nil { + return nil, err + } + + return results[0].Interface().([]byte), convertError(results[1]) +} + +func (p *PluginBackend) DeleteBucketPolicy(ctx context.Context, bucket string) error { + _, err := p.callPluginFunc("DeleteBucketPolicy", []any{ctx, bucket}) + return err +} + +func (p *PluginBackend) PutBucketOwnershipControls(ctx context.Context, bucket string, ownership types.ObjectOwnership) error { + _, err := p.callPluginFunc("PutBucketOwnershipControls", []any{ctx, bucket, ownership}) + return err +} + +func (p *PluginBackend) GetBucketOwnershipControls(ctx context.Context, bucket string) (types.ObjectOwnership, error) { + results, err := p.callPluginFunc("GetBucketOwnershipControls", []any{ctx, bucket}) + if err != nil { + return "", err + } + + return results[0].Interface().(types.ObjectOwnership), convertError(results[1]) +} + +func (p *PluginBackend) DeleteBucketOwnershipControls(ctx context.Context, bucket string) error { + _, err := p.callPluginFunc("DeleteBucketOwnershipControls", []any{ctx, bucket}) + return err +} + +func (p *PluginBackend) PutBucketCors(ctx context.Context, data []byte) error { + _, err := p.callPluginFunc("PutBucketCors", []any{ctx, data}) + return err +} + +func (p *PluginBackend) GetBucketCors(ctx context.Context, bucket string) ([]byte, error) { + results, err := p.callPluginFunc("GetBucketCors", []any{ctx, bucket}) + if err != nil { + return nil, err + } + + return results[0].Interface().([]byte), convertError(results[1]) +} + +func (p *PluginBackend) DeleteBucketCors(ctx context.Context, bucket string) error { + _, err := p.callPluginFunc("DeleteBucketCors", []any{ctx, bucket}) + return err +} + +func (p *PluginBackend) CreateMultipartUpload(ctx context.Context, input s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) { + results, err := p.callPluginFunc("CreateMultipartUpload", []any{ctx, input}) + if err != nil { + return s3response.InitiateMultipartUploadResult{}, err + } + + return results[0].Interface().(s3response.InitiateMultipartUploadResult), convertError(results[1]) +} + +func (p *PluginBackend) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) { + results, err := p.callPluginFunc("CompleteMultipartUpload", []any{ctx, input}) + if err != nil { + return nil, err + } + + return results[0].Interface().(*s3.CompleteMultipartUploadOutput), convertError(results[1]) +} + +func (p *PluginBackend) AbortMultipartUpload(ctx context.Context, input *s3.AbortMultipartUploadInput) error { + _, err := p.callPluginFunc("AbortMultipartUpload", []any{ctx, input}) + return err +} + +func (p *PluginBackend) ListMultipartUploads(ctx context.Context, input *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) { + results, err := p.callPluginFunc("ListMultipartUploads", []any{ctx, input}) + if err != nil { + return s3response.ListMultipartUploadsResult{}, err + } + + return results[0].Interface().(s3response.ListMultipartUploadsResult), convertError(results[1]) +} + +func (p *PluginBackend) ListParts(ctx context.Context, input *s3.ListPartsInput) (s3response.ListPartsResult, error) { + results, err := p.callPluginFunc("ListParts", []any{ctx, input}) + if err != nil { + return s3response.ListPartsResult{}, err + } + + return results[0].Interface().(s3response.ListPartsResult), convertError(results[1]) +} + +func (p *PluginBackend) UploadPart(ctx context.Context, input *s3.UploadPartInput) (*s3.UploadPartOutput, error) { + results, err := p.callPluginFunc("UploadPart", []any{ctx, input}) + if err != nil { + return nil, err + } + + return results[0].Interface().(*s3.UploadPartOutput), convertError(results[1]) +} + +func (p *PluginBackend) UploadPartCopy(ctx context.Context, input *s3.UploadPartCopyInput) (s3response.CopyPartResult, error) { + results, err := p.callPluginFunc("UploadPartCopy", []any{ctx, input}) + if err != nil { + return s3response.CopyPartResult{}, err + } + + return results[0].Interface().(s3response.CopyPartResult), convertError(results[1]) +} + +func (p *PluginBackend) PutObject(ctx context.Context, input s3response.PutObjectInput) (s3response.PutObjectOutput, error) { + results, err := p.callPluginFunc("PutObject", []any{ctx, input}) + if err != nil { + return s3response.PutObjectOutput{}, err + } + + return results[0].Interface().(s3response.PutObjectOutput), convertError(results[1]) +} + +func (p *PluginBackend) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) { + results, err := p.callPluginFunc("HeadObject", []any{ctx, input}) + if err != nil { + return nil, err + } + + return results[0].Interface().(*s3.HeadObjectOutput), convertError(results[1]) +} + +func (p *PluginBackend) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.GetObjectOutput, error) { + results, err := p.callPluginFunc("GetObject", []any{ctx, input}) + if err != nil { + return nil, err + } + + return results[0].Interface().(*s3.GetObjectOutput), convertError(results[1]) +} + +func (p *PluginBackend) GetObjectAcl(ctx context.Context, input *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) { + results, err := p.callPluginFunc("GetObjectAcl", []any{ctx, input}) + if err != nil { + return nil, err + } + + return results[0].Interface().(*s3.GetObjectAclOutput), convertError(results[1]) +} + +func (p *PluginBackend) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResponse, error) { + results, err := p.callPluginFunc("GetObjectAttributes", []any{ctx, input}) + if err != nil { + return s3response.GetObjectAttributesResponse{}, err + } + + return results[0].Interface().(s3response.GetObjectAttributesResponse), convertError(results[1]) +} + +func (p *PluginBackend) CopyObject(ctx context.Context, input s3response.CopyObjectInput) (*s3.CopyObjectOutput, error) { + results, err := p.callPluginFunc("CopyObject", []any{ctx, input}) + if err != nil { + return nil, err + } + + return results[0].Interface().(*s3.CopyObjectOutput), convertError(results[1]) +} + +func (p *PluginBackend) ListObjects(ctx context.Context, input *s3.ListObjectsInput) (s3response.ListObjectsResult, error) { + results, err := p.callPluginFunc("ListObjects", []any{ctx, input}) + if err != nil { + return s3response.ListObjectsResult{}, err + } + + return results[0].Interface().(s3response.ListObjectsResult), convertError(results[1]) +} + +func (p *PluginBackend) ListObjectsV2(ctx context.Context, input *s3.ListObjectsV2Input) (s3response.ListObjectsV2Result, error) { + results, err := p.callPluginFunc("ListObjectsV2", []any{ctx, input}) + if err != nil { + return s3response.ListObjectsV2Result{}, err + } + + return results[0].Interface().(s3response.ListObjectsV2Result), convertError(results[1]) +} + +func (p *PluginBackend) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) { + results, err := p.callPluginFunc("DeleteObject", []any{ctx, input}) + if err != nil { + return nil, err + } + + return results[0].Interface().(*s3.DeleteObjectOutput), convertError(results[1]) +} + +func (p *PluginBackend) DeleteObjects(ctx context.Context, input *s3.DeleteObjectsInput) (s3response.DeleteResult, error) { + results, err := p.callPluginFunc("DeleteObjects", []any{ctx, input}) + if err != nil { + return s3response.DeleteResult{}, err + } + + return results[0].Interface().(s3response.DeleteResult), convertError(results[1]) +} + +func (p *PluginBackend) PutObjectAcl(ctx context.Context, input *s3.PutObjectAclInput) error { + _, err := p.callPluginFunc("PutObjectAcl", []any{ctx, input}) + return err +} + +func (p *PluginBackend) ListObjectVersions(ctx context.Context, input *s3.ListObjectVersionsInput) (s3response.ListVersionsResult, error) { + results, err := p.callPluginFunc("ListObjectVersions", []any{ctx, input}) + if err != nil { + return s3response.ListVersionsResult{}, err + } + + return results[0].Interface().(s3response.ListVersionsResult), convertError(results[1]) +} + +func (p *PluginBackend) RestoreObject(ctx context.Context, input *s3.RestoreObjectInput) error { + _, err := p.callPluginFunc("RestoreObject", []any{ctx, input}) + return err +} + +func (p *PluginBackend) SelectObjectContent(ctx context.Context, input *s3.SelectObjectContentInput) func(w *bufio.Writer) { + results, err := p.callPluginFunc("SelectObjectContent", []any{ctx, input}) + if err != nil { + return func(w *bufio.Writer) {} + } + + return results[0].Interface().(func(w *bufio.Writer)) +} + +func (p *PluginBackend) GetBucketTagging(ctx context.Context, bucket string) (map[string]string, error) { + results, err := p.callPluginFunc("GetBucketTagging", []any{ctx, bucket}) + if err != nil { + return nil, err + } + + return results[0].Interface().(map[string]string), convertError(results[1]) +} + +func (p *PluginBackend) PutBucketTagging(ctx context.Context, bucket string, tags map[string]string) error { + _, err := p.callPluginFunc("PutBucketTagging", []any{ctx, bucket, tags}) + return err +} + +func (p *PluginBackend) DeleteBucketTagging(ctx context.Context, bucket string) error { + _, err := p.callPluginFunc("DeleteBucketTagging", []any{ctx, bucket}) + return err +} + +func (p *PluginBackend) GetObjectTagging(ctx context.Context, bucket, object string) (map[string]string, error) { + results, err := p.callPluginFunc("GetObjectTagging", []any{ctx, bucket, object}) + if err != nil { + return nil, err + } + + return results[0].Interface().(map[string]string), convertError(results[1]) +} + +func (p *PluginBackend) PutObjectTagging(ctx context.Context, bucket, object string, tags map[string]string) error { + _, err := p.callPluginFunc("PutObjectTagging", []any{ctx, bucket, object, tags}) + return err +} + +func (p *PluginBackend) DeleteObjectTagging(ctx context.Context, bucket, object string) error { + _, err := p.callPluginFunc("DeleteObjectTagging", []any{ctx, bucket, object}) + return err +} + +func (p *PluginBackend) PutObjectLockConfiguration(ctx context.Context, bucket string, config []byte) error { + _, err := p.callPluginFunc("PutObjectLockConfiguration", []any{ctx, bucket, config}) + return err +} + +func (p *PluginBackend) GetObjectLockConfiguration(ctx context.Context, bucket string) ([]byte, error) { + results, err := p.callPluginFunc("GetObjectLockConfiguration", []any{ctx, bucket}) + if err != nil { + return nil, err + } + + return results[0].Interface().([]byte), convertError(results[1]) +} + +func (p *PluginBackend) PutObjectRetention(ctx context.Context, bucket, object, versionId string, bypass bool, retention []byte) error { + _, err := p.callPluginFunc("PutObjectRetention", []any{ctx, bucket, object, versionId, bypass, retention}) + return err +} + +func (p *PluginBackend) GetObjectRetention(ctx context.Context, bucket, object, versionId string) ([]byte, error) { + results, err := p.callPluginFunc("GetObjectRetention", []any{ctx, bucket, object, versionId}) + if err != nil { + return nil, err + } + + return results[0].Interface().([]byte), convertError(results[1]) +} + +func (p *PluginBackend) PutObjectLegalHold(ctx context.Context, bucket, object, versionId string, status bool) error { + _, err := p.callPluginFunc("PutObjectLegalHold", []any{ctx, bucket, object, versionId, status}) + return err +} + +func (p *PluginBackend) GetObjectLegalHold(ctx context.Context, bucket, object, versionId string) (*bool, error) { + results, err := p.callPluginFunc("GetObjectLegalHold", []any{ctx, bucket, object, versionId}) + if err != nil { + return nil, err + } + + val := results[0].Interface() + + if val == nil { + return nil, convertError(results[1]) + } + + return val.(*bool), convertError(results[1]) +} + +func (p *PluginBackend) ChangeBucketOwner(ctx context.Context, bucket string, acl []byte) error { + _, err := p.callPluginFunc("ChangeBucketOwner", []any{ctx, bucket, acl}) + return err +} + +func (p *PluginBackend) ListBucketsAndOwners(ctx context.Context) ([]s3response.Bucket, error) { + results, err := p.callPluginFunc("ListBucketsAndOwners", []any{ctx}) + if err != nil { + return nil, err + } + + return results[0].Interface().([]s3response.Bucket), convertError(results[1]) +} + +func convertError(result reflect.Value) error { + if result.IsNil() { + return nil + } + + err, ok := result.Interface().(error) + if !ok { + return fmt.Errorf("expected error, got %T", result.Interface()) + } + + return err +} + +var _ backend.Backend = &PluginBackend{} diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index b501c8c..0d1fefb 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -98,6 +98,7 @@ func main() { scoutfsCommand(), s3Command(), azureCommand(), + pluginCommand(), adminCommand(), testCommand(), utilsCommand(), diff --git a/cmd/versitygw/plugin.go b/cmd/versitygw/plugin.go new file mode 100644 index 0000000..7fba6c1 --- /dev/null +++ b/cmd/versitygw/plugin.go @@ -0,0 +1,64 @@ +// Copyright 2025 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" + vgwplugin "github.com/versity/versitygw/backend/plugin" +) + +var ( + pluginPath string + pluginConfig string +) + +func pluginCommand() *cli.Command { + return &cli.Command{ + Name: "plugin", + Usage: "plugin storage backend", + Description: `This tells the gateway to load the backend from a dynamic runtime plugin.`, + Action: runPlugin, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Usage: "path to plugin shared object file", + Value: "", + Required: true, + EnvVars: []string{"VGW_PLUGIN_FILE"}, + Destination: &pluginPath, + Aliases: []string{"f"}, + }, + &cli.StringFlag{ + Name: "config", + Usage: "configuration option for the plugin", + Value: "", + Required: true, + EnvVars: []string{"VGW_PLUGIN_CONFIG"}, + Destination: &pluginConfig, + Aliases: []string{"c"}, + }, + }, + } +} + +func runPlugin(ctx *cli.Context) error { + be, err := vgwplugin.NewPluginBackend(pluginPath, pluginConfig) + if err != nil { + return fmt.Errorf("init plugin backend: %w", err) + } + return runGateway(ctx.Context, be) +}