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

Chore: Refactor cobra commands

This helps align the different commands with each other.
- Variable names have been improved to be less confusing.
- Flags have been sorted, and completion options added on some flags where missing.
- Each command creates its own options to avoid default flag value conflicts.
- Reusing a command under two paths is now done by calling that commands "new" function.
- Global-but-not-really-global options have been moved to be associated with the specific commands that use them.

Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
Brandon Mitchell 2025-02-27 10:56:08 -05:00
parent b753620f11
commit 288f2c3da0
No known key found for this signature in database
GPG Key ID: 6E0FF28C767A8BEE
18 changed files with 1723 additions and 1543 deletions

View File

@ -165,7 +165,7 @@ defaults:
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rootOpts := rootCmd{
rootOpts := rootOpts{
dryRun: tt.dryrun,
conf: conf,
log: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})),

View File

@ -29,7 +29,7 @@ More details at <https://github.com/regclient/regclient>`
UserAgent = "regclient/regbot"
)
type rootCmd struct {
type rootOpts struct {
confFile string
dryRun bool
verbosity string
@ -41,105 +41,111 @@ type rootCmd struct {
throttle *pqueue.Queue[struct{}]
}
func NewRootCmd() (*cobra.Command, *rootCmd) {
rootOpts := rootCmd{
func NewRootCmd() (*cobra.Command, *rootOpts) {
opts := rootOpts{
log: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})),
}
var rootTopCmd = &cobra.Command{
Use: "regbot <cmd>",
Short: "Utility for automating repository actions",
Long: usageDesc,
SilenceUsage: true,
SilenceErrors: true,
cmd := &cobra.Command{
Use: "regbot <cmd>",
Short: "Utility for automating repository actions",
Long: usageDesc,
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: opts.rootPreRun,
}
var serverCmd = &cobra.Command{
serverCmd := &cobra.Command{
Use: "server",
Short: "run the regbot server",
Long: `Runs the various scripts according to their schedule.`,
Args: cobra.RangeArgs(0, 0),
RunE: rootOpts.runServer,
RunE: opts.runServer,
}
var onceCmd = &cobra.Command{
onceCmd := &cobra.Command{
Use: "once",
Short: "runs each script once",
Long: `Each script is executed once ignoring any scheduling. The command
returns after the last script completes.`,
Args: cobra.RangeArgs(0, 0),
RunE: rootOpts.runOnce,
RunE: opts.runOnce,
}
var versionCmd = &cobra.Command{
versionCmd := &cobra.Command{
Use: "version",
Short: "Show the version",
Long: `Show the version`,
Args: cobra.RangeArgs(0, 0),
RunE: rootOpts.runVersion,
RunE: opts.runVersion,
}
rootTopCmd.PersistentFlags().StringVarP(&rootOpts.confFile, "config", "c", "", "Config file")
rootTopCmd.PersistentFlags().BoolVarP(&rootOpts.dryRun, "dry-run", "", false, "Dry Run, skip all external actions")
rootTopCmd.PersistentFlags().StringVarP(&rootOpts.verbosity, "verbosity", "v", slog.LevelInfo.String(), "Log level (trace, debug, info, warn, error)")
rootTopCmd.PersistentFlags().StringArrayVar(&rootOpts.logopts, "logopt", []string{}, "Log options")
versionCmd.Flags().StringVarP(&rootOpts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
cmd.PersistentFlags().StringArrayVar(&opts.logopts, "logopt", []string{}, "Log options")
cmd.PersistentFlags().StringVarP(&opts.verbosity, "verbosity", "v", slog.LevelInfo.String(), "Log level (trace, debug, info, warn, error)")
_ = rootTopCmd.MarkPersistentFlagFilename("config")
_ = serverCmd.MarkPersistentFlagRequired("config")
_ = onceCmd.MarkPersistentFlagRequired("config")
for _, curCmd := range []*cobra.Command{serverCmd, onceCmd} {
curCmd.Flags().StringVarP(&opts.confFile, "config", "c", "", "Config file")
_ = curCmd.MarkFlagFilename("config")
_ = curCmd.MarkFlagRequired("config")
curCmd.Flags().BoolVarP(&opts.dryRun, "dry-run", "", false, "Dry Run, skip all external actions")
}
rootTopCmd.AddCommand(serverCmd)
rootTopCmd.AddCommand(onceCmd)
rootTopCmd.AddCommand(versionCmd)
rootTopCmd.AddCommand(cobradoc.NewCmd(rootTopCmd.Name(), "cli-doc"))
versionCmd.Flags().StringVarP(&opts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
_ = versionCmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp
})
rootTopCmd.PersistentPreRunE = rootOpts.rootPreRun
return rootTopCmd, &rootOpts
cmd.AddCommand(
serverCmd,
onceCmd,
versionCmd,
cobradoc.NewCmd(cmd.Name(), "cli-doc"),
)
return cmd, &opts
}
func (rootOpts *rootCmd) rootPreRun(cmd *cobra.Command, args []string) error {
func (opts *rootOpts) rootPreRun(cmd *cobra.Command, args []string) error {
var lvl slog.Level
err := lvl.UnmarshalText([]byte(rootOpts.verbosity))
err := lvl.UnmarshalText([]byte(opts.verbosity))
if err != nil {
// handle custom levels
if rootOpts.verbosity == strings.ToLower("trace") {
if opts.verbosity == strings.ToLower("trace") {
lvl = types.LevelTrace
} else {
return fmt.Errorf("unable to parse verbosity %s: %v", rootOpts.verbosity, err)
return fmt.Errorf("unable to parse verbosity %s: %v", opts.verbosity, err)
}
}
formatJSON := false
for _, opt := range rootOpts.logopts {
for _, opt := range opts.logopts {
if opt == "json" {
formatJSON = true
}
}
if formatJSON {
rootOpts.log = slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}))
opts.log = slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}))
} else {
rootOpts.log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}))
opts.log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lvl}))
}
return nil
}
func (rootOpts *rootCmd) runVersion(cmd *cobra.Command, args []string) error {
func (opts *rootOpts) runVersion(cmd *cobra.Command, args []string) error {
info := version.GetInfo()
return template.Writer(os.Stdout, rootOpts.format, info)
return template.Writer(os.Stdout, opts.format, info)
}
// runOnce processes the file in one pass, ignoring cron
func (rootOpts *rootCmd) runOnce(cmd *cobra.Command, args []string) error {
err := rootOpts.loadConf()
func (opts *rootOpts) runOnce(cmd *cobra.Command, args []string) error {
err := opts.loadConf()
if err != nil {
return err
}
ctx := cmd.Context()
var wg sync.WaitGroup
var mainErr error
for _, s := range rootOpts.conf.Scripts {
if rootOpts.conf.Defaults.Parallel > 0 {
for _, s := range opts.conf.Scripts {
if opts.conf.Defaults.Parallel > 0 {
wg.Add(1)
go func() {
defer wg.Done()
err := rootOpts.process(ctx, s)
err := opts.process(ctx, s)
if err != nil {
if mainErr == nil {
mainErr = err
@ -148,7 +154,7 @@ func (rootOpts *rootCmd) runOnce(cmd *cobra.Command, args []string) error {
}
}()
} else {
err := rootOpts.process(ctx, s)
err := opts.process(ctx, s)
if err != nil {
if mainErr == nil {
mainErr = err
@ -161,8 +167,8 @@ func (rootOpts *rootCmd) runOnce(cmd *cobra.Command, args []string) error {
}
// runServer stays running with cron scheduled tasks
func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
err := rootOpts.loadConf()
func (opts *rootOpts) runServer(cmd *cobra.Command, args []string) error {
err := opts.loadConf()
if err != nil {
return err
}
@ -172,27 +178,27 @@ func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
c := cron.New(cron.WithChain(
cron.SkipIfStillRunning(cron.DefaultLogger),
))
for _, s := range rootOpts.conf.Scripts {
for _, s := range opts.conf.Scripts {
sched := s.Schedule
if sched == "" && s.Interval != 0 {
sched = "@every " + s.Interval.String()
}
if sched != "" {
rootOpts.log.Debug("Scheduled task",
opts.log.Debug("Scheduled task",
slog.String("name", s.Name),
slog.String("sched", sched))
_, errCron := c.AddFunc(sched, func() {
rootOpts.log.Debug("Running task",
opts.log.Debug("Running task",
slog.String("name", s.Name))
wg.Add(1)
defer wg.Done()
err := rootOpts.process(ctx, s)
err := opts.process(ctx, s)
if mainErr == nil {
mainErr = err
}
})
if errCron != nil {
rootOpts.log.Error("Failed to schedule cron",
opts.log.Error("Failed to schedule cron",
slog.String("name", s.Name),
slog.String("sched", sched),
slog.String("err", errCron.Error()))
@ -201,7 +207,7 @@ func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
}
}
} else {
rootOpts.log.Error("No schedule or interval found, ignoring",
opts.log.Error("No schedule or interval found, ignoring",
slog.String("name", s.Name))
}
}
@ -211,28 +217,28 @@ func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
if done != nil {
<-done
}
rootOpts.log.Info("Stopping server")
opts.log.Info("Stopping server")
// clean shutdown
c.Stop()
rootOpts.log.Debug("Waiting on running tasks")
opts.log.Debug("Waiting on running tasks")
wg.Wait()
return mainErr
}
func (rootOpts *rootCmd) loadConf() error {
func (opts *rootOpts) loadConf() error {
var err error
if rootOpts.confFile == "-" {
rootOpts.conf, err = ConfigLoadReader(os.Stdin)
if opts.confFile == "-" {
opts.conf, err = ConfigLoadReader(os.Stdin)
if err != nil {
return err
}
} else if rootOpts.confFile != "" {
r, err := os.Open(rootOpts.confFile)
} else if opts.confFile != "" {
r, err := os.Open(opts.confFile)
if err != nil {
return err
}
defer r.Close()
rootOpts.conf, err = ConfigLoadReader(r)
opts.conf, err = ConfigLoadReader(r)
if err != nil {
return err
}
@ -240,25 +246,25 @@ func (rootOpts *rootCmd) loadConf() error {
return ErrMissingInput
}
// use a throttle to control parallelism
concurrent := rootOpts.conf.Defaults.Parallel
concurrent := opts.conf.Defaults.Parallel
if concurrent <= 0 {
concurrent = 1
}
rootOpts.log.Debug("Configuring parallel settings",
opts.log.Debug("Configuring parallel settings",
slog.Int("concurrent", concurrent))
rootOpts.throttle = pqueue.New(pqueue.Opts[struct{}]{Max: concurrent})
opts.throttle = pqueue.New(pqueue.Opts[struct{}]{Max: concurrent})
// set the regclient, loading docker creds unless disabled, and inject logins from config file
rcOpts := []regclient.Opt{
regclient.WithSlog(rootOpts.log),
regclient.WithSlog(opts.log),
}
if rootOpts.conf.Defaults.BlobLimit != 0 {
rcOpts = append(rcOpts, regclient.WithRegOpts(reg.WithBlobLimit(rootOpts.conf.Defaults.BlobLimit)))
if opts.conf.Defaults.BlobLimit != 0 {
rcOpts = append(rcOpts, regclient.WithRegOpts(reg.WithBlobLimit(opts.conf.Defaults.BlobLimit)))
}
if !rootOpts.conf.Defaults.SkipDockerConf {
if !opts.conf.Defaults.SkipDockerConf {
rcOpts = append(rcOpts, regclient.WithDockerCreds(), regclient.WithDockerCerts())
}
if rootOpts.conf.Defaults.UserAgent != "" {
rcOpts = append(rcOpts, regclient.WithUserAgent(rootOpts.conf.Defaults.UserAgent))
if opts.conf.Defaults.UserAgent != "" {
rcOpts = append(rcOpts, regclient.WithUserAgent(opts.conf.Defaults.UserAgent))
} else {
info := version.GetInfo()
if info.VCSTag != "" {
@ -268,9 +274,9 @@ func (rootOpts *rootCmd) loadConf() error {
}
}
rcHosts := []config.Host{}
for _, host := range rootOpts.conf.Creds {
for _, host := range opts.conf.Creds {
if host.Scheme != "" {
rootOpts.log.Warn("Scheme is deprecated, for http set TLS to disabled",
opts.log.Warn("Scheme is deprecated, for http set TLS to disabled",
slog.String("name", host.Name))
}
rcHosts = append(rcHosts, host)
@ -278,13 +284,13 @@ func (rootOpts *rootCmd) loadConf() error {
if len(rcHosts) > 0 {
rcOpts = append(rcOpts, regclient.WithConfigHost(rcHosts...))
}
rootOpts.rc = regclient.New(rcOpts...)
opts.rc = regclient.New(rcOpts...)
return nil
}
// process a sync step
func (rootOpts *rootCmd) process(ctx context.Context, s ConfigScript) error {
rootOpts.log.Debug("Starting script",
func (opts *rootOpts) process(ctx context.Context, s ConfigScript) error {
opts.log.Debug("Starting script",
slog.String("script", s.Name))
// add a timeout to the context
if s.Timeout > 0 {
@ -294,23 +300,23 @@ func (rootOpts *rootCmd) process(ctx context.Context, s ConfigScript) error {
}
sbOpts := []sandbox.Opt{
sandbox.WithContext(ctx),
sandbox.WithRegClient(rootOpts.rc),
sandbox.WithSlog(rootOpts.log),
sandbox.WithThrottle(rootOpts.throttle),
sandbox.WithRegClient(opts.rc),
sandbox.WithSlog(opts.log),
sandbox.WithThrottle(opts.throttle),
}
if rootOpts.dryRun {
if opts.dryRun {
sbOpts = append(sbOpts, sandbox.WithDryRun())
}
sb := sandbox.New(s.Name, sbOpts...)
defer sb.Close()
err := sb.RunScript(s.Script)
if err != nil {
rootOpts.log.Warn("Error running script",
opts.log.Warn("Error running script",
slog.String("script", s.Name),
slog.String("error", err.Error()))
return fmt.Errorf("%w%.0w", err, ErrScriptFailed)
}
rootOpts.log.Debug("Finished script",
opts.log.Debug("Finished script",
slog.String("script", s.Name))
return nil
}

View File

@ -57,8 +57,8 @@ var configKnownTypes = []string{
"application/vnd.sylabs.sif.config.v1+json",
}
type artifactCmd struct {
rootOpts *rootCmd
type artifactOpts struct {
rootOpts *rootOpts
annotations []string
artifactMT string
artifactType string
@ -72,9 +72,7 @@ type artifactCmd struct {
externalRepo string
filterAT string
filterAnnot []string
formatList string
formatPut string
formatTree string
format string
getConfig bool
index bool
latest bool
@ -87,16 +85,23 @@ type artifactCmd struct {
subject string
}
func NewArtifactCmd(rootOpts *rootCmd) *cobra.Command {
artifactOpts := artifactCmd{
rootOpts: rootOpts,
}
var artifactTopCmd = &cobra.Command{
func NewArtifactCmd(rOpts *rootOpts) *cobra.Command {
cmd := &cobra.Command{
Use: "artifact <cmd>",
Short: "manage artifacts",
}
var artifactGetCmd = &cobra.Command{
cmd.AddCommand(newArtifactGetCmd(rOpts))
cmd.AddCommand(newArtifactListCmd(rOpts))
cmd.AddCommand(newArtifactPutCmd(rOpts))
cmd.AddCommand(newArtifactTreeCmd(rOpts))
return cmd
}
func newArtifactGetCmd(rOpts *rootOpts) *cobra.Command {
opts := artifactOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "get <reference>",
Aliases: []string{"pull"},
Short: "download artifacts",
@ -115,9 +120,36 @@ regctl artifact get \
regctl artifact get registry.example.org/artifact:0.0.1 --config`,
Args: cobra.RangeArgs(0, 1),
ValidArgs: []string{}, // do not auto complete repository/tag
RunE: artifactOpts.runArtifactGet,
RunE: opts.runArtifactGet,
}
var artifactListCmd = &cobra.Command{
cmd.Flags().BoolVar(&opts.getConfig, "config", false, "Show the config, overrides file options")
cmd.Flags().StringVar(&opts.artifactConfig, "config-file", "", "Output config to a file")
cmd.Flags().StringVar(&opts.externalRepo, "external", "", "Query referrers from a separate source")
cmd.Flags().StringArrayVarP(&opts.artifactFile, "file", "f", []string{}, "Filter by artifact filename")
cmd.Flags().StringArrayVarP(&opts.artifactFileMT, "file-media-type", "m", []string{}, "Filter by artifact media-type")
_ = cmd.RegisterFlagCompletionFunc("file-media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return artifactFileKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().StringArrayVar(&opts.filterAnnot, "filter-annotation", []string{}, "Filter referrers by annotation (key=value)")
cmd.Flags().StringVar(&opts.filterAT, "filter-artifact-type", "", "Filter referrers by artifactType")
cmd.Flags().BoolVar(&opts.latest, "latest", false, "Get the most recent referrer using the OCI created annotation")
cmd.Flags().StringVarP(&opts.outputDir, "output", "o", "", "Output directory for multiple artifacts")
cmd.Flags().StringVarP(&opts.platform, "platform", "p", "", "Specify platform of a subject (e.g. linux/amd64 or local)")
_ = cmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
cmd.Flags().StringVar(&opts.refers, "refers", "", "Deprecated: Get a referrer to the reference")
_ = cmd.Flags().MarkHidden("refers")
cmd.Flags().StringVar(&opts.sortAnnot, "sort-annotation", "", "Annotation used for sorting results")
cmd.Flags().BoolVar(&opts.sortDesc, "sort-desc", false, "Sort in descending order")
cmd.Flags().StringVar(&opts.subject, "subject", "", "Get a referrer to the subject reference")
cmd.Flags().BoolVar(&opts.stripDirs, "strip-dirs", false, "Strip directories from filenames in output dir")
return cmd
}
func newArtifactListCmd(rOpts *rootOpts) *cobra.Command {
opts := artifactOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "list <reference>",
Aliases: []string{"ls"},
Short: "list artifacts that have a subject to the given reference",
@ -133,11 +165,29 @@ regctl artifact list registry.example.com/repo:v1 --format body
regctl artifact list registry.example.com/repo:v1 --format '{{jsonPretty .Manifest}}'`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{}, // do not auto complete repository/tag
RunE: artifactOpts.runArtifactList,
RunE: opts.runArtifactList,
}
var artifactPutCmd = &cobra.Command{
cmd.Flags().BoolVar(&opts.digestTags, "digest-tags", false, "Include digest tags")
cmd.Flags().StringVar(&opts.externalRepo, "external", "", "Query referrers from a separate source")
cmd.Flags().StringVar(&opts.filterAT, "filter-artifact-type", "", "Filter descriptors by artifactType")
cmd.Flags().StringArrayVar(&opts.filterAnnot, "filter-annotation", []string{}, "Filter descriptors by annotation (key=value)")
cmd.Flags().StringVar(&opts.format, "format", "{{printPretty .}}", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
cmd.Flags().BoolVar(&opts.latest, "latest", false, "Sort using the OCI created annotation")
cmd.Flags().StringVarP(&opts.platform, "platform", "p", "", "Specify platform (e.g. linux/amd64 or local)")
_ = cmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
cmd.Flags().StringVar(&opts.sortAnnot, "sort-annotation", "", "Annotation used for sorting results")
cmd.Flags().BoolVar(&opts.sortDesc, "sort-desc", false, "Sort in descending order")
return cmd
}
func newArtifactPutCmd(rOpts *rootOpts) *cobra.Command {
opts := artifactOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "put <reference>",
Aliases: []string{"push"},
Aliases: []string{"create", "push"},
Short: "upload artifacts",
Long: `Upload artifacts to the registry.`,
Example: `
@ -161,9 +211,46 @@ regctl artifact put \
< spdx.json`,
Args: cobra.RangeArgs(0, 1),
ValidArgs: []string{}, // do not auto complete repository/tag
RunE: artifactOpts.runArtifactPut,
RunE: opts.runArtifactPut,
}
var artifactTreeCmd = &cobra.Command{
cmd.Flags().StringArrayVar(&opts.annotations, "annotation", []string{}, "Annotation to include on manifest")
cmd.Flags().StringVar(&opts.artifactType, "artifact-type", "", "Artifact type (recommended)")
_ = cmd.RegisterFlagCompletionFunc("artifact-type", completeArgNone)
cmd.Flags().BoolVar(&opts.byDigest, "by-digest", false, "Push manifest by digest instead of tag")
cmd.Flags().StringVar(&opts.artifactConfig, "config-file", "", "Filename for config content")
cmd.Flags().StringVar(&opts.artifactConfigMT, "config-type", "", "Config mediaType")
_ = cmd.RegisterFlagCompletionFunc("config-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return configKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().StringVar(&opts.externalRepo, "external", "", "Push referrers to a separate repository")
cmd.Flags().StringArrayVarP(&opts.artifactFile, "file", "f", []string{}, "Artifact filename")
cmd.Flags().StringArrayVarP(&opts.artifactFileMT, "file-media-type", "m", []string{}, "Set the mediaType for the individual files")
_ = cmd.RegisterFlagCompletionFunc("file-media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return artifactFileKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().BoolVar(&opts.artifactTitle, "file-title", false, "Include a title annotation with the filename")
cmd.Flags().StringVarP(&opts.artifactMT, "media-type", "", mediatype.OCI1Manifest, "EXPERIMENTAL: Manifest media-type")
_ = cmd.RegisterFlagCompletionFunc("media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return manifestKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
_ = cmd.Flags().MarkHidden("media-type")
cmd.Flags().StringVar(&opts.format, "format", "", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
cmd.Flags().BoolVar(&opts.index, "index", false, "Create/append artifact to an index")
cmd.Flags().StringVarP(&opts.platform, "platform", "p", "", "Specify platform of a subject (e.g. linux/amd64 or local)")
_ = cmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
cmd.Flags().StringVar(&opts.refers, "refers", "", "EXPERIMENTAL: Set a referrer to the reference")
_ = cmd.Flags().MarkHidden("refers")
cmd.Flags().BoolVar(&opts.stripDirs, "strip-dirs", false, "Strip directories from filenames in file-title")
cmd.Flags().StringVar(&opts.subject, "subject", "", "Set the subject to a reference (used for referrer queries)")
return cmd
}
func newArtifactTreeCmd(rOpts *rootOpts) *cobra.Command {
opts := artifactOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "tree <reference>",
Aliases: []string{},
Short: "tree listing of artifacts",
@ -178,109 +265,42 @@ regctl artifact tree ghcr.io/regclient/regsync:latest
regctl artifact tree --digest-tags ghcr.io/regclient/regsync:latest`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{}, // do not auto complete repository/tag
RunE: artifactOpts.runArtifactTree,
RunE: opts.runArtifactTree,
}
artifactGetCmd.Flags().StringVar(&artifactOpts.subject, "subject", "", "Get a referrer to the subject reference")
artifactGetCmd.Flags().StringVar(&artifactOpts.externalRepo, "external", "", "Query referrers from a separate source")
artifactGetCmd.Flags().StringVarP(&artifactOpts.platform, "platform", "p", "", "Specify platform of a subject (e.g. linux/amd64 or local)")
_ = artifactGetCmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
artifactGetCmd.Flags().StringVar(&artifactOpts.filterAT, "filter-artifact-type", "", "Filter referrers by artifactType")
artifactGetCmd.Flags().StringArrayVar(&artifactOpts.filterAnnot, "filter-annotation", []string{}, "Filter referrers by annotation (key=value)")
artifactGetCmd.Flags().BoolVar(&artifactOpts.getConfig, "config", false, "Show the config, overrides file options")
artifactGetCmd.Flags().StringVar(&artifactOpts.artifactConfig, "config-file", "", "Output config to a file")
artifactGetCmd.Flags().StringArrayVarP(&artifactOpts.artifactFile, "file", "f", []string{}, "Filter by artifact filename")
artifactGetCmd.Flags().StringArrayVarP(&artifactOpts.artifactFileMT, "file-media-type", "m", []string{}, "Filter by artifact media-type")
_ = artifactGetCmd.RegisterFlagCompletionFunc("file-media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return artifactFileKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
artifactGetCmd.Flags().BoolVar(&artifactOpts.latest, "latest", false, "Get the most recent referrer using the OCI created annotation")
artifactGetCmd.Flags().StringVarP(&artifactOpts.outputDir, "output", "o", "", "Output directory for multiple artifacts")
artifactGetCmd.Flags().BoolVar(&artifactOpts.stripDirs, "strip-dirs", false, "Strip directories from filenames in output dir")
artifactGetCmd.Flags().StringVar(&artifactOpts.refers, "refers", "", "Deprecated: Get a referrer to the reference")
_ = artifactGetCmd.Flags().MarkHidden("refers")
artifactGetCmd.Flags().StringVar(&artifactOpts.sortAnnot, "sort-annotation", "", "Annotation used for sorting results")
artifactGetCmd.Flags().BoolVar(&artifactOpts.sortDesc, "sort-desc", false, "Sort in descending order")
artifactListCmd.Flags().BoolVar(&artifactOpts.digestTags, "digest-tags", false, "Include digest tags")
artifactListCmd.Flags().StringVar(&artifactOpts.externalRepo, "external", "", "Query referrers from a separate source")
artifactListCmd.Flags().StringVar(&artifactOpts.filterAT, "filter-artifact-type", "", "Filter descriptors by artifactType")
artifactListCmd.Flags().StringArrayVar(&artifactOpts.filterAnnot, "filter-annotation", []string{}, "Filter descriptors by annotation (key=value)")
artifactListCmd.Flags().StringVar(&artifactOpts.formatList, "format", "{{printPretty .}}", "Format output with go template syntax")
artifactListCmd.Flags().BoolVar(&artifactOpts.latest, "latest", false, "Sort using the OCI created annotation")
artifactListCmd.Flags().StringVarP(&artifactOpts.platform, "platform", "p", "", "Specify platform (e.g. linux/amd64 or local)")
_ = artifactListCmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
artifactListCmd.Flags().StringVar(&artifactOpts.sortAnnot, "sort-annotation", "", "Annotation used for sorting results")
artifactListCmd.Flags().BoolVar(&artifactOpts.sortDesc, "sort-desc", false, "Sort in descending order")
artifactPutCmd.Flags().StringVarP(&artifactOpts.artifactMT, "media-type", "", mediatype.OCI1Manifest, "EXPERIMENTAL: Manifest media-type")
_ = artifactPutCmd.RegisterFlagCompletionFunc("media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return manifestKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
_ = artifactPutCmd.Flags().MarkHidden("media-type")
artifactPutCmd.Flags().StringVar(&artifactOpts.artifactType, "artifact-type", "", "Artifact type (recommended)")
_ = artifactPutCmd.RegisterFlagCompletionFunc("artifact-type", completeArgNone)
artifactPutCmd.Flags().StringVar(&artifactOpts.artifactConfig, "config-file", "", "Filename for config content")
artifactPutCmd.Flags().StringVar(&artifactOpts.artifactConfigMT, "config-type", "", "Config mediaType")
_ = artifactPutCmd.RegisterFlagCompletionFunc("config-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return configKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
artifactPutCmd.Flags().StringVar(&artifactOpts.externalRepo, "external", "", "Push referrers to a separate repository")
artifactPutCmd.Flags().StringArrayVarP(&artifactOpts.artifactFile, "file", "f", []string{}, "Artifact filename")
artifactPutCmd.Flags().StringArrayVarP(&artifactOpts.artifactFileMT, "file-media-type", "m", []string{}, "Set the mediaType for the individual files")
_ = artifactPutCmd.RegisterFlagCompletionFunc("file-media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return artifactFileKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
artifactPutCmd.Flags().BoolVar(&artifactOpts.artifactTitle, "file-title", false, "Include a title annotation with the filename")
artifactPutCmd.Flags().StringArrayVar(&artifactOpts.annotations, "annotation", []string{}, "Annotation to include on manifest")
artifactPutCmd.Flags().BoolVar(&artifactOpts.byDigest, "by-digest", false, "Push manifest by digest instead of tag")
artifactPutCmd.Flags().StringVar(&artifactOpts.formatPut, "format", "", "Format output with go template syntax")
artifactPutCmd.Flags().BoolVar(&artifactOpts.index, "index", false, "Create/append artifact to an index")
artifactPutCmd.Flags().StringVar(&artifactOpts.subject, "subject", "", "Set the subject to a reference (used for referrer queries)")
artifactPutCmd.Flags().BoolVar(&artifactOpts.stripDirs, "strip-dirs", false, "Strip directories from filenames in file-title")
artifactPutCmd.Flags().StringVarP(&artifactOpts.platform, "platform", "p", "", "Specify platform of a subject (e.g. linux/amd64 or local)")
_ = artifactPutCmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
artifactPutCmd.Flags().StringVar(&artifactOpts.refers, "refers", "", "EXPERIMENTAL: Set a referrer to the reference")
_ = artifactPutCmd.Flags().MarkHidden("refers")
artifactTreeCmd.Flags().BoolVar(&artifactOpts.digestTags, "digest-tags", false, "Include digest tags")
artifactTreeCmd.Flags().StringVar(&artifactOpts.externalRepo, "external", "", "Query referrers from a separate source")
artifactTreeCmd.Flags().StringVar(&artifactOpts.filterAT, "filter-artifact-type", "", "Filter descriptors by artifactType")
artifactTreeCmd.Flags().StringArrayVar(&artifactOpts.filterAnnot, "filter-annotation", []string{}, "Filter descriptors by annotation (key=value)")
artifactTreeCmd.Flags().StringVar(&artifactOpts.formatTree, "format", "{{printPretty .}}", "Format output with go template syntax")
artifactTopCmd.AddCommand(artifactGetCmd)
artifactTopCmd.AddCommand(artifactListCmd)
artifactTopCmd.AddCommand(artifactPutCmd)
artifactTopCmd.AddCommand(artifactTreeCmd)
return artifactTopCmd
cmd.Flags().BoolVar(&opts.digestTags, "digest-tags", false, "Include digest tags")
cmd.Flags().StringVar(&opts.externalRepo, "external", "", "Query referrers from a separate source")
cmd.Flags().StringVar(&opts.filterAT, "filter-artifact-type", "", "Filter descriptors by artifactType")
cmd.Flags().StringArrayVar(&opts.filterAnnot, "filter-annotation", []string{}, "Filter descriptors by annotation (key=value)")
cmd.Flags().StringVar(&opts.format, "format", "{{printPretty .}}", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
return cmd
}
func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []string) error {
func (opts *artifactOpts) runArtifactGet(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
rc := artifactOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
// validate inputs
if artifactOpts.refers != "" {
artifactOpts.rootOpts.log.Warn("--refers is deprecated, use --subject instead")
if artifactOpts.subject == "" {
artifactOpts.subject = artifactOpts.refers
if opts.refers != "" {
opts.rootOpts.log.Warn("--refers is deprecated, use --subject instead")
if opts.subject == "" {
opts.subject = opts.refers
}
}
if artifactOpts.externalRepo != "" && artifactOpts.subject == "" {
artifactOpts.rootOpts.log.Warn("--external option depends on --subject")
if opts.externalRepo != "" && opts.subject == "" {
opts.rootOpts.log.Warn("--external option depends on --subject")
}
if artifactOpts.latest && artifactOpts.sortAnnot != "" {
if opts.latest && opts.sortAnnot != "" {
return fmt.Errorf("--latest cannot be used with --sort-annotation")
}
// if output dir defined, ensure it exists
if artifactOpts.outputDir != "" {
fi, err := os.Stat(artifactOpts.outputDir)
if opts.outputDir != "" {
fi, err := os.Stat(opts.outputDir)
if err != nil {
return fmt.Errorf("output directory unavailable: %w", err)
}
if !fi.IsDir() {
return fmt.Errorf("output must be a directory: \"%s\"", artifactOpts.outputDir)
return fmt.Errorf("output must be a directory: \"%s\"", opts.outputDir)
}
}
// dedup warnings
@ -290,13 +310,13 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
r := ref.Ref{}
matchOpts := descriptor.MatchOpt{
ArtifactType: artifactOpts.filterAT,
SortAnnotation: artifactOpts.sortAnnot,
SortDesc: artifactOpts.sortDesc,
ArtifactType: opts.filterAT,
SortAnnotation: opts.sortAnnot,
SortDesc: opts.sortDesc,
}
if artifactOpts.filterAnnot != nil {
if opts.filterAnnot != nil {
matchOpts.Annotations = map[string]string{}
for _, kv := range artifactOpts.filterAnnot {
for _, kv := range opts.filterAnnot {
kvSplit := strings.SplitN(kv, "=", 2)
if len(kvSplit) == 2 {
matchOpts.Annotations[kvSplit[0]] = kvSplit[1]
@ -305,12 +325,12 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
}
}
}
if artifactOpts.latest {
if opts.latest {
matchOpts.SortAnnotation = types.AnnotationCreated
matchOpts.SortDesc = true
}
if artifactOpts.platform != "" {
p, err := platform.Parse(artifactOpts.platform)
if opts.platform != "" {
p, err := platform.Parse(opts.platform)
if err != nil {
return fmt.Errorf("platform could not be parsed: %w", err)
}
@ -318,8 +338,8 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
}
// lookup referrers to the subject
if len(args) == 0 && artifactOpts.subject != "" {
rSubject, err := ref.New(artifactOpts.subject)
if len(args) == 0 && opts.subject != "" {
rSubject, err := ref.New(opts.subject)
if err != nil {
return err
}
@ -328,11 +348,11 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
referrerOpts := []scheme.ReferrerOpts{
scheme.WithReferrerMatchOpt(referrerMatchOpts),
}
if artifactOpts.platform != "" {
referrerOpts = append(referrerOpts, scheme.WithReferrerPlatform(artifactOpts.platform))
if opts.platform != "" {
referrerOpts = append(referrerOpts, scheme.WithReferrerPlatform(opts.platform))
}
if artifactOpts.externalRepo != "" {
rExt, err := ref.New(artifactOpts.externalRepo)
if opts.externalRepo != "" {
rExt, err := ref.New(opts.externalRepo)
if err != nil {
return fmt.Errorf("failed to parse external ref: %w", err)
}
@ -346,11 +366,11 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
return err
}
if len(rl.Descriptors) == 0 {
return fmt.Errorf("no matching referrers to %s", artifactOpts.subject)
} else if len(rl.Descriptors) > 1 && artifactOpts.sortAnnot == "" && !artifactOpts.latest {
artifactOpts.rootOpts.log.Warn("multiple referrers match, using first match",
return fmt.Errorf("no matching referrers to %s", opts.subject)
} else if len(rl.Descriptors) > 1 && opts.sortAnnot == "" && !opts.latest {
opts.rootOpts.log.Warn("multiple referrers match, using first match",
slog.Int("match count", len(rl.Descriptors)),
slog.String("subject", artifactOpts.subject))
slog.String("subject", opts.subject))
}
r = r.SetDigest(rl.Descriptors[0].Digest.String())
} else if len(args) > 0 {
@ -394,7 +414,7 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
}
// if config-file defined, create file as writer, perform a blob get
if artifactOpts.artifactConfig != "" || artifactOpts.getConfig {
if opts.artifactConfig != "" || opts.getConfig {
d, err := mi.GetConfig()
if err != nil {
return err
@ -404,8 +424,8 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
return err
}
defer rdr.Close()
if artifactOpts.artifactConfig != "" {
fh, err := os.Create(artifactOpts.artifactConfig)
if opts.artifactConfig != "" {
fh, err := os.Create(opts.artifactConfig)
if err != nil {
return err
}
@ -420,7 +440,7 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
return err
}
}
if artifactOpts.getConfig {
if opts.getConfig {
// do not return layer contents if request is only for a config
return nil
}
@ -432,18 +452,18 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
return err
}
// filter by media-type if defined
if len(artifactOpts.artifactFileMT) > 0 {
if len(opts.artifactFileMT) > 0 {
for i := len(layers) - 1; i >= 0; i-- {
if !slices.Contains(artifactOpts.artifactFileMT, layers[i].MediaType) {
if !slices.Contains(opts.artifactFileMT, layers[i].MediaType) {
layers = slices.Delete(layers, i, i+1)
}
}
}
// filter by filename if defined
if len(artifactOpts.artifactFile) > 0 {
if len(opts.artifactFile) > 0 {
for i := len(layers) - 1; i >= 0; i-- {
af, ok := layers[i].Annotations[ociAnnotTitle]
if !ok || !slices.Contains(artifactOpts.artifactFile, af) {
if !ok || !slices.Contains(opts.artifactFile, af) {
layers = slices.Delete(layers, i, i+1)
}
}
@ -453,7 +473,7 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
return fmt.Errorf("no matching layers found in the artifact, verify media-type and filename%.0w", errs.ErrNotFound)
}
if artifactOpts.outputDir != "" {
if opts.outputDir != "" {
// loop through each matching layer
for _, l := range layers {
if err = l.Digest.Validate(); err != nil {
@ -476,7 +496,7 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
if strings.HasSuffix(l.Annotations[ociAnnotTitle], "/") || l.Annotations["io.deis.oras.content.unpack"] == "true" {
f = f + "/"
}
if artifactOpts.stripDirs {
if opts.stripDirs {
f = f[strings.LastIndex(f, "/"):]
}
dirs := strings.Split(f, "/")
@ -484,7 +504,7 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
if len(dirs) > 2 {
// strip the leading empty dir and trailing filename
dirs = dirs[1 : len(dirs)-1]
dest := filepath.Join(artifactOpts.outputDir, filepath.Join(dirs...))
dest := filepath.Join(opts.outputDir, filepath.Join(dirs...))
fi, err := os.Stat(dest)
if os.IsNotExist(err) {
//#nosec G301 defer to user umask setting, simplifies container scenarios, registry content is often public
@ -500,13 +520,13 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
}
// if there's a trailing slash, expand the compressed blob into the folder
if strings.HasSuffix(f, "/") {
err = archive.Extract(ctx, filepath.Join(artifactOpts.outputDir, f), rdr)
err = archive.Extract(ctx, filepath.Join(opts.outputDir, f), rdr)
if err != nil {
return err
}
} else {
// create file as writer
out := filepath.Join(artifactOpts.outputDir, f)
out := filepath.Join(opts.outputDir, f)
//#nosec G304 command is run by a user accessing their own files
fh, err := os.Create(out)
if err != nil {
@ -545,7 +565,7 @@ func (artifactOpts *artifactCmd) runArtifactGet(cmd *cobra.Command, args []strin
return nil
}
func (artifactOpts *artifactCmd) runArtifactList(cmd *cobra.Command, args []string) error {
func (opts *artifactOpts) runArtifactList(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// validate inputs
@ -553,7 +573,7 @@ func (artifactOpts *artifactCmd) runArtifactList(cmd *cobra.Command, args []stri
if err != nil {
return err
}
if artifactOpts.latest && artifactOpts.sortAnnot != "" {
if opts.latest && opts.sortAnnot != "" {
return fmt.Errorf("--latest cannot be used with --sort-annotation")
}
// dedup warnings
@ -561,17 +581,17 @@ func (artifactOpts *artifactCmd) runArtifactList(cmd *cobra.Command, args []stri
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
}
rc := artifactOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, rSubject)
matchOpts := descriptor.MatchOpt{
ArtifactType: artifactOpts.filterAT,
SortAnnotation: artifactOpts.sortAnnot,
SortDesc: artifactOpts.sortDesc,
ArtifactType: opts.filterAT,
SortAnnotation: opts.sortAnnot,
SortDesc: opts.sortDesc,
}
if artifactOpts.filterAnnot != nil {
if opts.filterAnnot != nil {
matchOpts.Annotations = map[string]string{}
for _, kv := range artifactOpts.filterAnnot {
for _, kv := range opts.filterAnnot {
kvSplit := strings.SplitN(kv, "=", 2)
if len(kvSplit) == 2 {
matchOpts.Annotations[kvSplit[0]] = kvSplit[1]
@ -580,18 +600,18 @@ func (artifactOpts *artifactCmd) runArtifactList(cmd *cobra.Command, args []stri
}
}
}
if artifactOpts.latest {
if opts.latest {
matchOpts.SortAnnotation = types.AnnotationCreated
matchOpts.SortDesc = true
}
referrerOpts := []scheme.ReferrerOpts{
scheme.WithReferrerMatchOpt(matchOpts),
}
if artifactOpts.platform != "" {
referrerOpts = append(referrerOpts, scheme.WithReferrerPlatform(artifactOpts.platform))
if opts.platform != "" {
referrerOpts = append(referrerOpts, scheme.WithReferrerPlatform(opts.platform))
}
if artifactOpts.externalRepo != "" {
rExternal, err := ref.New(artifactOpts.externalRepo)
if opts.externalRepo != "" {
rExternal, err := ref.New(opts.externalRepo)
if err != nil {
return fmt.Errorf("failed to parse external ref: %w", err)
}
@ -604,7 +624,7 @@ func (artifactOpts *artifactCmd) runArtifactList(cmd *cobra.Command, args []stri
}
// include digest tags if requested
if artifactOpts.digestTags {
if opts.digestTags {
tl, err := rc.TagList(ctx, rSubject)
if err != nil {
return fmt.Errorf("failed to list tags: %w", err)
@ -638,31 +658,31 @@ func (artifactOpts *artifactCmd) runArtifactList(cmd *cobra.Command, args []stri
}
}
switch artifactOpts.formatList {
switch opts.format {
case "raw":
artifactOpts.formatList = "{{ range $key,$vals := .Manifest.RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .Manifest.RawBody}}"
opts.format = "{{ range $key,$vals := .Manifest.RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .Manifest.RawBody}}"
case "rawBody", "raw-body", "body":
artifactOpts.formatList = "{{printf \"%s\" .Manifest.RawBody}}"
opts.format = "{{printf \"%s\" .Manifest.RawBody}}"
case "rawHeaders", "raw-headers", "headers":
artifactOpts.formatList = "{{ range $key,$vals := .Manifest.RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
opts.format = "{{ range $key,$vals := .Manifest.RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
}
return template.Writer(cmd.OutOrStdout(), artifactOpts.formatList, rl)
return template.Writer(cmd.OutOrStdout(), opts.format, rl)
}
func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []string) error {
func (opts *artifactOpts) runArtifactPut(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
hasConfig := false
var r, rArt, rSubject ref.Ref
var err error
switch artifactOpts.artifactMT {
switch opts.artifactMT {
case mediatype.OCI1Artifact:
artifactOpts.rootOpts.log.Warn("changing media-type is experimental and non-portable")
opts.rootOpts.log.Warn("changing media-type is experimental and non-portable")
hasConfig = false
case "", mediatype.OCI1Manifest:
hasConfig = true
default:
return fmt.Errorf("unsupported manifest media type: %s%.0w", artifactOpts.artifactMT, errs.ErrUnsupportedMediaType)
return fmt.Errorf("unsupported manifest media type: %s%.0w", opts.artifactMT, errs.ErrUnsupportedMediaType)
}
// dedup warnings
@ -671,17 +691,17 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
}
// validate inputs
if artifactOpts.refers != "" {
artifactOpts.rootOpts.log.Warn("--refers is deprecated, use --subject instead")
if artifactOpts.subject == "" {
artifactOpts.subject = artifactOpts.refers
if opts.refers != "" {
opts.rootOpts.log.Warn("--refers is deprecated, use --subject instead")
if opts.subject == "" {
opts.subject = opts.refers
}
}
if len(args) == 0 && artifactOpts.subject == "" {
if len(args) == 0 && opts.subject == "" {
return fmt.Errorf("either a reference or subject must be provided")
}
if artifactOpts.subject != "" {
rSubject, err = ref.New(artifactOpts.subject)
if opts.subject != "" {
rSubject, err = ref.New(opts.subject)
if err != nil {
return err
}
@ -694,11 +714,11 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
}
r = rArt
}
if artifactOpts.externalRepo != "" {
if opts.externalRepo != "" {
if rSubject.IsZero() {
return fmt.Errorf("pushing a referrer to an external repository requires a subject%.0w", errs.ErrUnsupported)
}
rExt, err := ref.New(artifactOpts.externalRepo)
rExt, err := ref.New(opts.externalRepo)
if err != nil {
return err
}
@ -714,56 +734,56 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
}
// validate/set artifactType and config.mediaType
if artifactOpts.artifactConfigMT != "" && !mediatype.Valid(artifactOpts.artifactConfigMT) {
return fmt.Errorf("invalid media type: %s%.0w", artifactOpts.artifactConfigMT, errs.ErrUnsupportedMediaType)
if opts.artifactConfigMT != "" && !mediatype.Valid(opts.artifactConfigMT) {
return fmt.Errorf("invalid media type: %s%.0w", opts.artifactConfigMT, errs.ErrUnsupportedMediaType)
}
if artifactOpts.artifactType != "" && !mediatype.Valid(artifactOpts.artifactType) {
return fmt.Errorf("invalid media type: %s%.0w", artifactOpts.artifactType, errs.ErrUnsupportedMediaType)
if opts.artifactType != "" && !mediatype.Valid(opts.artifactType) {
return fmt.Errorf("invalid media type: %s%.0w", opts.artifactType, errs.ErrUnsupportedMediaType)
}
for _, mt := range artifactOpts.artifactFileMT {
for _, mt := range opts.artifactFileMT {
if !mediatype.Valid(mt) {
return fmt.Errorf("invalid media type: %s%.0w", mt, errs.ErrUnsupportedMediaType)
}
}
if hasConfig && artifactOpts.artifactConfigMT == "" {
if artifactOpts.artifactConfig == "" {
artifactOpts.artifactConfigMT = mediatype.OCI1Empty
if hasConfig && opts.artifactConfigMT == "" {
if opts.artifactConfig == "" {
opts.artifactConfigMT = mediatype.OCI1Empty
} else {
if artifactOpts.artifactType != "" {
artifactOpts.artifactConfigMT = artifactOpts.artifactType
artifactOpts.rootOpts.log.Warn("setting config-type using artifact-type")
if opts.artifactType != "" {
opts.artifactConfigMT = opts.artifactType
opts.rootOpts.log.Warn("setting config-type using artifact-type")
} else {
return fmt.Errorf("config-type is required for config-file")
}
}
}
if !hasConfig && (artifactOpts.artifactConfig != "" || artifactOpts.artifactConfigMT != "") {
return fmt.Errorf("cannot set config-type or config-file on %s%.0w", artifactOpts.artifactMT, errs.ErrUnsupportedMediaType)
if !hasConfig && (opts.artifactConfig != "" || opts.artifactConfigMT != "") {
return fmt.Errorf("cannot set config-type or config-file on %s%.0w", opts.artifactMT, errs.ErrUnsupportedMediaType)
}
if artifactOpts.artifactType == "" {
if !hasConfig || artifactOpts.artifactConfigMT == mediatype.OCI1Empty {
artifactOpts.rootOpts.log.Warn("using default value for artifact-type is not recommended")
artifactOpts.artifactType = defaultMTArtifact
if opts.artifactType == "" {
if !hasConfig || opts.artifactConfigMT == mediatype.OCI1Empty {
opts.rootOpts.log.Warn("using default value for artifact-type is not recommended")
opts.artifactType = defaultMTArtifact
}
}
// set and validate artifact files with media types
if len(artifactOpts.artifactFile) <= 1 && len(artifactOpts.artifactFileMT) == 0 && artifactOpts.artifactType != "" && artifactOpts.artifactType != defaultMTArtifact {
if len(opts.artifactFile) <= 1 && len(opts.artifactFileMT) == 0 && opts.artifactType != "" && opts.artifactType != defaultMTArtifact {
// special case for single file and artifact-type
artifactOpts.artifactFileMT = []string{artifactOpts.artifactType}
} else if len(artifactOpts.artifactFile) == 1 && len(artifactOpts.artifactFileMT) == 0 {
opts.artifactFileMT = []string{opts.artifactType}
} else if len(opts.artifactFile) == 1 && len(opts.artifactFileMT) == 0 {
// default media-type for a single file, same is used for stdin
artifactOpts.artifactFileMT = []string{defaultMTLayer}
} else if len(artifactOpts.artifactFile) == 0 && len(artifactOpts.artifactFileMT) == 1 {
opts.artifactFileMT = []string{defaultMTLayer}
} else if len(opts.artifactFile) == 0 && len(opts.artifactFileMT) == 1 {
// no-op, special case for stdin with a media type
} else if len(artifactOpts.artifactFile) != len(artifactOpts.artifactFileMT) {
} else if len(opts.artifactFile) != len(opts.artifactFileMT) {
// all other mis-matches are invalid
return fmt.Errorf("one artifact media-type must be set for each artifact file")
}
// include annotations
annotations := map[string]string{}
for _, a := range artifactOpts.annotations {
for _, a := range opts.annotations {
aSplit := strings.SplitN(a, "=", 2)
if len(aSplit) == 1 {
annotations[aSplit[0]] = ""
@ -773,16 +793,16 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
}
// setup regclient
rc := artifactOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
var subjectDesc *descriptor.Descriptor
if rSubject.IsSet() {
mOpts := []regclient.ManifestOpts{regclient.WithManifestRequireDigest()}
if artifactOpts.platform != "" {
p, err := platform.Parse(artifactOpts.platform)
if opts.platform != "" {
p, err := platform.Parse(opts.platform)
if err != nil {
return fmt.Errorf("failed to parse platform %s: %w", artifactOpts.platform, err)
return fmt.Errorf("failed to parse platform %s: %w", opts.platform, err)
}
mOpts = append(mOpts, regclient.WithManifestPlatform(p))
}
@ -799,12 +819,12 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
if hasConfig {
var configBytes []byte
var configDigest digest.Digest
if artifactOpts.artifactConfig == "" {
if opts.artifactConfig == "" {
configBytes = descriptor.EmptyData
configDigest = descriptor.EmptyDigest
} else {
var err error
configBytes, err = os.ReadFile(artifactOpts.artifactConfig)
configBytes, err = os.ReadFile(opts.artifactConfig)
if err != nil {
return err
}
@ -817,19 +837,19 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
}
// save config descriptor to manifest
confDesc = descriptor.Descriptor{
MediaType: artifactOpts.artifactConfigMT,
MediaType: opts.artifactConfigMT,
Digest: configDigest,
Size: int64(len(configBytes)),
}
}
blobs := []descriptor.Descriptor{}
if len(artifactOpts.artifactFile) > 0 {
if len(opts.artifactFile) > 0 {
// if files were passed
for i, f := range artifactOpts.artifactFile {
for i, f := range opts.artifactFile {
// wrap in a closure to trigger defer on each step, avoiding open file handles
err = func() error {
mt := artifactOpts.artifactFileMT[i]
mt := opts.artifactFileMT[i]
openF := f
// if file is a directory, compress it into a tgz first
// this unfortunately needs a temp file for the digest
@ -872,9 +892,9 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
desc.Size = l
desc.Digest = digester.Digest()
// add layer to manifest
if artifactOpts.artifactTitle {
if opts.artifactTitle {
af := f
if artifactOpts.stripDirs {
if opts.stripDirs {
fSplit := strings.Split(f, "/")
if fSplit[len(fSplit)-1] != "" {
af = fSplit[len(fSplit)-1]
@ -911,8 +931,8 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
} else {
// no files passed, push from stdin
mt := defaultMTLayer
if len(artifactOpts.artifactFileMT) > 0 {
mt = artifactOpts.artifactFileMT[0]
if len(opts.artifactFileMT) > 0 {
mt = opts.artifactFileMT[0]
}
d, err := rc.BlobPut(ctx, r, descriptor.Descriptor{}, cmd.InOrStdin())
if err != nil {
@ -923,11 +943,11 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
}
mOpts := []manifest.Opts{}
switch artifactOpts.artifactMT {
switch opts.artifactMT {
case mediatype.OCI1Artifact:
m := v1.ArtifactManifest{
MediaType: mediatype.OCI1Artifact,
ArtifactType: artifactOpts.artifactType,
ArtifactType: opts.artifactType,
Blobs: blobs,
Annotations: annotations,
Subject: subjectDesc,
@ -937,7 +957,7 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
m := v1.Manifest{
Versioned: v1.ManifestSchemaVersion,
MediaType: mediatype.OCI1Manifest,
ArtifactType: artifactOpts.artifactType,
ArtifactType: opts.artifactType,
Config: confDesc,
Layers: blobs,
Annotations: annotations,
@ -945,7 +965,7 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
}
mOpts = append(mOpts, manifest.WithOrig(m))
default:
return fmt.Errorf("unsupported manifest media type: %s", artifactOpts.artifactMT)
return fmt.Errorf("unsupported manifest media type: %s", opts.artifactMT)
}
// generate manifest
@ -954,13 +974,13 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
return err
}
if artifactOpts.byDigest || artifactOpts.index || rArt.IsZero() {
if opts.byDigest || opts.index || rArt.IsZero() {
r = r.SetDigest(mm.GetDescriptor().Digest.String())
}
// push manifest
putOpts := []regclient.ManifestOpts{}
if rArt.IsZero() || artifactOpts.index {
if rArt.IsZero() || opts.index {
putOpts = append(putOpts, regclient.WithManifestChild())
}
err = rc.ManifestPut(ctx, r, mm, putOpts...)
@ -969,13 +989,13 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
}
// create/append to index
if artifactOpts.index && rArt.IsSet() {
if opts.index && rArt.IsSet() {
// create a descriptor to add
d := mm.GetDescriptor()
d.ArtifactType = artifactOpts.artifactType
d.ArtifactType = opts.artifactType
d.Annotations = annotations
if artifactOpts.platform != "" {
p, err := platform.Parse(artifactOpts.platform)
if opts.platform != "" {
p, err := platform.Parse(opts.platform)
if err != nil {
return fmt.Errorf("failed to parse platform: %w", err)
}
@ -1024,13 +1044,13 @@ func (artifactOpts *artifactCmd) runArtifactPut(cmd *cobra.Command, args []strin
}{
Manifest: mm,
}
if artifactOpts.byDigest && artifactOpts.formatPut == "" {
artifactOpts.formatPut = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
if opts.byDigest && opts.format == "" {
opts.format = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
}
return template.Writer(cmd.OutOrStdout(), artifactOpts.formatPut, result)
return template.Writer(cmd.OutOrStdout(), opts.format, result)
}
func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []string) error {
func (opts *artifactOpts) runArtifactTree(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// validate inputs
@ -1039,7 +1059,7 @@ func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []stri
return err
}
rc := artifactOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
// dedup warnings
@ -1047,12 +1067,12 @@ func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []stri
ctx = warning.NewContext(ctx, &warning.Warning{Hook: warning.DefaultHook()})
}
referrerOpts := []scheme.ReferrerOpts{}
if artifactOpts.filterAT != "" {
referrerOpts = append(referrerOpts, scheme.WithReferrerMatchOpt(descriptor.MatchOpt{ArtifactType: artifactOpts.filterAT}))
if opts.filterAT != "" {
referrerOpts = append(referrerOpts, scheme.WithReferrerMatchOpt(descriptor.MatchOpt{ArtifactType: opts.filterAT}))
}
if artifactOpts.filterAnnot != nil {
if opts.filterAnnot != nil {
af := map[string]string{}
for _, kv := range artifactOpts.filterAnnot {
for _, kv := range opts.filterAnnot {
kvSplit := strings.SplitN(kv, "=", 2)
if len(kvSplit) == 2 {
af[kvSplit[0]] = kvSplit[1]
@ -1063,8 +1083,8 @@ func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []stri
referrerOpts = append(referrerOpts, scheme.WithReferrerMatchOpt(descriptor.MatchOpt{Annotations: af}))
}
rRefSrc := r
if artifactOpts.externalRepo != "" {
rRefSrc, err = ref.New(artifactOpts.externalRepo)
if opts.externalRepo != "" {
rRefSrc, err = ref.New(opts.externalRepo)
if err != nil {
return fmt.Errorf("failed to parse external ref: %w", err)
}
@ -1073,7 +1093,7 @@ func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []stri
// include digest tags if requested
tags := []string{}
if artifactOpts.digestTags {
if opts.digestTags {
tl, err := rc.TagList(ctx, r)
if err != nil {
return fmt.Errorf("failed to list tags: %w", err)
@ -1082,10 +1102,10 @@ func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []stri
}
seen := []string{}
tr, err := artifactOpts.treeAddResult(ctx, rc, r, seen, referrerOpts, tags)
tr, err := opts.treeAddResult(ctx, rc, r, seen, referrerOpts, tags)
var twErr error
if tr != nil {
twErr = template.Writer(cmd.OutOrStdout(), artifactOpts.formatTree, tr)
twErr = template.Writer(cmd.OutOrStdout(), opts.format, tr)
}
if err != nil {
return err
@ -1093,7 +1113,7 @@ func (artifactOpts *artifactCmd) runArtifactTree(cmd *cobra.Command, args []stri
return twErr
}
func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclient.RegClient, r ref.Ref, seen []string, rOpts []scheme.ReferrerOpts, tags []string) (*treeResult, error) {
func (opts *artifactOpts) treeAddResult(ctx context.Context, rc *regclient.RegClient, r ref.Ref, seen []string, rOpts []scheme.ReferrerOpts, tags []string) (*treeResult, error) {
tr := treeResult{
Ref: r,
}
@ -1126,7 +1146,7 @@ func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclien
}
for _, d := range dl {
rChild := r.SetDigest(d.Digest.String())
tChild, err := artifactOpts.treeAddResult(ctx, rc, rChild, seen, rOpts, tags)
tChild, err := opts.treeAddResult(ctx, rc, rChild, seen, rOpts, tags)
if tChild != nil {
tChild.ArtifactType = d.ArtifactType
if d.Platform != nil {
@ -1156,7 +1176,7 @@ func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclien
tr.Referrer = []*treeResult{}
for _, d := range rl.Descriptors {
rReferrer = rReferrer.SetDigest(d.Digest.String())
tReferrer, err := artifactOpts.treeAddResult(ctx, rc, rReferrer, seen, rOpts, tags)
tReferrer, err := opts.treeAddResult(ctx, rc, rReferrer, seen, rOpts, tags)
if tReferrer != nil {
tReferrer.ArtifactType = d.ArtifactType
if d.Platform != nil {
@ -1172,7 +1192,7 @@ func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclien
}
// include digest tags if requested
if artifactOpts.digestTags {
if opts.digestTags {
prefix, err := referrer.FallbackTag(r)
if err != nil {
return &tr, fmt.Errorf("failed to compute fallback tag: %w", err)
@ -1180,7 +1200,7 @@ func (artifactOpts *artifactCmd) treeAddResult(ctx context.Context, rc *regclien
for _, t := range tags {
if strings.HasPrefix(t, prefix.Tag) && !slices.Contains(rl.Tags, t) {
rTag := r.SetTag(t)
tReferrer, err := artifactOpts.treeAddResult(ctx, rc, rTag, seen, rOpts, tags)
tReferrer, err := opts.treeAddResult(ctx, rc, rTag, seen, rOpts, tags)
if tReferrer != nil {
tReferrer.Ref = tReferrer.Ref.SetTag(t)
tr.Referrer = append(tr.Referrer, tReferrer)

View File

@ -26,30 +26,60 @@ import (
"github.com/regclient/regclient/types/warning"
)
type blobCmd struct {
rootOpts *rootCmd
type blopOpts struct {
rootOpts *rootOpts
diffCtx int
diffFullCtx bool
diffIgnoreTime bool
formatGet string
formatFile string
formatHead string
formatPut string
format string
mt string
digest string
}
func NewBlobCmd(rootOpts *rootCmd) *cobra.Command {
blobOpts := blobCmd{
rootOpts: rootOpts,
}
var blobTopCmd = &cobra.Command{
func NewBlobCmd(rOpts *rootOpts) *cobra.Command {
cmd := &cobra.Command{
Use: "blob <cmd>",
Aliases: []string{"layer"},
Short: "manage image blobs/layers",
}
var blobDeleteCmd = &cobra.Command{
cmd.AddCommand(newBlobCopyCmd(rOpts))
cmd.AddCommand(newBlobDeleteCmd(rOpts))
cmd.AddCommand(newBlobDiffConfigCmd(rOpts))
cmd.AddCommand(newBlobDiffLayerCmd(rOpts))
cmd.AddCommand(newBlobGetCmd(rOpts))
cmd.AddCommand(newBlobGetFileCmd(rOpts))
cmd.AddCommand(newBlobHeadCmd(rOpts))
cmd.AddCommand(newBlobPutCmd(rOpts))
return cmd
}
func newBlobCopyCmd(rOpts *rootOpts) *cobra.Command {
opts := blopOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "copy <src_image_ref> <dst_image_ref> <digest>",
Aliases: []string{"cp"},
Short: "copy blob",
Long: `Copy a blob between repositories. This works in the same registry only. It
attempts to mount the layers between repositories. And within the same repository
it only sends the manifest with the new tag.`,
Example: `
# copy a blob
regctl blob copy alpine registry.example.org/library/alpine \
sha256:9123ac7c32f74759e6283f04dbf571f18246abe5bb2c779efcb32cd50f3ff13c`,
Args: cobra.ExactArgs(3),
ValidArgs: []string{}, // do not auto complete repository or digest
RunE: opts.runBlobCopy,
}
return cmd
}
func newBlobDeleteCmd(rOpts *rootOpts) *cobra.Command {
opts := blopOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "delete <repository> <digest>",
Aliases: []string{"del", "rm"},
Short: "delete a blob",
@ -63,9 +93,16 @@ regctl blob delete registry.example.org/repo \
sha256:a58ecd4f0c864650a4286c3c2d49c7219a3f2fc8d7a0bf478aa9834acfe14ae7`,
Args: cobra.ExactArgs(2),
ValidArgs: []string{}, // do not auto complete repository or digest
RunE: blobOpts.runBlobDelete,
RunE: opts.runBlobDelete,
}
var blobDiffConfigCmd = &cobra.Command{
return cmd
}
func newBlobDiffConfigCmd(rOpts *rootOpts) *cobra.Command {
opts := blopOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "diff-config <repository> <digest> <repository> <digest>",
Short: "diff two image configs",
Long: `This returns the difference between two configs, comparing the contents of each config json.`,
@ -76,9 +113,18 @@ regctl blob diff-config \
busybox sha256:3f57d9401f8d42f986df300f0c69192fc41da28ccc8d797829467780db3dd741`,
Args: cobra.ExactArgs(4),
ValidArgs: []string{}, // do not auto complete repository or digest
RunE: blobOpts.runBlobDiffConfig,
RunE: opts.runBlobDiffConfig,
}
var blobDiffLayerCmd = &cobra.Command{
cmd.Flags().IntVarP(&opts.diffCtx, "context", "", 3, "Lines of context")
cmd.Flags().BoolVarP(&opts.diffFullCtx, "context-full", "", false, "Show all lines of context")
return cmd
}
func newBlobDiffLayerCmd(rOpts *rootOpts) *cobra.Command {
opts := blopOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "diff-layer <repository> <digest> <repository> <digest>",
Short: "diff two tar layers",
Long: `This returns the difference between two layers, comparing the contents of each tar.`,
@ -89,9 +135,19 @@ regctl blob diff-layer \
busybox sha256:9ad63333ebc97e32b987ae66aa3cff81300e4c2e6d2f2395cef8a3ae18b249fe --ignore-timestamp`,
Args: cobra.ExactArgs(4),
ValidArgs: []string{}, // do not auto complete repository or digest
RunE: blobOpts.runBlobDiffLayer,
RunE: opts.runBlobDiffLayer,
}
var blobGetCmd = &cobra.Command{
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().BoolVarP(&opts.diffIgnoreTime, "ignore-timestamp", "", false, "Ignore timestamps on files")
return cmd
}
func newBlobGetCmd(rOpts *rootOpts) *cobra.Command {
opts := blopOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "get <repository> <digest>",
Aliases: []string{"pull"},
Short: "download a blob/layer",
@ -105,9 +161,25 @@ regctl blob get busybox \
| tar -tvzf -`,
Args: cobra.ExactArgs(2),
ValidArgs: []string{}, // do not auto complete repository or digest
RunE: blobOpts.runBlobGet,
RunE: opts.runBlobGet,
}
var blobGetFileCmd = &cobra.Command{
cmd.Flags().StringVarP(&opts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
cmd.Flags().StringVarP(&opts.mt, "media-type", "", "", "Set the requested mediaType (deprecated)")
_ = cmd.RegisterFlagCompletionFunc("media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
"application/octet-stream",
}, cobra.ShellCompDirectiveNoFileComp
})
_ = cmd.Flags().MarkHidden("media-type")
return cmd
}
func newBlobGetFileCmd(rOpts *rootOpts) *cobra.Command {
opts := blopOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "get-file <repository> <digest> <file> [out-file]",
Aliases: []string{"cat"},
Short: "get a file from a layer",
@ -119,9 +191,18 @@ regctl blob get-file alpine \
/etc/alpine-release`,
Args: cobra.RangeArgs(3, 4),
ValidArgs: []string{}, // do not auto complete repository, digest, or filenames
RunE: blobOpts.runBlobGetFile,
RunE: opts.runBlobGetFile,
}
var blobHeadCmd = &cobra.Command{
cmd.Flags().StringVarP(&opts.format, "format", "", "", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
return cmd
}
func newBlobHeadCmd(rOpts *rootOpts) *cobra.Command {
opts := blopOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "head <repository> <digest>",
Aliases: []string{"digest"},
Short: "http head request for a blob",
@ -132,9 +213,18 @@ regctl blob head alpine \
sha256:9123ac7c32f74759e6283f04dbf571f18246abe5bb2c779efcb32cd50f3ff13c`,
Args: cobra.ExactArgs(2),
ValidArgs: []string{}, // do not auto complete repository or digest
RunE: blobOpts.runBlobHead,
RunE: opts.runBlobHead,
}
var blobPutCmd = &cobra.Command{
cmd.Flags().StringVarP(&opts.format, "format", "", "", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
return cmd
}
func newBlobPutCmd(rOpts *rootOpts) *cobra.Command {
opts := blopOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "put <repository>",
Aliases: []string{"push"},
Short: "upload a blob/layer",
@ -145,70 +235,51 @@ is the digest of the blob.`,
regctl blob put registry.example.org/repo <layer.tgz`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{}, // do not auto complete repository
RunE: blobOpts.runBlobPut,
RunE: opts.runBlobPut,
}
var blobCopyCmd = &cobra.Command{
Use: "copy <src_image_ref> <dst_image_ref> <digest>",
Aliases: []string{"cp"},
Short: "copy blob",
Long: `Copy a blob between repositories. This works in the same registry only. It
attempts to mount the layers between repositories. And within the same repository
it only sends the manifest with the new tag.`,
Example: `
# copy a blob
regctl blob copy alpine registry.example.org/library/alpine \
sha256:9123ac7c32f74759e6283f04dbf571f18246abe5bb2c779efcb32cd50f3ff13c`,
Args: cobra.ExactArgs(3),
ValidArgs: []string{}, // do not auto complete repository or digest
RunE: blobOpts.runBlobCopy,
}
blobDiffConfigCmd.Flags().IntVarP(&blobOpts.diffCtx, "context", "", 3, "Lines of context")
blobDiffConfigCmd.Flags().BoolVarP(&blobOpts.diffFullCtx, "context-full", "", false, "Show all lines of context")
blobDiffLayerCmd.Flags().IntVarP(&blobOpts.diffCtx, "context", "", 3, "Lines of context")
blobDiffLayerCmd.Flags().BoolVarP(&blobOpts.diffFullCtx, "context-full", "", false, "Show all lines of context")
blobDiffLayerCmd.Flags().BoolVarP(&blobOpts.diffIgnoreTime, "ignore-timestamp", "", false, "Ignore timestamps on files")
blobGetCmd.Flags().StringVarP(&blobOpts.formatGet, "format", "", "{{printPretty .}}", "Format output with go template syntax")
blobGetCmd.Flags().StringVarP(&blobOpts.mt, "media-type", "", "", "Set the requested mediaType (deprecated)")
_ = blobGetCmd.RegisterFlagCompletionFunc("format", completeArgNone)
_ = blobGetCmd.RegisterFlagCompletionFunc("media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
cmd.Flags().StringVarP(&opts.mt, "content-type", "", "", "Set the requested content type (deprecated)")
_ = cmd.RegisterFlagCompletionFunc("content-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
"application/octet-stream",
}, cobra.ShellCompDirectiveNoFileComp
})
_ = blobGetCmd.Flags().MarkHidden("media-type")
blobGetFileCmd.Flags().StringVarP(&blobOpts.formatFile, "format", "", "", "Format output with go template syntax")
blobHeadCmd.Flags().StringVarP(&blobOpts.formatHead, "format", "", "", "Format output with go template syntax")
_ = blobHeadCmd.RegisterFlagCompletionFunc("format", completeArgNone)
blobPutCmd.Flags().StringVarP(&blobOpts.mt, "content-type", "", "", "Set the requested content type (deprecated)")
blobPutCmd.Flags().StringVarP(&blobOpts.digest, "digest", "", "", "Set the expected digest")
blobPutCmd.Flags().StringVarP(&blobOpts.formatPut, "format", "", "{{println .Digest}}", "Format output with go template syntax")
_ = blobPutCmd.RegisterFlagCompletionFunc("content-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
"application/octet-stream",
}, cobra.ShellCompDirectiveNoFileComp
})
_ = blobPutCmd.RegisterFlagCompletionFunc("digest", completeArgNone)
_ = blobPutCmd.Flags().MarkHidden("content-type")
blobTopCmd.AddCommand(blobDeleteCmd)
blobTopCmd.AddCommand(blobDiffConfigCmd)
blobTopCmd.AddCommand(blobDiffLayerCmd)
blobTopCmd.AddCommand(blobGetCmd)
blobTopCmd.AddCommand(blobGetFileCmd)
blobTopCmd.AddCommand(blobHeadCmd)
blobTopCmd.AddCommand(blobPutCmd)
blobTopCmd.AddCommand(blobCopyCmd)
return blobTopCmd
_ = cmd.Flags().MarkHidden("content-type")
cmd.Flags().StringVarP(&opts.digest, "digest", "", "", "Set the expected digest")
_ = cmd.RegisterFlagCompletionFunc("digest", completeArgNone)
cmd.Flags().StringVarP(&opts.format, "format", "", "{{println .Digest}}", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
return cmd
}
func (blobOpts *blobCmd) runBlobDelete(cmd *cobra.Command, args []string) error {
func (opts *blopOpts) runBlobCopy(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
rSrc, err := ref.New(args[0])
if err != nil {
return err
}
rTgt, err := ref.New(args[1])
if err != nil {
return err
}
d, err := digest.Parse(args[2])
if err != nil {
return err
}
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, rSrc)
opts.rootOpts.log.Debug("Blob copy",
slog.String("source", rSrc.CommonName()),
slog.String("target", rTgt.CommonName()),
slog.String("digest", args[2]))
err = rc.BlobCopy(ctx, rSrc, rTgt, descriptor.Descriptor{Digest: d})
if err != nil {
return err
}
return nil
}
func (opts *blopOpts) runBlobDelete(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])
if err != nil {
@ -218,22 +289,22 @@ func (blobOpts *blobCmd) runBlobDelete(cmd *cobra.Command, args []string) error
if err != nil {
return err
}
rc := blobOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
blobOpts.rootOpts.log.Debug("Deleting blob",
opts.rootOpts.log.Debug("Deleting blob",
slog.String("host", r.Registry),
slog.String("repository", r.Repository),
slog.String("digest", args[1]))
return rc.BlobDelete(ctx, r, descriptor.Descriptor{Digest: d})
}
func (blobOpts *blobCmd) runBlobDiffConfig(cmd *cobra.Command, args []string) error {
func (opts *blopOpts) runBlobDiffConfig(cmd *cobra.Command, args []string) error {
diffOpts := []diff.Opt{}
if blobOpts.diffCtx > 0 {
diffOpts = append(diffOpts, diff.WithContext(blobOpts.diffCtx, blobOpts.diffCtx))
if opts.diffCtx > 0 {
diffOpts = append(diffOpts, diff.WithContext(opts.diffCtx, opts.diffCtx))
}
if blobOpts.diffFullCtx {
if opts.diffFullCtx {
diffOpts = append(diffOpts, diff.WithFullContext())
}
ctx := cmd.Context()
@ -249,7 +320,7 @@ func (blobOpts *blobCmd) runBlobDiffConfig(cmd *cobra.Command, args []string) er
if err != nil {
return err
}
rc := blobOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
// open both configs, and output each as formatted json
d1, err := digest.Parse(args[1])
@ -286,12 +357,12 @@ func (blobOpts *blobCmd) runBlobDiffConfig(cmd *cobra.Command, args []string) er
// return template.Writer(cmd.OutOrStdout(), blobOpts.format, cDiff)
}
func (blobOpts *blobCmd) runBlobDiffLayer(cmd *cobra.Command, args []string) error {
func (opts *blopOpts) runBlobDiffLayer(cmd *cobra.Command, args []string) error {
diffOpts := []diff.Opt{}
if blobOpts.diffCtx > 0 {
diffOpts = append(diffOpts, diff.WithContext(blobOpts.diffCtx, blobOpts.diffCtx))
if opts.diffCtx > 0 {
diffOpts = append(diffOpts, diff.WithContext(opts.diffCtx, opts.diffCtx))
}
if blobOpts.diffFullCtx {
if opts.diffFullCtx {
diffOpts = append(diffOpts, diff.WithFullContext())
}
ctx := cmd.Context()
@ -307,7 +378,7 @@ func (blobOpts *blobCmd) runBlobDiffLayer(cmd *cobra.Command, args []string) err
if err != nil {
return err
}
rc := blobOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
// open both blobs, and generate reports of each content
d1, err := digest.Parse(args[1])
@ -327,7 +398,7 @@ func (blobOpts *blobCmd) runBlobDiffLayer(cmd *cobra.Command, args []string) err
if err != nil {
return err
}
rep1, err := blobOpts.blobReportLayer(tr1)
rep1, err := opts.blobReportLayer(tr1)
if err != nil {
return err
}
@ -353,7 +424,7 @@ func (blobOpts *blobCmd) runBlobDiffLayer(cmd *cobra.Command, args []string) err
if err != nil {
return err
}
rep2, err := blobOpts.blobReportLayer(tr2)
rep2, err := opts.blobReportLayer(tr2)
if err != nil {
return err
}
@ -368,7 +439,7 @@ func (blobOpts *blobCmd) runBlobDiffLayer(cmd *cobra.Command, args []string) err
return err
}
func (blobOpts *blobCmd) runBlobGet(cmd *cobra.Command, args []string) error {
func (opts *blopOpts) runBlobGet(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])
if err != nil {
@ -378,14 +449,14 @@ func (blobOpts *blobCmd) runBlobGet(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
rc := blobOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
if blobOpts.mt != "" {
blobOpts.rootOpts.log.Info("Specifying the blob media type is deprecated",
slog.String("mt", blobOpts.mt))
if opts.mt != "" {
opts.rootOpts.log.Info("Specifying the blob media type is deprecated",
slog.String("mt", opts.mt))
}
blobOpts.rootOpts.log.Debug("Pulling blob",
opts.rootOpts.log.Debug("Pulling blob",
slog.String("host", r.Registry),
slog.String("repository", r.Repository),
slog.String("digest", args[1]))
@ -394,23 +465,23 @@ func (blobOpts *blobCmd) runBlobGet(cmd *cobra.Command, args []string) error {
return err
}
switch blobOpts.formatGet {
switch opts.format {
case "raw":
blobOpts.formatGet = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .RawBody}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .RawBody}}"
case "rawBody", "raw-body", "body":
_, err = io.Copy(cmd.OutOrStdout(), blob)
return err
case "rawHeaders", "raw-headers", "headers":
blobOpts.formatGet = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
case "{{printPretty .}}":
_, err = io.Copy(cmd.OutOrStdout(), blob)
return err
}
return template.Writer(cmd.OutOrStdout(), blobOpts.formatGet, blob)
return template.Writer(cmd.OutOrStdout(), opts.format, blob)
}
func (blobOpts *blobCmd) runBlobGetFile(cmd *cobra.Command, args []string) error {
func (opts *blopOpts) runBlobGetFile(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])
if err != nil {
@ -422,10 +493,10 @@ func (blobOpts *blobCmd) runBlobGetFile(cmd *cobra.Command, args []string) error
}
filename := args[2]
filename = strings.TrimPrefix(filename, "/")
rc := blobOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
blobOpts.rootOpts.log.Debug("Get file",
opts.rootOpts.log.Debug("Get file",
slog.String("host", r.Registry),
slog.String("repository", r.Repository),
slog.String("digest", args[1]),
@ -442,7 +513,7 @@ func (blobOpts *blobCmd) runBlobGetFile(cmd *cobra.Command, args []string) error
if err != nil {
return err
}
if blobOpts.formatFile != "" {
if opts.format != "" {
data := struct {
Header *tar.Header
Reader io.Reader
@ -450,7 +521,7 @@ func (blobOpts *blobCmd) runBlobGetFile(cmd *cobra.Command, args []string) error
Header: th,
Reader: rdr,
}
return template.Writer(cmd.OutOrStdout(), blobOpts.formatFile, data)
return template.Writer(cmd.OutOrStdout(), opts.format, data)
}
var w io.Writer
if len(args) < 4 {
@ -474,7 +545,7 @@ func (blobOpts *blobCmd) runBlobGetFile(cmd *cobra.Command, args []string) error
return nil
}
func (blobOpts *blobCmd) runBlobHead(cmd *cobra.Command, args []string) error {
func (opts *blopOpts) runBlobHead(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])
if err != nil {
@ -484,10 +555,10 @@ func (blobOpts *blobCmd) runBlobHead(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
rc := blobOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
blobOpts.rootOpts.log.Debug("Blob head",
opts.rootOpts.log.Debug("Blob head",
slog.String("host", r.Registry),
slog.String("repository", r.Repository),
slog.String("digest", args[1]))
@ -496,32 +567,32 @@ func (blobOpts *blobCmd) runBlobHead(cmd *cobra.Command, args []string) error {
return err
}
switch blobOpts.formatHead {
switch opts.format {
case "", "rawHeaders", "raw-headers", "headers":
blobOpts.formatHead = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
}
return template.Writer(cmd.OutOrStdout(), blobOpts.formatHead, blob)
return template.Writer(cmd.OutOrStdout(), opts.format, blob)
}
func (blobOpts *blobCmd) runBlobPut(cmd *cobra.Command, args []string) error {
func (opts *blopOpts) runBlobPut(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])
if err != nil {
return err
}
rc := blobOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
if blobOpts.mt != "" {
blobOpts.rootOpts.log.Info("Specifying the blob media type is deprecated",
slog.String("mt", blobOpts.mt))
if opts.mt != "" {
opts.rootOpts.log.Info("Specifying the blob media type is deprecated",
slog.String("mt", opts.mt))
}
blobOpts.rootOpts.log.Debug("Pushing blob",
opts.rootOpts.log.Debug("Pushing blob",
slog.String("host", r.Registry),
slog.String("repository", r.Repository),
slog.String("digest", blobOpts.digest))
dOut, err := rc.BlobPut(ctx, r, descriptor.Descriptor{Digest: digest.Digest(blobOpts.digest)}, cmd.InOrStdin())
slog.String("digest", opts.digest))
dOut, err := rc.BlobPut(ctx, r, descriptor.Descriptor{Digest: digest.Digest(opts.digest)}, cmd.InOrStdin())
if err != nil {
return err
}
@ -534,38 +605,10 @@ func (blobOpts *blobCmd) runBlobPut(cmd *cobra.Command, args []string) error {
Size: dOut.Size,
}
return template.Writer(cmd.OutOrStdout(), blobOpts.formatPut, result)
return template.Writer(cmd.OutOrStdout(), opts.format, result)
}
func (blobOpts *blobCmd) runBlobCopy(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
rSrc, err := ref.New(args[0])
if err != nil {
return err
}
rTgt, err := ref.New(args[1])
if err != nil {
return err
}
d, err := digest.Parse(args[2])
if err != nil {
return err
}
rc := blobOpts.rootOpts.newRegClient()
defer rc.Close(ctx, rSrc)
blobOpts.rootOpts.log.Debug("Blob copy",
slog.String("source", rSrc.CommonName()),
slog.String("target", rTgt.CommonName()),
slog.String("digest", args[2]))
err = rc.BlobCopy(ctx, rSrc, rTgt, descriptor.Descriptor{Digest: d})
if err != nil {
return err
}
return nil
}
func (blobOpts *blobCmd) blobReportLayer(tr *tar.Reader) ([]string, error) {
func (opts *blopOpts) blobReportLayer(tr *tar.Reader) ([]string, error) {
report := []string{}
if tr == nil {
return report, nil
@ -582,7 +625,7 @@ func (blobOpts *blobCmd) blobReportLayer(tr *tar.Reader) ([]string, error) {
return report, fmt.Errorf("integer conversion overflow/underflow (file mode = %d)", th.Mode)
}
line := fmt.Sprintf("%s %d/%d %8d", fs.FileMode(th.Mode).String(), th.Uid, th.Gid, th.Size)
if !blobOpts.diffIgnoreTime {
if !opts.diffIgnoreTime {
line += " " + th.ModTime.Format(time.RFC3339)
}
line += fmt.Sprintf(" %-40s", th.Name)

View File

@ -52,7 +52,7 @@ func completeArgMediaTypeManifest(cmd *cobra.Command, args []string, toComplete
}, cobra.ShellCompDirectiveNoFileComp
}
func (rootOpts *rootCmd) completeArgTag(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
func (opts *rootOpts) completeArgTag(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
result := []string{}
// TODO: is it possible to expand registry, then repo, then tag?
input := strings.TrimRight(toComplete, ":")
@ -60,7 +60,7 @@ func (rootOpts *rootCmd) completeArgTag(cmd *cobra.Command, args []string, toCom
if err != nil || r.Digest != "" {
return result, cobra.ShellCompDirectiveNoFileComp
}
rc := rootOpts.newRegClient()
rc := opts.newRegClient()
tl, err := rc.TagList(context.Background(), r)
if err != nil {
return result, cobra.ShellCompDirectiveNoFileComp

View File

@ -35,8 +35,8 @@ type Config struct {
IncDockerCred *bool `json:"incDockerCred,omitempty"`
}
type configCmd struct {
rootOpts *rootCmd
type configOpts struct {
rootOpts *rootOpts
blobLimit int64
defCredHelper string
dockerCert bool
@ -44,15 +44,21 @@ type configCmd struct {
format string
}
func NewConfigCmd(rootOpts *rootCmd) *cobra.Command {
configOpts := configCmd{
rootOpts: rootOpts,
}
var configTopCmd = &cobra.Command{
func NewConfigCmd(rOpts *rootOpts) *cobra.Command {
cmd := &cobra.Command{
Use: "config <cmd>",
Short: "read/set configuration options",
}
var configGetCmd = &cobra.Command{
cmd.AddCommand(newConfigGetCmd(rOpts))
cmd.AddCommand(newConfigSetCmd(rOpts))
return cmd
}
func newConfigGetCmd(rOpts *rootOpts) *cobra.Command {
opts := configOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "get",
Short: "show the config",
Long: `Displays the configuration. Passwords are not included in the output.`,
@ -63,9 +69,18 @@ regctl config get
# display the filename of the config
regctl config get --format '{{.Filename}}'`,
Args: cobra.ExactArgs(0),
RunE: configOpts.runConfigGet,
RunE: opts.runConfigGet,
}
var configSetCmd = &cobra.Command{
cmd.Flags().StringVar(&opts.format, "format", "{{ printPretty . }}", "format the output with Go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
return cmd
}
func newConfigSetCmd(rOpts *rootOpts) *cobra.Command {
opts := configOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "set",
Short: "set a configuration option",
Long: `Modifies an option used in future executions.`,
@ -76,22 +91,16 @@ regctl config set --docker-cred=false
# enable loading credentials from docker
regctl config set --docker-cred`,
Args: cobra.ExactArgs(0),
RunE: configOpts.runConfigSet,
RunE: opts.runConfigSet,
}
configGetCmd.Flags().StringVar(&configOpts.format, "format", "{{ printPretty . }}", "format the output with Go template syntax")
configSetCmd.Flags().Int64Var(&configOpts.blobLimit, "blob-limit", 0, "limit for blob chunks, this is stored in memory")
configSetCmd.Flags().BoolVar(&configOpts.dockerCert, "docker-cert", false, "load certificates from docker")
configSetCmd.Flags().BoolVar(&configOpts.dockerCred, "docker-cred", false, "load credentials from docker")
configSetCmd.Flags().StringVar(&configOpts.defCredHelper, "default-cred-helper", "", "default credential helper")
configTopCmd.AddCommand(configGetCmd)
configTopCmd.AddCommand(configSetCmd)
return configTopCmd
cmd.Flags().Int64Var(&opts.blobLimit, "blob-limit", 0, "limit for blob chunks, this is stored in memory")
cmd.Flags().StringVar(&opts.defCredHelper, "default-cred-helper", "", "default credential helper")
cmd.Flags().BoolVar(&opts.dockerCert, "docker-cert", false, "load certificates from docker")
cmd.Flags().BoolVar(&opts.dockerCred, "docker-cred", false, "load credentials from docker")
return cmd
}
func (configOpts *configCmd) runConfigGet(cmd *cobra.Command, args []string) error {
func (opts *configOpts) runConfigGet(cmd *cobra.Command, args []string) error {
c, err := ConfigLoadDefault()
if err != nil {
return err
@ -101,38 +110,38 @@ func (configOpts *configCmd) runConfigGet(cmd *cobra.Command, args []string) err
c.Hosts[i].Token = ""
}
return template.Writer(cmd.OutOrStdout(), configOpts.format, c)
return template.Writer(cmd.OutOrStdout(), opts.format, c)
}
func (configOpts *configCmd) runConfigSet(cmd *cobra.Command, args []string) error {
func (opts *configOpts) runConfigSet(cmd *cobra.Command, args []string) error {
c, err := ConfigLoadDefault()
if err != nil {
return err
}
if flagChanged(cmd, "blob-limit") {
c.BlobLimit = configOpts.blobLimit
c.BlobLimit = opts.blobLimit
}
if flagChanged(cmd, "default-cred-helper") {
if c.HostDefault != nil {
c.HostDefault.CredHelper = configOpts.defCredHelper
c.HostDefault.CredHelper = opts.defCredHelper
}
if c.HostDefault == nil && configOpts.defCredHelper != "" {
if c.HostDefault == nil && opts.defCredHelper != "" {
c.HostDefault = &config.Host{
CredHelper: configOpts.defCredHelper,
CredHelper: opts.defCredHelper,
}
}
}
if flagChanged(cmd, "docker-cert") {
if !configOpts.dockerCert {
c.IncDockerCert = &configOpts.dockerCert
if !opts.dockerCert {
c.IncDockerCert = &opts.dockerCert
} else {
c.IncDockerCert = nil
}
}
if flagChanged(cmd, "docker-cred") {
if !configOpts.dockerCred {
c.IncDockerCred = &configOpts.dockerCred
if !opts.dockerCred {
c.IncDockerCred = &opts.dockerCred
} else {
c.IncDockerCred = nil
}

View File

@ -14,18 +14,18 @@ import (
"github.com/regclient/regclient/pkg/template"
)
type digestCmd struct {
rootOpts *rootCmd
type digestOpts struct {
rootOpts *rootOpts
algo string
format string
}
func NewDigestCmd(rootOpts *rootCmd) *cobra.Command {
digestOpts := digestCmd{
rootOpts: rootOpts,
func NewDigestCmd(rOpts *rootOpts) *cobra.Command {
opts := digestOpts{
rootOpts: rOpts,
}
// TODO(bmitch): consider if this should be moved out of hidden/experimental
var digestCmd = &cobra.Command{
cmd := &cobra.Command{
Hidden: true,
Use: "digest",
Short: "compute digest on stdin",
@ -35,19 +35,22 @@ This command is EXPERIMENTAL and could be removed in the future.`,
# compute the digest of hello world
echo hello world | regctl digest`,
Args: cobra.RangeArgs(0, 0),
RunE: digestOpts.runDigest,
RunE: opts.runDigest,
}
cmd.Flags().StringVar(&opts.algo, "algorithm", "sha256", "Digest algorithm")
_ = cmd.RegisterFlagCompletionFunc("algorithm", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"sha256", "sha512"}, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().StringVar(&opts.format, "format", "{{.String}}", "Go template to output the digest result")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
digestCmd.Flags().StringVar(&digestOpts.algo, "algorithm", "sha256", "Digest algorithm")
digestCmd.Flags().StringVar(&digestOpts.format, "format", "{{.String}}", "Go template to output the digest result")
return digestCmd
return cmd
}
func (digestOpts *digestCmd) runDigest(cmd *cobra.Command, args []string) error {
algo := digest.Algorithm(digestOpts.algo)
func (opts *digestOpts) runDigest(cmd *cobra.Command, args []string) error {
algo := digest.Algorithm(opts.algo)
if !algo.Available() {
return fmt.Errorf("digest algorithm %s is not available", digestOpts.algo)
return fmt.Errorf("digest algorithm %s is not available", opts.algo)
}
digester := algo.Digester()
@ -56,5 +59,5 @@ func (digestOpts *digestCmd) runDigest(cmd *cobra.Command, args []string) error
return err
}
return template.Writer(cmd.OutOrStdout(), digestOpts.format, digester.Digest())
return template.Writer(cmd.OutOrStdout(), opts.format, digester.Digest())
}

File diff suppressed because it is too large Load Diff

View File

@ -27,8 +27,8 @@ var indexKnownTypes = []string{
mediatype.Docker2ManifestList,
}
type indexCmd struct {
rootOpts *rootCmd
type indexOpts struct {
rootOpts *rootOpts
annotations []string
artifactType string
byDigest bool
@ -44,16 +44,22 @@ type indexCmd struct {
subject string
}
func NewIndexCmd(rootOpts *rootCmd) *cobra.Command {
indexOpts := indexCmd{
rootOpts: rootOpts,
}
var indexTopCmd = &cobra.Command{
func NewIndexCmd(rOpts *rootOpts) *cobra.Command {
var indexCmd = &cobra.Command{
Use: "index <cmd>",
Short: "manage manifest lists and OCI index",
}
indexCmd.AddCommand(newIndexAddCmd(rOpts))
indexCmd.AddCommand(newIndexCreateCmd(rOpts))
indexCmd.AddCommand(newIndexDeleteCmd(rOpts))
return indexCmd
}
var indexAddCmd = &cobra.Command{
func newIndexAddCmd(rOpts *rootOpts) *cobra.Command {
opts := indexOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "add <image_ref>",
Aliases: []string{"append", "insert"},
Short: "add an index entry",
@ -63,12 +69,26 @@ func NewIndexCmd(rootOpts *rootCmd) *cobra.Command {
regctl index add registry.example.org/repo:v1 --ref registry.example.org/repo:arm64`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{}, // do not auto complete digests
RunE: indexOpts.runIndexAdd,
RunE: opts.runIndexAdd,
}
cmd.Flags().StringArrayVar(&opts.descAnnotations, "desc-annotation", []string{}, "Annotation to add to descriptors of new entries")
cmd.Flags().StringVar(&opts.descPlatform, "desc-platform", "", "Platform to set in descriptors of new entries")
cmd.Flags().StringArrayVar(&opts.digests, "digest", []string{}, "Digest to add")
cmd.Flags().BoolVar(&opts.incDigestTags, "digest-tags", false, "Include digest tags")
cmd.Flags().BoolVar(&opts.incReferrers, "referrers", false, "Include referrers")
cmd.Flags().StringArrayVar(&opts.refs, "ref", []string{}, "References to add")
cmd.Flags().StringArrayVar(&opts.platforms, "platform", []string{}, "Platforms to include from ref")
_ = cmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
return cmd
}
var indexCreateCmd = &cobra.Command{
func newIndexCreateCmd(rOpts *rootOpts) *cobra.Command {
opts := indexOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "create <image_ref>",
Aliases: []string{"init", "new"},
Aliases: []string{"init", "new", "put"},
Short: "create an index",
Long: `Create a manifest list or OCI Index.`,
Example: `
@ -91,10 +111,34 @@ regctl index create registry.example.org/library/golang:windows \
--platform windows/amd64,osver=10.0.17763.5458`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{}, // do not auto complete digests
RunE: indexOpts.runIndexCreate,
RunE: opts.runIndexCreate,
}
cmd.Flags().StringArrayVar(&opts.annotations, "annotation", []string{}, "Annotation to set on manifest")
cmd.Flags().StringVar(&opts.artifactType, "artifact-type", "", "Include an artifactType value")
cmd.Flags().BoolVar(&opts.byDigest, "by-digest", false, "Push manifest by digest instead of tag")
cmd.Flags().StringArrayVar(&opts.descAnnotations, "desc-annotation", []string{}, "Annotation to add to descriptors of new entries")
cmd.Flags().StringVar(&opts.descPlatform, "desc-platform", "", "Platform to set in descriptors of new entries")
cmd.Flags().StringArrayVar(&opts.digests, "digest", []string{}, "Digest to include in new index")
cmd.Flags().BoolVar(&opts.incDigestTags, "digest-tags", false, "Include digest tags")
cmd.Flags().StringVar(&opts.format, "format", "", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
cmd.Flags().StringVarP(&opts.mediaType, "media-type", "m", mediatype.OCI1ManifestList, "Media-type for manifest list or OCI Index")
_ = cmd.RegisterFlagCompletionFunc("media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return indexKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().StringArrayVar(&opts.platforms, "platform", []string{}, "Platforms to include from ref")
_ = cmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
cmd.Flags().StringArrayVar(&opts.refs, "ref", []string{}, "References to include in new index")
cmd.Flags().BoolVar(&opts.incReferrers, "referrers", false, "Include referrers")
cmd.Flags().StringVar(&opts.subject, "subject", "", "Specify a subject tag or digest (this manifest must already exist in the repo)")
return cmd
}
var indexDeleteCmd = &cobra.Command{
func newIndexDeleteCmd(rOpts *rootOpts) *cobra.Command {
opts := indexOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "delete <image_ref>",
Aliases: []string{"del", "rm", "remove"},
Short: "delete an index entry",
@ -106,47 +150,15 @@ regctl index delete registry.example.org/repo:v1 \
--platform linux/ppc64le --platform linux/mips64le`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{}, // do not auto complete digests
RunE: indexOpts.runIndexDelete,
RunE: opts.runIndexDelete,
}
indexAddCmd.Flags().StringArrayVar(&indexOpts.descAnnotations, "desc-annotation", []string{}, "Annotation to add to descriptors of new entries")
indexAddCmd.Flags().StringVar(&indexOpts.descPlatform, "desc-platform", "", "Platform to set in descriptors of new entries")
indexAddCmd.Flags().StringArrayVar(&indexOpts.digests, "digest", []string{}, "Digest to add")
indexAddCmd.Flags().BoolVar(&indexOpts.incDigestTags, "digest-tags", false, "Include digest tags")
indexAddCmd.Flags().BoolVar(&indexOpts.incReferrers, "referrers", false, "Include referrers")
indexAddCmd.Flags().StringArrayVar(&indexOpts.refs, "ref", []string{}, "References to add")
indexAddCmd.Flags().StringArrayVar(&indexOpts.platforms, "platform", []string{}, "Platforms to include from ref")
_ = indexAddCmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
indexCreateCmd.Flags().StringArrayVar(&indexOpts.annotations, "annotation", []string{}, "Annotation to set on manifest")
indexCreateCmd.Flags().StringVar(&indexOpts.artifactType, "artifact-type", "", "Include an artifactType value")
indexCreateCmd.Flags().BoolVar(&indexOpts.byDigest, "by-digest", false, "Push manifest by digest instead of tag")
indexCreateCmd.Flags().StringArrayVar(&indexOpts.descAnnotations, "desc-annotation", []string{}, "Annotation to add to descriptors of new entries")
indexCreateCmd.Flags().StringVar(&indexOpts.descPlatform, "desc-platform", "", "Platform to set in descriptors of new entries")
indexCreateCmd.Flags().StringArrayVar(&indexOpts.digests, "digest", []string{}, "Digest to include in new index")
indexCreateCmd.Flags().StringVar(&indexOpts.format, "format", "", "Format output with go template syntax")
indexCreateCmd.Flags().BoolVar(&indexOpts.incDigestTags, "digest-tags", false, "Include digest tags")
indexCreateCmd.Flags().BoolVar(&indexOpts.incReferrers, "referrers", false, "Include referrers")
indexCreateCmd.Flags().StringVarP(&indexOpts.mediaType, "media-type", "m", mediatype.OCI1ManifestList, "Media-type for manifest list or OCI Index")
_ = indexCreateCmd.RegisterFlagCompletionFunc("media-type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return indexKnownTypes, cobra.ShellCompDirectiveNoFileComp
})
indexCreateCmd.Flags().StringVar(&indexOpts.subject, "subject", "", "Specify a subject tag or digest (this manifest must already exist in the repo)")
indexCreateCmd.Flags().StringArrayVar(&indexOpts.refs, "ref", []string{}, "References to include in new index")
indexCreateCmd.Flags().StringArrayVar(&indexOpts.platforms, "platform", []string{}, "Platforms to include from ref")
_ = indexCreateCmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
indexDeleteCmd.Flags().StringArrayVar(&indexOpts.digests, "digest", []string{}, "Digest to delete")
indexDeleteCmd.Flags().StringArrayVar(&indexOpts.platforms, "platform", []string{}, "Platform to delete")
_ = indexDeleteCmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
indexTopCmd.AddCommand(indexAddCmd)
indexTopCmd.AddCommand(indexCreateCmd)
indexTopCmd.AddCommand(indexDeleteCmd)
return indexTopCmd
cmd.Flags().StringArrayVar(&opts.digests, "digest", []string{}, "Digest to delete")
cmd.Flags().StringArrayVar(&opts.platforms, "platform", []string{}, "Platform to delete")
_ = cmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
return cmd
}
func (indexOpts *indexCmd) runIndexAdd(cmd *cobra.Command, args []string) error {
func (opts *indexOpts) runIndexAdd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// dedup warnings
if w := warning.FromContext(ctx); w == nil {
@ -160,7 +172,7 @@ func (indexOpts *indexCmd) runIndexAdd(cmd *cobra.Command, args []string) error
}
// setup regclient
rc := indexOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
// pull existing index
@ -178,7 +190,7 @@ func (indexOpts *indexCmd) runIndexAdd(cmd *cobra.Command, args []string) error
}
// generate a list of descriptors from CLI args
descList, err := indexOpts.indexBuildDescList(ctx, rc, r)
descList, err := opts.indexBuildDescList(ctx, rc, r)
if err != nil {
return err
}
@ -206,13 +218,13 @@ func (indexOpts *indexCmd) runIndexAdd(cmd *cobra.Command, args []string) error
}{
Manifest: m,
}
if r.Tag == "" && r.Digest != "" && indexOpts.format == "" {
indexOpts.format = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
if r.Tag == "" && r.Digest != "" && opts.format == "" {
opts.format = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
}
return template.Writer(cmd.OutOrStdout(), indexOpts.format, result)
return template.Writer(cmd.OutOrStdout(), opts.format, result)
}
func (indexOpts *indexCmd) runIndexCreate(cmd *cobra.Command, args []string) error {
func (opts *indexOpts) runIndexCreate(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// dedup warnings
if w := warning.FromContext(ctx); w == nil {
@ -220,8 +232,8 @@ func (indexOpts *indexCmd) runIndexCreate(cmd *cobra.Command, args []string) err
}
// validate media type
if indexOpts.mediaType != mediatype.OCI1ManifestList && indexOpts.mediaType != mediatype.Docker2ManifestList {
return fmt.Errorf("unsupported manifest media type: %s%.0w", indexOpts.mediaType, errs.ErrUnsupportedMediaType)
if opts.mediaType != mediatype.OCI1ManifestList && opts.mediaType != mediatype.Docker2ManifestList {
return fmt.Errorf("unsupported manifest media type: %s%.0w", opts.mediaType, errs.ErrUnsupportedMediaType)
}
// parse ref
@ -231,12 +243,12 @@ func (indexOpts *indexCmd) runIndexCreate(cmd *cobra.Command, args []string) err
}
// setup regclient
rc := indexOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
// parse annotations
annotations := map[string]string{}
for _, a := range indexOpts.annotations {
for _, a := range opts.annotations {
aSplit := strings.SplitN(a, "=", 2)
if len(aSplit) == 1 {
annotations[aSplit[0]] = ""
@ -246,20 +258,20 @@ func (indexOpts *indexCmd) runIndexCreate(cmd *cobra.Command, args []string) err
}
// generate a list of descriptors from CLI args
descList, err := indexOpts.indexBuildDescList(ctx, rc, r)
descList, err := opts.indexBuildDescList(ctx, rc, r)
if err != nil {
return err
}
descList = indexDescListRmDup(descList)
var subj *descriptor.Descriptor
if indexOpts.subject != "" && indexOpts.mediaType == mediatype.OCI1ManifestList {
if opts.subject != "" && opts.mediaType == mediatype.OCI1ManifestList {
var rSubj ref.Ref
dig, err := digest.Parse(indexOpts.subject)
dig, err := digest.Parse(opts.subject)
if err == nil {
rSubj = r.SetDigest(dig.String())
} else {
rSubj = r.SetTag(indexOpts.subject)
rSubj = r.SetTag(opts.subject)
}
mSubj, err := rc.ManifestHead(ctx, rSubj, regclient.WithManifestRequireDigest())
if err != nil {
@ -272,12 +284,12 @@ func (indexOpts *indexCmd) runIndexCreate(cmd *cobra.Command, args []string) err
// build the index
mOpts := []manifest.Opts{}
switch indexOpts.mediaType {
switch opts.mediaType {
case mediatype.OCI1ManifestList:
m := v1.Index{
Versioned: v1.IndexSchemaVersion,
MediaType: mediatype.OCI1ManifestList,
ArtifactType: indexOpts.artifactType,
ArtifactType: opts.artifactType,
Manifests: descList,
Subject: subj,
}
@ -301,7 +313,7 @@ func (indexOpts *indexCmd) runIndexCreate(cmd *cobra.Command, args []string) err
}
// push the index
if indexOpts.byDigest {
if opts.byDigest {
r = r.SetDigest(mm.GetDescriptor().Digest.String())
}
err = rc.ManifestPut(ctx, r, mm)
@ -315,13 +327,13 @@ func (indexOpts *indexCmd) runIndexCreate(cmd *cobra.Command, args []string) err
}{
Manifest: mm,
}
if indexOpts.byDigest && indexOpts.format == "" {
indexOpts.format = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
if opts.byDigest && opts.format == "" {
opts.format = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
}
return template.Writer(cmd.OutOrStdout(), indexOpts.format, result)
return template.Writer(cmd.OutOrStdout(), opts.format, result)
}
func (indexOpts *indexCmd) runIndexDelete(cmd *cobra.Command, args []string) error {
func (opts *indexOpts) runIndexDelete(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// dedup warnings
if w := warning.FromContext(ctx); w == nil {
@ -335,7 +347,7 @@ func (indexOpts *indexCmd) runIndexDelete(cmd *cobra.Command, args []string) err
}
// setup regclient
rc := indexOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
// pull existing index
@ -353,7 +365,7 @@ func (indexOpts *indexCmd) runIndexDelete(cmd *cobra.Command, args []string) err
}
// for each CLI arg, find and delete matching entries
for _, dig := range indexOpts.digests {
for _, dig := range opts.digests {
i := len(curDesc) - 1
for i >= 0 {
if curDesc[i].Digest.String() == dig {
@ -362,7 +374,7 @@ func (indexOpts *indexCmd) runIndexDelete(cmd *cobra.Command, args []string) err
i--
}
}
for _, platStr := range indexOpts.platforms {
for _, platStr := range opts.platforms {
plat, err := platform.Parse(platStr)
if err != nil {
return err
@ -397,25 +409,25 @@ func (indexOpts *indexCmd) runIndexDelete(cmd *cobra.Command, args []string) err
}{
Manifest: m,
}
if r.Tag == "" && r.Digest != "" && indexOpts.format == "" {
indexOpts.format = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
if r.Tag == "" && r.Digest != "" && opts.format == "" {
opts.format = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
}
return template.Writer(cmd.OutOrStdout(), indexOpts.format, result)
return template.Writer(cmd.OutOrStdout(), opts.format, result)
}
func (indexOpts *indexCmd) indexBuildDescList(ctx context.Context, rc *regclient.RegClient, r ref.Ref) ([]descriptor.Descriptor, error) {
func (opts *indexOpts) indexBuildDescList(ctx context.Context, rc *regclient.RegClient, r ref.Ref) ([]descriptor.Descriptor, error) {
imgCopyOpts := []regclient.ImageOpts{
regclient.ImageWithChild(),
}
if indexOpts.incDigestTags {
if opts.incDigestTags {
imgCopyOpts = append(imgCopyOpts, regclient.ImageWithDigestTags())
}
if indexOpts.incReferrers {
if opts.incReferrers {
imgCopyOpts = append(imgCopyOpts, regclient.ImageWithReferrers())
}
descAnnotations := map[string]string{}
for _, a := range indexOpts.descAnnotations {
for _, a := range opts.descAnnotations {
aSplit := strings.SplitN(a, "=", 2)
if len(aSplit) == 1 {
descAnnotations[aSplit[0]] = ""
@ -424,7 +436,7 @@ func (indexOpts *indexCmd) indexBuildDescList(ctx context.Context, rc *regclient
}
}
platforms := []platform.Platform{}
for _, pStr := range indexOpts.platforms {
for _, pStr := range opts.platforms {
p, err := platform.Parse(pStr)
if err != nil {
return nil, fmt.Errorf("failed to parse platform %s: %w", pStr, err)
@ -433,10 +445,10 @@ func (indexOpts *indexCmd) indexBuildDescList(ctx context.Context, rc *regclient
}
// copy each ref by digest to the destination repository
if indexOpts.digests == nil {
indexOpts.digests = []string{}
if opts.digests == nil {
opts.digests = []string{}
}
for _, rStr := range indexOpts.refs {
for _, rStr := range opts.refs {
srcRef, err := ref.New(rStr)
if err != nil {
return nil, err
@ -453,7 +465,7 @@ func (indexOpts *indexCmd) indexBuildDescList(ctx context.Context, rc *regclient
if err != nil {
return nil, err
}
indexOpts.digests = append(indexOpts.digests, desc.Digest.String())
opts.digests = append(opts.digests, desc.Digest.String())
} else {
// platform specific descriptors are being extracted from a manifest list
mCopy, err = rc.ManifestGet(ctx, srcRef)
@ -476,7 +488,7 @@ func (indexOpts *indexCmd) indexBuildDescList(ctx context.Context, rc *regclient
if err != nil {
return nil, err
}
indexOpts.digests = append(indexOpts.digests, d.Digest.String())
opts.digests = append(opts.digests, d.Digest.String())
}
}
}
@ -484,7 +496,7 @@ func (indexOpts *indexCmd) indexBuildDescList(ctx context.Context, rc *regclient
// parse each digest, pull manifest, get config, append to list of descriptors
descList := []descriptor.Descriptor{}
for _, dig := range indexOpts.digests {
for _, dig := range opts.digests {
rDig := r.SetDigest(dig)
mDig, err := rc.ManifestHead(ctx, rDig, regclient.WithManifestRequireDigest())
if err != nil {
@ -492,8 +504,8 @@ func (indexOpts *indexCmd) indexBuildDescList(ctx context.Context, rc *regclient
}
desc := mDig.GetDescriptor()
plat := &platform.Platform{}
if indexOpts.descPlatform != "" {
*plat, err = platform.Parse(indexOpts.descPlatform)
if opts.descPlatform != "" {
*plat, err = platform.Parse(opts.descPlatform)
} else {
plat, err = indexGetPlatform(ctx, rc, rDig, mDig)
}

View File

@ -13,19 +13,19 @@ import (
func main() {
ctx, cancel := context.WithCancel(context.Background())
rootTopCmd, rootOpts := NewRootCmd()
cmd, opts := NewRootCmd()
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
go func() {
<-sig
rootOpts.log.Debug("Interrupt received, stopping")
opts.log.Debug("Interrupt received, stopping")
// clean shutdown
cancel()
}()
godbg.SignalTrace()
if err := rootTopCmd.ExecuteContext(ctx); err != nil {
if err := cmd.ExecuteContext(ctx); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
// provide tips for common error messages
switch {

View File

@ -19,16 +19,14 @@ import (
"github.com/regclient/regclient/types/warning"
)
type manifestCmd struct {
rootOpts *rootCmd
type manifestOpts struct {
rootOpts *rootOpts
byDigest bool
contentType string
diffCtx int
diffFullCtx bool
forceTagDeref bool
formatGet string
formatHead string
formatPut string
format string
list bool
platform string
referrers bool
@ -36,16 +34,25 @@ type manifestCmd struct {
requireList bool
}
func NewManifestCmd(rootOpts *rootCmd) *cobra.Command {
manifestOpts := manifestCmd{
rootOpts: rootOpts,
}
var manifestTopCmd = &cobra.Command{
func NewManifestCmd(rOpts *rootOpts) *cobra.Command {
cmd := &cobra.Command{
Use: "manifest <cmd>",
Short: "manage manifests",
}
var manifestDeleteCmd = &cobra.Command{
cmd.AddCommand(newManifestDeleteCmd(rOpts))
cmd.AddCommand(newManifestDiffCmd(rOpts))
cmd.AddCommand(newManifestHeadCmd(rOpts))
cmd.AddCommand(newManifestGetCmd(rOpts))
cmd.AddCommand(newManifestPutCmd(rOpts))
return cmd
}
func newManifestDeleteCmd(rOpts *rootOpts) *cobra.Command {
opts := manifestOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "delete <image_ref>",
Aliases: []string{"del", "rm", "remove"},
Short: "delete a manifest",
@ -66,10 +73,18 @@ regctl manifest delete --referrers \
registry.example.org/repo@sha256:fab3c890d0480549d05d2ff3d746f42e360b7f0e3fe64bdf39fc572eab94911b`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{}, // do not auto complete digests
RunE: manifestOpts.runManifestDelete,
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")
return cmd
}
var manifestDiffCmd = &cobra.Command{
func newManifestDiffCmd(rOpts *rootOpts) *cobra.Command {
opts := manifestOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "diff <image_ref> <image_ref>",
Short: "compare manifests",
Long: `Show the differences between two image manifests`,
@ -84,11 +99,19 @@ regctl manifest diff --context-full \
ghcr.io/regclient/regctl@sha256:9b7057d06ce061cefc7a0b7cb28cad626164e6629a1a4f09cee4b4d400c9aef0 \
ghcr.io/regclient/regctl@sha256:4d113b278bd425d094848ba5d7b4d6baca13a2a9d20d265b32bc12020d501002`,
Args: cobra.ExactArgs(2),
ValidArgsFunction: rootOpts.completeArgTag,
RunE: manifestOpts.runManifestDiff,
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")
return cmd
}
var manifestGetCmd = &cobra.Command{
func newManifestGetCmd(rOpts *rootOpts) *cobra.Command {
opts := manifestOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "get <image_ref>",
Aliases: []string{"pull"},
Short: "retrieve manifest or manifest list",
@ -103,11 +126,24 @@ regctl manifest get alpine --format raw-body --platform local
# retrieve the manifest for a specific windows version
regctl manifest get golang --platform windows/amd64,osver=10.0.17763.4974`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: rootOpts.completeArgTag,
RunE: manifestOpts.runManifestGet,
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.RegisterFlagCompletionFunc("format", completeArgNone)
cmd.Flags().BoolVarP(&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")
return cmd
}
var manifestHeadCmd = &cobra.Command{
func newManifestHeadCmd(rOpts *rootOpts) *cobra.Command {
opts := manifestOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "head <image_ref>",
Aliases: []string{"digest"},
Short: "http head request for manifest",
@ -116,17 +152,34 @@ regctl manifest get golang --platform windows/amd64,osver=10.0.17763.4974`,
# show the digest for an image
regctl manifest head alpine
# "regctl image digest" is an alias
regctl image digest alpine
# show the digest for a specific platform (this will perform a GET request)
regctl manifest head alpine --platform linux/arm64
# show all headers for the request
regctl manifest head alpine --format raw-headers`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: rootOpts.completeArgTag,
RunE: manifestOpts.runManifestHead,
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.RegisterFlagCompletionFunc("format", completeArgNone)
cmd.Flags().BoolVarP(&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")
return cmd
}
var manifestPutCmd = &cobra.Command{
func newManifestPutCmd(rOpts *rootOpts) *cobra.Command {
opts := manifestOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "put <image_ref>",
Aliases: []string{"push"},
Short: "push manifest or manifest list",
@ -137,47 +190,18 @@ regctl manifest put \
--content-type application/vnd.oci.image.manifest.v1+json \
registry.example.org/repo:v1 <manifest.json`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: rootOpts.completeArgTag,
RunE: manifestOpts.runManifestPut,
ValidArgsFunction: rOpts.completeArgTag,
RunE: opts.runManifestPut,
}
manifestDeleteCmd.Flags().BoolVarP(&manifestOpts.forceTagDeref, "force-tag-dereference", "", false, "Dereference the a tag to a digest, this is unsafe")
manifestDeleteCmd.Flags().BoolVarP(&manifestOpts.referrers, "referrers", "", false, "Check for referrers, recommended when deleting artifacts")
manifestDiffCmd.Flags().IntVarP(&manifestOpts.diffCtx, "context", "", 3, "Lines of context")
manifestDiffCmd.Flags().BoolVarP(&manifestOpts.diffFullCtx, "context-full", "", false, "Show all lines of context")
manifestHeadCmd.Flags().StringVarP(&manifestOpts.formatHead, "format", "", "", "Format output with go template syntax (use \"raw-body\" for the original manifest)")
manifestHeadCmd.Flags().BoolVarP(&manifestOpts.list, "list", "", true, "Do not resolve platform from manifest list (enabled by default)")
_ = manifestHeadCmd.Flags().MarkHidden("list")
manifestHeadCmd.Flags().StringVarP(&manifestOpts.platform, "platform", "p", "", "Specify platform (e.g. linux/amd64 or local, requires a get request)")
_ = manifestHeadCmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
manifestHeadCmd.Flags().BoolVarP(&manifestOpts.requireDigest, "require-digest", "", false, "Fallback to get request if digest is not received")
manifestHeadCmd.Flags().BoolVarP(&manifestOpts.requireList, "require-list", "", false, "Fail if manifest list is not received")
manifestGetCmd.Flags().BoolVarP(&manifestOpts.list, "list", "", true, "Deprecated: Output manifest list if available")
_ = manifestGetCmd.Flags().MarkHidden("list")
manifestGetCmd.Flags().StringVarP(&manifestOpts.platform, "platform", "p", "", "Specify platform (e.g. linux/amd64 or local)")
_ = manifestGetCmd.RegisterFlagCompletionFunc("platform", completeArgPlatform)
manifestGetCmd.Flags().BoolVarP(&manifestOpts.requireList, "require-list", "", false, "Deprecated: Fail if manifest list is not received")
manifestGetCmd.Flags().StringVarP(&manifestOpts.formatGet, "format", "", "{{printPretty .}}", "Format output with go template syntax (use \"raw-body\" for the original manifest)")
_ = manifestGetCmd.RegisterFlagCompletionFunc("format", completeArgNone)
manifestPutCmd.Flags().BoolVarP(&manifestOpts.byDigest, "by-digest", "", false, "Push manifest by digest instead of tag")
manifestPutCmd.Flags().StringVarP(&manifestOpts.contentType, "content-type", "t", "", "Specify content-type (e.g. application/vnd.docker.distribution.manifest.v2+json)")
_ = manifestPutCmd.RegisterFlagCompletionFunc("content-type", completeArgMediaTypeManifest)
manifestPutCmd.Flags().StringVarP(&manifestOpts.formatPut, "format", "", "", "Format output with go template syntax")
_ = manifestPutCmd.RegisterFlagCompletionFunc("format", completeArgNone)
manifestTopCmd.AddCommand(manifestDeleteCmd)
manifestTopCmd.AddCommand(manifestDiffCmd)
manifestTopCmd.AddCommand(manifestHeadCmd)
manifestTopCmd.AddCommand(manifestGetCmd)
manifestTopCmd.AddCommand(manifestPutCmd)
return manifestTopCmd
cmd.Flags().BoolVarP(&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.RegisterFlagCompletionFunc("format", completeArgNone)
return cmd
}
func (manifestOpts *manifestCmd) runManifestDelete(cmd *cobra.Command, args []string) error {
func (opts *manifestOpts) runManifestDelete(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// dedup warnings
if w := warning.FromContext(ctx); w == nil {
@ -187,26 +211,26 @@ func (manifestOpts *manifestCmd) runManifestDelete(cmd *cobra.Command, args []st
if err != nil {
return err
}
rc := manifestOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
if r.Digest == "" && manifestOpts.forceTagDeref {
if r.Digest == "" && opts.forceTagDeref {
m, err := rc.ManifestHead(ctx, r, regclient.WithManifestRequireDigest())
if err != nil {
return err
}
r = r.AddDigest(manifest.GetDigest(m).String())
manifestOpts.rootOpts.log.Debug("Forced dereference of tag",
opts.rootOpts.log.Debug("Forced dereference of tag",
slog.String("orig", args[0]),
slog.String("resolved", r.CommonName()))
}
manifestOpts.rootOpts.log.Debug("Manifest delete",
opts.rootOpts.log.Debug("Manifest delete",
slog.String("host", r.Registry),
slog.String("repo", r.Repository),
slog.String("digest", r.Digest))
mOpts := []regclient.ManifestOpts{}
if manifestOpts.referrers {
if opts.referrers {
mOpts = append(mOpts, regclient.WithManifestCheckReferrers())
}
@ -217,12 +241,12 @@ func (manifestOpts *manifestCmd) runManifestDelete(cmd *cobra.Command, args []st
return nil
}
func (manifestOpts *manifestCmd) runManifestDiff(cmd *cobra.Command, args []string) error {
func (opts *manifestOpts) runManifestDiff(cmd *cobra.Command, args []string) error {
diffOpts := []diff.Opt{}
if manifestOpts.diffCtx > 0 {
diffOpts = append(diffOpts, diff.WithContext(manifestOpts.diffCtx, manifestOpts.diffCtx))
if opts.diffCtx > 0 {
diffOpts = append(diffOpts, diff.WithContext(opts.diffCtx, opts.diffCtx))
}
if manifestOpts.diffFullCtx {
if opts.diffFullCtx {
diffOpts = append(diffOpts, diff.WithFullContext())
}
ctx := cmd.Context()
@ -239,9 +263,9 @@ func (manifestOpts *manifestCmd) runManifestDiff(cmd *cobra.Command, args []stri
return err
}
rc := manifestOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
manifestOpts.rootOpts.log.Debug("Manifest diff",
opts.rootOpts.log.Debug("Manifest diff",
slog.String("ref1", r1.CommonName()),
slog.String("ref2", r2.CommonName()))
@ -271,12 +295,12 @@ func (manifestOpts *manifestCmd) runManifestDiff(cmd *cobra.Command, args []stri
// return template.Writer(cmd.OutOrStdout(), manifestOpts.format, mDiff)
}
func (manifestOpts *manifestCmd) runManifestHead(cmd *cobra.Command, args []string) error {
func (opts *manifestOpts) runManifestHead(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if flagChanged(cmd, "list") {
manifestOpts.rootOpts.log.Info("list option has been deprecated, manifest list is output by default until a platform is specified")
opts.rootOpts.log.Info("list option has been deprecated, manifest list is output by default until a platform is specified")
}
if manifestOpts.platform != "" && manifestOpts.requireList {
if opts.platform != "" && opts.requireList {
return fmt.Errorf("cannot request a platform and require-list simultaneously")
}
@ -284,22 +308,22 @@ func (manifestOpts *manifestCmd) runManifestHead(cmd *cobra.Command, args []stri
if err != nil {
return err
}
rc := manifestOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
manifestOpts.rootOpts.log.Debug("Manifest head",
opts.rootOpts.log.Debug("Manifest head",
slog.String("host", r.Registry),
slog.String("repo", r.Repository),
slog.String("tag", r.Tag))
mOpts := []regclient.ManifestOpts{}
if manifestOpts.requireDigest || (!flagChanged(cmd, "require-digest") && !flagChanged(cmd, "format")) {
if opts.requireDigest || (!flagChanged(cmd, "require-digest") && !flagChanged(cmd, "format")) {
mOpts = append(mOpts, regclient.WithManifestRequireDigest())
}
if manifestOpts.platform != "" {
p, err := platform.Parse(manifestOpts.platform)
if opts.platform != "" {
p, err := platform.Parse(opts.platform)
if err != nil {
return fmt.Errorf("failed to parse platform %s: %w", manifestOpts.platform, err)
return fmt.Errorf("failed to parse platform %s: %w", opts.platform, err)
}
mOpts = append(mOpts, regclient.WithManifestPlatform(p))
}
@ -309,21 +333,21 @@ func (manifestOpts *manifestCmd) runManifestHead(cmd *cobra.Command, args []stri
return err
}
switch manifestOpts.formatHead {
switch opts.format {
case "", "digest":
manifestOpts.formatHead = "{{ printf \"%s\\n\" .GetDescriptor.Digest }}"
opts.format = "{{ printf \"%s\\n\" .GetDescriptor.Digest }}"
case "rawHeaders", "raw-headers", "headers":
manifestOpts.formatHead = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
}
return template.Writer(cmd.OutOrStdout(), manifestOpts.formatHead, m)
return template.Writer(cmd.OutOrStdout(), opts.format, m)
}
func (manifestOpts *manifestCmd) runManifestGet(cmd *cobra.Command, args []string) error {
func (opts *manifestOpts) runManifestGet(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
if flagChanged(cmd, "list") {
manifestOpts.rootOpts.log.Info("list option has been deprecated, manifest list is output by default until a platform is specified")
opts.rootOpts.log.Info("list option has been deprecated, manifest list is output by default until a platform is specified")
}
if manifestOpts.platform != "" && manifestOpts.requireList {
if opts.platform != "" && opts.requireList {
return fmt.Errorf("cannot request a platform and require-list simultaneously")
}
@ -331,19 +355,19 @@ func (manifestOpts *manifestCmd) runManifestGet(cmd *cobra.Command, args []strin
if err != nil {
return err
}
rc := manifestOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
manifestOpts.rootOpts.log.Debug("Manifest get",
opts.rootOpts.log.Debug("Manifest get",
slog.String("host", r.Registry),
slog.String("repo", r.Repository),
slog.String("tag", r.Tag))
mOpts := []regclient.ManifestOpts{}
if manifestOpts.platform != "" {
p, err := platform.Parse(manifestOpts.platform)
if opts.platform != "" {
p, err := platform.Parse(opts.platform)
if err != nil {
return fmt.Errorf("failed to parse platform %s: %w", manifestOpts.platform, err)
return fmt.Errorf("failed to parse platform %s: %w", opts.platform, err)
}
mOpts = append(mOpts, regclient.WithManifestPlatform(p))
}
@ -353,44 +377,44 @@ func (manifestOpts *manifestCmd) runManifestGet(cmd *cobra.Command, args []strin
return err
}
switch manifestOpts.formatGet {
switch opts.format {
case "raw":
manifestOpts.formatGet = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .RawBody}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .RawBody}}"
case "rawBody", "raw-body", "body":
manifestOpts.formatGet = "{{printf \"%s\" .RawBody}}"
opts.format = "{{printf \"%s\" .RawBody}}"
case "rawHeaders", "raw-headers", "headers":
manifestOpts.formatGet = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
}
return template.Writer(cmd.OutOrStdout(), manifestOpts.formatGet, m)
return template.Writer(cmd.OutOrStdout(), opts.format, m)
}
func (manifestOpts *manifestCmd) runManifestPut(cmd *cobra.Command, args []string) error {
func (opts *manifestOpts) runManifestPut(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])
if err != nil {
return err
}
rc := manifestOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
raw, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
return err
}
opts := []manifest.Opts{
mOpts := []manifest.Opts{
manifest.WithRef(r),
manifest.WithRaw(raw),
}
if manifestOpts.contentType != "" {
opts = append(opts, manifest.WithDesc(descriptor.Descriptor{
MediaType: manifestOpts.contentType,
if opts.contentType != "" {
mOpts = append(mOpts, manifest.WithDesc(descriptor.Descriptor{
MediaType: opts.contentType,
}))
}
rcM, err := manifest.New(opts...)
rcM, err := manifest.New(mOpts...)
if err != nil {
return err
}
if manifestOpts.byDigest {
if opts.byDigest {
r = r.SetDigest(rcM.GetDescriptor().Digest.String())
}
@ -404,8 +428,8 @@ func (manifestOpts *manifestCmd) runManifestPut(cmd *cobra.Command, args []strin
}{
Manifest: rcM,
}
if manifestOpts.byDigest && manifestOpts.formatPut == "" {
manifestOpts.formatPut = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
if opts.byDigest && opts.format == "" {
opts.format = "{{ printf \"%s\\n\" .Manifest.GetDescriptor.Digest }}"
}
return template.Writer(cmd.OutOrStdout(), manifestOpts.formatPut, result)
return template.Writer(cmd.OutOrStdout(), opts.format, result)
}

View File

@ -13,17 +13,17 @@ import (
"github.com/regclient/regclient/types/ref"
)
type refCmd struct {
rootOpts *rootCmd
type refOpts struct {
rootOpts *rootOpts
format string
}
func NewRefCmd(rootOpts *rootCmd) *cobra.Command {
refOpts := refCmd{
rootOpts: rootOpts,
func NewRefCmd(rOpts *rootOpts) *cobra.Command {
opts := refOpts{
rootOpts: rOpts,
}
// TODO(bmitch): consider if this should be moved out of hidden/experimental
var refCmd = &cobra.Command{
cmd := &cobra.Command{
Hidden: true,
Use: "ref",
Short: "parse an image ref",
@ -34,19 +34,19 @@ This command is EXPERIMENTAL and could be removed in the future.`,
regctl ref nginx --format '{{ .Registry }}'
`,
Args: cobra.ExactArgs(1),
RunE: refOpts.runRef,
RunE: opts.runRef,
}
cmd.Flags().StringVar(&opts.format, "format", "{{.CommonName}}", "Format the output using a Go template")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
refCmd.Flags().StringVar(&refOpts.format, "format", "{{.CommonName}}", "Format the output using a Go template")
return refCmd
return cmd
}
func (refOpts *refCmd) runRef(cmd *cobra.Command, args []string) error {
func (opts *refOpts) runRef(cmd *cobra.Command, args []string) error {
r, err := ref.New(args[0])
if err != nil {
return fmt.Errorf("failed to parse %s: %w", args[0], err)
}
return template.Writer(cmd.OutOrStdout(), refOpts.format, r)
return template.Writer(cmd.OutOrStdout(), opts.format, r)
}

View File

@ -20,9 +20,9 @@ import (
"github.com/regclient/regclient/types/ref"
)
type registryCmd struct {
rootOpts *rootCmd
formatConf string
type registryOpts struct {
rootOpts *rootOpts
format string
user, pass string // login opts
passStdin bool
credHelper string
@ -42,15 +42,24 @@ type registryCmd struct {
dns []string // TODO: remove
}
func NewRegistryCmd(rootOpts *rootCmd) *cobra.Command {
registryOpts := registryCmd{
rootOpts: rootOpts,
}
var registryTopCmd = &cobra.Command{
func NewRegistryCmd(rOpts *rootOpts) *cobra.Command {
cmd := &cobra.Command{
Use: "registry <cmd>",
Short: "manage registries",
}
var registryConfigCmd = &cobra.Command{
cmd.AddCommand(newRegistryConfigCmd(rOpts))
cmd.AddCommand(newRegistryLoginCmd(rOpts))
cmd.AddCommand(newRegistryLogoutCmd(rOpts))
cmd.AddCommand(newRegistrySetCmd(rOpts))
cmd.AddCommand(newRegistryWhoamiCmd(rOpts))
return cmd
}
func newRegistryConfigCmd(rOpts *rootOpts) *cobra.Command {
opts := registryOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "config [registry]",
Short: "show registry config",
Long: `Displays the configuration used for a registry. Secrets are not included
@ -69,9 +78,18 @@ regctl registry config docker.io
regctl registry config docker.io --format '{{.User}}'`,
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: registryArgListReg,
RunE: registryOpts.runRegistryConfig,
RunE: opts.runRegistryConfig,
}
var registryLoginCmd = &cobra.Command{
cmd.Flags().StringVar(&opts.format, "format", "{{jsonPretty .}}", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
return cmd
}
func newRegistryLoginCmd(rOpts *rootOpts) *cobra.Command {
opts := registryOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "login <registry>",
Short: "login to a registry",
Long: `Provide login credentials for a registry. This may not be necessary if you
@ -87,9 +105,22 @@ regctl registry login registry.example.org
echo "${token}" | regctl registry login ghcr.io -u "${username}" --pass-stdin`,
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: registryArgListReg,
RunE: registryOpts.runRegistryLogin,
RunE: opts.runRegistryLogin,
}
var registryLogoutCmd = &cobra.Command{
cmd.Flags().StringVarP(&opts.pass, "pass", "p", "", "Password")
_ = cmd.RegisterFlagCompletionFunc("pass", completeArgNone)
cmd.Flags().BoolVar(&opts.passStdin, "pass-stdin", false, "Read password from stdin")
cmd.Flags().BoolVar(&opts.skipCheck, "skip-check", false, "Skip checking connectivity to the registry")
cmd.Flags().StringVarP(&opts.user, "user", "u", "", "Username")
_ = cmd.RegisterFlagCompletionFunc("user", completeArgNone)
return cmd
}
func newRegistryLogoutCmd(rOpts *rootOpts) *cobra.Command {
opts := registryOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "logout <registry>",
Short: "logout of a registry",
Long: `Remove registry credentials from the configuration.`,
@ -101,9 +132,16 @@ regctl registry logout
regctl registry logout registry.example.org`,
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: registryArgListReg,
RunE: registryOpts.runRegistryLogout,
RunE: opts.runRegistryLogout,
}
var registrySetCmd = &cobra.Command{
return cmd
}
func newRegistrySetCmd(rOpts *rootOpts) *cobra.Command {
opts := registryOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "set <registry>",
Short: "set options on a registry",
Long: `Set or modify the configuration of a registry.`,
@ -121,9 +159,52 @@ regctl registry set docker.io --mirror hub-mirror.example.org
regctl registry set quay.io --req-per-sec 10`,
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: registryArgListReg,
RunE: registryOpts.runRegistrySet,
RunE: opts.runRegistrySet,
}
var registryWhoamiCmd = &cobra.Command{
cmd.Flags().StringArrayVar(&opts.apiOpts, "api-opts", nil, "List of options (key=value))")
cmd.Flags().Int64Var(&opts.blobChunk, "blob-chunk", 0, "Blob chunk size")
_ = cmd.RegisterFlagCompletionFunc("blob-chunk", completeArgNone)
cmd.Flags().Int64Var(&opts.blobMax, "blob-max", 0, "Blob size before switching to chunked push, -1 to disable")
_ = cmd.RegisterFlagCompletionFunc("blob-max", completeArgNone)
cmd.Flags().StringVar(&opts.cacert, "cacert", "", "CA Certificate (not a filename, use \"$(cat ca.pem)\" to use a file)")
_ = cmd.RegisterFlagCompletionFunc("cacert", completeArgNone)
cmd.Flags().StringVar(&opts.clientCert, "client-cert", "", "Client certificate for mTLS (not a filename, use \"$(cat client.pem)\" to use a file)")
cmd.Flags().StringVar(&opts.clientKey, "client-key", "", "Client key for mTLS (not a filename, use \"$(cat client.key)\" to use a file)")
cmd.Flags().StringVar(&opts.credHelper, "cred-helper", "", "Credential helper (full binary name, including docker-credential- prefix)")
cmd.Flags().StringVar(&opts.hostname, "hostname", "", "Hostname or ip with port")
_ = cmd.RegisterFlagCompletionFunc("hostname", completeArgNone)
cmd.Flags().StringArrayVar(&opts.mirrors, "mirror", nil, "List of mirrors (registry names)")
_ = cmd.RegisterFlagCompletionFunc("mirror", completeArgNone)
cmd.Flags().StringVar(&opts.pathPrefix, "path-prefix", "", "Prefix to all repositories")
_ = cmd.RegisterFlagCompletionFunc("path-prefix", completeArgNone)
cmd.Flags().UintVar(&opts.priority, "priority", 0, "Priority (for sorting mirrors)")
_ = cmd.RegisterFlagCompletionFunc("priority", completeArgNone)
cmd.Flags().BoolVar(&opts.repoAuth, "repo-auth", false, "Separate auth requests per repository instead of per registry")
cmd.Flags().Int64Var(&opts.reqConcurrent, "req-concurrent", 0, "Concurrent requests")
cmd.Flags().Float64Var(&opts.reqPerSec, "req-per-sec", 0, "Requests per second")
cmd.Flags().BoolVar(&opts.skipCheck, "skip-check", false, "Skip checking connectivity to the registry")
cmd.Flags().StringVar(&opts.tls, "tls", "", "TLS (enabled, insecure, disabled)")
_ = cmd.RegisterFlagCompletionFunc("tls", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
"enabled",
"insecure",
"disabled",
}, cobra.ShellCompDirectiveNoFileComp
})
// TODO: eventually remove
cmd.Flags().StringArrayVar(&opts.dns, "dns", nil, "[Deprecated] DNS hostname or ip with port")
_ = cmd.Flags().MarkHidden("dns")
cmd.Flags().StringVar(&opts.scheme, "scheme", "", "[Deprecated] Scheme (http, https)")
_ = cmd.Flags().MarkHidden("scheme")
return cmd
}
func newRegistryWhoamiCmd(rOpts *rootOpts) *cobra.Command {
opts := registryOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "whoami [registry]",
Short: "show current login for a registry",
Long: `Displays the username for a given registry.`,
@ -135,61 +216,9 @@ regctl registry whoami
regctl registry whoami registry.example.org`,
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: registryArgListReg,
RunE: registryOpts.runRegistryWhoami,
RunE: opts.runRegistryWhoami,
}
registryConfigCmd.Flags().StringVar(&registryOpts.formatConf, "format", "{{jsonPretty .}}", "Format output with go template syntax")
registryLoginCmd.Flags().StringVarP(&registryOpts.user, "user", "u", "", "Username")
registryLoginCmd.Flags().StringVarP(&registryOpts.pass, "pass", "p", "", "Password")
registryLoginCmd.Flags().BoolVar(&registryOpts.passStdin, "pass-stdin", false, "Read password from stdin")
registryLoginCmd.Flags().BoolVar(&registryOpts.skipCheck, "skip-check", false, "Skip checking connectivity to the registry")
_ = registryLoginCmd.RegisterFlagCompletionFunc("user", completeArgNone)
_ = registryLoginCmd.RegisterFlagCompletionFunc("pass", completeArgNone)
registrySetCmd.Flags().StringVar(&registryOpts.credHelper, "cred-helper", "", "Credential helper (full binary name, including docker-credential- prefix)")
registrySetCmd.Flags().StringVar(&registryOpts.cacert, "cacert", "", "CA Certificate (not a filename, use \"$(cat ca.pem)\" to use a file)")
registrySetCmd.Flags().StringVar(&registryOpts.clientCert, "client-cert", "", "Client certificate for mTLS (not a filename, use \"$(cat client.pem)\" to use a file)")
registrySetCmd.Flags().StringVar(&registryOpts.clientKey, "client-key", "", "Client key for mTLS (not a filename, use \"$(cat client.key)\" to use a file)")
registrySetCmd.Flags().StringVar(&registryOpts.tls, "tls", "", "TLS (enabled, insecure, disabled)")
registrySetCmd.Flags().StringVar(&registryOpts.hostname, "hostname", "", "Hostname or ip with port")
registrySetCmd.Flags().StringVar(&registryOpts.pathPrefix, "path-prefix", "", "Prefix to all repositories")
registrySetCmd.Flags().StringArrayVar(&registryOpts.mirrors, "mirror", nil, "List of mirrors (registry names)")
registrySetCmd.Flags().UintVar(&registryOpts.priority, "priority", 0, "Priority (for sorting mirrors)")
registrySetCmd.Flags().BoolVar(&registryOpts.repoAuth, "repo-auth", false, "Separate auth requests per repository instead of per registry")
registrySetCmd.Flags().Int64Var(&registryOpts.blobChunk, "blob-chunk", 0, "Blob chunk size")
registrySetCmd.Flags().Int64Var(&registryOpts.blobMax, "blob-max", 0, "Blob size before switching to chunked push, -1 to disable")
registrySetCmd.Flags().Float64Var(&registryOpts.reqPerSec, "req-per-sec", 0, "Requests per second")
registrySetCmd.Flags().Int64Var(&registryOpts.reqConcurrent, "req-concurrent", 0, "Concurrent requests")
registrySetCmd.Flags().BoolVar(&registryOpts.skipCheck, "skip-check", false, "Skip checking connectivity to the registry")
registrySetCmd.Flags().StringArrayVar(&registryOpts.apiOpts, "api-opts", nil, "List of options (key=value))")
_ = registrySetCmd.RegisterFlagCompletionFunc("cacert", completeArgNone)
_ = registrySetCmd.RegisterFlagCompletionFunc("tls", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
"enabled",
"insecure",
"disabled",
}, cobra.ShellCompDirectiveNoFileComp
})
_ = registrySetCmd.RegisterFlagCompletionFunc("hostname", completeArgNone)
_ = registrySetCmd.RegisterFlagCompletionFunc("path-prefix", completeArgNone)
_ = registrySetCmd.RegisterFlagCompletionFunc("mirror", completeArgNone)
_ = registrySetCmd.RegisterFlagCompletionFunc("priority", completeArgNone)
_ = registrySetCmd.RegisterFlagCompletionFunc("blob-chunk", completeArgNone)
_ = registrySetCmd.RegisterFlagCompletionFunc("blob-max", completeArgNone)
// TODO: eventually remove
registrySetCmd.Flags().StringVar(&registryOpts.scheme, "scheme", "", "[Deprecated] Scheme (http, https)")
registrySetCmd.Flags().StringArrayVar(&registryOpts.dns, "dns", nil, "[Deprecated] DNS hostname or ip with port")
_ = registrySetCmd.Flags().MarkHidden("scheme")
_ = registrySetCmd.Flags().MarkHidden("dns")
registryTopCmd.AddCommand(registryConfigCmd)
registryTopCmd.AddCommand(registryLoginCmd)
registryTopCmd.AddCommand(registryLogoutCmd)
registryTopCmd.AddCommand(registrySetCmd)
registryTopCmd.AddCommand(registryWhoamiCmd)
return registryTopCmd
return cmd
}
func registryArgListReg(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@ -206,7 +235,7 @@ func registryArgListReg(cmd *cobra.Command, args []string, toComplete string) ([
return result, cobra.ShellCompDirectiveNoFileComp
}
func (registryOpts *registryCmd) runRegistryConfig(cmd *cobra.Command, args []string) error {
func (opts *registryOpts) runRegistryConfig(cmd *cobra.Command, args []string) error {
c, err := ConfigLoadDefault()
if err != nil {
return err
@ -214,7 +243,7 @@ func (registryOpts *registryCmd) runRegistryConfig(cmd *cobra.Command, args []st
if len(args) > 0 {
h, ok := c.Hosts[args[0]]
if !ok {
registryOpts.rootOpts.log.Warn("No configuration found for registry",
opts.rootOpts.log.Warn("No configuration found for registry",
slog.String("registry", args[0]))
return nil
}
@ -225,7 +254,7 @@ func (registryOpts *registryCmd) runRegistryConfig(cmd *cobra.Command, args []st
h.Pass = ""
h.Token = ""
h.ClientKey = ""
return template.Writer(cmd.OutOrStdout(), registryOpts.formatConf, h)
return template.Writer(cmd.OutOrStdout(), opts.format, h)
} else {
// do not output secrets
for i := range c.Hosts {
@ -233,11 +262,11 @@ func (registryOpts *registryCmd) runRegistryConfig(cmd *cobra.Command, args []st
c.Hosts[i].Token = ""
c.Hosts[i].ClientKey = ""
}
return template.Writer(cmd.OutOrStdout(), registryOpts.formatConf, c)
return template.Writer(cmd.OutOrStdout(), opts.format, c)
}
}
func (registryOpts *registryCmd) runRegistryLogin(cmd *cobra.Command, args []string) error {
func (opts *registryOpts) runRegistryLogin(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
// disable signal handler to allow ctrl-c to be used on prompts (context cancel on a blocking reader is difficult)
signal.Reset(os.Interrupt, syscall.SIGTERM)
@ -258,8 +287,8 @@ func (registryOpts *registryCmd) runRegistryLogin(cmd *cobra.Command, args []str
c.Hosts[h.Name] = h
}
if flagChanged(cmd, "user") {
h.User = registryOpts.user
} else if registryOpts.passStdin {
h.User = opts.user
} else if opts.passStdin {
return fmt.Errorf("user must be provided to read password from stdin")
} else {
// prompt for username
@ -274,14 +303,14 @@ func (registryOpts *registryCmd) runRegistryLogin(cmd *cobra.Command, args []str
if user != "" {
h.User = user
} else if h.User == "" {
registryOpts.rootOpts.log.Error("Username is required")
opts.rootOpts.log.Error("Username is required")
return ErrMissingInput
}
}
if flagChanged(cmd, "pass") {
h.Pass = registryOpts.pass
} else if registryOpts.passStdin {
h.Pass = opts.pass
} else if opts.passStdin {
pass, err := io.ReadAll(cmd.InOrStdin())
if err != nil {
return fmt.Errorf("failed to read password from stdin: %w", err)
@ -290,7 +319,7 @@ func (registryOpts *registryCmd) runRegistryLogin(cmd *cobra.Command, args []str
if passwd != "" {
h.Pass = passwd
} else {
registryOpts.rootOpts.log.Error("Password is required")
opts.rootOpts.log.Error("Password is required")
return ErrMissingInput
}
@ -312,7 +341,7 @@ func (registryOpts *registryCmd) runRegistryLogin(cmd *cobra.Command, args []str
if passwd != "" {
h.Pass = passwd
} else {
registryOpts.rootOpts.log.Error("Password is required")
opts.rootOpts.log.Error("Password is required")
return ErrMissingInput
}
@ -329,25 +358,25 @@ func (registryOpts *registryCmd) runRegistryLogin(cmd *cobra.Command, args []str
if err != nil {
return err
}
if !registryOpts.skipCheck {
if !opts.skipCheck {
r, err := ref.NewHost(args[0])
if err != nil {
return err
}
rc := registryOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
_, err = rc.Ping(ctx, r)
if err != nil {
registryOpts.rootOpts.log.Warn("Failed to ping registry, credentials were still stored")
opts.rootOpts.log.Warn("Failed to ping registry, credentials were still stored")
return err
}
}
registryOpts.rootOpts.log.Info("Credentials set",
opts.rootOpts.log.Info("Credentials set",
slog.String("registry", args[0]))
return nil
}
func (registryOpts *registryCmd) runRegistryLogout(cmd *cobra.Command, args []string) error {
func (opts *registryOpts) runRegistryLogout(cmd *cobra.Command, args []string) error {
c, err := ConfigLoadDefault()
if err != nil {
return err
@ -362,7 +391,7 @@ func (registryOpts *registryCmd) runRegistryLogout(cmd *cobra.Command, args []st
if curH, ok := c.Hosts[h.Name]; ok {
h = curH
} else {
registryOpts.rootOpts.log.Warn("No configuration/credentials found",
opts.rootOpts.log.Warn("No configuration/credentials found",
slog.String("registry", h.Name))
return nil
}
@ -375,12 +404,12 @@ func (registryOpts *registryCmd) runRegistryLogout(cmd *cobra.Command, args []st
return err
}
registryOpts.rootOpts.log.Debug("Credentials unset",
opts.rootOpts.log.Debug("Credentials unset",
slog.String("registry", args[0]))
return nil
}
func (registryOpts *registryCmd) runRegistrySet(cmd *cobra.Command, args []string) error {
func (opts *registryOpts) runRegistrySet(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
c, err := ConfigLoadDefault()
if err != nil {
@ -399,64 +428,64 @@ func (registryOpts *registryCmd) runRegistrySet(cmd *cobra.Command, args []strin
c.Hosts[h.Name] = h
}
if flagChanged(cmd, "scheme") {
registryOpts.rootOpts.log.Warn("Scheme flag is deprecated, for http set tls to disabled",
opts.rootOpts.log.Warn("Scheme flag is deprecated, for http set tls to disabled",
slog.String("name", h.Name),
slog.String("scheme", registryOpts.scheme))
slog.String("scheme", opts.scheme))
}
if flagChanged(cmd, "dns") {
registryOpts.rootOpts.log.Warn("DNS flag is deprecated, use hostname and mirrors instead",
opts.rootOpts.log.Warn("DNS flag is deprecated, use hostname and mirrors instead",
slog.String("name", h.Name),
slog.Any("dns", registryOpts.dns))
slog.Any("dns", opts.dns))
}
if flagChanged(cmd, "cred-helper") {
h.CredHelper = registryOpts.credHelper
h.CredHelper = opts.credHelper
}
if flagChanged(cmd, "tls") {
if err := h.TLS.UnmarshalText([]byte(registryOpts.tls)); err != nil {
if err := h.TLS.UnmarshalText([]byte(opts.tls)); err != nil {
return err
}
}
if flagChanged(cmd, "cacert") {
h.RegCert = registryOpts.cacert
h.RegCert = opts.cacert
}
if flagChanged(cmd, "client-cert") {
h.ClientCert = registryOpts.clientCert
h.ClientCert = opts.clientCert
}
if flagChanged(cmd, "client-key") {
h.ClientKey = registryOpts.clientKey
h.ClientKey = opts.clientKey
}
if flagChanged(cmd, "hostname") {
h.Hostname = registryOpts.hostname
h.Hostname = opts.hostname
}
if flagChanged(cmd, "path-prefix") {
h.PathPrefix = registryOpts.pathPrefix
h.PathPrefix = opts.pathPrefix
}
if flagChanged(cmd, "mirror") {
h.Mirrors = registryOpts.mirrors
h.Mirrors = opts.mirrors
}
if flagChanged(cmd, "priority") {
h.Priority = registryOpts.priority
h.Priority = opts.priority
}
if flagChanged(cmd, "repo-auth") {
h.RepoAuth = registryOpts.repoAuth
h.RepoAuth = opts.repoAuth
}
if flagChanged(cmd, "blob-chunk") {
h.BlobChunk = registryOpts.blobChunk
h.BlobChunk = opts.blobChunk
}
if flagChanged(cmd, "blob-max") {
h.BlobMax = registryOpts.blobMax
h.BlobMax = opts.blobMax
}
if flagChanged(cmd, "req-per-sec") {
h.ReqPerSec = registryOpts.reqPerSec
h.ReqPerSec = opts.reqPerSec
}
if flagChanged(cmd, "req-concurrent") {
h.ReqConcurrent = registryOpts.reqConcurrent
h.ReqConcurrent = opts.reqConcurrent
}
if flagChanged(cmd, "api-opts") {
if h.APIOpts == nil {
h.APIOpts = map[string]string{}
}
for _, kv := range registryOpts.apiOpts {
for _, kv := range opts.apiOpts {
kvArr := strings.SplitN(kv, "=", 2)
if len(kvArr) == 2 && kvArr[1] != "" {
// set a value
@ -473,26 +502,26 @@ func (registryOpts *registryCmd) runRegistrySet(cmd *cobra.Command, args []strin
return err
}
if !registryOpts.skipCheck {
if !opts.skipCheck {
r, err := ref.NewHost(args[0])
if err != nil {
return err
}
rc := registryOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
_, err = rc.Ping(ctx, r)
if err != nil {
registryOpts.rootOpts.log.Warn("Failed to ping registry, configuration still updated")
opts.rootOpts.log.Warn("Failed to ping registry, configuration still updated")
return err
}
}
registryOpts.rootOpts.log.Info("Registry configuration updated/set",
opts.rootOpts.log.Info("Registry configuration updated/set",
slog.String("name", h.Name))
return nil
}
func (registryOpts *registryCmd) runRegistryWhoami(cmd *cobra.Command, args []string) error {
func (opts *registryOpts) runRegistryWhoami(cmd *cobra.Command, args []string) error {
c, err := ConfigLoadDefault()
if err != nil {
return err

View File

@ -10,22 +10,27 @@ import (
"github.com/regclient/regclient/scheme"
)
type repoCmd struct {
rootOpts *rootCmd
type repoOpts struct {
rootOpts *rootOpts
last string
limit int
format string
}
func NewRepoCmd(rootOpts *rootCmd) *cobra.Command {
repoOpts := repoCmd{
rootOpts: rootOpts,
}
var repoTopCmd = &cobra.Command{
func NewRepoCmd(rOpts *rootOpts) *cobra.Command {
cmd := &cobra.Command{
Use: "repo <cmd>",
Short: "manage repositories",
}
var repoLsCmd = &cobra.Command{
cmd.AddCommand(newRepoLsCmd(rOpts))
return cmd
}
func newRepoLsCmd(rOpts *rootOpts) *cobra.Command {
opts := repoOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "ls <registry>",
Aliases: []string{"list"},
Short: "list repositories in a registry",
@ -39,53 +44,50 @@ regctl repo ls registry.example.org
regctl repo ls --last repo1 --limit 5 registry.example.org`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: registryArgListReg,
RunE: repoOpts.runRepoLs,
RunE: opts.runRepoLs,
}
repoLsCmd.Flags().StringVarP(&repoOpts.last, "last", "", "", "Specify the last repo from a previous request for pagination")
_ = repoLsCmd.RegisterFlagCompletionFunc("last", completeArgNone)
repoLsCmd.Flags().IntVarP(&repoOpts.limit, "limit", "", 0, "Specify the number of repos to retrieve")
_ = repoLsCmd.RegisterFlagCompletionFunc("limit", completeArgNone)
repoLsCmd.Flags().StringVarP(&repoOpts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
_ = repoLsCmd.RegisterFlagCompletionFunc("format", completeArgNone)
repoTopCmd.AddCommand(repoLsCmd)
return repoTopCmd
cmd.Flags().StringVarP(&opts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
cmd.Flags().StringVarP(&opts.last, "last", "", "", "Specify the last repo from a previous request for pagination")
_ = cmd.RegisterFlagCompletionFunc("last", completeArgNone)
cmd.Flags().IntVarP(&opts.limit, "limit", "", 0, "Specify the number of repos to retrieve")
_ = cmd.RegisterFlagCompletionFunc("limit", completeArgNone)
return cmd
}
func (repoOpts *repoCmd) runRepoLs(cmd *cobra.Command, args []string) error {
func (opts *repoOpts) runRepoLs(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
host := args[0]
// TODO: use regex to validate hostname + port
i := strings.IndexRune(host, '/')
if i >= 0 {
repoOpts.rootOpts.log.Error("Hostname invalid",
opts.rootOpts.log.Error("Hostname invalid",
slog.String("host", host))
return ErrInvalidInput
}
rc := repoOpts.rootOpts.newRegClient()
repoOpts.rootOpts.log.Debug("Listing repositories",
rc := opts.rootOpts.newRegClient()
opts.rootOpts.log.Debug("Listing repositories",
slog.String("host", host),
slog.String("last", repoOpts.last),
slog.Int("limit", repoOpts.limit))
opts := []scheme.RepoOpts{}
if repoOpts.last != "" {
opts = append(opts, scheme.WithRepoLast(repoOpts.last))
slog.String("last", opts.last),
slog.Int("limit", opts.limit))
sOpts := []scheme.RepoOpts{}
if opts.last != "" {
sOpts = append(sOpts, scheme.WithRepoLast(opts.last))
}
if repoOpts.limit != 0 {
opts = append(opts, scheme.WithRepoLimit(repoOpts.limit))
if opts.limit != 0 {
sOpts = append(sOpts, scheme.WithRepoLimit(opts.limit))
}
rl, err := rc.RepoList(ctx, host, opts...)
rl, err := rc.RepoList(ctx, host, sOpts...)
if err != nil {
return err
}
switch repoOpts.format {
switch opts.format {
case "raw":
repoOpts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .RawBody}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .RawBody}}"
case "rawBody", "raw-body", "body":
repoOpts.format = "{{printf \"%s\" .RawBody}}"
opts.format = "{{printf \"%s\" .RawBody}}"
case "rawHeaders", "raw-headers", "headers":
repoOpts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
}
return template.Writer(cmd.OutOrStdout(), repoOpts.format, rl)
return template.Writer(cmd.OutOrStdout(), opts.format, rl)
}

View File

@ -24,19 +24,23 @@ const (
UserAgent = "regclient/regctl"
)
type rootCmd struct {
type rootOpts struct {
name string
verbosity string
logopts []string
log *slog.Logger
format string // for Go template formatting of various commands
hosts []string
userAgent string
}
func NewRootCmd() (*cobra.Command, *rootCmd) {
rootOpts := rootCmd{}
var rootTopCmd = &cobra.Command{
type versionOpts struct {
rootOpts *rootOpts
format string
}
func NewRootCmd() (*cobra.Command, *rootOpts) {
rOpts := &rootOpts{}
cmd := &cobra.Command{
Use: "regctl <cmd>",
Short: "Utility for accessing docker registries",
Long: `Utility for accessing docker registries
@ -62,11 +66,47 @@ regctl image digest --host reg=localhost:5000,tls=disabled localhost:5000/repo:v
SilenceUsage: true,
SilenceErrors: true,
}
rootOpts.name = rootTopCmd.Name()
var versionCmd = &cobra.Command{
rOpts.name = cmd.Name()
rOpts.log = slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: slog.LevelWarn}))
cmd.PersistentFlags().StringVarP(&rOpts.verbosity, "verbosity", "v", slog.LevelWarn.String(), "Log level (trace, debug, info, warn, error)")
_ = cmd.RegisterFlagCompletionFunc("verbosity", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"trace", "debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp
})
cmd.PersistentFlags().StringArrayVar(&rOpts.logopts, "logopt", []string{}, "Log options")
_ = cmd.RegisterFlagCompletionFunc("logopt", completeArgNone)
cmd.PersistentFlags().StringArrayVar(&rOpts.hosts, "host", []string{}, "Registry hosts to add (reg=registry,user=username,pass=password,tls=enabled)")
_ = cmd.RegisterFlagCompletionFunc("host", completeArgNone)
cmd.PersistentFlags().StringVarP(&rOpts.userAgent, "user-agent", "", "", "Override user agent")
_ = cmd.RegisterFlagCompletionFunc("user-agent", completeArgNone)
cmd.PersistentPreRunE = rOpts.rootPreRun
cmd.AddCommand(cobradoc.NewCmd(rOpts.name, "cli-doc"))
cmd.AddCommand(
NewArtifactCmd(rOpts),
NewBlobCmd(rOpts),
NewConfigCmd(rOpts),
NewDigestCmd(rOpts),
NewImageCmd(rOpts),
NewIndexCmd(rOpts),
NewManifestCmd(rOpts),
NewRefCmd(rOpts),
NewRegistryCmd(rOpts),
NewRepoCmd(rOpts),
NewTagCmd(rOpts),
newVersionCmd(rOpts),
)
return cmd, rOpts
}
func newVersionCmd(rOpts *rootOpts) *cobra.Command {
opts := versionOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "version",
Short: "Show the version",
Long: fmt.Sprintf(`Show the version of %s`, rootOpts.name),
Long: fmt.Sprintf(`Show the version of %s`, opts.rootOpts.name),
Example: `
# display full version details
regctl version
@ -74,78 +114,42 @@ regctl version
# retrieve the version number
regctl version --format '{{.VCSTag}}'`,
Args: cobra.ExactArgs(0),
RunE: rootOpts.runVersion,
RunE: opts.runVersion,
}
rootOpts.log = slog.New(slog.NewTextHandler(rootTopCmd.ErrOrStderr(), &slog.HandlerOptions{Level: slog.LevelWarn}))
rootTopCmd.PersistentFlags().StringVarP(&rootOpts.verbosity, "verbosity", "v", slog.LevelWarn.String(), "Log level (trace, debug, info, warn, error)")
_ = rootTopCmd.RegisterFlagCompletionFunc("verbosity", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"trace", "debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp
})
rootTopCmd.PersistentFlags().StringArrayVar(&rootOpts.logopts, "logopt", []string{}, "Log options")
_ = rootTopCmd.RegisterFlagCompletionFunc("logopt", completeArgNone)
rootTopCmd.PersistentFlags().StringArrayVar(&rootOpts.hosts, "host", []string{}, "Registry hosts to add (reg=registry,user=username,pass=password,tls=enabled)")
_ = rootTopCmd.RegisterFlagCompletionFunc("host", completeArgNone)
rootTopCmd.PersistentFlags().StringVarP(&rootOpts.userAgent, "user-agent", "", "", "Override user agent")
_ = rootTopCmd.RegisterFlagCompletionFunc("user-agent", completeArgNone)
versionCmd.Flags().StringVarP(&rootOpts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
_ = versionCmd.RegisterFlagCompletionFunc("format", completeArgNone)
rootTopCmd.PersistentPreRunE = rootOpts.rootPreRun
rootTopCmd.AddCommand(versionCmd)
rootTopCmd.AddCommand(cobradoc.NewCmd(rootOpts.name, "cli-doc"))
rootTopCmd.AddCommand(
NewArtifactCmd(&rootOpts),
NewBlobCmd(&rootOpts),
NewConfigCmd(&rootOpts),
NewDigestCmd(&rootOpts),
NewImageCmd(&rootOpts),
NewIndexCmd(&rootOpts),
NewManifestCmd(&rootOpts),
NewRefCmd(&rootOpts),
NewRegistryCmd(&rootOpts),
NewRepoCmd(&rootOpts),
NewTagCmd(&rootOpts),
)
return rootTopCmd, &rootOpts
cmd.Flags().StringVarP(&opts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
return cmd
}
func (rootOpts *rootCmd) rootPreRun(cmd *cobra.Command, args []string) error {
func (opts *rootOpts) rootPreRun(cmd *cobra.Command, args []string) error {
var lvl slog.Level
err := lvl.UnmarshalText([]byte(rootOpts.verbosity))
err := lvl.UnmarshalText([]byte(opts.verbosity))
if err != nil {
// handle custom levels
if rootOpts.verbosity == strings.ToLower("trace") {
if opts.verbosity == strings.ToLower("trace") {
lvl = types.LevelTrace
} else {
return fmt.Errorf("unable to parse verbosity %s: %v", rootOpts.verbosity, err)
return fmt.Errorf("unable to parse verbosity %s: %v", opts.verbosity, err)
}
}
formatJSON := false
for _, opt := range rootOpts.logopts {
for _, opt := range opts.logopts {
if opt == "json" {
formatJSON = true
}
}
if formatJSON {
rootOpts.log = slog.New(slog.NewJSONHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: lvl}))
opts.log = slog.New(slog.NewJSONHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: lvl}))
} else {
rootOpts.log = slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: lvl}))
opts.log = slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: lvl}))
}
return nil
}
func (rootOpts *rootCmd) runVersion(cmd *cobra.Command, args []string) error {
info := version.GetInfo()
return template.Writer(cmd.OutOrStdout(), rootOpts.format, info)
}
func (rootOpts *rootCmd) newRegClient() *regclient.RegClient {
func (opts *rootOpts) newRegClient() *regclient.RegClient {
conf, err := ConfigLoadDefault()
if err != nil {
rootOpts.log.Warn("Failed to load default config",
opts.log.Warn("Failed to load default config",
slog.String("err", err.Error()))
if conf == nil {
conf = ConfigNew()
@ -153,11 +157,11 @@ func (rootOpts *rootCmd) newRegClient() *regclient.RegClient {
}
rcOpts := []regclient.Opt{
regclient.WithSlog(rootOpts.log),
regclient.WithSlog(opts.log),
regclient.WithRegOpts(reg.WithCache(time.Minute*5, 500)),
}
if rootOpts.userAgent != "" {
rcOpts = append(rcOpts, regclient.WithUserAgent(rootOpts.userAgent))
if opts.userAgent != "" {
rcOpts = append(rcOpts, regclient.WithUserAgent(opts.userAgent))
} else {
info := version.GetInfo()
if info.VCSTag != "" {
@ -184,10 +188,10 @@ func (rootOpts *rootCmd) newRegClient() *regclient.RegClient {
host.Name = name
rcHosts = append(rcHosts, *host)
}
for _, h := range rootOpts.hosts {
for _, h := range opts.hosts {
hKV, err := strparse.SplitCSKV(h)
if err != nil {
rootOpts.log.Warn("unable to parse host string",
opts.log.Warn("unable to parse host string",
slog.String("host", h),
slog.String("err", err.Error()))
}
@ -200,7 +204,7 @@ func (rootOpts *rootCmd) newRegClient() *regclient.RegClient {
var hostTLS config.TLSConf
err := hostTLS.UnmarshalText([]byte(hKV["tls"]))
if err != nil {
rootOpts.log.Warn("unable to parse tls setting",
opts.log.Warn("unable to parse tls setting",
slog.String("host", h),
slog.String("tls", hKV["tls"]),
slog.String("err", err.Error()))
@ -217,6 +221,11 @@ func (rootOpts *rootCmd) newRegClient() *regclient.RegClient {
return regclient.New(rcOpts...)
}
func (opts *versionOpts) runVersion(cmd *cobra.Command, args []string) error {
info := version.GetInfo()
return template.Writer(cmd.OutOrStdout(), opts.format, info)
}
func flagChanged(cmd *cobra.Command, name string) bool {
flag := cmd.Flags().Lookup(name)
if flag == nil {

View File

@ -12,8 +12,8 @@ import (
"github.com/regclient/regclient/types/ref"
)
type tagCmd struct {
rootOpts *rootCmd
type tagOpts struct {
rootOpts *rootOpts
limit int
last string
include []string
@ -21,15 +21,21 @@ type tagCmd struct {
format string
}
func NewTagCmd(rootOpts *rootCmd) *cobra.Command {
tagOpts := tagCmd{
rootOpts: rootOpts,
}
var tagTopCmd = &cobra.Command{
func NewTagCmd(rOpts *rootOpts) *cobra.Command {
cmd := &cobra.Command{
Use: "tag <cmd>",
Short: "manage tags",
}
var tagDeleteCmd = &cobra.Command{
cmd.AddCommand(newTagDeleteCmd(rOpts))
cmd.AddCommand(newTagLsCmd(rOpts))
return cmd
}
func newTagDeleteCmd(rOpts *rootOpts) *cobra.Command {
opts := tagOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "delete <image_ref>",
Aliases: []string{"del", "rm", "remove"},
Short: "delete a tag in a repo",
@ -42,10 +48,17 @@ If the registry does not support the delete API, the dummy manifest will remain.
# delete a tag
regctl tag delete registry.example.org/repo:v42`,
Args: cobra.ExactArgs(1),
ValidArgsFunction: rootOpts.completeArgTag,
RunE: tagOpts.runTagDelete,
ValidArgsFunction: rOpts.completeArgTag,
RunE: opts.runTagDelete,
}
var tagLsCmd = &cobra.Command{
return cmd
}
func newTagLsCmd(rOpts *rootOpts) *cobra.Command {
opts := tagOpts{
rootOpts: rOpts,
}
cmd := &cobra.Command{
Use: "ls <repository>",
Aliases: []string{"list"},
Short: "list tags in a repo",
@ -60,34 +73,31 @@ regctl tag ls registry.example.org/repo
regctl tag ls registry.example.org/repo --exclude 'sha256-.*'`,
Args: cobra.ExactArgs(1),
ValidArgs: []string{},
RunE: tagOpts.runTagLs,
RunE: opts.runTagLs,
}
tagLsCmd.Flags().StringVarP(&tagOpts.last, "last", "", "", "Specify the last tag from a previous request for pagination (depends on registry support)")
_ = tagLsCmd.RegisterFlagCompletionFunc("last", completeArgNone)
tagLsCmd.Flags().IntVarP(&tagOpts.limit, "limit", "", 0, "Specify the number of tags to retrieve (depends on registry support)")
_ = tagLsCmd.RegisterFlagCompletionFunc("limit", completeArgNone)
tagLsCmd.Flags().StringArrayVar(&tagOpts.include, "include", []string{}, "Regexp of tags to include (expression is bound to beginning and ending of tag)")
_ = tagLsCmd.RegisterFlagCompletionFunc("include", completeArgNone)
tagLsCmd.Flags().StringArrayVar(&tagOpts.exclude, "exclude", []string{}, "Regexp of tags to exclude (expression is bound to beginning and ending of tag)")
_ = tagLsCmd.RegisterFlagCompletionFunc("exclude", completeArgNone)
tagLsCmd.Flags().StringVarP(&tagOpts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
_ = tagLsCmd.RegisterFlagCompletionFunc("format", completeArgNone)
tagTopCmd.AddCommand(tagDeleteCmd)
tagTopCmd.AddCommand(tagLsCmd)
return tagTopCmd
cmd.Flags().StringArrayVar(&opts.exclude, "exclude", []string{}, "Regexp of tags to exclude (expression is bound to beginning and ending of tag)")
_ = cmd.RegisterFlagCompletionFunc("exclude", completeArgNone)
cmd.Flags().StringVarP(&opts.format, "format", "", "{{printPretty .}}", "Format output with go template syntax")
_ = cmd.RegisterFlagCompletionFunc("format", completeArgNone)
cmd.Flags().StringArrayVar(&opts.include, "include", []string{}, "Regexp of tags to include (expression is bound to beginning and ending of tag)")
_ = cmd.RegisterFlagCompletionFunc("include", completeArgNone)
cmd.Flags().StringVarP(&opts.last, "last", "", "", "Specify the last tag from a previous request for pagination (depends on registry support)")
_ = cmd.RegisterFlagCompletionFunc("last", completeArgNone)
cmd.Flags().IntVarP(&opts.limit, "limit", "", 0, "Specify the number of tags to retrieve (depends on registry support)")
_ = cmd.RegisterFlagCompletionFunc("limit", completeArgNone)
return cmd
}
func (tagOpts *tagCmd) runTagDelete(cmd *cobra.Command, args []string) error {
func (opts *tagOpts) runTagDelete(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])
if err != nil {
return err
}
rc := tagOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
tagOpts.rootOpts.log.Debug("Delete tag",
opts.rootOpts.log.Debug("Delete tag",
slog.String("host", r.Registry),
slog.String("repository", r.Repository),
slog.String("tag", r.Tag))
@ -98,7 +108,7 @@ func (tagOpts *tagCmd) runTagDelete(cmd *cobra.Command, args []string) error {
return nil
}
func (tagOpts *tagCmd) runTagLs(cmd *cobra.Command, args []string) error {
func (opts *tagOpts) runTagLs(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])
if err != nil {
@ -106,33 +116,33 @@ func (tagOpts *tagCmd) runTagLs(cmd *cobra.Command, args []string) error {
}
reInclude := []*regexp.Regexp{}
reExclude := []*regexp.Regexp{}
for _, expr := range tagOpts.include {
for _, expr := range opts.include {
re, err := regexp.Compile("^" + expr + "$")
if err != nil {
return fmt.Errorf("failed to parse regexp \"%s\": %w", expr, err)
}
reInclude = append(reInclude, re)
}
for _, expr := range tagOpts.exclude {
for _, expr := range opts.exclude {
re, err := regexp.Compile("^" + expr + "$")
if err != nil {
return fmt.Errorf("failed to parse regexp \"%s\": %w", expr, err)
}
reExclude = append(reExclude, re)
}
rc := tagOpts.rootOpts.newRegClient()
rc := opts.rootOpts.newRegClient()
defer rc.Close(ctx, r)
tagOpts.rootOpts.log.Debug("Listing tags",
opts.rootOpts.log.Debug("Listing tags",
slog.String("host", r.Registry),
slog.String("repository", r.Repository))
opts := []scheme.TagOpts{}
if tagOpts.limit != 0 {
opts = append(opts, scheme.WithTagLimit(tagOpts.limit))
sOpts := []scheme.TagOpts{}
if opts.limit != 0 {
sOpts = append(sOpts, scheme.WithTagLimit(opts.limit))
}
if tagOpts.last != "" {
opts = append(opts, scheme.WithTagLast(tagOpts.last))
if opts.last != "" {
sOpts = append(sOpts, scheme.WithTagLast(opts.last))
}
tl, err := rc.TagList(ctx, r, opts...)
tl, err := rc.TagList(ctx, r, sOpts...)
if err != nil {
return err
}
@ -161,13 +171,13 @@ func (tagOpts *tagCmd) runTagLs(cmd *cobra.Command, args []string) error {
}
tl.Tags = filtered
}
switch tagOpts.format {
switch opts.format {
case "raw":
tagOpts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .RawBody}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}{{printf \"\\n%s\" .RawBody}}"
case "rawBody", "raw-body", "body":
tagOpts.format = "{{printf \"%s\" .RawBody}}"
opts.format = "{{printf \"%s\" .RawBody}}"
case "rawHeaders", "raw-headers", "headers":
tagOpts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
opts.format = "{{ range $key,$vals := .RawHeaders}}{{range $val := $vals}}{{printf \"%s: %s\\n\" $key $val }}{{end}}{{end}}"
}
return template.Writer(cmd.OutOrStdout(), tagOpts.format, tl)
return template.Writer(cmd.OutOrStdout(), opts.format, tl)
}

View File

@ -698,7 +698,7 @@ defaults:
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
// run each test
rootOpts := rootCmd{
rootOpts := rootOpts{
conf: conf,
rc: rc,
throttle: pq,
@ -811,7 +811,7 @@ func TestProcessRef(t *testing.T) {
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
rootOpts := rootCmd{
rootOpts := rootOpts{
rc: rc,
conf: &Config{
Sync: []ConfigSync{cs},

View File

@ -53,7 +53,7 @@ const (
// This is separate from the concurrency limits in regclient itself.
type throttle struct{}
type rootCmd struct {
type rootOpts struct {
confFile string
verbosity string
logopts []string
@ -65,24 +65,26 @@ type rootCmd struct {
throttle *pqueue.Queue[throttle]
}
func NewRootCmd() (*cobra.Command, *rootCmd) {
var rootTopCmd = &cobra.Command{
func NewRootCmd() (*cobra.Command, *rootOpts) {
opts := rootOpts{}
var cmd = &cobra.Command{
Use: "regsync <cmd>",
Short: "Utility for mirroring docker repositories",
Long: `Utility for mirroring docker repositories
More details at <https://github.com/regclient/regclient>`,
SilenceUsage: true,
SilenceErrors: true,
}
rootOpts := rootCmd{
log: slog.New(slog.NewTextHandler(rootTopCmd.ErrOrStderr(), &slog.HandlerOptions{Level: slog.LevelInfo})),
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: opts.rootPreRun,
}
cmd.PersistentFlags().StringVarP(&opts.verbosity, "verbosity", "v", slog.LevelInfo.String(), "Log level (trace, debug, info, warn, error)")
cmd.PersistentFlags().StringArrayVar(&opts.logopts, "logopt", []string{}, "Log options")
var serverCmd = &cobra.Command{
Use: "server",
Short: "run the regsync server",
Long: `Sync registries according to the configuration.`,
Args: cobra.RangeArgs(0, 0),
RunE: rootOpts.runServer,
RunE: opts.runServer,
}
var checkCmd = &cobra.Command{
Use: "check",
@ -92,7 +94,7 @@ Manifests are checked to see if a copy is needed, but only log, skip copying.
No jobs are run in parallel, and the command returns after any error or last
sync step is finished.`,
Args: cobra.RangeArgs(0, 0),
RunE: rootOpts.runCheck,
RunE: opts.runCheck,
}
var onceCmd = &cobra.Command{
Use: "once",
@ -101,15 +103,20 @@ sync step is finished.`,
No jobs are run in parallel, and the command returns after any error or last
sync step is finished.`,
Args: cobra.RangeArgs(0, 0),
RunE: rootOpts.runOnce,
RunE: opts.runOnce,
}
onceCmd.Flags().BoolVar(&opts.missing, "missing", false, "Only copy tags that are missing on target")
var configCmd = &cobra.Command{
Use: "config",
Short: "Show the config",
Long: `Show the config`,
Args: cobra.RangeArgs(0, 0),
RunE: rootOpts.runConfig,
RunE: opts.runConfig,
}
for _, curCmd := range []*cobra.Command{serverCmd, checkCmd, onceCmd, configCmd} {
curCmd.Flags().StringVarP(&opts.confFile, "config", "c", "", "Config file")
_ = curCmd.MarkFlagFilename("config")
_ = curCmd.MarkFlagRequired("config")
}
var versionCmd = &cobra.Command{
@ -117,91 +124,84 @@ sync step is finished.`,
Short: "Show the version",
Long: `Show the version`,
Args: cobra.RangeArgs(0, 0),
RunE: rootOpts.runVersion,
RunE: opts.runVersion,
}
versionCmd.Flags().StringVar(&opts.format, "format", "{{printPretty .}}", "Format output with go template syntax")
_ = versionCmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp
})
rootTopCmd.PersistentFlags().StringVarP(&rootOpts.confFile, "config", "c", "", "Config file")
rootTopCmd.PersistentFlags().StringVarP(&rootOpts.verbosity, "verbosity", "v", slog.LevelInfo.String(), "Log level (trace, debug, info, warn, error)")
rootTopCmd.PersistentFlags().StringArrayVar(&rootOpts.logopts, "logopt", []string{}, "Log options")
versionCmd.Flags().StringVar(&rootOpts.format, "format", "{{printPretty .}}", "Format output with go template syntax")
onceCmd.Flags().BoolVar(&rootOpts.missing, "missing", false, "Only copy tags that are missing on target")
_ = rootTopCmd.MarkPersistentFlagFilename("config")
_ = serverCmd.MarkPersistentFlagRequired("config")
_ = checkCmd.MarkPersistentFlagRequired("config")
_ = onceCmd.MarkPersistentFlagRequired("config")
_ = configCmd.MarkPersistentFlagRequired("config")
rootTopCmd.AddCommand(serverCmd)
rootTopCmd.AddCommand(checkCmd)
rootTopCmd.AddCommand(onceCmd)
rootTopCmd.AddCommand(configCmd)
rootTopCmd.AddCommand(versionCmd)
rootTopCmd.AddCommand(cobradoc.NewCmd(rootTopCmd.Name(), "cli-doc"))
rootTopCmd.PersistentPreRunE = rootOpts.rootPreRun
return rootTopCmd, &rootOpts
opts.log = slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: slog.LevelInfo}))
cmd.AddCommand(
serverCmd,
checkCmd,
onceCmd,
configCmd,
versionCmd,
cobradoc.NewCmd(cmd.Name(), "cli-doc"),
)
return cmd, &opts
}
func (rootOpts *rootCmd) rootPreRun(cmd *cobra.Command, args []string) error {
func (opts *rootOpts) rootPreRun(cmd *cobra.Command, args []string) error {
var lvl slog.Level
err := lvl.UnmarshalText([]byte(rootOpts.verbosity))
err := lvl.UnmarshalText([]byte(opts.verbosity))
if err != nil {
// handle custom levels
if rootOpts.verbosity == strings.ToLower("trace") {
if opts.verbosity == strings.ToLower("trace") {
lvl = types.LevelTrace
} else {
return fmt.Errorf("unable to parse verbosity %s: %v", rootOpts.verbosity, err)
return fmt.Errorf("unable to parse verbosity %s: %v", opts.verbosity, err)
}
}
formatJSON := false
for _, opt := range rootOpts.logopts {
for _, opt := range opts.logopts {
if opt == "json" {
formatJSON = true
}
}
if formatJSON {
rootOpts.log = slog.New(slog.NewJSONHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: lvl}))
opts.log = slog.New(slog.NewJSONHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: lvl}))
} else {
rootOpts.log = slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: lvl}))
opts.log = slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{Level: lvl}))
}
return nil
}
func (rootOpts *rootCmd) runVersion(cmd *cobra.Command, args []string) error {
func (opts *rootOpts) runVersion(cmd *cobra.Command, args []string) error {
info := version.GetInfo()
return template.Writer(os.Stdout, rootOpts.format, info)
return template.Writer(os.Stdout, opts.format, info)
}
// runConfig processes the file in one pass, ignoring cron
func (rootOpts *rootCmd) runConfig(cmd *cobra.Command, args []string) error {
err := rootOpts.loadConf()
func (opts *rootOpts) runConfig(cmd *cobra.Command, args []string) error {
err := opts.loadConf()
if err != nil {
return err
}
return ConfigWrite(rootOpts.conf, cmd.OutOrStdout())
return ConfigWrite(opts.conf, cmd.OutOrStdout())
}
// runOnce processes the file in one pass, ignoring cron
func (rootOpts *rootCmd) runOnce(cmd *cobra.Command, args []string) error {
err := rootOpts.loadConf()
func (opts *rootOpts) runOnce(cmd *cobra.Command, args []string) error {
err := opts.loadConf()
if err != nil {
return err
}
action := actionCopy
if rootOpts.missing {
if opts.missing {
action = actionMissing
}
ctx := cmd.Context()
var wg sync.WaitGroup
var mainErr error
for _, s := range rootOpts.conf.Sync {
if rootOpts.conf.Defaults.Parallel > 0 {
for _, s := range opts.conf.Sync {
if opts.conf.Defaults.Parallel > 0 {
wg.Add(1)
go func() {
defer wg.Done()
err := rootOpts.process(ctx, s, action)
err := opts.process(ctx, s, action)
if err != nil {
if mainErr == nil {
mainErr = err
@ -210,7 +210,7 @@ func (rootOpts *rootCmd) runOnce(cmd *cobra.Command, args []string) error {
}
}()
} else {
err := rootOpts.process(ctx, s, action)
err := opts.process(ctx, s, action)
if err != nil {
if mainErr == nil {
mainErr = err
@ -223,8 +223,8 @@ func (rootOpts *rootCmd) runOnce(cmd *cobra.Command, args []string) error {
}
// runServer stays running with cron scheduled tasks
func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
err := rootOpts.loadConf()
func (opts *rootOpts) runServer(cmd *cobra.Command, args []string) error {
err := opts.loadConf()
if err != nil {
return err
}
@ -235,31 +235,31 @@ func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
c := cron.New(cron.WithChain(
cron.SkipIfStillRunning(cron.DefaultLogger),
))
for _, s := range rootOpts.conf.Sync {
for _, s := range opts.conf.Sync {
sched := s.Schedule
if sched == "" && s.Interval != 0 {
sched = "@every " + s.Interval.String()
}
if sched != "" {
rootOpts.log.Debug("Scheduled task",
opts.log.Debug("Scheduled task",
slog.String("source", s.Source),
slog.String("target", s.Target),
slog.String("type", s.Type),
slog.String("sched", sched))
_, errCron := c.AddFunc(sched, func() {
rootOpts.log.Debug("Running task",
opts.log.Debug("Running task",
slog.String("source", s.Source),
slog.String("target", s.Target),
slog.String("type", s.Type))
wg.Add(1)
defer wg.Done()
err := rootOpts.process(ctx, s, actionCopy)
err := opts.process(ctx, s, actionCopy)
if mainErr == nil {
mainErr = err
}
})
if errCron != nil {
rootOpts.log.Error("Failed to schedule cron",
opts.log.Error("Failed to schedule cron",
slog.String("source", s.Source),
slog.String("target", s.Target),
slog.String("sched", sched),
@ -269,11 +269,11 @@ func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
}
}
// immediately copy any images that are missing from target
if rootOpts.conf.Defaults.Parallel > 0 {
if opts.conf.Defaults.Parallel > 0 {
wg.Add(1)
go func() {
defer wg.Done()
err := rootOpts.process(ctx, s, actionMissing)
err := opts.process(ctx, s, actionMissing)
if err != nil {
if mainErr == nil {
mainErr = err
@ -282,7 +282,7 @@ func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
}
}()
} else {
err := rootOpts.process(ctx, s, actionMissing)
err := opts.process(ctx, s, actionMissing)
if err != nil {
if mainErr == nil {
mainErr = err
@ -290,7 +290,7 @@ func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
}
}
} else {
rootOpts.log.Error("No schedule or interval found, ignoring",
opts.log.Error("No schedule or interval found, ignoring",
slog.String("source", s.Source),
slog.String("target", s.Target),
slog.String("type", s.Type))
@ -304,24 +304,24 @@ func (rootOpts *rootCmd) runServer(cmd *cobra.Command, args []string) error {
if done != nil {
<-done
}
rootOpts.log.Info("Stopping server")
opts.log.Info("Stopping server")
// clean shutdown
c.Stop()
rootOpts.log.Debug("Waiting on running tasks")
opts.log.Debug("Waiting on running tasks")
wg.Wait()
return mainErr
}
// run check is used for a dry-run
func (rootOpts *rootCmd) runCheck(cmd *cobra.Command, args []string) error {
err := rootOpts.loadConf()
func (opts *rootOpts) runCheck(cmd *cobra.Command, args []string) error {
err := opts.loadConf()
if err != nil {
return err
}
var mainErr error
ctx := cmd.Context()
for _, s := range rootOpts.conf.Sync {
err := rootOpts.process(ctx, s, actionCheck)
for _, s := range opts.conf.Sync {
err := opts.process(ctx, s, actionCheck)
if err != nil {
if mainErr == nil {
mainErr = err
@ -331,20 +331,20 @@ func (rootOpts *rootCmd) runCheck(cmd *cobra.Command, args []string) error {
return mainErr
}
func (rootOpts *rootCmd) loadConf() error {
func (opts *rootOpts) loadConf() error {
var err error
if rootOpts.confFile == "-" {
rootOpts.conf, err = ConfigLoadReader(os.Stdin)
if opts.confFile == "-" {
opts.conf, err = ConfigLoadReader(os.Stdin)
if err != nil {
return err
}
} else if rootOpts.confFile != "" {
r, err := os.Open(rootOpts.confFile)
} else if opts.confFile != "" {
r, err := os.Open(opts.confFile)
if err != nil {
return err
}
defer r.Close()
rootOpts.conf, err = ConfigLoadReader(r)
opts.conf, err = ConfigLoadReader(r)
if err != nil {
return err
}
@ -352,28 +352,28 @@ func (rootOpts *rootCmd) loadConf() error {
return ErrMissingInput
}
// use a throttle to control parallelism
concurrent := rootOpts.conf.Defaults.Parallel
concurrent := opts.conf.Defaults.Parallel
if concurrent <= 0 {
concurrent = 1
}
rootOpts.log.Debug("Configuring parallel settings",
opts.log.Debug("Configuring parallel settings",
slog.Int("concurrent", concurrent))
rootOpts.throttle = pqueue.New(pqueue.Opts[throttle]{Max: concurrent})
opts.throttle = pqueue.New(pqueue.Opts[throttle]{Max: concurrent})
// set the regclient, loading docker creds unless disabled, and inject logins from config file
rcOpts := []regclient.Opt{
regclient.WithSlog(rootOpts.log),
regclient.WithSlog(opts.log),
}
if rootOpts.conf.Defaults.BlobLimit != 0 {
rcOpts = append(rcOpts, regclient.WithRegOpts(reg.WithBlobLimit(rootOpts.conf.Defaults.BlobLimit)))
if opts.conf.Defaults.BlobLimit != 0 {
rcOpts = append(rcOpts, regclient.WithRegOpts(reg.WithBlobLimit(opts.conf.Defaults.BlobLimit)))
}
if rootOpts.conf.Defaults.CacheCount > 0 && rootOpts.conf.Defaults.CacheTime > 0 {
rcOpts = append(rcOpts, regclient.WithRegOpts(reg.WithCache(rootOpts.conf.Defaults.CacheTime, rootOpts.conf.Defaults.CacheCount)))
if opts.conf.Defaults.CacheCount > 0 && opts.conf.Defaults.CacheTime > 0 {
rcOpts = append(rcOpts, regclient.WithRegOpts(reg.WithCache(opts.conf.Defaults.CacheTime, opts.conf.Defaults.CacheCount)))
}
if !rootOpts.conf.Defaults.SkipDockerConf {
if !opts.conf.Defaults.SkipDockerConf {
rcOpts = append(rcOpts, regclient.WithDockerCreds(), regclient.WithDockerCerts())
}
if rootOpts.conf.Defaults.UserAgent != "" {
rcOpts = append(rcOpts, regclient.WithUserAgent(rootOpts.conf.Defaults.UserAgent))
if opts.conf.Defaults.UserAgent != "" {
rcOpts = append(rcOpts, regclient.WithUserAgent(opts.conf.Defaults.UserAgent))
} else {
info := version.GetInfo()
if info.VCSTag != "" {
@ -383,9 +383,9 @@ func (rootOpts *rootCmd) loadConf() error {
}
}
rcHosts := []config.Host{}
for _, host := range rootOpts.conf.Creds {
for _, host := range opts.conf.Creds {
if host.Scheme != "" {
rootOpts.log.Warn("Scheme is deprecated, for http set TLS to disabled",
opts.log.Warn("Scheme is deprecated, for http set TLS to disabled",
slog.String("name", host.Name))
}
rcHosts = append(rcHosts, host)
@ -393,27 +393,27 @@ func (rootOpts *rootCmd) loadConf() error {
if len(rcHosts) > 0 {
rcOpts = append(rcOpts, regclient.WithConfigHost(rcHosts...))
}
rootOpts.rc = regclient.New(rcOpts...)
opts.rc = regclient.New(rcOpts...)
return nil
}
// process a sync step
func (rootOpts *rootCmd) process(ctx context.Context, s ConfigSync, action actionType) error {
func (opts *rootOpts) process(ctx context.Context, s ConfigSync, action actionType) error {
switch s.Type {
case "registry":
if err := rootOpts.processRegistry(ctx, s, s.Source, s.Target, action); err != nil {
if err := opts.processRegistry(ctx, s, s.Source, s.Target, action); err != nil {
return err
}
case "repository":
if err := rootOpts.processRepo(ctx, s, s.Source, s.Target, action); err != nil {
if err := opts.processRepo(ctx, s, s.Source, s.Target, action); err != nil {
return err
}
case "image":
if err := rootOpts.processImage(ctx, s, s.Source, s.Target, action); err != nil {
if err := opts.processImage(ctx, s, s.Source, s.Target, action); err != nil {
return err
}
default:
rootOpts.log.Error("Type not recognized, must be one of: registry, repository, or image",
opts.log.Error("Type not recognized, must be one of: registry, repository, or image",
slog.Any("step", s),
slog.String("type", s.Type))
return ErrInvalidInput
@ -421,7 +421,7 @@ func (rootOpts *rootCmd) process(ctx context.Context, s ConfigSync, action actio
return nil
}
func (rootOpts *rootCmd) processRegistry(ctx context.Context, s ConfigSync, src, tgt string, action actionType) error {
func (opts *rootOpts) processRegistry(ctx context.Context, s ConfigSync, src, tgt string, action actionType) error {
last := ""
var retErr error
for {
@ -429,16 +429,16 @@ func (rootOpts *rootCmd) processRegistry(ctx context.Context, s ConfigSync, src,
if last != "" {
repoOpts = append(repoOpts, scheme.WithRepoLast(last))
}
sRepos, err := rootOpts.rc.RepoList(ctx, src, repoOpts...)
sRepos, err := opts.rc.RepoList(ctx, src, repoOpts...)
if err != nil {
rootOpts.log.Error("Failed to list source repositories",
opts.log.Error("Failed to list source repositories",
slog.String("source", src),
slog.String("error", err.Error()))
return err
}
sRepoList, err := sRepos.GetRepos()
if err != nil {
rootOpts.log.Error("Failed to list source repositories",
opts.log.Error("Failed to list source repositories",
slog.String("source", src),
slog.String("error", err.Error()))
return err
@ -450,7 +450,7 @@ func (rootOpts *rootCmd) processRegistry(ctx context.Context, s ConfigSync, src,
// filter repos according to allow/deny rules
sRepoList, err = filterList(s.Repos, sRepoList)
if err != nil {
rootOpts.log.Error("Failed processing repo filters",
opts.log.Error("Failed processing repo filters",
slog.String("source", src),
slog.Any("allow", s.Repos.Allow),
slog.Any("deny", s.Repos.Deny),
@ -458,7 +458,7 @@ func (rootOpts *rootCmd) processRegistry(ctx context.Context, s ConfigSync, src,
return err
}
for _, repo := range sRepoList {
if err := rootOpts.processRepo(ctx, s, fmt.Sprintf("%s/%s", src, repo), fmt.Sprintf("%s/%s", tgt, repo), action); err != nil {
if err := opts.processRepo(ctx, s, fmt.Sprintf("%s/%s", src, repo), fmt.Sprintf("%s/%s", tgt, repo), action); err != nil {
retErr = err
}
}
@ -466,31 +466,31 @@ func (rootOpts *rootCmd) processRegistry(ctx context.Context, s ConfigSync, src,
return retErr
}
func (rootOpts *rootCmd) processRepo(ctx context.Context, s ConfigSync, src, tgt string, action actionType) error {
func (opts *rootOpts) processRepo(ctx context.Context, s ConfigSync, src, tgt string, action actionType) error {
sRepoRef, err := ref.New(src)
if err != nil {
rootOpts.log.Error("Failed parsing source",
opts.log.Error("Failed parsing source",
slog.String("source", src),
slog.String("error", err.Error()))
return err
}
sTags, err := rootOpts.rc.TagList(ctx, sRepoRef)
sTags, err := opts.rc.TagList(ctx, sRepoRef)
if err != nil {
rootOpts.log.Error("Failed getting source tags",
opts.log.Error("Failed getting source tags",
slog.String("source", sRepoRef.CommonName()),
slog.String("error", err.Error()))
return err
}
sTagsList, err := sTags.GetTags()
if err != nil {
rootOpts.log.Error("Failed getting source tags",
opts.log.Error("Failed getting source tags",
slog.String("source", sRepoRef.CommonName()),
slog.String("error", err.Error()))
return err
}
sTagList, err := filterList(s.Tags, sTagsList)
if err != nil {
rootOpts.log.Error("Failed processing tag filters",
opts.log.Error("Failed processing tag filters",
slog.String("source", sRepoRef.CommonName()),
slog.Any("allow", s.Tags.Allow),
slog.Any("deny", s.Tags.Deny),
@ -498,7 +498,7 @@ func (rootOpts *rootCmd) processRepo(ctx context.Context, s ConfigSync, src, tgt
return err
}
if len(sTagList) == 0 {
rootOpts.log.Warn("No matching tags found",
opts.log.Warn("No matching tags found",
slog.String("source", sRepoRef.CommonName()),
slog.Any("allow", s.Tags.Allow),
slog.Any("deny", s.Tags.Deny),
@ -509,14 +509,14 @@ func (rootOpts *rootCmd) processRepo(ctx context.Context, s ConfigSync, src, tgt
if action == actionMissing {
tRepoRef, err := ref.New(tgt)
if err != nil {
rootOpts.log.Error("Failed parsing target",
opts.log.Error("Failed parsing target",
slog.String("target", tgt),
slog.String("error", err.Error()))
return err
}
tTags, err := rootOpts.rc.TagList(ctx, tRepoRef)
tTags, err := opts.rc.TagList(ctx, tRepoRef)
if err != nil {
rootOpts.log.Debug("Failed getting target tags",
opts.log.Debug("Failed getting target tags",
slog.String("target", tRepoRef.CommonName()),
slog.String("error", err.Error()))
}
@ -524,7 +524,7 @@ func (rootOpts *rootCmd) processRepo(ctx context.Context, s ConfigSync, src, tgt
if err == nil {
tTagList, err = tTags.GetTags()
if err != nil {
rootOpts.log.Debug("Failed getting target tags",
opts.log.Debug("Failed getting target tags",
slog.String("target", tRepoRef.CommonName()),
slog.String("error", err.Error()))
}
@ -542,7 +542,7 @@ func (rootOpts *rootCmd) processRepo(ctx context.Context, s ConfigSync, src, tgt
case 1:
sI--
default:
rootOpts.log.Warn("strings.Compare unexpected result",
opts.log.Warn("strings.Compare unexpected result",
slog.Int("result", strings.Compare(sTagList[sI], tTagList[tI])),
slog.String("left", sTagList[sI]),
slog.String("right", tTagList[tI]))
@ -553,37 +553,37 @@ func (rootOpts *rootCmd) processRepo(ctx context.Context, s ConfigSync, src, tgt
}
var retErr error
for _, tag := range sTagList {
if err := rootOpts.processImage(ctx, s, fmt.Sprintf("%s:%s", src, tag), fmt.Sprintf("%s:%s", tgt, tag), action); err != nil {
if err := opts.processImage(ctx, s, fmt.Sprintf("%s:%s", src, tag), fmt.Sprintf("%s:%s", tgt, tag), action); err != nil {
retErr = err
}
}
return retErr
}
func (rootOpts *rootCmd) processImage(ctx context.Context, s ConfigSync, src, tgt string, action actionType) error {
func (opts *rootOpts) processImage(ctx context.Context, s ConfigSync, src, tgt string, action actionType) error {
sRef, err := ref.New(src)
if err != nil {
rootOpts.log.Error("Failed parsing source",
opts.log.Error("Failed parsing source",
slog.String("source", src),
slog.String("error", err.Error()))
return err
}
tRef, err := ref.New(tgt)
if err != nil {
rootOpts.log.Error("Failed parsing target",
opts.log.Error("Failed parsing target",
slog.String("target", tgt),
slog.String("error", err.Error()))
return err
}
err = rootOpts.processRef(ctx, s, sRef, tRef, action)
err = opts.processRef(ctx, s, sRef, tRef, action)
if err != nil {
rootOpts.log.Error("Failed to sync",
opts.log.Error("Failed to sync",
slog.String("target", tRef.CommonName()),
slog.String("source", sRef.CommonName()),
slog.String("error", err.Error()))
}
if err := rootOpts.rc.Close(ctx, tRef); err != nil {
rootOpts.log.Error("Error closing ref",
if err := opts.rc.Close(ctx, tRef); err != nil {
opts.log.Error("Error closing ref",
slog.String("ref", tRef.CommonName()),
slog.String("error", err.Error()))
}
@ -591,13 +591,13 @@ func (rootOpts *rootCmd) processImage(ctx context.Context, s ConfigSync, src, tg
}
// process a sync step
func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt ref.Ref, action actionType) error {
mSrc, err := rootOpts.rc.ManifestHead(ctx, src, regclient.WithManifestRequireDigest())
func (opts *rootOpts) processRef(ctx context.Context, s ConfigSync, src, tgt ref.Ref, action actionType) error {
mSrc, err := opts.rc.ManifestHead(ctx, src, regclient.WithManifestRequireDigest())
if err != nil && errors.Is(err, errs.ErrUnsupportedAPI) {
mSrc, err = rootOpts.rc.ManifestGet(ctx, src)
mSrc, err = opts.rc.ManifestGet(ctx, src)
}
if err != nil {
rootOpts.log.Error("Failed to lookup source manifest",
opts.log.Error("Failed to lookup source manifest",
slog.String("source", src.CommonName()),
slog.String("error", err.Error()))
return err
@ -606,20 +606,20 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
forceRecursive := (s.ForceRecursive != nil && *s.ForceRecursive)
referrers := (s.Referrers != nil && *s.Referrers)
digestTags := (s.DigestTags != nil && *s.DigestTags)
mTgt, err := rootOpts.rc.ManifestHead(ctx, tgt, regclient.WithManifestRequireDigest())
mTgt, err := opts.rc.ManifestHead(ctx, tgt, regclient.WithManifestRequireDigest())
tgtExists := (err == nil)
tgtMatches := false
if err == nil && manifest.GetDigest(mSrc).String() == manifest.GetDigest(mTgt).String() {
tgtMatches = true
}
if tgtMatches && (fastCheck || (!forceRecursive && !referrers && !digestTags)) {
rootOpts.log.Debug("Image matches",
opts.log.Debug("Image matches",
slog.String("source", src.CommonName()),
slog.String("target", tgt.CommonName()))
return nil
}
if tgtExists && action == actionMissing {
rootOpts.log.Debug("target exists",
opts.log.Debug("target exists",
slog.String("source", src.CommonName()),
slog.String("target", tgt.CommonName()))
return nil
@ -628,7 +628,7 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
// skip when source manifest is an unsupported type
smt := manifest.GetMediaType(mSrc)
if !slices.Contains(s.MediaTypes, smt) {
rootOpts.log.Info("Skipping unsupported media type",
opts.log.Info("Skipping unsupported media type",
slog.String("ref", src.CommonName()),
slog.String("mediaType", manifest.GetMediaType(mSrc)),
slog.Any("allowed", s.MediaTypes))
@ -637,7 +637,7 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
// if platform is defined and source is a list, resolve the source platform
if mSrc.IsList() && s.Platform != "" {
platDigest, err := rootOpts.getPlatformDigest(ctx, src, s.Platform, mSrc)
platDigest, err := opts.getPlatformDigest(ctx, src, s.Platform, mSrc)
if err != nil {
return err
}
@ -646,7 +646,7 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
tgtMatches = true
}
if tgtMatches && (s.ForceRecursive == nil || !*s.ForceRecursive) {
rootOpts.log.Debug("Image matches for platform",
opts.log.Debug("Image matches for platform",
slog.String("source", src.CommonName()),
slog.String("platform", s.Platform),
slog.String("target", tgt.CommonName()))
@ -654,14 +654,14 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
}
}
if tgtMatches {
rootOpts.log.Info("Image refreshing",
opts.log.Info("Image refreshing",
slog.String("source", src.CommonName()),
slog.String("target", tgt.CommonName()),
slog.Bool("forced", forceRecursive),
slog.Bool("digestTags", digestTags),
slog.Bool("referrers", referrers))
} else {
rootOpts.log.Info("Image sync needed",
opts.log.Info("Image sync needed",
slog.String("source", src.CommonName()),
slog.String("target", tgt.CommonName()))
}
@ -670,16 +670,16 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
}
// wait for parallel tasks
throttleDone, err := rootOpts.throttle.Acquire(ctx, throttle{})
throttleDone, err := opts.throttle.Acquire(ctx, throttle{})
if err != nil {
return fmt.Errorf("failed to acquire throttle: %w", err)
}
// delay for rate limit on source
if s.RateLimit.Min > 0 && manifest.GetRateLimit(mSrc).Set {
// refresh current rate limit after acquiring throttle
mSrc, err = rootOpts.rc.ManifestHead(ctx, src)
mSrc, err = opts.rc.ManifestHead(ctx, src)
if err != nil {
rootOpts.log.Error("rate limit check failed",
opts.log.Error("rate limit check failed",
slog.String("source", src.CommonName()),
slog.String("error", err.Error()))
throttleDone()
@ -689,7 +689,7 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
rlSrc := manifest.GetRateLimit(mSrc)
for rlSrc.Remain < s.RateLimit.Min {
throttleDone()
rootOpts.log.Info("Delaying for rate limit",
opts.log.Info("Delaying for rate limit",
slog.String("source", src.CommonName()),
slog.Int("source-remain", rlSrc.Remain),
slog.Int("source-limit", rlSrc.Limit),
@ -700,13 +700,13 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
return ErrCanceled
case <-time.After(s.RateLimit.Retry):
}
throttleDone, err = rootOpts.throttle.Acquire(ctx, throttle{})
throttleDone, err = opts.throttle.Acquire(ctx, throttle{})
if err != nil {
return fmt.Errorf("failed to reacquire throttle: %w", err)
}
mSrc, err = rootOpts.rc.ManifestHead(ctx, src)
mSrc, err = opts.rc.ManifestHead(ctx, src)
if err != nil {
rootOpts.log.Error("rate limit check failed",
opts.log.Error("rate limit check failed",
slog.String("source", src.CommonName()),
slog.String("error", err.Error()))
throttleDone()
@ -714,7 +714,7 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
}
rlSrc = manifest.GetRateLimit(mSrc)
}
rootOpts.log.Debug("Rate limit passed",
opts.log.Debug("Rate limit passed",
slog.String("source", src.CommonName()),
slog.Int("source-remain", rlSrc.Remain),
slog.Int("step-min", s.RateLimit.Min))
@ -738,7 +738,7 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
}{Ref: tgt, Step: s, Sync: s}
backupStr, err := template.String(s.Backup, data)
if err != nil {
rootOpts.log.Error("Failed to expand backup template",
opts.log.Error("Failed to expand backup template",
slog.String("original", tgt.CommonName()),
slog.String("backup-template", s.Backup),
slog.String("error", err.Error()))
@ -750,7 +750,7 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
// if the : or / are in the string, parse it as a full reference
backupRef, err = ref.New(backupStr)
if err != nil {
rootOpts.log.Error("Failed to parse backup reference",
opts.log.Error("Failed to parse backup reference",
slog.String("original", tgt.CommonName()),
slog.String("template", s.Backup),
slog.String("backup", backupStr),
@ -761,15 +761,15 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
// else parse backup string as just a tag
backupRef = backupRef.SetTag(backupStr)
}
defer rootOpts.rc.Close(ctx, backupRef)
defer opts.rc.Close(ctx, backupRef)
// run copy from tgt ref to backup ref
rootOpts.log.Info("Saving backup",
opts.log.Info("Saving backup",
slog.String("original", tgt.CommonName()),
slog.String("backup", backupRef.CommonName()))
err = rootOpts.rc.ImageCopy(ctx, tgt, backupRef)
err = opts.rc.ImageCopy(ctx, tgt, backupRef)
if err != nil {
// Possible registry corruption with existing image, only warn and continue/overwrite
rootOpts.log.Warn("Failed to backup existing image",
opts.log.Warn("Failed to backup existing image",
slog.String("original", tgt.CommonName()),
slog.String("template", s.Backup),
slog.String("backup", backupRef.CommonName()),
@ -777,13 +777,13 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
}
}
opts := []regclient.ImageOpts{}
rcOpts := []regclient.ImageOpts{}
if s.DigestTags != nil && *s.DigestTags {
opts = append(opts, regclient.ImageWithDigestTags())
rcOpts = append(rcOpts, regclient.ImageWithDigestTags())
}
if s.Referrers != nil && *s.Referrers {
if len(s.ReferrerFilters) == 0 {
opts = append(opts, regclient.ImageWithReferrers())
rcOpts = append(rcOpts, regclient.ImageWithReferrers())
} else {
for _, filter := range s.ReferrerFilters {
rOpts := []scheme.ReferrerOpts{}
@ -793,48 +793,48 @@ func (rootOpts *rootCmd) processRef(ctx context.Context, s ConfigSync, src, tgt
if filter.Annotations != nil {
rOpts = append(rOpts, scheme.WithReferrerMatchOpt(descriptor.MatchOpt{Annotations: filter.Annotations}))
}
opts = append(opts, regclient.ImageWithReferrers(rOpts...))
rcOpts = append(rcOpts, regclient.ImageWithReferrers(rOpts...))
}
}
if s.ReferrerSrc != "" {
referrerSrc, err := ref.New(s.ReferrerSrc)
if err != nil {
rootOpts.log.Error("failed to parse referrer source reference",
opts.log.Error("failed to parse referrer source reference",
slog.String("referrerSource", s.ReferrerSrc),
slog.String("error", err.Error()))
}
opts = append(opts, regclient.ImageWithReferrerSrc(referrerSrc))
rcOpts = append(rcOpts, regclient.ImageWithReferrerSrc(referrerSrc))
}
if s.ReferrerTgt != "" {
referrerTgt, err := ref.New(s.ReferrerTgt)
if err != nil {
rootOpts.log.Error("failed to parse referrer target reference",
opts.log.Error("failed to parse referrer target reference",
slog.String("referrerTarget", s.ReferrerTgt),
slog.String("error", err.Error()))
}
opts = append(opts, regclient.ImageWithReferrerTgt(referrerTgt))
rcOpts = append(rcOpts, regclient.ImageWithReferrerTgt(referrerTgt))
}
}
if s.FastCheck != nil && *s.FastCheck {
opts = append(opts, regclient.ImageWithFastCheck())
rcOpts = append(rcOpts, regclient.ImageWithFastCheck())
}
if s.ForceRecursive != nil && *s.ForceRecursive {
opts = append(opts, regclient.ImageWithForceRecursive())
rcOpts = append(rcOpts, regclient.ImageWithForceRecursive())
}
if s.IncludeExternal != nil && *s.IncludeExternal {
opts = append(opts, regclient.ImageWithIncludeExternal())
rcOpts = append(rcOpts, regclient.ImageWithIncludeExternal())
}
if len(s.Platforms) > 0 {
opts = append(opts, regclient.ImageWithPlatforms(s.Platforms))
rcOpts = append(rcOpts, regclient.ImageWithPlatforms(s.Platforms))
}
// Copy the image
rootOpts.log.Debug("Image sync running",
opts.log.Debug("Image sync running",
slog.String("source", src.CommonName()),
slog.String("target", tgt.CommonName()))
err = rootOpts.rc.ImageCopy(ctx, src, tgt, opts...)
err = opts.rc.ImageCopy(ctx, src, tgt, rcOpts...)
if err != nil {
rootOpts.log.Error("Failed to copy image",
opts.log.Error("Failed to copy image",
slog.String("source", src.CommonName()),
slog.String("target", tgt.CommonName()),
slog.String("error", err.Error()))
@ -901,10 +901,10 @@ func init() {
// getPlatformDigest resolves a manifest list to a specific platform's digest
// This uses the above cache to only call ManifestGet when a new manifest list digest is seen
func (rootOpts *rootCmd) getPlatformDigest(ctx context.Context, r ref.Ref, platStr string, origMan manifest.Manifest) (digest.Digest, error) {
func (opts *rootOpts) getPlatformDigest(ctx context.Context, r ref.Ref, platStr string, origMan manifest.Manifest) (digest.Digest, error) {
plat, err := platform.Parse(platStr)
if err != nil {
rootOpts.log.Warn("Could not parse platform",
opts.log.Warn("Could not parse platform",
slog.String("platform", platStr),
slog.String("err", err.Error()))
return "", err
@ -913,9 +913,9 @@ func (rootOpts *rootCmd) getPlatformDigest(ctx context.Context, r ref.Ref, platS
manifestCache.mu.Lock()
getMan, ok := manifestCache.manifests[manifest.GetDigest(origMan).String()]
if !ok {
getMan, err = rootOpts.rc.ManifestGet(ctx, r)
getMan, err = opts.rc.ManifestGet(ctx, r)
if err != nil {
rootOpts.log.Error("Failed to get source manifest",
opts.log.Error("Failed to get source manifest",
slog.String("source", r.CommonName()),
slog.String("error", err.Error()))
manifestCache.mu.Unlock()
@ -931,7 +931,7 @@ func (rootOpts *rootCmd) getPlatformDigest(ctx context.Context, r ref.Ref, platS
for _, p := range pl {
ps = append(ps, p.String())
}
rootOpts.log.Warn("Platform could not be found in source manifest list",
opts.log.Warn("Platform could not be found in source manifest list",
slog.String("platform", plat.String()),
slog.String("err", err.Error()),
slog.String("platforms", strings.Join(ps, ", ")))