get.porter.sh/porter@v1.3.0/pkg/storage/plugin_adapter.go (about)

     1  package storage
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"strings"
     9  
    10  	"get.porter.sh/porter/pkg/storage/plugins"
    11  	"go.mongodb.org/mongo-driver/bson"
    12  )
    13  
    14  var _ Store = PluginAdapter{}
    15  
    16  // PluginAdapter converts between the low-level plugin.StorageProtocol which
    17  // operates on bson documents, and the document types stored by Porter which are
    18  // marshaled using json.
    19  //
    20  // Specifically it handles converting from bson.Raw to the type specified by
    21  // ResultType on plugin.ResultOptions so that you can just cast the result to
    22  // the specified type safely.
    23  type PluginAdapter struct {
    24  	plugin plugins.StorageProtocol
    25  }
    26  
    27  // NewPluginAdapter wraps the specified storage plugin.
    28  func NewPluginAdapter(plugin plugins.StorageProtocol) PluginAdapter {
    29  	return PluginAdapter{
    30  		plugin: plugin,
    31  	}
    32  }
    33  
    34  func (a PluginAdapter) Close() error {
    35  	if closer, ok := a.plugin.(io.Closer); ok {
    36  		return closer.Close()
    37  	}
    38  	return nil
    39  }
    40  
    41  func (a PluginAdapter) Aggregate(ctx context.Context, collection string, opts AggregateOptions, out interface{}) error {
    42  	rawResults, err := a.plugin.Aggregate(ctx, opts.ToPluginOptions(collection))
    43  	if err != nil {
    44  		return err
    45  	}
    46  
    47  	return a.unmarshalSlice(rawResults, out)
    48  }
    49  
    50  func (a PluginAdapter) EnsureIndex(ctx context.Context, opts EnsureIndexOptions) error {
    51  	return a.plugin.EnsureIndex(ctx, opts.ToPluginOptions())
    52  }
    53  
    54  func (a PluginAdapter) Count(ctx context.Context, collection string, opts CountOptions) (int64, error) {
    55  	return a.plugin.Count(ctx, opts.ToPluginOptions(collection))
    56  }
    57  
    58  func (a PluginAdapter) Find(ctx context.Context, collection string, opts FindOptions, out interface{}) error {
    59  	rawResults, err := a.plugin.Find(ctx, opts.ToPluginOptions(collection))
    60  	if err != nil {
    61  		return a.handleError(err, collection)
    62  
    63  	}
    64  
    65  	return a.unmarshalSlice(rawResults, out)
    66  }
    67  
    68  // FindOne queries a collection and returns the first result, returning
    69  // ErrNotFound when no results are returned.
    70  func (a PluginAdapter) FindOne(ctx context.Context, collection string, opts FindOptions, out interface{}) error {
    71  	rawResults, err := a.plugin.Find(ctx, opts.ToPluginOptions(collection))
    72  	if err != nil {
    73  		return a.handleError(err, collection)
    74  	}
    75  
    76  	if len(rawResults) == 0 {
    77  		notFoundErr := ErrNotFound{Collection: collection}
    78  		if name, ok := opts.Filter["name"]; ok {
    79  			notFoundErr.Item = fmt.Sprint(name)
    80  		}
    81  		return notFoundErr
    82  	}
    83  
    84  	err = a.unmarshal(rawResults[0], out)
    85  	if err != nil {
    86  		return fmt.Errorf("could not unmarshal document of type %T: %w", out, err)
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  // unmarshalSlice unpacks a slice of bson documents onto the specified type slice (out)
    93  // by going through a temporary representation of the document as json so that we
    94  // use the json marshal logic defined on the struct, e.g. if fields have different
    95  // names defined with json tags.
    96  func (a PluginAdapter) unmarshalSlice(bsonResults []bson.Raw, out interface{}) error {
    97  	// We want to go from []bson.Raw -> []bson.M -> json -> out (typed struct)
    98  
    99  	// Populate a single document with all the results in an intermediate
   100  	// format of map[string]interface
   101  	tmpResults := make([]bson.M, len(bsonResults))
   102  	for i, bsonResult := range bsonResults {
   103  		var result bson.M
   104  		err := bson.Unmarshal(bsonResult, &result)
   105  		if err != nil {
   106  			return err
   107  		}
   108  		tmpResults[i] = result
   109  	}
   110  
   111  	// Marshal the consolidated document to json
   112  	data, err := json.Marshal(tmpResults)
   113  	if err != nil {
   114  		return fmt.Errorf("error marshaling results into a single result document: %w", err)
   115  	}
   116  
   117  	// Unmarshal the consolidated results onto our destination output
   118  	err = json.Unmarshal(data, out)
   119  	if err != nil {
   120  		return fmt.Errorf("could not unmarshal slice onto type %T: %w", out, err)
   121  	}
   122  
   123  	return nil
   124  }
   125  
   126  // unmarshalSlice a bson document onto the specified typed output
   127  // by going through a temporary representation of the document as json so that we
   128  // use the json marshal logic defined on the struct, e.g. if fields have different
   129  // names defined with json tags.
   130  func (a PluginAdapter) unmarshal(bsonResult bson.Raw, out interface{}) error {
   131  	// We want to go from bson.Raw -> bson.M -> json -> out (typed struct)
   132  
   133  	var tmpResult bson.M
   134  	err := bson.Unmarshal(bsonResult, &tmpResult)
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	// Marshal the consolidated document to json
   140  	data, err := json.Marshal(tmpResult)
   141  	if err != nil {
   142  		return fmt.Errorf("error marshaling results into a single result document: %w", err)
   143  	}
   144  
   145  	// Unmarshal the consolidated results onto our destination output
   146  	err = json.Unmarshal(data, out)
   147  	if err != nil {
   148  		return fmt.Errorf("could not unmarshal slice onto type %T: %w", out, err)
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  func (a PluginAdapter) Get(ctx context.Context, collection string, opts GetOptions, out interface{}) error {
   155  	findOpts := opts.ToFindOptions()
   156  	err := a.FindOne(ctx, collection, findOpts, out)
   157  	return a.handleError(err, collection)
   158  }
   159  
   160  func (a PluginAdapter) Insert(ctx context.Context, collection string, opts InsertOptions) error {
   161  	pluginOpts, err := opts.ToPluginOptions(collection)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	err = a.plugin.Insert(ctx, pluginOpts)
   167  	return a.handleError(err, collection)
   168  }
   169  
   170  func (a PluginAdapter) Patch(ctx context.Context, collection string, opts PatchOptions) error {
   171  	err := a.plugin.Patch(ctx, opts.ToPluginOptions(collection))
   172  	return a.handleError(err, collection)
   173  }
   174  
   175  func (a PluginAdapter) Remove(ctx context.Context, collection string, opts RemoveOptions) error {
   176  	err := a.plugin.Remove(ctx, opts.ToPluginOptions(collection))
   177  	return a.handleError(err, collection)
   178  }
   179  
   180  func (a PluginAdapter) Update(ctx context.Context, collection string, opts UpdateOptions) error {
   181  	pluginOpts, err := opts.ToPluginOptions(collection)
   182  	if err != nil {
   183  		return err
   184  	}
   185  	err = a.plugin.Update(ctx, pluginOpts)
   186  	return a.handleError(err, collection)
   187  }
   188  
   189  // handleError unwraps errors returned from a plugin (which due to the round trip
   190  // through the plugin framework means the original typed error may not be the right type anymore
   191  // and turns it back into a well known error such as NotFound.
   192  func (a PluginAdapter) handleError(err error, collection string) error {
   193  	if err != nil && strings.Contains(strings.ToLower(err.Error()), "not found") {
   194  		return ErrNotFound{Collection: collection}
   195  	}
   196  	return err
   197  }
   198  
   199  // ErrNotFound indicates that the requested document was not found.
   200  // You can test for this error using errors.Is(err, storage.ErrNotFound{})
   201  type ErrNotFound struct {
   202  	Collection string
   203  	Item       string
   204  }
   205  
   206  func (e ErrNotFound) Error() string {
   207  	var docType string
   208  	switch e.Collection {
   209  	case "installations":
   210  		docType = "Installation"
   211  	case "runs":
   212  		docType = "Run"
   213  	case "results":
   214  		docType = "Result"
   215  	case "output":
   216  		docType = "Output"
   217  	case "credentials", "parameters":
   218  		if len(e.Item) > 0 {
   219  			docType = e.Item
   220  		}
   221  	}
   222  
   223  	return fmt.Sprintf("%s not found", docType)
   224  }
   225  
   226  func (e ErrNotFound) Is(err error) bool {
   227  	_, ok := err.(ErrNotFound)
   228  	return ok
   229  }