Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: report warnings #560

Merged
merged 25 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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