Skip to content

Commit

Permalink
pkg/pagination: add token type
Browse files Browse the repository at this point in the history
This change pulls as much pagination logic out of the database
implementation as possible. Database implementations should now be able
to marshal whatever state they need into opaque tokens with the
utilities in the pagination package.
  • Loading branch information
jzelinskie committed Sep 7, 2018
1 parent d193b46 commit 0565938
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 91 deletions.
5 changes: 3 additions & 2 deletions api/v3/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
pb "github.com/coreos/clair/api/v3/clairpb"
"github.com/coreos/clair/database"
"github.com/coreos/clair/pkg/commonerr"
"github.com/coreos/clair/pkg/pagination"
)

// NotificationServer implements NotificationService interface for serving RPC.
Expand Down Expand Up @@ -214,8 +215,8 @@ func (s *NotificationServer) GetNotification(ctx context.Context, req *pb.GetNot
dbNotification, ok, err := tx.FindVulnerabilityNotification(
req.GetName(),
int(req.GetLimit()),
database.PageNumber(req.GetOldVulnerabilityPage()),
database.PageNumber(req.GetNewVulnerabilityPage()),
pagination.Token(req.GetOldVulnerabilityPage()),
pagination.Token(req.GetNewVulnerabilityPage()),
)

if err != nil {
Expand Down
15 changes: 5 additions & 10 deletions database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"errors"
"fmt"
"time"

"github.com/coreos/clair/pkg/pagination"
)

var (
Expand Down Expand Up @@ -168,16 +170,9 @@ type Session interface {
// affected ancestries affected by old or new vulnerability.
//
// Because the number of affected ancestries maybe large, they are paginated
// and their pages are specified by the given encrypted PageNumbers, which,
// if empty, are always considered first page.
//
// Session interface implementation should have encrypt and decrypt
// functions for PageNumber.
FindVulnerabilityNotification(name string, limit int,
oldVulnerabilityPage PageNumber,
newVulnerabilityPage PageNumber) (
noti VulnerabilityNotificationWithVulnerable,
found bool, err error)
// and their pages are specified by the paination token, which, if empty, are
// always considered first page.
FindVulnerabilityNotification(name string, limit int, oldVulnerabilityPage pagination.Token, newVulnerabilityPage pagination.Token) (noti VulnerabilityNotificationWithVulnerable, found bool, err error)

// MarkNotificationNotified marks a Notification as notified now, assuming
// the requested notification is in the database.
Expand Down
10 changes: 7 additions & 3 deletions database/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

package database

import "time"
import (
"time"

"github.com/coreos/clair/pkg/pagination"
)

// MockSession implements Session and enables overriding each available method.
// The default behavior of each method is to simply panic.
Expand All @@ -38,7 +42,7 @@ type MockSession struct {
FctDeleteVulnerabilities func([]VulnerabilityID) error
FctInsertVulnerabilityNotifications func([]VulnerabilityNotification) error
FctFindNewNotification func(lastNotified time.Time) (NotificationHook, bool, error)
FctFindVulnerabilityNotification func(name string, limit int, oldPage PageNumber, newPage PageNumber) (
FctFindVulnerabilityNotification func(name string, limit int, oldPage pagination.Token, newPage pagination.Token) (
vuln VulnerabilityNotificationWithVulnerable, ok bool, err error)
FctMarkNotificationNotified func(name string) error
FctDeleteNotification func(name string) error
Expand Down Expand Up @@ -182,7 +186,7 @@ func (ms *MockSession) FindNewNotification(lastNotified time.Time) (Notification
panic("required mock function not implemented")
}

func (ms *MockSession) FindVulnerabilityNotification(name string, limit int, oldPage PageNumber, newPage PageNumber) (
func (ms *MockSession) FindVulnerabilityNotification(name string, limit int, oldPage pagination.Token, newPage pagination.Token) (
VulnerabilityNotificationWithVulnerable, bool, error) {
if ms.FctFindVulnerabilityNotification != nil {
return ms.FctFindVulnerabilityNotification(name, limit, oldPage, newPage)
Expand Down
9 changes: 4 additions & 5 deletions database/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"database/sql/driver"
"encoding/json"
"time"

"github.com/coreos/clair/pkg/pagination"
)

// Processors are extentions to scan a layer's content.
Expand Down Expand Up @@ -173,8 +175,8 @@ type PagedVulnerableAncestries struct {
Affected map[int]string

Limit int
Current PageNumber
Next PageNumber
Current pagination.Token
Next pagination.Token

// End signals the end of the pages.
End bool
Expand Down Expand Up @@ -209,9 +211,6 @@ type VulnerabilityNotificationWithVulnerable struct {
New *PagedVulnerableAncestries
}

// PageNumber is used to do pagination.
type PageNumber string

// MetadataMap is for storing the metadata returned by vulnerability database.
type MetadataMap map[string]interface{}

Expand Down
23 changes: 12 additions & 11 deletions database/pgsql/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/coreos/clair/database"
"github.com/coreos/clair/pkg/commonerr"
"github.com/coreos/clair/pkg/pagination"
)

var (
Expand Down Expand Up @@ -163,12 +164,12 @@ func (tx *pgSession) FindNewNotification(notifiedBefore time.Time) (database.Not
return notification, true, nil
}

func (tx *pgSession) findPagedVulnerableAncestries(vulnID int64, limit int, currentPage database.PageNumber) (database.PagedVulnerableAncestries, error) {
func (tx *pgSession) findPagedVulnerableAncestries(vulnID int64, limit int, currentToken pagination.Token) (database.PagedVulnerableAncestries, error) {
vulnPage := database.PagedVulnerableAncestries{Limit: limit}
current := Page{0}
if currentPage != "" {
currentPage := Page{0}
if currentToken != pagination.FirstPageToken {
var err error
current, err = PageFromPageNumber(tx.key, currentPage)
err = tx.key.UnmarshalToken(currentToken, &currentPage)
if err != nil {
return vulnPage, err
}
Expand All @@ -188,7 +189,7 @@ func (tx *pgSession) findPagedVulnerableAncestries(vulnID int64, limit int, curr
}

// the last result is used for the next page's startID
rows, err := tx.Query(searchNotificationVulnerableAncestry, vulnID, current.StartID, limit+1)
rows, err := tx.Query(searchNotificationVulnerableAncestry, vulnID, currentPage.StartID, limit+1)
if err != nil {
return vulnPage, handleError("searchNotificationVulnerableAncestry", err)
}
Expand All @@ -209,9 +210,9 @@ func (tx *pgSession) findPagedVulnerableAncestries(vulnID int64, limit int, curr
lastIndex = len(ancestries)
vulnPage.End = true
} else {
// Use the last ancestry's ID as the next PageNumber.
// Use the last ancestry's ID as the next page.
lastIndex = len(ancestries) - 1
vulnPage.Next, err = Page{ancestries[len(ancestries)-1].id}.PageNumber(tx.key)
vulnPage.Next, err = tx.key.MarshalToken(Page{ancestries[len(ancestries)-1].id})
if err != nil {
return vulnPage, err
}
Expand All @@ -222,15 +223,15 @@ func (tx *pgSession) findPagedVulnerableAncestries(vulnID int64, limit int, curr
vulnPage.Affected[int(ancestry.id)] = ancestry.name
}

vulnPage.Current, err = current.PageNumber(tx.key)
vulnPage.Current, err = tx.key.MarshalToken(currentPage)
if err != nil {
return vulnPage, err
}

return vulnPage, nil
}

func (tx *pgSession) FindVulnerabilityNotification(name string, limit int, oldPage database.PageNumber, newPage database.PageNumber) (
func (tx *pgSession) FindVulnerabilityNotification(name string, limit int, oldPageToken pagination.Token, newPageToken pagination.Token) (
database.VulnerabilityNotificationWithVulnerable, bool, error) {
var (
noti database.VulnerabilityNotificationWithVulnerable
Expand Down Expand Up @@ -270,15 +271,15 @@ func (tx *pgSession) FindVulnerabilityNotification(name string, limit int, oldPa
}

if oldVulnID.Valid {
page, err := tx.findPagedVulnerableAncestries(oldVulnID.Int64, limit, oldPage)
page, err := tx.findPagedVulnerableAncestries(oldVulnID.Int64, limit, oldPageToken)
if err != nil {
return noti, false, err
}
noti.Old = &page
}

if newVulnID.Valid {
page, err := tx.findPagedVulnerableAncestries(newVulnID.Int64, limit, newPage)
page, err := tx.findPagedVulnerableAncestries(newVulnID.Int64, limit, newPageToken)
if err != nil {
return noti, false, err
}
Expand Down
19 changes: 12 additions & 7 deletions database/pgsql/notification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,20 @@ func TestPagination(t *testing.T) {
if assert.Nil(t, err) && assert.True(t, ok) {
assert.Equal(t, "test", noti.Name)
if assert.NotNil(t, noti.Old) && assert.NotNil(t, noti.New) {
oldPage, err := PageFromPageNumber(tx.key, noti.Old.Current)
var oldPage Page
err := tx.key.UnmarshalToken(noti.Old.Current, &oldPage)
if !assert.Nil(t, err) {
assert.FailNow(t, "")
}

assert.Equal(t, int64(0), oldPage.StartID)
newPage, err := PageFromPageNumber(tx.key, noti.New.Current)
var newPage Page
err = tx.key.UnmarshalToken(noti.New.Current, &newPage)
if !assert.Nil(t, err) {
assert.FailNow(t, "")
}
newPageNext, err := PageFromPageNumber(tx.key, noti.New.Next)
var newPageNext Page
err = tx.key.UnmarshalToken(noti.New.Next, &newPageNext)
if !assert.Nil(t, err) {
assert.FailNow(t, "")
}
Expand All @@ -98,12 +101,12 @@ func TestPagination(t *testing.T) {
}
}

pageNum1, err := Page{0}.PageNumber(tx.key)
pageNum1, err := tx.key.MarshalToken(Page{0})
if !assert.Nil(t, err) {
assert.FailNow(t, "")
}

pageNum2, err := Page{4}.PageNumber(tx.key)
pageNum2, err := tx.key.MarshalToken(Page{4})
if !assert.Nil(t, err) {
assert.FailNow(t, "")
}
Expand All @@ -112,12 +115,14 @@ func TestPagination(t *testing.T) {
if assert.Nil(t, err) && assert.True(t, ok) {
assert.Equal(t, "test", noti.Name)
if assert.NotNil(t, noti.Old) && assert.NotNil(t, noti.New) {
oldCurrentPage, err := PageFromPageNumber(tx.key, noti.Old.Current)
var oldCurrentPage Page
err = tx.key.UnmarshalToken(noti.Old.Current, &oldCurrentPage)
if !assert.Nil(t, err) {
assert.FailNow(t, "")
}

newCurrentPage, err := PageFromPageNumber(tx.key, noti.New.Current)
var newCurrentPage Page
err = tx.key.UnmarshalToken(noti.New.Current, &newCurrentPage)
if !assert.Nil(t, err) {
assert.FailNow(t, "")
}
Expand Down
45 changes: 0 additions & 45 deletions database/pgsql/page.go

This file was deleted.

9 changes: 9 additions & 0 deletions database/pgsql/pgsql.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ func (pgSQL *pgSQL) Ping() bool {
return pgSQL.DB.Ping() == nil
}

// Page is the representation of a page for the Postgres schema.
type Page struct {
// StartID is the ID being used as the basis for pagination across database
// results. It is used to search for an ancestry with ID >= StartID.
//
// StartID is required to be unique to every ancestry and always increasing.
StartID int64
}

// Config is the configuration that is used by openDatabase.
type Config struct {
Source string
Expand Down
2 changes: 1 addition & 1 deletion database/pgsql/pgsql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func generateTestConfig(testName string, loadFixture bool, manageLife bool) data
"cachesize": 0,
"managedatabaselifecycle": manageLife,
"fixturepath": fixturePath,
"paginationkey": pagination.MustGenerateNewKey().String(),
"paginationkey": pagination.Must(pagination.NewKey()).String(),
},
}
}
Expand Down
22 changes: 15 additions & 7 deletions pkg/pagination/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ type Key struct {
fkey *fernet.Key
}

// Token represents an opaque pagination token keeping track of a user's
// progress iterating through a list of results.
type Token string

// FirstPageToken is used to represent the first page of content.
var FirstPageToken = Token("")

// NewKey generates a new random pagination key.
func NewKey() (k Key, err error) {
k.fkey = new(fernet.Key)
Expand Down Expand Up @@ -71,21 +78,22 @@ func (k Key) String() string {
return k.fkey.Encode()
}

// MarshalToken encodes an interface into JSON bytes and encrypts it.
func (k Key) MarshalToken(v interface{}) ([]byte, error) {
// MarshalToken encodes an interface into JSON bytes and produces a Token.
func (k Key) MarshalToken(v interface{}) (Token, error) {
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(v)
if err != nil {
return nil, err
return Token(""), err
}

return fernet.EncryptAndSign(buf.Bytes(), k.fkey)
tokenBytes, err := fernet.EncryptAndSign(buf.Bytes(), k.fkey)
return Token(tokenBytes), err
}

// UnmarshalToken decrypts a token using provided key and decodes the result
// UnmarshalToken decrypts a Token using provided key and decodes the result
// into the provided interface.
func (k Key) UnmarshalToken(token string, v interface{}) error {
msg := fernet.VerifyAndDecrypt([]byte(token), time.Hour, []*fernet.Key{k.fkey})
func (k Key) UnmarshalToken(t Token, v interface{}) error {
msg := fernet.VerifyAndDecrypt([]byte(t), time.Hour, []*fernet.Key{k.fkey})
if msg == nil {
return ErrInvalidToken
}
Expand Down

0 comments on commit 0565938

Please sign in to comment.