mirror of
https://github.com/containers/podman.git
synced 2025-06-22 18:08:11 +08:00
Merge pull request #15022 from vrothberg/fix-14971
remote push: show copy progress
This commit is contained in:
@ -1,7 +1,6 @@
|
|||||||
package libpod
|
package libpod
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -14,13 +13,11 @@ import (
|
|||||||
"github.com/containers/buildah"
|
"github.com/containers/buildah"
|
||||||
"github.com/containers/common/libimage"
|
"github.com/containers/common/libimage"
|
||||||
"github.com/containers/image/v5/manifest"
|
"github.com/containers/image/v5/manifest"
|
||||||
"github.com/containers/image/v5/types"
|
|
||||||
"github.com/containers/podman/v4/libpod"
|
"github.com/containers/podman/v4/libpod"
|
||||||
"github.com/containers/podman/v4/libpod/define"
|
"github.com/containers/podman/v4/libpod/define"
|
||||||
"github.com/containers/podman/v4/pkg/api/handlers"
|
"github.com/containers/podman/v4/pkg/api/handlers"
|
||||||
"github.com/containers/podman/v4/pkg/api/handlers/utils"
|
"github.com/containers/podman/v4/pkg/api/handlers/utils"
|
||||||
api "github.com/containers/podman/v4/pkg/api/types"
|
api "github.com/containers/podman/v4/pkg/api/types"
|
||||||
"github.com/containers/podman/v4/pkg/auth"
|
|
||||||
"github.com/containers/podman/v4/pkg/domain/entities"
|
"github.com/containers/podman/v4/pkg/domain/entities"
|
||||||
"github.com/containers/podman/v4/pkg/domain/entities/reports"
|
"github.com/containers/podman/v4/pkg/domain/entities/reports"
|
||||||
"github.com/containers/podman/v4/pkg/domain/infra/abi"
|
"github.com/containers/podman/v4/pkg/domain/infra/abi"
|
||||||
@ -416,74 +413,6 @@ func ImagesImport(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.WriteResponse(w, http.StatusOK, report)
|
utils.WriteResponse(w, http.StatusOK, report)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushImage is the handler for the compat http endpoint for pushing images.
|
|
||||||
func PushImage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
|
||||||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
|
||||||
|
|
||||||
query := struct {
|
|
||||||
All bool `schema:"all"`
|
|
||||||
Destination string `schema:"destination"`
|
|
||||||
Format string `schema:"format"`
|
|
||||||
RemoveSignatures bool `schema:"removeSignatures"`
|
|
||||||
TLSVerify bool `schema:"tlsVerify"`
|
|
||||||
}{
|
|
||||||
// This is where you can override the golang default value for one of fields
|
|
||||||
}
|
|
||||||
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
||||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
source := strings.TrimSuffix(utils.GetName(r), "/push") // GetName returns the entire path
|
|
||||||
if _, err := utils.ParseStorageReference(source); err != nil {
|
|
||||||
utils.Error(w, http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
destination := query.Destination
|
|
||||||
if destination == "" {
|
|
||||||
destination = source
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := utils.IsRegistryReference(destination); err != nil {
|
|
||||||
utils.Error(w, http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
authconf, authfile, err := auth.GetCredentials(r)
|
|
||||||
if err != nil {
|
|
||||||
utils.Error(w, http.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer auth.RemoveAuthfile(authfile)
|
|
||||||
var username, password string
|
|
||||||
if authconf != nil {
|
|
||||||
username = authconf.Username
|
|
||||||
password = authconf.Password
|
|
||||||
}
|
|
||||||
options := entities.ImagePushOptions{
|
|
||||||
All: query.All,
|
|
||||||
Authfile: authfile,
|
|
||||||
Format: query.Format,
|
|
||||||
Password: password,
|
|
||||||
Quiet: true,
|
|
||||||
RemoveSignatures: query.RemoveSignatures,
|
|
||||||
Username: username,
|
|
||||||
}
|
|
||||||
if _, found := r.URL.Query()["tlsVerify"]; found {
|
|
||||||
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
|
|
||||||
}
|
|
||||||
|
|
||||||
imageEngine := abi.ImageEngine{Libpod: runtime}
|
|
||||||
if err := imageEngine.Push(context.Background(), source, destination, options); err != nil {
|
|
||||||
utils.Error(w, http.StatusBadRequest, fmt.Errorf("error pushing image %q: %w", destination, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.WriteResponse(w, http.StatusOK, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func CommitContainer(w http.ResponseWriter, r *http.Request) {
|
func CommitContainer(w http.ResponseWriter, r *http.Request) {
|
||||||
var (
|
var (
|
||||||
destImage string
|
destImage string
|
||||||
|
144
pkg/api/handlers/libpod/images_push.go
Normal file
144
pkg/api/handlers/libpod/images_push.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package libpod
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/image/v5/types"
|
||||||
|
"github.com/containers/podman/v4/libpod"
|
||||||
|
"github.com/containers/podman/v4/pkg/api/handlers/utils"
|
||||||
|
api "github.com/containers/podman/v4/pkg/api/types"
|
||||||
|
"github.com/containers/podman/v4/pkg/auth"
|
||||||
|
"github.com/containers/podman/v4/pkg/channel"
|
||||||
|
"github.com/containers/podman/v4/pkg/domain/entities"
|
||||||
|
"github.com/containers/podman/v4/pkg/domain/infra/abi"
|
||||||
|
"github.com/gorilla/schema"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PushImage is the handler for the compat http endpoint for pushing images.
|
||||||
|
func PushImage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
||||||
|
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||||||
|
|
||||||
|
query := struct {
|
||||||
|
All bool `schema:"all"`
|
||||||
|
Destination string `schema:"destination"`
|
||||||
|
Format string `schema:"format"`
|
||||||
|
RemoveSignatures bool `schema:"removeSignatures"`
|
||||||
|
TLSVerify bool `schema:"tlsVerify"`
|
||||||
|
Quiet bool `schema:"quiet"`
|
||||||
|
}{
|
||||||
|
// #14971: older versions did not sent *any* data, so we need
|
||||||
|
// to be quiet by default to remain backwards compatible
|
||||||
|
Quiet: true,
|
||||||
|
}
|
||||||
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
||||||
|
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
source := strings.TrimSuffix(utils.GetName(r), "/push") // GetName returns the entire path
|
||||||
|
if _, err := utils.ParseStorageReference(source); err != nil {
|
||||||
|
utils.Error(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
destination := query.Destination
|
||||||
|
if destination == "" {
|
||||||
|
destination = source
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.IsRegistryReference(destination); err != nil {
|
||||||
|
utils.Error(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authconf, authfile, err := auth.GetCredentials(r)
|
||||||
|
if err != nil {
|
||||||
|
utils.Error(w, http.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer auth.RemoveAuthfile(authfile)
|
||||||
|
|
||||||
|
var username, password string
|
||||||
|
if authconf != nil {
|
||||||
|
username = authconf.Username
|
||||||
|
password = authconf.Password
|
||||||
|
}
|
||||||
|
options := entities.ImagePushOptions{
|
||||||
|
All: query.All,
|
||||||
|
Authfile: authfile,
|
||||||
|
Format: query.Format,
|
||||||
|
Password: password,
|
||||||
|
Quiet: true,
|
||||||
|
RemoveSignatures: query.RemoveSignatures,
|
||||||
|
Username: username,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := r.URL.Query()["tlsVerify"]; found {
|
||||||
|
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
|
||||||
|
}
|
||||||
|
|
||||||
|
imageEngine := abi.ImageEngine{Libpod: runtime}
|
||||||
|
|
||||||
|
// Let's keep thing simple when running in quiet mode and push directly.
|
||||||
|
if query.Quiet {
|
||||||
|
if err := imageEngine.Push(context.Background(), source, destination, options); err != nil {
|
||||||
|
utils.Error(w, http.StatusBadRequest, fmt.Errorf("error pushing image %q: %w", destination, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteResponse(w, http.StatusOK, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := channel.NewWriter(make(chan []byte))
|
||||||
|
defer writer.Close()
|
||||||
|
options.Writer = writer
|
||||||
|
|
||||||
|
pushCtx, pushCancel := context.WithCancel(r.Context())
|
||||||
|
var pushError error
|
||||||
|
go func() {
|
||||||
|
defer pushCancel()
|
||||||
|
pushError = imageEngine.Push(pushCtx, source, destination, options)
|
||||||
|
}()
|
||||||
|
|
||||||
|
flush := func() {
|
||||||
|
if flusher, ok := w.(http.Flusher); ok {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
flush()
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetEscapeHTML(true)
|
||||||
|
for {
|
||||||
|
var report entities.ImagePushReport
|
||||||
|
select {
|
||||||
|
case s := <-writer.Chan():
|
||||||
|
report.Stream = string(s)
|
||||||
|
if err := enc.Encode(report); err != nil {
|
||||||
|
logrus.Warnf("Failed to encode json: %v", err)
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
case <-pushCtx.Done():
|
||||||
|
if pushError != nil {
|
||||||
|
report.Error = pushError.Error()
|
||||||
|
if err := enc.Encode(report); err != nil {
|
||||||
|
logrus.Warnf("Failed to encode json: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
return
|
||||||
|
case <-r.Context().Done():
|
||||||
|
// Client has closed connection
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -730,6 +730,11 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
|
|||||||
// description: Require TLS verification.
|
// description: Require TLS verification.
|
||||||
// type: boolean
|
// type: boolean
|
||||||
// default: true
|
// default: true
|
||||||
|
// - in: query
|
||||||
|
// name: quiet
|
||||||
|
// description: "silences extra stream data on push"
|
||||||
|
// type: boolean
|
||||||
|
// default: true
|
||||||
// - in: header
|
// - in: header
|
||||||
// name: X-Registry-Auth
|
// name: X-Registry-Auth
|
||||||
// type: string
|
// type: string
|
||||||
|
@ -267,46 +267,6 @@ func Import(ctx context.Context, r io.Reader, options *ImportOptions) (*entities
|
|||||||
return &report, response.Process(&report)
|
return &report, response.Process(&report)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push is the binding for libpod's v2 endpoints for push images. Note that
|
|
||||||
// `source` must be a referring to an image in the remote's container storage.
|
|
||||||
// The destination must be a reference to a registry (i.e., of docker transport
|
|
||||||
// or be normalized to one). Other transports are rejected as they do not make
|
|
||||||
// sense in a remote context.
|
|
||||||
func Push(ctx context.Context, source string, destination string, options *PushOptions) error {
|
|
||||||
if options == nil {
|
|
||||||
options = new(PushOptions)
|
|
||||||
}
|
|
||||||
conn, err := bindings.GetClient(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
params, err := options.ToParams()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// SkipTLSVerify is special. We need to delete the param added by
|
|
||||||
// toparams and change the key and flip the bool
|
|
||||||
if options.SkipTLSVerify != nil {
|
|
||||||
params.Del("SkipTLSVerify")
|
|
||||||
params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify()))
|
|
||||||
}
|
|
||||||
params.Set("destination", destination)
|
|
||||||
|
|
||||||
path := fmt.Sprintf("/images/%s/push", source)
|
|
||||||
response, err := conn.DoRequest(ctx, nil, http.MethodPost, path, params, header)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
return response.Process(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search is the binding for libpod's v2 endpoints for Search images.
|
// Search is the binding for libpod's v2 endpoints for Search images.
|
||||||
func Search(ctx context.Context, term string, options *SearchOptions) ([]entities.ImageSearchReport, error) {
|
func Search(ctx context.Context, term string, options *SearchOptions) ([]entities.ImageSearchReport, error) {
|
||||||
if options == nil {
|
if options == nil {
|
||||||
|
96
pkg/bindings/images/push.go
Normal file
96
pkg/bindings/images/push.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
imageTypes "github.com/containers/image/v5/types"
|
||||||
|
"github.com/containers/podman/v4/pkg/auth"
|
||||||
|
"github.com/containers/podman/v4/pkg/bindings"
|
||||||
|
"github.com/containers/podman/v4/pkg/domain/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Push is the binding for libpod's endpoints for push images. Note that
|
||||||
|
// `source` must be a referring to an image in the remote's container storage.
|
||||||
|
// The destination must be a reference to a registry (i.e., of docker transport
|
||||||
|
// or be normalized to one). Other transports are rejected as they do not make
|
||||||
|
// sense in a remote context.
|
||||||
|
func Push(ctx context.Context, source string, destination string, options *PushOptions) error {
|
||||||
|
if options == nil {
|
||||||
|
options = new(PushOptions)
|
||||||
|
}
|
||||||
|
conn, err := bindings.GetClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := options.ToParams()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// SkipTLSVerify is special. We need to delete the param added by
|
||||||
|
// toparams and change the key and flip the bool
|
||||||
|
if options.SkipTLSVerify != nil {
|
||||||
|
params.Del("SkipTLSVerify")
|
||||||
|
params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify()))
|
||||||
|
}
|
||||||
|
params.Set("destination", destination)
|
||||||
|
|
||||||
|
path := fmt.Sprintf("/images/%s/push", source)
|
||||||
|
response, err := conn.DoRequest(ctx, nil, http.MethodPost, path, params, header)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if !response.IsSuccess() {
|
||||||
|
return response.Process(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Historically push writes status to stderr
|
||||||
|
writer := io.Writer(os.Stderr)
|
||||||
|
if options.GetQuiet() {
|
||||||
|
writer = ioutil.Discard
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := json.NewDecoder(response.Body)
|
||||||
|
for {
|
||||||
|
var report entities.ImagePushReport
|
||||||
|
if err := dec.Decode(&report); err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-response.Request.Context().Done():
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// non-blocking select
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case report.Stream != "":
|
||||||
|
fmt.Fprint(writer, report.Stream)
|
||||||
|
case report.Error != "":
|
||||||
|
// There can only be one error.
|
||||||
|
return errors.New(report.Error)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("failed to parse push results stream, unexpected input: %v", report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -133,6 +133,8 @@ type PushOptions struct {
|
|||||||
RemoveSignatures *bool
|
RemoveSignatures *bool
|
||||||
// Username for authenticating against the registry.
|
// Username for authenticating against the registry.
|
||||||
Username *string
|
Username *string
|
||||||
|
// Quiet can be specified to suppress progress when pushing.
|
||||||
|
Quiet *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:generate go run ../generator/generator.go SearchOptions
|
//go:generate go run ../generator/generator.go SearchOptions
|
||||||
|
@ -136,3 +136,18 @@ func (o *PushOptions) GetUsername() string {
|
|||||||
}
|
}
|
||||||
return *o.Username
|
return *o.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithQuiet set field Quiet to given value
|
||||||
|
func (o *PushOptions) WithQuiet(value bool) *PushOptions {
|
||||||
|
o.Quiet = &value
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuiet returns value of field Quiet
|
||||||
|
func (o *PushOptions) GetQuiet() bool {
|
||||||
|
if o.Quiet == nil {
|
||||||
|
var z bool
|
||||||
|
return z
|
||||||
|
}
|
||||||
|
return *o.Quiet
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -192,8 +193,7 @@ type ImagePushOptions struct {
|
|||||||
// image. Default is manifest type of source, with fallbacks.
|
// image. Default is manifest type of source, with fallbacks.
|
||||||
// Ignored for remote calls.
|
// Ignored for remote calls.
|
||||||
Format string
|
Format string
|
||||||
// Quiet can be specified to suppress pull progress when pulling. Ignored
|
// Quiet can be specified to suppress push progress when pushing.
|
||||||
// for remote calls.
|
|
||||||
Quiet bool
|
Quiet bool
|
||||||
// Rm indicates whether to remove the manifest list if push succeeds
|
// Rm indicates whether to remove the manifest list if push succeeds
|
||||||
Rm bool
|
Rm bool
|
||||||
@ -211,6 +211,17 @@ type ImagePushOptions struct {
|
|||||||
Progress chan types.ProgressProperties
|
Progress chan types.ProgressProperties
|
||||||
// CompressionFormat is the format to use for the compression of the blobs
|
// CompressionFormat is the format to use for the compression of the blobs
|
||||||
CompressionFormat string
|
CompressionFormat string
|
||||||
|
// Writer is used to display copy information including progress bars.
|
||||||
|
Writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImagePushReport is the response from pushing an image.
|
||||||
|
// Currently only used in the remote API.
|
||||||
|
type ImagePushReport struct {
|
||||||
|
// Stream used to provide push progress
|
||||||
|
Stream string `json:"stream,omitempty"`
|
||||||
|
// Error contains text of errors from pushing
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageSearchOptions are the arguments for searching images.
|
// ImageSearchOptions are the arguments for searching images.
|
||||||
|
@ -305,6 +305,7 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
|
|||||||
pushOptions.RemoveSignatures = options.RemoveSignatures
|
pushOptions.RemoveSignatures = options.RemoveSignatures
|
||||||
pushOptions.SignBy = options.SignBy
|
pushOptions.SignBy = options.SignBy
|
||||||
pushOptions.InsecureSkipTLSVerify = options.SkipTLSVerify
|
pushOptions.InsecureSkipTLSVerify = options.SkipTLSVerify
|
||||||
|
pushOptions.Writer = options.Writer
|
||||||
|
|
||||||
compressionFormat := options.CompressionFormat
|
compressionFormat := options.CompressionFormat
|
||||||
if compressionFormat == "" {
|
if compressionFormat == "" {
|
||||||
@ -322,7 +323,7 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
|
|||||||
pushOptions.CompressionFormat = &algo
|
pushOptions.CompressionFormat = &algo
|
||||||
}
|
}
|
||||||
|
|
||||||
if !options.Quiet {
|
if !options.Quiet && pushOptions.Writer == nil {
|
||||||
pushOptions.Writer = os.Stderr
|
pushOptions.Writer = os.Stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +240,7 @@ func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOpti
|
|||||||
|
|
||||||
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) error {
|
||||||
options := new(images.PushOptions)
|
options := new(images.PushOptions)
|
||||||
options.WithAll(opts.All).WithCompress(opts.Compress).WithUsername(opts.Username).WithPassword(opts.Password).WithAuthfile(opts.Authfile).WithFormat(opts.Format).WithRemoveSignatures(opts.RemoveSignatures)
|
options.WithAll(opts.All).WithCompress(opts.Compress).WithUsername(opts.Username).WithPassword(opts.Password).WithAuthfile(opts.Authfile).WithFormat(opts.Format).WithRemoveSignatures(opts.RemoveSignatures).WithQuiet(opts.Quiet)
|
||||||
|
|
||||||
if s := opts.SkipTLSVerify; s != types.OptionalBoolUndefined {
|
if s := opts.SkipTLSVerify; s != types.OptionalBoolUndefined {
|
||||||
if s == types.OptionalBoolTrue {
|
if s == types.OptionalBoolTrue {
|
||||||
|
@ -116,15 +116,26 @@ var _ = Describe("Podman push", func() {
|
|||||||
push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"})
|
push := podmanTest.Podman([]string{"push", "-q", "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"})
|
||||||
push.WaitWithDefaultTimeout()
|
push.WaitWithDefaultTimeout()
|
||||||
Expect(push).Should(Exit(0))
|
Expect(push).Should(Exit(0))
|
||||||
|
Expect(len(push.ErrorToString())).To(Equal(0))
|
||||||
|
|
||||||
SkipIfRemote("Remote does not support --digestfile")
|
push = podmanTest.Podman([]string{"push", "--tls-verify=false", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"})
|
||||||
// Test --digestfile option
|
push.WaitWithDefaultTimeout()
|
||||||
push2 := podmanTest.Podman([]string{"push", "--tls-verify=false", "--digestfile=/tmp/digestfile.txt", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"})
|
Expect(push).Should(Exit(0))
|
||||||
push2.WaitWithDefaultTimeout()
|
output := push.ErrorToString()
|
||||||
fi, err := os.Lstat("/tmp/digestfile.txt")
|
Expect(output).To(ContainSubstring("Copying blob "))
|
||||||
Expect(err).To(BeNil())
|
Expect(output).To(ContainSubstring("Copying config "))
|
||||||
Expect(fi.Name()).To(Equal("digestfile.txt"))
|
Expect(output).To(ContainSubstring("Writing manifest to image destination"))
|
||||||
Expect(push2).Should(Exit(0))
|
Expect(output).To(ContainSubstring("Storing signatures"))
|
||||||
|
|
||||||
|
if !IsRemote() { // Remote does not support --digestfile
|
||||||
|
// Test --digestfile option
|
||||||
|
push2 := podmanTest.Podman([]string{"push", "--tls-verify=false", "--digestfile=/tmp/digestfile.txt", "--remove-signatures", ALPINE, "localhost:5000/my-alpine"})
|
||||||
|
push2.WaitWithDefaultTimeout()
|
||||||
|
fi, err := os.Lstat("/tmp/digestfile.txt")
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(fi.Name()).To(Equal("digestfile.txt"))
|
||||||
|
Expect(push2).Should(Exit(0))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
It("podman push to local registry with authorization", func() {
|
It("podman push to local registry with authorization", func() {
|
||||||
|
Reference in New Issue
Block a user