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

Merge pull request #917 from sudo-bmitch/pr-check-base-output

Feat: Improve regctl image check-base output
This commit is contained in:
Brandon Mitchell 2025-03-06 09:31:50 -05:00 committed by GitHub
commit f8fd7f93f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 151 additions and 12 deletions

View File

@ -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 {

View File

@ -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"

View File

@ -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"):

View File

@ -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)
}

View File

@ -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 {