github.com/hellofresh/janus@v0.0.0-20230925145208-ce8de8183c67/pkg/api/mongodb_repository.go (about) 1 package api 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 log "github.com/sirupsen/logrus" 9 "go.mongodb.org/mongo-driver/bson" 10 "go.mongodb.org/mongo-driver/mongo" 11 "go.mongodb.org/mongo-driver/mongo/options" 12 "go.mongodb.org/mongo-driver/mongo/readpref" 13 "go.mongodb.org/mongo-driver/x/mongo/driver/connstring" 14 ) 15 16 const ( 17 collectionName = "api_specs" 18 19 mongoConnTimeout = 10 * time.Second 20 mongoQueryTimeout = 10 * time.Second 21 ) 22 23 // MongoRepository represents a mongodb repository 24 type MongoRepository struct { 25 //TODO: we need to expose this so the plugins can use the same session. We should abstract mongo DB and provide 26 // the plugins with a simple interface to search, insert, update and remove data from whatever backend implementation 27 DB *mongo.Database 28 collection *mongo.Collection 29 client *mongo.Client 30 refreshTime time.Duration 31 } 32 33 // NewMongoAppRepository creates a mongo API definition repo 34 func NewMongoAppRepository(dsn string, refreshTime time.Duration) (*MongoRepository, error) { 35 log.WithField("dsn", dsn).Debug("Trying to connect to MongoDB...") 36 37 ctx, cancel := context.WithTimeout(context.Background(), mongoConnTimeout) 38 defer cancel() 39 40 connString, err := connstring.Parse(dsn) 41 if err != nil { 42 return nil, fmt.Errorf("could not parse mongodb connection string: %w", err) 43 } 44 45 client, err := mongo.Connect(ctx, options.Client().ApplyURI(dsn)) 46 if err != nil { 47 return nil, fmt.Errorf("could not connect to mongodb: %w", err) 48 } 49 50 if err := client.Ping(ctx, readpref.Primary()); err != nil { 51 return nil, fmt.Errorf("could not ping mongodb after connect: %w", err) 52 } 53 54 mongoDB := client.Database(connString.Database) 55 return &MongoRepository{ 56 DB: mongoDB, 57 client: client, 58 collection: mongoDB.Collection(collectionName), 59 refreshTime: refreshTime, 60 }, nil 61 } 62 63 // Close terminates underlying mongo connection. It's a runtime error to use a session 64 // after it has been closed. 65 func (r *MongoRepository) Close() error { 66 return r.client.Disconnect(context.TODO()) 67 } 68 69 // Listen watches for changes on the configuration 70 func (r *MongoRepository) Listen(ctx context.Context, cfgChan <-chan ConfigurationMessage) { 71 go func() { 72 log.Debug("Listening for changes on the provider...") 73 for { 74 select { 75 case cfg := <-cfgChan: 76 switch cfg.Operation { 77 case AddedOperation: 78 err := r.add(cfg.Configuration) 79 if err != nil { 80 log.WithError(err).Error("Could not add the configuration on the provider") 81 } 82 case UpdatedOperation: 83 err := r.add(cfg.Configuration) 84 if err != nil { 85 log.WithError(err).Error("Could not update the configuration on the provider") 86 } 87 case RemovedOperation: 88 err := r.remove(cfg.Configuration.Name) 89 if err != nil { 90 log.WithError(err).Error("Could not remove the configuration from the provider") 91 } 92 } 93 case <-ctx.Done(): 94 return 95 } 96 } 97 }() 98 } 99 100 // Watch watches for changes on the database 101 func (r *MongoRepository) Watch(ctx context.Context, cfgChan chan<- ConfigurationChanged) { 102 t := time.NewTicker(r.refreshTime) 103 go func(refreshTicker *time.Ticker) { 104 defer refreshTicker.Stop() 105 log.Debug("Watching Provider...") 106 for { 107 select { 108 case <-refreshTicker.C: 109 defs, err := r.FindAll() 110 if err != nil { 111 log.WithError(err).Error("Failed to get configurations on watch") 112 continue 113 } 114 115 cfgChan <- ConfigurationChanged{ 116 Configurations: &Configuration{Definitions: defs}, 117 } 118 case <-ctx.Done(): 119 return 120 } 121 } 122 }(t) 123 } 124 125 // FindAll fetches all the API definitions available 126 func (r *MongoRepository) FindAll() ([]*Definition, error) { 127 var result []*Definition 128 129 ctx, cancel := context.WithTimeout(context.Background(), mongoQueryTimeout) 130 defer cancel() 131 132 // sort by name to have the same order all the time - for easier comparison 133 cur, err := r.collection.Find(ctx, bson.M{}, options.Find().SetSort(bson.D{{Key: "name", Value: 1}})) 134 if err != nil { 135 return nil, err 136 } 137 defer cur.Close(ctx) 138 139 for cur.Next(ctx) { 140 d := new(Definition) 141 if err := cur.Decode(d); err != nil { 142 return nil, err 143 } 144 145 result = append(result, d) 146 } 147 148 return result, cur.Err() 149 } 150 151 // Add adds an API definition to the repository 152 func (r *MongoRepository) add(definition *Definition) error { 153 isValid, err := definition.Validate() 154 if false == isValid && err != nil { 155 log.WithError(err).Error("Validation errors") 156 return err 157 } 158 159 ctx, cancel := context.WithTimeout(context.Background(), mongoQueryTimeout) 160 defer cancel() 161 162 if err := r.collection.FindOneAndUpdate( 163 ctx, 164 bson.M{"name": definition.Name}, 165 bson.M{"$set": definition}, 166 options.FindOneAndUpdate().SetUpsert(true), 167 ).Err(); err != nil { 168 log.WithField("name", definition.Name).Error("There was an error adding the resource") 169 return err 170 } 171 172 log.WithField("name", definition.Name).Debug("Resource added") 173 return nil 174 } 175 176 // Remove removes an API definition from the repository 177 func (r *MongoRepository) remove(name string) error { 178 ctx, cancel := context.WithTimeout(context.Background(), mongoQueryTimeout) 179 defer cancel() 180 181 res, err := r.collection.DeleteOne(ctx, bson.M{"name": name}) 182 if err != nil { 183 log.WithField("name", name).Error("There was an error removing the resource") 184 return err 185 } 186 187 if res.DeletedCount < 1 { 188 return ErrAPIDefinitionNotFound 189 } 190 191 log.WithField("name", name).Debug("Resource removed") 192 return nil 193 }