feat: add plugin backend

This new plugin backend allows loading a shared object to
implement the backend storage logic. See the following for
caveats with Go plugins: https://pkg.go.dev/plugin#hdr-Warnings.

This will also need cgo enabled within the builds, which is
currently disabled for github releases. So this will require
building the gateway from source for now.
This commit is contained in:
Ben McClelland
2025-03-30 22:11:19 -07:00
parent b5c98b0106
commit db9cefa27c
4 changed files with 585 additions and 0 deletions

View File

@@ -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{}

516
backend/plugin/plugin.go Normal file
View File

@@ -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{}

View File

@@ -98,6 +98,7 @@ func main() {
scoutfsCommand(),
s3Command(),
azureCommand(),
pluginCommand(),
adminCommand(),
testCommand(),
utilsCommand(),

64
cmd/versitygw/plugin.go Normal file
View File

@@ -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)
}