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:
commit
f8fd7f93f4
@ -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