-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
client: add differ and refactor client
Signed-off-by: Hank Donnay <hdonnay@redhat.com>
- Loading branch information
Showing
6 changed files
with
519 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
}) | ||
}) | ||
} |
Oops, something went wrong.