add rdbms support for cdn metadata storage (#151)

* add rdbms support for cdn metadata storage

* move cdn metadata model to the metadata pkg

* simplify get

* add mongo saver and test

* remove 'stutters'

* change default table name

* missing cdn metadata pkg rename
This commit is contained in:
marpio
2018-06-06 22:59:26 +02:00
committed by Aaron Schlesinger
parent f08d3138bc
commit 977d816c89
24 changed files with 348 additions and 64 deletions
+3 -3
View File
@@ -6,13 +6,13 @@ import (
"strings"
"github.com/gobuffalo/buffalo"
"github.com/gomods/athens/pkg/cdn"
cdnmetadata "github.com/gomods/athens/pkg/cdn/metadata"
"github.com/gomods/athens/pkg/paths"
)
// GoGet is middleware that checks for the 'go-get=1' query string. If it exists,
// uses getter to determine the redirect location
func GoGet(getter cdn.Getter) buffalo.MiddlewareFunc {
func GoGet(getter cdnmetadata.Getter) buffalo.MiddlewareFunc {
return func(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
if strings.Contains(c.Request().URL.Query().Get("go-get"), "1") {
@@ -23,7 +23,7 @@ func GoGet(getter cdn.Getter) buffalo.MiddlewareFunc {
}
}
func goGetMeta(c buffalo.Context, getter cdn.Getter) error {
func goGetMeta(c buffalo.Context, getter cdnmetadata.Getter) error {
params, err := paths.GetAllParams(c)
if err != nil {
return err
+3 -3
View File
@@ -1,10 +1,10 @@
package actions
import (
"github.com/gomods/athens/pkg/cdn"
"github.com/gomods/athens/pkg/cdn/fake"
cdnmetadata "github.com/gomods/athens/pkg/cdn/metadata"
"github.com/gomods/athens/pkg/cdn/metadata/fake"
)
func newCDNGetter() cdn.Getter {
func newCDNGetter() cdnmetadata.Getter {
return &fake.Getter{URL: "https://mycdn.com"}
}
+3 -3
View File
@@ -5,13 +5,13 @@ import (
"net/http"
"github.com/gobuffalo/buffalo"
"github.com/gomods/athens/pkg/cdn"
cdnmetadata "github.com/gomods/athens/pkg/cdn/metadata"
"github.com/gomods/athens/pkg/paths"
)
// GoGet is middleware that checks for the 'go-get=1' query string. If it exists,
// uses getter to determine the redirect location
func GoGet(getter cdn.Getter) buffalo.MiddlewareFunc {
func GoGet(getter cdnmetadata.Getter) buffalo.MiddlewareFunc {
return func(next buffalo.Handler) buffalo.Handler {
return func(c buffalo.Context) error {
if paths.IsGoGet(c.Request().URL) {
@@ -22,7 +22,7 @@ func GoGet(getter cdn.Getter) buffalo.MiddlewareFunc {
}
}
func goGetMeta(c buffalo.Context, getter cdn.Getter) error {
func goGetMeta(c buffalo.Context, getter cdnmetadata.Getter) error {
params, err := paths.GetAllParams(c)
if err != nil {
return err
@@ -0,0 +1 @@
drop_table("cdn_metadata_entries")
@@ -0,0 +1,5 @@
create_table("cdn_metadata_entries", func(t) {
t.Column("id", "uuid", {"primary": true})
t.Column("module", "text", {})
t.Column("redirect_url", "text", {})
})
+55
View File
@@ -0,0 +1,55 @@
package metadata
import (
"encoding/json"
"time"
"github.com/gobuffalo/pop"
"github.com/gobuffalo/uuid"
"github.com/gobuffalo/validate"
"github.com/gobuffalo/validate/validators"
)
// CDNMetadataEntry stores the module name and cdn URL.
type CDNMetadataEntry struct {
ID uuid.UUID `json:"id" db:"id" bson:"id"`
CreatedAt time.Time `json:"created_at" db:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at" bson:"updated_at"`
Module string `json:"module" db:"module" bson:"module"`
RedirectURL string `json:"redirect_url" db:"redirect_url" bson:"redirect_url"`
}
// String is not required by pop and may be deleted
func (e CDNMetadataEntry) String() string {
je, _ := json.Marshal(e)
return string(je)
}
// CdnMetadataEntries is not required by pop and may be deleted
type CdnMetadataEntries []CDNMetadataEntry
// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method.
// This method is not required and may be deleted.
func (e *CDNMetadataEntry) Validate(tx *pop.Connection) (*validate.Errors, error) {
return validate.Validate(
&validators.StringIsPresent{Field: e.Module, Name: "Module"},
&validators.StringIsPresent{Field: e.RedirectURL, Name: "RedirectURL"},
), nil
}
// ValidateCreate gets run every time you call "pop.ValidateAndCreate" method.
// This method is not required and may be deleted.
func (e *CDNMetadataEntry) ValidateCreate(tx *pop.Connection) (*validate.Errors, error) {
return validate.NewErrors(), nil
}
// ValidateUpdate gets run every time you call "pop.ValidateAndUpdate" method.
// This method is not required and may be deleted.
func (e *CDNMetadataEntry) ValidateUpdate(tx *pop.Connection) (*validate.Errors, error) {
return validate.NewErrors(), nil
}
// TableName changes the default name which would be c_d_n_metadata_entry
func (e *CDNMetadataEntry) TableName() string {
return "cdn_metadata_entries"
}
@@ -1,6 +1,6 @@
package fake
// Getter is a (./pkg/cdn).Getter implementation that always returns URL
// Getter is a (./pkg/cdn/metadata).Getter implementation that always returns URL
type Getter struct {
URL string
}
@@ -1,4 +1,4 @@
package cdn
package metadata
// Getter gets the details about a given baseURL & module and returns the base
// URL of the module metadata and content. For example, if
+15
View File
@@ -0,0 +1,15 @@
package mongo
import (
"github.com/globalsign/mgo/bson"
"github.com/gomods/athens/pkg/cdn/metadata"
)
// Get retrieves the cdn base URL for a module
func (s *MetadataStore) Get(module string) (string, error) {
coll := s.session.DB(s.db).C(s.col)
params := bson.M{"module": module}
entry := metadata.CDNMetadataEntry{}
err := coll.Find(params).One(&entry)
return entry.RedirectURL, err
}
+39
View File
@@ -0,0 +1,39 @@
package mongo
import "github.com/globalsign/mgo"
// MetadataStore represents a Mongo backed metadata store.
type MetadataStore struct {
session *mgo.Session
db string
col string
url string
}
// NewStorage returns an unconnected Mongo backed storage
// that satisfies the Storage interface. You must call
// Connect() on the returned store before using it.
func NewStorage(url, dbName string) *MetadataStore {
return &MetadataStore{url: url, db: dbName}
}
// Connect conntect the the newly created mongo backend.
func (m *MetadataStore) Connect() error {
s, err := mgo.Dial(m.url)
if err != nil {
return err
}
m.session = s
m.col = "cdn_metadata"
index := mgo.Index{
Key: []string{"base_url", "module"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
c := m.session.DB(m.db).C(m.col)
return c.EnsureIndex(index)
}
+51
View File
@@ -0,0 +1,51 @@
package mongo
import (
"testing"
"github.com/gomods/athens/pkg/cdn/metadata"
"github.com/gomods/athens/pkg/fixtures"
"github.com/stretchr/testify/suite"
)
const (
module = "testmodule"
redirectURL = "https://mycdn.com/gomods.io/my/testmodule"
)
type MongoTests struct {
*fixtures.Mongo
storage metadata.StorageConnector
}
func TestMongo(t *testing.T) {
suite.Run(t, &MongoTests{Mongo: fixtures.NewMongo(fixtures.DefaultMongoURL)})
}
func (m *MongoTests) SetupTest() {
m.Mongo.SetupTest()
store := NewStorage(fixtures.DefaultMongoURL, m.Mongo.DBName)
store.Connect()
m.storage = store
}
func (m *MongoTests) TestGetSaveListRoundTrip() {
r := m.Require()
err := m.storage.Save(module, redirectURL)
r.NoError(err)
gotten, err := m.storage.Get(module)
r.NoError(err)
r.Equal(gotten, redirectURL)
}
func (m *MongoTests) TestNewMongoStorage() {
r := m.Require()
getterSaver := NewStorage(fixtures.DefaultMongoURL, m.Mongo.DBName)
getterSaver.Connect()
r.NotNil(getterSaver.col)
r.NotNil(getterSaver.db)
r.NotNil(getterSaver.session)
r.Equal(getterSaver.url, fixtures.DefaultMongoURL)
}
+26
View File
@@ -0,0 +1,26 @@
package mongo
import (
"time"
"github.com/gobuffalo/uuid"
"github.com/gomods/athens/pkg/cdn/metadata"
)
// Save stores a module in mongo storage.
func (s *MetadataStore) Save(module, redirectURL string) error {
id, err := uuid.NewV4()
if err != nil {
return err
}
m := &metadata.CDNMetadataEntry{
ID: id,
Module: module,
RedirectURL: redirectURL,
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
c := s.session.DB(s.db).C(s.col)
return c.Insert(m)
}
+27
View File
@@ -0,0 +1,27 @@
package rdbms
import (
"testing"
"github.com/gobuffalo/suite"
"github.com/gomods/athens/pkg/cdn/metadata"
)
const (
module = "testmodule"
redirectURL = "https://mycdn.com/gomods.io/my/testmodule"
)
type RDBMSTestSuite struct {
*suite.Model
storage metadata.StorageConnector
}
func (rd *RDBMSTestSuite) SetupTest() {
rd.storage = &MetadataStore{conn: rd.DB}
rd.Model.SetupTest()
}
func Test_ActionSuite(t *testing.T) {
suite.Run(t, &RDBMSTestSuite{Model: suite.NewModel()})
}
+11
View File
@@ -0,0 +1,11 @@
package rdbms
import "github.com/gomods/athens/pkg/cdn/metadata"
// Get retrieves the cdn base URL for a module
func (s *MetadataStore) Get(module string) (string, error) {
result := metadata.CDNMetadataEntry{}
query := s.conn.Where("module = ?", module)
err := query.First(&result)
return result.RedirectURL, err
}
+31
View File
@@ -0,0 +1,31 @@
package rdbms
import (
"github.com/gobuffalo/pop"
)
// MetadataStore represents a rdbms(postgres, mysql, sqlite, cockroachdb) backed metadata store.
type MetadataStore struct {
conn *pop.Connection
connectionName string // settings name from database.yml
}
// NewStorage returns an unconnected RDBMS Metadata Storage
// that satisfies the Getter and Setter interfaces. You must call
// Connect() on the returned store before using it.
// connectionName
func NewStorage(connectionName string) *MetadataStore {
return &MetadataStore{
connectionName: connectionName,
}
}
// Connect creates connection to rdmbs backend.
func (r *MetadataStore) Connect() error {
c, err := pop.Connect(r.connectionName)
if err != nil {
return err
}
r.conn = c
return nil
}
+21
View File
@@ -0,0 +1,21 @@
package rdbms
func (rd *RDBMSTestSuite) TestGetSaveRoundTrip() {
r := rd.Require()
err := rd.storage.Save(module, redirectURL)
r.NoError(err)
gotten, err := rd.storage.Get(module)
r.NoError(err)
r.Equal(gotten, redirectURL)
}
func (rd *RDBMSTestSuite) TestNewRDBMSStorage() {
r := rd.Require()
e := "development"
getterSaver := NewStorage(e)
getterSaver.Connect()
r.NotNil(getterSaver.conn)
r.Equal(getterSaver.connectionName, e)
}
+9
View File
@@ -0,0 +1,9 @@
package rdbms
import "github.com/gomods/athens/pkg/cdn/metadata"
// Save saves the module and it's cdn base URL.
func (s *MetadataStore) Save(module, redirectURL string) error {
r := metadata.CDNMetadataEntry{Module: module, RedirectURL: redirectURL}
return s.conn.Create(&r)
}
+9
View File
@@ -0,0 +1,9 @@
package metadata
// Saver saves the module's base URL
// of the module metadata and content. For example:
// module: gomods.io/my/module - URL: 'https://mycdn.com/gomods.io/my/module'
type Saver interface {
// Save saves the module and base URL pair.
Save(module, redirectURL string) error
}
+7
View File
@@ -0,0 +1,7 @@
package metadata
// Storage is a complete cdn metadata storage backend (i.e. file system, database) implementation - a getter and saver
type Storage interface {
Getter
Saver
}
+27
View File
@@ -0,0 +1,27 @@
package metadata
// StorageConnector is a regular storage Storage with Connect functionality
type StorageConnector interface {
Storage
Connect() error
}
type noOpConnectedStorage struct {
s Storage
}
// NoOpStorageConnector wraps storage Storage with Connect functionality
func NoOpStorageConnector(s Storage) StorageConnector {
return noOpConnectedStorage{s: s}
}
func (n noOpConnectedStorage) Connect() error {
return nil
}
func (n noOpConnectedStorage) Get(module string) (string, error) {
return n.s.Get(module)
}
func (n noOpConnectedStorage) Save(module, redirectURL string) error {
return n.s.Save(module, redirectURL)
}
-9
View File
@@ -1,9 +0,0 @@
package mongo
// Entry is stored in the DB. Right now it just holds a redirect URL to
// the CDN
type Entry struct {
Module string `bson:"module"`
RedirectURL string `bson:"redirect_url"`
// Other fields?
}
-22
View File
@@ -1,22 +0,0 @@
package mongo
import (
"github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
)
type getter struct {
conn *mgo.Session
db string
coll string
}
func (g *getter) Get(module string) (string, error) {
coll := g.conn.DB(g.db).C(g.coll)
params := bson.M{"module": module}
entry := Entry{}
if err := coll.Find(params).One(&entry); err != nil {
return "", nil
}
return entry.RedirectURL, nil
}
-19
View File
@@ -1,19 +0,0 @@
package mongo
import (
"testing"
"github.com/gomods/athens/pkg/fixtures"
"github.com/stretchr/testify/suite"
)
type MongoTests struct {
*fixtures.Mongo
}
func RunMongoTests(t *testing.T) {
suite.Run(t, &MongoTests{Mongo: fixtures.NewMongo(fixtures.DefaultMongoURL)})
}
// TODO: add round-trip tests when a mongo saver is done
// (https://github.com/gomods/athens/issues/50)
+3 -3
View File
@@ -25,7 +25,7 @@ const DefaultMongoURL = "127.0.0.1:27017"
type Mongo struct {
suite.Suite
url string
dbName string
DBName string
DB *mgo.Database
}
@@ -36,7 +36,7 @@ type Mongo struct {
func (m *Mongo) SetupTest() {
sess, err := mgo.Dial(m.url)
m.Require().NoError(err)
m.DB = sess.DB(m.dbName)
m.DB = sess.DB(m.DBName)
}
// TearDownTest drops the database that was created in SetupTest
@@ -50,6 +50,6 @@ func (m *Mongo) TearDownTest() {
func NewMongo(url string) *Mongo {
return &Mongo{
url: url,
dbName: names.NameSep("-") + "-athens-testing",
DBName: names.NameSep("-") + "-athens-testing",
}
}