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:
parent
2881dcfb0d
commit
0a514c3e2d
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
22
ping.go
@ -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
28
scheme/ocidir/ping.go
Normal 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
|
||||
}
|
73
scheme/ocidir/ping_test.go
Normal file
73
scheme/ocidir/ping_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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 := ®http.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
179
scheme/reg/ping_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
@ -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
13
types/ping/ping.go
Normal 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.
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user