Skip to content

Commit

Permalink
feat: report warnings (#560)
Browse files Browse the repository at this point in the history
Add `Repository.HandleWarning(Warning)` that handles encountered warning
headers.

Resolves: #469
Signed-off-by: Lixia (Sylvia) Lei <[email protected]>
  • Loading branch information
Wwwsylvia authored Aug 2, 2023
1 parent 3a2e0c1 commit b59a33e
Show file tree
Hide file tree
Showing 7 changed files with 574 additions and 16 deletions.
36 changes: 36 additions & 0 deletions registry/remote/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ func TestMain(m *testing.M) {
w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
w.Header().Set("Docker-Content-Digest", exampleManifestDigest)
w.Header().Set("Content-Length", strconv.Itoa(len([]byte(exampleManifest))))
w.Header().Set("Warning", `299 - "This image is deprecated and will be removed soon."`)
if m == "GET" {
w.Write([]byte(exampleManifest))
}
Expand Down Expand Up @@ -730,6 +731,41 @@ func Example_pullByDigest() {
// {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]}
}

func Example_handleWarning() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
// 1. specify HandleWarning
repo.HandleWarning = func(warning remote.Warning) {
fmt.Printf("Warning from %s: %s\n", repo.Reference.Repository, warning.Text)
}

ctx := context.Background()
exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7"
// 2. resolve the descriptor
descriptor, err := repo.Resolve(ctx, exampleDigest)
if err != nil {
panic(err)
}
fmt.Println(descriptor.Digest)
fmt.Println(descriptor.Size)

// 3. fetch the content byte[] from the repository
pulledBlob, err := content.FetchAll(ctx, repo, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulledBlob))

// Output:
// Warning from example: This image is deprecated and will be removed soon.
// sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7
// 337
// Warning from example: This image is deprecated and will be removed soon.
// {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]}
}

// Example_pushAndTag gives example snippet of pushing an OCI image with a tag.
func Example_pushAndTag() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
Expand Down
19 changes: 17 additions & 2 deletions registry/remote/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ func (r *Registry) client() Client {
return r.Client
}

// do sends an HTTP request and returns an HTTP response using the HTTP client
// returned by r.client().
func (r *Registry) do(req *http.Request) (*http.Response, error) {
if r.HandleWarning == nil {
return r.client().Do(req)
}

resp, err := r.client().Do(req)
if err != nil {
return nil, err
}
handleWarningHeaders(resp.Header.Values(headerWarning), r.HandleWarning)
return resp, nil
}

// Ping checks whether or not the registry implement Docker Registry API V2 or
// OCI Distribution Specification.
// Ping can be used to check authentication when an auth client is configured.
Expand All @@ -87,7 +102,7 @@ func (r *Registry) Ping(ctx context.Context) error {
return err
}

resp, err := r.client().Do(req)
resp, err := r.do(req)
if err != nil {
return err
}
Expand Down Expand Up @@ -142,7 +157,7 @@ func (r *Registry) repositories(ctx context.Context, last string, fn func(repos
}
req.URL.RawQuery = q.Encode()
}
resp, err := r.client().Do(req)
resp, err := r.do(req)
if err != nil {
return "", err
}
Expand Down
114 changes: 114 additions & 0 deletions registry/remote/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ limitations under the License.
package remote

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
Expand Down Expand Up @@ -266,6 +268,118 @@ func TestRegistry_Repositories_WithLastParam(t *testing.T) {
}
}

func TestRegistry_do(t *testing.T) {
data := []byte(`hello world!`)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/test" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Add("Warning", `299 - "Test 1: Good warning."`)
w.Header().Add("Warning", `199 - "Test 2: Warning with a non-299 code."`)
w.Header().Add("Warning", `299 - "Test 3: Good warning."`)
w.Header().Add("Warning", `299 myregistry.example.com "Test 4: Warning with a non-unknown agent"`)
w.Header().Add("Warning", `299 - "Test 5: Warning with a date." "Sat, 25 Aug 2012 23:34:45 GMT"`)
w.Header().Add("wArnIng", `299 - "Test 6: Good warning."`)
w.Write(data)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
testURL := ts.URL + "/test"

// test do() without HandleWarning
reg, err := NewRegistry(uri.Host)
if err != nil {
t.Fatal("NewRegistry() error =", err)
}
req, err := http.NewRequest(http.MethodGet, testURL, nil)
if err != nil {
t.Fatal("failed to create test request:", err)
}
resp, err := reg.do(req)
if err != nil {
t.Fatal("Registry.do() error =", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Registry.do() status code = %v, want %v", resp.StatusCode, http.StatusOK)
}
if got := len(resp.Header["Warning"]); got != 6 {
t.Errorf("Registry.do() warning header len = %v, want %v", got, 6)
}
got, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal("io.ReadAll() error =", err)
}
resp.Body.Close()
if !bytes.Equal(got, data) {
t.Errorf("Registry.do() = %v, want %v", got, data)
}

// test do() with HandleWarning
reg, err = NewRegistry(uri.Host)
if err != nil {
t.Fatal("NewRegistry() error =", err)
}
var gotWarnings []Warning
reg.HandleWarning = func(warning Warning) {
gotWarnings = append(gotWarnings, warning)
}

req, err = http.NewRequest(http.MethodGet, testURL, nil)
if err != nil {
t.Fatal("failed to create test request:", err)
}
resp, err = reg.do(req)
if err != nil {
t.Fatal("Registry.do() error =", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Registry.do() status code = %v, want %v", resp.StatusCode, http.StatusOK)
}
if got := len(resp.Header["Warning"]); got != 6 {
t.Errorf("Registry.do() warning header len = %v, want %v", got, 6)
}
got, err = io.ReadAll(resp.Body)
if err != nil {
t.Errorf("Registry.do() = %v, want %v", got, data)
}
resp.Body.Close()
if !bytes.Equal(got, data) {
t.Errorf("Registry.do() = %v, want %v", got, data)
}

wantWarnings := []Warning{
{
WarningValue: WarningValue{
Code: 299,
Agent: "-",
Text: "Test 1: Good warning.",
},
},
{
WarningValue: WarningValue{
Code: 299,
Agent: "-",
Text: "Test 3: Good warning.",
},
},
{
WarningValue: WarningValue{
Code: 299,
Agent: "-",
Text: "Test 6: Good warning.",
},
},
}
if !reflect.DeepEqual(gotWarnings, wantWarnings) {
t.Errorf("Registry.do() = %v, want %v", gotWarnings, wantWarnings)
}
}

// indexOf returns the index of an element within a slice
func indexOf(element string, data []string) int {
for ind, val := range data {
Expand Down
51 changes: 37 additions & 14 deletions registry/remote/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ type Repository struct {
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#deleting-manifests
SkipReferrersGC bool

// HandleWarning handles the warning returned by the remote server.
// Callers SHOULD deduplicate warnings from multiple associated responses.
//
// References:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc3/spec.md#warnings
// - https://www.rfc-editor.org/rfc/rfc7234#section-5.5
HandleWarning func(warning Warning)

// NOTE: Must keep fields in sync with newRepositoryWithOptions function.

// referrersState represents that if the repository supports Referrers API.
Expand Down Expand Up @@ -234,6 +242,21 @@ func (r *Repository) client() Client {
return r.Client
}

// do sends an HTTP request and returns an HTTP response using the HTTP client
// returned by r.client().
func (r *Repository) do(req *http.Request) (*http.Response, error) {
if r.HandleWarning == nil {
return r.client().Do(req)
}

resp, err := r.client().Do(req)
if err != nil {
return nil, err
}
handleWarningHeaders(resp.Header.Values(headerWarning), r.HandleWarning)
return resp, nil
}

// blobStore detects the blob store for the given descriptor.
func (r *Repository) blobStore(desc ocispec.Descriptor) registry.BlobStore {
if isManifest(r.ManifestMediaTypes, desc) {
Expand Down Expand Up @@ -391,7 +414,7 @@ func (r *Repository) tags(ctx context.Context, last string, fn func(tags []strin
}
req.URL.RawQuery = q.Encode()
}
resp, err := r.client().Do(req)
resp, err := r.do(req)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -508,7 +531,7 @@ func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string
req.URL.RawQuery = q.Encode()
}

resp, err := r.client().Do(req)
resp, err := r.do(req)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -619,7 +642,7 @@ func (r *Repository) pingReferrers(ctx context.Context) (bool, error) {
if err != nil {
return false, err
}
resp, err := r.client().Do(req)
resp, err := r.do(req)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -657,7 +680,7 @@ func (r *Repository) delete(ctx context.Context, target ocispec.Descriptor, isMa
return err
}

resp, err := r.client().Do(req)
resp, err := r.do(req)
if err != nil {
return err
}
Expand Down Expand Up @@ -689,7 +712,7 @@ func (s *blobStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io
return nil, err
}

resp, err := s.repo.client().Do(req)
resp, err := s.repo.do(req)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -736,7 +759,7 @@ func (s *blobStore) Mount(ctx context.Context, desc ocispec.Descriptor, fromRepo
if err != nil {
return err
}
resp, err := s.repo.client().Do(req)
resp, err := s.repo.do(req)
if err != nil {
return err
}
Expand Down Expand Up @@ -809,7 +832,7 @@ func (s *blobStore) Push(ctx context.Context, expected ocispec.Descriptor, conte
return err
}

resp, err := s.repo.client().Do(req)
resp, err := s.repo.do(req)
if err != nil {
return err
}
Expand Down Expand Up @@ -864,7 +887,7 @@ func (s *blobStore) completePushAfterInitialPost(ctx context.Context, req *http.
if auth := resp.Request.Header.Get("Authorization"); auth != "" {
req.Header.Set("Authorization", auth)
}
resp, err = s.repo.client().Do(req)
resp, err = s.repo.do(req)
if err != nil {
return err
}
Expand Down Expand Up @@ -910,7 +933,7 @@ func (s *blobStore) Resolve(ctx context.Context, reference string) (ocispec.Desc
return ocispec.Descriptor{}, err
}

resp, err := s.repo.client().Do(req)
resp, err := s.repo.do(req)
if err != nil {
return ocispec.Descriptor{}, err
}
Expand Down Expand Up @@ -945,7 +968,7 @@ func (s *blobStore) FetchReference(ctx context.Context, reference string) (desc
return ocispec.Descriptor{}, nil, err
}

resp, err := s.repo.client().Do(req)
resp, err := s.repo.do(req)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
Expand Down Expand Up @@ -1021,7 +1044,7 @@ func (s *manifestStore) Fetch(ctx context.Context, target ocispec.Descriptor) (r
}
req.Header.Set("Accept", target.MediaType)

resp, err := s.repo.client().Do(req)
resp, err := s.repo.do(req)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1147,7 +1170,7 @@ func (s *manifestStore) Resolve(ctx context.Context, reference string) (ocispec.
}
req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes))

resp, err := s.repo.client().Do(req)
resp, err := s.repo.do(req)
if err != nil {
return ocispec.Descriptor{}, err
}
Expand Down Expand Up @@ -1179,7 +1202,7 @@ func (s *manifestStore) FetchReference(ctx context.Context, reference string) (d
}
req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes))

resp, err := s.repo.client().Do(req)
resp, err := s.repo.do(req)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
Expand Down Expand Up @@ -1277,7 +1300,7 @@ func (s *manifestStore) push(ctx context.Context, expected ocispec.Descriptor, c
return err
}
}
resp, err := client.Do(req)
resp, err := s.repo.do(req)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit b59a33e

Please sign in to comment.