From d041811523ea4e4dc5aeb636ab3c252c19a3b5b8 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Tue, 26 May 2026 08:04:48 +0300 Subject: [PATCH] cacher: add 'docker download --pull' and silence cobra usage on errors * docker download --pull: on cache miss, falls back to docker pull + docker save + S3 seed, mirroring the --url fallback on file download. Collapses the if/else cache-or-pull bash dance in CI manifests to a single command. * SilenceUsage on rootCmd: cache-miss / key-not-found exits aren't bad-usage errors, so the cobra Usage block dump on every error was noise. Short error line remains. --- cmd/docker.go | 63 ++++++++++++++++++++++++++++++++++++++++----------- cmd/root.go | 4 ++++ 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/cmd/docker.go b/cmd/docker.go index 1efae1306ead0d967f6ff98816add5cc29e1dd49..0ef88ce066a9214eacf105a32e6b3249b3c54d3a 100644 --- a/cmd/docker.go +++ b/cmd/docker.go @@ -40,11 +40,19 @@ var dockerExistsCmd = &cobra.Command{ }, } +var flagDockerPull bool + var dockerDownloadCmd = &cobra.Command{ Use: "download ", Short: "Pull a cached image from S3 into the local docker daemon", - Long: `Streams s3://bucket/key through zstd-decode into ` + "`docker load`" + `.`, - Args: cobra.ExactArgs(2), + Long: `Streams s3://bucket/key through zstd-decode into ` + "`docker load`" + `. + +With --pull, a cache miss falls back to ` + "`docker pull `" + ` then +seeds the S3 cache via ` + "`docker save`" + `. This makes a single invocation +into the full cache-or-fetch pattern callable from a CI manifest: + + cacher docker download garage/v2.3.0.tar.zst dxflrs/garage:v2.3.0 --pull`, + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { cli, cfg, err := client() if err != nil { @@ -62,22 +70,37 @@ var dockerDownloadCmd = &cobra.Command{ if err != nil { return err } - if !ok { + if ok { + fmt.Fprintf(cmd.ErrOrStderr(), "Cache HIT — %s → load %s\n", key, tag) + body, err := cli.Get(ctx, key) + if err != nil { + return err + } + defer body.Close() + dec, err := zstd.NewReader(body) + if err != nil { + return err + } + defer dec.Close() + return runDockerLoad(ctx, dec) + } + fmt.Fprintf(cmd.ErrOrStderr(), "Cache MISS — %s\n", key) + if !flagDockerPull { return fmt.Errorf("%w: %s", ErrNotFound, key) } - fmt.Fprintf(cmd.ErrOrStderr(), "Cache HIT — %s → load %s\n", key, tag) - - body, err := cli.Get(ctx, key) - if err != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "Pulling %s\n", tag) + if err := runDockerPull(ctx, tag); err != nil { return err } - defer body.Close() - dec, err := zstd.NewReader(body) - if err != nil { - return err + // Image is now in the local daemon. Seed the S3 cache so the + // next run hits. Treat the upload as best-effort — the pull + // already gave us what we needed locally. + if err := saveTagToS3(ctx, cli, key, tag); err != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "warn: seed upload failed: %v\n", err) + return nil } - defer dec.Close() - return runDockerLoad(ctx, dec) + fmt.Fprintf(cmd.ErrOrStderr(), "Cached → %s\n", key) + return nil }, } @@ -120,10 +143,24 @@ func init() { addKeyFlags(c) } dockerUploadCmd.Flags().BoolVar(&flagForce, "force", false, "overwrite if key already exists") + dockerDownloadCmd.Flags().BoolVar(&flagDockerPull, "pull", false, "on cache miss, docker pull and seed S3") dockerCmd.AddCommand(dockerExistsCmd, dockerDownloadCmd, dockerUploadCmd) rootCmd.AddCommand(dockerCmd) } +// runDockerPull invokes `docker pull `, surfacing daemon output as +// a single warn line per chunk so the test log isn't dominated by pull +// progress bars. +func runDockerPull(ctx context.Context, tag string) error { + pull := exec.CommandContext(ctx, "docker", "pull", tag) + pull.Stdout = newWarnWriter("docker pull") + pull.Stderr = newWarnWriter("docker pull") + if err := pull.Run(); err != nil { + return fmt.Errorf("docker pull %s: %w", tag, err) + } + return nil +} + // saveTagToS3 wires `docker save` stdout → zstd encoder → io.Pipe → // s3.Put. The encoder runs in a goroutine writing into the pipe; the // uploader goroutine reads from the pipe. Both ends signal errors back diff --git a/cmd/root.go b/cmd/root.go index 483e1b559e5993f9894ecaea19e412792cd66a62..a57988f856ccdccb981775a721c50d7a98b215fd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,6 +19,10 @@ var ( var rootCmd = &cobra.Command{ Use: "cacher", Short: "S3-backed CI cache helper for builds.sr.ht — replaces .builds/lib/ci-lib.sh", + // Subcommands fail in normal, scriptable ways (cache miss, key absent). + // Dumping the cobra usage block on every such error is noise — the + // short error line is enough. + SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { level := slog.LevelInfo switch flagLogLevel {