Skip to content

Commit

Permalink
client: add differ and refactor client
Browse files Browse the repository at this point in the history
Signed-off-by: Hank Donnay <hdonnay@redhat.com>
  • Loading branch information
hdonnay committed Apr 8, 2020
1 parent e783062 commit 1ba6891
Show file tree
Hide file tree
Showing 6 changed files with 519 additions and 100 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.13

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/google/go-cmp v0.4.0
github.com/google/go-containerregistry v0.0.0-20191206185556-eb7c14b719c6
github.com/google/uuid v1.1.1
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
Expand Down
176 changes: 176 additions & 0 deletions httptransport/client/differ.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package client

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"path"
"strings"
"sync"

"github.com/google/uuid"
"github.com/quay/claircore/libvuln/driver"

"github.com/quay/clair/v4/httptransport"
"github.com/quay/clair/v4/matcher"
)

var _ matcher.Differ = (HTTP)(nil)

// DeleteUpdateOperations attempts to delete the referenced update operations.
func (c HTTP) DeleteUpdateOperations(ctx context.Context, ref ...uuid.UUID) error {
u, err := c.addr.Parse(httptransport.UpdatesAPIPath)
if err != nil {
return err
}

// Spawn a few requests that will write their result into "errs".
//
// These'll most likely be multiplexed and to the same host, so pick a nice
// lowish number like 4.
//
// Don't use an errgroup because we want to actually issue all the DELETEs,
// not stop all requests on the first error.
var wg sync.WaitGroup
item := make(chan int)
errs := make([]error, len(ref))
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := range item {
u, err := u.Parse(ref[i].String())
if err != nil {
errs[i] = err
return
}
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u.String(), nil)
if err != nil {
errs[i] = err
return
}
res, err := c.c.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
errs[i] = err
return
}
if got, want := res.StatusCode, http.StatusOK; got != want {
errs[i] = fmt.Errorf("%v: unexpected status: %s", u.Path, res.Status)
}
}
}()
}
for i, lim := 0, len(ref); i < lim; i++ {
item <- i
}
close(item)
wg.Wait()

var b strings.Builder
var errd bool
for _, err := range errs {
if err != nil {
if errd {
b.WriteByte('\n')
}
b.WriteString(err.Error())
errd = true
}
}

if errd {
return errors.New("deletion errors: " + b.String())
}
return nil
}

// LatestUpdateOperation shouldn't be used by client code and is implemented
// only to satisfy the matcher.Differ interface.
func (c HTTP) LatestUpdateOperation(_ context.Context) (uuid.UUID, error) {
return uuid.Nil, nil
}

// ErrUnchanged is returned from LatestUpdateOperations if there have been no
// new update operations since the last call.
var ErrUnchanged = errors.New("response unchanged from last call")

// LatestUpdateOperations returns a map of updater name to ref of its latest
// update.
func (c HTTP) LatestUpdateOperations(ctx context.Context) (map[string]uuid.UUID, error) {
u, err := c.addr.Parse(httptransport.UpdatesAPIPath)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
if v := c.diffValidator.Load().(string); v != "" {
req.Header.Set("if-none-match", v)
}
res, err := c.c.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
switch res.StatusCode {
case http.StatusOK:
if v := res.Header.Get("etag"); v != "" && !strings.HasPrefix(v, "W/") {
c.diffValidator.Store(v)
}
case http.StatusNotModified:
return nil, ErrUnchanged
default:
return nil, fmt.Errorf("%v: unexpected status: %s", u.Path, res.Status)
}
m := make(map[string]uuid.UUID)
if err := json.NewDecoder(res.Body).Decode(&m); err != nil {
return nil, err
}
return m, nil
}

// UpdateDiff reports the diff of two update operations, identified by the
// provided refs.
//
// "Prev" may be passed uuid.Nil if the client's last known state has been
// forgotten by the server.
func (c HTTP) UpdateDiff(ctx context.Context, prev, cur uuid.UUID) (*driver.UpdateDiff, error) {
u, err := c.addr.Parse(path.Join(httptransport.UpdatesAPIPath, "diff"))
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
v := req.URL.Query()
if prev != uuid.Nil {
v.Set("prev", prev.String())
}
v.Set("cur", cur.String())
req.URL.RawQuery = v.Encode()

res, err := c.c.Do(req)
if res != nil {
defer res.Body.Close()
}
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%v: unexpected status: %s", u.Path, res.Status)
}
d := driver.UpdateDiff{}
if err := json.NewDecoder(res.Body).Decode(&d); err != nil {
return nil, err
}
return &d, nil
}
179 changes: 179 additions & 0 deletions httptransport/client/differ_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package client_test

import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"path"
"strconv"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/quay/claircore/libvuln/driver"

"github.com/quay/clair/v4/httptransport"
"github.com/quay/clair/v4/httptransport/client"
)

// TestDiffer puts the Differ methods of the client through its paces.
func TestDiffer(t *testing.T) {
ctx, done := context.WithCancel(context.Background())
defer done()

t.Run("OK", func(t *testing.T) {
t.Run("Delete", func(t *testing.T) {
t.Parallel()
// Generate a set of refs.
refs := make([]uuid.UUID, 10)
expected := make(map[string]struct{}, 10)
for i := range refs {
id := uuid.New()
refs[i] = id
expected[id.String()] = struct{}{}
}

// Spin up a server that mocks a delete call.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if !strings.HasPrefix(r.URL.Path, httptransport.UpdatesAPIPath) {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
got := path.Base(r.URL.Path)
t.Logf("got: %s", got)
if _, ok := expected[got]; !ok {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()

// Create a client.
c, err := client.NewHTTP(ctx, client.WithAddr(srv.URL))
if err != nil {
t.Fatal(err)
}

// Do the call.
if err := c.DeleteUpdateOperations(ctx, refs...); err != nil {
t.Error(err)
}
})

t.Run("Latest", func(t *testing.T) {
t.Parallel()
// Generate a set of names and refs.
want := make(map[string]uuid.UUID)
for i := 0; i < 10; i++ {
want[strconv.Itoa(i)] = uuid.New()
}
validator := `"validator"`

// Spin up a server that mocks a latest call.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if !strings.HasPrefix(r.URL.Path, httptransport.UpdatesAPIPath) {
w.WriteHeader(http.StatusBadRequest)
return
}
if v := r.Header.Get("If-None-Match"); v != "" && v == validator {
w.WriteHeader(http.StatusNotModified)
return
}
w.Header().Set("etag", validator)

if err := json.NewEncoder(w).Encode(want); err != nil {
t.Error(err)
}
}))
defer srv.Close()

// Create a client.
c, err := client.NewHTTP(ctx, client.WithAddr(srv.URL))
if err != nil {
t.Fatal(err)
}

t.Run("Initial", func(t *testing.T) {
// Do the call.
got, err := c.LatestUpdateOperations(ctx)
if err != nil {
t.Error(err)
}
if !cmp.Equal(got, want) {
t.Error(cmp.Diff(got, want))
}
})
t.Run("Second", func(t *testing.T) {
// Do the call.
_, err := c.LatestUpdateOperations(ctx)
if got, want := err, client.ErrUnchanged; !errors.Is(got, want) {
t.Errorf("got: %v, want: %v", got, want)
}
})
})

t.Run("Diff", func(t *testing.T) {
t.Parallel()
// Create two refs and a delta between them.
prev, cur := uuid.New(), uuid.New()
want := &driver.UpdateDiff{
A: driver.UpdateOperation{Ref: prev},
B: driver.UpdateOperation{Ref: cur},
Added: nil,
Removed: nil,
}

// Spin up a server that mocks the diff call.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

prevStr, curStr := r.FormValue("prev"), r.FormValue("cur")
if got, want := prevStr, prev.String(); got != want {
t.Errorf("got: %q, want: %q", got, want)
}
if got, want := curStr, cur.String(); got != want {
t.Errorf("got: %q, want: %q", got, want)
}
if t.Failed() {
w.WriteHeader(http.StatusBadRequest)
return
}

if err := json.NewEncoder(w).Encode(want); err != nil {
t.Error(err)
}
}))
defer srv.Close()

// Create a client.
c, err := client.NewHTTP(ctx, client.WithAddr(srv.URL))
if err != nil {
t.Fatal(err)
}

// Do the call.
got, err := c.UpdateDiff(ctx, prev, cur)
if err != nil {
t.Error(err)
}
if !cmp.Equal(got, want) {
t.Error(cmp.Diff(got, want))
}
})
})
}
Loading

0 comments on commit 1ba6891

Please sign in to comment.