diff options
Diffstat (limited to 'pkg')
26 files changed, 472 insertions, 185 deletions
diff --git a/pkg/api/handlers/compat/containers.go b/pkg/api/handlers/compat/containers.go index ad341c3ab..4539199d3 100644 --- a/pkg/api/handlers/compat/containers.go +++ b/pkg/api/handlers/compat/containers.go @@ -36,6 +36,7 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { query := struct { Force bool `schema:"force"` Ignore bool `schema:"ignore"` + Depend bool `schema:"depend"` Link bool `schema:"link"` Timeout *uint `schema:"timeout"` DockerVolumes bool `schema:"v"` @@ -57,6 +58,7 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { if utils.IsLibpodRequest(r) { options.Volumes = query.LibpodVolumes options.Timeout = query.Timeout + options.Depend = query.Depend } else { if query.Link { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, @@ -71,7 +73,7 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { // code. containerEngine := abi.ContainerEngine{Libpod: runtime} name := utils.GetName(r) - report, err := containerEngine.ContainerRm(r.Context(), []string{name}, options) + reports, err := containerEngine.ContainerRm(r.Context(), []string{name}, options) if err != nil { if errors.Cause(err) == define.ErrNoSuchCtr { utils.ContainerNotFound(w, name, err) @@ -81,8 +83,8 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - if len(report) > 0 && report[0].Err != nil { - err = report[0].Err + if len(reports) > 0 && reports[0].Err != nil { + err = reports[0].Err if errors.Cause(err) == define.ErrNoSuchCtr { utils.ContainerNotFound(w, name, err) return @@ -90,7 +92,10 @@ func RemoveContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - + if utils.IsLibpodRequest(r) { + utils.WriteResponse(w, http.StatusOK, reports) + return + } utils.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/pkg/api/handlers/compat/images_build.go b/pkg/api/handlers/compat/images_build.go index 0fcac5330..2d296b5ce 100644 --- a/pkg/api/handlers/compat/images_build.go +++ b/pkg/api/handlers/compat/images_build.go @@ -138,7 +138,7 @@ func BuildImage(w http.ResponseWriter, r *http.Request) { // if layers field not set assume its not from a valid podman-client // could be a docker client, set `layers=true` since that is the default - // expected behviour + // expected behaviour if !utils.IsLibpodRequest(r) { if _, found := r.URL.Query()["layers"]; !found { query.Layers = true diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 1b29831b4..43b39060b 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/define" "github.com/containers/podman/v3/pkg/api/handlers" @@ -62,15 +61,8 @@ func PodCreate(w http.ResponseWriter, r *http.Request) { psg.InfraContainerSpec.Name = psg.InfraName psg.InfraContainerSpec.ConmonPidFile = psg.InfraConmonPidFile psg.InfraContainerSpec.ContainerCreateCommand = psg.InfraCommand - imageName := psg.InfraImage - rawImageName := psg.InfraImage - if imageName == "" { - imageName = config.DefaultInfraImage - rawImageName = config.DefaultInfraImage - } - psg.InfraImage = imageName - psg.InfraContainerSpec.Image = imageName - psg.InfraContainerSpec.RawImageName = rawImageName + psg.InfraContainerSpec.Image = psg.InfraImage + psg.InfraContainerSpec.RawImageName = psg.InfraImage } podSpecComplete := entities.PodSpec{PodSpecGen: psg} pod, err := generate.MakePod(&podSpecComplete, runtime) diff --git a/pkg/api/handlers/swagger/swagger.go b/pkg/api/handlers/swagger/swagger.go index 9844839b7..7868ff206 100644 --- a/pkg/api/handlers/swagger/swagger.go +++ b/pkg/api/handlers/swagger/swagger.go @@ -111,6 +111,13 @@ type swagLibpodInspectImageResponse struct { } } +// Rm containers +// swagger:response DocsLibpodContainerRmReport +type swagLibpodContainerRmReport struct { + // in: body + Body []handlers.LibpodContainersRmReport +} + // Prune containers // swagger:response DocsContainerPruneReport type swagContainerPruneReport struct { diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index f850db3d8..588758b2c 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -53,6 +53,17 @@ type LibpodContainersPruneReport struct { PruneError string `json:"Err,omitempty"` } +type LibpodContainersRmReport struct { + ID string `json:"Id"` + // Error which occurred during Rm operation (if any). + // This field is optional and may be omitted if no error occurred. + // + // Extensions: + // x-omitempty: true + // x-nullable: true + RmError string `json:"Err,omitempty"` +} + type Info struct { docker.Info BuildahVersion string diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 601e1251b..4d19c04d4 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -817,9 +817,22 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // required: true // description: the name or ID of the container // - in: query + // name: depend + // type: boolean + // description: additionally remove containers that depend on the container to be removed + // - in: query // name: force // type: boolean - // description: need something + // description: force stop container if running + // - in: query + // name: ignore + // type: boolean + // description: ignore errors when the container to be removed does not existxo + // - in: query + // name: timeout + // type: integer + // default: 10 + // description: number of seconds to wait before killing container when force removing // - in: query // name: v // type: boolean @@ -827,6 +840,8 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // produces: // - application/json // responses: + // 200: + // $ref: "#/responses/DocsLibpodContainerRmReport" // 204: // description: no error // 400: diff --git a/pkg/bindings/containers/containers.go b/pkg/bindings/containers/containers.go index 14a173025..0148e62cb 100644 --- a/pkg/bindings/containers/containers.go +++ b/pkg/bindings/containers/containers.go @@ -78,25 +78,26 @@ func Prune(ctx context.Context, options *PruneOptions) ([]*reports.PruneReport, // The volumes bool dictates that a container's volumes should also be removed. // The All option indicates that all containers should be removed // The Ignore option indicates that if a container did not exist, ignore the error -func Remove(ctx context.Context, nameOrID string, options *RemoveOptions) error { +func Remove(ctx context.Context, nameOrID string, options *RemoveOptions) ([]*reports.RmReport, error) { if options == nil { options = new(RemoveOptions) } + var reports []*reports.RmReport conn, err := bindings.GetClient(ctx) if err != nil { - return err + return reports, err } params, err := options.ToParams() if err != nil { - return err + return reports, err } response, err := conn.DoRequest(ctx, nil, http.MethodDelete, "/containers/%s", params, nil, nameOrID) if err != nil { - return err + return reports, err } defer response.Body.Close() - return response.Process(nil) + return reports, response.Process(&reports) } // Inspect returns low level information about a Container. The nameOrID can be a container name diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go index 81a53a549..db3eb3e1b 100644 --- a/pkg/bindings/containers/types.go +++ b/pkg/bindings/containers/types.go @@ -138,6 +138,7 @@ type PruneOptions struct { //go:generate go run ../generator/generator.go RemoveOptions // RemoveOptions are optional options for removing containers type RemoveOptions struct { + Depend *bool Ignore *bool Force *bool Volumes *bool diff --git a/pkg/bindings/containers/types_remove_options.go b/pkg/bindings/containers/types_remove_options.go index 1e52e819d..7fa198d2f 100644 --- a/pkg/bindings/containers/types_remove_options.go +++ b/pkg/bindings/containers/types_remove_options.go @@ -17,6 +17,21 @@ func (o *RemoveOptions) ToParams() (url.Values, error) { return util.ToParams(o) } +// WithDepend set field Depend to given value +func (o *RemoveOptions) WithDepend(value bool) *RemoveOptions { + o.Depend = &value + return o +} + +// GetDepend returns value of field Depend +func (o *RemoveOptions) GetDepend() bool { + if o.Depend == nil { + var z bool + return z + } + return *o.Depend +} + // WithIgnore set field Ignore to given value func (o *RemoveOptions) WithIgnore(value bool) *RemoveOptions { o.Ignore = &value diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index b6c06756b..cab032a40 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -175,7 +175,7 @@ var _ = Describe("Podman containers ", func() { Expect(err).To(BeNil()) err = containers.Pause(bt.conn, cid, nil) Expect(err).To(BeNil()) - err = containers.Remove(bt.conn, cid, nil) + _, err = containers.Remove(bt.conn, cid, nil) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -188,8 +188,10 @@ var _ = Describe("Podman containers ", func() { Expect(err).To(BeNil()) err = containers.Pause(bt.conn, cid, nil) Expect(err).To(BeNil()) - err = containers.Remove(bt.conn, cid, new(containers.RemoveOptions).WithForce(true)) + rmResponse, err := containers.Remove(bt.conn, cid, new(containers.RemoveOptions).WithForce(true)) Expect(err).To(BeNil()) + Expect(len(reports.RmReportsErrs(rmResponse))).To(Equal(0)) + Expect(len(reports.RmReportsIds(rmResponse))).To(Equal(1)) }) It("podman stop a paused container by name", func() { @@ -669,7 +671,8 @@ var _ = Describe("Podman containers ", func() { }) It("podman remove bogus container", func() { - err = containers.Remove(bt.conn, "foobar", nil) + _, err := containers.Remove(bt.conn, "foobar", nil) + Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusNotFound)) }) @@ -679,7 +682,7 @@ var _ = Describe("Podman containers ", func() { _, err := bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) // Removing running container should fail - err = containers.Remove(bt.conn, name, nil) + _, err = containers.Remove(bt.conn, name, nil) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -690,7 +693,7 @@ var _ = Describe("Podman containers ", func() { cid, err := bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) // Removing running container should fail - err = containers.Remove(bt.conn, cid, nil) + _, err = containers.Remove(bt.conn, cid, nil) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -700,22 +703,22 @@ var _ = Describe("Podman containers ", func() { var name = "top" _, err := bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) - // Removing running container should fail - err = containers.Remove(bt.conn, name, new(containers.RemoveOptions).WithForce(true)) + // Removing running container should succeed + rmResponse, err := containers.Remove(bt.conn, name, new(containers.RemoveOptions).WithForce(true)) Expect(err).To(BeNil()) - //code, _ := bindings.CheckResponseCode(err) - //Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + Expect(len(reports.RmReportsErrs(rmResponse))).To(Equal(0)) + Expect(len(reports.RmReportsIds(rmResponse))).To(Equal(1)) }) It("podman forcibly remove running container by ID", func() { var name = "top" cid, err := bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) - // Removing running container should fail - err = containers.Remove(bt.conn, cid, new(containers.RemoveOptions).WithForce(true)) + // Forcably Removing running container should succeed + rmResponse, err := containers.Remove(bt.conn, cid, new(containers.RemoveOptions).WithForce(true)) Expect(err).To(BeNil()) - //code, _ := bindings.CheckResponseCode(err) - //Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + Expect(len(reports.RmReportsErrs(rmResponse))).To(Equal(0)) + Expect(len(reports.RmReportsIds(rmResponse))).To(Equal(1)) }) It("podman remove running container and volume by name", func() { @@ -723,7 +726,7 @@ var _ = Describe("Podman containers ", func() { _, err := bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) // Removing running container should fail - err = containers.Remove(bt.conn, name, new(containers.RemoveOptions).WithVolumes(true)) + _, err = containers.Remove(bt.conn, name, new(containers.RemoveOptions).WithVolumes(true)) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -734,7 +737,7 @@ var _ = Describe("Podman containers ", func() { cid, err := bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) // Removing running container should fail - err = containers.Remove(bt.conn, cid, new(containers.RemoveOptions).WithVolumes(true)) + _, err = containers.Remove(bt.conn, cid, new(containers.RemoveOptions).WithVolumes(true)) Expect(err).ToNot(BeNil()) code, _ := bindings.CheckResponseCode(err) Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) @@ -744,11 +747,11 @@ var _ = Describe("Podman containers ", func() { var name = "top" _, err := bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) - // Removing running container should fail - err = containers.Remove(bt.conn, name, new(containers.RemoveOptions).WithVolumes(true).WithForce(true)) + // Forcibly Removing running container should succeed + rmResponse, err := containers.Remove(bt.conn, name, new(containers.RemoveOptions).WithVolumes(true).WithForce(true)) Expect(err).To(BeNil()) - //code, _ := bindings.CheckResponseCode(err) - //Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + Expect(len(reports.RmReportsErrs(rmResponse))).To(Equal(0)) + Expect(len(reports.RmReportsIds(rmResponse))).To(Equal(1)) }) It("podman forcibly remove running container and volume by ID", func() { @@ -756,10 +759,10 @@ var _ = Describe("Podman containers ", func() { cid, err := bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) // Removing running container should fail - err = containers.Remove(bt.conn, cid, new(containers.RemoveOptions).WithForce(true).WithVolumes(true)) + rmResponse, err := containers.Remove(bt.conn, cid, new(containers.RemoveOptions).WithForce(true).WithVolumes(true)) Expect(err).To(BeNil()) - //code, _ := bindings.CheckResponseCode(err) - //Expect(code).To(BeNumerically("==", http.StatusInternalServerError)) + Expect(len(reports.RmReportsErrs(rmResponse))).To(Equal(0)) + Expect(len(reports.RmReportsIds(rmResponse))).To(Equal(1)) }) It("List containers with filters", func() { diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index ae441b7f3..e3f8f1b7c 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -129,6 +129,7 @@ type RestartReport struct { type RmOptions struct { All bool + Depend bool Force bool Ignore bool Latest bool @@ -136,11 +137,6 @@ type RmOptions struct { Volumes bool } -type RmReport struct { - Err error - Id string //nolint -} - type ContainerInspectReport struct { *define.InspectContainerData } diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go index a8023f7cf..055af7ff9 100644 --- a/pkg/domain/entities/engine.go +++ b/pkg/domain/entities/engine.go @@ -40,6 +40,7 @@ type PodmanConfig struct { Identity string // ssh identity for connecting to server MaxWorks int // maximum number of parallel threads MemoryProfile string // Hidden: Should memory profile be taken + NoOut bool // Don't output to stdout RegistriesConf string // allows for specifying a custom registries.conf Remote bool // Connection to Podman API Service will use RESTful API RuntimePath string // --runtime flag will set Engine.RuntimePath diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 383e42098..7ce4dd0f6 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -40,7 +40,7 @@ type ContainerEngine interface { ContainerRename(ctr context.Context, nameOrID string, options ContainerRenameOptions) error ContainerRestart(ctx context.Context, namesOrIds []string, options RestartOptions) ([]*RestartReport, error) ContainerRestore(ctx context.Context, namesOrIds []string, options RestoreOptions) ([]*RestoreReport, error) - ContainerRm(ctx context.Context, namesOrIds []string, options RmOptions) ([]*RmReport, error) + ContainerRm(ctx context.Context, namesOrIds []string, options RmOptions) ([]*reports.RmReport, error) ContainerRun(ctx context.Context, opts ContainerRunOptions) (*ContainerRunReport, error) ContainerRunlabel(ctx context.Context, label string, image string, args []string, opts ContainerRunlabelOptions) error ContainerStart(ctx context.Context, namesOrIds []string, options ContainerStartOptions) ([]*ContainerStartReport, error) diff --git a/pkg/domain/entities/events.go b/pkg/domain/entities/events.go index 73a375b94..fa815d7b9 100644 --- a/pkg/domain/entities/events.go +++ b/pkg/domain/entities/events.go @@ -42,7 +42,7 @@ func ConvertToLibpodEvent(e Event) *libpodEvents.Event { Image: image, Name: name, Status: status, - Time: time.Unix(e.Time, e.TimeNano), + Time: time.Unix(0, e.TimeNano), Type: t, Details: libpodEvents.Details{ Attributes: details, diff --git a/pkg/domain/entities/reports/containers.go b/pkg/domain/entities/reports/containers.go new file mode 100644 index 000000000..54bcd092b --- /dev/null +++ b/pkg/domain/entities/reports/containers.go @@ -0,0 +1,28 @@ +package reports + +type RmReport struct { + Id string `json:"Id"` //nolint + Err error `json:"Err,omitempty"` +} + +func RmReportsIds(r []*RmReport) []string { + ids := make([]string, 0, len(r)) + for _, v := range r { + if v == nil || v.Id == "" { + continue + } + ids = append(ids, v.Id) + } + return ids +} + +func RmReportsErrs(r []*RmReport) []error { + errs := make([]error, 0, len(r)) + for _, v := range r { + if v == nil || v.Err == nil { + continue + } + errs = append(errs, v.Err) + } + return errs +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index bf4dcff62..a4522698e 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -301,27 +301,27 @@ func (ic *ContainerEngine) removeContainer(ctx context.Context, ctr *libpod.Cont return err } -func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, options entities.RmOptions) ([]*entities.RmReport, error) { - reports := []*entities.RmReport{} +func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, options entities.RmOptions) ([]*reports.RmReport, error) { + rmReports := []*reports.RmReport{} names := namesOrIds // Attempt to remove named containers directly from storage, if container is defined in libpod // this will fail and code will fall through to removing the container from libpod.` tmpNames := []string{} for _, ctr := range names { - report := entities.RmReport{Id: ctr} + report := reports.RmReport{Id: ctr} report.Err = ic.Libpod.RemoveStorageContainer(ctr, options.Force) switch errors.Cause(report.Err) { case nil: // remove container names that we successfully deleted - reports = append(reports, &report) + rmReports = append(rmReports, &report) case define.ErrNoSuchCtr, 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 { // remove container failed, but not a libpod container - reports = append(reports, &report) + rmReports = append(rmReports, &report) continue } // attempt to remove as a libpod container @@ -340,23 +340,34 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, for _, ctr := range names { logrus.Debugf("Evicting container %q", ctr) - report := entities.RmReport{Id: ctr} + report := reports.RmReport{Id: ctr} _, err := ic.Libpod.EvictContainer(ctx, ctr, options.Volumes) if err != nil { if options.Ignore && errors.Cause(err) == define.ErrNoSuchCtr { logrus.Debugf("Ignoring error (--allow-missing): %v", err) - reports = append(reports, &report) + rmReports = append(rmReports, &report) continue } report.Err = err - reports = append(reports, &report) + rmReports = append(rmReports, &report) continue } - reports = append(reports, &report) + rmReports = append(rmReports, &report) } - return reports, nil + return rmReports, nil } + if !options.All && options.Depend { + // Add additional containers based on dependencies to container map + for _, ctr := range ctrs { + reports, err := ic.Libpod.RemoveDepend(ctx, ctr, options.Force, options.Volumes, options.Timeout) + if err != nil { + return rmReports, err + } + rmReports = append(rmReports, reports...) + } + return rmReports, nil + } errMap, err := parallelctr.ContainerOp(ctx, ctrs, func(c *libpod.Container) error { return ic.removeContainer(ctx, c, options) }) @@ -364,12 +375,12 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, return nil, err } for ctr, err := range errMap { - report := new(entities.RmReport) + report := new(reports.RmReport) report.Id = ctr.ID() report.Err = err - reports = append(reports, report) + rmReports = append(rmReports, report) } - return reports, nil + return rmReports, nil } func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []string, options entities.InspectOptions) ([]*entities.ContainerInspectReport, []error, error) { diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 84c83ea8e..592e0f4e3 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -745,11 +745,17 @@ func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStore // TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error { var cmdSave *exec.Cmd - saveCommand := parentFlags - saveCommand = append(saveCommand, []string{"save", "--output", source.File, source.Image}...) + saveCommand, loadCommand := parentFlags, parentFlags + saveCommand = append(saveCommand, []string{"save"}...) + loadCommand = append(loadCommand, []string{"load"}...) + if source.Quiet { + saveCommand = append(saveCommand, "-q") + loadCommand = append(loadCommand, "-q") + } + + saveCommand = append(saveCommand, []string{"--output", source.File, source.Image}...) - loadCommand := parentFlags - loadCommand = append(loadCommand, []string{"load", "--input", dest.File}...) + loadCommand = append(loadCommand, []string{"--input", dest.File}...) if source.User == "root" { cmdSave = exec.Command("sudo", podman) @@ -757,7 +763,7 @@ func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOpt cmdSave = exec.Command(podman) } cmdSave = utils.CreateSCPCommand(cmdSave, saveCommand) - logrus.Debug("Executing save command") + logrus.Debugf("Executing save command: %q", cmdSave) err := cmdSave.Run() if err != nil { return err @@ -770,20 +776,22 @@ func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOpt cmdLoad = exec.Command(podman) } cmdLoad = utils.CreateSCPCommand(cmdLoad, loadCommand) - logrus.Debug("Executing load command") - err = cmdLoad.Run() - if err != nil { - return err - } - return nil + logrus.Debugf("Executing load command: %q", cmdLoad) + return cmdLoad.Run() } -// TransferRootless creates new podman processes using exec.Command and su/machinectl, transferring images between the given source and destination users +// TransferRootful creates new podman processes using exec.Command and su/machinectl, transferring images between the given source and destination users func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error { basicCommand := []string{podman} basicCommand = append(basicCommand, parentFlags...) - saveCommand := append(basicCommand, []string{"save", "--output", source.File, source.Image}...) - loadCommand := append(basicCommand, []string{"load", "--input", dest.File}...) + saveCommand := append(basicCommand, "save") + loadCommand := append(basicCommand, "load") + if source.Quiet { + saveCommand = append(saveCommand, "-q") + loadCommand = append(loadCommand, "-q") + } + saveCommand = append(saveCommand, []string{"--output", source.File, source.Image}...) + loadCommand = append(loadCommand, []string{"--input", dest.File}...) save := []string{strings.Join(saveCommand, " ")} load := []string{strings.Join(loadCommand, " ")} @@ -846,18 +854,18 @@ func lookupUser(u string) (*user.User, error) { func execSu(execUser *user.User, command []string) error { cmd := exec.Command("su", "-l", execUser.Username, "--command") cmd = utils.CreateSCPCommand(cmd, command) - logrus.Debug("Executing command su") + logrus.Debugf("Executing via su: %q", cmd) return cmd.Run() } func execMachine(execUser *user.User, command []string, machinectl string) error { - var cmd *exec.Cmd + verb := machinectl + args := []string{"shell", "-q", execUser.Username + "@.host"} if execUser.Uid == "0" { - cmd = exec.Command("sudo", machinectl, "shell", "-q", execUser.Username+"@.host") - } else { - cmd = exec.Command(machinectl, "shell", "-q", execUser.Username+"@.host") + args = append([]string{verb}, args...) + verb = "sudo" } - cmd = utils.CreateSCPCommand(cmd, command) - logrus.Debug("Executing command machinectl") + cmd := utils.CreateSCPCommand(exec.Command(verb, args...), command) + logrus.Debugf("Executing via machinectl: %q", cmd) return cmd.Run() } diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 2127f8749..4f72eab96 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -182,9 +182,9 @@ func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []st return reports, nil } -func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, opts entities.RmOptions) ([]*entities.RmReport, error) { +func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, opts entities.RmOptions) ([]*reports.RmReport, error) { // TODO there is no endpoint for container eviction. Need to discuss - options := new(containers.RemoveOptions).WithForce(opts.Force).WithVolumes(opts.Volumes).WithIgnore(opts.Ignore) + options := new(containers.RemoveOptions).WithForce(opts.Force).WithVolumes(opts.Volumes).WithIgnore(opts.Ignore).WithDepend(opts.Depend) if opts.Timeout != nil { options = options.WithTimeout(*opts.Timeout) } @@ -193,25 +193,31 @@ func (ic *ContainerEngine) ContainerRm(ctx context.Context, namesOrIds []string, if err != nil { return nil, err } - reports := make([]*entities.RmReport, 0, len(ctrs)) + rmReports := make([]*reports.RmReport, 0, len(ctrs)) for _, c := range ctrs { - reports = append(reports, &entities.RmReport{ - Id: c.ID, - Err: containers.Remove(ic.ClientCtx, c.ID, options), - }) + report, err := containers.Remove(ic.ClientCtx, c.ID, options) + if err != nil { + return rmReports, err + } + rmReports = append(rmReports, report...) } - return reports, nil + return rmReports, nil } - reports := make([]*entities.RmReport, 0, len(namesOrIds)) + rmReports := make([]*reports.RmReport, 0, len(namesOrIds)) for _, name := range namesOrIds { - reports = append(reports, &entities.RmReport{ - Id: name, - Err: containers.Remove(ic.ClientCtx, name, options), - }) + report, err := containers.Remove(ic.ClientCtx, name, options) + if err != nil { + rmReports = append(rmReports, &reports.RmReport{ + Id: name, + Err: err, + }) + continue + } + rmReports = append(rmReports, report...) } - return reports, nil + return rmReports, nil } func (ic *ContainerEngine) ContainerPrune(ctx context.Context, opts entities.ContainerPruneOptions) ([]*reports.PruneReport, error) { @@ -552,6 +558,27 @@ func startAndAttach(ic *ContainerEngine, name string, detachKeys *string, input, return <-attachErr } +func logIfRmError(id string, err error, reports []*reports.RmReport) { + logError := func(id string, err error) { + if errorhandling.Contains(err, define.ErrNoSuchCtr) || + errorhandling.Contains(err, define.ErrCtrRemoved) || + errorhandling.Contains(err, types.ErrLayerUnknown) { + logrus.Debugf("Container %s does not exist: %v", id, err) + } else { + logrus.Errorf("Removing container %s: %v", id, err) + } + } + if err != nil { + logError(id, err) + } else { + for _, report := range reports { + if report.Err != nil { + logError(report.Id, report.Err) + } + } + } +} + func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []string, options entities.ContainerStartOptions) ([]*entities.ContainerStartReport, error) { reports := []*entities.ContainerStartReport{} var exitCode = define.ExecErrorCodeGeneric @@ -590,14 +617,8 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri } removeOptions := new(containers.RemoveOptions).WithVolumes(true).WithForce(false) removeContainer := func(id string) { - if err := containers.Remove(ic.ClientCtx, id, removeOptions); err != nil { - if errorhandling.Contains(err, define.ErrNoSuchCtr) || - errorhandling.Contains(err, define.ErrCtrRemoved) { - logrus.Debugf("Container %s does not exist: %v", id, err) - } else { - logrus.Errorf("Removing container %s: %v", id, err) - } - } + reports, err := containers.Remove(ic.ClientCtx, id, removeOptions) + logIfRmError(id, err, reports) } // There can only be one container if attach was used @@ -674,15 +695,8 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri if err != nil { if ctr.AutoRemove { rmOptions := new(containers.RemoveOptions).WithForce(false).WithVolumes(true) - if err := containers.Remove(ic.ClientCtx, ctr.ID, rmOptions); err != nil { - if errorhandling.Contains(err, define.ErrNoSuchCtr) || - errorhandling.Contains(err, define.ErrCtrRemoved) || - errorhandling.Contains(err, types.ErrLayerUnknown) { - logrus.Debugf("Container %s does not exist: %v", ctr.ID, err) - } else { - logrus.Errorf("Removing container %s: %v", ctr.ID, err) - } - } + reports, err := containers.Remove(ic.ClientCtx, ctr.ID, rmOptions) + logIfRmError(ctr.ID, err, reports) } report.Err = errors.Wrapf(err, "unable to start container %q", name) report.ExitCode = define.ExitCode(err) @@ -741,7 +755,8 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta report.ExitCode = define.ExitCode(err) if opts.Rm { - if rmErr := containers.Remove(ic.ClientCtx, con.ID, new(containers.RemoveOptions).WithForce(false).WithVolumes(true)); rmErr != nil { + reports, rmErr := containers.Remove(ic.ClientCtx, con.ID, new(containers.RemoveOptions).WithForce(false).WithVolumes(true)) + if rmErr != nil || reports[0].Err != nil { logrus.Debugf("unable to remove container %s after failing to start and attach to it", con.ID) } } @@ -759,15 +774,8 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta } if !shouldRestart { - if err := containers.Remove(ic.ClientCtx, con.ID, new(containers.RemoveOptions).WithForce(false).WithVolumes(true)); err != nil { - if errorhandling.Contains(err, define.ErrNoSuchCtr) || - errorhandling.Contains(err, define.ErrCtrRemoved) || - errorhandling.Contains(err, types.ErrLayerUnknown) { - logrus.Debugf("Container %s does not exist: %v", con.ID, err) - } else { - logrus.Errorf("Removing container %s: %v", con.ID, err) - } - } + reports, err := containers.Remove(ic.ClientCtx, con.ID, new(containers.RemoveOptions).WithForce(false).WithVolumes(true)) + logIfRmError(con.ID, err, reports) } }() } diff --git a/pkg/machine/config.go b/pkg/machine/config.go index 33a352898..97237f5e5 100644 --- a/pkg/machine/config.go +++ b/pkg/machine/config.go @@ -29,6 +29,15 @@ type InitOptions struct { ReExec bool } +type QemuMachineStatus = string + +const ( + // Running indicates the qemu vm is running + Running QemuMachineStatus = "running" + // Stopped indicates the vm has stopped + Stopped QemuMachineStatus = "stopped" +) + type Provider interface { NewMachine(opts InitOptions) (VM, error) LoadVMByName(name string) (VM, error) @@ -66,15 +75,18 @@ type Download struct { type ListOptions struct{} type ListResponse struct { - Name string - CreatedAt time.Time - LastUp time.Time - Running bool - Stream string - VMType string - CPUs uint64 - Memory uint64 - DiskSize uint64 + Name string + CreatedAt time.Time + LastUp time.Time + Running bool + Stream string + VMType string + CPUs uint64 + Memory uint64 + DiskSize uint64 + Port int + RemoteUsername string + IdentityPath string } type SSHOptions struct { diff --git a/pkg/machine/ignition.go b/pkg/machine/ignition.go index 84d3be296..ac2cf71cf 100644 --- a/pkg/machine/ignition.go +++ b/pkg/machine/ignition.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" + "github.com/containers/common/pkg/config" "github.com/sirupsen/logrus" ) @@ -340,6 +341,24 @@ machine_enabled=true }, }) + setProxyOpts := getProxyVariables() + if setProxyOpts != "" { + files = append(files, File{ + Node: Node{ + Group: getNodeGrp("root"), + Path: "/etc/profile.d/proxy-opts.sh", + User: getNodeUsr("root"), + }, + FileEmbedded1: FileEmbedded1{ + Append: nil, + Contents: Resource{ + Source: encodeDataURLPtr(setProxyOpts), + }, + Mode: intToPtr(0644), + }, + }) + } + setDockerHost := `export DOCKER_HOST="unix://$(podman info -f "{{.Host.RemoteSocket.Path}}")" ` @@ -365,52 +384,110 @@ machine_enabled=true return files } - certFiles := getCerts(filepath.Join(userHome, ".config/containers/certs.d")) + certFiles := getCerts(filepath.Join(userHome, ".config/containers/certs.d"), true) files = append(files, certFiles...) - certFiles = getCerts(filepath.Join(userHome, ".config/docker/certs.d")) + certFiles = getCerts(filepath.Join(userHome, ".config/docker/certs.d"), true) files = append(files, certFiles...) + if sslCertFile, ok := os.LookupEnv("SSL_CERT_FILE"); ok { + if _, err := os.Stat(sslCertFile); err == nil { + certFiles = getCerts(sslCertFile, false) + files = append(files, certFiles...) + + if len(certFiles) > 0 { + setSSLCertFile := fmt.Sprintf("export %s=%s", "SSL_CERT_FILE", filepath.Join("/etc/containers/certs.d", filepath.Base(sslCertFile))) + files = append(files, File{ + Node: Node{ + Group: getNodeGrp("root"), + Path: "/etc/profile.d/ssl_cert_file.sh", + User: getNodeUsr("root"), + }, + FileEmbedded1: FileEmbedded1{ + Append: nil, + Contents: Resource{ + Source: encodeDataURLPtr(setSSLCertFile), + }, + Mode: intToPtr(0644), + }, + }) + } + } + } + return files } -func getCerts(certsDir string) []File { +func getCerts(certsDir string, isDir bool) []File { var ( files []File ) certs, err := ioutil.ReadDir(certsDir) - if err == nil { - for _, cert := range certs { - b, err := ioutil.ReadFile(filepath.Join(certsDir, cert.Name())) - if err != nil { - logrus.Warnf("Unable to read cert file %s", err.Error()) - continue - } - files = append(files, File{ - Node: Node{ - Group: getNodeGrp("root"), - Path: filepath.Join("/etc/containers/certs.d/", cert.Name()), - User: getNodeUsr("root"), - }, - FileEmbedded1: FileEmbedded1{ - Append: nil, - Contents: Resource{ - Source: encodeDataURLPtr(string(b)), + if isDir { + if err == nil { + for _, cert := range certs { + b, err := ioutil.ReadFile(filepath.Join(certsDir, cert.Name())) + if err != nil { + logrus.Warnf("Unable to read cert file %s", err.Error()) + continue + } + files = append(files, File{ + Node: Node{ + Group: getNodeGrp("root"), + Path: filepath.Join("/etc/containers/certs.d/", cert.Name()), + User: getNodeUsr("root"), }, - Mode: intToPtr(0644), - }, - }) + FileEmbedded1: FileEmbedded1{ + Append: nil, + Contents: Resource{ + Source: encodeDataURLPtr(string(b)), + }, + Mode: intToPtr(0644), + }, + }) + } + } else { + if !os.IsNotExist(err) { + logrus.Warnf("Unable to copy certs via ignition, error while reading certs from %s: %s", certsDir, err.Error()) + } } } else { - if !os.IsNotExist(err) { - logrus.Warnf("Unable to copy certs via ignition, error while reading certs from %s: %s", certsDir, err.Error()) + fileName := filepath.Base(certsDir) + b, err := ioutil.ReadFile(certsDir) + if err != nil { + logrus.Warnf("Unable to read cert file %s", err.Error()) + return files } + files = append(files, File{ + Node: Node{ + Group: getNodeGrp("root"), + Path: filepath.Join("/etc/containers/certs.d/", fileName), + User: getNodeUsr("root"), + }, + FileEmbedded1: FileEmbedded1{ + Append: nil, + Contents: Resource{ + Source: encodeDataURLPtr(string(b)), + }, + Mode: intToPtr(0644), + }, + }) } return files } +func getProxyVariables() string { + proxyOpts := "" + for _, variable := range config.ProxyEnv { + if value, ok := os.LookupEnv(variable); ok { + proxyOpts += fmt.Sprintf("\n export %s=%s", variable, value) + } + } + return proxyOpts +} + func getLinks(usrName string) []Link { return []Link{{ Node: Node{ diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index f09107c71..560037542 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -386,8 +386,16 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { } if len(v.Mounts) > 0 { - for !v.isRunning() || !v.isListening() { + running, err := v.isRunning() + if err != nil { + return err + } + for running || !v.isListening() { time.Sleep(100 * time.Millisecond) + running, err = v.isRunning() + if err != nil { + return err + } } } for _, mount := range v.Mounts { @@ -416,8 +424,48 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { return nil } +func (v *MachineVM) checkStatus(monitor *qmp.SocketMonitor) (machine.QemuMachineStatus, error) { + // this is the format returned from the monitor + // {"return": {"status": "running", "singlestep": false, "running": true}} + + type statusDetails struct { + Status string `json:"status"` + Step bool `json:"singlestep"` + Running bool `json:"running"` + } + type statusResponse struct { + Response statusDetails `json:"return"` + } + var response statusResponse + + checkCommand := struct { + Execute string `json:"execute"` + }{ + Execute: "query-status", + } + input, err := json.Marshal(checkCommand) + if err != nil { + return "", err + } + b, err := monitor.Run(input) + if err != nil { + if errors.Cause(err) == os.ErrNotExist { + return machine.Stopped, nil + } + return "", err + } + if err := json.Unmarshal(b, &response); err != nil { + return "", err + } + if response.Response.Status == machine.Running { + return machine.Running, nil + } + return machine.Stopped, nil +} + // Stop uses the qmp monitor to call a system_powerdown func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { + var disconnected bool // check if the qmp socket is there. if not, qemu instance is gone if _, err := os.Stat(v.QMPMonitor.Address); os.IsNotExist(err) { // Right now it is NOT an error to stop a stopped machine @@ -442,19 +490,22 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { return err } defer func() { - if err := qmpMonitor.Disconnect(); err != nil { - logrus.Error(err) + if !disconnected { + if err := qmpMonitor.Disconnect(); err != nil { + logrus.Error(err) + } } }() + if _, err = qmpMonitor.Run(input); err != nil { return err } + qemuSocketFile, pidFile, err := v.getSocketandPid() if err != nil { return err } if _, err := os.Stat(pidFile); os.IsNotExist(err) { - logrus.Info(err) return nil } pidString, err := ioutil.ReadFile(pidFile) @@ -483,6 +534,24 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { return err } + if err := qmpMonitor.Disconnect(); err != nil { + return nil + } + + disconnected = true + waitInternal := 250 * time.Millisecond + for i := 0; i < 5; i++ { + running, err := v.isRunning() + if err != nil { + return err + } + if !running { + break + } + time.Sleep(waitInternal) + waitInternal = waitInternal * 2 + } + return nil } @@ -519,7 +588,11 @@ func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, fun ) // cannot remove a running vm - if v.isRunning() { + running, err := v.isRunning() + if err != nil { + return "", nil, err + } + if running { return "", nil, errors.Errorf("running vm %q cannot be destroyed", v.Name) } @@ -578,16 +651,33 @@ func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, fun }, nil } -func (v *MachineVM) isRunning() bool { +func (v *MachineVM) isRunning() (bool, error) { // Check if qmp socket path exists if _, err := os.Stat(v.QMPMonitor.Address); os.IsNotExist(err) { - return false + return false, nil } // Check if we can dial it - if _, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address, v.QMPMonitor.Timeout); err != nil { - return false + monitor, err := qmp.NewSocketMonitor(v.QMPMonitor.Network, v.QMPMonitor.Address, v.QMPMonitor.Timeout) + if err != nil { + return false, nil } - return true + if err := monitor.Connect(); err != nil { + return false, err + } + defer func() { + if err := monitor.Disconnect(); err != nil { + logrus.Error(err) + } + }() + // If there is a monitor, lets see if we can query state + state, err := v.checkStatus(monitor) + if err != nil { + return false, err + } + if state == machine.Running { + return true, nil + } + return false, nil } func (v *MachineVM) isListening() bool { @@ -603,7 +693,11 @@ func (v *MachineVM) isListening() bool { // SSH opens an interactive SSH session to the vm specified. // Added ssh function to VM interface: pkg/machine/config/go : line 58 func (v *MachineVM) SSH(name string, opts machine.SSHOptions) error { - if !v.isRunning() { + running, err := v.isRunning() + if err != nil { + return err + } + if !running { return errors.Errorf("vm %q is not running.", v.Name) } @@ -696,6 +790,9 @@ func GetVMInfos() ([]*machine.ListResponse, error) { listEntry.CPUs = vm.CPUs listEntry.Memory = vm.Memory * units.MiB listEntry.DiskSize = vm.DiskSize * units.GiB + listEntry.Port = vm.Port + listEntry.RemoteUsername = vm.RemoteUsername + listEntry.IdentityPath = vm.IdentityPath fi, err := os.Stat(fullPath) if err != nil { return err @@ -707,7 +804,11 @@ func GetVMInfos() ([]*machine.ListResponse, error) { return err } listEntry.LastUp = fi.ModTime() - if vm.isRunning() { + running, err := vm.isRunning() + if err != nil { + return err + } + if running { listEntry.Running = true } diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index 5ec7c7b03..2c7b3c091 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -7,6 +7,7 @@ import ( "time" "github.com/containers/common/libimage" + "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/define" ann "github.com/containers/podman/v3/pkg/annotations" @@ -126,16 +127,7 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat if s.EnvHost { defaultEnvs = envLib.Join(defaultEnvs, osEnv) } else if s.HTTPProxy { - for _, envSpec := range []string{ - "http_proxy", - "HTTP_PROXY", - "https_proxy", - "HTTPS_PROXY", - "ftp_proxy", - "FTP_PROXY", - "no_proxy", - "NO_PROXY", - } { + for _, envSpec := range config.ProxyEnv { if v, ok := osEnv[envSpec]; ok { defaultEnvs[envSpec] = v } diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index 6d9f598c9..b41ee8db0 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -291,7 +291,10 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener return nil, err } - envs[env.Name] = value + // Only set the env if the value is not "" + if value != "" { + envs[env.Name] = value + } } for _, envFrom := range opts.Container.EnvFrom { cmEnvs, err := envVarsFrom(envFrom, opts) diff --git a/pkg/specgen/generate/ports.go b/pkg/specgen/generate/ports.go index b60cc1e98..34b43a62e 100644 --- a/pkg/specgen/generate/ports.go +++ b/pkg/specgen/generate/ports.go @@ -206,7 +206,7 @@ func ParsePortMapping(portMappings []types.PortMapping, exposePorts map[uint16][ } // we do no longer need the original port mappings - // set it to 0 length so we can resuse it to populate + // set it to 0 length so we can reuse it to populate // the slice again while keeping the underlying capacity portMappings = portMappings[:0] diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go index a18364882..31f96b933 100644 --- a/pkg/specgen/podspecgen.go +++ b/pkg/specgen/podspecgen.go @@ -97,7 +97,7 @@ type PodNetworkConfig struct { // Map of networks names ot ids the container should join to. // You can request additional settings for each network, you can // set network aliases, static ips, static mac address and the - // network interface name for this container on the specifc network. + // network interface name for this container on the specific network. // If the map is empty and the bridge network mode is set the container // will be joined to the default network. Networks map[string]types.PerNetworkOptions diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 5989456c9..6c1011a78 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -426,7 +426,7 @@ type ContainerNetworkConfig struct { // Map of networks names ot ids the container should join to. // You can request additional settings for each network, you can // set network aliases, static ips, static mac address and the - // network interface name for this container on the specifc network. + // network interface name for this container on the specific network. // If the map is empty and the bridge network mode is set the container // will be joined to the default network. Networks map[string]nettypes.PerNetworkOptions |