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

     1  package mongodb
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  	"time"
     9  
    10  	"get.porter.sh/porter/pkg/portercontext"
    11  	"get.porter.sh/porter/pkg/storage/plugins"
    12  	"get.porter.sh/porter/pkg/tracing"
    13  	"github.com/davecgh/go-spew/spew"
    14  	"go.mongodb.org/mongo-driver/bson"
    15  	"go.mongodb.org/mongo-driver/mongo"
    16  	"go.mongodb.org/mongo-driver/mongo/options"
    17  	"go.mongodb.org/mongo-driver/mongo/readpref"
    18  	"go.mongodb.org/mongo-driver/x/mongo/driver/connstring"
    19  	"go.opentelemetry.io/otel/attribute"
    20  )
    21  
    22  var (
    23  	_               plugins.StorageProtocol = &Store{}
    24  	ErrNotConnected                         = errors.New("cannot execute command against the mongodb plugin because the session is closed (or was never connected)")
    25  )
    26  
    27  // Store implements the Porter plugin.StoragePlugin interface for mongodb.
    28  type Store struct {
    29  	*portercontext.Context
    30  
    31  	url      string
    32  	database string
    33  	client   *mongo.Client
    34  	timeout  time.Duration
    35  }
    36  
    37  // NewStore creates a new storage engine that uses MongoDB.
    38  func NewStore(c *portercontext.Context, cfg PluginConfig) *Store {
    39  	timeout := cfg.Timeout
    40  	if timeout <= 0 {
    41  		timeout = 10 // default to 10 seconds
    42  	}
    43  	return &Store{
    44  		Context: c,
    45  		url:     cfg.URL,
    46  		timeout: time.Duration(timeout) * time.Second,
    47  	}
    48  }
    49  
    50  // Connect initializes the plugin for use.
    51  // The plugin itself is responsible for ensuring it was called.
    52  // Close is called automatically when the plugin is used by Porter.
    53  func (s *Store) Connect(ctx context.Context) error {
    54  	if s.client != nil {
    55  		return nil
    56  	}
    57  
    58  	ctx, span := tracing.StartSpan(ctx)
    59  	defer span.EndSpan()
    60  
    61  	connStr, err := connstring.ParseAndValidate(s.url)
    62  	if err != nil {
    63  		// I'm not tracing additional information like the url since it is sensitive data
    64  		return span.Error(fmt.Errorf("invalid mongodb connection string"))
    65  	}
    66  
    67  	if connStr.Database == "" {
    68  		s.database = "porter"
    69  	} else {
    70  		s.database = strings.TrimSuffix(connStr.Database, "/")
    71  	}
    72  	span.SetAttributes(attribute.String("database", s.database))
    73  
    74  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
    75  	defer cancel()
    76  
    77  	client, err := mongo.Connect(cxt, options.Client().ApplyURI(s.url))
    78  	if err != nil {
    79  		return span.Error(err)
    80  	}
    81  
    82  	s.client = client
    83  	return nil
    84  }
    85  
    86  func (s *Store) Close() error {
    87  	if s.client != nil {
    88  		cxt, cancel := context.WithTimeout(context.Background(), s.timeout)
    89  		defer cancel()
    90  
    91  		if err := s.client.Disconnect(cxt); err != nil {
    92  			return err
    93  		}
    94  		s.client = nil
    95  	}
    96  	return nil
    97  }
    98  
    99  // Ping the connected session to check if everything is okay.
   100  func (s *Store) Ping(ctx context.Context) error {
   101  	if err := s.Connect(ctx); err != nil {
   102  		return err
   103  	}
   104  
   105  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   106  	defer cancel()
   107  	return s.client.Ping(cxt, readpref.Primary())
   108  }
   109  
   110  func (s *Store) Aggregate(ctx context.Context, opts plugins.AggregateOptions) ([]bson.Raw, error) {
   111  	ctx, span := tracing.StartSpan(ctx)
   112  	defer span.EndSpan()
   113  	if err := s.Connect(ctx); err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	// TODO(carolynvs): wrap each call with session.refresh  on error and a single retry
   118  	c := s.getCollection(opts.Collection)
   119  
   120  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   121  	defer cancel()
   122  	cur, err := c.Aggregate(cxt, opts.Pipeline)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	var results []bson.Raw
   128  	for cur.Next(cxt) {
   129  		results = append(results, cur.Current)
   130  	}
   131  	return results, err
   132  }
   133  
   134  // EnsureIndexes makes sure that the specified indexes exist and are
   135  // defined appropriately.
   136  func (s *Store) EnsureIndex(ctx context.Context, opts plugins.EnsureIndexOptions) error {
   137  	ctx, span := tracing.StartSpan(ctx)
   138  	defer span.EndSpan()
   139  	if err := s.Connect(ctx); err != nil {
   140  		return err
   141  	}
   142  
   143  	indices := make(map[string][]mongo.IndexModel, len(opts.Indices))
   144  	for _, index := range opts.Indices {
   145  		model := mongo.IndexModel{
   146  			Keys:    index.Keys,
   147  			Options: options.Index(),
   148  		}
   149  		model.Options.SetUnique(index.Unique)
   150  
   151  		c, ok := indices[index.Collection]
   152  		if !ok {
   153  			c = make([]mongo.IndexModel, 0, 1)
   154  		}
   155  		c = append(c, model)
   156  		indices[index.Collection] = c
   157  	}
   158  
   159  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   160  	defer cancel()
   161  	for collectionName, models := range indices {
   162  		c := s.getCollection(collectionName)
   163  		if _, err := c.Indexes().CreateMany(cxt, models); err != nil {
   164  			return span.Error(fmt.Errorf("invalid index specified: %v: %w", spew.Sdump(models), err))
   165  		}
   166  	}
   167  
   168  	return nil
   169  }
   170  
   171  func (s *Store) Count(ctx context.Context, opts plugins.CountOptions) (int64, error) {
   172  	ctx, span := tracing.StartSpan(ctx)
   173  	defer span.EndSpan()
   174  	if err := s.Connect(ctx); err != nil {
   175  		return 0, err
   176  	}
   177  
   178  	c := s.getCollection(opts.Collection)
   179  
   180  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   181  	defer cancel()
   182  	count, err := c.CountDocuments(cxt, opts.Filter)
   183  	return count, span.Error(err)
   184  }
   185  
   186  func (s *Store) Find(ctx context.Context, opts plugins.FindOptions) ([]bson.Raw, error) {
   187  	ctx, span := tracing.StartSpan(ctx)
   188  	defer span.EndSpan()
   189  	if err := s.Connect(ctx); err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	c := s.getCollection(opts.Collection)
   194  	findOpts, err := s.buildFindOptions(opts)
   195  	if err != nil {
   196  		return nil, span.Error(err)
   197  	}
   198  
   199  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   200  	defer cancel()
   201  	cur, err := c.Find(cxt, opts.Filter, findOpts)
   202  	if err != nil {
   203  		return nil, span.Error(err)
   204  	}
   205  	defer cur.Close(ctx)
   206  
   207  	var results []bson.Raw
   208  	for cur.Next(cxt) {
   209  		results = append(results, cur.Current)
   210  	}
   211  	return results, span.Error(err)
   212  }
   213  
   214  func (s *Store) buildFindOptions(opts plugins.FindOptions) (*options.FindOptions, error) {
   215  	query := options.Find()
   216  
   217  	if opts.Select != nil {
   218  		query.SetProjection(opts.Select)
   219  	}
   220  
   221  	if opts.Limit > 0 {
   222  		query.SetLimit(opts.Limit)
   223  	}
   224  
   225  	if opts.Skip > 0 {
   226  		query.SetSkip(opts.Skip)
   227  	}
   228  
   229  	if opts.Sort != nil {
   230  		query.SetSort(opts.Sort)
   231  	}
   232  
   233  	return query, nil
   234  }
   235  
   236  func (s *Store) Insert(ctx context.Context, opts plugins.InsertOptions) error {
   237  	ctx, span := tracing.StartSpan(ctx)
   238  	defer span.EndSpan()
   239  
   240  	if err := s.Connect(ctx); err != nil {
   241  		return err
   242  	}
   243  
   244  	c := s.getCollection(opts.Collection)
   245  
   246  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   247  	defer cancel()
   248  
   249  	docs := make([]interface{}, len(opts.Documents))
   250  	for i, doc := range opts.Documents {
   251  		docs[i] = doc
   252  	}
   253  	_, err := c.InsertMany(cxt, docs)
   254  	return span.Error(err)
   255  }
   256  
   257  func (s *Store) Patch(ctx context.Context, opts plugins.PatchOptions) error {
   258  	ctx, span := tracing.StartSpan(ctx)
   259  	defer span.EndSpan()
   260  
   261  	if err := s.Connect(ctx); err != nil {
   262  		return err
   263  	}
   264  
   265  	c := s.getCollection(opts.Collection)
   266  
   267  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   268  	defer cancel()
   269  	_, err := c.UpdateOne(cxt, opts.QueryDocument, opts.Transformation)
   270  	return span.Error(err)
   271  }
   272  
   273  func (s *Store) Remove(ctx context.Context, opts plugins.RemoveOptions) error {
   274  	ctx, span := tracing.StartSpan(ctx)
   275  	defer span.EndSpan()
   276  
   277  	if err := s.Connect(ctx); err != nil {
   278  		return err
   279  	}
   280  
   281  	c := s.getCollection(opts.Collection)
   282  
   283  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   284  	defer cancel()
   285  
   286  	if opts.All {
   287  		_, err := c.DeleteMany(ctx, opts.Filter)
   288  		return span.Error(err)
   289  	}
   290  
   291  	_, err := c.DeleteOne(cxt, opts.Filter)
   292  	return span.Error(err)
   293  }
   294  
   295  func (s *Store) Update(ctx context.Context, opts plugins.UpdateOptions) error {
   296  	ctx, span := tracing.StartSpan(ctx)
   297  	defer span.EndSpan()
   298  
   299  	if err := s.Connect(ctx); err != nil {
   300  		return err
   301  	}
   302  
   303  	c := s.getCollection(opts.Collection)
   304  
   305  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   306  	defer cancel()
   307  
   308  	_, err := c.ReplaceOne(cxt, opts.Filter, opts.Document, &options.ReplaceOptions{Upsert: &opts.Upsert})
   309  	return span.Error(err)
   310  }
   311  
   312  func (s *Store) getCollection(collection string) *mongo.Collection {
   313  	return s.client.Database(s.database).Collection(collection)
   314  }
   315  
   316  // RemoveDatabase removes the current database.
   317  func (s *Store) RemoveDatabase(ctx context.Context) error {
   318  	ctx, span := tracing.StartSpan(ctx)
   319  	defer span.EndSpan()
   320  
   321  	if err := s.Connect(ctx); err != nil {
   322  		return err
   323  	}
   324  
   325  	cxt, cancel := context.WithTimeout(ctx, s.timeout)
   326  	defer cancel()
   327  
   328  	span.Info("Dropping database", attribute.String("database", s.database))
   329  	return s.client.Database(s.database).Drop(cxt)
   330  }