mirror of
https://github.com/regclient/regclient.git
synced 2025-04-18 22:44:00 +03:00
Merge pull request #930 from sudo-bmitch/pr-regctl-tag-rm-missing
Feat: regctl flag to ignore missing images on delete
This commit is contained in:
commit
e5a71ed0be
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
@ -13,6 +14,7 @@ import (
|
||||
"github.com/regclient/regclient/internal/diff"
|
||||
"github.com/regclient/regclient/pkg/template"
|
||||
"github.com/regclient/regclient/types/descriptor"
|
||||
"github.com/regclient/regclient/types/errs"
|
||||
"github.com/regclient/regclient/types/manifest"
|
||||
"github.com/regclient/regclient/types/platform"
|
||||
"github.com/regclient/regclient/types/ref"
|
||||
@ -27,6 +29,7 @@ type manifestOpts struct {
|
||||
diffFullCtx bool
|
||||
forceTagDeref bool
|
||||
format string
|
||||
ignoreMissing bool
|
||||
list bool
|
||||
platform string
|
||||
referrers bool
|
||||
@ -75,8 +78,9 @@ regctl manifest delete --referrers \
|
||||
ValidArgs: []string{}, // do not auto complete digests
|
||||
RunE: opts.runManifestDelete,
|
||||
}
|
||||
cmd.Flags().BoolVarP(&opts.forceTagDeref, "force-tag-dereference", "", false, "Dereference the a tag to a digest, this is unsafe")
|
||||
cmd.Flags().BoolVarP(&opts.referrers, "referrers", "", false, "Check for referrers, recommended when deleting artifacts")
|
||||
cmd.Flags().BoolVar(&opts.forceTagDeref, "force-tag-dereference", false, "Dereference the a tag to a digest, this is unsafe")
|
||||
cmd.Flags().BoolVar(&opts.ignoreMissing, "ignore-missing", false, "Ignore errors if manifest is missing")
|
||||
cmd.Flags().BoolVar(&opts.referrers, "referrers", false, "Check for referrers, recommended when deleting artifacts")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -102,8 +106,8 @@ regctl manifest diff --context-full \
|
||||
ValidArgsFunction: rOpts.completeArgTag,
|
||||
RunE: opts.runManifestDiff,
|
||||
}
|
||||
cmd.Flags().IntVarP(&opts.diffCtx, "context", "", 3, "Lines of context")
|
||||
cmd.Flags().BoolVarP(&opts.diffFullCtx, "context-full", "", false, "Show all lines of context")
|
||||
cmd.Flags().IntVar(&opts.diffCtx, "context", 3, "Lines of context")
|
||||
cmd.Flags().BoolVar(&opts.diffFullCtx, "context-full", false, "Show all lines of context")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -129,13 +133,13 @@ regctl manifest get golang --platform windows/amd64,osver=10.0.17763.4974`,
|
||||
ValidArgsFunction: rOpts.completeArgTag,
|
||||
RunE: opts.runManifestGet,
|
||||
}
|
||||
cmd.Flags().StringVarP(&opts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax (use \"raw-body\" for the original manifest)")
|
||||
cmd.Flags().StringVar(&opts.format, "format", "{{printPretty .}}", "Format output with go template syntax (use \"raw-body\" for the original manifest)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
|
||||
cmd.Flags().BoolVarP(&opts.list, "list", "", true, "Deprecated: Output manifest list if available")
|
||||
cmd.Flags().BoolVar(&opts.list, "list", true, "Deprecated: Output manifest list if available")
|
||||
_ = cmd.Flags().MarkHidden("list")
|
||||
cmd.Flags().StringVarP(&opts.platform, "platform", "p", "", "Specify platform (e.g. linux/amd64 or local)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
|
||||
cmd.Flags().BoolVarP(&opts.requireList, "require-list", "", false, "Deprecated: Fail if manifest list is not received")
|
||||
cmd.Flags().BoolVar(&opts.requireList, "require-list", false, "Deprecated: Fail if manifest list is not received")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -164,14 +168,14 @@ regctl manifest head alpine --format raw-headers`,
|
||||
ValidArgsFunction: rOpts.completeArgTag,
|
||||
RunE: opts.runManifestHead,
|
||||
}
|
||||
cmd.Flags().StringVarP(&opts.format, "format", "", "", "Format output with go template syntax (use \"raw-body\" for the original manifest)")
|
||||
cmd.Flags().StringVar(&opts.format, "format", "", "Format output with go template syntax (use \"raw-body\" for the original manifest)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
|
||||
cmd.Flags().BoolVarP(&opts.list, "list", "", true, "Do not resolve platform from manifest list (enabled by default)")
|
||||
cmd.Flags().BoolVar(&opts.list, "list", true, "Do not resolve platform from manifest list (enabled by default)")
|
||||
_ = cmd.Flags().MarkHidden("list")
|
||||
cmd.Flags().StringVarP(&opts.platform, "platform", "p", "", "Specify platform (e.g. linux/amd64 or local, requires a get request)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
|
||||
cmd.Flags().BoolVarP(&opts.requireDigest, "require-digest", "", false, "Fallback to a GET request if digest is not received")
|
||||
cmd.Flags().BoolVarP(&opts.requireList, "require-list", "", false, "Fail if manifest list is not received")
|
||||
cmd.Flags().BoolVar(&opts.requireDigest, "require-digest", false, "Fallback to a GET request if digest is not received")
|
||||
cmd.Flags().BoolVar(&opts.requireList, "require-list", false, "Fail if manifest list is not received")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -193,10 +197,10 @@ regctl manifest put \
|
||||
ValidArgsFunction: rOpts.completeArgTag,
|
||||
RunE: opts.runManifestPut,
|
||||
}
|
||||
cmd.Flags().BoolVarP(&opts.byDigest, "by-digest", "", false, "Push manifest by digest instead of tag")
|
||||
cmd.Flags().BoolVar(&opts.byDigest, "by-digest", false, "Push manifest by digest instead of tag")
|
||||
cmd.Flags().StringVarP(&opts.contentType, "content-type", "t", "", "Specify content-type (e.g. application/vnd.docker.distribution.manifest.v2+json)")
|
||||
_ = cmd.RegisterFlagCompletionFunc("content-type", completeArgMediaTypeManifest)
|
||||
cmd.Flags().StringVarP(&opts.format, "format", "", "", "Format output with go template syntax")
|
||||
cmd.Flags().StringVar(&opts.format, "format", "", "Format output with go template syntax")
|
||||
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
|
||||
return cmd
|
||||
}
|
||||
@ -216,6 +220,12 @@ func (opts *manifestOpts) runManifestDelete(cmd *cobra.Command, args []string) e
|
||||
|
||||
if r.Digest == "" && opts.forceTagDeref {
|
||||
m, err := rc.ManifestHead(ctx, r, regclient.WithManifestRequireDigest())
|
||||
if err != nil && opts.ignoreMissing {
|
||||
_, mErr := rc.ManifestHead(ctx, r)
|
||||
if errors.Is(mErr, errs.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -235,6 +245,12 @@ func (opts *manifestOpts) runManifestDelete(cmd *cobra.Command, args []string) e
|
||||
}
|
||||
|
||||
err = rc.ManifestDelete(ctx, r, mOpts...)
|
||||
if err != nil && opts.ignoreMissing {
|
||||
_, mErr := rc.ManifestHead(ctx, r)
|
||||
if errors.Is(mErr, errs.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -1,12 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/olareg/olareg"
|
||||
oConfig "github.com/olareg/olareg/config"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
"github.com/regclient/regclient"
|
||||
"github.com/regclient/regclient/config"
|
||||
"github.com/regclient/regclient/scheme/reg"
|
||||
"github.com/regclient/regclient/types/errs"
|
||||
"github.com/regclient/regclient/types/ref"
|
||||
)
|
||||
|
||||
func TestManifestHead(t *testing.T) {
|
||||
@ -70,3 +83,119 @@ func TestManifestHead(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestRm(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
boolT := true
|
||||
regHandler := olareg.New(oConfig.Config{
|
||||
Storage: oConfig.ConfigStorage{
|
||||
StoreType: oConfig.StoreMem,
|
||||
RootDir: "../../testdata",
|
||||
},
|
||||
API: oConfig.ConfigAPI{
|
||||
DeleteEnabled: &boolT,
|
||||
},
|
||||
})
|
||||
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: "invalid-tls." + tsHost,
|
||||
Hostname: tsHost,
|
||||
TLS: config.TLSEnabled,
|
||||
},
|
||||
),
|
||||
regclient.WithRegOpts(reg.WithDelay(time.Millisecond*10, time.Millisecond*100), reg.WithRetryLimit(2)),
|
||||
}
|
||||
rb, err := ref.New("ocidir://../../testdata/testrepo:v3")
|
||||
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.String()
|
||||
missing := digest.Canonical.FromString("missing").String()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectErr error
|
||||
expectOut string
|
||||
outContains bool
|
||||
}{
|
||||
{
|
||||
name: "Missing arg",
|
||||
args: []string{"manifest", "rm"},
|
||||
expectErr: fmt.Errorf("accepts 1 arg(s), received 0"),
|
||||
},
|
||||
{
|
||||
name: "Delete v3 by digest",
|
||||
args: []string{"manifest", "rm", tsHost + "/testrepo@" + dig},
|
||||
},
|
||||
{
|
||||
name: "Delete v1 without deref",
|
||||
args: []string{"manifest", "rm", tsHost + "/testrepo:v1"},
|
||||
expectErr: errs.ErrMissingDigest,
|
||||
},
|
||||
{
|
||||
name: "Delete v1 with deref",
|
||||
args: []string{"manifest", "rm", tsHost + "/testrepo:v1", "--force-tag-dereference"},
|
||||
},
|
||||
{
|
||||
name: "Delete missing digest",
|
||||
args: []string{"manifest", "rm", tsHost + "/testrepo@" + missing, "--force-tag-dereference"},
|
||||
expectErr: errs.ErrNotFound,
|
||||
},
|
||||
{
|
||||
name: "Delete missing digest with ignore missing",
|
||||
args: []string{"manifest", "rm", tsHost + "/testrepo@" + missing, "--ignore-missing"},
|
||||
},
|
||||
{
|
||||
name: "Delete missing tag with deref",
|
||||
args: []string{"manifest", "rm", tsHost + "/testrepo:missing", "--force-tag-dereference"},
|
||||
expectErr: errs.ErrNotFound,
|
||||
},
|
||||
{
|
||||
name: "Delete missing tag with deref and ignore missing",
|
||||
args: []string{"manifest", "rm", tsHost + "/testrepo:missing", "--force-tag-dereference", "--ignore-missing"},
|
||||
},
|
||||
{
|
||||
name: "Delete tls error with ignore missing",
|
||||
args: []string{"manifest", "rm", "invalid-tls." + tsHost + "/testrepo@" + missing, "--ignore-missing"},
|
||||
expectErr: http.ErrSchemeMismatch,
|
||||
},
|
||||
}
|
||||
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 (!tc.outContains && out != tc.expectOut) || (tc.outContains && !strings.Contains(out, tc.expectOut)) {
|
||||
t.Errorf("unexpected output, expected %s, received %s", tc.expectOut, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"regexp"
|
||||
@ -9,16 +10,18 @@ import (
|
||||
|
||||
"github.com/regclient/regclient/pkg/template"
|
||||
"github.com/regclient/regclient/scheme"
|
||||
"github.com/regclient/regclient/types/errs"
|
||||
"github.com/regclient/regclient/types/ref"
|
||||
)
|
||||
|
||||
type tagOpts struct {
|
||||
rootOpts *rootOpts
|
||||
limit int
|
||||
last string
|
||||
include []string
|
||||
exclude []string
|
||||
format string
|
||||
rootOpts *rootOpts
|
||||
limit int
|
||||
last string
|
||||
include []string
|
||||
exclude []string
|
||||
format string
|
||||
ignoreMissing bool
|
||||
}
|
||||
|
||||
func NewTagCmd(rOpts *rootOpts) *cobra.Command {
|
||||
@ -51,6 +54,7 @@ regctl tag delete registry.example.org/repo:v42`,
|
||||
ValidArgsFunction: rOpts.completeArgTag,
|
||||
RunE: opts.runTagDelete,
|
||||
}
|
||||
cmd.Flags().BoolVar(&opts.ignoreMissing, "ignore-missing", false, "Ignore errors if tag is missing")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -102,6 +106,12 @@ func (opts *tagOpts) runTagDelete(cmd *cobra.Command, args []string) error {
|
||||
slog.String("repository", r.Repository),
|
||||
slog.String("tag", r.Tag))
|
||||
err = rc.TagDelete(ctx, r)
|
||||
if err != nil && opts.ignoreMissing {
|
||||
_, mErr := rc.ManifestHead(ctx, r)
|
||||
if errors.Is(mErr, errs.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -4,13 +4,25 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/olareg/olareg"
|
||||
oConfig "github.com/olareg/olareg/config"
|
||||
"github.com/opencontainers/go-digest"
|
||||
|
||||
"github.com/regclient/regclient"
|
||||
"github.com/regclient/regclient/config"
|
||||
"github.com/regclient/regclient/scheme/reg"
|
||||
"github.com/regclient/regclient/types/errs"
|
||||
)
|
||||
|
||||
func TestTagList(t *testing.T) {
|
||||
t.Parallel()
|
||||
tt := []struct {
|
||||
name string
|
||||
args []string
|
||||
@ -78,3 +90,95 @@ func TestTagList(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagRm(t *testing.T) {
|
||||
t.Parallel()
|
||||
boolT := true
|
||||
regHandler := olareg.New(oConfig.Config{
|
||||
Storage: oConfig.ConfigStorage{
|
||||
StoreType: oConfig.StoreMem,
|
||||
RootDir: "../../testdata",
|
||||
},
|
||||
API: oConfig.ConfigAPI{
|
||||
DeleteEnabled: &boolT,
|
||||
},
|
||||
})
|
||||
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: "invalid-tls." + tsHost,
|
||||
Hostname: tsHost,
|
||||
TLS: config.TLSEnabled,
|
||||
},
|
||||
),
|
||||
regclient.WithRegOpts(reg.WithDelay(time.Millisecond*10, time.Millisecond*100), reg.WithRetryLimit(2)),
|
||||
}
|
||||
dig := digest.Canonical.FromString("test digest").String()
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectErr error
|
||||
expectOut string
|
||||
outContains bool
|
||||
}{
|
||||
{
|
||||
name: "Missing arg",
|
||||
args: []string{"tag", "rm"},
|
||||
expectErr: fmt.Errorf("accepts 1 arg(s), received 0"),
|
||||
},
|
||||
{
|
||||
name: "Delete digest",
|
||||
args: []string{"tag", "rm", tsHost + "/testrepo@" + dig},
|
||||
expectErr: errs.ErrMissingTag,
|
||||
},
|
||||
{
|
||||
name: "Delete v1",
|
||||
args: []string{"tag", "rm", tsHost + "/testrepo:v1"},
|
||||
},
|
||||
{
|
||||
name: "Delete missing",
|
||||
args: []string{"tag", "rm", tsHost + "/testrepo:missing"},
|
||||
expectErr: errs.ErrNotFound,
|
||||
},
|
||||
{
|
||||
name: "Delete missing with ignore missing",
|
||||
args: []string{"tag", "rm", tsHost + "/testrepo:missing", "--ignore-missing"},
|
||||
},
|
||||
{
|
||||
name: "Delete tls error with ignore missing",
|
||||
args: []string{"tag", "rm", "invalid-tls." + tsHost + "/testrepo:missing", "--ignore-missing"},
|
||||
expectErr: http.ErrSchemeMismatch,
|
||||
},
|
||||
}
|
||||
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 (!tc.outContains && out != tc.expectOut) || (tc.outContains && !strings.Contains(out, tc.expectOut)) {
|
||||
t.Errorf("unexpected output, expected %s, received %s", tc.expectOut, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user