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 }