diff options
Diffstat (limited to 'pkg')
43 files changed, 1380 insertions, 261 deletions
diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index 616f0a138..38fe0196a 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -2,6 +2,7 @@ package compat import ( "encoding/json" + "errors" "fmt" "net/http" "sort" @@ -27,7 +28,6 @@ import ( "github.com/docker/go-connections/nat" "github.com/docker/go-units" "github.com/gorilla/schema" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -46,7 +46,7 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { } if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } @@ -73,7 +73,7 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { name := utils.GetName(r) reports, err := containerEngine.ContainerRm(r.Context(), []string{name}, options) if err != nil { - if errors.Cause(err) == define.ErrNoSuchCtr { + if errors.Is(err, define.ErrNoSuchCtr) { utils.ContainerNotFound(w, name, err) return } @@ -83,7 +83,7 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { } if len(reports) > 0 && reports[0].Err != nil { err = reports[0].Err - if errors.Cause(err) == define.ErrNoSuchCtr { + if errors.Is(err, define.ErrNoSuchCtr) { utils.ContainerNotFound(w, name, err) return } @@ -110,12 +110,12 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { filterMap, err := util.PrepareFilters(r) if err != nil { - utils.Error(w, http.StatusInternalServerError, errors.Wrapf(err, "failed to decode filter parameters for %s", r.URL.String())) + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to decode filter parameters for %s: %w", r.URL.String(), err)) return } if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusInternalServerError, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } @@ -164,7 +164,7 @@ func ListContainers(w http.ResponseWriter, r *http.Request) { for _, ctnr := range containers { api, err := LibpodToContainer(ctnr, query.Size) if err != nil { - if errors.Cause(err) == define.ErrNoSuchCtr { + if errors.Is(err, define.ErrNoSuchCtr) { // container was removed between the initial fetch of the list and conversion logrus.Debugf("Container %s removed between initial fetch and conversion, ignoring in output", ctnr.ID()) continue @@ -187,7 +187,7 @@ func GetContainer(w http.ResponseWriter, r *http.Request) { } if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } @@ -215,7 +215,7 @@ func KillContainer(w http.ResponseWriter, r *http.Request) { Signal: "KILL", } if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } @@ -228,12 +228,12 @@ func KillContainer(w http.ResponseWriter, r *http.Request) { } report, err := containerEngine.ContainerKill(r.Context(), []string{name}, options) if err != nil { - if errors.Cause(err) == define.ErrCtrStateInvalid || - errors.Cause(err) == define.ErrCtrStopped { + if errors.Is(err, define.ErrCtrStateInvalid) || + errors.Is(err, define.ErrCtrStopped) { utils.Error(w, http.StatusConflict, err) return } - if errors.Cause(err) == define.ErrNoSuchCtr { + if errors.Is(err, define.ErrNoSuchCtr) { utils.ContainerNotFound(w, name, err) return } @@ -289,8 +289,10 @@ func LibpodToContainer(l *libpod.Container, sz bool) (*handlers.Container, error return nil, err } stateStr := state.String() - if stateStr == "configured" { - stateStr = "created" + + // Some docker states are not the same as ours. This makes sure the state string stays true to the Docker API + if state == define.ContainerStateCreated { + stateStr = define.ContainerStateConfigured.String() } switch state { @@ -420,9 +422,9 @@ func LibpodToContainerJSON(l *libpod.Container, sz bool) (*types.ContainerJSON, state.Running = true } - // docker calls the configured state "created" - if state.Status == define.ContainerStateConfigured.String() { - state.Status = define.ContainerStateCreated.String() + // Dockers created state is our configured state + if state.Status == define.ContainerStateCreated.String() { + state.Status = define.ContainerStateConfigured.String() } if l.HasHealthCheck() && state.Status != "created" { @@ -510,7 +512,7 @@ func LibpodToContainerJSON(l *libpod.Container, sz bool) (*types.ContainerJSON, for ep := range inspect.HostConfig.PortBindings { splitp := strings.SplitN(ep, "/", 2) if len(splitp) != 2 { - return nil, errors.Errorf("PORT/PROTOCOL Format required for %q", ep) + return nil, fmt.Errorf("PORT/PROTOCOL Format required for %q", ep) } exposedPort, err := nat.NewPort(splitp[1], splitp[0]) if err != nil { @@ -614,7 +616,7 @@ func RenameContainer(w http.ResponseWriter, r *http.Request) { Name string `schema:"name"` }{} if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } @@ -625,7 +627,7 @@ func RenameContainer(w http.ResponseWriter, r *http.Request) { } if _, err := runtime.RenameContainer(r.Context(), ctr, query.Name); err != nil { - if errors.Cause(err) == define.ErrPodExists || errors.Cause(err) == define.ErrCtrExists { + if errors.Is(err, define.ErrPodExists) || errors.Is(err, define.ErrCtrExists) { utils.Error(w, http.StatusConflict, err) return } diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index a8a50ae58..b71217efa 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -21,7 +21,9 @@ import ( 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/reports" "github.com/containers/podman/v4/pkg/domain/infra/abi" + domainUtils "github.com/containers/podman/v4/pkg/domain/utils" "github.com/containers/podman/v4/pkg/errorhandling" "github.com/containers/podman/v4/pkg/util" utils2 "github.com/containers/podman/v4/utils" @@ -613,10 +615,11 @@ func ImagesBatchRemove(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - All bool `schema:"all"` - Force bool `schema:"force"` - Ignore bool `schema:"ignore"` - Images []string `schema:"images"` + All bool `schema:"all"` + Force bool `schema:"force"` + Ignore bool `schema:"ignore"` + LookupManifest bool `schema:"lookupManifest"` + Images []string `schema:"images"` }{} if err := decoder.Decode(&query, r.URL.Query()); err != nil { @@ -624,7 +627,7 @@ func ImagesBatchRemove(w http.ResponseWriter, r *http.Request) { return } - opts := entities.ImageRemoveOptions{All: query.All, Force: query.Force, Ignore: query.Ignore} + opts := entities.ImageRemoveOptions{All: query.All, Force: query.Force, Ignore: query.Ignore, LookupManifest: query.LookupManifest} imageEngine := abi.ImageEngine{Libpod: runtime} rmReport, rmErrors := imageEngine.Remove(r.Context(), query.Images, opts) strErrs := errorhandling.ErrorsToStrings(rmErrors) @@ -637,7 +640,8 @@ func ImagesRemove(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - Force bool `schema:"force"` + Force bool `schema:"force"` + LookupManifest bool `schema:"lookupManifest"` }{ Force: false, } @@ -647,7 +651,7 @@ func ImagesRemove(w http.ResponseWriter, r *http.Request) { return } - opts := entities.ImageRemoveOptions{Force: query.Force} + opts := entities.ImageRemoveOptions{Force: query.Force, LookupManifest: query.LookupManifest} imageEngine := abi.ImageEngine{Libpod: runtime} rmReport, rmErrors := imageEngine.Remove(r.Context(), []string{utils.GetName(r)}, opts) @@ -670,3 +674,32 @@ func ImagesRemove(w http.ResponseWriter, r *http.Request) { utils.Error(w, http.StatusInternalServerError, errorhandling.JoinErrors(rmErrors)) } } + +func ImageScp(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) + query := struct { + Destination string `schema:"destination"` + Quiet bool `schema:"quiet"` + }{ + // 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, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + sourceArg := utils.GetName(r) + + rep, source, dest, _, err := domainUtils.ExecuteTransfer(sourceArg, query.Destination, []string{}, query.Quiet) + if err != nil { + utils.Error(w, http.StatusInternalServerError, err) + return + } + + if source != nil || dest != nil { + utils.Error(w, http.StatusBadRequest, errors.Wrapf(define.ErrInvalidArg, "cannot use the user transfer function on the remote client")) + return + } + + utils.WriteResponse(w, http.StatusOK, &reports.ScpReport{Id: rep.Names[0]}) +} diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 5b92358fa..92fd94390 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -2,6 +2,7 @@ package libpod import ( "encoding/json" + "errors" "fmt" "net/http" "strings" @@ -19,7 +20,6 @@ import ( "github.com/containers/podman/v4/pkg/specgenutil" "github.com/containers/podman/v4/pkg/util" "github.com/gorilla/schema" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -33,11 +33,11 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { ) psg := specgen.PodSpecGenerator{InfraContainerSpec: &specgen.SpecGenerator{}} if err := json.NewDecoder(r.Body).Decode(&psg); err != nil { - utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, failedToDecodeSpecgen)) + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("%v: %w", failedToDecodeSpecgen, err)) return } if err != nil { - utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, failedToDecodeSpecgen)) + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("%v: %w", failedToDecodeSpecgen, err)) return } if !psg.NoInfra { @@ -51,17 +51,17 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { } err = specgenutil.FillOutSpecGen(psg.InfraContainerSpec, &infraOptions, []string{}) // necessary for default values in many cases (userns, idmappings) if err != nil { - utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "error filling out specgen")) + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("error filling out specgen: %w", err)) return } out, err := json.Marshal(psg) // marshal our spec so the matching options can be unmarshaled into infra if err != nil { - utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, failedToDecodeSpecgen)) + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("%v: %w", failedToDecodeSpecgen, err)) return } err = json.Unmarshal(out, psg.InfraContainerSpec) // unmarhal matching options if err != nil { - utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, failedToDecodeSpecgen)) + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("%v: %w", failedToDecodeSpecgen, err)) return } // a few extra that do not have the same json tags @@ -75,10 +75,10 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { pod, err := generate.MakePod(&podSpecComplete, runtime) if err != nil { httpCode := http.StatusInternalServerError - if errors.Cause(err) == define.ErrPodExists { + if errors.Is(err, define.ErrPodExists) { httpCode = http.StatusConflict } - utils.Error(w, httpCode, errors.Wrap(err, "failed to make pod")) + utils.Error(w, httpCode, fmt.Errorf("failed to make pod: %w", err)) return } utils.WriteResponse(w, http.StatusCreated, entities.IDResponse{ID: pod.ID()}) @@ -89,7 +89,7 @@ func Pods(w http.ResponseWriter, r *http.Request) { filterMap, err := util.PrepareFilters(r) if err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } @@ -139,7 +139,7 @@ func PodStop(w http.ResponseWriter, r *http.Request) { } if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } name := utils.GetName(r) @@ -164,7 +164,7 @@ func PodStop(w http.ResponseWriter, r *http.Request) { } else { responses, stopError = pod.Stop(r.Context(), false) } - if stopError != nil && errors.Cause(stopError) != define.ErrPodPartialFail { + if stopError != nil && !errors.Is(stopError, define.ErrPodPartialFail) { utils.Error(w, http.StatusInternalServerError, err) return } @@ -178,7 +178,7 @@ func PodStop(w http.ResponseWriter, r *http.Request) { report := entities.PodStopReport{Id: pod.ID()} for id, err := range responses { - report.Errs = append(report.Errs, errors.Wrapf(err, "error stopping container %s", id)) + report.Errs = append(report.Errs, fmt.Errorf("error stopping container %s: %w", id, err)) } code := http.StatusOK @@ -207,14 +207,14 @@ func PodStart(w http.ResponseWriter, r *http.Request) { } responses, err := pod.Start(r.Context()) - if err != nil && errors.Cause(err) != define.ErrPodPartialFail { + if err != nil && !errors.Is(err, define.ErrPodPartialFail) { utils.Error(w, http.StatusConflict, err) return } report := entities.PodStartReport{Id: pod.ID()} for id, err := range responses { - report.Errs = append(report.Errs, errors.Wrapf(err, "error starting container "+id)) + report.Errs = append(report.Errs, fmt.Errorf("%v: %w", "error starting container "+id, err)) } code := http.StatusOK @@ -237,7 +237,7 @@ func PodDelete(w http.ResponseWriter, r *http.Request) { } if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } name := utils.GetName(r) @@ -263,14 +263,14 @@ func PodRestart(w http.ResponseWriter, r *http.Request) { return } responses, err := pod.Restart(r.Context()) - if err != nil && errors.Cause(err) != define.ErrPodPartialFail { + if err != nil && !errors.Is(err, define.ErrPodPartialFail) { utils.Error(w, http.StatusInternalServerError, err) return } report := entities.PodRestartReport{Id: pod.ID()} for id, err := range responses { - report.Errs = append(report.Errs, errors.Wrapf(err, "error restarting container %s", id)) + report.Errs = append(report.Errs, fmt.Errorf("error restarting container %s: %w", id, err)) } code := http.StatusOK @@ -314,14 +314,14 @@ func PodPause(w http.ResponseWriter, r *http.Request) { return } responses, err := pod.Pause(r.Context()) - if err != nil && errors.Cause(err) != define.ErrPodPartialFail { + if err != nil && !errors.Is(err, define.ErrPodPartialFail) { utils.Error(w, http.StatusInternalServerError, err) return } report := entities.PodPauseReport{Id: pod.ID()} for id, v := range responses { - report.Errs = append(report.Errs, errors.Wrapf(v, "error pausing container %s", id)) + report.Errs = append(report.Errs, fmt.Errorf("error pausing container %s: %w", id, v)) } code := http.StatusOK @@ -340,14 +340,14 @@ func PodUnpause(w http.ResponseWriter, r *http.Request) { return } responses, err := pod.Unpause(r.Context()) - if err != nil && errors.Cause(err) != define.ErrPodPartialFail { + if err != nil && !errors.Is(err, define.ErrPodPartialFail) { utils.Error(w, http.StatusInternalServerError, err) return } report := entities.PodUnpauseReport{Id: pod.ID()} for id, v := range responses { - report.Errs = append(report.Errs, errors.Wrapf(v, "error unpausing container %s", id)) + report.Errs = append(report.Errs, fmt.Errorf("error unpausing container %s: %w", id, v)) } code := http.StatusOK @@ -374,7 +374,7 @@ func PodTop(w http.ResponseWriter, r *http.Request) { PsArgs: psArgs, } if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } @@ -456,7 +456,7 @@ func PodKill(w http.ResponseWriter, r *http.Request) { // override any golang type defaults } if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } if _, found := r.URL.Query()["signal"]; found { @@ -465,7 +465,7 @@ func PodKill(w http.ResponseWriter, r *http.Request) { sig, err := util.ParseSignal(signal) if err != nil { - utils.InternalServerError(w, errors.Wrapf(err, "unable to parse signal value")) + utils.InternalServerError(w, fmt.Errorf("unable to parse signal value: %w", err)) return } name := utils.GetName(r) @@ -488,12 +488,12 @@ func PodKill(w http.ResponseWriter, r *http.Request) { } } if !hasRunning { - utils.Error(w, http.StatusConflict, errors.Errorf("cannot kill a pod with no running containers: %s", pod.ID())) + utils.Error(w, http.StatusConflict, fmt.Errorf("cannot kill a pod with no running containers: %s", pod.ID())) return } responses, err := pod.Kill(r.Context(), uint(sig)) - if err != nil && errors.Cause(err) != define.ErrPodPartialFail { + if err != nil && !errors.Is(err, define.ErrPodPartialFail) { utils.Error(w, http.StatusInternalServerError, err) return } @@ -534,7 +534,7 @@ func PodStats(w http.ResponseWriter, r *http.Request) { // default would go here } if err := decoder.Decode(&query, r.URL.Query()); err != nil { - utils.Error(w, http.StatusBadRequest, errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } @@ -549,13 +549,12 @@ func PodStats(w http.ResponseWriter, r *http.Request) { reports, err := containerEngine.PodStats(r.Context(), query.NamesOrIDs, options) // Error checks as documented in swagger. - switch errors.Cause(err) { - case define.ErrNoSuchPod: - utils.Error(w, http.StatusNotFound, err) - return - case nil: - // Nothing to do. - default: + if err != nil { + if errors.Is(err, define.ErrNoSuchPod) { + utils.Error(w, http.StatusNotFound, err) + return + } + utils.InternalServerError(w, err) return } diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index e792dea35..5eac76f5b 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -2,9 +2,12 @@ package libpod import ( "encoding/json" + "fmt" "net/http" "net/url" + "errors" + "github.com/containers/podman/v4/libpod" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/api/handlers/utils" @@ -16,7 +19,6 @@ import ( "github.com/containers/podman/v4/pkg/domain/infra/abi/parse" "github.com/containers/podman/v4/pkg/util" "github.com/gorilla/schema" - "github.com/pkg/errors" ) func CreateVolume(w http.ResponseWriter, r *http.Request) { @@ -30,14 +32,14 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { } if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, http.StatusInternalServerError, - errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } input := entities.VolumeCreateOptions{} // decode params from body if err := json.NewDecoder(r.Body).Decode(&input); err != nil { - utils.Error(w, http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + utils.Error(w, http.StatusInternalServerError, fmt.Errorf("Decode(): %w", err)) return } @@ -108,7 +110,7 @@ func ListVolumes(w http.ResponseWriter, r *http.Request) { filterMap, err := util.PrepareFilters(r) if err != nil { utils.Error(w, http.StatusInternalServerError, - errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } @@ -181,7 +183,7 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, http.StatusInternalServerError, - errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) return } name := utils.GetName(r) @@ -191,7 +193,7 @@ func RemoveVolume(w http.ResponseWriter, r *http.Request) { return } if err := runtime.RemoveVolume(r.Context(), vol, query.Force, query.Timeout); err != nil { - if errors.Cause(err) == define.ErrVolumeBeingUsed { + if errors.Is(err, define.ErrVolumeBeingUsed) { utils.Error(w, http.StatusConflict, err) return } diff --git a/pkg/api/handlers/swagger/responses.go b/pkg/api/handlers/swagger/responses.go index 55fc1a77f..93a508b39 100644 --- a/pkg/api/handlers/swagger/responses.go +++ b/pkg/api/handlers/swagger/responses.go @@ -41,6 +41,13 @@ type imagesLoadResponseLibpod struct { Body entities.ImageLoadReport } +// Image Scp +// swagger:response +type imagesScpResponseLibpod struct { + // in:body + Body reports.ScpReport +} + // Image Import // swagger:response type imagesImportResponseLibpod struct { diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index bf60b2c84..ab1b6f227 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -1,17 +1,18 @@ package utils import ( + "errors" + "fmt" "net/http" "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/errorhandling" "github.com/containers/storage" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) var ( - ErrLinkNotSupport = errors.New("Link is not supported") + ErrLinkNotSupport = errors.New("link is not supported") ) // TODO: document the exported functions in this file and make them more @@ -25,7 +26,7 @@ func Error(w http.ResponseWriter, code int, err error) { // Log detailed message of what happened to machine running podman service log.Infof("Request Failed(%s): %s", http.StatusText(code), err.Error()) em := errorhandling.ErrorModel{ - Because: (errors.Cause(err)).Error(), + Because: errorhandling.Cause(err).Error(), Message: err.Error(), ResponseCode: code, } @@ -33,51 +34,50 @@ func Error(w http.ResponseWriter, code int, err error) { } func VolumeNotFound(w http.ResponseWriter, name string, err error) { - if errors.Cause(err) != define.ErrNoSuchVolume { + if !errors.Is(err, define.ErrNoSuchVolume) { InternalServerError(w, err) } Error(w, http.StatusNotFound, err) } func ContainerNotFound(w http.ResponseWriter, name string, err error) { - switch errors.Cause(err) { - case define.ErrNoSuchCtr, define.ErrCtrExists: + if errors.Is(err, define.ErrNoSuchCtr) || errors.Is(err, define.ErrCtrExists) { Error(w, http.StatusNotFound, err) - default: + } else { InternalServerError(w, err) } } func ImageNotFound(w http.ResponseWriter, name string, err error) { - if errors.Cause(err) != storage.ErrImageUnknown { + if !errors.Is(err, storage.ErrImageUnknown) { InternalServerError(w, err) } Error(w, http.StatusNotFound, err) } func NetworkNotFound(w http.ResponseWriter, name string, err error) { - if errors.Cause(err) != define.ErrNoSuchNetwork { + if !errors.Is(err, define.ErrNoSuchNetwork) { InternalServerError(w, err) } Error(w, http.StatusNotFound, err) } func PodNotFound(w http.ResponseWriter, name string, err error) { - if errors.Cause(err) != define.ErrNoSuchPod { + if !errors.Is(err, define.ErrNoSuchPod) { InternalServerError(w, err) } Error(w, http.StatusNotFound, err) } func SessionNotFound(w http.ResponseWriter, name string, err error) { - if errors.Cause(err) != define.ErrNoSuchExecSession { + if !errors.Is(err, define.ErrNoSuchExecSession) { InternalServerError(w, err) } Error(w, http.StatusNotFound, err) } func SecretNotFound(w http.ResponseWriter, nameOrID string, err error) { - if errors.Cause(err).Error() != "no such secret" { + if errorhandling.Cause(err).Error() != "no such secret" { InternalServerError(w, err) } Error(w, http.StatusNotFound, err) @@ -92,7 +92,7 @@ func InternalServerError(w http.ResponseWriter, err error) { } func BadRequest(w http.ResponseWriter, key string, value string, err error) { - e := errors.Wrapf(err, "failed to parse query parameter '%s': %q", key, value) + e := fmt.Errorf("failed to parse query parameter '%s': %q: %w", key, value, err) Error(w, http.StatusBadRequest, e) } diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 1617a5dd7..a2f46cb35 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -948,6 +948,10 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // name: ignore // description: Ignore if a specified image does not exist and do not throw an error. // type: boolean + // - in: query + // name: lookupManifest + // description: Resolves to manifest list instead of image. + // type: boolean // produces: // - application/json // responses: @@ -1615,5 +1619,39 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/internalError" r.Handle(VersionedPath("/libpod/build"), s.APIHandler(compat.BuildImage)).Methods(http.MethodPost) + + // swagger:operation POST /libpod/images/scp/{name} libpod ImageScpLibpod + // --- + // tags: + // - images + // summary: Copy an image from one host to another + // description: Copy an image from one host to another + // parameters: + // - in: path + // name: name + // required: true + // description: source connection/image + // type: string + // - in: query + // name: destination + // required: false + // description: dest connection/image + // type: string + // - in: query + // name: quiet + // required: false + // description: quiet output + // type: boolean + // default: false + // produces: + // - application/json + // responses: + // 200: + // $ref: "#/responses/imagesScpResponseLibpod" + // 400: + // $ref: "#/responses/badParamError" + // 500: + // $ref: '#/responses/internalError' + r.Handle(VersionedPath("/libpod/images/scp/{name:.*}"), s.APIHandler(libpod.ImageScp)).Methods(http.MethodPost) return nil } diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index 32372019b..57c8bd597 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -346,3 +346,23 @@ func Search(ctx context.Context, term string, options *SearchOptions) ([]entitie return results, nil } + +func Scp(ctx context.Context, source, destination *string, options ScpOptions) (reports.ScpReport, error) { + rep := reports.ScpReport{} + + conn, err := bindings.GetClient(ctx) + if err != nil { + return rep, err + } + params, err := options.ToParams() + if err != nil { + return rep, err + } + response, err := conn.DoRequest(ctx, nil, http.MethodPost, fmt.Sprintf("/images/scp/%s", *source), params, nil) + if err != nil { + return rep, err + } + defer response.Body.Close() + + return rep, response.Process(&rep) +} diff --git a/pkg/bindings/images/types.go b/pkg/bindings/images/types.go index 16dbad380..3728ae5c0 100644 --- a/pkg/bindings/images/types.go +++ b/pkg/bindings/images/types.go @@ -13,6 +13,8 @@ type RemoveOptions struct { Force *bool // Ignore if a specified image does not exist and do not throw an error. Ignore *bool + // Confirms if given name is a manifest list and removes it, otherwise returns error. + LookupManifest *bool } //go:generate go run ../generator/generator.go DiffOptions @@ -188,3 +190,8 @@ type BuildOptions struct { // ExistsOptions are optional options for checking if an image exists type ExistsOptions struct { } + +type ScpOptions struct { + Quiet *bool + Destination *string +} diff --git a/pkg/bindings/images/types_remove_options.go b/pkg/bindings/images/types_remove_options.go index 613a33183..559ebcfd5 100644 --- a/pkg/bindings/images/types_remove_options.go +++ b/pkg/bindings/images/types_remove_options.go @@ -61,3 +61,18 @@ func (o *RemoveOptions) GetIgnore() bool { } return *o.Ignore } + +// WithLookupManifest set field LookupManifest to given value +func (o *RemoveOptions) WithLookupManifest(value bool) *RemoveOptions { + o.LookupManifest = &value + return o +} + +// GetLookupManifest returns value of field LookupManifest +func (o *RemoveOptions) GetLookupManifest() bool { + if o.LookupManifest == nil { + var z bool + return z + } + return *o.LookupManifest +} diff --git a/pkg/bindings/images/types_scp_options.go b/pkg/bindings/images/types_scp_options.go new file mode 100644 index 000000000..5a1178cb1 --- /dev/null +++ b/pkg/bindings/images/types_scp_options.go @@ -0,0 +1,12 @@ +package images + +import ( + "net/url" + + "github.com/containers/podman/v4/pkg/bindings/internal/util" +) + +// ToParams formats struct fields to be passed to API service +func (o *ScpOptions) ToParams() (url.Values, error) { + return util.ToParams(o) +} diff --git a/pkg/bindings/manifests/manifests.go b/pkg/bindings/manifests/manifests.go index feff5d6e8..a68dd5a4e 100644 --- a/pkg/bindings/manifests/manifests.go +++ b/pkg/bindings/manifests/manifests.go @@ -117,6 +117,26 @@ func Remove(ctx context.Context, name, digest string, _ *RemoveOptions) (string, return Modify(ctx, name, []string{digest}, optionsv4) } +// Delete removes specified manifest from local storage. +func Delete(ctx context.Context, name string) (*entities.ManifestRemoveReport, error) { + var report entities.ManifestRemoveReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(ctx, nil, http.MethodDelete, "/manifests/%s", nil, nil, name) + if err != nil { + return nil, err + } + defer response.Body.Close() + + if err := response.Process(&report); err != nil { + return nil, err + } + + return &report, errorhandling.JoinErrors(errorhandling.StringsToErrors(report.Errors)) +} + // Push takes a manifest list and pushes to a destination. If the destination is not specified, // the name will be used instead. If the optional all boolean is specified, all images specified // in the list will be pushed as well. @@ -211,7 +231,7 @@ func Modify(ctx context.Context, name string, images []string, options *ModifyOp err = errorhandling.JoinErrors(report.Errors) if err != nil { errModel := errorhandling.ErrorModel{ - Because: (errors.Cause(err)).Error(), + Because: errorhandling.Cause(err).Error(), Message: err.Error(), ResponseCode: response.StatusCode, } diff --git a/pkg/bindings/test/manifests_test.go b/pkg/bindings/test/manifests_test.go index e6c93817d..6a34ef5a6 100644 --- a/pkg/bindings/test/manifests_test.go +++ b/pkg/bindings/test/manifests_test.go @@ -62,6 +62,19 @@ var _ = Describe("podman manifest", func() { Expect(len(list.Manifests)).To(BeNumerically("==", 1)) }) + It("delete manifest", func() { + id, err := manifests.Create(bt.conn, "quay.io/libpod/foobar:latest", []string{}, nil) + Expect(err).ToNot(HaveOccurred(), err) + list, err := manifests.Inspect(bt.conn, id, nil) + Expect(err).ToNot(HaveOccurred()) + + Expect(len(list.Manifests)).To(BeZero()) + + removeReport, err := manifests.Delete(bt.conn, "quay.io/libpod/foobar:latest") + Expect(err).ToNot(HaveOccurred()) + Expect(len(removeReport.Deleted)).To(BeNumerically("==", 1)) + }) + It("inspect", func() { _, err := manifests.Inspect(bt.conn, "larry", nil) Expect(err).To(HaveOccurred()) diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 5011d82aa..5f76ae50b 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -22,12 +22,12 @@ type ImageEngine interface { Push(ctx context.Context, source string, destination string, opts ImagePushOptions) 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) error Search(ctx context.Context, term string, opts ImageSearchOptions) ([]ImageSearchReport, error) SetTrust(ctx context.Context, args []string, options SetTrustOptions) error ShowTrust(ctx context.Context, args []string, options ShowTrustOptions) (*ShowTrustReport, error) Shutdown(ctx context.Context) Tag(ctx context.Context, nameOrID string, tags []string, options ImageTagOptions) error - Transfer(ctx context.Context, source ImageScpOptions, dest ImageScpOptions, parentFlags []string) error Tree(ctx context.Context, nameOrID string, options ImageTreeOptions) (*ImageTreeReport, error) Unmount(ctx context.Context, images []string, options ImageUnmountOptions) ([]*ImageUnmountReport, error) Untag(ctx context.Context, nameOrID string, tags []string, options ImageUntagOptions) error diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 11f6e8687..da317cfad 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -325,6 +325,8 @@ type ImageScpOptions struct { Image string `json:"image,omitempty"` // User is used in conjunction with Transfer to determine if a valid user was given to save from/load into User string `json:"user,omitempty"` + // Tag is the name to be used for the image on the destination + Tag string `json:"tag,omitempty"` } // ImageScpConnections provides the ssh related information used in remote image transfer diff --git a/pkg/domain/entities/manifest.go b/pkg/domain/entities/manifest.go index 81f3e837b..e88c5f854 100644 --- a/pkg/domain/entities/manifest.go +++ b/pkg/domain/entities/manifest.go @@ -67,6 +67,21 @@ type ManifestModifyOptions struct { type ManifestRemoveOptions struct { } +// ManifestRemoveReport provides the model for the removed manifest +// +// swagger:model +type ManifestRemoveReport struct { + // Deleted manifest list. + Deleted []string `json:",omitempty"` + // Untagged images. Can be longer than Deleted. + Untagged []string `json:",omitempty"` + // Errors associated with operation + Errors []string `json:",omitempty"` + // ExitCode describes the exit codes as described in the `podman rmi` + // man page. + ExitCode int +} + // ManifestModifyReport provides the model for removed digests and changed manifest // // swagger:model diff --git a/pkg/domain/entities/reports/scp.go b/pkg/domain/entities/reports/scp.go new file mode 100644 index 000000000..1e102bab3 --- /dev/null +++ b/pkg/domain/entities/reports/scp.go @@ -0,0 +1,5 @@ +package reports + +type ScpReport struct { + Id string `json:"Id"` //nolint:revive,stylecheck +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 281e448f6..1688be57e 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -2,6 +2,7 @@ package abi import ( "context" + "errors" "fmt" "io/ioutil" "os" @@ -32,7 +33,6 @@ import ( "github.com/containers/podman/v4/pkg/specgenutil" "github.com/containers/podman/v4/pkg/util" "github.com/containers/storage" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -80,7 +80,7 @@ func getContainersByContext(all, latest bool, names []string, runtime *libpod.Ru func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrID string, options entities.ContainerExistsOptions) (*entities.BoolReport, error) { _, err := ic.Libpod.LookupContainer(nameOrID) if err != nil { - if errors.Cause(err) != define.ErrNoSuchCtr { + if !errors.Is(err, define.ErrNoSuchCtr) { return nil, err } if options.External { @@ -120,7 +120,7 @@ func (ic *ContainerEngine) ContainerPause(ctx context.Context, namesOrIds []stri report := make([]*entities.PauseUnpauseReport, 0, len(ctrs)) for _, c := range ctrs { err := c.Pause() - if err != nil && options.All && errors.Cause(err) == define.ErrCtrStateInvalid { + if err != nil && options.All && errors.Is(err, define.ErrCtrStateInvalid) { logrus.Debugf("Container %s is not running", c.ID()) continue } @@ -137,7 +137,7 @@ func (ic *ContainerEngine) ContainerUnpause(ctx context.Context, namesOrIds []st report := make([]*entities.PauseUnpauseReport, 0, len(ctrs)) for _, c := range ctrs { err := c.Unpause() - if err != nil && options.All && errors.Cause(err) == define.ErrCtrStateInvalid { + if err != nil && options.All && errors.Is(err, define.ErrCtrStateInvalid) { logrus.Debugf("Container %s is not paused", c.ID()) continue } @@ -148,7 +148,7 @@ func (ic *ContainerEngine) ContainerUnpause(ctx context.Context, namesOrIds []st func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []string, options entities.StopOptions) ([]*entities.StopReport, error) { names := namesOrIds ctrs, rawInputs, err := getContainersAndInputByContext(options.All, options.Latest, names, ic.Libpod) - if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr) { + if err != nil && !(options.Ignore && errors.Is(err, define.ErrNoSuchCtr)) { return nil, err } ctrMap := map[string]string{} @@ -166,13 +166,13 @@ func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []strin } if err != nil { switch { - case errors.Cause(err) == define.ErrCtrStopped: + case errors.Is(err, define.ErrCtrStopped): logrus.Debugf("Container %s is already stopped", c.ID()) - case options.All && errors.Cause(err) == define.ErrCtrStateInvalid: + case options.All && errors.Is(err, define.ErrCtrStateInvalid): logrus.Debugf("Container %s is not running, could not stop", c.ID()) // container never created in OCI runtime // docker parity: do nothing just return container id - case errors.Cause(err) == define.ErrCtrStateInvalid: + case errors.Is(err, define.ErrCtrStateInvalid): logrus.Debugf("Container %s is either not created on runtime or is in a invalid state", c.ID()) default: return err @@ -238,7 +238,7 @@ func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []strin reports := make([]*entities.KillReport, 0, len(ctrs)) for _, con := range ctrs { err := con.Kill(uint(sig)) - if options.All && errors.Cause(err) == define.ErrCtrStateInvalid { + if options.All && errors.Is(err, define.ErrCtrStateInvalid) { logrus.Debugf("Container %s is not running", con.ID()) continue } @@ -289,8 +289,7 @@ func (ic *ContainerEngine) removeContainer(ctx context.Context, ctr *libpod.Cont return nil } logrus.Debugf("Failed to remove container %s: %s", ctr.ID(), err.Error()) - switch errors.Cause(err) { - case define.ErrNoSuchCtr: + if errors.Is(err, define.ErrNoSuchCtr) { // Ignore if the container does not exist (anymore) when either // it has been requested by the user of if the container is a // service one. Service containers are removed along with its @@ -301,7 +300,7 @@ func (ic *ContainerEngine) removeContainer(ctx context.Context, ctr *libpod.Cont logrus.Debugf("Ignoring error (--allow-missing): %v", err) return nil } - case define.ErrCtrRemoved: + } else if errors.Is(err, define.ErrCtrRemoved) { return nil } return err @@ -317,15 +316,15 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, for _, ctr := range names { report := reports.RmReport{Id: ctr} report.Err = ic.Libpod.RemoveStorageContainer(ctr, options.Force) - switch errors.Cause(report.Err) { - case nil: + //nolint:gocritic + if report.Err == nil { // remove container names that we successfully deleted rmReports = append(rmReports, &report) - case define.ErrNoSuchCtr, define.ErrCtrExists: + } else if errors.Is(report.Err, define.ErrNoSuchCtr) || errors.Is(report.Err, define.ErrCtrExists) { // There is still a potential this is a libpod container tmpNames = append(tmpNames, ctr) - default: - if _, err := ic.Libpod.LookupContainer(ctr); errors.Cause(err) == define.ErrNoSuchCtr { + } else { + if _, err := ic.Libpod.LookupContainer(ctr); errors.Is(err, define.ErrNoSuchCtr) { // remove container failed, but not a libpod container rmReports = append(rmReports, &report) continue @@ -337,7 +336,7 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, names = tmpNames ctrs, err := getContainersByContext(options.All, options.Latest, names, ic.Libpod) - if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr) { + if err != nil && !(options.Ignore && errors.Is(err, define.ErrNoSuchCtr)) { // Failed to get containers. If force is specified, get the containers ID // and evict them if !options.Force { @@ -349,7 +348,7 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, report := reports.RmReport{Id: ctr} _, err := ic.Libpod.EvictContainer(ctx, ctr, options.Volumes) if err != nil { - if options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr { + if options.Ignore && errors.Is(err, define.ErrNoSuchCtr) { logrus.Debugf("Ignoring error (--allow-missing): %v", err) rmReports = append(rmReports, &report) continue @@ -426,7 +425,7 @@ func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []st ctr, err := ic.Libpod.GetLatestContainer() if err != nil { if errors.Is(err, define.ErrNoSuchCtr) { - return nil, []error{errors.Wrapf(err, "no containers to inspect")}, nil + return nil, []error{fmt.Errorf("no containers to inspect: %w", err)}, nil } return nil, nil, err } @@ -452,7 +451,7 @@ func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []st // ErrNoSuchCtr is non-fatal, other errors will be // treated as fatal. if errors.Is(err, define.ErrNoSuchCtr) { - errs = append(errs, errors.Errorf("no such container %s", name)) + errs = append(errs, fmt.Errorf("no such container %s", name)) continue } return nil, nil, err @@ -463,7 +462,7 @@ func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []st // ErrNoSuchCtr is non-fatal, other errors will be // treated as fatal. if errors.Is(err, define.ErrNoSuchCtr) { - errs = append(errs, errors.Errorf("no such container %s", name)) + errs = append(errs, fmt.Errorf("no such container %s", name)) continue } return nil, nil, err @@ -487,7 +486,7 @@ func (ic *ContainerEngine) ContainerTop(ctx context.Context, options entities.To container, err = ic.Libpod.LookupContainer(options.NameOrID) } if err != nil { - return nil, errors.Wrap(err, "unable to look up requested container") + return nil, fmt.Errorf("unable to look up requested container: %w", err) } // Run Top. @@ -512,12 +511,12 @@ func (ic *ContainerEngine) ContainerCommit(ctx context.Context, nameOrID string, case "oci": mimeType = buildah.OCIv1ImageManifest if len(options.Message) > 0 { - return nil, errors.Errorf("messages are only compatible with the docker image format (-f docker)") + return nil, fmt.Errorf("messages are only compatible with the docker image format (-f docker)") } case "docker": mimeType = manifest.DockerV2Schema2MediaType default: - return nil, errors.Errorf("unrecognized image format %q", options.Format) + return nil, fmt.Errorf("unrecognized image format %q", options.Format) } sc := ic.Libpod.SystemContext() @@ -660,7 +659,7 @@ func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []st // CRImportCheckpoint is expected to import exactly one container from checkpoint image checkpointImageImportErrors = append( checkpointImageImportErrors, - errors.Errorf("unable to import checkpoint from image: %q: %v", nameOrID, err), + fmt.Errorf("unable to import checkpoint from image: %q: %v", nameOrID, err), ) } else { containers = append(containers, importedContainers[0]) @@ -720,16 +719,16 @@ func (ic *ContainerEngine) ContainerAttach(ctx context.Context, nameOrID string, ctr := ctrs[0] conState, err := ctr.State() if err != nil { - return errors.Wrapf(err, "unable to determine state of %s", ctr.ID()) + return fmt.Errorf("unable to determine state of %s: %w", ctr.ID(), err) } if conState != define.ContainerStateRunning { - return errors.Errorf("you can only attach to running containers") + return fmt.Errorf("you can only attach to running containers") } // If the container is in a pod, also set to recursively start dependencies err = terminal.StartAttachCtr(ctx, ctr, options.Stdout, options.Stderr, options.Stdin, options.DetachKeys, options.SigProxy, false) - if err != nil && errors.Cause(err) != define.ErrDetach { - return errors.Wrapf(err, "error attaching to container %s", ctr.ID()) + if err != nil && !errors.Is(err, define.ErrDetach) { + return fmt.Errorf("error attaching to container %s: %w", ctr.ID(), err) } os.Stdout.WriteString("\n") return nil @@ -751,12 +750,12 @@ func makeExecConfig(options entities.ExecOptions, rt *libpod.Runtime) (*libpod.E storageConfig := rt.StorageConfig() runtimeConfig, err := rt.GetConfig() if err != nil { - return nil, errors.Wrapf(err, "error retrieving Libpod configuration to build exec exit command") + return nil, fmt.Errorf("error retrieving Libpod configuration to build exec exit command: %w", err) } // TODO: Add some ability to toggle syslog exitCommandArgs, err := specgenutil.CreateExitCommandArgs(storageConfig, runtimeConfig, logrus.IsLevelEnabled(logrus.DebugLevel), false, true) if err != nil { - return nil, errors.Wrapf(err, "error constructing exit command for exec session") + return nil, fmt.Errorf("error constructing exit command for exec session: %w", err) } execConfig.ExitCommand = exitCommandArgs @@ -774,7 +773,7 @@ func checkExecPreserveFDs(options entities.ExecOptions) error { for _, e := range entries { i, err := strconv.Atoi(e.Name()) if err != nil { - return errors.Wrapf(err, "cannot parse %s in /proc/self/fd", e.Name()) + return fmt.Errorf("cannot parse %s in /proc/self/fd: %w", e.Name(), err) } m[i] = true } @@ -891,7 +890,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri if options.Attach { err = terminal.StartAttachCtr(ctx, ctr, options.Stdout, options.Stderr, options.Stdin, options.DetachKeys, options.SigProxy, !ctrRunning) - if errors.Cause(err) == define.ErrDetach { + if errors.Is(err, define.ErrDetach) { // User manually detached // Exit cleanly immediately reports = append(reports, &entities.ContainerStartReport{ @@ -903,7 +902,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri return reports, nil } - if errors.Cause(err) == define.ErrWillDeadlock { + if errors.Is(err, define.ErrWillDeadlock) { logrus.Debugf("Deadlock error: %v", err) reports = append(reports, &entities.ContainerStartReport{ Id: ctr.ID(), @@ -911,7 +910,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri Err: err, ExitCode: define.ExitCode(err), }) - return reports, errors.Errorf("attempting to start container %s would cause a deadlock; please run 'podman system renumber' to resolve", ctr.ID()) + return reports, fmt.Errorf("attempting to start container %s would cause a deadlock; please run 'podman system renumber' to resolve", ctr.ID()) } if ctrRunning { @@ -936,7 +935,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri logrus.Errorf("Removing container %s: %v", ctr.ID(), err) } } - return reports, errors.Wrapf(err, "unable to start container %s", ctr.ID()) + return reports, fmt.Errorf("unable to start container %s: %w", ctr.ID(), err) } exitCode = ic.GetContainerExitCode(ctx, ctr) @@ -960,12 +959,12 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri } if err := ctr.Start(ctx, true); err != nil { report.Err = err - if errors.Cause(err) == define.ErrWillDeadlock { - report.Err = errors.Wrapf(err, "please run 'podman system renumber' to resolve deadlocks") + if errors.Is(err, define.ErrWillDeadlock) { + report.Err = fmt.Errorf("please run 'podman system renumber' to resolve deadlocks: %w", err) reports = append(reports, report) continue } - report.Err = errors.Wrapf(err, "unable to start container %q", ctr.ID()) + report.Err = fmt.Errorf("unable to start container %q: %w", ctr.ID(), err) reports = append(reports, report) if ctr.AutoRemove() { if err := ic.removeContainer(ctx, ctr, entities.RmOptions{}); err != nil { @@ -1001,7 +1000,7 @@ func (ic *ContainerEngine) Diff(ctx context.Context, namesOrIDs []string, opts e if opts.Latest { ctnr, err := ic.Libpod.GetLatestContainer() if err != nil { - return nil, errors.Wrap(err, "unable to get latest container") + return nil, fmt.Errorf("unable to get latest container: %w", err) } base = ctnr.ID() } @@ -1064,7 +1063,7 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta // We've manually detached from the container // Do not perform cleanup, or wait for container exit code // Just exit immediately - if errors.Cause(err) == define.ErrDetach { + if errors.Is(err, define.ErrDetach) { report.ExitCode = 0 return &report, nil } @@ -1074,10 +1073,10 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta logrus.Debugf("unable to remove container %s after failing to start and attach to it", ctr.ID()) } } - if errors.Cause(err) == define.ErrWillDeadlock { + if errors.Is(err, define.ErrWillDeadlock) { logrus.Debugf("Deadlock error on %q: %v", ctr.ID(), err) report.ExitCode = define.ExitCode(err) - return &report, errors.Errorf("attempting to start container %s would cause a deadlock; please run 'podman system renumber' to resolve", ctr.ID()) + return &report, fmt.Errorf("attempting to start container %s would cause a deadlock; please run 'podman system renumber' to resolve", ctr.ID()) } report.ExitCode = define.ExitCode(err) return &report, err @@ -1086,8 +1085,8 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta if opts.Rm && !ctr.ShouldRestart(ctx) { var timeout *uint if err := ic.Libpod.RemoveContainer(ctx, ctr, false, true, timeout); err != nil { - if errors.Cause(err) == define.ErrNoSuchCtr || - errors.Cause(err) == define.ErrCtrRemoved { + if errors.Is(err, define.ErrNoSuchCtr) || + errors.Is(err, define.ErrCtrRemoved) { logrus.Infof("Container %s was already removed, skipping --rm", ctr.ID()) } else { logrus.Errorf("Removing container %s: %v", ctr.ID(), err) @@ -1180,12 +1179,12 @@ func (ic *ContainerEngine) ContainerCleanup(ctx context.Context, namesOrIds []st var timeout *uint err = ic.Libpod.RemoveContainer(ctx, ctr, false, true, timeout) if err != nil { - report.RmErr = errors.Wrapf(err, "failed to clean up and remove container %v", ctr.ID()) + report.RmErr = fmt.Errorf("failed to clean up and remove container %v: %w", ctr.ID(), err) } } else { err := ctr.Cleanup(ctx) if err != nil { - report.CleanErr = errors.Wrapf(err, "failed to clean up container %v", ctr.ID()) + report.CleanErr = fmt.Errorf("failed to clean up container %v: %w", ctr.ID(), err) } } @@ -1212,7 +1211,7 @@ func (ic *ContainerEngine) ContainerInit(ctx context.Context, namesOrIds []strin err := ctr.Init(ctx, ctr.PodID() != "") // If we're initializing all containers, ignore invalid state errors - if options.All && errors.Cause(err) == define.ErrCtrStateInvalid { + if options.All && errors.Is(err, define.ErrCtrStateInvalid) { err = nil } report.Err = err @@ -1323,7 +1322,7 @@ func (ic *ContainerEngine) ContainerUnmount(ctx context.Context, nameOrIDs []str if mounted { report := entities.ContainerUnmountReport{Id: sctr.ID} if _, report.Err = ic.Libpod.UnmountStorageContainer(sctr.ID, options.Force); report.Err != nil { - if errors.Cause(report.Err) != define.ErrCtrExists { + if !errors.Is(report.Err, define.ErrCtrExists) { reports = append(reports, &report) } } else { @@ -1357,11 +1356,11 @@ func (ic *ContainerEngine) ContainerUnmount(ctx context.Context, nameOrIDs []str report := entities.ContainerUnmountReport{Id: ctr.ID()} if err := ctr.Unmount(options.Force); err != nil { - if options.All && errors.Cause(err) == storage.ErrLayerNotMounted { + if options.All && errors.Is(err, storage.ErrLayerNotMounted) { logrus.Debugf("Error umounting container %s, storage.ErrLayerNotMounted", ctr.ID()) continue } - report.Err = errors.Wrapf(err, "error unmounting container %s", ctr.ID()) + report.Err = fmt.Errorf("error unmounting container %s: %w", ctr.ID(), err) } reports = append(reports, &report) } @@ -1410,7 +1409,7 @@ func (ic *ContainerEngine) Shutdown(_ context.Context) { func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []string, options entities.ContainerStatsOptions) (statsChan chan entities.ContainerStatsReport, err error) { if options.Interval < 1 { - return nil, errors.New("Invalid interval, must be a positive number greater zero") + return nil, errors.New("invalid interval, must be a positive number greater zero") } if rootless.IsRootless() { unified, err := cgroups.IsCgroup2UnifiedMode() @@ -1465,19 +1464,18 @@ func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []stri computeStats := func() ([]define.ContainerStats, error) { containers, err = containerFunc() if err != nil { - return nil, errors.Wrapf(err, "unable to get list of containers") + return nil, fmt.Errorf("unable to get list of containers: %w", err) } reportStats := []define.ContainerStats{} for _, ctr := range containers { stats, err := ctr.GetContainerStats(containerStats[ctr.ID()]) if err != nil { - cause := errors.Cause(err) - if queryAll && (cause == define.ErrCtrRemoved || cause == define.ErrNoSuchCtr || cause == define.ErrCtrStateInvalid) { + if queryAll && (errors.Is(err, define.ErrCtrRemoved) || errors.Is(err, define.ErrNoSuchCtr) || errors.Is(err, define.ErrCtrStateInvalid)) { continue } - if cause == cgroups.ErrCgroupV1Rootless { - err = cause + if errors.Is(err, cgroups.ErrCgroupV1Rootless) { + err = cgroups.ErrCgroupV1Rootless } return nil, err } diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index d63de2424..02aa5923d 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -3,6 +3,7 @@ package abi import ( "context" "fmt" + "io/fs" "io/ioutil" "net/url" "os" @@ -29,7 +30,6 @@ import ( domainUtils "github.com/containers/podman/v4/pkg/domain/utils" "github.com/containers/podman/v4/pkg/errorhandling" "github.com/containers/podman/v4/pkg/rootless" - "github.com/containers/podman/v4/utils" "github.com/containers/storage" dockerRef "github.com/docker/distribution/reference" "github.com/opencontainers/go-digest" @@ -350,22 +350,6 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri } return pushError } - -// Transfer moves images between root and rootless storage so the user specified in the scp call can access and use the image modified by root -func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error { - if source.User == "" { - return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage") - } - podman, err := os.Executable() - if err != nil { - return err - } - if rootless.IsRootless() && (len(dest.User) == 0 || dest.User == "root") { // if we are rootless and do not have a destination user we can just use sudo - return transferRootless(source, dest, podman, parentFlags) - } - return transferRootful(source, dest, podman, parentFlags) -} - func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error { // Allow tagging manifest list instead of resolving instances from manifest lookupOptions := &libimage.LookupImageOptions{ManifestList: true} @@ -694,53 +678,32 @@ func (ir *ImageEngine) Sign(ctx context.Context, names []string, options entitie return nil, nil } -func getSigFilename(sigStoreDirPath string) (string, error) { - sigFileSuffix := 1 - sigFiles, err := ioutil.ReadDir(sigStoreDirPath) +func (ir *ImageEngine) Scp(ctx context.Context, src, dst string, parentFlags []string, quiet bool) error { + rep, source, dest, flags, err := domainUtils.ExecuteTransfer(src, dst, parentFlags, quiet) if err != nil { - return "", err - } - sigFilenames := make(map[string]bool) - for _, file := range sigFiles { - sigFilenames[file.Name()] = true + return err } - for { - sigFilename := "signature-" + strconv.Itoa(sigFileSuffix) - if _, exists := sigFilenames[sigFilename]; !exists { - return sigFilename, nil + if (rep == nil && err == nil) && (source != nil && dest != nil) { // we need to execute the transfer + err := Transfer(ctx, *source, *dest, flags) + if err != nil { + return err } - sigFileSuffix++ - } -} - -func localPathFromURI(url *url.URL) (string, error) { - if url.Scheme != "file" { - return "", errors.Errorf("writing to %s is not supported. Use a supported scheme", url.String()) } - return url.Path, nil + return nil } -// putSignature creates signature and saves it to the signstore file -func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStoreDir string, instanceDigest digest.Digest, dockerReference dockerRef.Reference, options entities.SignOptions) error { - newSig, err := signature.SignDockerManifest(manifestBlob, dockerReference.String(), mech, options.SignBy) - if err != nil { - return err - } - signatureDir := fmt.Sprintf("%s@%s=%s", sigStoreDir, instanceDigest.Algorithm(), instanceDigest.Hex()) - if err := os.MkdirAll(signatureDir, 0751); err != nil { - // The directory is allowed to exist - if !os.IsExist(err) { - return err - } +func Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error { + if source.User == "" { + return errors.Wrapf(define.ErrInvalidArg, "you must define a user when transferring from root to rootless storage") } - sigFilename, err := getSigFilename(signatureDir) + podman, err := os.Executable() if err != nil { return err } - if err = ioutil.WriteFile(filepath.Join(signatureDir, sigFilename), newSig, 0644); err != nil { - return err + if rootless.IsRootless() && (len(dest.User) == 0 || dest.User == "root") { // if we are rootless and do not have a destination user we can just use sudo + return transferRootless(source, dest, podman, parentFlags) } - return nil + return transferRootful(source, dest, podman, parentFlags) } // TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users @@ -763,7 +726,7 @@ func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOpt } else { cmdSave = exec.Command(podman) } - cmdSave = utils.CreateSCPCommand(cmdSave, saveCommand) + cmdSave = domainUtils.CreateSCPCommand(cmdSave, saveCommand) logrus.Debugf("Executing save command: %q", cmdSave) err := cmdSave.Run() if err != nil { @@ -776,8 +739,11 @@ func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOpt } else { cmdLoad = exec.Command(podman) } - cmdLoad = utils.CreateSCPCommand(cmdLoad, loadCommand) + cmdLoad = domainUtils.CreateSCPCommand(cmdLoad, loadCommand) logrus.Debugf("Executing load command: %q", cmdLoad) + if len(dest.Tag) > 0 { + return domainUtils.ScpTag(cmdLoad, podman, dest) + } return cmdLoad.Run() } @@ -833,11 +799,20 @@ func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOpti return err } } - err = execPodman(uSave, saveCommand) + _, err = execTransferPodman(uSave, saveCommand, false) + if err != nil { + return err + } + out, err := execTransferPodman(uLoad, loadCommand, (len(dest.Tag) > 0)) if err != nil { return err } - return execPodman(uLoad, loadCommand) + if out != nil { + image := domainUtils.ExtractImage(out) + _, err := execTransferPodman(uLoad, []string{podman, "tag", image, dest.Tag}, false) + return err + } + return nil } func lookupUser(u string) (*user.User, error) { @@ -847,10 +822,10 @@ func lookupUser(u string) (*user.User, error) { return user.Lookup(u) } -func execPodman(execUser *user.User, command []string) error { - cmdLogin, err := utils.LoginUser(execUser.Username) +func execTransferPodman(execUser *user.User, command []string, needToTag bool) ([]byte, error) { + cmdLogin, err := domainUtils.LoginUser(execUser.Username) if err != nil { - return err + return nil, err } defer func() { @@ -864,11 +839,11 @@ func execPodman(execUser *user.User, command []string) error { cmd.Stdout = os.Stdout uid, err := strconv.ParseInt(execUser.Uid, 10, 32) if err != nil { - return err + return nil, err } gid, err := strconv.ParseInt(execUser.Gid, 10, 32) if err != nil { - return err + return nil, err } cmd.SysProcAttr = &syscall.SysProcAttr{ Credential: &syscall.Credential{ @@ -878,5 +853,55 @@ func execPodman(execUser *user.User, command []string) error { NoSetGroups: false, }, } - return cmd.Run() + if needToTag { + cmd.Stdout = nil + return cmd.Output() + } + return nil, cmd.Run() +} + +func getSigFilename(sigStoreDirPath string) (string, error) { + sigFileSuffix := 1 + sigFiles, err := ioutil.ReadDir(sigStoreDirPath) + if err != nil { + return "", err + } + sigFilenames := make(map[string]bool) + for _, file := range sigFiles { + sigFilenames[file.Name()] = true + } + for { + sigFilename := "signature-" + strconv.Itoa(sigFileSuffix) + if _, exists := sigFilenames[sigFilename]; !exists { + return sigFilename, nil + } + sigFileSuffix++ + } +} + +func localPathFromURI(url *url.URL) (string, error) { + if url.Scheme != "file" { + return "", errors.Errorf("writing to %s is not supported. Use a supported scheme", url.String()) + } + return url.Path, nil +} + +// putSignature creates signature and saves it to the signstore file +func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStoreDir string, instanceDigest digest.Digest, dockerReference dockerRef.Reference, options entities.SignOptions) error { + newSig, err := signature.SignDockerManifest(manifestBlob, dockerReference.String(), mech, options.SignBy) + if err != nil { + return err + } + signatureDir := fmt.Sprintf("%s@%s=%s", sigStoreDir, instanceDigest.Algorithm(), instanceDigest.Hex()) + if err := os.MkdirAll(signatureDir, 0751); err != nil { + // The directory is allowed to exist + if !errors.Is(err, fs.ErrExist) { + return err + } + } + sigFilename, err := getSigFilename(signatureDir) + if err != nil { + return err + } + return ioutil.WriteFile(filepath.Join(signatureDir, sigFilename), newSig, 0644) } diff --git a/pkg/domain/infra/abi/parse/parse.go b/pkg/domain/infra/abi/parse/parse.go index 66794e592..4e8c2e508 100644 --- a/pkg/domain/infra/abi/parse/parse.go +++ b/pkg/domain/infra/abi/parse/parse.go @@ -78,6 +78,16 @@ func VolumeOptions(opts map[string]string) ([]libpod.VolumeCreateOption, error) libpodOptions = append(libpodOptions, libpod.WithVolumeDisableQuota()) // set option "NOQUOTA": "true" volumeOptions["NOQUOTA"] = "true" + case "timeout": + if len(splitO) != 2 { + return nil, errors.Wrapf(define.ErrInvalidArg, "timeout option must provide a valid timeout in seconds") + } + intTimeout, err := strconv.Atoi(splitO[1]) + if err != nil { + return nil, errors.Wrapf(err, "cannot convert Timeout %s to an integer", splitO[1]) + } + logrus.Debugf("Removing timeout from options and adding WithTimeout for Timeout %d", intTimeout) + libpodOptions = append(libpodOptions, libpod.WithVolumeDriverTimeout(intTimeout)) default: finalVal = append(finalVal, o) } diff --git a/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go index 6e26026d4..2bd88ed85 100644 --- a/pkg/domain/infra/abi/system.go +++ b/pkg/domain/infra/abi/system.go @@ -319,8 +319,8 @@ func (ic *ContainerEngine) SystemDf(ctx context.Context, options entities.System } dfVolumes := make([]*entities.SystemDfVolumeReport, 0, len(vols)) - var reclaimableSize uint64 for _, v := range vols { + var reclaimableSize uint64 var consInUse int mountPoint, err := v.MountPoint() if err != nil { @@ -341,7 +341,7 @@ func (ic *ContainerEngine) SystemDf(ctx context.Context, options entities.System return nil, err } if len(inUse) == 0 { - reclaimableSize += volSize + reclaimableSize = volSize } for _, viu := range inUse { if cutil.StringInSlice(viu, runningContainers) { diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 97838d596..09f8ac4c3 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -2,6 +2,7 @@ package tunnel import ( "context" + "fmt" "io/ioutil" "os" "strconv" @@ -12,7 +13,6 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/types" - "github.com/containers/podman/v4/libpod/define" "github.com/containers/podman/v4/pkg/bindings/images" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/containers/podman/v4/pkg/domain/entities/reports" @@ -28,7 +28,7 @@ func (ir *ImageEngine) Exists(_ context.Context, nameOrID string) (*entities.Boo } func (ir *ImageEngine) Remove(ctx context.Context, imagesArg []string, opts entities.ImageRemoveOptions) (*entities.ImageRemoveReport, []error) { - options := new(images.RemoveOptions).WithForce(opts.Force).WithIgnore(opts.Ignore).WithAll(opts.All) + options := new(images.RemoveOptions).WithForce(opts.Force).WithIgnore(opts.Ignore).WithAll(opts.All).WithLookupManifest(opts.LookupManifest) return images.Remove(ir.ClientCtx, imagesArg, options) } @@ -123,10 +123,6 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, opts entities. return &entities.ImagePullReport{Images: pulledImages}, nil } -func (ir *ImageEngine) Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error { - return errors.Wrapf(define.ErrNotImplemented, "cannot use the remote client to transfer images between root and rootless storage") -} - func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, opt entities.ImageTagOptions) error { options := new(images.TagOptions) for _, newTag := range tags { @@ -367,3 +363,23 @@ func (ir *ImageEngine) Shutdown(_ context.Context) { func (ir *ImageEngine) Sign(ctx context.Context, names []string, options entities.SignOptions) (*entities.SignReport, error) { return nil, errors.New("not implemented yet") } + +func (ir *ImageEngine) Scp(ctx context.Context, src, dst string, parentFlags []string, quiet bool) error { + options := new(images.ScpOptions) + + var destination *string + if len(dst) > 1 { + destination = &dst + } + options.Quiet = &quiet + options.Destination = destination + + rep, err := images.Scp(ir.ClientCtx, &src, destination, *options) + if err != nil { + return err + } + + fmt.Println("Loaded Image(s):", rep.Id) + + return nil +} diff --git a/pkg/domain/utils/scp.go b/pkg/domain/utils/scp.go new file mode 100644 index 000000000..a4ff6b950 --- /dev/null +++ b/pkg/domain/utils/scp.go @@ -0,0 +1,581 @@ +package utils + +import ( + "bytes" + "fmt" + "io/ioutil" + "net" + "net/url" + "os" + "os/exec" + "os/user" + "strconv" + "strings" + "time" + + scpD "github.com/dtylman/scp" + + "github.com/containers/common/pkg/config" + "github.com/containers/podman/v4/libpod/define" + "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/containers/podman/v4/pkg/terminal" + "github.com/docker/distribution/reference" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +func ExecuteTransfer(src, dst string, parentFlags []string, quiet bool) (*entities.ImageLoadReport, *entities.ImageScpOptions, *entities.ImageScpOptions, []string, error) { + source := entities.ImageScpOptions{} + dest := entities.ImageScpOptions{} + sshInfo := entities.ImageScpConnections{} + report := entities.ImageLoadReport{Names: []string{}} + + podman, err := os.Executable() + if err != nil { + return nil, nil, nil, nil, err + } + + f, err := ioutil.TempFile("", "podman") // open temp file for load/save output + if err != nil { + return nil, nil, nil, nil, err + } + + confR, err := config.NewConfig("") // create a hand made config for the remote engine since we might use remote and native at once + if err != nil { + return nil, nil, nil, nil, errors.Wrapf(err, "could not make config") + } + + cfg, err := config.ReadCustomConfig() // get ready to set ssh destination if necessary + if err != nil { + return nil, nil, nil, nil, err + } + locations := []*entities.ImageScpOptions{} + cliConnections := []string{} + args := []string{src} + if len(dst) > 0 { + args = append(args, dst) + } + for _, arg := range args { + loc, connect, err := ParseImageSCPArg(arg) + if err != nil { + return nil, nil, nil, nil, err + } + locations = append(locations, loc) + cliConnections = append(cliConnections, connect...) + } + source = *locations[0] + switch { + case len(locations) > 1: + if err = ValidateSCPArgs(locations); err != nil { + return nil, nil, nil, nil, err + } + dest = *locations[1] + case len(locations) == 1: + switch { + case len(locations[0].Image) == 0: + return nil, nil, nil, nil, errors.Wrapf(define.ErrInvalidArg, "no source image specified") + case len(locations[0].Image) > 0 && !locations[0].Remote && len(locations[0].User) == 0: // if we have podman image scp $IMAGE + return nil, nil, nil, nil, errors.Wrapf(define.ErrInvalidArg, "must specify a destination") + } + } + + source.Quiet = quiet + source.File = f.Name() // after parsing the arguments, set the file for the save/load + dest.File = source.File + if err = os.Remove(source.File); err != nil { // remove the file and simply use its name so podman creates the file upon save. avoids umask errors + return nil, nil, nil, nil, err + } + + allLocal := true // if we are all localhost, do not validate connections but if we are using one localhost and one non we need to use sshd + for _, val := range cliConnections { + if !strings.Contains(val, "@localhost::") { + allLocal = false + break + } + } + if allLocal { + cliConnections = []string{} + } + + var serv map[string]config.Destination + serv, err = GetServiceInformation(&sshInfo, cliConnections, cfg) + if err != nil { + return nil, nil, nil, nil, err + } + + confR.Engine = config.EngineConfig{Remote: true, CgroupManager: "cgroupfs", ServiceDestinations: serv} // pass the service dest (either remote or something else) to engine + saveCmd, loadCmd := CreateCommands(source, dest, parentFlags, podman) + + switch { + case source.Remote: // if we want to load FROM the remote, dest can either be local or remote in this case + err = SaveToRemote(source.Image, source.File, "", sshInfo.URI[0], sshInfo.Identities[0]) + if err != nil { + return nil, nil, nil, nil, err + } + if dest.Remote { // we want to load remote -> remote, both source and dest are remote + rep, id, err := LoadToRemote(dest, dest.File, "", sshInfo.URI[1], sshInfo.Identities[1]) + if err != nil { + return nil, nil, nil, nil, err + } + if len(rep) > 0 { + fmt.Println(rep) + } + if len(id) > 0 { + report.Names = append(report.Names, id) + } + break + } + id, err := ExecPodman(dest, podman, loadCmd) + if err != nil { + return nil, nil, nil, nil, err + } + if len(id) > 0 { + report.Names = append(report.Names, id) + } + case dest.Remote: // remote host load, implies source is local + _, err = ExecPodman(dest, podman, saveCmd) + if err != nil { + return nil, nil, nil, nil, err + } + rep, id, err := LoadToRemote(dest, source.File, "", sshInfo.URI[0], sshInfo.Identities[0]) + if err != nil { + return nil, nil, nil, nil, err + } + if len(rep) > 0 { + fmt.Println(rep) + } + if len(id) > 0 { + report.Names = append(report.Names, id) + } + if err = os.Remove(source.File); err != nil { + return nil, nil, nil, nil, err + } + default: // else native load, both source and dest are local and transferring between users + if source.User == "" { // source user has to be set, destination does not + source.User = os.Getenv("USER") + if source.User == "" { + u, err := user.Current() + if err != nil { + return nil, nil, nil, nil, errors.Wrapf(err, "could not obtain user, make sure the environmental variable $USER is set") + } + source.User = u.Username + } + } + return nil, &source, &dest, parentFlags, nil // transfer needs to be done in ABI due to cross issues + } + + return &report, nil, nil, nil, nil +} + +// CreateSCPCommand takes an existing command, appends the given arguments and returns a configured podman command for image scp +func CreateSCPCommand(cmd *exec.Cmd, command []string) *exec.Cmd { + cmd.Args = append(cmd.Args, command...) + cmd.Env = os.Environ() + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd +} + +// ScpTag is a helper function for native podman to tag an image after a local load from image SCP +func ScpTag(cmd *exec.Cmd, podman string, dest entities.ImageScpOptions) error { + cmd.Stdout = nil + out, err := cmd.Output() // this function captures the output temporarily in order to execute the next command + if err != nil { + return err + } + image := ExtractImage(out) + if cmd.Args[0] == "sudo" { // transferRootless will need the sudo since we are loading to sudo from a user acct + cmd = exec.Command("sudo", podman, "tag", image, dest.Tag) + } else { + cmd = exec.Command(podman, "tag", image, dest.Tag) + } + cmd.Stdout = os.Stdout + return cmd.Run() +} + +// ExtractImage pulls out the last line of output from save/load (image id) +func ExtractImage(out []byte) string { + fmt.Println(strings.TrimSuffix(string(out), "\n")) // print output + stringOut := string(out) // get all output + arrOut := strings.Split(stringOut, " ") // split it into an array + return strings.ReplaceAll(arrOut[len(arrOut)-1], "\n", "") // replace the trailing \n +} + +// LoginUser starts the user process on the host so that image scp can use systemd-run +func LoginUser(user string) (*exec.Cmd, error) { + sleep, err := exec.LookPath("sleep") + if err != nil { + return nil, err + } + machinectl, err := exec.LookPath("machinectl") + if err != nil { + return nil, err + } + cmd := exec.Command(machinectl, "shell", "-q", user+"@.host", sleep, "inf") + err = cmd.Start() + return cmd, err +} + +// loadToRemote takes image and remote connection information. it connects to the specified client +// and copies the saved image dir over to the remote host and then loads it onto the machine +// returns a string containing output or an error +func LoadToRemote(dest entities.ImageScpOptions, localFile string, tag string, url *url.URL, iden string) (string, string, error) { + dial, remoteFile, err := CreateConnection(url, iden) + if err != nil { + return "", "", err + } + defer dial.Close() + + n, err := scpD.CopyTo(dial, localFile, remoteFile) + if err != nil { + errOut := strconv.Itoa(int(n)) + " Bytes copied before error" + return " ", "", errors.Wrapf(err, errOut) + } + var run string + if tag != "" { + return "", "", errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported") + } + podman := os.Args[0] + run = podman + " image load --input=" + remoteFile + ";rm " + remoteFile // run ssh image load of the file copied via scp + out, err := ExecRemoteCommand(dial, run) + if err != nil { + return "", "", err + } + rep := strings.TrimSuffix(string(out), "\n") + outArr := strings.Split(rep, " ") + id := outArr[len(outArr)-1] + if len(dest.Tag) > 0 { // tag the remote image using the output ID + run = podman + " tag " + id + " " + dest.Tag + _, err = ExecRemoteCommand(dial, run) + if err != nil { + return "", "", err + } + } + return rep, id, nil +} + +// saveToRemote takes image information and remote connection information. it connects to the specified client +// and saves the specified image on the remote machine and then copies it to the specified local location +// returns an error if one occurs. +func SaveToRemote(image, localFile string, tag string, uri *url.URL, iden string) error { + dial, remoteFile, err := CreateConnection(uri, iden) + + if err != nil { + return err + } + defer dial.Close() + + if tag != "" { + return errors.Wrapf(define.ErrInvalidArg, "Renaming of an image is currently not supported") + } + podman := os.Args[0] + run := podman + " image save " + image + " --format=oci-archive --output=" + remoteFile // run ssh image load of the file copied via scp. Files are reverse in this case... + _, err = ExecRemoteCommand(dial, run) + if err != nil { + return err + } + n, err := scpD.CopyFrom(dial, remoteFile, localFile) + if _, conErr := ExecRemoteCommand(dial, "rm "+remoteFile); conErr != nil { + logrus.Errorf("Removing file on endpoint: %v", conErr) + } + if err != nil { + errOut := strconv.Itoa(int(n)) + " Bytes copied before error" + return errors.Wrapf(err, errOut) + } + return nil +} + +// makeRemoteFile creates the necessary remote file on the host to +// save or load the image to. returns a string with the file name or an error +func MakeRemoteFile(dial *ssh.Client) (string, error) { + run := "mktemp" + remoteFile, err := ExecRemoteCommand(dial, run) + if err != nil { + return "", err + } + return strings.TrimSuffix(string(remoteFile), "\n"), nil +} + +// createConnections takes a boolean determining which ssh client to dial +// and returns the dials client, its newly opened remote file, and an error if applicable. +func CreateConnection(url *url.URL, iden string) (*ssh.Client, string, error) { + cfg, err := ValidateAndConfigure(url, iden) + if err != nil { + return nil, "", err + } + dialAdd, err := ssh.Dial("tcp", url.Host, cfg) // dial the client + if err != nil { + return nil, "", errors.Wrapf(err, "failed to connect") + } + file, err := MakeRemoteFile(dialAdd) + if err != nil { + return nil, "", err + } + + return dialAdd, file, nil +} + +// GetSerivceInformation takes the parsed list of hosts to connect to and validates the information +func GetServiceInformation(sshInfo *entities.ImageScpConnections, cliConnections []string, cfg *config.Config) (map[string]config.Destination, error) { + var serv map[string]config.Destination + var urlS string + var iden string + for i, val := range cliConnections { + splitEnv := strings.SplitN(val, "::", 2) + sshInfo.Connections = append(sshInfo.Connections, splitEnv[0]) + conn, found := cfg.Engine.ServiceDestinations[sshInfo.Connections[i]] + if found { + urlS = conn.URI + iden = conn.Identity + } else { // no match, warn user and do a manual connection. + urlS = "ssh://" + sshInfo.Connections[i] + iden = "" + logrus.Warnf("Unknown connection name given. Please use system connection add to specify the default remote socket location") + } + urlFinal, err := url.Parse(urlS) // create an actual url to pass to exec command + if err != nil { + return nil, err + } + if urlFinal.User.Username() == "" { + if urlFinal.User, err = GetUserInfo(urlFinal); err != nil { + return nil, err + } + } + sshInfo.URI = append(sshInfo.URI, urlFinal) + sshInfo.Identities = append(sshInfo.Identities, iden) + } + return serv, nil +} + +// execPodman executes the podman save/load command given the podman binary +func ExecPodman(dest entities.ImageScpOptions, podman string, command []string) (string, error) { + cmd := exec.Command(podman) + CreateSCPCommand(cmd, command[1:]) + logrus.Debugf("Executing podman command: %q", cmd) + if strings.Contains(strings.Join(command, " "), "load") { // need to tag + if len(dest.Tag) > 0 { + return "", ScpTag(cmd, podman, dest) + } + cmd.Stdout = nil + out, err := cmd.Output() + if err != nil { + return "", err + } + img := ExtractImage(out) + return img, nil + } + return "", cmd.Run() +} + +// createCommands forms the podman save and load commands used by SCP +func CreateCommands(source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string, podman string) ([]string, []string) { + var parentString string + quiet := "" + if source.Quiet { + quiet = "-q " + } + if len(parentFlags) > 0 { + parentString = strings.Join(parentFlags, " ") + " " // if there are parent args, an extra space needs to be added + } else { + parentString = strings.Join(parentFlags, " ") + } + loadCmd := strings.Split(fmt.Sprintf("%s %sload %s--input %s", podman, parentString, quiet, dest.File), " ") + saveCmd := strings.Split(fmt.Sprintf("%s %vsave %s--output %s %s", podman, parentString, quiet, source.File, source.Image), " ") + return saveCmd, loadCmd +} + +// parseImageSCPArg returns the valid connection, and source/destination data based off of the information provided by the user +// arg is a string containing one of the cli arguments returned is a filled out source/destination options structs as well as a connections array and an error if applicable +func ParseImageSCPArg(arg string) (*entities.ImageScpOptions, []string, error) { + location := entities.ImageScpOptions{} + var err error + cliConnections := []string{} + + switch { + case strings.Contains(arg, "@localhost::"): // image transfer between users + location.User = strings.Split(arg, "@")[0] + location, err = ValidateImagePortion(location, arg) + if err != nil { + return nil, nil, err + } + cliConnections = append(cliConnections, arg) + case strings.Contains(arg, "::"): + location, err = ValidateImagePortion(location, arg) + if err != nil { + return nil, nil, err + } + location.Remote = true + cliConnections = append(cliConnections, arg) + default: + location.Image = arg + } + return &location, cliConnections, nil +} + +// validateImagePortion is a helper function to validate the image name in an SCP argument +func ValidateImagePortion(location entities.ImageScpOptions, arg string) (entities.ImageScpOptions, error) { + if RemoteArgLength(arg, 1) > 0 { + err := ValidateImageName(strings.Split(arg, "::")[1]) + if err != nil { + return location, err + } + location.Image = strings.Split(arg, "::")[1] // this will get checked/set again once we validate connections + } + return location, nil +} + +// validateSCPArgs takes the array of source and destination options and checks for common errors +func ValidateSCPArgs(locations []*entities.ImageScpOptions) error { + if len(locations) > 2 { + return errors.Wrapf(define.ErrInvalidArg, "cannot specify more than two arguments") + } + switch { + case len(locations[0].Image) > 0 && len(locations[1].Image) > 0: + locations[1].Tag = locations[1].Image + locations[1].Image = "" + case len(locations[0].Image) == 0 && len(locations[1].Image) == 0: + return errors.Wrapf(define.ErrInvalidArg, "a source image must be specified") + } + return nil +} + +// validateImageName makes sure that the image given is valid and no injections are occurring +// we simply use this for error checking, bot setting the image +func ValidateImageName(input string) error { + // ParseNormalizedNamed transforms a shortname image into its + // full name reference so busybox => docker.io/library/busybox + // we want to keep our shortnames, so only return an error if + // we cannot parse what the user has given us + _, err := reference.ParseNormalizedNamed(input) + return err +} + +// remoteArgLength is a helper function to simplify the extracting of host argument data +// returns an int which contains the length of a specified index in a host::image string +func RemoteArgLength(input string, side int) int { + if strings.Contains(input, "::") { + return len((strings.Split(input, "::"))[side]) + } + return -1 +} + +// ExecRemoteCommand takes a ssh client connection and a command to run and executes the +// command on the specified client. The function returns the Stdout from the client or the Stderr +func ExecRemoteCommand(dial *ssh.Client, run string) ([]byte, error) { + sess, err := dial.NewSession() // new ssh client session + if err != nil { + return nil, err + } + defer sess.Close() + + var buffer bytes.Buffer + var bufferErr bytes.Buffer + sess.Stdout = &buffer // output from client funneled into buffer + sess.Stderr = &bufferErr // err form client funneled into buffer + if err := sess.Run(run); err != nil { // run the command on the ssh client + return nil, errors.Wrapf(err, bufferErr.String()) + } + return buffer.Bytes(), nil +} + +func GetUserInfo(uri *url.URL) (*url.Userinfo, error) { + var ( + usr *user.User + err error + ) + if u, found := os.LookupEnv("_CONTAINERS_ROOTLESS_UID"); found { + usr, err = user.LookupId(u) + if err != nil { + return nil, errors.Wrapf(err, "failed to lookup rootless user") + } + } else { + usr, err = user.Current() + if err != nil { + return nil, errors.Wrapf(err, "failed to obtain current user") + } + } + + pw, set := uri.User.Password() + if set { + return url.UserPassword(usr.Username, pw), nil + } + return url.User(usr.Username), nil +} + +// ValidateAndConfigure will take a ssh url and an identity key (rsa and the like) and ensure the information given is valid +// iden iden can be blank to mean no identity key +// once the function validates the information it creates and returns an ssh.ClientConfig. +func ValidateAndConfigure(uri *url.URL, iden string) (*ssh.ClientConfig, error) { + var signers []ssh.Signer + passwd, passwdSet := uri.User.Password() + if iden != "" { // iden might be blank if coming from image scp or if no validation is needed + value := iden + s, err := terminal.PublicKey(value, []byte(passwd)) + if err != nil { + return nil, errors.Wrapf(err, "failed to read identity %q", value) + } + signers = append(signers, s) + logrus.Debugf("SSH Ident Key %q %s %s", value, ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type()) + } + if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { // validate ssh information, specifically the unix file socket used by the ssh agent. + logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock) + + c, err := net.Dial("unix", sock) + if err != nil { + return nil, err + } + agentSigners, err := agent.NewClient(c).Signers() + if err != nil { + return nil, err + } + + signers = append(signers, agentSigners...) + + if logrus.IsLevelEnabled(logrus.DebugLevel) { + for _, s := range agentSigners { + logrus.Debugf("SSH Agent Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type()) + } + } + } + var authMethods []ssh.AuthMethod // now we validate and check for the authorization methods, most notaibly public key authorization + if len(signers) > 0 { + var dedup = make(map[string]ssh.Signer) + for _, s := range signers { + fp := ssh.FingerprintSHA256(s.PublicKey()) + if _, found := dedup[fp]; found { + logrus.Debugf("Dedup SSH Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type()) + } + dedup[fp] = s + } + + var uniq []ssh.Signer + for _, s := range dedup { + uniq = append(uniq, s) + } + authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { + return uniq, nil + })) + } + if passwdSet { // if password authentication is given and valid, add to the list + authMethods = append(authMethods, ssh.Password(passwd)) + } + if len(authMethods) == 0 { + authMethods = append(authMethods, ssh.PasswordCallback(func() (string, error) { + pass, err := terminal.ReadPassword(fmt.Sprintf("%s's login password:", uri.User.Username())) + return string(pass), err + })) + } + tick, err := time.ParseDuration("40s") + if err != nil { + return nil, err + } + cfg := &ssh.ClientConfig{ + User: uri.User.Username(), + Auth: authMethods, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: tick, + } + return cfg, nil +} diff --git a/pkg/domain/utils/utils_test.go b/pkg/domain/utils/utils_test.go index 952a4b5be..291567f6b 100644 --- a/pkg/domain/utils/utils_test.go +++ b/pkg/domain/utils/utils_test.go @@ -5,6 +5,7 @@ import ( "sort" "testing" + "github.com/containers/podman/v4/pkg/domain/entities" "github.com/stretchr/testify/assert" ) @@ -74,3 +75,41 @@ func TestToURLValues(t *testing.T) { }) } } + +func TestParseSCPArgs(t *testing.T) { + args := []string{"alpine", "root@localhost::"} + var source *entities.ImageScpOptions + var dest *entities.ImageScpOptions + var err error + source, _, err = ParseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.Equal(t, source.Image, "alpine") + + dest, _, err = ParseImageSCPArg(args[1]) + assert.Nil(t, err) + assert.Equal(t, dest.Image, "") + assert.Equal(t, dest.User, "root") + + args = []string{"root@localhost::alpine"} + source, _, err = ParseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.Equal(t, source.User, "root") + assert.Equal(t, source.Image, "alpine") + + args = []string{"charliedoern@192.168.68.126::alpine", "foobar@192.168.68.126::"} + source, _, err = ParseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.True(t, source.Remote) + assert.Equal(t, source.Image, "alpine") + + dest, _, err = ParseImageSCPArg(args[1]) + assert.Nil(t, err) + assert.True(t, dest.Remote) + assert.Equal(t, dest.Image, "") + + args = []string{"charliedoern@192.168.68.126::alpine"} + source, _, err = ParseImageSCPArg(args[0]) + assert.Nil(t, err) + assert.True(t, source.Remote) + assert.Equal(t, source.Image, "alpine") +} diff --git a/pkg/errorhandling/errorhandling.go b/pkg/errorhandling/errorhandling.go index fc6772c08..9b456c9c0 100644 --- a/pkg/errorhandling/errorhandling.go +++ b/pkg/errorhandling/errorhandling.go @@ -1,11 +1,11 @@ package errorhandling import ( + "errors" "os" "strings" "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -121,3 +121,22 @@ func (e PodConflictErrorModel) Error() string { func (e PodConflictErrorModel) Code() int { return 409 } + +// Cause returns the most underlying error for the provided one. There is a +// maximum error depth of 100 to avoid endless loops. An additional error log +// message will be created if this maximum has reached. +func Cause(err error) (cause error) { + cause = err + + const maxDepth = 100 + for i := 0; i <= maxDepth; i++ { + res := errors.Unwrap(cause) + if res == nil { + return cause + } + cause = res + } + + logrus.Errorf("Max error depth of %d reached, cannot unwrap until root cause: %v", maxDepth, err) + return cause +} diff --git a/pkg/errorhandling/errorhandling_test.go b/pkg/errorhandling/errorhandling_test.go new file mode 100644 index 000000000..ec720c5e7 --- /dev/null +++ b/pkg/errorhandling/errorhandling_test.go @@ -0,0 +1,53 @@ +package errorhandling + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCause(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + err func() error + expectedErr error + }{ + { + name: "nil error", + err: func() error { return nil }, + expectedErr: nil, + }, + { + name: "equal errors", + err: func() error { return errors.New("foo") }, + expectedErr: errors.New("foo"), + }, + { + name: "wrapped error", + err: func() error { return fmt.Errorf("baz: %w", fmt.Errorf("bar: %w", errors.New("foo"))) }, + expectedErr: errors.New("foo"), + }, + { + name: "max depth reached", + err: func() error { + err := errors.New("error") + for i := 0; i <= 101; i++ { + err = fmt.Errorf("%d: %w", i, err) + } + return err + }, + expectedErr: fmt.Errorf("0: %w", errors.New("error")), + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := Cause(tc.err()) + assert.Equal(t, tc.expectedErr, err) + }) + } +} diff --git a/pkg/k8s.io/apimachinery/pkg/api/resource/amount.go b/pkg/k8s.io/apimachinery/pkg/api/resource/amount.go index d05984dac..69613321f 100644 --- a/pkg/k8s.io/apimachinery/pkg/api/resource/amount.go +++ b/pkg/k8s.io/apimachinery/pkg/api/resource/amount.go @@ -48,7 +48,7 @@ const ( var ( Zero = int64Amount{} - // Used by quantity strings - treat as read only + // Used by quantity strings - treat as read-only zeroBytes = []byte("0") ) diff --git a/pkg/k8s.io/apimachinery/pkg/api/resource/math.go b/pkg/k8s.io/apimachinery/pkg/api/resource/math.go index 9d03f5c05..59a4c14de 100644 --- a/pkg/k8s.io/apimachinery/pkg/api/resource/math.go +++ b/pkg/k8s.io/apimachinery/pkg/api/resource/math.go @@ -29,13 +29,13 @@ const ( ) var ( - // Commonly needed big.Int values-- treat as read only! + // Commonly needed big.Int values-- treat as read-only! bigTen = big.NewInt(10) bigZero = big.NewInt(0) bigOne = big.NewInt(1) big1024 = big.NewInt(1024) - // Commonly needed inf.Dec values-- treat as read only! + // Commonly needed inf.Dec values-- treat as read-only! decZero = inf.NewDec(0, 0) decOne = inf.NewDec(1, 0) diff --git a/pkg/machine/config_test.go b/pkg/machine/config_test.go index d9fc5425e..ca08660b9 100644 --- a/pkg/machine/config_test.go +++ b/pkg/machine/config_test.go @@ -1,3 +1,6 @@ +//go:build amd64 || arm64 +// +build amd64 arm64 + package machine import ( diff --git a/pkg/machine/keys.go b/pkg/machine/keys.go index 45d9801cc..463271427 100644 --- a/pkg/machine/keys.go +++ b/pkg/machine/keys.go @@ -4,14 +4,15 @@ package machine import ( - "errors" "fmt" + "io" "io/ioutil" "os" "os/exec" "path/filepath" "strings" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -51,7 +52,23 @@ func CreateSSHKeysPrefix(dir string, file string, passThru bool, skipExisting bo // generatekeys creates an ed25519 set of keys func generatekeys(writeLocation string) error { args := append(append([]string{}, sshCommand[1:]...), writeLocation) - return exec.Command(sshCommand[0], args...).Run() + cmd := exec.Command(sshCommand[0], args...) + stdErr, err := cmd.StderrPipe() + if err != nil { + return err + } + if err := cmd.Start(); err != nil { + return err + } + waitErr := cmd.Wait() + if waitErr == nil { + return nil + } + errMsg, err := io.ReadAll(stdErr) + if err != nil { + return fmt.Errorf("key generation failed, unable to read from stderr: %w", waitErr) + } + return fmt.Errorf("failed to generate keys: %s: %w", string(errMsg), waitErr) } // generatekeys creates an ed25519 set of keys diff --git a/pkg/machine/qemu/config.go b/pkg/machine/qemu/config.go index 56c95e3b3..bada1af9b 100644 --- a/pkg/machine/qemu/config.go +++ b/pkg/machine/qemu/config.go @@ -72,8 +72,10 @@ type MachineVM struct { Mounts []machine.Mount // Name of VM Name string - // PidFilePath is the where the PID file lives + // PidFilePath is the where the Proxy PID file lives PidFilePath machine.VMFile + // VMPidFilePath is the where the VM PID file lives + VMPidFilePath machine.VMFile // QMPMonitor is the qemu monitor object for sending commands QMPMonitor Monitor // ReadySocket tells host when vm is booted diff --git a/pkg/machine/qemu/config_test.go b/pkg/machine/qemu/config_test.go index 4d96ec6e7..72cb3ed90 100644 --- a/pkg/machine/qemu/config_test.go +++ b/pkg/machine/qemu/config_test.go @@ -1,3 +1,6 @@ +//go:build (amd64 && !windows) || (arm64 && !windows) +// +build amd64,!windows arm64,!windows + package qemu import ( diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 3a1495021..7e9c786a9 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -32,6 +32,7 @@ import ( "github.com/docker/go-units" "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" ) var ( @@ -107,6 +108,9 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { if err != nil { return nil, err } + if err := vm.setPIDSocket(); err != nil { + return nil, err + } cmd := []string{execPath} // Add memory cmd = append(cmd, []string{"-m", strconv.Itoa(int(vm.Memory))}...) @@ -135,11 +139,9 @@ func (p *Provider) NewMachine(opts machine.InitOptions) (machine.VM, error) { "-device", "virtio-serial", // qemu needs to establish the long name; other connections can use the symlink'd "-chardev", "socket,path=" + vm.ReadySocket.Path + ",server=on,wait=off,id=" + vm.Name + "_ready", - "-device", "virtserialport,chardev=" + vm.Name + "_ready" + ",name=org.fedoraproject.port.0"}...) + "-device", "virtserialport,chardev=" + vm.Name + "_ready" + ",name=org.fedoraproject.port.0", + "-pidfile", vm.VMPidFilePath.GetPath()}...) vm.CmdLine = cmd - if err := vm.setPIDSocket(); err != nil { - return nil, err - } return vm, nil } @@ -753,17 +755,17 @@ func (v *MachineVM) Stop(_ string, _ machine.StopOptions) error { if _, err := os.Stat(v.PidFilePath.GetPath()); os.IsNotExist(err) { return nil } - pidString, err := v.PidFilePath.Read() + proxyPidString, err := v.PidFilePath.Read() if err != nil { return err } - pidNum, err := strconv.Atoi(string(pidString)) + proxyPid, err := strconv.Atoi(string(proxyPidString)) if err != nil { return err } - p, err := os.FindProcess(pidNum) - if p == nil && err != nil { + proxyProc, err := os.FindProcess(proxyPid) + if proxyProc == nil && err != nil { return err } @@ -772,7 +774,7 @@ func (v *MachineVM) Stop(_ string, _ machine.StopOptions) error { return err } // Kill the process - if err := p.Kill(); err != nil { + if err := proxyProc.Kill(); err != nil { return err } // Remove the pidfile @@ -788,22 +790,50 @@ func (v *MachineVM) Stop(_ string, _ machine.StopOptions) error { // FIXME: this error should probably be returned return nil //nolint: nilerr } - disconnected = true - waitInternal := 250 * time.Millisecond - for i := 0; i < 5; i++ { - state, err := v.State(false) - if err != nil { - return err - } - if state != machine.Running { - break + + if err := v.ReadySocket.Delete(); err != nil { + return err + } + + if v.VMPidFilePath.GetPath() == "" { + // no vm pid file path means it's probably a machine created before we + // started using it, so we revert to the old way of waiting for the + // machine to stop + fmt.Println("Waiting for VM to stop running...") + waitInternal := 250 * time.Millisecond + for i := 0; i < 5; i++ { + state, err := v.State(false) + if err != nil { + return err + } + if state != machine.Running { + break + } + time.Sleep(waitInternal) + waitInternal *= 2 } - time.Sleep(waitInternal) - waitInternal *= 2 + // after the machine stops running it normally takes about 1 second for the + // qemu VM to exit so we wait a bit to try to avoid issues + time.Sleep(2 * time.Second) + return nil + } + + vmPidString, err := v.VMPidFilePath.Read() + if err != nil { + return err + } + vmPid, err := strconv.Atoi(strings.TrimSpace(string(vmPidString))) + if err != nil { + return err } - return v.ReadySocket.Delete() + fmt.Println("Waiting for VM to exit...") + for isProcessAlive(vmPid) { + time.Sleep(500 * time.Millisecond) + } + + return nil } // NewQMPMonitor creates the monitor subsection of our vm @@ -896,8 +926,11 @@ func (v *MachineVM) Remove(_ string, opts machine.RemoveOptions) (string, func() // remove socket and pid file if any: warn at low priority if things fail // Remove the pidfile + if err := v.VMPidFilePath.Delete(); err != nil { + logrus.Debugf("Error while removing VM pidfile: %v", err) + } if err := v.PidFilePath.Delete(); err != nil { - logrus.Debugf("Error while removing pidfile: %v", err) + logrus.Debugf("Error while removing proxy pidfile: %v", err) } // Remove socket if err := v.QMPMonitor.Address.Delete(); err != nil { @@ -930,7 +963,12 @@ func (v *MachineVM) State(bypass bool) (machine.Status, error) { } monitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address.GetPath(), v.QMPMonitor.Timeout) if err != nil { - // FIXME: this error should probably be returned + // If an improper cleanup was done and the socketmonitor was not deleted, + // it can appear as though the machine state is not stopped. Check for ECONNREFUSED + // almost assures us that the vm is stopped. + if errors.Is(err, syscall.ECONNREFUSED) { + return machine.Stopped, nil + } return "", err } if err := monitor.Connect(); err != nil { @@ -975,7 +1013,7 @@ func (v *MachineVM) SSH(_ string, opts machine.SSHOptions) error { port := strconv.Itoa(v.Port) args := []string{"-i", v.IdentityPath, "-p", port, sshDestination, "-o", "UserKnownHostsFile=/dev/null", - "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR"} + "-o", "StrictHostKeyChecking=no", "-o", "LogLevel=ERROR", "-o", "SetEnv=LC_ALL="} if len(opts.Args) > 0 { args = append(args, opts.Args...) } else { @@ -1305,13 +1343,19 @@ func (v *MachineVM) setPIDSocket() error { if !rootless.IsRootless() { rtPath = "/run" } - pidFileName := fmt.Sprintf("%s.pid", v.Name) socketDir := filepath.Join(rtPath, "podman") - pidFilePath, err := machine.NewMachineFile(filepath.Join(socketDir, pidFileName), &pidFileName) + vmPidFileName := fmt.Sprintf("%s_vm.pid", v.Name) + proxyPidFileName := fmt.Sprintf("%s_proxy.pid", v.Name) + vmPidFilePath, err := machine.NewMachineFile(filepath.Join(socketDir, vmPidFileName), &vmPidFileName) + if err != nil { + return err + } + proxyPidFilePath, err := machine.NewMachineFile(filepath.Join(socketDir, proxyPidFileName), &proxyPidFileName) if err != nil { return err } - v.PidFilePath = *pidFilePath + v.VMPidFilePath = *vmPidFilePath + v.PidFilePath = *proxyPidFilePath return nil } @@ -1648,3 +1692,12 @@ func (p *Provider) RemoveAndCleanMachines() error { } return prevErr } + +func isProcessAlive(pid int) bool { + err := unix.Kill(pid, syscall.Signal(0)) + if err == nil || err == unix.EPERM { + return true + } + + return false +} diff --git a/pkg/machine/qemu/machine_test.go b/pkg/machine/qemu/machine_test.go index 62ca6068a..4c393d0f4 100644 --- a/pkg/machine/qemu/machine_test.go +++ b/pkg/machine/qemu/machine_test.go @@ -1,3 +1,6 @@ +//go:build (amd64 && !windows) || (arm64 && !windows) +// +build amd64,!windows arm64,!windows + package qemu import ( diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index 30c759495..8fdd87adf 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -38,10 +38,19 @@ func getImageFromSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGen } // Need to look up image. - image, resolvedName, err := r.LibimageRuntime().LookupImage(s.Image, nil) + lookupOptions := &libimage.LookupImageOptions{ManifestList: true} + image, resolvedName, err := r.LibimageRuntime().LookupImage(s.Image, lookupOptions) if err != nil { return nil, "", nil, err } + manifestList, err := image.ToManifestList() + // only process if manifest list found otherwise expect it to be regular image + if err == nil { + image, err = manifestList.LookupInstance(ctx, s.ImageArch, s.ImageOS, s.ImageVariant) + if err != nil { + return nil, "", nil, err + } + } s.SetImage(image, resolvedName) inspectData, err := image.Inspect(ctx, nil) if err != nil { diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index 689c740f0..39e15f950 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -810,8 +810,8 @@ func envVarValueResourceFieldRef(env v1.EnvVar, opts *CtrSpecGenOptions) (*strin } // k8s rounds up the result to the nearest integer - intValue := int(math.Ceil(value.AsApproximateFloat64() / divisor.AsApproximateFloat64())) - stringValue := strconv.Itoa(intValue) + intValue := int64(math.Ceil(value.AsApproximateFloat64() / divisor.AsApproximateFloat64())) + stringValue := strconv.FormatInt(intValue, 10) return &stringValue, nil } diff --git a/pkg/specgen/generate/kube/play_test.go b/pkg/specgen/generate/kube/play_test.go index e01d62b08..466dab610 100644 --- a/pkg/specgen/generate/kube/play_test.go +++ b/pkg/specgen/generate/kube/play_test.go @@ -2,7 +2,6 @@ package kube import ( "encoding/json" - "fmt" "math" "runtime" "strconv" @@ -777,8 +776,7 @@ func TestEnvVarValue(t *testing.T) { if test.expected == nilString { assert.Nil(t, result) } else { - fmt.Println(*result, test.expected) - assert.Equal(t, &(test.expected), result) + assert.Equal(t, test.expected, *result) } }) } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 79e20667b..42b89ece1 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -103,6 +103,12 @@ type ContainerBasicConfig struct { // RawImageName is the user-specified and unprocessed input referring // to a local or a remote image. RawImageName string `json:"raw_image_name,omitempty"` + // ImageOS is the user-specified image OS + ImageOS string `json:"image_os,omitempty"` + // ImageArch is the user-specified image architecture + ImageArch string `json:"image_arch,omitempty"` + // ImageVariant is the user-specified image variant + ImageVariant string `json:"image_variant,omitempty"` // RestartPolicy is the container's restart policy - an action which // will be taken when the container exits. // If not given, the default policy, which does nothing, will be used. diff --git a/pkg/specgen/volumes.go b/pkg/specgen/volumes.go index f272a5c11..c9f944abf 100644 --- a/pkg/specgen/volumes.go +++ b/pkg/specgen/volumes.go @@ -37,7 +37,7 @@ type OverlayVolume struct { // ImageVolume is a volume based on a container image. The container image is // first mounted on the host and is then bind-mounted into the container. An -// ImageVolume is always mounted read only. +// ImageVolume is always mounted read-only. type ImageVolume struct { // Source is the source of the image volume. The image can be referred // to by name and by ID. @@ -139,7 +139,13 @@ func GenVolumeMounts(volumeFlag []string) (map[string]spec.Mount, map[string]*Na // This is a overlay volume newOverlayVol := new(OverlayVolume) newOverlayVol.Destination = dest - newOverlayVol.Source = src + // convert src to absolute path so we don't end up passing + // relative values as lowerdir for overlay mounts + source, err := filepath.Abs(src) + if err != nil { + return nil, nil, nil, errors.Wrapf(err, "failed while resolving absolute path for source %v for overlay mount", src) + } + newOverlayVol.Source = source newOverlayVol.Options = options if _, ok := overlayVolumes[newOverlayVol.Destination]; ok { diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index ab45a8d47..8ad0a92e7 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -1134,17 +1134,21 @@ func parseLinuxResourcesDeviceAccess(device string) (specs.LinuxDeviceCgroup, er } number := strings.SplitN(value[1], ":", 2) - i, err := strconv.ParseInt(number[0], 10, 64) - if err != nil { - return specs.LinuxDeviceCgroup{}, err + if number[0] != "*" { + i, err := strconv.ParseUint(number[0], 10, 64) + if err != nil { + return specs.LinuxDeviceCgroup{}, err + } + m := int64(i) + major = &m } - major = &i if len(number) == 2 && number[1] != "*" { - i, err := strconv.ParseInt(number[1], 10, 64) + i, err := strconv.ParseUint(number[1], 10, 64) if err != nil { return specs.LinuxDeviceCgroup{}, err } - minor = &i + m := int64(i) + minor = &m } access = value[2] for _, c := range strings.Split(access, "") { diff --git a/pkg/specgenutil/specgenutil_test.go b/pkg/specgenutil/specgenutil_test.go index 5867b0ae0..fb2743f17 100644 --- a/pkg/specgenutil/specgenutil_test.go +++ b/pkg/specgenutil/specgenutil_test.go @@ -75,3 +75,82 @@ func TestWinPath(t *testing.T) { } } } + +func TestParseLinuxResourcesDeviceAccess(t *testing.T) { + d, err := parseLinuxResourcesDeviceAccess("a *:* rwm") + assert.Nil(t, err, "err is nil") + assert.True(t, d.Allow, "allow is true") + assert.Equal(t, d.Type, "a", "type is 'a'") + assert.Nil(t, d.Minor, "minor is nil") + assert.Nil(t, d.Major, "major is nil") + + d, err = parseLinuxResourcesDeviceAccess("b 3:* rwm") + assert.Nil(t, err, "err is nil") + assert.True(t, d.Allow, "allow is true") + assert.Equal(t, d.Type, "b", "type is 'b'") + assert.Nil(t, d.Minor, "minor is nil") + assert.NotNil(t, d.Major, "major is not nil") + assert.Equal(t, *d.Major, int64(3), "major is 3") + + d, err = parseLinuxResourcesDeviceAccess("a *:3 rwm") + assert.Nil(t, err, "err is nil") + assert.True(t, d.Allow, "allow is true") + assert.Equal(t, d.Type, "a", "type is 'a'") + assert.Nil(t, d.Major, "major is nil") + assert.NotNil(t, d.Minor, "minor is not nil") + assert.Equal(t, *d.Minor, int64(3), "minor is 3") + + d, err = parseLinuxResourcesDeviceAccess("c 1:2 rwm") + assert.Nil(t, err, "err is nil") + assert.True(t, d.Allow, "allow is true") + assert.Equal(t, d.Type, "c", "type is 'c'") + assert.NotNil(t, d.Major, "minor is not nil") + assert.Equal(t, *d.Major, int64(1), "minor is 1") + assert.NotNil(t, d.Minor, "minor is not nil") + assert.Equal(t, *d.Minor, int64(2), "minor is 2") + + _, err = parseLinuxResourcesDeviceAccess("q *:* rwm") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a a:* rwm") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a *:a rwm") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a *:* abc") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("* *:* *") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("* *:a2 *") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("*") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("*:*") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a *:*") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a *:*") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a 12a:* r") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a a12:* r") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a 0x1:* r") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a -2:* r") + assert.NotNil(t, err, "err is not nil") + + _, err = parseLinuxResourcesDeviceAccess("a *:-3 r") + assert.NotNil(t, err, "err is not nil") +} diff --git a/pkg/specgenutil/volumes.go b/pkg/specgenutil/volumes.go index 50d745380..016166a20 100644 --- a/pkg/specgenutil/volumes.go +++ b/pkg/specgenutil/volumes.go @@ -605,7 +605,7 @@ func getNamedVolume(args []string) (*specgen.NamedVolume, error) { // Parse the arguments into an image volume. An image volume is a volume based // on a container image. The container image is first mounted on the host and // is then bind-mounted into the container. An ImageVolume is always mounted -// read only. +// read-only. func getImageVolume(args []string) (*specgen.ImageVolume, error) { newVolume := new(specgen.ImageVolume) diff --git a/pkg/util/mountOpts.go b/pkg/util/mountOpts.go index e37394619..d1dd75a82 100644 --- a/pkg/util/mountOpts.go +++ b/pkg/util/mountOpts.go @@ -25,7 +25,7 @@ type defaultMountOptions struct { // The sourcePath variable, if not empty, contains a bind mount source. func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string, error) { var ( - foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ, foundU, foundOverlay, foundIdmap bool + foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ, foundU, foundOverlay, foundIdmap, foundCopy bool ) newOptions := make([]string, 0, len(options)) @@ -55,6 +55,11 @@ func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string } switch splitOpt[0] { + case "copy", "nocopy": + if foundCopy { + return nil, errors.Wrapf(ErrDupeMntOption, "only one of 'nocopy' and 'copy' can be used") + } + foundCopy = true case "O": foundOverlay = true case "volume-opt": |