diff --git a/internal/database/boltdb.go b/internal/database/boltdb.go index 35890f6..16807d7 100644 --- a/internal/database/boltdb.go +++ b/internal/database/boltdb.go @@ -63,7 +63,7 @@ func NewBoltStorage(path string, key *[32]byte) (Storage, error) { b.rs = &boltRouteStorage{b} b.ts = &boltTokenStorage{b} - return Storage(b), nil + return b, nil } // Certs gets the certificate storage interface. diff --git a/internal/database/common_test.go b/internal/database/common_test.go index 73722d0..8b5873d 100644 --- a/internal/database/common_test.go +++ b/internal/database/common_test.go @@ -25,3 +25,19 @@ func newTestBoltStorage(t *testing.T) (Storage, string, context.Context, context return st, p, ctx, cancel } + +func newTestPostgresStorage(t *testing.T, url string) (Storage, context.Context, context.CancelFunc) { + k, err := routecrypto.ParseKey(cryptoKey) + if err != nil { + t.Fatal(err) + } + + st, err := NewPostgresStorage(url, k) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + + return st, ctx, cancel +} diff --git a/internal/database/dmigrations/bindata.go b/internal/database/dmigrations/bindata.go index db671ae..32075d7 100644 --- a/internal/database/dmigrations/bindata.go +++ b/internal/database/dmigrations/bindata.go @@ -194,7 +194,7 @@ func _1513982254_tokensUpSql() (*asset, error) { return a, nil } -var _postgresSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x94\x51\xeb\xda\x30\x14\xc5\xdf\xf3\x29\xee\xdb\xdf\x42\xf3\xb0\xa7\xc1\x44\x8a\x68\xc6\x0a\x55\xa1\x76\xdb\xa3\xc4\xe6\xba\x85\xd5\x46\x92\x28\xf3\xdb\x8f\xd4\xa8\x69\xd5\x22\x43\xc1\x07\x49\xc9\x39\xbf\x9c\x7b\x12\x4a\xa1\xe6\x5b\xfc\x02\xb2\x36\xa8\x2d\x2d\x51\x5b\xb9\x91\x25\xb7\x48\x48\x3a\x5f\xb2\xbc\x80\x74\x5e\x2c\x60\x72\xfd\x60\x08\xc0\x40\xa8\x2d\x97\x75\x0c\x82\x5b\x1e\x91\x1f\xe3\xec\x3b\x5b\xba\xf5\x24\x86\x24\x1a\x12\x72\x11\xde\xc8\x5a\x50\x55\x63\x5b\x7a\xc9\x32\x36\x29\xdc\x06\x29\x62\x08\xc5\x62\x28\x35\x72\x8b\x62\xc5\x6d\x0c\x28\xe4\xf9\x2f\x2f\xad\x3c\x60\x44\xbe\xe6\x8b\x59\x1b\xe7\xe7\x37\x96\x33\x2f\x32\x4a\x48\x96\xce\xd2\x02\x3e\x85\x10\x1a\xb7\xea\x80\xb7\x18\x53\x96\xb1\x82\xfd\x97\xe4\x2f\xb4\x94\x57\x55\xa8\x67\x5e\x7b\xae\xd0\xcd\x8f\x47\xab\x7d\x67\x30\x04\x20\x77\x8b\x66\xd0\xc8\x2b\x1d\xc3\x6f\x65\xac\xdb\xf5\xdc\x58\x1a\x49\xba\x3e\x52\x29\xba\xf8\x37\x8a\xcf\x9d\xe1\xc4\xe3\x23\x94\xe2\x7e\x7c\xb7\xfe\xce\xe4\x1d\x04\xe7\xad\x3d\x1c\x6e\x8e\x0d\x87\xa1\x1b\xa5\xe9\xde\xa0\x7e\x07\x89\xd7\x18\x25\x21\x80\xc0\x0a\x2d\x5e\xa3\x68\x97\x32\xdc\x4f\xa0\x49\x13\xc6\xf3\x69\x6f\x33\x7d\x57\xac\xfa\x83\x75\xfb\x12\x17\x6e\xa9\xb9\xbe\x6b\x25\x8e\xc1\xa1\x4c\xa9\x76\x68\x62\xc0\xbf\x3b\xa9\xd1\xac\xb8\xed\x96\xe7\xf4\x8b\xba\x37\xc0\x61\x7b\xa3\x76\x5e\x0f\x0c\x5a\x99\x5d\xcc\x3a\xa1\x79\xcc\xfe\x02\xb5\xdc\x5d\x7f\x9c\xe3\xdb\x28\x9c\x50\xff\x3b\xd0\x70\x3c\xee\xcf\xcb\x48\xee\x96\x28\x78\xdf\xfc\x34\xc2\x12\x3d\x95\xa7\x97\x38\x41\x08\x7f\x9c\x1e\x9d\x2b\x2d\x50\x90\xb5\x45\x7d\xe0\x15\x7c\x7c\x06\xc1\x8f\xe6\x63\x48\xfe\x05\x00\x00\xff\xff\x1c\x82\x69\x4c\x5a\x06\x00\x00") +var _postgresSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x94\x4f\x8b\xea\x30\x14\xc5\xf7\xfd\x14\x77\x67\x85\x66\xf1\x56\x0f\xde\xe3\x21\xa2\x79\x4c\xa1\x2a\xd4\xce\xcc\x52\x62\x73\x9d\x09\x53\x1b\x49\xa2\x8c\xdf\x7e\x48\x5b\x35\xd1\xfa\x87\xa1\x03\x5d\x94\x40\xce\xf9\xdd\x73\x4f\x4b\x08\x94\x6c\x8d\x7f\x40\x94\x1a\x95\x21\x39\x2a\x23\x56\x22\x67\x06\x83\x78\x3a\xa7\x69\x06\xf1\x34\x9b\xc1\xe8\x74\xae\x43\x2e\xd7\x4c\x94\x11\x70\x66\x58\x1f\x5e\x86\xc9\x33\x9d\x43\x38\x88\x60\xd0\xff\x1b\x04\x47\xc9\x95\x28\x39\x91\x25\x7a\xa2\x73\x9a\xd0\x51\x06\x82\x47\xe0\xca\x44\x90\x2b\x64\x06\xf9\x82\x99\x08\x90\x8b\xc3\x2b\xcb\x8d\xd8\x21\xfc\x4f\x67\x13\x0f\x02\x5e\x9f\x68\x4a\x1b\x0d\xf8\x07\x03\x48\xe2\x49\x9c\xc1\x2f\x07\x40\xe1\x5a\xee\xf0\x02\x61\x4c\x13\x9a\xd1\xef\x49\xbe\xa1\x21\xac\x28\x5c\x3d\xdd\xd9\x4c\x8e\x4f\xb3\x0e\x25\xb7\x67\x8b\x48\xed\x89\x0e\x2b\x65\xa9\x22\x78\x97\xda\xd8\x2b\x67\x7b\x68\x5b\x43\x25\x46\x96\x7b\x22\xb8\x8b\x7c\x21\xf5\x10\x77\xcd\xd1\x44\x26\xf8\x95\xb8\x2e\xbd\xad\x49\xc7\xee\x87\x9b\xb7\x18\xec\xce\x2a\x06\x4d\x56\x52\x91\xad\x46\xd5\x31\x45\x23\x61\x21\x1c\x73\x8e\x05\x1a\x3c\x45\xe0\x95\xaf\x2d\xc3\xe1\x74\x7c\xbb\x81\x4d\x33\x8c\xfc\xc0\xd2\x6b\x46\x66\x4f\x74\xb8\x94\x7c\xef\x0c\xa4\x73\xb9\x41\x1d\x01\x7e\x6e\x84\x42\xbd\x60\xc6\x6f\x4a\xfd\xf4\xcf\x2a\x6e\x79\x6b\x07\x27\xa4\x2b\xca\x5e\x50\x47\x17\x3f\xa9\x9a\xed\x5e\x5b\x3c\x67\x5b\x16\xeb\xf8\x13\x04\x56\xe7\xce\x07\x5e\x31\xb4\x97\xa5\x2b\x8a\xf6\xc6\x38\x3f\xad\x7a\x03\x6e\x63\x1e\xcb\xb1\x91\xa8\x21\x78\x33\xca\x75\x9d\x13\x2c\x10\x10\xa5\x41\xb5\x63\x05\xf4\x7e\x03\x67\x7b\xdd\x0b\xbe\x02\x00\x00\xff\xff\x49\x2d\x7b\x8a\x23\x06\x00\x00") func postgresSqlBytes() ([]byte, error) { return bindataRead( @@ -209,7 +209,7 @@ func postgresSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "postgres.sql", size: 1626, mode: os.FileMode(420), modTime: time.Unix(1516564385, 0)} + info := bindataFileInfo{name: "postgres.sql", size: 1571, mode: os.FileMode(420), modTime: time.Unix(1516571679, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/internal/database/migrations/postgres.sql b/internal/database/migrations/postgres.sql index 4335f58..0128f80 100644 --- a/internal/database/migrations/postgres.sql +++ b/internal/database/migrations/postgres.sql @@ -1,108 +1,42 @@ -- name: insert-certificate - -INSERT INTO Certificates - (domain, data) -VALUES - (?, ?); +INSERT INTO Certificates(domain, data) VALUES ($1, $2); -- name: find-one-certificate - -SELECT - (id, domain, data, created_at, edited_at, active) -FROM Certificates -WHERE domain=? -LIMIT 1; +SELECT id, domain, data, created_at, edited_at, active FROM Certificates WHERE domain = $1 LIMIT 1; -- name: remove-one-certificate - -DELETE -FROM Certificates -WHERE domain=? -LIMIT 1; +DELETE FROM Certificates WHERE domain = $1 LIMIT 1; -- name: get-all-certificates - -SELECT - (id, domain, data, created_at, edited_at, active) -FROM Certificates; +SELECT id, domain, data, created_at, edited_at, active FROM Certificates; -- name: insert-route - -INSERT INTO - Routes(creator, hostname) -VALUES - (?, ?); +INSERT INTO Routes(creator, hostname) VALUES ($1, $2); -- name: find-one-route-by-id - -SELECT - (id, creator, hostname, created_at, edited_at, active) -FROM Routes -WHERE id=? -LIMIT 1; +SELECT id, creator, hostname, created_at, edited_at, active FROM Routes WHERE id = $1 LIMIT 1; -- name: find-one-route-by-host - -SELECT - (id, creator, hostname, created_at, edited_at, active) -FROM Routes -WHERE hostname=? -LIMIT 1; +SELECT id, creator, hostname, created_at, edited_at, active FROM Routes WHERE hostname = $1 LIMIT 1; -- name: find-all-routes-for-user - -SELECT - (id, creator, hostname, created_at, edited_at, active) -FROM Routes -WHERE creator=?; +SELECT id, creator, hostname, created_at, edited_at, active FROM Routes WHERE creator = $1; -- name: delete-one-route - -DELETE -FROM Routes -WHERE - id=? AND domain=? -LIMIT 1; +DELETE FROM Routes WHERE id = $1 AND domain = $2 LIMIT 1; -- name: insert-token - -INSERT INTO Tokens - (body, creator, scopes, expires_at) -VALUES - (?, ?, ?, ?); +INSERT INTO Tokens(body, creator, scopes, expires_at) VALUES ($1, $2, $3, $4); -- name: get-one-token - -SELECT - (id, body, creator, scopes, created_at, expires_at, active) -FROM Tokens -WHERE id=? -LIMIT 1; +SELECT id, body, creator, scopes, created_at, expires_at, active FROM Tokens WHERE id = $1 LIMIT 1; -- name: get-one-token-by-body - -SELECT - (id, body, creator, scopes, created_at, expires_at, active) -FROM Tokens -WHERE body=? -LIMIT 1; +SELECT id, body, creator, scopes, created_at, expires_at, active FROM Tokens WHERE body = $1 LIMIT 1; -- name: get-all-tokens-for-user - -SELECT - (id, body, creator, scopes, created_at, expires_at, active) -FROM Tokens -WHERE creator=?; +SELECT id, body, creator, scopes, created_at, expires_at, active FROM Tokens WHERE creator = $1; -- name: remove-one-token +DELETE FROM Tokens WHERE id = $1 LIMIT 1; -DELETE -FROM Tokens -WHERE id=? -LIMIT 1; - --- name: remove-expired-tokens - -DELETE -FROM Tokens -WHERE expires_at - interval '7 days'; diff --git a/internal/database/postgres.go b/internal/database/postgres.go new file mode 100644 index 0000000..c5a1bbc --- /dev/null +++ b/internal/database/postgres.go @@ -0,0 +1,180 @@ +package database + +import ( + "bytes" + "database/sql" + "log" + + "git.xeserv.us/xena/route/internal/database/dmigrations" + "github.com/Xe/uuid" + "github.com/brandur/simplebox" + "github.com/gchaincl/dotsql" + _ "github.com/lib/pq" + "golang.org/x/net/context" +) + +type Scanner interface { + Scan(...interface{}) error +} + +type PostgresStorage struct { + ds *dotsql.DotSql + db *sql.DB + sb *simplebox.SimpleBox + + //cs *postgresCertificateStorage + rs *postgresRouteStorage + //ts *postgresTokenStorage +} + +type postgresCertificateStorage struct { + *PostgresStorage +} + +type postgresRouteStorage struct { + *PostgresStorage +} + +type postgresTokenStorage struct { + *PostgresStorage +} + +// NewPostgresStorage creates a new Storage instance backed by postgres at the +// given URL. +func NewPostgresStorage(url string, key *[32]byte) (Storage, error) { + db, err := sql.Open("postgres", url) + if err != nil { + return nil, err + } + + data, err := dmigrations.Asset("postgres.sql") + if err != nil { + return nil, err + } + + buf := bytes.NewBuffer(data) + + ds, err := dotsql.Load(buf) + if err != nil { + return nil, err + } + + for k := range ds.QueryMap() { + log.Printf("preparing %s", k) + stmt, err := ds.Prepare(db, k) + if err != nil { + db.Close() + return nil, err + } + defer stmt.Close() + } + + p := &PostgresStorage{ + db: db, + ds: ds, + sb: simplebox.NewFromSecretKey(key), + } + + //p.cs = &postgresCertificateStorage{p} + p.rs = &postgresRouteStorage{p} + //p.ts = &postgresTokenStorage{p} + + return p, nil +} + +// Certs gets the certificate storage interface. +func (p *PostgresStorage) Certs() Certs { return nil } + +// Routes gets the route storage interface. +func (p *PostgresStorage) Routes() Routes { return p.rs } + +// Tokens gets the token storage interface. +func (p *PostgresStorage) Tokens() Tokens { return nil } + +// Close cleans up resources for this Storage. +func (p *PostgresStorage) Close() error { return p.db.Close() } + +// interface compliance +var ( + _ Storage = &PostgresStorage{} + //_ Certs = &postgresCertificateStorage{} + _ Routes = &postgresRouteStorage{} + //_ Tokens = &postgresTokenStorage{} +) + +func (p *postgresRouteStorage) getRouteInner(ctx context.Context, arg string, kind string) (Route, error) { + r, err := p.ds.QueryRow(p.db, kind, arg) + if err != nil { + return Route{}, err + } + + rt := Route{} + + err = (&rt).Scan(r) + if err != nil { + return Route{}, err + } + + return rt, nil +} + +func (p *postgresRouteStorage) Get(ctx context.Context, id string) (Route, error) { + return p.getRouteInner(ctx, id, "find-one-route-by-id") +} + +func (p *postgresRouteStorage) GetHost(ctx context.Context, host string) (Route, error) { + return p.getRouteInner(ctx, host, "find-one-route-by-host") +} + +func (p *postgresRouteStorage) GetAll(ctx context.Context, user string) ([]Route, error) { + var result []Route + + rows, err := p.ds.Query(p.db, "find-all-routes-for-user", user) + if err != nil { + return nil, err + } + + defer rows.Close() + for rows.Next() { + rt := &Route{} + + if err := rt.Scan(rows); err != nil { + return nil, err + } + + result = append(result, *rt) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return result, nil +} + +func (p *postgresRouteStorage) Put(ctx context.Context, r Route) (Route, error) { + if r.ID == "" { + r.ID = uuid.New() + } + + _, err := p.ds.Exec(p.db, "insert-route", r.ID, r.Hostname) + if err != nil { + return Route{}, err + } + + return p.Get(ctx, r.ID) +} + +func (p *postgresRouteStorage) Delete(ctx context.Context, r Route) (Route, error) { + rt, err := p.Get(ctx, r.ID) + if err != nil { + return Route{}, err + } + + _, err = p.ds.Exec(p.db, "delete-one-route", rt.ID, rt.Hostname) + if err != nil { + return Route{}, err + } + + return rt, nil +} diff --git a/internal/database/route.go b/internal/database/route.go index c4b9fa6..8c9cd59 100644 --- a/internal/database/route.go +++ b/internal/database/route.go @@ -2,6 +2,7 @@ package database import ( "io" + "time" proto "git.xeserv.us/xena/route/proto" "github.com/Xe/ln" @@ -26,6 +27,10 @@ type Route struct { ID string `storm:"id"` Creator string Hostname string `storm:"unique"` + + CreatedAt time.Time + EditedAt time.Time + Active bool } // F https://godoc.org/github.com/Xe/ln#F @@ -37,6 +42,10 @@ func (r Route) F() ln.F { } } +func (r *Route) Scan(row Scanner) error { + return row.Scan(&r.ID, &r.Creator, &r.Hostname, &r.CreatedAt, &r.EditedAt, &r.Active) +} + // AsProto converts this into a protobuf Route. func (r Route) AsProto() *proto.Route { return &proto.Route{ diff --git a/internal/database/route_test.go b/internal/database/route_test.go index 6c28efd..68a3aed 100644 --- a/internal/database/route_test.go +++ b/internal/database/route_test.go @@ -75,3 +75,11 @@ func TestBoltDBRouteStorage(t *testing.T) { testRoutes(ctx, t, st.Routes()) } + +func TestPostgresRouteStorage(t *testing.T) { + st, ctx, cancel := newTestPostgresStorage(t, os.Getenv("DATABASE_URL")) + defer st.Close() + defer cancel() + + testRoutes(ctx, t, st.Routes()) +}