1
0
mirror of https://github.com/regclient/regclient.git synced 2025-04-18 22:44:00 +03:00

Updating Ping method

- Adds a struct to the return so headers can be inspected.
- Puts Ping into the scheme interface.
- Adds an ocidir implementation to verify directory is accessible.
- Fixes some http/auth handling.
- Warns on `regctl registry config` and `login` of Ping failure.

Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
Brandon Mitchell 2023-11-08 16:18:31 -05:00
parent 2881dcfb0d
commit 0a514c3e2d
No known key found for this signature in database
GPG Key ID: 6E0FF28C767A8BEE
10 changed files with 351 additions and 24 deletions

View File

@ -15,6 +15,7 @@ import (
"github.com/regclient/regclient"
"github.com/regclient/regclient/config"
"github.com/regclient/regclient/types/ref"
)
type registryCmd struct {
@ -180,6 +181,7 @@ func (registryOpts *registryCmd) runRegistryConfig(cmd *cobra.Command, args []st
}
func (registryOpts *registryCmd) runRegistryLogin(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
c, err := ConfigLoadDefault()
if err != nil {
return err
@ -257,6 +259,18 @@ func (registryOpts *registryCmd) runRegistryLogin(cmd *cobra.Command, args []str
return err
}
r, err := ref.NewHost(args[0])
if err != nil {
return err
}
rc := registryOpts.rootOpts.newRegClient()
_, err = rc.Ping(ctx, r)
if err != nil {
log.WithFields(logrus.Fields{
"err": err,
}).Warn("Failed to ping registry with credentials")
}
log.WithFields(logrus.Fields{
"registry": args[0],
}).Info("Credentials set")
@ -296,6 +310,7 @@ func (registryOpts *registryCmd) runRegistryLogout(cmd *cobra.Command, args []st
}
func (registryOpts *registryCmd) runRegistrySet(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
c, err := ConfigLoadDefault()
if err != nil {
return err
@ -386,6 +401,18 @@ func (registryOpts *registryCmd) runRegistrySet(cmd *cobra.Command, args []strin
return err
}
r, err := ref.NewHost(args[0])
if err != nil {
return err
}
rc := registryOpts.rootOpts.newRegClient()
_, err = rc.Ping(ctx, r)
if err != nil {
log.WithFields(logrus.Fields{
"err": err,
}).Warn("Failed to ping registry with settings")
}
log.WithFields(logrus.Fields{
"name": h.Name,
}).Info("Registry configuration updated/set")

View File

@ -16,6 +16,8 @@ import (
"time"
"github.com/sirupsen/logrus"
"github.com/regclient/regclient/types"
)
type charLU byte
@ -499,7 +501,7 @@ func (b *BasicHandler) ProcessChallenge(c Challenge) error {
func (b *BasicHandler) GenerateAuth() (string, error) {
cred := b.credsFn(b.host)
if cred.User == "" || cred.Password == "" {
return "", ErrNotFound
return "", fmt.Errorf("no credentials available: %w", types.ErrHTTPUnauthorized)
}
auth := base64.StdEncoding.EncodeToString([]byte(cred.User + ":" + cred.Password))
return fmt.Sprintf("Basic %s", auth), nil
@ -617,14 +619,14 @@ func (b *BearerHandler) GenerateAuth() (string, error) {
if err := b.tryPost(); err == nil {
return fmt.Sprintf("Bearer %s", b.token.Token), nil
} else if err != ErrUnauthorized {
return "", err
return "", fmt.Errorf("failed to request auth token (post): %v%.0w", err, types.ErrHTTPUnauthorized)
}
// attempt a get (with basic auth if user/pass available)
if err := b.tryGet(); err == nil {
return fmt.Sprintf("Bearer %s", b.token.Token), nil
} else if err != ErrUnauthorized {
return "", err
return "", fmt.Errorf("failed to request auth token (get): %v%.0w", err, types.ErrHTTPUnauthorized)
}
return "", ErrUnauthorized

View File

@ -412,7 +412,11 @@ func (resp *clientResp) Next() error {
// add auth headers
err = hAuth.UpdateRequest(httpReq)
if err != nil {
backoff = true
if errors.Is(err, types.ErrHTTPUnauthorized) {
dropHost = true
} else {
backoff = true
}
return err
}
}

22
ping.go
View File

@ -2,27 +2,17 @@ package regclient
import (
"context"
"github.com/regclient/regclient/types"
"github.com/regclient/regclient/types/ping"
"github.com/regclient/regclient/types/ref"
)
type repoPinger interface {
Ping(ctx context.Context, r ref.Ref) error
}
// Ping accesses the registry's root endpoint, which allows to determine whether
// the registry implements the OCI Distribution Specification and whether the
// registry accepts the supplied credentials.
func (rc *RegClient) Ping(ctx context.Context, r ref.Ref) error {
// Ping verifies access to a registry or equivalent.
func (rc *RegClient) Ping(ctx context.Context, r ref.Ref) (ping.Result, error) {
schemeAPI, err := rc.schemeGet(r.Scheme)
if err != nil {
return err
return ping.Result{}, err
}
rp, ok := schemeAPI.(repoPinger)
if !ok {
return types.ErrNotImplemented
}
return rp.Ping(ctx, r)
return schemeAPI.Ping(ctx, r)
}

28
scheme/ocidir/ping.go Normal file
View File

@ -0,0 +1,28 @@
package ocidir
import (
"context"
"fmt"
"github.com/regclient/regclient/types/ping"
"github.com/regclient/regclient/types/ref"
)
// Ping for an ocidir verifies access to read the path.
func (o *OCIDir) Ping(ctx context.Context, r ref.Ref) (ping.Result, error) {
ret := ping.Result{}
fd, err := o.fs.Open(r.Path)
if err != nil {
return ret, err
}
defer fd.Close()
fi, err := fd.Stat()
if err != nil {
return ret, err
}
ret.Stat = fi
if !fi.IsDir() {
return ret, fmt.Errorf("failed to access %s: not a directory", r.Path)
}
return ret, nil
}

View File

@ -0,0 +1,73 @@
package ocidir
import (
"context"
"testing"
"github.com/regclient/regclient/internal/rwfs"
"github.com/regclient/regclient/types/ref"
)
func TestPing(t *testing.T) {
t.Parallel()
ctx := context.Background()
f := rwfs.OSNew("")
o := New(WithFS(f))
rOkay, err := ref.NewHost("ocidir://testdata/regctl")
if err != nil {
t.Errorf("failed to create ref: %v", err)
return
}
result, err := o.Ping(ctx, rOkay)
if err != nil {
t.Errorf("failed to ping: %v", err)
return
}
if result.Header != nil {
t.Errorf("header is not nil")
}
if result.Stat == nil {
t.Errorf("stat is nil")
} else {
if !result.Stat.IsDir() {
t.Errorf("stat is not a directory")
}
}
rMissing, err := ref.NewHost("ocidir://testdata/missing")
if err != nil {
t.Errorf("failed to create ref: %v", err)
return
}
result, err = o.Ping(ctx, rMissing)
if err == nil {
t.Errorf("ping to missing directory succeeded")
}
if result.Header != nil {
t.Errorf("header is not nil")
}
if result.Stat != nil {
t.Errorf("stat on missing is not nil")
}
rFile, err := ref.NewHost("ocidir://testdata/regctl/index.json")
if err != nil {
t.Errorf("failed to create ref: %v", err)
return
}
result, err = o.Ping(ctx, rFile)
if err == nil {
t.Errorf("ping to a file did not fail")
return
}
if result.Header != nil {
t.Errorf("header is not nil")
}
if result.Stat == nil {
t.Errorf("stat to a file is nil")
} else {
if result.Stat.IsDir() {
t.Errorf("stat of a file is a directory")
}
}
}

View File

@ -3,11 +3,15 @@ package reg
import (
"context"
"fmt"
"github.com/regclient/regclient/internal/reghttp"
"github.com/regclient/regclient/types/ping"
"github.com/regclient/regclient/types/ref"
)
func (reg *Reg) Ping(ctx context.Context, r ref.Ref) error {
// Ping queries the /v2/ API of the registry to verify connectivity and access.
func (reg *Reg) Ping(ctx context.Context, r ref.Ref) (ping.Result, error) {
ret := ping.Result{}
req := &reghttp.Req{
Host: r.Registry,
NoMirrors: true,
@ -20,15 +24,18 @@ func (reg *Reg) Ping(ctx context.Context, r ref.Ref) error {
}
resp, err := reg.reghttp.Do(ctx, req)
if resp != nil && resp.HTTPResponse() != nil {
ret.Header = resp.HTTPResponse().Header
}
if err != nil {
return fmt.Errorf("failed to ping registry %s: %w", r.Registry, err)
return ret, fmt.Errorf("failed to ping registry %s: %w", r.Registry, err)
}
defer resp.Close()
if resp.HTTPResponse().StatusCode != 200 {
return fmt.Errorf("failed to ping registry %s: %w",
return ret, fmt.Errorf("failed to ping registry %s: %w",
r.Registry, reghttp.HTTPError(resp.HTTPResponse().StatusCode))
}
return nil
return ret, nil
}

179
scheme/reg/ping_test.go Normal file
View File

@ -0,0 +1,179 @@
package reg
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"github.com/sirupsen/logrus"
"github.com/regclient/regclient/config"
"github.com/regclient/regclient/internal/reqresp"
"github.com/regclient/regclient/types"
"github.com/regclient/regclient/types/ref"
)
func TestPing(t *testing.T) {
t.Parallel()
respOkay := "{}"
respUnauth := `{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}`
ctx := context.Background()
contentType := "application/json"
rrsOkay := []reqresp.ReqResp{
{
ReqEntry: reqresp.ReqEntry{
Name: "Get Okay",
Method: "GET",
Path: "/v2/",
},
RespEntry: reqresp.RespEntry{
Status: http.StatusOK,
Headers: http.Header{
"Content-Length": {fmt.Sprintf("%d", len(respOkay))},
"Content-Type": []string{contentType},
"Docker-Distribution-API-Version": {"registry/2.0"},
},
Body: []byte(respOkay),
},
},
}
rrsUnauth := []reqresp.ReqResp{
{
ReqEntry: reqresp.ReqEntry{
Name: "Get Unauth",
Method: "GET",
Path: "/v2/",
},
RespEntry: reqresp.RespEntry{
Status: http.StatusUnauthorized,
Headers: http.Header{
"WWW-Authenticate": []string{"Basic realm=\"test\""},
"Content-Length": {fmt.Sprintf("%d", len(respUnauth))},
"Content-Type": []string{contentType},
"Docker-Distribution-API-Version": {"registry/2.0"},
},
Body: []byte(respUnauth),
},
},
}
rrsNotFound := []reqresp.ReqResp{
{
ReqEntry: reqresp.ReqEntry{
Name: "Get NotFound",
Method: "GET",
Path: "/v2/",
},
RespEntry: reqresp.RespEntry{
Status: http.StatusNotFound,
Headers: http.Header{
"Content-Length": {"0"},
},
Body: []byte(""),
},
},
}
// create a server
tsOkay := httptest.NewServer(reqresp.NewHandler(t, rrsOkay))
defer tsOkay.Close()
tsUnauth := httptest.NewServer(reqresp.NewHandler(t, rrsUnauth))
defer tsUnauth.Close()
tsNotFound := httptest.NewServer(reqresp.NewHandler(t, rrsNotFound))
defer tsNotFound.Close()
// setup the reg
tsOkayURL, _ := url.Parse(tsOkay.URL)
tsOkayHost := tsOkayURL.Host
tsUnauthURL, _ := url.Parse(tsUnauth.URL)
tsUnauthHost := tsUnauthURL.Host
tsNotFoundURL, _ := url.Parse(tsNotFound.URL)
tsNotFoundHost := tsNotFoundURL.Host
rcHosts := []*config.Host{
{
Name: tsOkayHost,
Hostname: tsOkayHost,
TLS: config.TLSDisabled,
},
{
Name: tsUnauthHost,
Hostname: tsUnauthHost,
TLS: config.TLSDisabled,
},
{
Name: tsNotFoundHost,
Hostname: tsNotFoundHost,
TLS: config.TLSDisabled,
},
}
log := &logrus.Logger{
Out: os.Stderr,
Formatter: new(logrus.TextFormatter),
Hooks: make(logrus.LevelHooks),
Level: logrus.WarnLevel,
}
delayInit, _ := time.ParseDuration("0.05s")
delayMax, _ := time.ParseDuration("0.10s")
reg := New(
WithConfigHosts(rcHosts),
WithLog(log),
WithDelay(delayInit, delayMax),
WithRetryLimit(3),
)
t.Run("Okay", func(t *testing.T) {
r, err := ref.NewHost(tsOkayHost)
if err != nil {
t.Errorf("failed to create ref \"%s\": %v", tsOkayHost, err)
return
}
result, err := reg.Ping(ctx, r)
if err != nil {
t.Errorf("failed to ping registry: %v", err)
}
if result.Header == nil {
t.Errorf("headers missing")
} else if result.Header.Get("Content-Type") != contentType {
t.Errorf("unexpected content type, expected %s, received %s", contentType, result.Header.Get("Content-Type"))
}
})
t.Run("Unauth", func(t *testing.T) {
r, err := ref.NewHost(tsUnauthHost)
if err != nil {
t.Errorf("failed to create ref \"%s\": %v", tsUnauthHost, err)
return
}
result, err := reg.Ping(ctx, r)
if err == nil {
t.Errorf("ping did not fail")
return
} else if !errors.Is(err, types.ErrHTTPUnauthorized) {
t.Errorf("unexpected error, expected %v, received %v", types.ErrHTTPUnauthorized, err)
}
if result.Header == nil {
t.Errorf("headers missing")
} else if result.Header.Get("Content-Type") != contentType {
t.Errorf("unexpected content type, expected %s, received %s", contentType, result.Header.Get("Content-Type"))
}
})
t.Run("NotFound", func(t *testing.T) {
r, err := ref.NewHost(tsNotFoundHost)
if err != nil {
t.Errorf("failed to create ref \"%s\": %v", tsNotFoundHost, err)
return
}
result, err := reg.Ping(ctx, r)
if err == nil {
t.Errorf("ping did not fail")
return
} else if !errors.Is(err, types.ErrNotFound) {
t.Errorf("unexpected error, expected %v, received %v", types.ErrNotFound, err)
return
}
if result.Header == nil {
t.Errorf("headers missing")
}
})
}

View File

@ -9,6 +9,7 @@ import (
"github.com/regclient/regclient/types"
"github.com/regclient/regclient/types/blob"
"github.com/regclient/regclient/types/manifest"
"github.com/regclient/regclient/types/ping"
"github.com/regclient/regclient/types/ref"
"github.com/regclient/regclient/types/referrer"
"github.com/regclient/regclient/types/tag"
@ -36,6 +37,9 @@ type API interface {
// ManifestPut sends a manifest to the repository.
ManifestPut(ctx context.Context, r ref.Ref, m manifest.Manifest, opts ...ManifestOpts) error
// Ping verifies access to a registry or equivalent.
Ping(ctx context.Context, r ref.Ref) (ping.Result, error)
// ReferrerList returns a list of referrers to a given reference.
ReferrerList(ctx context.Context, r ref.Ref, opts ...ReferrerOpts) (referrer.ReferrerList, error)

13
types/ping/ping.go Normal file
View File

@ -0,0 +1,13 @@
// Package ping is used for data types with the Ping methods.
package ping
import (
"io/fs"
"net/http"
)
// Result is the response to a ping request.
type Result struct {
Header http.Header // Header is defined for responses from a registry.
Stat fs.FileInfo // Stat is defined for responses from an ocidir.
}