mirror of
https://github.com/regclient/regclient.git
synced 2025-04-18 22:44:00 +03:00
Feat: Improve regctl image check-base output
Output makes it more apparent when the base image has changed. Tests were added, which required some work to pass through "regclient.Opts". The empty error message is also suppressed to avoid an extra linefeed to stderr. Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
parent
6a085162bb
commit
e9a21b954f
@ -63,6 +63,7 @@ type imageOpts struct {
|
||||
modOpts []mod.Opts
|
||||
platform string
|
||||
platforms []string
|
||||
quiet bool
|
||||
referrers bool
|
||||
referrerSrc string
|
||||
referrerTgt string
|
||||
@ -107,11 +108,15 @@ If the base name is not provided, annotations will be checked in the image.
|
||||
If the digest is available, this checks if that matches the base name.
|
||||
If the digest is not available, layers of each manifest are compared.
|
||||
If the layers match, the config (history and roots) are optionally compared.
|
||||
If the base image does not match, the command exits with a non-zero status.
|
||||
Use "-v info" to see more details.`,
|
||||
If the base image does not match, the command exits with a non-zero status.`,
|
||||
Example: `
|
||||
# report if base image has changed using annotations
|
||||
regctl image check-base ghcr.io/regclient/regctl:alpine -v info`,
|
||||
regctl image check-base ghcr.io/regclient/regctl:alpine
|
||||
|
||||
# suppress the normal output with --quiet for scripts
|
||||
if ! regctl image check-base ghcr.io/regclient/regctl:alpine --quiet; then
|
||||
echo build a new image here
|
||||
fi`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: rOpts.completeArgTag,
|
||||
RunE: opts.runImageCheckBase,
|
||||
@ -120,6 +125,7 @@ regctl image check-base ghcr.io/regclient/regctl:alpine -v info`,
|
||||
cmd.Flags().StringVar(&opts.checkBaseDigest, "digest", "", "Base image digest (checks if digest matches base)")
|
||||
cmd.Flags().BoolVar(&opts.checkSkipConfig, "no-config", false, "Skip check of config history")
|
||||
cmd.Flags().StringVarP(&opts.platform, "platform", "p", "", "Specify platform (e.g. linux/amd64 or local)")
|
||||
cmd.Flags().BoolVar(&opts.quiet, "quiet", false, "Do not output to stdout")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -1059,15 +1065,19 @@ func (opts *imageOpts) runImageCheckBase(cmd *cobra.Command, args []string) erro
|
||||
err = rc.ImageCheckBase(ctx, r, rcOpts...)
|
||||
if err == nil {
|
||||
opts.rootOpts.log.Info("base image matches")
|
||||
return nil
|
||||
if !opts.quiet {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "base image matches\n")
|
||||
}
|
||||
} else if errors.Is(err, errs.ErrMismatch) {
|
||||
opts.rootOpts.log.Info("base image mismatch",
|
||||
slog.String("err", err.Error()))
|
||||
// return empty error message
|
||||
return fmt.Errorf("%.0w", err)
|
||||
} else {
|
||||
return err
|
||||
err = fmt.Errorf("%.0w", err)
|
||||
if !opts.quiet {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "base image has changed\n")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (opts *imageOpts) runImageCopy(cmd *cobra.Command, args []string) error {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
@ -12,9 +13,125 @@ import (
|
||||
"github.com/olareg/olareg"
|
||||
oConfig "github.com/olareg/olareg/config"
|
||||
|
||||
"github.com/regclient/regclient"
|
||||
"github.com/regclient/regclient/config"
|
||||
"github.com/regclient/regclient/types/errs"
|
||||
"github.com/regclient/regclient/types/ref"
|
||||
)
|
||||
|
||||
func TestImageCheckBase(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
regHandler := olareg.New(oConfig.Config{
|
||||
Storage: oConfig.ConfigStorage{
|
||||
StoreType: oConfig.StoreMem,
|
||||
RootDir: "../../testdata",
|
||||
},
|
||||
})
|
||||
ts := httptest.NewServer(regHandler)
|
||||
tsURL, _ := url.Parse(ts.URL)
|
||||
tsHost := tsURL.Host
|
||||
t.Cleanup(func() {
|
||||
ts.Close()
|
||||
_ = regHandler.Close()
|
||||
})
|
||||
rcOpts := []regclient.Opt{
|
||||
regclient.WithConfigHost(
|
||||
config.Host{
|
||||
Name: tsHost,
|
||||
TLS: config.TLSDisabled,
|
||||
},
|
||||
config.Host{
|
||||
Name: "registry.example.org",
|
||||
Hostname: tsHost,
|
||||
TLS: config.TLSDisabled,
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
rb, err := ref.New("ocidir://../../testdata/testrepo:b3")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse ref: %v", err)
|
||||
}
|
||||
rc := regclient.New(rcOpts...)
|
||||
mb, err := rc.ManifestHead(ctx, rb, regclient.WithManifestRequireDigest())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to head ref %s: %v", rb.CommonName(), err)
|
||||
}
|
||||
dig := mb.GetDescriptor().Digest
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectErr error
|
||||
expectOut string
|
||||
}{
|
||||
{
|
||||
name: "missing annotation",
|
||||
args: []string{"image", "check-base", tsHost + "/testrepo:v1"},
|
||||
expectErr: errs.ErrMissingAnnotation,
|
||||
},
|
||||
{
|
||||
name: "annotation v2",
|
||||
args: []string{"image", "check-base", tsHost + "/testrepo:v2"},
|
||||
expectErr: errs.ErrMismatch,
|
||||
expectOut: "base image has changed",
|
||||
},
|
||||
{
|
||||
name: "annotation v3",
|
||||
args: []string{"image", "check-base", tsHost + "/testrepo:v3"},
|
||||
expectErr: errs.ErrMismatch,
|
||||
expectOut: "base image has changed",
|
||||
},
|
||||
{
|
||||
name: "manual v2 b1",
|
||||
args: []string{"image", "check-base", tsHost + "/testrepo:v2", "--base", tsHost + "/testrepo:b1"},
|
||||
expectOut: "base image matches",
|
||||
},
|
||||
{
|
||||
name: "manual v2 b2",
|
||||
args: []string{"image", "check-base", tsHost + "/testrepo:v2", "--base", tsHost + "/testrepo:b2"},
|
||||
expectErr: errs.ErrMismatch,
|
||||
expectOut: "base image has changed",
|
||||
},
|
||||
{
|
||||
name: "manual v3 b1",
|
||||
args: []string{"image", "check-base", tsHost + "/testrepo:v3", "--base", tsHost + "/testrepo:b1"},
|
||||
expectOut: "base image matches",
|
||||
},
|
||||
{
|
||||
name: "manual v3 b3",
|
||||
args: []string{"image", "check-base", tsHost + "/testrepo:v3", "--base", tsHost + "/testrepo:b3"},
|
||||
expectErr: errs.ErrMismatch,
|
||||
expectOut: "base image has changed",
|
||||
},
|
||||
{
|
||||
name: "manual v3 b3 with digest",
|
||||
args: []string{"image", "check-base", tsHost + "/testrepo:v3", "--base", tsHost + "/testrepo:b3", "--digest", dig.String()},
|
||||
expectOut: "base image matches",
|
||||
},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
out, err := cobraTest(t, &cobraTestOpts{rcOpts: rcOpts}, tc.args...)
|
||||
if tc.expectErr != nil {
|
||||
if err == nil {
|
||||
t.Errorf("did not receive expected error: %v", tc.expectErr)
|
||||
} else if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() {
|
||||
t.Errorf("unexpected error, received %v, expected %v", err, tc.expectErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("returned unexpected error: %v", err)
|
||||
}
|
||||
if out != tc.expectOut {
|
||||
t.Errorf("unexpected output, expected %s, received %s", tc.expectOut, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageCopy(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
srcRef := "ocidir://../../testdata/testrepo:v2"
|
||||
|
@ -26,7 +26,9 @@ func main() {
|
||||
godbg.SignalTrace()
|
||||
|
||||
if err := cmd.ExecuteContext(ctx); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||
if err.Error() != "" {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||
}
|
||||
// provide tips for common error messages
|
||||
switch {
|
||||
case strings.Contains(err.Error(), "http: server gave HTTP response to HTTPS client"):
|
||||
|
@ -5,17 +5,23 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/regclient/regclient"
|
||||
)
|
||||
|
||||
type cobraTestOpts struct {
|
||||
stdin io.Reader
|
||||
stdin io.Reader
|
||||
rcOpts []regclient.Opt
|
||||
}
|
||||
|
||||
func cobraTest(t *testing.T, opts *cobraTestOpts, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
rootTopCmd, _ := NewRootCmd()
|
||||
rootTopCmd, rootOpts := NewRootCmd()
|
||||
if opts != nil && opts.rcOpts != nil {
|
||||
rootOpts.rcOpts = opts.rcOpts
|
||||
}
|
||||
if opts != nil && opts.stdin != nil {
|
||||
rootTopCmd.SetIn(opts.stdin)
|
||||
}
|
||||
|
@ -25,12 +25,13 @@ const (
|
||||
)
|
||||
|
||||
type rootOpts struct {
|
||||
hosts []string
|
||||
name string
|
||||
verbosity string
|
||||
logopts []string
|
||||
log *slog.Logger
|
||||
hosts []string
|
||||
rcOpts []regclient.Opt
|
||||
userAgent string
|
||||
verbosity string
|
||||
}
|
||||
|
||||
type versionOpts struct {
|
||||
@ -160,6 +161,9 @@ func (opts *rootOpts) newRegClient() *regclient.RegClient {
|
||||
regclient.WithSlog(opts.log),
|
||||
regclient.WithRegOpts(reg.WithCache(time.Minute*5, 500)),
|
||||
}
|
||||
if len(opts.rcOpts) > 0 {
|
||||
rcOpts = append(rcOpts, opts.rcOpts...)
|
||||
}
|
||||
if opts.userAgent != "" {
|
||||
rcOpts = append(rcOpts, regclient.WithUserAgent(opts.userAgent))
|
||||
} else {
|
||||
|
Loading…
x
Reference in New Issue
Block a user