mirror of
https://github.com/containers/podman.git
synced 2025-10-17 11:14:40 +08:00
support --digestfile
for remote push
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: #18216 Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
This commit is contained in:
@ -24,6 +24,7 @@ type pushOptionsWrapper struct {
|
||||
SignBySigstoreParamFileCLI string
|
||||
EncryptionKeys []string
|
||||
EncryptLayers []int
|
||||
DigestFile string
|
||||
}
|
||||
|
||||
var (
|
||||
@ -140,7 +141,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)
|
||||
@ -203,5 +203,16 @@ 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 != "" {
|
||||
if err := os.WriteFile(pushOptions.DigestFile, []byte(report.ManifestDigest), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ type manifestPushOptsWrapper struct {
|
||||
CredentialsCLI string
|
||||
SignBySigstoreParamFileCLI string
|
||||
SignPassphraseFileCLI string
|
||||
DigestFile string
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -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)
|
||||
|
@ -4,9 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
@ -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}
|
||||
@ -97,15 +88,14 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
|
||||
password = authconf.Password
|
||||
}
|
||||
options := entities.ImagePushOptions{
|
||||
All: query.All,
|
||||
Authfile: authfile,
|
||||
Compress: query.Compress,
|
||||
Format: query.Format,
|
||||
Password: password,
|
||||
Username: username,
|
||||
DigestFile: digestFile.Name(),
|
||||
Quiet: true,
|
||||
Progress: make(chan types.ProgressProperties),
|
||||
All: query.All,
|
||||
Authfile: authfile,
|
||||
Compress: query.Compress,
|
||||
Format: query.Format,
|
||||
Password: password,
|
||||
Username: username,
|
||||
Quiet: true,
|
||||
Progress: make(chan types.ProgressProperties),
|
||||
}
|
||||
if _, found := r.URL.Query()["tlsVerify"]; found {
|
||||
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
|
||||
@ -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
|
||||
@ -187,23 +180,16 @@ 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"
|
||||
}
|
||||
report.Status = fmt.Sprintf("%s: digest: %s size: %d", tag, string(digestBytes), len(rawManifest))
|
||||
|
||||
var digestStr string
|
||||
if pushReport != nil {
|
||||
digestStr = pushReport.ManifestDigest
|
||||
}
|
||||
report.Status = fmt.Sprintf("%s: digest: %s size: %d", tag, digestStr, len(rawManifest))
|
||||
if err := enc.Encode(report); err != nil {
|
||||
logrus.Warnf("Failed to json encode error %q", err.Error())
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
@ -104,9 +105,10 @@ 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)
|
||||
}()
|
||||
|
||||
flush := func() {
|
||||
@ -131,6 +133,9 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
flush()
|
||||
case <-pushCtx.Done():
|
||||
if pushReport != nil {
|
||||
stream.ManifestDigest = pushReport.ManifestDigest
|
||||
}
|
||||
if pushError != nil {
|
||||
stream.Error = pushError.Error()
|
||||
if err := enc.Encode(stream); err != nil {
|
||||
|
@ -87,6 +87,8 @@ LOOP:
|
||||
switch {
|
||||
case report.Stream != "":
|
||||
fmt.Fprint(writer, report.Stream)
|
||||
case report.ManifestDigest != "":
|
||||
options.ManifestDigest = &report.ManifestDigest
|
||||
case report.Error != "":
|
||||
// There can only be one error.
|
||||
return errors.New(report.Error)
|
||||
|
@ -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.
|
||||
ManifestDigest *string
|
||||
}
|
||||
|
||||
// SearchOptions are optional options for searching images on registries
|
||||
|
@ -182,3 +182,18 @@ func (o *PushOptions) GetQuiet() bool {
|
||||
}
|
||||
return *o.Quiet
|
||||
}
|
||||
|
||||
// WithManifestDigest set field ManifestDigest to given value
|
||||
func (o *PushOptions) WithManifestDigest(value string) *PushOptions {
|
||||
o.ManifestDigest = &value
|
||||
return o
|
||||
}
|
||||
|
||||
// GetManifestDigest returns value of field ManifestDigest
|
||||
func (o *PushOptions) GetManifestDigest() string {
|
||||
if o.ManifestDigest == nil {
|
||||
var z string
|
||||
return z
|
||||
}
|
||||
return *o.ManifestDigest
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
@ -247,9 +244,17 @@ type ImagePushOptions struct {
|
||||
OciEncryptLayers *[]int
|
||||
}
|
||||
|
||||
// ImagePushReport is the response from pushing an image.
|
||||
type ImagePushReport struct {
|
||||
// The digest of the manifest of the pushed image.
|
||||
ManifestDigest string
|
||||
}
|
||||
|
||||
// ImagePushStream is the response from pushing an image. Only used in the
|
||||
// remote API.
|
||||
type ImagePushStream struct {
|
||||
// ManifestDigest is the digest of the manifest of the pushed image.
|
||||
ManifestDigest string `json:"manifestdigest,omitempty"`
|
||||
// Stream used to provide push progress
|
||||
Stream string `json:"stream,omitempty"`
|
||||
// Error contains text of errors from pushing
|
||||
|
@ -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 "":
|
||||
@ -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{}
|
||||
@ -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
|
||||
}
|
||||
@ -338,27 +338,24 @@ 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
|
||||
}
|
||||
manifestDigest, err := manifest.Digest(pushedManifestBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
return &entities.ImagePushReport{ManifestDigest: manifestDigest.String()}, 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{ManifestDigest: pushedManifestString}, nil
|
||||
}
|
||||
return pushError
|
||||
return nil, pushError
|
||||
}
|
||||
|
||||
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error {
|
||||
|
@ -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)
|
||||
@ -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{ManifestDigest: options.GetManifestDigest()}, nil
|
||||
}
|
||||
|
||||
func (ir *ImageEngine) Save(ctx context.Context, nameOrID string, tags []string, opts entities.ImageSaveOptions) error {
|
||||
|
@ -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")
|
||||
|
Reference in New Issue
Block a user