diff options
Diffstat (limited to 'pkg')
55 files changed, 1466 insertions, 245 deletions
diff --git a/pkg/api/handlers/compat/containers_prune.go b/pkg/api/handlers/compat/containers_prune.go index b4e98ac1f..9d77f612b 100644 --- a/pkg/api/handlers/compat/containers_prune.go +++ b/pkg/api/handlers/compat/containers_prune.go @@ -38,21 +38,24 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) { filterFuncs = append(filterFuncs, generatedFunc) } } - prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs) - if err != nil { - utils.InternalServerError(w, err) - return - } // Libpod response differs if utils.IsLibpodRequest(r) { - report := &entities.ContainerPruneReport{ - Err: pruneErrors, - ID: prunedContainers, + report, err := PruneContainersHelper(w, r, filterFuncs) + if err != nil { + utils.InternalServerError(w, err) + return } + utils.WriteResponse(w, http.StatusOK, report) return } + + prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs) + if err != nil { + utils.InternalServerError(w, err) + return + } for ctrID, size := range prunedContainers { if pruneErrors[ctrID] == nil { space += size @@ -65,3 +68,19 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, report) } + +func PruneContainersHelper(w http.ResponseWriter, r *http.Request, filterFuncs []libpod.ContainerFilter) ( + *entities.ContainerPruneReport, error) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs) + if err != nil { + utils.InternalServerError(w, err) + return nil, err + } + + report := &entities.ContainerPruneReport{ + Err: pruneErrors, + ID: prunedContainers, + } + return report, nil +} diff --git a/pkg/api/handlers/libpod/containers_create.go b/pkg/api/handlers/libpod/containers_create.go index f64132d55..40b6cacdb 100644 --- a/pkg/api/handlers/libpod/containers_create.go +++ b/pkg/api/handlers/libpod/containers_create.go @@ -1,6 +1,7 @@ package libpod import ( + "context" "encoding/json" "net/http" @@ -26,7 +27,7 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.InternalServerError(w, err) return } - ctr, err := generate.MakeContainer(runtime, &sg) + ctr, err := generate.MakeContainer(context.Background(), runtime, &sg) if err != nil { utils.InternalServerError(w, err) return diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 760ab1b7c..f7be5ce9a 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -283,7 +283,7 @@ func ImagesLoad(w http.ResponseWriter, r *http.Request) { return } } - utils.WriteResponse(w, http.StatusOK, entities.ImageLoadReport{Name: loadedImage}) + utils.WriteResponse(w, http.StatusOK, entities.ImageLoadReport{Names: split}) } func ImagesImport(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 618d48ac0..c3f8d5d66 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -11,6 +11,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi" "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/specgen/generate" "github.com/containers/libpod/pkg/util" @@ -230,14 +231,22 @@ func PodRestart(w http.ResponseWriter, r *http.Request) { } func PodPrune(w http.ResponseWriter, r *http.Request) { + reports, err := PodPruneHelper(w, r) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, reports) +} + +func PodPruneHelper(w http.ResponseWriter, r *http.Request) ([]*entities.PodPruneReport, error) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) reports []*entities.PodPruneReport ) responses, err := runtime.PrunePods(r.Context()) if err != nil { - utils.InternalServerError(w, err) - return + return nil, err } for k, v := range responses { reports = append(reports, &entities.PodPruneReport{ @@ -245,7 +254,7 @@ func PodPrune(w http.ResponseWriter, r *http.Request) { Id: k, }) } - utils.WriteResponse(w, http.StatusOK, reports) + return reports, nil } func PodPause(w http.ResponseWriter, r *http.Request) { @@ -419,3 +428,44 @@ func PodExists(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusNoContent, "") } + +func PodStats(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + + query := struct { + NamesOrIDs []string `schema:"namesOrIDs"` + All bool `schema:"all"` + }{ + // default would go here + } + 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 + } + + // Validate input. + options := entities.PodStatsOptions{All: query.All} + if err := entities.ValidatePodStatsOptions(query.NamesOrIDs, &options); err != nil { + utils.InternalServerError(w, err) + } + + // Collect the stats and send them over the wire. + containerEngine := abi.ContainerEngine{Libpod: runtime} + 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, "one or more pods not found", http.StatusNotFound, err) + return + case nil: + // Nothing to do. + default: + utils.InternalServerError(w, err) + return + } + + utils.WriteResponse(w, http.StatusOK, reports) +} diff --git a/pkg/api/handlers/libpod/system.go b/pkg/api/handlers/libpod/system.go new file mode 100644 index 000000000..98e33bf10 --- /dev/null +++ b/pkg/api/handlers/libpod/system.go @@ -0,0 +1,71 @@ +package libpod + +import ( + "net/http" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/api/handlers/compat" + "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +// SystemPrune removes unused data +func SystemPrune(w http.ResponseWriter, r *http.Request) { + var ( + decoder = r.Context().Value("decoder").(*schema.Decoder) + runtime = r.Context().Value("runtime").(*libpod.Runtime) + systemPruneReport = new(entities.SystemPruneReport) + ) + query := struct { + All bool `schema:"all"` + Volumes bool `schema:"volumes"` + }{} + + 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 + } + + podPruneReport, err := PodPruneHelper(w, r) + if err != nil { + utils.InternalServerError(w, err) + return + } + systemPruneReport.PodPruneReport = podPruneReport + + // We could parallelize this, should we? + containerPruneReport, err := compat.PruneContainersHelper(w, r, nil) + if err != nil { + utils.InternalServerError(w, err) + return + } + systemPruneReport.ContainerPruneReport = containerPruneReport + + results, err := runtime.ImageRuntime().PruneImages(r.Context(), query.All, nil) + if err != nil { + utils.InternalServerError(w, err) + return + } + + report := entities.ImagePruneReport{ + Report: entities.Report{ + Id: results, + Err: nil, + }, + } + + systemPruneReport.ImagePruneReport = &report + + if query.Volumes { + volumePruneReport, err := pruneVolumesHelper(w, r) + if err != nil { + utils.InternalServerError(w, err) + return + } + systemPruneReport.VolumePruneReport = volumePruneReport + } + utils.WriteResponse(w, http.StatusOK, systemPruneReport) +} diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 18c561a0d..c42ca407b 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -147,14 +147,22 @@ func ListVolumes(w http.ResponseWriter, r *http.Request) { } func PruneVolumes(w http.ResponseWriter, r *http.Request) { + reports, err := pruneVolumesHelper(w, r) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, reports) +} + +func pruneVolumesHelper(w http.ResponseWriter, r *http.Request) ([]*entities.VolumePruneReport, error) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) reports []*entities.VolumePruneReport ) pruned, err := runtime.PruneVolumes(r.Context()) if err != nil { - utils.InternalServerError(w, err) - return + return nil, err } for k, v := range pruned { reports = append(reports, &entities.VolumePruneReport{ @@ -162,9 +170,8 @@ func PruneVolumes(w http.ResponseWriter, r *http.Request) { Id: k, }) } - utils.WriteResponse(w, http.StatusOK, reports) + return reports, nil } - func RemoveVolume(w http.ResponseWriter, r *http.Request) { var ( runtime = r.Context().Value("runtime").(*libpod.Runtime) diff --git a/pkg/api/handlers/swagger/swagger.go b/pkg/api/handlers/swagger/swagger.go index 87891d4a8..0aceaf5f6 100644 --- a/pkg/api/handlers/swagger/swagger.go +++ b/pkg/api/handlers/swagger/swagger.go @@ -122,6 +122,13 @@ type swagPodTopResponse struct { } } +// List processes in pod +// swagger:response DocsPodStatsResponse +type swagPodStatsResponse struct { + // in:body + Body []*entities.PodStatsReport +} + // Inspect container // swagger:response LibpodInspectContainerResponse type swagLibpodInspectContainerResponse struct { @@ -143,7 +150,7 @@ type swagListPodsResponse struct { type swagInspectPodResponse struct { // in:body Body struct { - libpod.PodInspect + define.InspectPodData } } diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index aafc64353..3253a9be3 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -14,6 +14,9 @@ var ( ErrLinkNotSupport = errors.New("Link is not supported") ) +// TODO: document the exported functions in this file and make them more +// generic (e.g., not tied to one ctr/pod). + // Error formats an API response to an error // // apiMessage and code must match the container API, and are sent to client diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index 63060af41..4156dd86b 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -286,9 +286,36 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // 200: // $ref: "#/responses/DocsPodTopResponse" // 404: - // $ref: "#/responses/NoSuchContainer" + // $ref: "#/responses/NoSuchPod" // 500: // $ref: "#/responses/InternalError" r.Handle(VersionedPath("/libpod/pods/{name}/top"), s.APIHandler(libpod.PodTop)).Methods(http.MethodGet) + // swagger:operation GET /libpod/pods/stats pods statsPod + // --- + // tags: + // - pods + // summary: Get stats for one or more pods + // description: Display a live stream of resource usage statistics for the containers in one or more pods + // parameters: + // - in: query + // name: all + // description: Provide statistics for all running pods. + // type: boolean + // - in: query + // name: namesOrIDs + // description: Names or IDs of pods. + // type: array + // items: + // type: string + // produces: + // - application/json + // responses: + // 200: + // $ref: "#/responses/DocsPodTopResponse" + // 404: + // $ref: "#/responses/NoSuchPod" + // 500: + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/pods/stats"), s.APIHandler(libpod.PodStats)).Methods(http.MethodGet) return nil } diff --git a/pkg/api/server/register_system.go b/pkg/api/server/register_system.go index 708ccd39b..7375a75c1 100644 --- a/pkg/api/server/register_system.go +++ b/pkg/api/server/register_system.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/containers/libpod/pkg/api/handlers/compat" + "github.com/containers/libpod/pkg/api/handlers/libpod" "github.com/gorilla/mux" ) @@ -11,5 +12,21 @@ func (s *APIServer) registerSystemHandlers(r *mux.Router) error { r.Handle(VersionedPath("/system/df"), s.APIHandler(compat.GetDiskUsage)).Methods(http.MethodGet) // Added non version path to URI to support docker non versioned paths r.Handle("/system/df", s.APIHandler(compat.GetDiskUsage)).Methods(http.MethodGet) + // Swagger:operation POST /libpod/system/prune libpod pruneSystem + // --- + // tags: + // - system + // summary: Prune unused data + // produces: + // - application/json + // responses: + // 200: + // $ref: '#/responses/SystemPruneReport' + // 400: + // $ref: "#/responses/BadParamError" + // 500: + // $ref: "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/system/prune"), s.APIHandler(libpod.SystemPrune)).Methods(http.MethodPost) + return nil } diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index 06f01c7a0..63fe2556b 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -57,7 +57,7 @@ func List(ctx context.Context, all *bool, filters map[string][]string) ([]*entit // Get performs an image inspect. To have the on-disk size of the image calculated, you can // use the optional size parameter. -func GetImage(ctx context.Context, nameOrID string, size *bool) (*entities.ImageData, error) { +func GetImage(ctx context.Context, nameOrID string, size *bool) (*entities.ImageInspectReport, error) { conn, err := bindings.GetClient(ctx) if err != nil { return nil, err @@ -66,7 +66,7 @@ func GetImage(ctx context.Context, nameOrID string, size *bool) (*entities.Image if size != nil { params.Set("size", strconv.FormatBool(*size)) } - inspectedData := entities.ImageData{} + inspectedData := entities.ImageInspectReport{} response, err := conn.DoRequest(nil, http.MethodGet, "/images/%s/json", params, nameOrID) if err != nil { return &inspectedData, err @@ -310,9 +310,10 @@ func Push(ctx context.Context, source string, destination string, options entiti params := url.Values{} params.Set("credentials", options.Credentials) params.Set("destination", destination) - if options.TLSVerify != types.OptionalBoolUndefined { - val := bool(options.TLSVerify == types.OptionalBoolTrue) - params.Set("tlsVerify", strconv.FormatBool(val)) + if options.SkipTLSVerify != types.OptionalBoolUndefined { + // Note: we have to verify if skipped is false. + verifyTLS := bool(options.SkipTLSVerify == types.OptionalBoolFalse) + params.Set("tlsVerify", strconv.FormatBool(verifyTLS)) } path := fmt.Sprintf("/images/%s/push", source) diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index 3c60fa2a0..b213c8c73 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -2,6 +2,7 @@ package pods import ( "context" + "errors" "net/http" "net/url" "strconv" @@ -189,11 +190,6 @@ func Start(ctx context.Context, nameOrID string) (*entities.PodStartReport, erro return &report, response.Process(&report) } -func Stats() error { - // TODO - return bindings.ErrNotImplemented -} - // Stop stops all containers in a Pod. The optional timeout parameter can be // used to override the timeout before the container is killed. func Stop(ctx context.Context, nameOrID string, timeout *int) (*entities.PodStopReport, error) { @@ -264,3 +260,26 @@ func Unpause(ctx context.Context, nameOrID string) (*entities.PodUnpauseReport, } return &report, response.Process(&report) } + +// Stats display resource-usage statistics of one or more pods. +func Stats(ctx context.Context, namesOrIDs []string, options entities.PodStatsOptions) ([]*entities.PodStatsReport, error) { + if options.Latest { + return nil, errors.New("latest is not supported") + } + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + for _, i := range namesOrIDs { + params.Add("namesOrIDs", i) + } + params.Set("all", strconv.FormatBool(options.All)) + + var reports []*entities.PodStatsReport + response, err := conn.DoRequest(nil, http.MethodGet, "/pods/stats", params) + if err != nil { + return nil, err + } + return reports, response.Process(&reports) +} diff --git a/pkg/bindings/system/system.go b/pkg/bindings/system/system.go index e2f264139..df6b529de 100644 --- a/pkg/bindings/system/system.go +++ b/pkg/bindings/system/system.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/url" + "strconv" "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/domain/entities" @@ -59,3 +60,26 @@ func Events(ctx context.Context, eventChan chan (entities.Event), cancelChan cha } return nil } + +// Prune removes all unused system data. +func Prune(ctx context.Context, all, volumes *bool) (*entities.SystemPruneReport, error) { + var ( + report entities.SystemPruneReport + ) + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + params := url.Values{} + if all != nil { + params.Set("All", strconv.FormatBool(*all)) + } + if volumes != nil { + params.Set("Volumes", strconv.FormatBool(*volumes)) + } + response, err := conn.DoRequest(nil, http.MethodPost, "/system/prune", params) + if err != nil { + return nil, err + } + return &report, response.Process(&report) +} diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index 6b8d6788c..f33e42440 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -3,13 +3,13 @@ package test_bindings import ( "context" "fmt" - "github.com/containers/libpod/libpod/define" "io/ioutil" "os" "os/exec" "path/filepath" "strings" + "github.com/containers/libpod/libpod/define" . "github.com/containers/libpod/pkg/bindings" "github.com/containers/libpod/pkg/bindings/containers" "github.com/containers/libpod/pkg/specgen" @@ -189,7 +189,7 @@ func (b *bindingTest) restoreImageFromCache(i testImage) { // Run a container within or without a pod // and add or append the alpine image to it func (b *bindingTest) RunTopContainer(containerName *string, insidePod *bool, podName *string) (string, error) { - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) s.Terminal = false s.Command = []string{"top"} if containerName != nil { diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index e288dc368..c79d89b73 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -360,7 +360,7 @@ var _ = Describe("Podman containers ", func() { It("logging", func() { stdoutChan := make(chan string, 10) - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) s.Terminal = true s.Command = []string{"date", "-R"} r, err := containers.CreateWithSpec(bt.conn, s) @@ -521,7 +521,7 @@ var _ = Describe("Podman containers ", func() { }) It("container init", func() { - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) ctr, err := containers.CreateWithSpec(bt.conn, s) Expect(err).To(BeNil()) err = containers.ContainerInit(bt.conn, ctr.ID) diff --git a/pkg/bindings/test/create_test.go b/pkg/bindings/test/create_test.go index f83a9b14d..a63aa79cf 100644 --- a/pkg/bindings/test/create_test.go +++ b/pkg/bindings/test/create_test.go @@ -31,7 +31,7 @@ var _ = Describe("Create containers ", func() { }) It("create a container running top", func() { - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) s.Command = []string{"top"} s.Terminal = true s.Name = "top" diff --git a/pkg/bindings/test/info_test.go b/pkg/bindings/test/info_test.go index d0e651134..64f2b458f 100644 --- a/pkg/bindings/test/info_test.go +++ b/pkg/bindings/test/info_test.go @@ -45,7 +45,7 @@ var _ = Describe("Podman info", func() { }) It("podman info container counts", func() { - s := specgen.NewSpecGenerator(alpine.name) + s := specgen.NewSpecGenerator(alpine.name, false) _, err := containers.CreateWithSpec(bt.conn, s) Expect(err).To(BeNil()) diff --git a/pkg/bindings/test/system_test.go b/pkg/bindings/test/system_test.go index 3abc26b34..87e6d56dc 100644 --- a/pkg/bindings/test/system_test.go +++ b/pkg/bindings/test/system_test.go @@ -4,7 +4,12 @@ import ( "time" "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/bindings/containers" + "github.com/containers/libpod/pkg/bindings/pods" "github.com/containers/libpod/pkg/bindings/system" + "github.com/containers/libpod/pkg/bindings/volumes" + "github.com/containers/libpod/pkg/domain/entities" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" @@ -12,13 +17,16 @@ import ( var _ = Describe("Podman system", func() { var ( - bt *bindingTest - s *gexec.Session + bt *bindingTest + s *gexec.Session + newpod string ) BeforeEach(func() { bt = newBindingTest() bt.RestoreImagesFromCache() + newpod = "newpod" + bt.Podcreate(&newpod) s = bt.startAPIService() time.Sleep(1 * time.Second) err := bt.NewConnection() @@ -48,4 +56,98 @@ var _ = Describe("Podman system", func() { cancelChan <- true Expect(len(messages)).To(BeNumerically("==", 3)) }) + + It("podman system prune - pod,container stopped", func() { + // Start and stop a pod to enter in exited state. + _, err := pods.Start(bt.conn, newpod) + Expect(err).To(BeNil()) + _, err = pods.Stop(bt.conn, newpod, nil) + Expect(err).To(BeNil()) + // Start and stop a container to enter in exited state. + var name = "top" + _, err = bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + err = containers.Stop(bt.conn, name, nil) + Expect(err).To(BeNil()) + + systemPruneResponse, err := system.Prune(bt.conn, &bindings.PTrue, &bindings.PFalse) + Expect(err).To(BeNil()) + Expect(len(systemPruneResponse.PodPruneReport)).To(Equal(1)) + Expect(len(systemPruneResponse.ContainerPruneReport.ID)).To(Equal(1)) + Expect(len(systemPruneResponse.ImagePruneReport.Report.Id)). + To(BeNumerically(">", 0)) + Expect(systemPruneResponse.ImagePruneReport.Report.Id). + To(ContainElement("docker.io/library/alpine:latest")) + Expect(len(systemPruneResponse.VolumePruneReport)).To(Equal(0)) + }) + + It("podman system prune running alpine container", func() { + // Start and stop a pod to enter in exited state. + _, err := pods.Start(bt.conn, newpod) + Expect(err).To(BeNil()) + _, err = pods.Stop(bt.conn, newpod, nil) + Expect(err).To(BeNil()) + + // Start and stop a container to enter in exited state. + var name = "top" + _, err = bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + err = containers.Stop(bt.conn, name, nil) + Expect(err).To(BeNil()) + + // Start container and leave in running + var name2 = "top2" + _, err = bt.RunTopContainer(&name2, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + + // Adding an unused volume + _, err = volumes.Create(bt.conn, entities.VolumeCreateOptions{}) + Expect(err).To(BeNil()) + + systemPruneResponse, err := system.Prune(bt.conn, &bindings.PTrue, &bindings.PFalse) + Expect(err).To(BeNil()) + Expect(len(systemPruneResponse.PodPruneReport)).To(Equal(1)) + Expect(len(systemPruneResponse.ContainerPruneReport.ID)).To(Equal(1)) + Expect(len(systemPruneResponse.ImagePruneReport.Report.Id)). + To(BeNumerically(">", 0)) + // Alpine image should not be pruned as used by running container + Expect(systemPruneResponse.ImagePruneReport.Report.Id). + ToNot(ContainElement("docker.io/library/alpine:latest")) + // Though unsed volume is available it should not be pruned as flag set to false. + Expect(len(systemPruneResponse.VolumePruneReport)).To(Equal(0)) + }) + + It("podman system prune running alpine container volume prune", func() { + // Start a pod and leave it running + _, err := pods.Start(bt.conn, newpod) + Expect(err).To(BeNil()) + + // Start and stop a container to enter in exited state. + var name = "top" + _, err = bt.RunTopContainer(&name, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + err = containers.Stop(bt.conn, name, nil) + Expect(err).To(BeNil()) + + // Start second container and leave in running + var name2 = "top2" + _, err = bt.RunTopContainer(&name2, &bindings.PFalse, nil) + Expect(err).To(BeNil()) + + // Adding an unused volume should work + _, err = volumes.Create(bt.conn, entities.VolumeCreateOptions{}) + Expect(err).To(BeNil()) + + systemPruneResponse, err := system.Prune(bt.conn, &bindings.PTrue, &bindings.PTrue) + Expect(err).To(BeNil()) + Expect(len(systemPruneResponse.PodPruneReport)).To(Equal(0)) + Expect(len(systemPruneResponse.ContainerPruneReport.ID)).To(Equal(1)) + Expect(len(systemPruneResponse.ImagePruneReport.Report.Id)). + To(BeNumerically(">", 0)) + // Alpine image should not be pruned as used by running container + Expect(systemPruneResponse.ImagePruneReport.Report.Id). + ToNot(ContainElement("docker.io/library/alpine:latest")) + // Volume should be pruned now as flag set true + Expect(len(systemPruneResponse.VolumePruneReport)).To(Equal(1)) + }) }) diff --git a/pkg/domain/entities/container_ps.go b/pkg/domain/entities/container_ps.go index 709bb58d6..fd94d93be 100644 --- a/pkg/domain/entities/container_ps.go +++ b/pkg/domain/entities/container_ps.go @@ -25,6 +25,8 @@ type ListContainer struct { ID string `json:"Id"` // Container image Image string + // Container image ID + ImageID string // If this container is a Pod infra container IsInfra bool // Labels for container @@ -159,3 +161,31 @@ func SortPsOutput(sortBy string, psOutput SortListContainers) (SortListContainer } return psOutput, nil } + +func (l ListContainer) CGROUPNS() string { + return l.Namespaces.Cgroup +} + +func (l ListContainer) IPC() string { + return l.Namespaces.IPC +} + +func (l ListContainer) MNT() string { + return l.Namespaces.MNT +} + +func (l ListContainer) NET() string { + return l.Namespaces.NET +} + +func (l ListContainer) PIDNS() string { + return l.Namespaces.PIDNS +} + +func (l ListContainer) USERNS() string { + return l.Namespaces.User +} + +func (l ListContainer) UTS() string { + return l.Namespaces.UTS +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 8c5bc3058..eebf4c033 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -41,6 +41,7 @@ type ContainerEngine interface { ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) Events(ctx context.Context, opts EventsOptions) error + GenerateSystemd(ctx context.Context, nameOrID string, opts GenerateSystemdOptions) (*GenerateSystemdReport, error) HealthCheckRun(ctx context.Context, nameOrId string, options HealthCheckOptions) (*define.HealthCheckResults, error) Info(ctx context.Context) (*define.Info, error) PodCreate(ctx context.Context, opts PodCreateOptions) (*PodCreateReport, error) @@ -53,6 +54,7 @@ type ContainerEngine interface { PodRestart(ctx context.Context, namesOrIds []string, options PodRestartOptions) ([]*PodRestartReport, error) PodRm(ctx context.Context, namesOrIds []string, options PodRmOptions) ([]*PodRmReport, error) PodStart(ctx context.Context, namesOrIds []string, options PodStartOptions) ([]*PodStartReport, error) + PodStats(ctx context.Context, namesOrIds []string, options PodStatsOptions) ([]*PodStatsReport, error) PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error) PodTop(ctx context.Context, options PodTopOptions) (*StringSliceReport, error) PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error) diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index b118a4104..46a96ca20 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -13,7 +13,7 @@ type ImageEngine interface { Exists(ctx context.Context, nameOrId string) (*BoolReport, error) History(ctx context.Context, nameOrId string, opts ImageHistoryOptions) (*ImageHistoryReport, error) Import(ctx context.Context, opts ImageImportOptions) (*ImageImportReport, error) - Inspect(ctx context.Context, names []string, opts InspectOptions) (*ImageInspectReport, error) + Inspect(ctx context.Context, namesOrIDs []string, opts InspectOptions) ([]*ImageInspectReport, error) List(ctx context.Context, opts ImageListOptions) ([]*ImageSummary, error) Load(ctx context.Context, opts ImageLoadOptions) (*ImageLoadReport, error) Prune(ctx context.Context, opts ImagePruneOptions) (*ImagePruneReport, error) diff --git a/pkg/domain/entities/generate.go b/pkg/domain/entities/generate.go new file mode 100644 index 000000000..6d65b52f8 --- /dev/null +++ b/pkg/domain/entities/generate.go @@ -0,0 +1,22 @@ +package entities + +// GenerateSystemdOptions control the generation of systemd unit files. +type GenerateSystemdOptions struct { + // Files - generate files instead of printing to stdout. + Files bool + // Name - use container/pod name instead of its ID. + Name bool + // New - create a new container instead of starting a new one. + New bool + // RestartPolicy - systemd restart policy. + RestartPolicy string + // StopTimeout - time when stopping the container. + StopTimeout *uint +} + +// GenerateSystemdReport +type GenerateSystemdReport struct { + // Output of the generate process. Either the generated files or their + // entire content. + Output string +} diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 773cd90b4..442b2cf3c 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -183,8 +183,8 @@ type ImagePushOptions struct { // SignBy adds a signature at the destination using the specified key. // Ignored for remote calls. SignBy string - // TLSVerify to enable/disable HTTPS and certificate verification. - TLSVerify types.OptionalBool + // SkipTLSVerify to skip HTTPS and certificate verification. + SkipTLSVerify types.OptionalBool } // ImageSearchOptions are the arguments for searching images. @@ -238,13 +238,9 @@ type ImagePruneReport struct { type ImageTagOptions struct{} type ImageUntagOptions struct{} -type ImageData struct { - *inspect.ImageData -} - +// ImageInspectReport is the data when inspecting an image. type ImageInspectReport struct { - Images []*ImageData - Errors map[string]error + *inspect.ImageData } type ImageLoadOptions struct { @@ -256,7 +252,7 @@ type ImageLoadOptions struct { } type ImageLoadReport struct { - Name string + Names []string } type ImageImportOptions struct { diff --git a/pkg/domain/entities/manifest.go b/pkg/domain/entities/manifest.go index a9c961f9d..7316735b0 100644 --- a/pkg/domain/entities/manifest.go +++ b/pkg/domain/entities/manifest.go @@ -10,6 +10,7 @@ type ManifestAddOptions struct { Arch string `json:"arch" schema:"arch"` Features []string `json:"features" schema:"features"` Images []string `json:"images" schema:"images"` + OS string `json:"os" schema:"os"` OSVersion string `json:"os_version" schema:"os_version"` Variant string `json:"variant" schema:"variant"` } diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index aa1445a6a..a4896ce4d 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -1,6 +1,7 @@ package entities import ( + "errors" "strings" "time" @@ -188,3 +189,50 @@ type PodInspectOptions struct { type PodInspectReport struct { *define.InspectPodData } + +// PodStatsOptions are options for the pod stats command. +type PodStatsOptions struct { + // All - provide stats for all running pods. + All bool + // Latest - provide stats for the latest pod. + Latest bool +} + +// PodStatsReport includes pod-resource statistics data. +type PodStatsReport struct { + CPU string + MemUsage string + Mem string + NetIO string + BlockIO string + PIDS string + Pod string + CID string + Name string +} + +// ValidatePodStatsOptions validates the specified slice and options. Allows +// for sharing code in the front- and the back-end. +func ValidatePodStatsOptions(args []string, options *PodStatsOptions) error { + num := 0 + if len(args) > 0 { + num++ + } + if options.All { + num++ + } + if options.Latest { + num++ + } + switch num { + case 0: + // Podman v1 compat: if nothing's specified get all running + // pods. + options.All = true + return nil + case 1: + return nil + default: + return errors.New("--all, --latest and arguments cannot be used together") + } +} diff --git a/pkg/domain/entities/system.go b/pkg/domain/entities/system.go index 3ddc04293..de93a382f 100644 --- a/pkg/domain/entities/system.go +++ b/pkg/domain/entities/system.go @@ -12,3 +12,17 @@ type ServiceOptions struct { Timeout time.Duration // duration of inactivity the service should wait before shutting down Command *cobra.Command // CLI command provided. Used in V1 code } + +// SystemPruneOptions provides options to prune system. +type SystemPruneOptions struct { + All bool + Volume bool +} + +// SystemPruneReport provides report after system prune is executed. +type SystemPruneReport struct { + PodPruneReport []*PodPruneReport + *ContainerPruneReport + *ImagePruneReport + VolumePruneReport []*VolumePruneReport +} diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go index d742cc53d..9fbe04c9a 100644 --- a/pkg/domain/entities/types.go +++ b/pkg/domain/entities/types.go @@ -47,10 +47,14 @@ type NetOptions struct { // All CLI inspect commands and inspect sub-commands use the same options type InspectOptions struct { + // Format - change the output to JSON or a Go template. Format string `json:",omitempty"` - Latest bool `json:",omitempty"` - Size bool `json:",omitempty"` - Type string `json:",omitempty"` + // Latest - inspect the latest container Podman is aware of. + Latest bool `json:",omitempty"` + // Size (containers only) - display total file size. + Size bool `json:",omitempty"` + // Type -- return JSON for specified type. + Type string `json:",omitempty"` } // All API and CLI diff commands and diff sub-commands use the same options diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index a77b18ce1..4c3389418 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -217,12 +217,23 @@ func (ic *ContainerEngine) ContainerKill(ctx context.Context, namesOrIds []strin } func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []string, options entities.RestartOptions) ([]*entities.RestartReport, error) { var ( + ctrs []*libpod.Container + err error reports []*entities.RestartReport ) - ctrs, err := getContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) - if err != nil { - return nil, err + + if options.Running { + ctrs, err = ic.Libpod.GetRunningContainers() + if err != nil { + return nil, err + } + } else { + ctrs, err = getContainersByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, err + } } + for _, con := range ctrs { timeout := con.StopTimeout() if options.Timeout != nil { @@ -481,7 +492,7 @@ func (ic *ContainerEngine) ContainerCreate(ctx context.Context, s *specgen.SpecG if err := generate.CompleteSpec(ctx, ic.Libpod, s); err != nil { return nil, err } - ctr, err := generate.MakeContainer(ic.Libpod, s) + ctr, err := generate.MakeContainer(ctx, ic.Libpod, s) if err != nil { return nil, err } @@ -669,7 +680,7 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta if err := generate.CompleteSpec(ctx, ic.Libpod, opts.Spec); err != nil { return nil, err } - ctr, err := generate.MakeContainer(ic.Libpod, opts.Spec) + ctr, err := generate.MakeContainer(ctx, ic.Libpod, opts.Spec) if err != nil { return nil, err } @@ -837,7 +848,13 @@ func (ic *ContainerEngine) ContainerInit(ctx context.Context, namesOrIds []strin } for _, ctr := range ctrs { report := entities.ContainerInitReport{Id: ctr.ID()} - report.Err = ctr.Init(ctx) + err := ctr.Init(ctx) + + // If we're initializing all containers, ignore invalid state errors + if options.All && errors.Cause(err) == define.ErrCtrStateInvalid { + err = nil + } + report.Err = err reports = append(reports, &report) } return reports, nil @@ -932,7 +949,7 @@ func (ic *ContainerEngine) Config(_ context.Context) (*config.Config, error) { func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrId string, options entities.ContainerPortOptions) ([]*entities.ContainerPortReport, error) { var reports []*entities.ContainerPortReport - ctrs, err := getContainersByContext(options.All, false, []string{nameOrId}, ic.Libpod) + ctrs, err := getContainersByContext(options.All, options.Latest, []string{nameOrId}, ic.Libpod) if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go new file mode 100644 index 000000000..f69ba560e --- /dev/null +++ b/pkg/domain/infra/abi/generate.go @@ -0,0 +1,174 @@ +package abi + +import ( + "context" + "fmt" + "strings" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/systemd/generate" + "github.com/pkg/errors" +) + +func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, options entities.GenerateSystemdOptions) (*entities.GenerateSystemdReport, error) { + opts := generate.Options{ + Files: options.Files, + New: options.New, + } + + // First assume it's a container. + if info, found, err := ic.generateSystemdgenContainerInfo(nameOrID, nil, options); found && err != nil { + return nil, err + } else if found && err == nil { + output, err := generate.CreateContainerSystemdUnit(info, opts) + if err != nil { + return nil, err + } + return &entities.GenerateSystemdReport{Output: output}, nil + } + + // --new does not support pods. + if options.New { + return nil, errors.Errorf("error generating systemd unit files: cannot generate generic files for a pod") + } + + // We're either having a pod or garbage. + pod, err := ic.Libpod.LookupPod(nameOrID) + if err != nil { + return nil, err + } + + // Error out if the pod has no infra container, which we require to be the + // main service. + if !pod.HasInfraContainer() { + return nil, fmt.Errorf("error generating systemd unit files: Pod %q has no infra container", pod.Name()) + } + + // Generate a systemdgen.ContainerInfo for the infra container. This + // ContainerInfo acts as the main service of the pod. + infraID, err := pod.InfraContainerID() + if err != nil { + return nil, nil + } + podInfo, _, err := ic.generateSystemdgenContainerInfo(infraID, pod, options) + if err != nil { + return nil, err + } + + // Compute the container-dependency graph for the Pod. + containers, err := pod.AllContainers() + if err != nil { + return nil, err + } + if len(containers) == 0 { + return nil, fmt.Errorf("error generating systemd unit files: Pod %q has no containers", pod.Name()) + } + graph, err := libpod.BuildContainerGraph(containers) + if err != nil { + return nil, err + } + + // Traverse the dependency graph and create systemdgen.ContainerInfo's for + // each container. + containerInfos := []*generate.ContainerInfo{podInfo} + for ctr, dependencies := range graph.DependencyMap() { + // Skip the infra container as we already generated it. + if ctr.ID() == infraID { + continue + } + ctrInfo, _, err := ic.generateSystemdgenContainerInfo(ctr.ID(), nil, options) + if err != nil { + return nil, err + } + // Now add the container's dependencies and at the container as a + // required service of the infra container. + for _, dep := range dependencies { + if dep.ID() == infraID { + ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, podInfo.ServiceName) + } else { + _, serviceName := generateServiceName(dep, nil, options) + ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, serviceName) + } + } + podInfo.RequiredServices = append(podInfo.RequiredServices, ctrInfo.ServiceName) + containerInfos = append(containerInfos, ctrInfo) + } + + // Now generate the systemd service for all containers. + builder := strings.Builder{} + for i, info := range containerInfos { + if i > 0 { + builder.WriteByte('\n') + } + out, err := generate.CreateContainerSystemdUnit(info, opts) + if err != nil { + return nil, err + } + builder.WriteString(out) + } + + return &entities.GenerateSystemdReport{Output: builder.String()}, nil +} + +// generateSystemdgenContainerInfo is a helper to generate a +// systemdgen.ContainerInfo for `GenerateSystemd`. +func (ic *ContainerEngine) generateSystemdgenContainerInfo(nameOrID string, pod *libpod.Pod, options entities.GenerateSystemdOptions) (*generate.ContainerInfo, bool, error) { + ctr, err := ic.Libpod.LookupContainer(nameOrID) + if err != nil { + return nil, false, err + } + + timeout := ctr.StopTimeout() + if options.StopTimeout != nil { + timeout = *options.StopTimeout + } + + config := ctr.Config() + conmonPidFile := config.ConmonPidFile + if conmonPidFile == "" { + return nil, true, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag") + } + + createCommand := []string{} + if config.CreateCommand != nil { + createCommand = config.CreateCommand + } else if options.New { + return nil, true, errors.Errorf("cannot use --new on container %q: no create command found", nameOrID) + } + + name, serviceName := generateServiceName(ctr, pod, options) + info := &generate.ContainerInfo{ + ServiceName: serviceName, + ContainerName: name, + RestartPolicy: options.RestartPolicy, + PIDFile: conmonPidFile, + StopTimeout: timeout, + GenerateTimestamp: true, + CreateCommand: createCommand, + } + + return info, true, nil +} + +// generateServiceName generates the container name and the service name for systemd service. +func generateServiceName(ctr *libpod.Container, pod *libpod.Pod, options entities.GenerateSystemdOptions) (string, string) { + var kind, name, ctrName string + if pod == nil { + kind = "container" + name = ctr.ID() + if options.Name { + name = ctr.Name() + } + ctrName = name + } else { + kind = "pod" + name = pod.ID() + ctrName = ctr.ID() + if options.Name { + name = pod.Name() + ctrName = ctr.Name() + } + } + return ctrName, fmt.Sprintf("%s-%s", kind, name) +} diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 64d9c9f12..d1245a45c 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -46,7 +46,6 @@ func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOption Id: results, Err: nil, }, - Size: 0, } return &report, nil } @@ -171,29 +170,24 @@ func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entiti return &entities.ImagePullReport{Images: foundIDs}, nil } -func (ir *ImageEngine) Inspect(ctx context.Context, names []string, opts entities.InspectOptions) (*entities.ImageInspectReport, error) { - report := entities.ImageInspectReport{ - Errors: make(map[string]error), - } - - for _, id := range names { - img, err := ir.Libpod.ImageRuntime().NewFromLocal(id) +func (ir *ImageEngine) Inspect(ctx context.Context, namesOrIDs []string, opts entities.InspectOptions) ([]*entities.ImageInspectReport, error) { + reports := []*entities.ImageInspectReport{} + for _, i := range namesOrIDs { + img, err := ir.Libpod.ImageRuntime().NewFromLocal(i) if err != nil { - report.Errors[id] = err - continue + return nil, err } - - results, err := img.Inspect(ctx) + result, err := img.Inspect(ctx) if err != nil { - report.Errors[id] = err - continue + return nil, err } - - cookedResults := entities.ImageData{} - _ = domainUtils.DeepCopy(&cookedResults, results) - report.Images = append(report.Images, &cookedResults) + report := entities.ImageInspectReport{} + if err := domainUtils.DeepCopy(&report, result); err != nil { + return nil, err + } + reports = append(reports, &report) } - return &report, nil + return reports, nil } func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, options entities.ImagePushOptions) error { @@ -227,7 +221,7 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri dockerRegistryOptions := image.DockerRegistryOptions{ DockerRegistryCreds: registryCreds, DockerCertPath: options.CertDir, - DockerInsecureSkipTLSVerify: options.TLSVerify, + DockerInsecureSkipTLSVerify: options.SkipTLSVerify, } signOptions := image.SigningOptions{ @@ -326,16 +320,19 @@ func (ir *ImageEngine) Load(ctx context.Context, opts entities.ImageLoadOptions) if err != nil { return nil, err } - newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(name) - if err != nil { - return nil, errors.Wrap(err, "image loaded but no additional tags were created") - } - if len(opts.Name) > 0 { - if err := newImage.TagImage(fmt.Sprintf("%s:%s", opts.Name, opts.Tag)); err != nil { - return nil, errors.Wrapf(err, "error adding %q to image %q", opts.Name, newImage.InputName) + names := strings.Split(name, ",") + if len(names) <= 1 { + newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(name) + if err != nil { + return nil, errors.Wrap(err, "image loaded but no additional tags were created") + } + if len(opts.Name) > 0 { + if err := newImage.TagImage(fmt.Sprintf("%s:%s", opts.Name, opts.Tag)); err != nil { + return nil, errors.Wrapf(err, "error adding %q to image %q", opts.Name, newImage.InputName) + } } } - return &entities.ImageLoadReport{Name: name}, nil + return &entities.ImageLoadReport{Names: names}, nil } func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOptions) (*entities.ImageImportReport, error) { diff --git a/pkg/domain/infra/abi/manifest.go b/pkg/domain/infra/abi/manifest.go index 27d4bf9a5..88331f96c 100644 --- a/pkg/domain/infra/abi/manifest.go +++ b/pkg/domain/infra/abi/manifest.go @@ -79,6 +79,7 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAd Arch: opts.Arch, Features: opts.Features, Images: opts.Images, + OS: opts.OS, OSVersion: opts.OSVersion, Variant: opts.Variant, } diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go index c4ae9efbf..b286bcf0d 100644 --- a/pkg/domain/infra/abi/pods.go +++ b/pkg/domain/infra/abi/pods.go @@ -145,7 +145,7 @@ func (ic *ContainerEngine) PodStop(ctx context.Context, namesOrIds []string, opt reports []*entities.PodStopReport ) pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) - if err != nil { + if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchPod) { return nil, err } for _, p := range pods { @@ -180,6 +180,7 @@ func (ic *ContainerEngine) PodRestart(ctx context.Context, namesOrIds []string, errs, err := p.Restart(ctx) if err != nil { report.Errs = []error{err} + reports = append(reports, &report) continue } if len(errs) > 0 { @@ -207,6 +208,7 @@ func (ic *ContainerEngine) PodStart(ctx context.Context, namesOrIds []string, op errs, err := p.Start(ctx) if err != nil { report.Errs = []error{err} + reports = append(reports, &report) continue } if len(errs) > 0 { @@ -226,7 +228,7 @@ func (ic *ContainerEngine) PodRm(ctx context.Context, namesOrIds []string, optio reports []*entities.PodRmReport ) pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) - if err != nil { + if err != nil && !(options.Ignore && errors.Cause(err) == define.ErrNoSuchPod) { return nil, err } for _, p := range pods { @@ -234,7 +236,6 @@ func (ic *ContainerEngine) PodRm(ctx context.Context, namesOrIds []string, optio err := ic.Libpod.RemovePod(ctx, p, true, options.Force) if err != nil { report.Err = err - continue } reports = append(reports, &report) } @@ -292,9 +293,12 @@ func (ic *ContainerEngine) PodTop(ctx context.Context, options entities.PodTopOp func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOptions) ([]*entities.ListPodsReport, error) { var ( + err error filters []libpod.PodFilter + pds []*libpod.Pod reports []*entities.ListPodsReport ) + for k, v := range options.Filters { for _, filter := range v { f, err := lpfilters.GeneratePodFilterFunc(k, filter) @@ -305,10 +309,19 @@ func (ic *ContainerEngine) PodPs(ctx context.Context, options entities.PodPSOpti } } - pds, err := ic.Libpod.Pods(filters...) - if err != nil { - return nil, err + if options.Latest { + pod, err := ic.Libpod.GetLatestPod() + if err != nil { + return nil, err + } + pds = append(pds, pod) + } else { + pds, err = ic.Libpod.Pods(filters...) + if err != nil { + return nil, err + } } + for _, p := range pds { var lpcs []*entities.ListPodContainer status, err := p.GetPodStatus() diff --git a/pkg/domain/infra/abi/pods_stats.go b/pkg/domain/infra/abi/pods_stats.go new file mode 100644 index 000000000..a41c01da0 --- /dev/null +++ b/pkg/domain/infra/abi/pods_stats.go @@ -0,0 +1,85 @@ +package abi + +import ( + "context" + "fmt" + + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/rootless" + "github.com/docker/go-units" + "github.com/pkg/errors" +) + +// PodStats implements printing stats about pods. +func (ic *ContainerEngine) PodStats(ctx context.Context, namesOrIds []string, options entities.PodStatsOptions) ([]*entities.PodStatsReport, error) { + // Cgroups v2 check for rootless. + if rootless.IsRootless() { + unified, err := cgroups.IsCgroup2UnifiedMode() + if err != nil { + return nil, err + } + if !unified { + return nil, errors.New("pod stats is not supported in rootless mode without cgroups v2") + } + } + // Get the (running) pods and convert them to the entities format. + pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) + if err != nil { + return nil, errors.Wrap(err, "unable to get list of pods") + } + return ic.podsToStatsReport(pods) +} + +// podsToStatsReport converts a slice of pods into a corresponding slice of stats reports. +func (ic *ContainerEngine) podsToStatsReport(pods []*libpod.Pod) ([]*entities.PodStatsReport, error) { + reports := []*entities.PodStatsReport{} + for i := range pods { // Access by index to prevent potential loop-variable leaks. + podStats, err := pods[i].GetPodStats(nil) + if err != nil { + return nil, err + } + podID := pods[i].ID()[:12] + for j := range podStats { + r := entities.PodStatsReport{ + CPU: floatToPercentString(podStats[j].CPU), + MemUsage: combineHumanValues(podStats[j].MemUsage, podStats[j].MemLimit), + Mem: floatToPercentString(podStats[j].MemPerc), + NetIO: combineHumanValues(podStats[j].NetInput, podStats[j].NetOutput), + BlockIO: combineHumanValues(podStats[j].BlockInput, podStats[j].BlockOutput), + PIDS: pidsToString(podStats[j].PIDs), + CID: podStats[j].ContainerID[:12], + Name: podStats[j].Name, + Pod: podID, + } + reports = append(reports, &r) + } + } + + return reports, nil +} + +func combineHumanValues(a, b uint64) string { + if a == 0 && b == 0 { + return "-- / --" + } + return fmt.Sprintf("%s / %s", units.HumanSize(float64(a)), units.HumanSize(float64(b))) +} + +func floatToPercentString(f float64) string { + strippedFloat, err := libpod.RemoveScientificNotationFromFloat(f) + if err != nil || strippedFloat == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%.2f", strippedFloat) + "%" +} + +func pidsToString(pid uint64) string { + if pid == 0 { + // If things go bazinga, return a safe value + return "--" + } + return fmt.Sprintf("%d", pid) +} diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 18d6613f4..32f9c4e36 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -115,11 +115,15 @@ func (ic *ContainerEngine) ContainerRestart(ctx context.Context, namesOrIds []st t := int(*options.Timeout) timeout = &t } + ctrs, err := getContainersByContext(ic.ClientCxt, options.All, namesOrIds) if err != nil { return nil, err } for _, c := range ctrs { + if options.Running && c.State != define.ContainerStateRunning.String() { + continue + } reports = append(reports, &entities.RestartReport{ Id: c.ID, Err: containers.Restart(ic.ClientCxt, c.ID, timeout), diff --git a/pkg/domain/infra/tunnel/generate.go b/pkg/domain/infra/tunnel/generate.go new file mode 100644 index 000000000..3cd483053 --- /dev/null +++ b/pkg/domain/infra/tunnel/generate.go @@ -0,0 +1,12 @@ +package tunnel + +import ( + "context" + + "github.com/containers/libpod/pkg/domain/entities" + "github.com/pkg/errors" +) + +func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, options entities.GenerateSystemdOptions) (*entities.GenerateSystemdReport, error) { + return nil, errors.New("not implemented for tunnel") +} diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 66e4e6e3f..dcc5fc3e7 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -143,16 +143,16 @@ func (ir *ImageEngine) Untag(ctx context.Context, nameOrId string, tags []string return nil } -func (ir *ImageEngine) Inspect(_ context.Context, names []string, opts entities.InspectOptions) (*entities.ImageInspectReport, error) { - report := entities.ImageInspectReport{} - for _, id := range names { - r, err := images.GetImage(ir.ClientCxt, id, &opts.Size) +func (ir *ImageEngine) Inspect(ctx context.Context, namesOrIDs []string, opts entities.InspectOptions) ([]*entities.ImageInspectReport, error) { + reports := []*entities.ImageInspectReport{} + for _, i := range namesOrIDs { + r, err := images.GetImage(ir.ClientCxt, i, &opts.Size) if err != nil { - report.Errors[id] = err + return nil, err } - report.Images = append(report.Images, r) + reports = append(reports, r) } - return &report, nil + return reports, nil } func (ir *ImageEngine) Load(ctx context.Context, opts entities.ImageLoadOptions) (*entities.ImageLoadReport, error) { diff --git a/pkg/domain/infra/tunnel/manifest.go b/pkg/domain/infra/tunnel/manifest.go index 338256530..18b400533 100644 --- a/pkg/domain/infra/tunnel/manifest.go +++ b/pkg/domain/infra/tunnel/manifest.go @@ -41,6 +41,7 @@ func (ir *ImageEngine) ManifestAdd(ctx context.Context, opts entities.ManifestAd Arch: opts.Arch, Features: opts.Features, Images: opts.Images, + OS: opts.OS, OSVersion: opts.OSVersion, Variant: opts.Variant, } diff --git a/pkg/domain/infra/tunnel/pods.go b/pkg/domain/infra/tunnel/pods.go index e7641c077..c193c6752 100644 --- a/pkg/domain/infra/tunnel/pods.go +++ b/pkg/domain/infra/tunnel/pods.go @@ -211,3 +211,7 @@ func (ic *ContainerEngine) PodInspect(ctx context.Context, options entities.PodI } return pods.Inspect(ic.ClientCxt, options.NameOrID) } + +func (ic *ContainerEngine) PodStats(ctx context.Context, namesOrIds []string, options entities.PodStatsOptions) ([]*entities.PodStatsReport, error) { + return pods.Stats(ic.ClientCxt, namesOrIds, options) +} diff --git a/pkg/namespaces/namespaces.go b/pkg/namespaces/namespaces.go index 2cb3c3f20..2ffbde977 100644 --- a/pkg/namespaces/namespaces.go +++ b/pkg/namespaces/namespaces.go @@ -31,7 +31,7 @@ func (n CgroupMode) IsHost() bool { // IsDefaultValue indicates whether the cgroup namespace has the default value. func (n CgroupMode) IsDefaultValue() bool { - return n == "" + return n == "" || n == defaultType } // IsNS indicates a cgroup namespace passed in by path (ns:<path>) @@ -102,6 +102,11 @@ func (n UsernsMode) IsAuto() bool { return parts[0] == "auto" } +// IsDefaultValue indicates whether the user namespace has the default value. +func (n UsernsMode) IsDefaultValue() bool { + return n == "" || n == defaultType +} + // GetAutoOptions returns a AutoUserNsOptions with the settings to setup automatically // a user namespace. func (n UsernsMode) GetAutoOptions() (*storage.AutoUserNsOptions, error) { diff --git a/pkg/ps/ps.go b/pkg/ps/ps.go index d0fef65c8..907063df9 100644 --- a/pkg/ps/ps.go +++ b/pkg/ps/ps.go @@ -158,6 +158,7 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities ExitedAt: exitedTime.Unix(), ID: conConfig.ID, Image: conConfig.RootfsImageName, + ImageID: conConfig.RootfsImageID, IsInfra: conConfig.IsInfra, Labels: conConfig.Labels, Mounts: ctr.UserVolumes(), diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index 72d461cdc..716db81dc 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -535,32 +535,30 @@ create_pause_process (const char *pause_pid_file_path, char **argv) } } -static void -join_namespace_or_die (int pid_to_join, const char *ns_file) +static int +open_namespace (int pid_to_join, const char *ns_file) { char ns_path[PATH_MAX]; int ret; - int fd; ret = snprintf (ns_path, PATH_MAX, "/proc/%d/ns/%s", pid_to_join, ns_file); if (ret == PATH_MAX) { fprintf (stderr, "internal error: namespace path too long\n"); - _exit (EXIT_FAILURE); + return -1; } - fd = open (ns_path, O_CLOEXEC | O_RDONLY); - if (fd < 0) - { - fprintf (stderr, "cannot open: %s\n", ns_path); - _exit (EXIT_FAILURE); - } - if (setns (fd, 0) < 0) + return open (ns_path, O_CLOEXEC | O_RDONLY); +} + +static void +join_namespace_or_die (const char *name, int ns_fd) +{ + if (setns (ns_fd, 0) < 0) { - fprintf (stderr, "cannot set namespace to %s: %s\n", ns_path, strerror (errno)); + fprintf (stderr, "cannot set %s namespace\n", name); _exit (EXIT_FAILURE); } - close (fd); } int @@ -570,6 +568,8 @@ reexec_userns_join (int pid_to_join, char *pause_pid_file_path) char gid[16]; char **argv; int pid; + int mnt_ns = -1; + int user_ns = -1; char *cwd = getcwd (NULL, 0); sigset_t sigset, oldsigset; @@ -589,14 +589,28 @@ reexec_userns_join (int pid_to_join, char *pause_pid_file_path) _exit (EXIT_FAILURE); } + user_ns = open_namespace (pid_to_join, "user"); + if (user_ns < 0) + return user_ns; + mnt_ns = open_namespace (pid_to_join, "mnt"); + if (mnt_ns < 0) + { + close (user_ns); + return mnt_ns; + } + pid = fork (); if (pid < 0) fprintf (stderr, "cannot fork: %s\n", strerror (errno)); if (pid) { - /* We passed down these fds, close them. */ int f; + + /* We passed down these fds, close them. */ + close (user_ns); + close (mnt_ns); + for (f = 3; f < open_files_max_fd; f++) if (open_files_set == NULL || FD_ISSET (f % FD_SETSIZE, &(open_files_set[f / FD_SETSIZE]))) close (f); @@ -634,8 +648,10 @@ reexec_userns_join (int pid_to_join, char *pause_pid_file_path) _exit (EXIT_FAILURE); } - join_namespace_or_die (pid_to_join, "user"); - join_namespace_or_die (pid_to_join, "mnt"); + join_namespace_or_die ("user", user_ns); + join_namespace_or_die ("mnt", mnt_ns); + close (user_ns); + close (mnt_ns); if (syscall_setresgid (0, 0, 0) < 0) { diff --git a/pkg/rootlessport/rootlessport_linux.go b/pkg/rootlessport/rootlessport_linux.go index 1c1ed39df..c686d80fc 100644 --- a/pkg/rootlessport/rootlessport_linux.go +++ b/pkg/rootlessport/rootlessport_linux.go @@ -102,25 +102,27 @@ func parent() error { return err } - sigC := make(chan os.Signal, 1) - signal.Notify(sigC, unix.SIGPIPE) - defer func() { - // dummy signal to terminate the goroutine - sigC <- unix.SIGKILL - }() + exitC := make(chan os.Signal, 1) + defer close(exitC) + go func() { + sigC := make(chan os.Signal, 1) + signal.Notify(sigC, unix.SIGPIPE) defer func() { signal.Stop(sigC) close(sigC) }() - s := <-sigC - if s == unix.SIGPIPE { - if f, err := os.OpenFile("/dev/null", os.O_WRONLY, 0755); err == nil { - unix.Dup2(int(f.Fd()), 1) // nolint:errcheck - unix.Dup2(int(f.Fd()), 2) // nolint:errcheck - f.Close() + select { + case s := <-sigC: + if s == unix.SIGPIPE { + if f, err := os.OpenFile("/dev/null", os.O_WRONLY, 0755); err == nil { + unix.Dup2(int(f.Fd()), 1) // nolint:errcheck + unix.Dup2(int(f.Fd()), 2) // nolint:errcheck + f.Close() + } } + case <-exitC: } }() diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 7ee2df890..a62344640 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -326,10 +326,6 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM } defaultEnv = env.Join(env.DefaultEnvVariables, defaultEnv) } - config.Env = env.Join(defaultEnv, config.Env) - for name, val := range config.Env { - g.AddProcessEnv(name, val) - } if err := addRlimits(config, &g); err != nil { return nil, err @@ -360,6 +356,11 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userM if err := config.Cgroup.ConfigureGenerator(&g); err != nil { return nil, err } + + config.Env = env.Join(defaultEnv, config.Env) + for name, val := range config.Env { + g.AddProcessEnv(name, val) + } configSpec := g.Config // If the container image specifies an label with a diff --git a/pkg/specgen/container_validate.go b/pkg/specgen/container_validate.go index 56c1a7ea9..94e456c52 100644 --- a/pkg/specgen/container_validate.go +++ b/pkg/specgen/container_validate.go @@ -14,7 +14,7 @@ var ( // SystemDValues describes the only values that SystemD can be SystemDValues = []string{"true", "false", "always"} // ImageVolumeModeValues describes the only values that ImageVolumeMode can be - ImageVolumeModeValues = []string{"ignore", "tmpfs", "bind"} + ImageVolumeModeValues = []string{"ignore", "tmpfs", "anonymous"} ) func exclusiveOptions(opt1, opt2 string) error { @@ -34,7 +34,7 @@ func (s *SpecGenerator) Validate() error { } // Cannot set hostname and utsns if len(s.ContainerBasicConfig.Hostname) > 0 && !s.ContainerBasicConfig.UtsNS.IsPrivate() { - return errors.Wrap(ErrInvalidSpecConfig, "cannot set hostname when creating an UTS namespace") + return errors.Wrap(ErrInvalidSpecConfig, "cannot set hostname when running in the host UTS namespace") } // systemd values must be true, false, or always if len(s.ContainerBasicConfig.Systemd) > 0 && !util.StringInSlice(strings.ToLower(s.ContainerBasicConfig.Systemd), SystemDValues) { diff --git a/pkg/specgen/generate/config_linux_cgo.go b/pkg/specgen/generate/config_linux_cgo.go index b06ef5c9a..5d629a6e6 100644 --- a/pkg/specgen/generate/config_linux_cgo.go +++ b/pkg/specgen/generate/config_linux_cgo.go @@ -24,6 +24,9 @@ func getSeccompConfig(s *specgen.SpecGenerator, configSpec *spec.Spec, img *imag } if scp == seccomp.PolicyImage { + if img == nil { + return nil, errors.New("cannot read seccomp profile without a valid image") + } labels, err := img.Labels(context.Background()) if err != nil { return nil, err diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index de3239fda..92a2b4d35 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -3,24 +3,38 @@ package generate import ( "context" + "github.com/containers/image/v5/manifest" "github.com/containers/libpod/libpod" ann "github.com/containers/libpod/pkg/annotations" envLib "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/signal" "github.com/containers/libpod/pkg/specgen" - "github.com/pkg/errors" "golang.org/x/sys/unix" ) func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerator) error { - var appendEntryPoint bool + // If a rootfs is used, then there is no image data + if s.ContainerStorageConfig.Rootfs != "" { + return nil + } - // TODO add support for raw rootfs newImage, err := r.ImageRuntime().NewFromLocal(s.Image) if err != nil { return err } + _, mediaType, err := newImage.Manifest(ctx) + if err != nil { + return err + } + + if s.HealthConfig == nil && mediaType == manifest.DockerV2Schema2MediaType { + s.HealthConfig, err = newImage.GetHealthCheck(ctx) + if err != nil { + return err + } + } + // Image stop signal if s.StopSignal == nil { stopSignal, err := newImage.StopSignal(ctx) @@ -96,28 +110,6 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat } s.Annotations = annotations - // entrypoint - entrypoint, err := newImage.Entrypoint(ctx) - if err != nil { - return err - } - if len(s.Entrypoint) < 1 && len(entrypoint) > 0 { - appendEntryPoint = true - s.Entrypoint = entrypoint - } - command, err := newImage.Cmd(ctx) - if err != nil { - return err - } - if len(s.Command) < 1 && len(command) > 0 { - if appendEntryPoint { - s.Command = entrypoint - } - s.Command = append(s.Command, command...) - } - if len(s.Command) < 1 && len(s.Entrypoint) < 1 { - return errors.Errorf("No command provided or as CMD or ENTRYPOINT in this image") - } // workdir workingDir, err := newImage.WorkingDir(ctx) if err != nil { @@ -140,13 +132,6 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat if err != nil { return err } - - // TODO This should be enabled when namespaces actually work - //case usernsMode.IsKeepID(): - // user = fmt.Sprintf("%d:%d", rootless.GetRootlessUID(), rootless.GetRootlessGID()) - if len(s.User) == 0 { - s.User = "0" - } } if err := finishThrottleDevices(s); err != nil { return err diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 1be77d315..01ddcf9c8 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -7,6 +7,7 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/specgen" "github.com/containers/storage" "github.com/pkg/errors" @@ -14,10 +15,7 @@ import ( ) // MakeContainer creates a container based on the SpecGenerator -func MakeContainer(rt *libpod.Runtime, s *specgen.SpecGenerator) (*libpod.Container, error) { - if err := s.Validate(); err != nil { - return nil, errors.Wrap(err, "invalid config provided") - } +func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGenerator) (*libpod.Container, error) { rtc, err := rt.GetConfig() if err != nil { return nil, err @@ -77,31 +75,48 @@ func MakeContainer(rt *libpod.Runtime, s *specgen.SpecGenerator) (*libpod.Contai s.CgroupNS = defaultNS } - options, err := createContainerOptions(rt, s, pod) + options := []libpod.CtrCreateOption{} + options = append(options, libpod.WithCreateCommand()) + + var newImage *image.Image + if s.Rootfs != "" { + options = append(options, libpod.WithRootFS(s.Rootfs)) + } else { + newImage, err = rt.ImageRuntime().NewFromLocal(s.Image) + if err != nil { + return nil, err + } + options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, s.RawImageName)) + } + if err := s.Validate(); err != nil { + return nil, errors.Wrap(err, "invalid config provided") + } + + finalMounts, finalVolumes, err := finalizeMounts(ctx, s, rt, rtc, newImage) if err != nil { return nil, err } - podmanPath, err := os.Executable() + opts, err := createContainerOptions(rt, s, pod, finalVolumes) if err != nil { return nil, err } - options = append(options, createExitCommandOption(s, rt.StorageConfig(), rtc, podmanPath)) - newImage, err := rt.ImageRuntime().NewFromLocal(s.Image) + options = append(options, opts...) + + podmanPath, err := os.Executable() if err != nil { return nil, err } + options = append(options, createExitCommandOption(s, rt.StorageConfig(), rtc, podmanPath)) - options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, s.RawImageName)) - - runtimeSpec, err := SpecGenToOCI(s, rt, newImage) + runtimeSpec, err := SpecGenToOCI(ctx, s, rt, rtc, newImage, finalMounts) if err != nil { return nil, err } - return rt.NewContainer(context.Background(), runtimeSpec, options...) + return rt.NewContainer(ctx, runtimeSpec, options...) } -func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod) ([]libpod.CtrCreateOption, error) { +func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod, volumes []*specgen.NamedVolume) ([]libpod.CtrCreateOption, error) { var options []libpod.CtrCreateOption var err error @@ -128,21 +143,21 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l for _, mount := range s.Mounts { destinations = append(destinations, mount.Destination) } - for _, volume := range s.Volumes { + for _, volume := range volumes { destinations = append(destinations, volume.Dest) } options = append(options, libpod.WithUserVolumes(destinations)) - if len(s.Volumes) != 0 { - var volumes []*libpod.ContainerNamedVolume - for _, v := range s.Volumes { - volumes = append(volumes, &libpod.ContainerNamedVolume{ + if len(volumes) != 0 { + var vols []*libpod.ContainerNamedVolume + for _, v := range volumes { + vols = append(vols, &libpod.ContainerNamedVolume{ Name: v.Name, Dest: v.Dest, Options: v.Options, }) } - options = append(options, libpod.WithNamedVolumes(volumes)) + options = append(options, libpod.WithNamedVolumes(vols)) } if len(s.Command) != 0 { diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index 2aaeb9513..a8b74b504 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -10,6 +10,7 @@ import ( "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/specgen" + "github.com/containers/libpod/pkg/util" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" @@ -26,7 +27,7 @@ func GetDefaultNamespaceMode(nsType string, cfg *config.Config, pod *libpod.Pod) nsType = strings.ToLower(nsType) // If the pod is not nil - check shared namespaces - if pod != nil { + if pod != nil && pod.HasInfraContainer() { podMode := false switch { case nsType == "pid" && pod.SharesPID(): @@ -175,6 +176,13 @@ func GenerateNamespaceOptions(s *specgen.SpecGenerator, rt *libpod.Runtime, pod // User switch s.UserNS.NSMode { + case specgen.KeepID: + if rootless.IsRootless() { + s.User = "" + } else { + // keep-id as root doesn't need a user namespace + s.UserNS.NSMode = specgen.Host + } case specgen.FromPod: if pod == nil || infraCtr == nil { return nil, errNoInfra @@ -378,6 +386,18 @@ func specConfigureNamespaces(s *specgen.SpecGenerator, g *generate.Generator, rt if err := g.RemoveLinuxNamespace(string(spec.UserNamespace)); err != nil { return err } + case specgen.KeepID: + var ( + err error + uid, gid int + ) + s.IDMappings, uid, gid, err = util.GetKeepIDMapping() + if err != nil { + return err + } + g.SetProcessUID(uint32(uid)) + g.SetProcessGID(uint32(gid)) + fallthrough case specgen.Private: if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { return err diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go index 8ca95016e..87262684e 100644 --- a/pkg/specgen/generate/oci.go +++ b/pkg/specgen/generate/oci.go @@ -1,8 +1,10 @@ package generate import ( + "context" "strings" + "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/rootless" @@ -10,6 +12,7 @@ import ( "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" + "github.com/pkg/errors" ) func addRlimits(s *specgen.SpecGenerator, g *generate.Generator) error { @@ -48,7 +51,51 @@ func addRlimits(s *specgen.SpecGenerator, g *generate.Generator) error { return nil } -func SpecGenToOCI(s *specgen.SpecGenerator, rt *libpod.Runtime, newImage *image.Image) (*spec.Spec, error) { +// Produce the final command for the container. +func makeCommand(ctx context.Context, s *specgen.SpecGenerator, img *image.Image, rtc *config.Config) ([]string, error) { + finalCommand := []string{} + + entrypoint := s.Entrypoint + if len(entrypoint) == 0 && img != nil { + newEntry, err := img.Entrypoint(ctx) + if err != nil { + return nil, err + } + entrypoint = newEntry + } + + finalCommand = append(finalCommand, entrypoint...) + + command := s.Command + if len(command) == 0 && img != nil { + newCmd, err := img.Cmd(ctx) + if err != nil { + return nil, err + } + command = newCmd + } + + finalCommand = append(finalCommand, command...) + + if len(finalCommand) == 0 { + return nil, errors.Errorf("no command or entrypoint provided, and no CMD or ENTRYPOINT from image") + } + + if s.Init { + initPath := s.InitPath + if initPath == "" && rtc != nil { + initPath = rtc.Engine.InitPath + } + if initPath == "" { + return nil, errors.Errorf("no path to init binary found but container requested an init") + } + finalCommand = append([]string{initPath, "--"}, finalCommand...) + } + + return finalCommand, nil +} + +func SpecGenToOCI(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, rtc *config.Config, newImage *image.Image, mounts []spec.Mount) (*spec.Spec, error) { var ( inUserNS bool ) @@ -173,7 +220,13 @@ func SpecGenToOCI(s *specgen.SpecGenerator, rt *libpod.Runtime, newImage *image. g.AddMount(cgroupMnt) } g.SetProcessCwd(s.WorkDir) - g.SetProcessArgs(s.Command) + + finalCmd, err := makeCommand(ctx, s, newImage, rtc) + if err != nil { + return nil, err + } + g.SetProcessArgs(finalCmd) + g.SetProcessTerminal(s.Terminal) for key, val := range s.Annotations { @@ -227,7 +280,7 @@ func SpecGenToOCI(s *specgen.SpecGenerator, rt *libpod.Runtime, newImage *image. } // BIND MOUNTS - configSpec.Mounts = SupercedeUserMounts(s.Mounts, configSpec.Mounts) + configSpec.Mounts = SupercedeUserMounts(mounts, configSpec.Mounts) // Process mounts to ensure correct options if err := InitFSMounts(configSpec.Mounts); err != nil { return nil, err diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index 292f9b155..babfba9bc 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -46,6 +46,13 @@ func createPodOptions(p *specgen.PodSpecGenerator) ([]libpod.PodCreateOption, er if len(p.HostAdd) > 0 { options = append(options, libpod.WithPodHosts(p.HostAdd)) } + if len(p.DNSServer) > 0 { + var dnsServers []string + for _, d := range p.DNSServer { + dnsServers = append(dnsServers, d.String()) + } + options = append(options, libpod.WithPodDNS(dnsServers)) + } if len(p.DNSOption) > 0 { options = append(options, libpod.WithPodDNSOption(p.DNSOption)) } diff --git a/pkg/specgen/generate/storage.go b/pkg/specgen/generate/storage.go index 7650e4e9a..241c9adeb 100644 --- a/pkg/specgen/generate/storage.go +++ b/pkg/specgen/generate/storage.go @@ -1,12 +1,20 @@ package generate import ( + "context" + "fmt" + "os" "path" "path/filepath" "strings" + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/pkg/specgen" "github.com/containers/libpod/pkg/util" spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -20,6 +28,301 @@ const ( TypeTmpfs = "tmpfs" ) +var ( + errDuplicateDest = errors.Errorf("duplicate mount destination") +) + +// Produce final mounts and named volumes for a container +func finalizeMounts(ctx context.Context, s *specgen.SpecGenerator, rt *libpod.Runtime, rtc *config.Config, img *image.Image) ([]spec.Mount, []*specgen.NamedVolume, error) { + // Get image volumes + baseMounts, baseVolumes, err := getImageVolumes(ctx, img, s) + if err != nil { + return nil, nil, err + } + + // Get volumes-from mounts + volFromMounts, volFromVolumes, err := getVolumesFrom(s.VolumesFrom, rt) + if err != nil { + return nil, nil, err + } + + // Supercede from --volumes-from. + for dest, mount := range volFromMounts { + baseMounts[dest] = mount + } + for dest, volume := range volFromVolumes { + baseVolumes[dest] = volume + } + + // Need to make map forms of specgen mounts/volumes. + unifiedMounts := map[string]spec.Mount{} + unifiedVolumes := map[string]*specgen.NamedVolume{} + for _, m := range s.Mounts { + if _, ok := unifiedMounts[m.Destination]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, "conflict in specified mounts - multiple mounts at %q", m.Destination) + } + unifiedMounts[m.Destination] = m + } + for _, v := range s.Volumes { + if _, ok := unifiedVolumes[v.Dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, "conflict in specified volumes - multiple volumes at %q", v.Dest) + } + unifiedVolumes[v.Dest] = v + } + + // If requested, add container init binary + if s.Init { + initPath := s.InitPath + if initPath == "" && rtc != nil { + initPath = rtc.Engine.InitPath + } + initMount, err := addContainerInitBinary(s, initPath) + if err != nil { + return nil, nil, err + } + if _, ok := unifiedMounts[initMount.Destination]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, "conflict with mount added by --init to %q", initMount.Destination) + } + unifiedMounts[initMount.Destination] = initMount + } + + // Before superseding, we need to find volume mounts which conflict with + // named volumes, and vice versa. + // We'll delete the conflicts here as we supersede. + for dest := range unifiedMounts { + if _, ok := baseVolumes[dest]; ok { + delete(baseVolumes, dest) + } + } + for dest := range unifiedVolumes { + if _, ok := baseMounts[dest]; ok { + delete(baseMounts, dest) + } + } + + // Supersede volumes-from/image volumes with unified volumes from above. + // This is an unconditional replacement. + for dest, mount := range unifiedMounts { + baseMounts[dest] = mount + } + for dest, volume := range unifiedVolumes { + baseVolumes[dest] = volume + } + + // TODO: Investigate moving readonlyTmpfs into here. Would be more + // correct. + + // Check for conflicts between named volumes and mounts + for dest := range baseMounts { + if _, ok := baseVolumes[dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest) + } + } + for dest := range baseVolumes { + if _, ok := baseMounts[dest]; ok { + return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest) + } + } + // Final step: maps to arrays + finalMounts := make([]spec.Mount, 0, len(baseMounts)) + for _, mount := range baseMounts { + if mount.Type == TypeBind { + absSrc, err := filepath.Abs(mount.Source) + if err != nil { + return nil, nil, errors.Wrapf(err, "error getting absolute path of %s", mount.Source) + } + mount.Source = absSrc + } + finalMounts = append(finalMounts, mount) + } + finalVolumes := make([]*specgen.NamedVolume, 0, len(baseVolumes)) + for _, volume := range baseVolumes { + finalVolumes = append(finalVolumes, volume) + } + + return finalMounts, finalVolumes, nil +} + +// Get image volumes from the given image +func getImageVolumes(ctx context.Context, img *image.Image, s *specgen.SpecGenerator) (map[string]spec.Mount, map[string]*specgen.NamedVolume, error) { + mounts := make(map[string]spec.Mount) + volumes := make(map[string]*specgen.NamedVolume) + + mode := strings.ToLower(s.ImageVolumeMode) + + // Image may be nil (rootfs in use), or image volume mode may be ignore. + if img == nil || mode == "ignore" { + return mounts, volumes, nil + } + + inspect, err := img.InspectNoSize(ctx) + if err != nil { + return nil, nil, errors.Wrapf(err, "error inspecting image to get image volumes") + } + for volume := range inspect.Config.Volumes { + logrus.Debugf("Image has volume at %q", volume) + cleanDest := filepath.Clean(volume) + switch mode { + case "", "anonymous": + // Anonymous volumes have no name. + newVol := new(specgen.NamedVolume) + newVol.Dest = cleanDest + newVol.Options = []string{"rprivate", "rw", "nodev", "exec"} + volumes[cleanDest] = newVol + logrus.Debugf("Adding anonymous image volume at %q", cleanDest) + case "tmpfs": + mount := spec.Mount{ + Destination: cleanDest, + Source: TypeTmpfs, + Type: TypeTmpfs, + Options: []string{"rprivate", "rw", "nodev", "exec"}, + } + mounts[cleanDest] = mount + logrus.Debugf("Adding tmpfs image volume at %q", cleanDest) + } + } + + return mounts, volumes, nil +} + +func getVolumesFrom(volumesFrom []string, runtime *libpod.Runtime) (map[string]spec.Mount, map[string]*specgen.NamedVolume, error) { + finalMounts := make(map[string]spec.Mount) + finalNamedVolumes := make(map[string]*specgen.NamedVolume) + + for _, volume := range volumesFrom { + var options []string + + splitVol := strings.SplitN(volume, ":", 2) + if len(splitVol) == 2 { + splitOpts := strings.Split(splitVol[1], ",") + for _, opt := range splitOpts { + setRORW := false + setZ := false + switch opt { + case "z": + if setZ { + return nil, nil, errors.Errorf("cannot set :z more than once in mount options") + } + setZ = true + case "ro", "rw": + if setRORW { + return nil, nil, errors.Errorf("cannot set ro or rw options more than once") + } + setRORW = true + default: + return nil, nil, errors.Errorf("invalid option %q specified - volumes from another container can only use z,ro,rw options", opt) + } + } + options = splitOpts + } + + ctr, err := runtime.LookupContainer(splitVol[0]) + if err != nil { + return nil, nil, errors.Wrapf(err, "error looking up container %q for volumes-from", splitVol[0]) + } + + logrus.Debugf("Adding volumes from container %s", ctr.ID()) + + // Look up the container's user volumes. This gets us the + // destinations of all mounts the user added to the container. + userVolumesArr := ctr.UserVolumes() + + // We're going to need to access them a lot, so convert to a map + // to reduce looping. + // We'll also use the map to indicate if we missed any volumes along the way. + userVolumes := make(map[string]bool) + for _, dest := range userVolumesArr { + userVolumes[dest] = false + } + + // Now we get the container's spec and loop through its volumes + // and append them in if we can find them. + spec := ctr.Spec() + if spec == nil { + return nil, nil, errors.Errorf("error retrieving container %s spec for volumes-from", ctr.ID()) + } + for _, mnt := range spec.Mounts { + if mnt.Type != TypeBind { + continue + } + if _, exists := userVolumes[mnt.Destination]; exists { + userVolumes[mnt.Destination] = true + + if len(options) != 0 { + mnt.Options = options + } + + if _, ok := finalMounts[mnt.Destination]; ok { + logrus.Debugf("Overriding mount to %s with new mount from container %s", mnt.Destination, ctr.ID()) + } + finalMounts[mnt.Destination] = mnt + } + } + + // We're done with the spec mounts. Add named volumes. + // Add these unconditionally - none of them are automatically + // part of the container, as some spec mounts are. + namedVolumes := ctr.NamedVolumes() + for _, namedVol := range namedVolumes { + if _, exists := userVolumes[namedVol.Dest]; exists { + userVolumes[namedVol.Dest] = true + } + + if len(options) != 0 { + namedVol.Options = options + } + + if _, ok := finalMounts[namedVol.Dest]; ok { + logrus.Debugf("Overriding named volume mount to %s with new named volume from container %s", namedVol.Dest, ctr.ID()) + } + + newVol := new(specgen.NamedVolume) + newVol.Dest = namedVol.Dest + newVol.Options = namedVol.Options + newVol.Name = namedVol.Name + + finalNamedVolumes[namedVol.Dest] = newVol + } + + // Check if we missed any volumes + for volDest, found := range userVolumes { + if !found { + logrus.Warnf("Unable to match volume %s from container %s for volumes-from", volDest, ctr.ID()) + } + } + } + + return finalMounts, finalNamedVolumes, nil +} + +// AddContainerInitBinary adds the init binary specified by path iff the +// container will run in a private PID namespace that is not shared with the +// host or another pre-existing container, where an init-like process is +// already running. +// This does *NOT* modify the container command - that must be done elsewhere. +func addContainerInitBinary(s *specgen.SpecGenerator, path string) (spec.Mount, error) { + mount := spec.Mount{ + Destination: "/dev/init", + Type: TypeBind, + Source: path, + Options: []string{TypeBind, "ro"}, + } + + if path == "" { + return mount, fmt.Errorf("please specify a path to the container-init binary") + } + if !s.PidNS.IsPrivate() { + return mount, fmt.Errorf("cannot add init binary as PID 1 (PID namespace isn't private)") + } + if s.Systemd == "true" || s.Systemd == "always" { + return mount, fmt.Errorf("cannot use container-init binary with systemd") + } + if _, err := os.Stat(path); os.IsNotExist(err) { + return mount, errors.Wrap(err, "container-init binary not found on the host") + } + return mount, nil +} + // Supersede existing mounts in the spec with new, user-specified mounts. // TODO: Should we unmount subtree mounts? E.g., if /tmp/ is mounted by // one mount, and we already have /tmp/a and /tmp/b, should we remove diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index fffbd6d9e..396563267 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -76,6 +76,17 @@ func (n *Namespace) IsPod() bool { func (n *Namespace) IsPrivate() bool { return n.NSMode == Private } + +// IsAuto indicates the namespace is auto +func (n *Namespace) IsAuto() bool { + return n.NSMode == Auto +} + +// IsKeepID indicates the namespace is KeepID +func (n *Namespace) IsKeepID() bool { + return n.NSMode == KeepID +} + func validateUserNS(n *Namespace) error { if n == nil { return nil @@ -148,6 +159,8 @@ func (n *Namespace) validate() error { func ParseNamespace(ns string) (Namespace, error) { toReturn := Namespace{} switch { + case ns == "pod": + toReturn.NSMode = FromPod case ns == "host": toReturn.NSMode = Host case ns == "private": @@ -186,12 +199,11 @@ func ParseUserNamespace(ns string) (Namespace, error) { if len(split) != 2 { return toReturn, errors.Errorf("invalid setting for auto: mode") } - toReturn.NSMode = KeepID + toReturn.NSMode = Auto toReturn.Value = split[1] return toReturn, nil case ns == "keep-id": toReturn.NSMode = KeepID - toReturn.NSMode = FromContainer return toReturn, nil } return ParseNamespace(ns) @@ -204,6 +216,10 @@ func ParseNetworkNamespace(ns string) (Namespace, []string, error) { toReturn := Namespace{} var cniNetworks []string switch { + case ns == "slirp4netns": + toReturn.NSMode = Slirp + case ns == "pod": + toReturn.NSMode = FromPod case ns == "bridge": toReturn.NSMode = Bridge case ns == "none": diff --git a/pkg/specgen/pod_validate.go b/pkg/specgen/pod_validate.go index f2f90e58d..98d59549e 100644 --- a/pkg/specgen/pod_validate.go +++ b/pkg/specgen/pod_validate.go @@ -62,7 +62,7 @@ func (p *PodSpecGenerator) Validate() error { return exclusivePodOptions("NoInfra", "NoManageResolvConf") } } - if p.NetNS.NSMode != Bridge { + if p.NetNS.NSMode != "" && p.NetNS.NSMode != Bridge && p.NetNS.NSMode != Default { if len(p.PortMappings) > 0 { return errors.New("PortMappings can only be used with Bridge mode networking") } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 37f2b3190..20c8f8800 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -154,14 +154,23 @@ type ContainerStorageConfig struct { // ImageVolumeMode indicates how image volumes will be created. // Supported modes are "ignore" (do not create), "tmpfs" (create as // tmpfs), and "anonymous" (create as anonymous volumes). - // The default is anonymous. + // The default if unset is anonymous. // Optional. ImageVolumeMode string `json:"image_volume_mode,omitempty"` - // VolumesFrom is a list of containers whose volumes will be added to - // this container. Supported mount options may be added after the - // container name with a : and include "ro" and "rw". - // Optional. + // VolumesFrom is a set of containers whose volumes will be added to + // this container. The name or ID of the container must be provided, and + // may optionally be followed by a : and then one or more + // comma-separated options. Valid options are 'ro', 'rw', and 'z'. + // Options will be used for all volumes sourced from the container. VolumesFrom []string `json:"volumes_from,omitempty"` + // Init specifies that an init binary will be mounted into the + // container, and will be used as PID1. + Init bool `json:"init,omitempty"` + // InitPath specifies the path to the init binary that will be added if + // Init is specified above. If not specified, the default set in the + // Libpod config will be used. Ignored if Init above is not set. + // Optional. + InitPath string `json:"init_path,omitempty"` // Mounts are mounts that will be added to the container. // These will supersede Image Volumes and VolumesFrom volumes where // there are conflicts. @@ -402,8 +411,13 @@ type NamedVolume struct { } // NewSpecGenerator returns a SpecGenerator struct given one of two mandatory inputs -func NewSpecGenerator(image string) *SpecGenerator { - csc := ContainerStorageConfig{Image: image} +func NewSpecGenerator(arg string, rootfs bool) *SpecGenerator { + csc := ContainerStorageConfig{} + if rootfs { + csc.Rootfs = arg + } else { + csc.Image = arg + } return &SpecGenerator{ ContainerStorageConfig: csc, } diff --git a/pkg/util/utils.go b/pkg/util/utils.go index 64331cf66..917f57742 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -330,6 +330,58 @@ func ParseSignal(rawSignal string) (syscall.Signal, error) { return sig, nil } +// GetKeepIDMapping returns the mappings and the user to use when keep-id is used +func GetKeepIDMapping() (*storage.IDMappingOptions, int, int, error) { + options := storage.IDMappingOptions{ + HostUIDMapping: true, + HostGIDMapping: true, + } + uid, gid := 0, 0 + if rootless.IsRootless() { + min := func(a, b int) int { + if a < b { + return a + } + return b + } + + uid = rootless.GetRootlessUID() + gid = rootless.GetRootlessGID() + + uids, gids, err := rootless.GetConfiguredMappings() + if err != nil { + return nil, -1, -1, errors.Wrapf(err, "cannot read mappings") + } + maxUID, maxGID := 0, 0 + for _, u := range uids { + maxUID += u.Size + } + for _, g := range gids { + maxGID += g.Size + } + + options.UIDMap, options.GIDMap = nil, nil + + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(uid, maxUID)}) + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: 0, Size: 1}) + if maxUID > uid { + options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid}) + } + + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(gid, maxGID)}) + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: 0, Size: 1}) + if maxGID > gid { + options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid}) + } + + options.HostUIDMapping = false + options.HostGIDMapping = false + + } + // Simply ignore the setting and do not setup an inner namespace for root as it is a no-op + return &options, uid, gid, nil +} + // ParseIDMapping takes idmappings and subuid and subgid maps and returns a storage mapping func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []string, subUIDMap, subGIDMap string) (*storage.IDMappingOptions, error) { options := storage.IDMappingOptions{ @@ -350,53 +402,8 @@ func ParseIDMapping(mode namespaces.UsernsMode, uidMapSlice, gidMapSlice []strin return &options, nil } if mode.IsKeepID() { - if len(uidMapSlice) > 0 || len(gidMapSlice) > 0 { - return nil, errors.New("cannot specify custom mappings with --userns=keep-id") - } - if len(subUIDMap) > 0 || len(subGIDMap) > 0 { - return nil, errors.New("cannot specify subuidmap or subgidmap with --userns=keep-id") - } - if rootless.IsRootless() { - min := func(a, b int) int { - if a < b { - return a - } - return b - } - - uid := rootless.GetRootlessUID() - gid := rootless.GetRootlessGID() - - uids, gids, err := rootless.GetConfiguredMappings() - if err != nil { - return nil, errors.Wrapf(err, "cannot read mappings") - } - maxUID, maxGID := 0, 0 - for _, u := range uids { - maxUID += u.Size - } - for _, g := range gids { - maxGID += g.Size - } - - options.UIDMap, options.GIDMap = nil, nil - - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(uid, maxUID)}) - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid, HostID: 0, Size: 1}) - if maxUID > uid { - options.UIDMap = append(options.UIDMap, idtools.IDMap{ContainerID: uid + 1, HostID: uid + 1, Size: maxUID - uid}) - } - - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: 0, HostID: 1, Size: min(gid, maxGID)}) - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid, HostID: 0, Size: 1}) - if maxGID > gid { - options.GIDMap = append(options.GIDMap, idtools.IDMap{ContainerID: gid + 1, HostID: gid + 1, Size: maxGID - gid}) - } - - options.HostUIDMapping = false - options.HostGIDMapping = false - } - // Simply ignore the setting and do not setup an inner namespace for root as it is a no-op + options.HostUIDMapping = false + options.HostGIDMapping = false return &options, nil } |