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:
parent
b753620f11
commit
288f2c3da0
@ -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})),
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(®istryOpts.formatConf, "format", "{{jsonPretty .}}", "Format output with go template syntax")
|
||||
|
||||
registryLoginCmd.Flags().StringVarP(®istryOpts.user, "user", "u", "", "Username")
|
||||
registryLoginCmd.Flags().StringVarP(®istryOpts.pass, "pass", "p", "", "Password")
|
||||
registryLoginCmd.Flags().BoolVar(®istryOpts.passStdin, "pass-stdin", false, "Read password from stdin")
|
||||
registryLoginCmd.Flags().BoolVar(®istryOpts.skipCheck, "skip-check", false, "Skip checking connectivity to the registry")
|
||||
_ = registryLoginCmd.RegisterFlagCompletionFunc("user", completeArgNone)
|
||||
_ = registryLoginCmd.RegisterFlagCompletionFunc("pass", completeArgNone)
|
||||
|
||||
registrySetCmd.Flags().StringVar(®istryOpts.credHelper, "cred-helper", "", "Credential helper (full binary name, including docker-credential- prefix)")
|
||||
registrySetCmd.Flags().StringVar(®istryOpts.cacert, "cacert", "", "CA Certificate (not a filename, use \"$(cat ca.pem)\" to use a file)")
|
||||
registrySetCmd.Flags().StringVar(®istryOpts.clientCert, "client-cert", "", "Client certificate for mTLS (not a filename, use \"$(cat client.pem)\" to use a file)")
|
||||
registrySetCmd.Flags().StringVar(®istryOpts.clientKey, "client-key", "", "Client key for mTLS (not a filename, use \"$(cat client.key)\" to use a file)")
|
||||
registrySetCmd.Flags().StringVar(®istryOpts.tls, "tls", "", "TLS (enabled, insecure, disabled)")
|
||||
registrySetCmd.Flags().StringVar(®istryOpts.hostname, "hostname", "", "Hostname or ip with port")
|
||||
registrySetCmd.Flags().StringVar(®istryOpts.pathPrefix, "path-prefix", "", "Prefix to all repositories")
|
||||
registrySetCmd.Flags().StringArrayVar(®istryOpts.mirrors, "mirror", nil, "List of mirrors (registry names)")
|
||||
registrySetCmd.Flags().UintVar(®istryOpts.priority, "priority", 0, "Priority (for sorting mirrors)")
|
||||
registrySetCmd.Flags().BoolVar(®istryOpts.repoAuth, "repo-auth", false, "Separate auth requests per repository instead of per registry")
|
||||
registrySetCmd.Flags().Int64Var(®istryOpts.blobChunk, "blob-chunk", 0, "Blob chunk size")
|
||||
registrySetCmd.Flags().Int64Var(®istryOpts.blobMax, "blob-max", 0, "Blob size before switching to chunked push, -1 to disable")
|
||||
registrySetCmd.Flags().Float64Var(®istryOpts.reqPerSec, "req-per-sec", 0, "Requests per second")
|
||||
registrySetCmd.Flags().Int64Var(®istryOpts.reqConcurrent, "req-concurrent", 0, "Concurrent requests")
|
||||
registrySetCmd.Flags().BoolVar(®istryOpts.skipCheck, "skip-check", false, "Skip checking connectivity to the registry")
|
||||
registrySetCmd.Flags().StringArrayVar(®istryOpts.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(®istryOpts.scheme, "scheme", "", "[Deprecated] Scheme (http, https)")
|
||||
registrySetCmd.Flags().StringArrayVar(®istryOpts.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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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},
|
||||
|
@ -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, ", ")))
|
||||
|
Loading…
x
Reference in New Issue
Block a user