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  }