diff options
-rw-r--r-- | cmd/podmanV2/containers/checkpoint.go | 79 | ||||
-rw-r--r-- | cmd/podmanV2/containers/restore.go | 104 | ||||
-rw-r--r-- | libpod/oci_conmon_linux.go | 12 | ||||
-rw-r--r-- | libpod/options.go | 2 | ||||
-rw-r--r-- | pkg/adapter/containers.go | 3 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/containers.go | 131 | ||||
-rw-r--r-- | pkg/api/server/register_containers.go | 95 | ||||
-rw-r--r-- | pkg/bindings/containers/checkpoint.go | 79 | ||||
-rw-r--r-- | pkg/checkpoint/checkpoint_restore.go (renamed from pkg/adapter/checkpoint_restore.go) | 8 | ||||
-rw-r--r-- | pkg/domain/entities/containers.go | 32 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 2 | ||||
-rw-r--r-- | pkg/domain/infra/abi/containers.go | 109 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/containers.go | 72 | ||||
-rw-r--r-- | pkg/spec/spec.go | 4 | ||||
-rw-r--r-- | pkg/spec/storage.go | 78 | ||||
-rw-r--r-- | pkg/specgen/oci.go | 4 | ||||
-rw-r--r-- | pkg/util/mountOpts.go | 24 | ||||
-rw-r--r-- | pkg/util/mountOpts_linux.go | 23 | ||||
-rw-r--r-- | pkg/util/mountOpts_other.go | 7 | ||||
-rw-r--r-- | test/e2e/checkpoint_test.go | 2 |
20 files changed, 777 insertions, 93 deletions
diff --git a/cmd/podmanV2/containers/checkpoint.go b/cmd/podmanV2/containers/checkpoint.go new file mode 100644 index 000000000..7c3e551bc --- /dev/null +++ b/cmd/podmanV2/containers/checkpoint.go @@ -0,0 +1,79 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + checkpointDescription = ` + podman container checkpoint + + Checkpoints one or more running containers. The container name or ID can be used. +` + checkpointCommand = &cobra.Command{ + Use: "checkpoint [flags] CONTAINER [CONTAINER...]", + Short: "Checkpoints one or more containers", + Long: checkpointDescription, + RunE: checkpoint, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, false, false) + }, + Example: `podman container checkpoint --keep ctrID + podman container checkpoint --all + podman container checkpoint --leave-running --latest`, + } +) + +var ( + checkpointOptions entities.CheckpointOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: checkpointCommand, + Parent: containerCmd, + }) + flags := checkpointCommand.Flags() + flags.BoolVarP(&checkpointOptions.Keep, "keep", "k", false, "Keep all temporary checkpoint files") + flags.BoolVarP(&checkpointOptions.LeaveRuninng, "leave-running", "R", false, "Leave the container running after writing checkpoint to disk") + flags.BoolVar(&checkpointOptions.TCPEstablished, "tcp-established", false, "Checkpoint a container with established TCP connections") + flags.BoolVarP(&checkpointOptions.All, "all", "a", false, "Checkpoint all running containers") + flags.BoolVarP(&checkpointOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.StringVarP(&checkpointOptions.Export, "export", "e", "", "Export the checkpoint image to a tar.gz") + flags.BoolVar(&checkpointOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not include root file-system changes when exporting") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func checkpoint(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + if rootless.IsRootless() { + return errors.New("checkpointing a container requires root") + } + if checkpointOptions.Export == "" && checkpointOptions.IgnoreRootFS { + return errors.Errorf("--ignore-rootfs can only be used with --export") + } + responses, err := registry.ContainerEngine().ContainerCheckpoint(context.Background(), args, checkpointOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podmanV2/containers/restore.go b/cmd/podmanV2/containers/restore.go new file mode 100644 index 000000000..6cab6ab50 --- /dev/null +++ b/cmd/podmanV2/containers/restore.go @@ -0,0 +1,104 @@ +package containers + +import ( + "context" + "fmt" + + "github.com/containers/libpod/cmd/podmanV2/parse" + "github.com/containers/libpod/cmd/podmanV2/registry" + "github.com/containers/libpod/cmd/podmanV2/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + restoreDescription = ` + podman container restore + + Restores a container from a checkpoint. The container name or ID can be used. +` + restoreCommand = &cobra.Command{ + Use: "restore [flags] CONTAINER [CONTAINER...]", + Short: "Restores one or more containers from a checkpoint", + Long: restoreDescription, + RunE: restore, + Args: func(cmd *cobra.Command, args []string) error { + return parse.CheckAllLatestAndCIDFile(cmd, args, true, false) + }, + Example: `podman container restore ctrID + podman container restore --latest + podman container restore --all`, + } +) + +var ( + restoreOptions entities.RestoreOptions +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: restoreCommand, + Parent: containerCmd, + }) + flags := restoreCommand.Flags() + flags.BoolVarP(&restoreOptions.All, "all", "a", false, "Restore all checkpointed containers") + flags.BoolVarP(&restoreOptions.Keep, "keep", "k", false, "Keep all temporary checkpoint files") + flags.BoolVarP(&restoreOptions.Latest, "latest", "l", false, "Act on the latest container podman is aware of") + flags.BoolVar(&restoreOptions.TCPEstablished, "tcp-established", false, "Restore a container with established TCP connections") + flags.StringVarP(&restoreOptions.Import, "import", "i", "", "Restore from exported checkpoint archive (tar.gz)") + flags.StringVarP(&restoreOptions.Name, "name", "n", "", "Specify new name for container restored from exported checkpoint (only works with --import)") + flags.BoolVar(&restoreOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not apply root file-system changes when importing from exported checkpoint") + flags.BoolVar(&restoreOptions.IgnoreStaticIP, "ignore-static-ip", false, "Ignore IP address set via --static-ip") + flags.BoolVar(&restoreOptions.IgnoreStaticMAC, "ignore-static-mac", false, "Ignore MAC address set via --mac-address") + if registry.IsRemote() { + _ = flags.MarkHidden("latest") + } +} + +func restore(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors + if rootless.IsRootless() { + return errors.New("restoring a container requires root") + } + if restoreOptions.Import == "" && restoreOptions.IgnoreRootFS { + return errors.Errorf("--ignore-rootfs can only be used with --import") + } + if restoreOptions.Import == "" && restoreOptions.Name != "" { + return errors.Errorf("--name can only be used with --import") + } + if restoreOptions.Name != "" && restoreOptions.TCPEstablished { + return errors.Errorf("--tcp-established cannot be used with --name") + } + + argLen := len(args) + if restoreOptions.Import != "" { + if restoreOptions.All || restoreOptions.Latest { + return errors.Errorf("Cannot use --import with --all or --latest") + } + if argLen > 0 { + return errors.Errorf("Cannot use --import with positional arguments") + } + } + if (restoreOptions.All || restoreOptions.Latest) && argLen > 0 { + return errors.Errorf("no arguments are needed with --all or --latest") + } + if argLen < 1 && !restoreOptions.All && !restoreOptions.Latest && restoreOptions.Import == "" { + return errors.Errorf("you must provide at least one name or id") + } + responses, err := registry.ContainerEngine().ContainerRestore(context.Background(), args, restoreOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.Id) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() + +} diff --git a/libpod/oci_conmon_linux.go b/libpod/oci_conmon_linux.go index 6a0097b8e..2e96dbe57 100644 --- a/libpod/oci_conmon_linux.go +++ b/libpod/oci_conmon_linux.go @@ -353,6 +353,9 @@ func (r *ConmonOCIRuntime) StartContainer(ctr *Container) error { if notify, ok := os.LookupEnv("NOTIFY_SOCKET"); ok { env = append(env, fmt.Sprintf("NOTIFY_SOCKET=%s", notify)) } + if path, ok := os.LookupEnv("PATH"); ok { + env = append(env, fmt.Sprintf("PATH=%s", path)) + } if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, env, r.path, "start", ctr.ID()); err != nil { return err } @@ -930,6 +933,13 @@ func (r *ConmonOCIRuntime) CheckpointContainer(ctr *Container, options Container if options.TCPEstablished { args = append(args, "--tcp-established") } + runtimeDir, err := util.GetRuntimeDir() + if err != nil { + return err + } + if err = os.Setenv("XDG_RUNTIME_DIR", runtimeDir); err != nil { + return errors.Wrapf(err, "cannot set XDG_RUNTIME_DIR") + } args = append(args, ctr.ID()) return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, args...) } @@ -939,7 +949,7 @@ func (r *ConmonOCIRuntime) CheckpointContainer(ctr *Container, options Container func (r *ConmonOCIRuntime) SupportsCheckpoint() bool { // Check if the runtime implements checkpointing. Currently only // runc's checkpoint/restore implementation is supported. - cmd := exec.Command(r.path, "checkpoint", "-h") + cmd := exec.Command(r.path, "checkpoint", "--help") if err := cmd.Start(); err != nil { return false } diff --git a/libpod/options.go b/libpod/options.go index dfbec364a..65a089131 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1334,7 +1334,7 @@ func WithNamedVolumes(volumes []*ContainerNamedVolume) CtrCreateOption { } destinations[vol.Dest] = true - mountOpts, err := util.ProcessOptions(vol.Options, false, nil) + mountOpts, err := util.ProcessOptions(vol.Options, false, "") if err != nil { return errors.Wrapf(err, "error processing options for named volume %q mounted at %q", vol.Name, vol.Dest) } diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index c395ffc7f..b4ebeb944 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -26,6 +26,7 @@ import ( "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/pkg/adapter/shortcuts" + "github.com/containers/libpod/pkg/checkpoint" envLib "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/systemd/generate" "github.com/containers/storage" @@ -625,7 +626,7 @@ func (r *LocalRuntime) Restore(ctx context.Context, c *cliconfig.RestoreValues) switch { case c.Import != "": - containers, err = crImportCheckpoint(ctx, r.Runtime, c.Import, c.Name) + containers, err = checkpoint.CRImportCheckpoint(ctx, r.Runtime, c.Import, c.Name) case c.All: containers, err = r.GetContainers(filterFuncs...) default: diff --git a/pkg/api/handlers/libpod/containers.go b/pkg/api/handlers/libpod/containers.go index cdc34004f..fde72552b 100644 --- a/pkg/api/handlers/libpod/containers.go +++ b/pkg/api/handlers/libpod/containers.go @@ -1,16 +1,21 @@ package libpod import ( + "io/ioutil" "net/http" + "os" "path/filepath" "sort" "strconv" "time" + "github.com/containers/libpod/pkg/api/handlers/compat" + "github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" "github.com/gorilla/schema" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -325,3 +330,129 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts shared.P } return ps, nil } + +func Checkpoint(w http.ResponseWriter, r *http.Request) { + var targetFile string + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Keep bool `schema:"keep"` + LeaveRunning bool `schema:"leaveRunning"` + TCPEstablished bool `schema:"tcpEstablished"` + Export bool `schema:"export"` + IgnoreRootFS bool `schema:"ignoreRootFS"` + }{ + // override any golang type defaults + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + name := utils.GetName(r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + if query.Export { + tmpFile, err := ioutil.TempFile("", "checkpoint") + if err != nil { + utils.InternalServerError(w, err) + return + } + defer os.Remove(tmpFile.Name()) + if err := tmpFile.Close(); err != nil { + utils.InternalServerError(w, err) + return + } + targetFile = tmpFile.Name() + } + options := libpod.ContainerCheckpointOptions{ + Keep: query.Keep, + KeepRunning: query.LeaveRunning, + TCPEstablished: query.TCPEstablished, + IgnoreRootfs: query.IgnoreRootFS, + } + if query.Export { + options.TargetFile = targetFile + } + err = ctr.Checkpoint(r.Context(), options) + if err != nil { + utils.InternalServerError(w, err) + return + } + if query.Export { + f, err := os.Open(targetFile) + if err != nil { + utils.InternalServerError(w, err) + return + } + defer f.Close() + utils.WriteResponse(w, http.StatusOK, f) + return + } + utils.WriteResponse(w, http.StatusOK, entities.CheckpointReport{Id: ctr.ID()}) +} + +func Restore(w http.ResponseWriter, r *http.Request) { + var ( + targetFile string + ) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + Keep bool `schema:"keep"` + TCPEstablished bool `schema:"tcpEstablished"` + Import bool `schema:"import"` + Name string `schema:"name"` + IgnoreRootFS bool `schema:"ignoreRootFS"` + IgnoreStaticIP bool `schema:"ignoreStaticIP"` + IgnoreStaticMAC bool `schema:"ignoreStaticMAC"` + }{ + // override any golang type defaults + } + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) + return + } + name := utils.GetName(r) + runtime := r.Context().Value("runtime").(*libpod.Runtime) + ctr, err := runtime.LookupContainer(name) + if err != nil { + utils.ContainerNotFound(w, name, err) + return + } + if query.Import { + t, err := ioutil.TempFile("", "restore") + if err != nil { + utils.InternalServerError(w, err) + return + } + defer t.Close() + if err := compat.SaveFromBody(t, r); err != nil { + utils.InternalServerError(w, err) + return + } + targetFile = t.Name() + } + + options := libpod.ContainerCheckpointOptions{ + Keep: query.Keep, + TCPEstablished: query.TCPEstablished, + IgnoreRootfs: query.IgnoreRootFS, + IgnoreStaticIP: query.IgnoreStaticIP, + IgnoreStaticMAC: query.IgnoreStaticMAC, + } + if query.Import { + options.TargetFile = targetFile + options.Name = query.Name + } + err = ctr.Restore(r.Context(), options) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, entities.RestoreReport{Id: ctr.ID()}) +} diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 145c054c0..f126112d0 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -1282,5 +1282,100 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/containers/{name}/export"), s.APIHandler(compat.ExportContainer)).Methods(http.MethodGet) + // swagger:operation GET /libpod/containers/{name}/checkout libpod libpodCheckpointContainer + // --- + // tags: + // - containers + // summary: Checkpoint a container + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the container + // - in: query + // name: keep + // type: boolean + // description: keep all temporary checkpoint files + // - in: query + // name: leaveRunning + // type: boolean + // description: leave the container running after writing checkpoint to disk + // - in: query + // name: tcpEstablished + // type: boolean + // description: checkpoint a container with established TCP connections + // - in: query + // name: export + // type: boolean + // description: export the checkpoint image to a tar.gz + // - in: query + // name: ignoreRootFS + // type: boolean + // description: do not include root file-system changes when exporting + // produces: + // - application/json + // responses: + // 200: + // description: tarball is returned in body if exported + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/checkpoint"), s.APIHandler(libpod.Checkpoint)).Methods(http.MethodPost) + // swagger:operation GET /libpod/containers/{name} restore libpod libpodRestoreContainer + // --- + // tags: + // - containers + // summary: Restore a container + // description: Restore a container from a checkpoint. + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or id of the container + // - in: query + // name: name + // type: string + // description: the name of the container when restored from a tar. can only be used with import + // - in: query + // name: keep + // type: boolean + // description: keep all temporary checkpoint files + // - in: query + // name: leaveRunning + // type: boolean + // description: leave the container running after writing checkpoint to disk + // - in: query + // name: tcpEstablished + // type: boolean + // description: checkpoint a container with established TCP connections + // - in: query + // name: import + // type: boolean + // description: import the restore from a checkpoint tar.gz + // - in: query + // name: ignoreRootFS + // type: boolean + // description: do not include root file-system changes when exporting + // - in: query + // name: ignoreStaticIP + // type: boolean + // description: ignore IP address if set statically + // - in: query + // name: ignoreStaticMAC + // type: boolean + // description: ignore MAC address if set statically + // produces: + // - application/json + // responses: + // 200: + // description: tarball is returned in body if exported + // 404: + // $ref: "#/responses/NoSuchContainer" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/containers/{name}/restore"), s.APIHandler(libpod.Restore)).Methods(http.MethodPost) return nil } diff --git a/pkg/bindings/containers/checkpoint.go b/pkg/bindings/containers/checkpoint.go new file mode 100644 index 000000000..84924587b --- /dev/null +++ b/pkg/bindings/containers/checkpoint.go @@ -0,0 +1,79 @@ +package containers + +import ( + "context" + "net/http" + "net/url" + "strconv" + + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" +) + +// Checkpoint checkpoints the given container (identified by nameOrId). All additional +// options are options and allow for more fine grained control of the checkpoint process. +func Checkpoint(ctx context.Context, nameOrId string, keep, leaveRunning, tcpEstablished, ignoreRootFS *bool, export *string) (*entities.CheckpointReport, error) { + var report entities.CheckpointReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + if keep != nil { + params.Set("keep", strconv.FormatBool(*keep)) + } + if leaveRunning != nil { + params.Set("leaveRunning", strconv.FormatBool(*leaveRunning)) + } + if tcpEstablished != nil { + params.Set("TCPestablished", strconv.FormatBool(*tcpEstablished)) + } + if ignoreRootFS != nil { + params.Set("ignoreRootFS", strconv.FormatBool(*ignoreRootFS)) + } + if export != nil { + params.Set("export", *export) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/checkpoint", params, nameOrId) + if err != nil { + return nil, err + } + return &report, response.Process(&report) +} + +// Restore restores a checkpointed container to running. The container is identified by the nameOrId option. All +// additional options are optional and allow finer control of the restore processs. +func Restore(ctx context.Context, nameOrId string, keep, tcpEstablished, ignoreRootFS, ignoreStaticIP, ignoreStaticMAC *bool, name, importArchive *string) (*entities.RestoreReport, error) { + var report entities.RestoreReport + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + if keep != nil { + params.Set("keep", strconv.FormatBool(*keep)) + } + if tcpEstablished != nil { + params.Set("TCPestablished", strconv.FormatBool(*tcpEstablished)) + } + if ignoreRootFS != nil { + params.Set("ignoreRootFS", strconv.FormatBool(*ignoreRootFS)) + } + if ignoreStaticIP != nil { + params.Set("ignoreStaticIP", strconv.FormatBool(*ignoreStaticIP)) + } + if ignoreStaticMAC != nil { + params.Set("ignoreStaticMAC", strconv.FormatBool(*ignoreStaticMAC)) + } + if name != nil { + params.Set("name", *name) + } + if importArchive != nil { + params.Set("import", *importArchive) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/containers/%s/restore", params, nameOrId) + if err != nil { + return nil, err + } + return &report, response.Process(&report) +} diff --git a/pkg/adapter/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go index a5b74013b..78f592d32 100644 --- a/pkg/adapter/checkpoint_restore.go +++ b/pkg/checkpoint/checkpoint_restore.go @@ -1,6 +1,4 @@ -// +build !remoteclient - -package adapter +package checkpoint import ( "context" @@ -42,9 +40,9 @@ func crImportFromJSON(filePath string, v interface{}) error { return nil } -// crImportCheckpoint it the function which imports the information +// CRImportCheckpoint it the function which imports the information // from checkpoint tarball and re-creates the container from that information -func crImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input string, name string) ([]*libpod.Container, error) { +func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, input string, name string) ([]*libpod.Container, error) { // First get the container definition from the // tarball to a temporary directory archiveFile, err := os.Open(input) diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index d51124f55..45dea98bf 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -121,3 +121,35 @@ type CommitReport struct { type ContainerExportOptions struct { Output string } + +type CheckpointOptions struct { + All bool + Export string + IgnoreRootFS bool + Keep bool + Latest bool + LeaveRuninng bool + TCPEstablished bool +} + +type CheckpointReport struct { + Err error + Id string +} + +type RestoreOptions struct { + All bool + IgnoreRootFS bool + IgnoreStaticIP bool + IgnoreStaticMAC bool + Import string + Keep bool + Latest bool + Name string + TCPEstablished bool +} + +type RestoreReport struct { + Err error + Id string +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index a122857cd..3aaa7136f 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -8,6 +8,8 @@ import ( type ContainerEngine interface { ContainerCommit(ctx context.Context, nameOrId string, options CommitOptions) (*CommitReport, error) + ContainerCheckpoint(ctx context.Context, namesOrIds []string, options CheckpointOptions) ([]*CheckpointReport, error) + ContainerRestore(ctx context.Context, namesOrIds []string, options RestoreOptions) ([]*RestoreReport, error) ContainerExists(ctx context.Context, nameOrId string) (*BoolReport, error) ContainerInspect(ctx context.Context, namesOrIds []string, options InspectOptions) ([]*ContainerInspectReport, error) ContainerExport(ctx context.Context, nameOrId string, options ContainerExportOptions) error diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index d4c5ac311..3c38b2093 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -13,12 +13,42 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/adapter/shortcuts" + "github.com/containers/libpod/pkg/checkpoint" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/signal" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) +// getContainersByContext gets pods whether all, latest, or a slice of names/ids +// is specified. +func getContainersByContext(all, latest bool, names []string, runtime *libpod.Runtime) (ctrs []*libpod.Container, err error) { + var ctr *libpod.Container + ctrs = []*libpod.Container{} + + switch { + case all: + ctrs, err = runtime.GetAllContainers() + case latest: + ctr, err = runtime.GetLatestContainer() + ctrs = append(ctrs, ctr) + default: + for _, n := range names { + ctr, e := runtime.LookupContainer(n) + if e != nil { + // Log all errors here, so callers don't need to. + logrus.Debugf("Error looking up container %q: %v", n, e) + if err == nil { + err = e + } + } else { + ctrs = append(ctrs, ctr) + } + } + } + return +} + // TODO: Should return *entities.ContainerExistsReport, error func (ic *ContainerEngine) ContainerExists(ctx context.Context, nameOrId string) (*entities.BoolReport, error) { _, err := ic.Libpod.LookupContainer(nameOrId) @@ -333,3 +363,82 @@ func (ic *ContainerEngine) ContainerExport(ctx context.Context, nameOrId string, } return ctr.Export(options.Output) } + +func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds []string, options entities.CheckpointOptions) ([]*entities.CheckpointReport, error) { + var ( + err error + cons []*libpod.Container + reports []*entities.CheckpointReport + ) + checkOpts := libpod.ContainerCheckpointOptions{ + Keep: options.Keep, + TCPEstablished: options.TCPEstablished, + TargetFile: options.Export, + IgnoreRootfs: options.IgnoreRootFS, + } + + if options.All { + running := func(c *libpod.Container) bool { + state, _ := c.State() + return state == define.ContainerStateRunning + } + cons, err = ic.Libpod.GetContainers(running) + } else { + cons, err = getContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, con := range cons { + err = con.Checkpoint(ctx, checkOpts) + reports = append(reports, &entities.CheckpointReport{ + Err: err, + Id: con.ID(), + }) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []string, options entities.RestoreOptions) ([]*entities.RestoreReport, error) { + var ( + cons []*libpod.Container + err error + filterFuncs []libpod.ContainerFilter + reports []*entities.RestoreReport + ) + + restoreOptions := libpod.ContainerCheckpointOptions{ + Keep: options.Keep, + TCPEstablished: options.TCPEstablished, + TargetFile: options.Import, + Name: options.Name, + IgnoreRootfs: options.IgnoreRootFS, + IgnoreStaticIP: options.IgnoreStaticIP, + IgnoreStaticMAC: options.IgnoreStaticMAC, + } + + filterFuncs = append(filterFuncs, func(c *libpod.Container) bool { + state, _ := c.State() + return state == define.ContainerStateExited + }) + + switch { + case options.Import != "": + cons, err = checkpoint.CRImportCheckpoint(ctx, ic.Libpod, options.Import, options.Name) + case options.All: + cons, err = ic.Libpod.GetContainers(filterFuncs...) + default: + cons, err = getContainersByContext(false, options.Latest, namesOrIds, ic.Libpod) + } + if err != nil { + return nil, err + } + for _, con := range cons { + err := con.Restore(ctx, restoreOptions) + reports = append(reports, &entities.RestoreReport{ + Err: err, + Id: con.ID(), + }) + } + return reports, nil +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 8885ae7c7..5832d41be 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -6,7 +6,8 @@ import ( "os" "github.com/containers/image/v5/docker/reference" - + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" @@ -226,3 +227,72 @@ func (ic *ContainerEngine) ContainerExport(ctx context.Context, nameOrId string, } return containers.Export(ic.ClientCxt, nameOrId, w) } + +func (ic *ContainerEngine) ContainerCheckpoint(ctx context.Context, namesOrIds []string, options entities.CheckpointOptions) ([]*entities.CheckpointReport, error) { + var ( + reports []*entities.CheckpointReport + err error + ctrs []libpod.ListContainer + ) + + if options.All { + allCtrs, err := getContainersByContext(ic.ClientCxt, true, []string{}) + if err != nil { + return nil, err + } + // narrow the list to running only + for _, c := range allCtrs { + if c.State == define.ContainerStateRunning.String() { + ctrs = append(ctrs, c) + } + } + + } else { + ctrs, err = getContainersByContext(ic.ClientCxt, false, namesOrIds) + if err != nil { + return nil, err + } + } + for _, c := range ctrs { + report, err := containers.Checkpoint(ic.ClientCxt, c.ID, &options.Keep, &options.LeaveRuninng, &options.TCPEstablished, &options.IgnoreRootFS, &options.Export) + if err != nil { + reports = append(reports, &entities.CheckpointReport{Id: c.ID, Err: err}) + } + reports = append(reports, report) + } + return reports, nil +} + +func (ic *ContainerEngine) ContainerRestore(ctx context.Context, namesOrIds []string, options entities.RestoreOptions) ([]*entities.RestoreReport, error) { + var ( + reports []*entities.RestoreReport + err error + ctrs []libpod.ListContainer + ) + if options.All { + allCtrs, err := getContainersByContext(ic.ClientCxt, true, []string{}) + if err != nil { + return nil, err + } + // narrow the list to exited only + for _, c := range allCtrs { + if c.State == define.ContainerStateExited.String() { + ctrs = append(ctrs, c) + } + } + + } else { + ctrs, err = getContainersByContext(ic.ClientCxt, false, namesOrIds) + if err != nil { + return nil, err + } + } + for _, c := range ctrs { + report, err := containers.Restore(ic.ClientCxt, c.ID, &options.Keep, &options.TCPEstablished, &options.IgnoreRootFS, &options.IgnoreStaticIP, &options.IgnoreStaticMAC, &options.Name, &options.Import) + if err != nil { + reports = append(reports, &entities.RestoreReport{Id: c.ID, Err: err}) + } + reports = append(reports, report) + } + return reports, nil +} diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 4732af757..5de07fc28 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -381,11 +381,9 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM // BIND MOUNTS configSpec.Mounts = SupercedeUserMounts(userMounts, configSpec.Mounts) // Process mounts to ensure correct options - finalMounts, err := InitFSMounts(configSpec.Mounts) - if err != nil { + if err := InitFSMounts(configSpec.Mounts); err != nil { return nil, err } - configSpec.Mounts = finalMounts // BLOCK IO blkio, err := config.CreateBlockIO() diff --git a/pkg/spec/storage.go b/pkg/spec/storage.go index b0687b4c2..68a84d638 100644 --- a/pkg/spec/storage.go +++ b/pkg/spec/storage.go @@ -10,7 +10,6 @@ import ( "github.com/containers/buildah/pkg/parse" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/util" - pmount "github.com/containers/storage/pkg/mount" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -855,75 +854,22 @@ func SupercedeUserMounts(mounts []spec.Mount, configMount []spec.Mount) []spec.M } // Ensure mount options on all mounts are correct -func InitFSMounts(inputMounts []spec.Mount) ([]spec.Mount, error) { - // We need to look up mounts so we can figure out the proper mount flags - // to apply. - systemMounts, err := pmount.GetMounts() - if err != nil { - return nil, errors.Wrapf(err, "error retrieving system mounts to look up mount options") - } - - // TODO: We probably don't need to re-build the mounts array - var mounts []spec.Mount - for _, m := range inputMounts { - if m.Type == TypeBind { - baseMnt, err := findMount(m.Destination, systemMounts) +func InitFSMounts(mounts []spec.Mount) error { + for i, m := range mounts { + switch { + case m.Type == TypeBind: + opts, err := util.ProcessOptions(m.Options, false, m.Source) if err != nil { - return nil, errors.Wrapf(err, "error looking up mountpoint for mount %s", m.Destination) - } - var noexec, nosuid, nodev bool - for _, baseOpt := range strings.Split(baseMnt.Opts, ",") { - switch baseOpt { - case "noexec": - noexec = true - case "nosuid": - nosuid = true - case "nodev": - nodev = true - } + return err } - - defaultMountOpts := new(util.DefaultMountOptions) - defaultMountOpts.Noexec = noexec - defaultMountOpts.Nosuid = nosuid - defaultMountOpts.Nodev = nodev - - opts, err := util.ProcessOptions(m.Options, false, defaultMountOpts) + mounts[i].Options = opts + case m.Type == TypeTmpfs && filepath.Clean(m.Destination) != "/dev": + opts, err := util.ProcessOptions(m.Options, true, "") if err != nil { - return nil, err + return err } - m.Options = opts - } - if m.Type == TypeTmpfs && filepath.Clean(m.Destination) != "/dev" { - opts, err := util.ProcessOptions(m.Options, true, nil) - if err != nil { - return nil, err - } - m.Options = opts - } - - mounts = append(mounts, m) - } - return mounts, nil -} - -// TODO: We could make this a bit faster by building a tree of the mountpoints -// and traversing it to identify the correct mount. -func findMount(target string, mounts []*pmount.Info) (*pmount.Info, error) { - var err error - target, err = filepath.Abs(target) - if err != nil { - return nil, errors.Wrapf(err, "cannot resolve %s", target) - } - var bestSoFar *pmount.Info - for _, i := range mounts { - if bestSoFar != nil && len(bestSoFar.Mountpoint) > len(i.Mountpoint) { - // Won't be better than what we have already found - continue - } - if strings.HasPrefix(target, i.Mountpoint) { - bestSoFar = i + mounts[i].Options = opts } } - return bestSoFar, nil + return nil } diff --git a/pkg/specgen/oci.go b/pkg/specgen/oci.go index 2523f21b3..db60dc25e 100644 --- a/pkg/specgen/oci.go +++ b/pkg/specgen/oci.go @@ -215,11 +215,9 @@ func (s *SpecGenerator) toOCISpec(rt *libpod.Runtime, newImage *image.Image) (*s // BIND MOUNTS configSpec.Mounts = createconfig.SupercedeUserMounts(s.Mounts, configSpec.Mounts) // Process mounts to ensure correct options - finalMounts, err := createconfig.InitFSMounts(configSpec.Mounts) - if err != nil { + if err := createconfig.InitFSMounts(configSpec.Mounts); err != nil { return nil, err } - configSpec.Mounts = finalMounts // Add annotations if configSpec.Annotations == nil { diff --git a/pkg/util/mountOpts.go b/pkg/util/mountOpts.go index d21800bc3..329a7c913 100644 --- a/pkg/util/mountOpts.go +++ b/pkg/util/mountOpts.go @@ -13,19 +13,17 @@ var ( ErrDupeMntOption = errors.Errorf("duplicate mount option passed") ) -// DefaultMountOptions sets default mount options for ProcessOptions. -type DefaultMountOptions struct { - Noexec bool - Nosuid bool - Nodev bool +type defaultMountOptions struct { + noexec bool + nosuid bool + nodev bool } // ProcessOptions parses the options for a bind or tmpfs mount and ensures that // they are sensible and follow convention. The isTmpfs variable controls // whether extra, tmpfs-specific options will be allowed. -// The defaults variable controls default mount options that will be set. If it -// is not included, they will be set unconditionally. -func ProcessOptions(options []string, isTmpfs bool, defaults *DefaultMountOptions) ([]string, error) { +// 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 bool ) @@ -122,13 +120,17 @@ func ProcessOptions(options []string, isTmpfs bool, defaults *DefaultMountOption if !foundProp { newOptions = append(newOptions, "rprivate") } - if !foundExec && (defaults == nil || defaults.Noexec) { + defaults, err := getDefaultMountOptions(sourcePath) + if err != nil { + return nil, err + } + if !foundExec && defaults.noexec { newOptions = append(newOptions, "noexec") } - if !foundSuid && (defaults == nil || defaults.Nosuid) { + if !foundSuid && defaults.nosuid { newOptions = append(newOptions, "nosuid") } - if !foundDev && (defaults == nil || defaults.Nodev) { + if !foundDev && defaults.nodev { newOptions = append(newOptions, "nodev") } if isTmpfs && !foundCopyUp { diff --git a/pkg/util/mountOpts_linux.go b/pkg/util/mountOpts_linux.go new file mode 100644 index 000000000..3eac4dd25 --- /dev/null +++ b/pkg/util/mountOpts_linux.go @@ -0,0 +1,23 @@ +package util + +import ( + "os" + + "golang.org/x/sys/unix" +) + +func getDefaultMountOptions(path string) (defaultMountOptions, error) { + opts := defaultMountOptions{true, true, true} + if path == "" { + return opts, nil + } + var statfs unix.Statfs_t + if e := unix.Statfs(path, &statfs); e != nil { + return opts, &os.PathError{Op: "statfs", Path: path, Err: e} + } + opts.nodev = (statfs.Flags&unix.MS_NODEV == unix.MS_NODEV) + opts.noexec = (statfs.Flags&unix.MS_NOEXEC == unix.MS_NOEXEC) + opts.nosuid = (statfs.Flags&unix.MS_NOSUID == unix.MS_NOSUID) + + return opts, nil +} diff --git a/pkg/util/mountOpts_other.go b/pkg/util/mountOpts_other.go new file mode 100644 index 000000000..6a34942e5 --- /dev/null +++ b/pkg/util/mountOpts_other.go @@ -0,0 +1,7 @@ +// +build !linux + +package util + +func getDefaultMountOptions(path string) (opts defaultMountOptions, err error) { + return +} diff --git a/test/e2e/checkpoint_test.go b/test/e2e/checkpoint_test.go index 237223283..e6a3d2f7a 100644 --- a/test/e2e/checkpoint_test.go +++ b/test/e2e/checkpoint_test.go @@ -37,7 +37,7 @@ var _ = Describe("Podman checkpoint", func() { podmanTest.SeedImages() // Check if the runtime implements checkpointing. Currently only // runc's checkpoint/restore implementation is supported. - cmd := exec.Command(podmanTest.OCIRuntime, "checkpoint", "-h") + cmd := exec.Command(podmanTest.OCIRuntime, "checkpoint", "--help") if err := cmd.Start(); err != nil { Skip("OCI runtime does not support checkpoint/restore") } |