Skip to content

Commit

Permalink
support --digestfile for remote push
Browse files Browse the repository at this point in the history
Wire in support for writing the digest of the pushed image to a
user-specified file.  Requires some massaging of _internal_ APIs
and the extension of the push endpoint to integrate the raw manifest
(i.e., in bytes) in the stream.

Closes: containers#18216
Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
  • Loading branch information
vrothberg committed Apr 20, 2023
1 parent 00fdfa0 commit 14d9440
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 64 deletions.
21 changes: 19 additions & 2 deletions cmd/podman/images/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v4/cmd/podman/common"
"github.com/containers/podman/v4/cmd/podman/registry"
Expand All @@ -24,6 +25,7 @@ type pushOptionsWrapper struct {
SignBySigstoreParamFileCLI string
EncryptionKeys []string
EncryptLayers []int
DigestFile string
}

var (
Expand Down Expand Up @@ -140,7 +142,6 @@ func pushFlags(cmd *cobra.Command) {
if registry.IsRemote() {
_ = flags.MarkHidden("cert-dir")
_ = flags.MarkHidden("compress")
_ = flags.MarkHidden("digestfile")
_ = flags.MarkHidden("quiet")
_ = flags.MarkHidden(signByFlagName)
_ = flags.MarkHidden(signBySigstoreFlagName)
Expand Down Expand Up @@ -203,5 +204,21 @@ func imagePush(cmd *cobra.Command, args []string) error {

// Let's do all the remaining Yoga in the API to prevent us from scattering
// logic across (too) many parts of the code.
return registry.ImageEngine().Push(registry.GetContext(), source, destination, pushOptions.ImagePushOptions)
report, err := registry.ImageEngine().Push(registry.GetContext(), source, destination, pushOptions.ImagePushOptions)
if err != nil {
return err
}

if pushOptions.DigestFile != "" {
manifestDigest, err := manifest.Digest(report.Manifest)
if err != nil {
return err
}

if err := os.WriteFile(pushOptions.DigestFile, []byte(manifestDigest.String()), 0644); err != nil {
return err
}
}

return nil
}
1 change: 1 addition & 0 deletions cmd/podman/manifest/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type manifestPushOptsWrapper struct {
CredentialsCLI string
SignBySigstoreParamFileCLI string
SignPassphraseFileCLI string
DigestFile string
}

var (
Expand Down
1 change: 0 additions & 1 deletion docs/source/markdown/options/digestfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@
#### **--digestfile**=*Digestfile*

After copying the image, write the digest of the resulting image to the file.
(This option is not available with the remote Podman client, including Mac and Windows (excluding WSL2) machines)
32 changes: 9 additions & 23 deletions pkg/api/handlers/compat/images_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/containers/image/v5/types"
Expand All @@ -27,13 +25,6 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)

digestFile, err := os.CreateTemp("", "digest.txt")
if err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("unable to create tempfile: %w", err))
return
}
defer digestFile.Close()

// Now use the ABI implementation to prevent us from having duplicate
// code.
imageEngine := abi.ImageEngine{Libpod: runtime}
Expand Down Expand Up @@ -103,7 +94,6 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
Format: query.Format,
Password: password,
Username: username,
DigestFile: digestFile.Name(),
Quiet: true,
Progress: make(chan types.ProgressProperties),
}
Expand Down Expand Up @@ -138,8 +128,11 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
flush()

pushErrChan := make(chan error)
var pushReport *entities.ImagePushReport
go func() {
pushErrChan <- imageEngine.Push(r.Context(), imageName, destination, options)
var err error
pushReport, err = imageEngine.Push(r.Context(), imageName, destination, options)
pushErrChan <- err
}()

loop: // break out of for/select infinite loop
Expand Down Expand Up @@ -187,22 +180,15 @@ loop: // break out of for/select infinite loop
break loop
}

digestBytes, err := io.ReadAll(digestFile)
if err != nil {
report.Error = &jsonmessage.JSONError{
Message: err.Error(),
}
report.ErrorMessage = err.Error()
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to json encode error %q", err.Error())
}
flush()
break loop
}
tag := query.Tag
if tag == "" {
tag = "latest"
}

var digestBytes []byte
if pushReport != nil {
digestBytes = pushReport.Manifest
}
report.Status = fmt.Sprintf("%s: digest: %s size: %d", tag, string(digestBytes), len(rawManifest))
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to json encode error %q", err.Error())
Expand Down
10 changes: 8 additions & 2 deletions pkg/api/handlers/libpod/images_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ func PushImage(w http.ResponseWriter, r *http.Request) {

// Let's keep thing simple when running in quiet mode and push directly.
if query.Quiet {
if err := imageEngine.Push(r.Context(), source, destination, options); err != nil {
_, err := imageEngine.Push(r.Context(), source, destination, options)
if err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("pushing image %q: %w", destination, err))
return
}
Expand All @@ -104,10 +105,12 @@ func PushImage(w http.ResponseWriter, r *http.Request) {

pushCtx, pushCancel := context.WithCancel(r.Context())
var pushError error
var pushReport *entities.ImagePushReport
go func() {
defer pushCancel()
pushError = imageEngine.Push(pushCtx, source, destination, options)
pushReport, pushError = imageEngine.Push(pushCtx, source, destination, options)
}()
_ = pushReport // FIXME

flush := func() {
if flusher, ok := w.(http.Flusher); ok {
Expand All @@ -131,6 +134,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
}
flush()
case <-pushCtx.Done():
if pushReport != nil {
stream.Manifest = pushReport.Manifest
}
if pushError != nil {
stream.Error = pushError.Error()
if err := enc.Encode(stream); err != nil {
Expand Down
2 changes: 2 additions & 0 deletions pkg/bindings/images/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ LOOP:
switch {
case report.Stream != "":
fmt.Fprint(writer, report.Stream)
case report.Manifest != nil:
options.Manifest = report.Manifest
case report.Error != "":
// There can only be one error.
return errors.New(report.Error)
Expand Down
3 changes: 3 additions & 0 deletions pkg/bindings/images/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ type PushOptions struct {
Username *string `schema:"-"`
// Quiet can be specified to suppress progress when pushing.
Quiet *bool

// Manifest of the pushed image. Set by images.Push.
Manifest []byte
}

// SearchOptions are optional options for searching images on registries
Expand Down
15 changes: 15 additions & 0 deletions pkg/bindings/images/types_push_options.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/domain/entities/engine_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type ImageEngine interface { //nolint:interfacebloat
Mount(ctx context.Context, images []string, options ImageMountOptions) ([]*ImageMountReport, error)
Prune(ctx context.Context, opts ImagePruneOptions) ([]*reports.PruneReport, error)
Pull(ctx context.Context, rawImage string, opts ImagePullOptions) (*ImagePullReport, error)
Push(ctx context.Context, source string, destination string, opts ImagePushOptions) error
Push(ctx context.Context, source string, destination string, opts ImagePushOptions) (*ImagePushReport, error)
Remove(ctx context.Context, images []string, opts ImageRemoveOptions) (*ImageRemoveReport, []error)
Save(ctx context.Context, nameOrID string, tags []string, options ImageSaveOptions) error
Scp(ctx context.Context, src, dst string, parentFlags []string, quiet bool, sshMode ssh.EngineMode) error
Expand Down
10 changes: 7 additions & 3 deletions pkg/domain/entities/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,6 @@ type ImagePushOptions struct {
Username string
// Password for authenticating against the registry.
Password string
// DigestFile, after copying the image, write the digest of the resulting
// image to the file. Ignored for remote calls.
DigestFile string
// Format is the Manifest type (oci, v2s1, or v2s2) to use when pushing an
// image. Default is manifest type of source, with fallbacks.
// Ignored for remote calls.
Expand Down Expand Up @@ -247,9 +244,16 @@ type ImagePushOptions struct {
OciEncryptLayers *[]int
}

// ImagePushReport is the response from pushing an image.
type ImagePushReport struct {
// Manifest of the pushed image in bytes.
Manifest []byte
}

// ImagePushStream is the response from pushing an image. Only used in the
// remote API.
type ImagePushStream struct {
Manifest []byte `json:"manifest,omitempty"`
// Stream used to provide push progress
Stream string `json:"stream,omitempty"`
// Error contains text of errors from pushing
Expand Down
29 changes: 11 additions & 18 deletions pkg/domain/infra/abi/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ func (ir *ImageEngine) Inspect(ctx context.Context, namesOrIDs []string, opts en
return reports, errs, nil
}

func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, options entities.ImagePushOptions) error {
func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, options entities.ImagePushOptions) (*entities.ImagePushReport, error) {
var manifestType string
switch options.Format {
case "":
Expand All @@ -293,7 +293,7 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
case "v2s2", "docker":
manifestType = manifest.DockerV2Schema2MediaType
default:
return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", options.Format)
return nil, fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", options.Format)
}

pushOptions := &libimage.PushOptions{}
Expand All @@ -320,14 +320,14 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
if compressionFormat == "" {
config, err := ir.Libpod.GetConfigNoCopy()
if err != nil {
return err
return nil, err
}
compressionFormat = config.Engine.CompressionFormat
}
if compressionFormat != "" {
algo, err := compression.AlgorithmByName(compressionFormat)
if err != nil {
return err
return nil, err
}
pushOptions.CompressionFormat = &algo
}
Expand All @@ -338,27 +338,20 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri

pushedManifestBytes, pushError := ir.Libpod.LibimageRuntime().Push(ctx, source, destination, pushOptions)
if pushError == nil {
if options.DigestFile != "" {
manifestDigest, err := manifest.Digest(pushedManifestBytes)
if err != nil {
return err
}

if err := os.WriteFile(options.DigestFile, []byte(manifestDigest.String()), 0644); err != nil {
return err
}
}
return nil
return &entities.ImagePushReport{Manifest: pushedManifestBytes}, nil
}
// If the image could not be found, we may be referring to a manifest
// list but could not find a matching image instance in the local
// containers storage. In that case, fall back and attempt to push the
// (entire) manifest.
if _, err := ir.Libpod.LibimageRuntime().LookupManifestList(source); err == nil {
_, err := ir.ManifestPush(ctx, source, destination, options)
return err
pushedManifestString, err := ir.ManifestPush(ctx, source, destination, options)
if err != nil {
return nil, err
}
return &entities.ImagePushReport{Manifest: []byte(pushedManifestString)}, nil
}
return pushError
return nil, pushError
}

func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error {
Expand Down
11 changes: 7 additions & 4 deletions pkg/domain/infra/tunnel/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,12 @@ func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOpti
return images.Import(ir.ClientCtx, f, options)
}

func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, opts entities.ImagePushOptions) error {
func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, opts entities.ImagePushOptions) (*entities.ImagePushReport, error) {
if opts.Signers != nil {
return fmt.Errorf("forwarding Signers is not supported for remote clients")
return nil, fmt.Errorf("forwarding Signers is not supported for remote clients")
}
if opts.OciEncryptConfig != nil {
return fmt.Errorf("encryption is not supported for remote clients")
return nil, fmt.Errorf("encryption is not supported for remote clients")
}

options := new(images.PushOptions)
Expand All @@ -261,7 +261,10 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
options.WithSkipTLSVerify(false)
}
}
return images.Push(ir.ClientCtx, source, destination, options)
if err := images.Push(ir.ClientCtx, source, destination, options); err != nil {
return nil, err
}
return &entities.ImagePushReport{Manifest: options.Manifest}, nil
}

func (ir *ImageEngine) Save(ctx context.Context, nameOrID string, tags []string, opts entities.ImageSaveOptions) error {
Expand Down
18 changes: 8 additions & 10 deletions test/e2e/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,16 +137,14 @@ var _ = Describe("Podman push", func() {
Expect(push).Should(Exit(0))
}

if !IsRemote() { // Remote does not support --digestfile
// Test --digestfile option
digestFile := filepath.Join(podmanTest.TempDir, "digestfile.txt")
push2 := podmanTest.Podman([]string{"push", "--tls-verify=false", "--digestfile=" + digestFile, "--remove-signatures", ALPINE, "localhost:5000/my-alpine"})
push2.WaitWithDefaultTimeout()
fi, err := os.Lstat(digestFile)
Expect(err).ToNot(HaveOccurred())
Expect(fi.Name()).To(Equal("digestfile.txt"))
Expect(push2).Should(Exit(0))
}
// Test --digestfile option
digestFile := filepath.Join(podmanTest.TempDir, "digestfile.txt")
push2 := podmanTest.Podman([]string{"push", "--tls-verify=false", "--digestfile=" + digestFile, "--remove-signatures", ALPINE, "localhost:5000/my-alpine"})
push2.WaitWithDefaultTimeout()
fi, err := os.Lstat(digestFile)
Expect(err).ToNot(HaveOccurred())
Expect(fi.Name()).To(Equal("digestfile.txt"))
Expect(push2).Should(Exit(0))

if !IsRemote() { // Remote does not support signing
By("pushing and pulling with --sign-by-sigstore-private-key")
Expand Down

0 comments on commit 14d9440

Please sign in to comment.