diff options
Diffstat (limited to 'pkg')
48 files changed, 1866 insertions, 430 deletions
diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 94d20a04a..1e175d664 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -86,6 +86,8 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "fill out specgen")) return } + // moby always create the working directory + sg.CreateWorkingDir = true ic := abi.ContainerEngine{Libpod: runtime} report, err := ic.ContainerCreate(r.Context(), sg) diff --git a/pkg/api/handlers/compat/containers_top.go b/pkg/api/handlers/compat/containers_top.go index b5debd37d..545320ad9 100644 --- a/pkg/api/handlers/compat/containers_top.go +++ b/pkg/api/handlers/compat/containers_top.go @@ -1,8 +1,11 @@ package compat import ( + "encoding/json" + "fmt" "net/http" "strings" + "time" "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/pkg/api/handlers" @@ -10,20 +13,24 @@ import ( api "github.com/containers/podman/v3/pkg/api/types" "github.com/gorilla/schema" "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) func TopContainer(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) - defaultValue := "-ef" + psArgs := "-ef" if utils.IsLibpodRequest(r) { - defaultValue = "" + psArgs = "" } query := struct { + Delay int `schema:"delay"` PsArgs string `schema:"ps_args"` + Stream bool `schema:"stream"` }{ - PsArgs: defaultValue, + Delay: 5, + PsArgs: psArgs, } if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, @@ -31,6 +38,12 @@ func TopContainer(w http.ResponseWriter, r *http.Request) { return } + if query.Delay < 1 { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + fmt.Errorf("\"delay\" parameter of value %d < 1", query.Delay)) + return + } + name := utils.GetName(r) c, err := runtime.LookupContainer(name) if err != nil { @@ -38,26 +51,56 @@ func TopContainer(w http.ResponseWriter, r *http.Request) { return } - output, err := c.Top([]string{query.PsArgs}) - if err != nil { - utils.InternalServerError(w, err) - return + // We are committed now - all errors logged but not reported to client, ship has sailed + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + if f, ok := w.(http.Flusher); ok { + f.Flush() } - var body = handlers.ContainerTopOKBody{} - if len(output) > 0 { - body.Titles = strings.Split(output[0], "\t") - for i := range body.Titles { - body.Titles[i] = strings.TrimSpace(body.Titles[i]) - } + encoder := json.NewEncoder(w) + +loop: // break out of for/select infinite` loop + for { + select { + case <-r.Context().Done(): + break loop + default: + output, err := c.Top([]string{query.PsArgs}) + if err != nil { + logrus.Infof("Error from %s %q : %v", r.Method, r.URL, err) + break loop + } + + if len(output) > 0 { + body := handlers.ContainerTopOKBody{} + body.Titles = strings.Split(output[0], "\t") + for i := range body.Titles { + body.Titles[i] = strings.TrimSpace(body.Titles[i]) + } + + for _, line := range output[1:] { + process := strings.Split(line, "\t") + for i := range process { + process[i] = strings.TrimSpace(process[i]) + } + body.Processes = append(body.Processes, process) + } + + if err := encoder.Encode(body); err != nil { + logrus.Infof("Error from %s %q : %v", r.Method, r.URL, err) + break loop + } + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + } - for _, line := range output[1:] { - process := strings.Split(line, "\t") - for i := range process { - process[i] = strings.TrimSpace(process[i]) + if query.Stream { + time.Sleep(time.Duration(query.Delay) * time.Second) + } else { + break loop } - body.Processes = append(body.Processes, process) } } - utils.WriteJSON(w, http.StatusOK, body) } diff --git a/pkg/api/handlers/libpod/pods.go b/pkg/api/handlers/libpod/pods.go index 1e64de0ee..2ba292579 100644 --- a/pkg/api/handlers/libpod/pods.go +++ b/pkg/api/handlers/libpod/pods.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod" @@ -363,10 +364,17 @@ func PodTop(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) + psArgs := "-ef" + if utils.IsLibpodRequest(r) { + psArgs = "" + } query := struct { + Delay int `schema:"delay"` PsArgs string `schema:"ps_args"` + Stream bool `schema:"stream"` }{ - PsArgs: "", + Delay: 5, + PsArgs: psArgs, } if err := decoder.Decode(&query, r.URL.Query()); err != nil { utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, @@ -374,31 +382,71 @@ func PodTop(w http.ResponseWriter, r *http.Request) { return } - name := utils.GetName(r) - pod, err := runtime.LookupPod(name) - if err != nil { - utils.PodNotFound(w, name, err) + if query.Delay < 1 { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + fmt.Errorf("\"delay\" parameter of value %d < 1", query.Delay)) return } - args := []string{} - if query.PsArgs != "" { - args = append(args, query.PsArgs) - } - output, err := pod.GetPodPidInformation(args) + name := utils.GetName(r) + pod, err := runtime.LookupPod(name) if err != nil { - utils.InternalServerError(w, err) + utils.PodNotFound(w, name, err) return } - var body = handlers.PodTopOKBody{} - if len(output) > 0 { - body.Titles = strings.Split(output[0], "\t") - for _, line := range output[1:] { - body.Processes = append(body.Processes, strings.Split(line, "\t")) + // We are committed now - all errors logged but not reported to client, ship has sailed + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + encoder := json.NewEncoder(w) + +loop: // break out of for/select infinite` loop + for { + select { + case <-r.Context().Done(): + break loop + default: + output, err := pod.GetPodPidInformation([]string{query.PsArgs}) + if err != nil { + logrus.Infof("Error from %s %q : %v", r.Method, r.URL, err) + break loop + } + + if len(output) > 0 { + var body = handlers.PodTopOKBody{} + body.Titles = strings.Split(output[0], "\t") + for i := range body.Titles { + body.Titles[i] = strings.TrimSpace(body.Titles[i]) + } + + for _, line := range output[1:] { + process := strings.Split(line, "\t") + for i := range process { + process[i] = strings.TrimSpace(process[i]) + } + body.Processes = append(body.Processes, process) + } + + if err := encoder.Encode(body); err != nil { + logrus.Infof("Error from %s %q : %v", r.Method, r.URL, err) + break loop + } + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + } + + if query.Stream { + time.Sleep(time.Duration(query.Delay) * time.Second) + } else { + break loop + } } } - utils.WriteJSON(w, http.StatusOK, body) } func PodKill(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index b90154e30..35120a1a5 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -42,9 +42,15 @@ type ContainersPruneReport struct { } type LibpodContainersPruneReport struct { - ID string `json:"id"` - SpaceReclaimed int64 `json:"space"` - PruneError string `json:"error"` + ID string `json:"Id"` + SpaceReclaimed int64 `json:"Size"` + // Error which occurred during prune operation (if any). + // This field is optional and may be omitted if no error occurred. + // + // Extensions: + // x-omitempty: true + // x-nullable: true + PruneError string `json:"Err,omitempty"` } type Info struct { diff --git a/pkg/api/handlers/utils/handler.go b/pkg/api/handlers/utils/handler.go index 29139a98e..96b7a957c 100644 --- a/pkg/api/handlers/utils/handler.go +++ b/pkg/api/handlers/utils/handler.go @@ -145,12 +145,12 @@ func MarshalErrorSliceJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) { } } -func MarshalErrorJSONIsEmpty(_ unsafe.Pointer) bool { - return false +func MarshalErrorJSONIsEmpty(ptr unsafe.Pointer) bool { + return *((*error)(ptr)) == nil } -func MarshalErrorSliceJSONIsEmpty(_ unsafe.Pointer) bool { - return false +func MarshalErrorSliceJSONIsEmpty(ptr unsafe.Pointer) bool { + return len(*((*[]error)(ptr))) <= 0 } // WriteJSON writes an interface value encoded as JSON to w diff --git a/pkg/api/handlers/utils/handler_test.go b/pkg/api/handlers/utils/handler_test.go index 18a1d2678..5957e7d74 100644 --- a/pkg/api/handlers/utils/handler_test.go +++ b/pkg/api/handlers/utils/handler_test.go @@ -138,3 +138,51 @@ func TestEqualVersion(t *testing.T) { rr.Body.String(), expected) } } + +func TestErrorEncoderFuncOmit(t *testing.T) { + data, err := json.Marshal(struct { + Err error `json:"err,omitempty"` + Errs []error `json:"errs,omitempty"` + }{}) + if err != nil { + t.Fatal(err) + } + + dataAsMap := make(map[string]interface{}) + err = json.Unmarshal(data, &dataAsMap) + if err != nil { + t.Fatal(err) + } + + _, ok := dataAsMap["err"] + if ok { + t.Errorf("the `err` field should have been omitted") + } + _, ok = dataAsMap["errs"] + if ok { + t.Errorf("the `errs` field should have been omitted") + } + + dataAsMap = make(map[string]interface{}) + data, err = json.Marshal(struct { + Err error `json:"err"` + Errs []error `json:"errs"` + }{}) + if err != nil { + t.Fatal(err) + } + + err = json.Unmarshal(data, &dataAsMap) + if err != nil { + t.Fatal(err) + } + + _, ok = dataAsMap["err"] + if !ok { + t.Errorf("the `err` field shouldn't have been omitted") + } + _, ok = dataAsMap["errs"] + if !ok { + t.Errorf("the `errs` field shouldn't have been omitted") + } +} diff --git a/pkg/api/server/handler_rid.go b/pkg/api/server/handler_rid.go index b624b99a6..7dcf436f7 100644 --- a/pkg/api/server/handler_rid.go +++ b/pkg/api/server/handler_rid.go @@ -2,6 +2,7 @@ package server import ( "fmt" + "io/ioutil" "net/http" "github.com/containers/podman/v3/pkg/api/types" @@ -15,7 +16,13 @@ import ( // and Apache style request logging func referenceIDHandler() mux.MiddlewareFunc { return func(h http.Handler) http.Handler { - return handlers.CombinedLoggingHandler(logrus.StandardLogger().Out, + // Only log Apache access_log-like entries at Info level or below + out := ioutil.Discard + if logrus.IsLevelEnabled(logrus.InfoLevel) { + out = logrus.StandardLogger().Out + } + + return handlers.CombinedLoggingHandler(out, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rid := r.Header.Get("X-Reference-Id") if rid == "" { diff --git a/pkg/api/server/register_containers.go b/pkg/api/server/register_containers.go index 8dcea1301..c4919182b 100644 --- a/pkg/api/server/register_containers.go +++ b/pkg/api/server/register_containers.go @@ -442,6 +442,7 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // - in: query // name: ps_args // type: string + // default: -ef // description: arguments to pass to ps such as aux. Requires ps(1) to be installed in the container if no ps(1) compatible AIX descriptors are used. // produces: // - application/json @@ -1142,19 +1143,23 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error { // name: name // type: string // required: true - // description: | - // Name of container to query for processes - // (As of version 1.xx) + // description: Name of container to query for processes (As of version 1.xx) // - in: query // name: stream // type: boolean - // default: true - // description: Stream the output + // description: when true, repeatedly stream the latest output (As of version 4.0) + // - in: query + // name: delay + // type: integer + // description: if streaming, delay in seconds between updates. Must be >1. (As of version 4.0) + // default: 5 // - in: query // name: ps_args // type: string // default: -ef - // description: arguments to pass to ps such as aux. Requires ps(1) to be installed in the container if no ps(1) compatible AIX descriptors are used. + // description: | + // arguments to pass to ps such as aux. + // Requires ps(1) to be installed in the container if no ps(1) compatible AIX descriptors are used. // produces: // - application/json // responses: diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 95a8b4939..38ceea271 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -103,7 +103,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - application/json // responses: // 200: - // $ref: "#/responses/DockerImageSummary" + // $ref: "#/responses/DockerImageSummaryResponse" // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/images/json"), s.APIHandler(compat.GetImages)).Methods(http.MethodGet) @@ -837,7 +837,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - application/json // responses: // 200: - // $ref: "#/responses/DockerImageSummary" + // $ref: "#/responses/LibpodImageSummaryResponse" // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/json"), s.APIHandler(libpod.GetImages)).Methods(http.MethodGet) @@ -967,7 +967,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - application/json // responses: // 200: - // $ref: "#/responses/DocsImageDeleteResponse" + // $ref: "#/responses/DocsLibpodImagesRemoveResponse" // 400: // $ref: "#/responses/BadParamError" // 404: @@ -1069,7 +1069,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - application/json // responses: // 200: - // $ref: "#/responses/DocsImageDeleteResponse" + // $ref: "#/responses/DocsLibpodPruneResponse" // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/prune"), s.APIHandler(libpod.PruneImages)).Methods(http.MethodPost) diff --git a/pkg/api/server/register_pods.go b/pkg/api/server/register_pods.go index de3669a0a..16a7bbb4c 100644 --- a/pkg/api/server/register_pods.go +++ b/pkg/api/server/register_pods.go @@ -296,18 +296,23 @@ func (s *APIServer) registerPodsHandlers(r *mux.Router) error { // name: name // type: string // required: true - // description: | - // Name of pod to query for processes + // description: Name of pod to query for processes // - in: query // name: stream // type: boolean - // default: true - // description: Stream the output + // description: when true, repeatedly stream the latest output (As of version 4.0) + // - in: query + // name: delay + // type: integer + // description: if streaming, delay in seconds between updates. Must be >1. (As of version 4.0) + // default: 5 // - in: query // name: ps_args // type: string // default: -ef - // description: arguments to pass to ps such as aux. Requires ps(1) to be installed in the container if no ps(1) compatible AIX descriptors are used. + // description: | + // arguments to pass to ps such as aux. + // Requires ps(1) to be installed in the container if no ps(1) compatible AIX descriptors are used. // responses: // 200: // $ref: "#/responses/DocsPodTopResponse" diff --git a/pkg/api/server/swagger.go b/pkg/api/server/swagger.go index 0fd66652e..3f8f6f9c5 100644 --- a/pkg/api/server/swagger.go +++ b/pkg/api/server/swagger.go @@ -6,6 +6,7 @@ import ( "github.com/containers/podman/v3/pkg/domain/entities" "github.com/containers/podman/v3/pkg/domain/entities/reports" "github.com/containers/podman/v3/pkg/errorhandling" + docker "github.com/docker/docker/api/types" ) // No such image @@ -134,9 +135,16 @@ type swagPodAlreadyStopped struct { } } -// Image summary -// swagger:response DockerImageSummary -type swagImageSummary struct { +// Image summary for compat API +// swagger:response DockerImageSummaryResponse +type swagDockerImageSummaryResponse struct { + // in:body + Body []docker.ImageSummary +} + +// Image summary for libpod API +// swagger:response LibpodImageSummaryResponse +type swagLibpodImageSummaryResponse struct { // in:body Body []entities.ImageSummary } diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index dc75dac5a..a2be44ab4 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -390,6 +390,11 @@ func (h *APIResponse) IsClientError() bool { return h.Response.StatusCode/100 == 4 } +// IsConflictError returns true if the response code is 409 +func (h *APIResponse) IsConflictError() bool { + return h.Response.StatusCode == 409 +} + // IsServerError returns true if the response code is 5xx func (h *APIResponse) IsServerError() bool { return h.Response.StatusCode/100 == 5 diff --git a/pkg/bindings/containers/attach.go b/pkg/bindings/containers/attach.go index c5f54c1af..47de89b33 100644 --- a/pkg/bindings/containers/attach.go +++ b/pkg/bindings/containers/attach.go @@ -214,7 +214,7 @@ func Attach(ctx context.Context, nameOrID string, stdin io.Reader, stdout io.Wri // Read multiplexed channels and write to appropriate stream fd, l, err := DemuxHeader(socket, buffer) if err != nil { - if errors.Is(err, io.EOF) { + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { return nil } return err @@ -531,7 +531,7 @@ func ExecStartAndAttach(ctx context.Context, sessionID string, options *ExecStar // Read multiplexed channels and write to appropriate stream fd, l, err := DemuxHeader(socket, buffer) if err != nil { - if errors.Is(err, io.EOF) { + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { return nil } return err diff --git a/pkg/bindings/containers/logs.go b/pkg/bindings/containers/logs.go index 67db94487..37ffdf0a5 100644 --- a/pkg/bindings/containers/logs.go +++ b/pkg/bindings/containers/logs.go @@ -39,7 +39,7 @@ func Logs(ctx context.Context, nameOrID string, options *LogOptions, stdoutChan, for { fd, l, err := DemuxHeader(response.Body, buffer) if err != nil { - if errors.Is(err, io.EOF) { + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { return nil } return err diff --git a/pkg/bindings/errors.go b/pkg/bindings/errors.go index 9c311d912..ec837b39c 100644 --- a/pkg/bindings/errors.go +++ b/pkg/bindings/errors.go @@ -12,17 +12,22 @@ var ( ErrNotImplemented = errors.New("function not implemented") ) -func handleError(data []byte) error { - e := errorhandling.ErrorModel{} - if err := json.Unmarshal(data, &e); err != nil { +func handleError(data []byte, unmarshalErrorInto interface{}) error { + if err := json.Unmarshal(data, unmarshalErrorInto); err != nil { return err } - return e + return unmarshalErrorInto.(error) } // Process drains the response body, and processes the HTTP status code // Note: Closing the response.Body is left to the caller func (h APIResponse) Process(unmarshalInto interface{}) error { + return h.ProcessWithError(unmarshalInto, &errorhandling.ErrorModel{}) +} + +// Process drains the response body, and processes the HTTP status code +// Note: Closing the response.Body is left to the caller +func (h APIResponse) ProcessWithError(unmarshalInto interface{}, unmarshalErrorInto interface{}) error { data, err := ioutil.ReadAll(h.Response.Body) if err != nil { return errors.Wrap(err, "unable to process API response") @@ -33,14 +38,22 @@ func (h APIResponse) Process(unmarshalInto interface{}) error { } return nil } + + if h.IsConflictError() { + return handleError(data, unmarshalErrorInto) + } + // TODO should we add a debug here with the response code? - return handleError(data) + return handleError(data, &errorhandling.ErrorModel{}) } func CheckResponseCode(inError error) (int, error) { - e, ok := inError.(errorhandling.ErrorModel) - if !ok { + switch e := inError.(type) { + case *errorhandling.ErrorModel: + return e.Code(), nil + case *errorhandling.PodConflictErrorModel: + return e.Code(), nil + default: return -1, errors.New("error is not type ErrorModel") } - return e.Code(), nil } diff --git a/pkg/bindings/pods/pods.go b/pkg/bindings/pods/pods.go index a1a431a3b..3b5832373 100644 --- a/pkg/bindings/pods/pods.go +++ b/pkg/bindings/pods/pods.go @@ -9,6 +9,7 @@ import ( "github.com/containers/podman/v3/pkg/api/handlers" "github.com/containers/podman/v3/pkg/bindings" "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/containers/podman/v3/pkg/errorhandling" jsoniter "github.com/json-iterator/go" ) @@ -97,7 +98,7 @@ func Kill(ctx context.Context, nameOrID string, options *KillOptions) (*entities } defer response.Body.Close() - return &report, response.Process(&report) + return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{}) } // Pause pauses all running containers in a given pod. @@ -117,7 +118,7 @@ func Pause(ctx context.Context, nameOrID string, options *PauseOptions) (*entiti } defer response.Body.Close() - return &report, response.Process(&report) + return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{}) } // Prune by default removes all non-running pods in local storage. @@ -184,7 +185,7 @@ func Restart(ctx context.Context, nameOrID string, options *RestartOptions) (*en } defer response.Body.Close() - return &report, response.Process(&report) + return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{}) } // Remove deletes a Pod from from local storage. The optional force parameter denotes @@ -232,7 +233,8 @@ func Start(ctx context.Context, nameOrID string, options *StartOptions) (*entiti report.Id = nameOrID return &report, nil } - return &report, response.Process(&report) + + return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{}) } // Stop stops all containers in a Pod. The optional timeout parameter can be @@ -260,7 +262,7 @@ func Stop(ctx context.Context, nameOrID string, options *StopOptions) (*entities report.Id = nameOrID return &report, nil } - return &report, response.Process(&report) + return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{}) } // Top gathers statistics about the running processes in a pod. The nameOrID can be a pod name @@ -316,7 +318,7 @@ func Unpause(ctx context.Context, nameOrID string, options *UnpauseOptions) (*en } defer response.Body.Close() - return &report, response.Process(&report) + return &report, response.ProcessWithError(&report, &errorhandling.PodConflictErrorModel{}) } // Stats display resource-usage statistics of one or more pods. diff --git a/pkg/bindings/test/common_test.go b/pkg/bindings/test/common_test.go index 91ebe21fc..d996595bf 100644 --- a/pkg/bindings/test/common_test.go +++ b/pkg/bindings/test/common_test.go @@ -225,12 +225,23 @@ func (b *bindingTest) RunTopContainer(containerName *string, podName *string) (s // This method creates a pod with the given pod name. // Podname is an optional parameter func (b *bindingTest) Podcreate(name *string) { + b.PodcreateAndExpose(name, nil) +} + +// This method creates a pod with the given pod name and publish port. +// Podname is an optional parameter +// port is an optional parameter +func (b *bindingTest) PodcreateAndExpose(name *string, port *string) { + command := []string{"pod", "create"} if name != nil { podname := *name - b.runPodman([]string{"pod", "create", "--name", podname}).Wait(45) - } else { - b.runPodman([]string{"pod", "create"}).Wait(45) + command = append(command, "--name", podname) + } + if port != nil { + podport := *port + command = append(command, "--publish", podport) } + b.runPodman(command).Wait(45) } // StringInSlice returns a boolean based on whether a given diff --git a/pkg/bindings/test/containers_test.go b/pkg/bindings/test/containers_test.go index b9ed67255..0f535bc31 100644 --- a/pkg/bindings/test/containers_test.go +++ b/pkg/bindings/test/containers_test.go @@ -259,6 +259,7 @@ var _ = Describe("Podman containers ", func() { _, err = bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) go func() { + defer GinkgoRecover() exitCode, err = containers.Wait(bt.conn, name, nil) errChan <- err close(errChan) @@ -281,6 +282,7 @@ var _ = Describe("Podman containers ", func() { _, err := bt.RunTopContainer(&name, nil) Expect(err).To(BeNil()) go func() { + defer GinkgoRecover() exitCode, err = containers.Wait(bt.conn, name, new(containers.WaitOptions).WithCondition([]define.ContainerStatus{pause})) errChan <- err close(errChan) @@ -366,7 +368,10 @@ var _ = Describe("Podman containers ", func() { opts := new(containers.LogOptions).WithStdout(true).WithFollow(true) go func() { - containers.Logs(bt.conn, r.ID, opts, stdoutChan, nil) + defer GinkgoRecover() + err := containers.Logs(bt.conn, r.ID, opts, stdoutChan, nil) + close(stdoutChan) + Expect(err).ShouldNot(HaveOccurred()) }() o := <-stdoutChan o = strings.TrimSpace(o) diff --git a/pkg/bindings/test/pods_test.go b/pkg/bindings/test/pods_test.go index 5331cf439..879d4d00d 100644 --- a/pkg/bindings/test/pods_test.go +++ b/pkg/bindings/test/pods_test.go @@ -1,6 +1,7 @@ package test_bindings import ( + "fmt" "net/http" "strings" "time" @@ -9,7 +10,9 @@ import ( "github.com/containers/podman/v3/pkg/bindings" "github.com/containers/podman/v3/pkg/bindings/pods" "github.com/containers/podman/v3/pkg/domain/entities" + "github.com/containers/podman/v3/pkg/errorhandling" "github.com/containers/podman/v3/pkg/specgen" + "github.com/containers/podman/v3/utils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" @@ -208,6 +211,29 @@ var _ = Describe("Podman pods", func() { } }) + It("start pod with port conflict", func() { + randomport, err := utils.GetRandomPort() + Expect(err).To(BeNil()) + + portPublish := fmt.Sprintf("%d:%d", randomport, randomport) + var podwithport string = "newpodwithport" + bt.PodcreateAndExpose(&podwithport, &portPublish) + + // Start pod and expose port 12345 + _, err = pods.Start(bt.conn, podwithport, nil) + Expect(err).To(BeNil()) + + // Start another pod and expose same port 12345 + var podwithport2 string = "newpodwithport2" + bt.PodcreateAndExpose(&podwithport2, &portPublish) + + _, err = pods.Start(bt.conn, podwithport2, nil) + Expect(err).ToNot(BeNil()) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusConflict)) + Expect(err).To(BeAssignableToTypeOf(&errorhandling.PodConflictErrorModel{})) + }) + It("start stop restart pod", func() { // Start an invalid pod _, err = pods.Start(bt.conn, "dummyName", nil) diff --git a/pkg/checkpoint/checkpoint_restore.go b/pkg/checkpoint/checkpoint_restore.go index f53e31f9b..da82c9745 100644 --- a/pkg/checkpoint/checkpoint_restore.go +++ b/pkg/checkpoint/checkpoint_restore.go @@ -6,6 +6,7 @@ import ( "os" metadata "github.com/checkpoint-restore/checkpointctl/lib" + "github.com/checkpoint-restore/go-criu/v5/stats" "github.com/containers/common/libimage" "github.com/containers/common/pkg/config" "github.com/containers/podman/v3/libpod" @@ -39,6 +40,7 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt "volumes", "ctr.log", "artifacts", + stats.StatsDump, metadata.RootFsDiffTar, metadata.DeletedFilesFile, metadata.NetworkStatusFile, @@ -193,7 +195,7 @@ func CRImportCheckpoint(ctx context.Context, runtime *libpod.Runtime, restoreOpt } if len(restoreOptions.PublishPorts) > 0 { - ports, _, _, err := generate.ParsePortMapping(restoreOptions.PublishPorts) + ports, err := generate.ParsePortMapping(restoreOptions.PublishPorts, nil) if err != nil { return nil, err } diff --git a/pkg/domain/entities/container_ps.go b/pkg/domain/entities/container_ps.go index 58f231a2f..d018d373f 100644 --- a/pkg/domain/entities/container_ps.go +++ b/pkg/domain/entities/container_ps.go @@ -54,7 +54,7 @@ type ListContainer struct { // boolean to be set PodName string // Port mappings - Ports []types.OCICNIPortMapping + Ports []types.PortMapping // Size of the container rootfs. Requires the size boolean to be true Size *define.ContainerSize // Time when container started diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index deae85fe1..869c616ea 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -422,7 +422,7 @@ type ContainerPortOptions struct { // the CLI to output ports type ContainerPortReport struct { Id string //nolint - Ports []nettypes.OCICNIPortMapping + Ports []nettypes.PortMapping } // ContainerCpOptions describes input options for cp. diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 7ac07a8b3..7583ce442 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -50,6 +50,7 @@ func (i *Image) Id() string { // nolint return i.ID } +// swagger:model LibpodImageSummary type ImageSummary struct { ID string `json:"Id"` ParentId string // nolint diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 3d8579acf..1df18be58 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -7,6 +7,7 @@ import ( commonFlag "github.com/containers/common/pkg/flag" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" "github.com/containers/podman/v3/pkg/specgen" "github.com/containers/podman/v3/pkg/util" "github.com/opencontainers/runtime-spec/specs-go" @@ -318,7 +319,8 @@ func ToPodSpecGen(s specgen.PodSpecGenerator, p *PodCreateOptions) (*specgen.Pod if p.Net != nil { s.NetNS = p.Net.Network s.StaticIP = p.Net.StaticIP - s.StaticMAC = p.Net.StaticMAC + // type cast to types.HardwareAddr + s.StaticMAC = (*types.HardwareAddr)(p.Net.StaticMAC) s.PortMappings = p.Net.PublishPorts s.CNINetworks = p.Net.CNINetworks s.NetworkOptions = p.Net.NetworkOptions diff --git a/pkg/domain/entities/reports/prune.go b/pkg/domain/entities/reports/prune.go index 5494ac3ae..219e35b67 100644 --- a/pkg/domain/entities/reports/prune.go +++ b/pkg/domain/entities/reports/prune.go @@ -1,9 +1,9 @@ package reports type PruneReport struct { - Id string //nolint - Err error - Size uint64 + Id string `json:"Id"` //nolint + Err error `json:"Err,omitempty"` + Size uint64 `json:"Size"` } func PruneReportsIds(r []*PruneReport) []string { diff --git a/pkg/domain/entities/system.go b/pkg/domain/entities/system.go index fe041dec8..49f0c2323 100644 --- a/pkg/domain/entities/system.go +++ b/pkg/domain/entities/system.go @@ -100,7 +100,7 @@ type SystemVersionReport struct { // SystemUnshareOptions describes the options for the unshare command type SystemUnshareOptions struct { - RootlessCNI bool + RootlessNetNS bool } type ComponentVersion struct { diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index 081a2464b..a4d6bcf86 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -124,8 +124,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, if err != nil { return nil, err } - - b, err := generateKubeYAML(po) + b, err := generateKubeYAML(libpod.ConvertV1PodToYAMLPod(po)) if err != nil { return nil, err } diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index e9528acf1..5c0227986 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -61,7 +61,7 @@ func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOption pruneOptions.Filters = append(pruneOptions.Filters, "containers=false") } - var pruneReports []*reports.PruneReport + pruneReports := make([]*reports.PruneReport, 0) // Now prune all images until we converge. numPreviouslyRemovedImages := 1 diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 4d21751d1..3fdb3f286 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -269,17 +269,11 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } if podOpt.Infra { - containerConfig := util.DefaultContainerConfig() - - pulledImages, err := pullImage(ic, writer, containerConfig.Engine.InfraImage, options, config.PullPolicyNewer) - if err != nil { - return nil, err - } + infraImage := util.DefaultContainerConfig().Engine.InfraImage infraOptions := entities.ContainerCreateOptions{ImageVolume: "bind"} - - podSpec.PodSpecGen.InfraImage = pulledImages[0].Names()[0] + podSpec.PodSpecGen.InfraImage = infraImage podSpec.PodSpecGen.NoInfra = false - podSpec.PodSpecGen.InfraContainerSpec = specgen.NewSpecGenerator(pulledImages[0].Names()[0], false) + podSpec.PodSpecGen.InfraContainerSpec = specgen.NewSpecGenerator(infraImage, false) podSpec.PodSpecGen.InfraContainerSpec.NetworkOptions = p.NetworkOptions err = specgenutil.FillOutSpecGen(podSpec.PodSpecGen.InfraContainerSpec, &infraOptions, []string{}) @@ -758,21 +752,3 @@ func (ic *ContainerEngine) PlayKubeDown(ctx context.Context, path string, _ enti } return reports, nil } - -// pullImage is a helper function to set up the proper pull options and pull the image for certain containers -func pullImage(ic *ContainerEngine, writer io.Writer, imagePull string, options entities.PlayKubeOptions, pullPolicy config.PullPolicy) ([]*libimage.Image, error) { - // This ensures the image is the image store - pullOptions := &libimage.PullOptions{} - pullOptions.AuthFilePath = options.Authfile - pullOptions.CertDirPath = options.CertDir - pullOptions.SignaturePolicyPath = options.SignaturePolicy - pullOptions.Writer = writer - pullOptions.Username = options.Username - pullOptions.Password = options.Password - pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify - pulledImages, err := ic.Libpod.LibimageRuntime().Pull(context.Background(), imagePull, pullPolicy, pullOptions) - if err != nil { - return nil, err - } - return pulledImages, nil -} diff --git a/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go index e326f26a8..7da7754f2 100644 --- a/pkg/domain/infra/abi/system.go +++ b/pkg/domain/infra/abi/system.go @@ -360,15 +360,15 @@ func (ic *ContainerEngine) Unshare(ctx context.Context, args []string, options e return cmd.Run() } - if options.RootlessCNI { - rootlesscni, err := ic.Libpod.GetRootlessCNINetNs(true) + if options.RootlessNetNS { + rootlessNetNS, err := ic.Libpod.GetRootlessNetNs(true) if err != nil { return err } // make sure to unlock, unshare can run for a long time - rootlesscni.Lock.Unlock() - defer rootlesscni.Cleanup(ic.Libpod) - return rootlesscni.Do(unshare) + rootlessNetNS.Lock.Unlock() + defer rootlessNetNS.Cleanup(ic.Libpod) + return rootlessNetNS.Do(unshare) } return unshare() } diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index 7ec6135ee..cfb674b6d 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -200,6 +200,9 @@ func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpo if fs.Changed("network-cmd-path") { options = append(options, libpod.WithNetworkCmdPath(cfg.Engine.NetworkCmdPath)) } + if fs.Changed("network-backend") { + options = append(options, libpod.WithNetworkBackend(cfg.Network.NetworkBackend)) + } if fs.Changed("events-backend") { options = append(options, libpod.WithEventsLogger(cfg.Engine.EventsLogger)) diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 3f78ba7bc..5b5a1912c 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -228,7 +228,7 @@ func (ic *ContainerEngine) ContainerInspect(ctx context.Context, namesOrIds []st for _, name := range namesOrIds { inspect, err := containers.Inspect(ic.ClientCtx, name, options) if err != nil { - errModel, ok := err.(errorhandling.ErrorModel) + errModel, ok := err.(*errorhandling.ErrorModel) if !ok { return nil, nil, err } diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index ed0c35de4..fde57972f 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -193,7 +193,7 @@ func (ir *ImageEngine) Inspect(ctx context.Context, namesOrIDs []string, opts en for _, i := range namesOrIDs { r, err := images.GetImage(ir.ClientCtx, i, options) if err != nil { - errModel, ok := err.(errorhandling.ErrorModel) + errModel, ok := err.(*errorhandling.ErrorModel) if !ok { return nil, nil, err } diff --git a/pkg/domain/infra/tunnel/network.go b/pkg/domain/infra/tunnel/network.go index 79fba1943..069982d30 100644 --- a/pkg/domain/infra/tunnel/network.go +++ b/pkg/domain/infra/tunnel/network.go @@ -25,7 +25,7 @@ func (ic *ContainerEngine) NetworkInspect(ctx context.Context, namesOrIds []stri for _, name := range namesOrIds { report, err := network.Inspect(ic.ClientCtx, name, options) if err != nil { - errModel, ok := err.(errorhandling.ErrorModel) + errModel, ok := err.(*errorhandling.ErrorModel) if !ok { return nil, nil, err } diff --git a/pkg/domain/infra/tunnel/secrets.go b/pkg/domain/infra/tunnel/secrets.go index 6337c7fbe..e5fa200bd 100644 --- a/pkg/domain/infra/tunnel/secrets.go +++ b/pkg/domain/infra/tunnel/secrets.go @@ -28,7 +28,7 @@ func (ic *ContainerEngine) SecretInspect(ctx context.Context, nameOrIDs []string for _, name := range nameOrIDs { inspected, err := secrets.Inspect(ic.ClientCtx, name, nil) if err != nil { - errModel, ok := err.(errorhandling.ErrorModel) + errModel, ok := err.(*errorhandling.ErrorModel) if !ok { return nil, nil, err } @@ -67,7 +67,7 @@ func (ic *ContainerEngine) SecretRm(ctx context.Context, nameOrIDs []string, opt for _, name := range nameOrIDs { secret, err := secrets.Inspect(ic.ClientCtx, name, nil) if err != nil { - errModel, ok := err.(errorhandling.ErrorModel) + errModel, ok := err.(*errorhandling.ErrorModel) if !ok { return nil, err } diff --git a/pkg/domain/infra/tunnel/volumes.go b/pkg/domain/infra/tunnel/volumes.go index cfd1574c3..ccb363935 100644 --- a/pkg/domain/infra/tunnel/volumes.go +++ b/pkg/domain/infra/tunnel/volumes.go @@ -59,7 +59,7 @@ func (ic *ContainerEngine) VolumeInspect(ctx context.Context, namesOrIds []strin for _, id := range namesOrIds { data, err := volumes.Inspect(ic.ClientCtx, id, nil) if err != nil { - errModel, ok := err.(errorhandling.ErrorModel) + errModel, ok := err.(*errorhandling.ErrorModel) if !ok { return nil, nil, err } diff --git a/pkg/errorhandling/errorhandling.go b/pkg/errorhandling/errorhandling.go index 44a0c3efd..04110b62a 100644 --- a/pkg/errorhandling/errorhandling.go +++ b/pkg/errorhandling/errorhandling.go @@ -83,6 +83,12 @@ func Contains(err error, sub error) bool { return strings.Contains(err.Error(), sub.Error()) } +// PodConflictErrorModel is used in remote connections with podman +type PodConflictErrorModel struct { + Errs []string + Id string //nolint +} + // ErrorModel is used in remote connections with podman type ErrorModel struct { // API root cause formatted for automated parsing @@ -106,3 +112,11 @@ func (e ErrorModel) Cause() error { func (e ErrorModel) Code() int { return e.ResponseCode } + +func (e PodConflictErrorModel) Error() string { + return strings.Join(e.Errs, ",") +} + +func (e PodConflictErrorModel) Code() int { + return 409 +} diff --git a/pkg/machine/qemu/machine.go b/pkg/machine/qemu/machine.go index 4d8242e39..a7174aac3 100644 --- a/pkg/machine/qemu/machine.go +++ b/pkg/machine/qemu/machine.go @@ -398,7 +398,6 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { return err } - fmt.Printf("Successfully stopped machine: %s", name) return nil } diff --git a/pkg/rootless/rootless_linux.go b/pkg/rootless/rootless_linux.go index 7f9228666..3e81d5c14 100644 --- a/pkg/rootless/rootless_linux.go +++ b/pkg/rootless/rootless_linux.go @@ -325,7 +325,7 @@ func becomeRootInUserNS(pausePid, fileToRead string, fileOutput *os.File) (_ boo uidsMapped = err == nil } if !uidsMapped { - logrus.Warnf("Using rootless single mapping into the namespace. This might break some images. Check /etc/subuid and /etc/subgid for adding sub*ids") + logrus.Warnf("Using rootless single mapping into the namespace. This might break some images. Check /etc/subuid and /etc/subgid for adding sub*ids if not using a network user") setgroups := fmt.Sprintf("/proc/%d/setgroups", pid) err = ioutil.WriteFile(setgroups, []byte("deny\n"), 0666) if err != nil { diff --git a/pkg/rootlessport/rootlessport_linux.go b/pkg/rootlessport/rootlessport_linux.go index 7b9e5bbfa..7e6075789 100644 --- a/pkg/rootlessport/rootlessport_linux.go +++ b/pkg/rootlessport/rootlessport_linux.go @@ -23,7 +23,7 @@ const ( // Config needs to be provided to the process via stdin as a JSON string. // stdin needs to be closed after the message has been written. type Config struct { - Mappings []types.OCICNIPortMapping + Mappings []types.PortMapping NetNSPath string ExitFD int ReadyFD int diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 9f398a0ed..f3dc28b01 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -378,6 +378,9 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. if s.WorkDir == "" { s.WorkDir = "/" } + if s.CreateWorkingDir { + options = append(options, libpod.WithCreateWorkingDir()) + } if s.StopSignal != nil { options = append(options, libpod.WithStopSignal(*s.StopSignal)) } diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index 88ebc7ae3..501bce05d 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -204,11 +204,11 @@ func createPodOptions(p *specgen.PodSpecGenerator, rt *libpod.Runtime, infraSpec // replacing necessary values with those specified in pod creation func MapSpec(p *specgen.PodSpecGenerator) (*specgen.SpecGenerator, error) { if len(p.PortMappings) > 0 { - ports, _, _, err := ParsePortMapping(p.PortMappings) + ports, err := ParsePortMapping(p.PortMappings, nil) if err != nil { return nil, err } - p.InfraContainerSpec.PortMappings = libpod.WithInfraContainerPorts(ports, p.InfraContainerSpec) + p.InfraContainerSpec.PortMappings = ports } switch p.NetNS.NSMode { case specgen.Default, "": diff --git a/pkg/specgen/generate/ports.go b/pkg/specgen/generate/ports.go index 992b4a8e9..53a5e5697 100644 --- a/pkg/specgen/generate/ports.go +++ b/pkg/specgen/generate/ports.go @@ -2,7 +2,9 @@ package generate import ( "context" + "fmt" "net" + "sort" "strconv" "strings" @@ -11,6 +13,7 @@ import ( "github.com/containers/podman/v3/utils" "github.com/containers/podman/v3/pkg/specgen" + "github.com/containers/podman/v3/pkg/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -21,252 +24,323 @@ const ( protoSCTP = "sctp" ) -// Parse port maps to OCICNI port mappings. -// Returns a set of OCICNI port mappings, and maps of utilized container and +// joinTwoPortsToRangePortIfPossible will expect two ports the previous port one must have a lower or equal hostPort than the current port. +func joinTwoPortsToRangePortIfPossible(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool, + previousPort *types.PortMapping, port types.PortMapping) (*types.PortMapping, error) { + // no previous port just return the current one + if previousPort == nil { + return &port, nil + } + if previousPort.HostPort+previousPort.Range >= port.HostPort { + // check if the port range matches the host and container ports + portDiff := port.HostPort - previousPort.HostPort + if portDiff == port.ContainerPort-previousPort.ContainerPort { + // calc the new range use the old range and add the difference between the ports + newRange := port.Range + portDiff + // if the newRange is greater than the old range use it + // this is important otherwise we would could lower the range + if newRange > previousPort.Range { + previousPort.Range = newRange + } + return previousPort, nil + } + // if both host port ranges overlap and the container port range did not match + // we have to error because we cannot assign the same host port to more than one container port + if previousPort.HostPort+previousPort.Range-1 > port.HostPort { + return nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", port.HostPort, port.Protocol) + } + } + // we could not join the ports so we append the old one to the list + // and return the current port as previous port + addPortToUsedPorts(ports, allHostPorts, allContainerPorts, currentHostPorts, previousPort) + return &port, nil +} + +// joinTwoContainerPortsToRangePortIfPossible will expect two ports with both no host port set, +// the previous port one must have a lower or equal containerPort than the current port. +func joinTwoContainerPortsToRangePortIfPossible(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool, + previousPort *types.PortMapping, port types.PortMapping) (*types.PortMapping, error) { + // no previous port just return the current one + if previousPort == nil { + return &port, nil + } + if previousPort.ContainerPort+previousPort.Range > port.ContainerPort { + // calc the new range use the old range and add the difference between the ports + newRange := port.ContainerPort - previousPort.ContainerPort + port.Range + // if the newRange is greater than the old range use it + // this is important otherwise we would could lower the range + if newRange > previousPort.Range { + previousPort.Range = newRange + } + return previousPort, nil + } + // we could not join the ports so we append the old one to the list + // and return the current port as previous port + newPort, err := getRandomHostPort(currentHostPorts, *previousPort) + if err != nil { + return nil, err + } + addPortToUsedPorts(ports, allHostPorts, allContainerPorts, currentHostPorts, &newPort) + return &port, nil +} + +func addPortToUsedPorts(ports *[]types.PortMapping, allHostPorts, allContainerPorts, currentHostPorts *[65536]bool, port *types.PortMapping) { + for i := uint16(0); i < port.Range; i++ { + h := port.HostPort + i + allHostPorts[h] = true + currentHostPorts[h] = true + c := port.ContainerPort + i + allContainerPorts[c] = true + } + *ports = append(*ports, *port) +} + +// getRandomHostPort get a random host port mapping for the given port +// the caller has to supply a array with he already used ports +func getRandomHostPort(hostPorts *[65536]bool, port types.PortMapping) (types.PortMapping, error) { +outer: + for i := 0; i < 15; i++ { + ranPort, err := utils.GetRandomPort() + if err != nil { + return port, err + } + + // if port range is exceeds max port we cannot use it + if ranPort+int(port.Range) > 65535 { + continue + } + + // check if there is a port in the range which is used + for j := 0; j < int(port.Range); j++ { + // port already used + if hostPorts[ranPort+j] { + continue outer + } + } + + port.HostPort = uint16(ranPort) + return port, nil + } + + // add range to error message if needed + rangePort := "" + if port.Range > 1 { + rangePort = fmt.Sprintf("with range %d ", port.Range) + } + + return port, errors.Errorf("failed to find an open port to expose container port %d %son the host", port.ContainerPort, rangePort) +} + +// Parse port maps to port mappings. +// Returns a set of port mappings, and maps of utilized container and // host ports. -func ParsePortMapping(portMappings []types.PortMapping) ([]types.OCICNIPortMapping, map[string]map[string]map[uint16]uint16, map[string]map[string]map[uint16]uint16, error) { - // First, we need to validate the ports passed in the specgen, and then - // convert them into CNI port mappings. - type tempMapping struct { - mapping types.OCICNIPortMapping - startOfRange bool - isInRange bool +func ParsePortMapping(portMappings []types.PortMapping, exposePorts map[uint16][]string) ([]types.PortMapping, error) { + if len(portMappings) == 0 && len(exposePorts) == 0 { + return nil, nil } - tempMappings := []tempMapping{} - - // To validate, we need two maps: one for host ports, one for container - // ports. - // Each is a map of protocol to map of IP address to map of port to - // port (for hostPortValidate, it's host port to container port; - // for containerPortValidate, container port to host port. - // These will ensure no collisions. - hostPortValidate := make(map[string]map[string]map[uint16]uint16) - containerPortValidate := make(map[string]map[string]map[uint16]uint16) - - // Initialize the first level of maps (we can't really guess keys for - // the rest). - for _, proto := range []string{protoTCP, protoUDP, protoSCTP} { - hostPortValidate[proto] = make(map[string]map[uint16]uint16) - containerPortValidate[proto] = make(map[string]map[uint16]uint16) + + // tempMapping stores the ports without ip and protocol + type tempMapping struct { + hostPort uint16 + containerPort uint16 + rangePort uint16 } - postAssignHostPort := false + // portMap is a temporary structure to sort all ports + // the map is hostIp -> protocol -> array of mappings + portMap := make(map[string]map[string][]tempMapping) + + // allUsedContainerPorts stores all used ports for each protocol + // the key is the protocol and the array is 65536 elements long for each port. + allUsedContainerPortsMap := make(map[string][65536]bool) + allUsedHostPortsMap := make(map[string][65536]bool) - // Iterate through all port mappings, generating OCICNI PortMapping - // structs and validating there is no overlap. + // First, we need to validate the ports passed in the specgen for _, port := range portMappings { // First, check proto protocols, err := checkProtocol(port.Protocol, true) if err != nil { - return nil, nil, nil, err - } - - // Validate host IP - hostIP := port.HostIP - if hostIP == "" { - hostIP = "0.0.0.0" + return nil, err } - if ip := net.ParseIP(hostIP); ip == nil { - return nil, nil, nil, errors.Errorf("invalid IP address %s in port mapping", port.HostIP) + if port.HostIP != "" { + if ip := net.ParseIP(port.HostIP); ip == nil { + return nil, errors.Errorf("invalid IP address %q in port mapping", port.HostIP) + } } // Validate port numbers and range. - len := port.Range - if len == 0 { - len = 1 + portRange := port.Range + if portRange == 0 { + portRange = 1 } containerPort := port.ContainerPort if containerPort == 0 { - return nil, nil, nil, errors.Errorf("container port number must be non-0") + return nil, errors.Errorf("container port number must be non-0") } hostPort := port.HostPort - if uint32(len-1)+uint32(containerPort) > 65535 { - return nil, nil, nil, errors.Errorf("container port range exceeds maximum allowable port number") + if uint32(portRange-1)+uint32(containerPort) > 65535 { + return nil, errors.Errorf("container port range exceeds maximum allowable port number") } - if uint32(len-1)+uint32(hostPort) > 65536 { - return nil, nil, nil, errors.Errorf("host port range exceeds maximum allowable port number") + if uint32(portRange-1)+uint32(hostPort) > 65535 { + return nil, errors.Errorf("host port range exceeds maximum allowable port number") } - // Iterate through ports, populating maps to check for conflicts - // and generating CNI port mappings. - for _, p := range protocols { - hostIPMap := hostPortValidate[p] - ctrIPMap := containerPortValidate[p] - - hostPortMap, ok := hostIPMap[hostIP] - if !ok { - hostPortMap = make(map[uint16]uint16) - hostIPMap[hostIP] = hostPortMap + hostProtoMap, ok := portMap[port.HostIP] + if !ok { + hostProtoMap = make(map[string][]tempMapping) + for _, proto := range []string{protoTCP, protoUDP, protoSCTP} { + hostProtoMap[proto] = make([]tempMapping, 0) } - ctrPortMap, ok := ctrIPMap[hostIP] - if !ok { - ctrPortMap = make(map[uint16]uint16) - ctrIPMap[hostIP] = ctrPortMap - } - - // Iterate through all port numbers in the requested - // range. - var index uint16 - for index = 0; index < len; index++ { - cPort := containerPort + index - hPort := hostPort - // Only increment host port if it's not 0. - if hostPort != 0 { - hPort += index - } - - if cPort == 0 { - return nil, nil, nil, errors.Errorf("container port cannot be 0") - } + portMap[port.HostIP] = hostProtoMap + } - // Host port is allowed to be 0. If it is, we - // select a random port on the host. - // This will happen *after* all other ports are - // placed, to ensure we don't accidentally - // select a port that a later mapping wanted. - if hPort == 0 { - // If we already have a host port - // assigned to their container port - - // just use that. - if ctrPortMap[cPort] != 0 { - hPort = ctrPortMap[cPort] - } else { - postAssignHostPort = true - } - } else { - testHPort := hostPortMap[hPort] - if testHPort != 0 && testHPort != cPort { - return nil, nil, nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", hPort, p) - } - hostPortMap[hPort] = cPort - - // Mapping a container port to multiple - // host ports is allowed. - // We only store the latest of these in - // the container port map - we don't - // need to know all of them, just one. - testCPort := ctrPortMap[cPort] - ctrPortMap[cPort] = hPort - - // If we have an exact duplicate, just continue - if testCPort == hPort && testHPort == cPort { - continue - } - } + p := tempMapping{ + hostPort: port.HostPort, + containerPort: port.ContainerPort, + rangePort: portRange, + } - // We appear to be clear. Make an OCICNI port - // struct. - // Don't use hostIP - we want to preserve the - // empty string hostIP by default for compat. - cniPort := types.OCICNIPortMapping{ - HostPort: int32(hPort), - ContainerPort: int32(cPort), - Protocol: p, - HostIP: port.HostIP, - } - tempMappings = append( - tempMappings, - tempMapping{ - mapping: cniPort, - startOfRange: port.Range > 1 && index == 0, - isInRange: port.Range > 1, - }, - ) - } + for _, proto := range protocols { + hostProtoMap[proto] = append(hostProtoMap[proto], p) } } - // Handle any 0 host ports now by setting random container ports. - if postAssignHostPort { - remadeMappings := make([]types.OCICNIPortMapping, 0, len(tempMappings)) - - var ( - candidate int - err error - ) - - // Iterate over all - for _, tmp := range tempMappings { - p := tmp.mapping + // we do no longer need the original port mappings + // set it to 0 length so we can resuse it to populate + // the slice again while keeping the underlying capacity + portMappings = portMappings[:0] - if p.HostPort != 0 { - remadeMappings = append(remadeMappings, p) + for hostIP, protoMap := range portMap { + for protocol, ports := range protoMap { + ports := ports + if len(ports) == 0 { continue } - - hostIPMap := hostPortValidate[p.Protocol] - ctrIPMap := containerPortValidate[p.Protocol] - - hostPortMap, ok := hostIPMap[p.HostIP] - if !ok { - hostPortMap = make(map[uint16]uint16) - hostIPMap[p.HostIP] = hostPortMap + // 1. sort the ports by host port + // use a small hack to make sure ports with host port 0 are sorted last + sort.Slice(ports, func(i, j int) bool { + if ports[i].hostPort == ports[j].hostPort { + return ports[i].containerPort < ports[j].containerPort + } + if ports[i].hostPort == 0 { + return false + } + if ports[j].hostPort == 0 { + return true + } + return ports[i].hostPort < ports[j].hostPort + }) + + allUsedContainerPorts := allUsedContainerPortsMap[protocol] + allUsedHostPorts := allUsedHostPortsMap[protocol] + var usedHostPorts [65536]bool + + var previousPort *types.PortMapping + var i int + for i = 0; i < len(ports); i++ { + if ports[i].hostPort == 0 { + // because the ports are sorted and host port 0 is last + // we can break when we hit 0 + // we will fit them in afterwards + break + } + p := types.PortMapping{ + HostIP: hostIP, + Protocol: protocol, + HostPort: ports[i].hostPort, + ContainerPort: ports[i].containerPort, + Range: ports[i].rangePort, + } + var err error + previousPort, err = joinTwoPortsToRangePortIfPossible(&portMappings, &allUsedHostPorts, + &allUsedContainerPorts, &usedHostPorts, previousPort, p) + if err != nil { + return nil, err + } } - ctrPortMap, ok := ctrIPMap[p.HostIP] - if !ok { - ctrPortMap = make(map[uint16]uint16) - ctrIPMap[p.HostIP] = ctrPortMap + if previousPort != nil { + addPortToUsedPorts(&portMappings, &allUsedHostPorts, + &allUsedContainerPorts, &usedHostPorts, previousPort) } - // See if container port has been used elsewhere - if ctrPortMap[uint16(p.ContainerPort)] != 0 { - // Duplicate definition. Let's not bother - // including it. - continue + // now take care of the hostPort = 0 ports + previousPort = nil + for i < len(ports) { + p := types.PortMapping{ + HostIP: hostIP, + Protocol: protocol, + ContainerPort: ports[i].containerPort, + Range: ports[i].rangePort, + } + var err error + previousPort, err = joinTwoContainerPortsToRangePortIfPossible(&portMappings, &allUsedHostPorts, + &allUsedContainerPorts, &usedHostPorts, previousPort, p) + if err != nil { + return nil, err + } + i++ + } + if previousPort != nil { + newPort, err := getRandomHostPort(&usedHostPorts, *previousPort) + if err != nil { + return nil, err + } + addPortToUsedPorts(&portMappings, &allUsedHostPorts, + &allUsedContainerPorts, &usedHostPorts, &newPort) } - // Max retries to ensure we don't loop forever. - for i := 0; i < 15; i++ { - // Only get a random candidate for single entries or the start - // of a range. Otherwise we just increment the candidate. - if !tmp.isInRange || tmp.startOfRange { - candidate, err = utils.GetRandomPort() + allUsedContainerPortsMap[protocol] = allUsedContainerPorts + allUsedHostPortsMap[protocol] = allUsedHostPorts + } + } + + if len(exposePorts) > 0 { + logrus.Debugf("Adding exposed ports") + + for port, protocols := range exposePorts { + newProtocols := make([]string, 0, len(protocols)) + for _, protocol := range protocols { + if !allUsedContainerPortsMap[protocol][port] { + p := types.PortMapping{ + ContainerPort: port, + Protocol: protocol, + Range: 1, + } + allPorts := allUsedContainerPortsMap[protocol] + p, err := getRandomHostPort(&allPorts, p) if err != nil { - return nil, nil, nil, errors.Wrapf(err, "error getting candidate host port for container port %d", p.ContainerPort) + return nil, err } + portMappings = append(portMappings, p) } else { - candidate++ - } - - if hostPortMap[uint16(candidate)] == 0 { - logrus.Debugf("Successfully assigned container port %d to host port %d (IP %s Protocol %s)", p.ContainerPort, candidate, p.HostIP, p.Protocol) - hostPortMap[uint16(candidate)] = uint16(p.ContainerPort) - ctrPortMap[uint16(p.ContainerPort)] = uint16(candidate) - p.HostPort = int32(candidate) - break + newProtocols = append(newProtocols, protocol) } } - if p.HostPort == 0 { - return nil, nil, nil, errors.Errorf("could not find open host port to map container port %d to", p.ContainerPort) + // make sure to delete the key from the map if there are no protocols left + if len(newProtocols) == 0 { + delete(exposePorts, port) + } else { + exposePorts[port] = newProtocols } - remadeMappings = append(remadeMappings, p) } - return remadeMappings, containerPortValidate, hostPortValidate, nil } + return portMappings, nil +} - finalMappings := []types.OCICNIPortMapping{} - for _, m := range tempMappings { - finalMappings = append(finalMappings, m.mapping) +func appendProtocolsNoDuplicates(slice []string, protocols []string) []string { + for _, proto := range protocols { + if util.StringInSlice(proto, slice) { + continue + } + slice = append(slice, proto) } - - return finalMappings, containerPortValidate, hostPortValidate, nil + return slice } // Make final port mappings for the container -func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.OCICNIPortMapping, map[uint16][]string, error) { - finalMappings, containerPortValidate, hostPortValidate, err := ParsePortMapping(s.PortMappings) - if err != nil { - return nil, nil, err - } - - // No exposed ports so return the port mappings we've made so far. - if len(s.Expose) == 0 && imageData == nil { - return finalMappings, nil, nil - } - - logrus.Debugf("Adding exposed ports") - +func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData *libimage.ImageData) ([]types.PortMapping, map[uint16][]string, error) { expose := make(map[uint16]string) + var err error if imageData != nil { expose, err = GenExposedPorts(imageData.Config.ExposedPorts) if err != nil { @@ -274,103 +348,30 @@ func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, imageData } } - // We need to merge s.Expose into image exposed ports - for k, v := range s.Expose { - expose[k] = v - } - // There's been a request to expose some ports. Let's do that. - // Start by figuring out what needs to be exposed. - // This is a map of container port number to protocols to expose. - toExpose := make(map[uint16][]string) - for port, proto := range expose { - // Validate protocol first - protocols, err := checkProtocol(proto, false) - if err != nil { - return nil, nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port) - } - - if port == 0 { - return nil, nil, errors.Errorf("cannot expose 0 as it is not a valid port number") - } - - // Check to see if the port is already present in existing - // mappings. - for _, p := range protocols { - ctrPortMap, ok := containerPortValidate[p]["0.0.0.0"] - if !ok { - ctrPortMap = make(map[uint16]uint16) - containerPortValidate[p]["0.0.0.0"] = ctrPortMap + toExpose := make(map[uint16][]string, len(s.Expose)+len(expose)) + for _, expose := range []map[uint16]string{expose, s.Expose} { + for port, proto := range expose { + if port == 0 { + return nil, nil, errors.Errorf("cannot expose 0 as it is not a valid port number") } - - if portNum := ctrPortMap[port]; portNum == 0 { - // We want to expose this port for this protocol - exposeProto, ok := toExpose[port] - if !ok { - exposeProto = []string{} - } - exposeProto = append(exposeProto, p) - toExpose[port] = exposeProto + protocols, err := checkProtocol(proto, false) + if err != nil { + return nil, nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port) } + toExpose[port] = appendProtocolsNoDuplicates(toExpose[port], protocols) } } - // If not publishing exposed ports return mappings and exposed ports. + publishPorts := toExpose if !s.PublishExposedPorts { - return finalMappings, toExpose, nil + publishPorts = nil } - // We now have a final list of ports that we want exposed. - // Let's find empty, unallocated host ports for them. - for port, protocols := range toExpose { - for _, p := range protocols { - // Find an open port on the host. - // I see a faint possibility that this will infinite - // loop trying to find a valid open port, so I've - // included a max-tries counter. - hostPort := 0 - tries := 15 - for hostPort == 0 && tries > 0 { - // We can't select a specific protocol, which is - // unfortunate for the UDP case. - candidate, err := utils.GetRandomPort() - if err != nil { - return nil, nil, err - } - - // Check if the host port is already bound - hostPortMap, ok := hostPortValidate[p]["0.0.0.0"] - if !ok { - hostPortMap = make(map[uint16]uint16) - hostPortValidate[p]["0.0.0.0"] = hostPortMap - } - - if checkPort := hostPortMap[uint16(candidate)]; checkPort != 0 { - // Host port is already allocated, try again - tries-- - continue - } - - hostPortMap[uint16(candidate)] = port - hostPort = candidate - logrus.Debugf("Mapping exposed port %d/%s to host port %d", port, p, hostPort) - - // Make a CNI port mapping - cniPort := types.OCICNIPortMapping{ - HostPort: int32(candidate), - ContainerPort: int32(port), - Protocol: p, - HostIP: "", - } - finalMappings = append(finalMappings, cniPort) - } - if tries == 0 && hostPort == 0 { - // We failed to find an open port. - return nil, nil, errors.Errorf("failed to find an open port to expose container port %d on the host", port) - } - } + finalMappings, err := ParsePortMapping(s.PortMappings, publishPorts) + if err != nil { + return nil, nil, err } - - return finalMappings, nil, nil + return finalMappings, toExpose, nil } // Check a string to ensure it is a comma-separated set of valid protocols @@ -409,7 +410,7 @@ func checkProtocol(protocol string, allowSCTP bool) ([]string, error) { } func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error) { - expose := make(map[uint16]string) + expose := make(map[uint16]string, len(exposedPorts)) for imgExpose := range exposedPorts { // Expose format is portNumber[/protocol] splitExpose := strings.SplitN(imgExpose, "/", 2) @@ -420,12 +421,20 @@ func GenExposedPorts(exposedPorts map[string]struct{}) (map[uint16]string, error if num > 65535 || num < 1 { return nil, errors.Errorf("%d from image EXPOSE statement %q is not a valid port number", num, imgExpose) } - // No need to validate protocol, we'll do it below. - if len(splitExpose) == 1 { - expose[uint16(num)] = "tcp" + + // No need to validate protocol, we'll do it later. + newProto := "tcp" + if len(splitExpose) == 2 { + newProto = splitExpose[1] + } + + proto := expose[uint16(num)] + if len(proto) > 1 { + proto = proto + "," + newProto } else { - expose[uint16(num)] = splitExpose[1] + proto = newProto } + expose[uint16(num)] = proto } return expose, nil } diff --git a/pkg/specgen/generate/ports_bench_test.go b/pkg/specgen/generate/ports_bench_test.go new file mode 100644 index 000000000..06f02acda --- /dev/null +++ b/pkg/specgen/generate/ports_bench_test.go @@ -0,0 +1,197 @@ +package generate + +import ( + "fmt" + "testing" + + "github.com/containers/podman/v3/libpod/network/types" +) + +func benchmarkParsePortMapping(b *testing.B, ports []types.PortMapping) { + for n := 0; n < b.N; n++ { + ParsePortMapping(ports, nil) + } +} + +func BenchmarkParsePortMappingNoPorts(b *testing.B) { + benchmarkParsePortMapping(b, nil) +} + +func BenchmarkParsePortMapping1(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + }) +} + +func BenchmarkParsePortMapping100(b *testing.B) { + ports := make([]types.PortMapping, 0, 100) + for i := uint16(8080); i < 8180; i++ { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMapping1k(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000) + for i := uint16(8080); i < 9080; i++ { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMapping10k(b *testing.B) { + ports := make([]types.PortMapping, 0, 30000) + for i := uint16(8080); i < 18080; i++ { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMapping1m(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000000) + for j := 0; j < 20; j++ { + for i := uint16(1); i <= 50000; i++ { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + HostIP: fmt.Sprintf("192.168.1.%d", j), + }) + } + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingReverse100(b *testing.B) { + ports := make([]types.PortMapping, 0, 100) + for i := uint16(8180); i > 8080; i-- { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingReverse1k(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000) + for i := uint16(9080); i > 8080; i-- { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingReverse10k(b *testing.B) { + ports := make([]types.PortMapping, 0, 30000) + for i := uint16(18080); i > 8080; i-- { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingReverse1m(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000000) + for j := 0; j < 20; j++ { + for i := uint16(50000); i > 0; i-- { + ports = append(ports, types.PortMapping{ + HostPort: i, + ContainerPort: i, + Protocol: "tcp", + HostIP: fmt.Sprintf("192.168.1.%d", j), + }) + } + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} + +func BenchmarkParsePortMappingRange1(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }) +} + +func BenchmarkParsePortMappingRange100(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 100, + }, + }) +} + +func BenchmarkParsePortMappingRange1k(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1000, + }, + }) +} + +func BenchmarkParsePortMappingRange10k(b *testing.B) { + benchmarkParsePortMapping(b, []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10000, + }, + }) +} + +func BenchmarkParsePortMappingRange1m(b *testing.B) { + ports := make([]types.PortMapping, 0, 1000000) + for j := 0; j < 20; j++ { + ports = append(ports, types.PortMapping{ + HostPort: 1, + ContainerPort: 1, + Protocol: "tcp", + Range: 50000, + HostIP: fmt.Sprintf("192.168.1.%d", j), + }) + } + b.ResetTimer() + benchmarkParsePortMapping(b, ports) +} diff --git a/pkg/specgen/generate/ports_test.go b/pkg/specgen/generate/ports_test.go new file mode 100644 index 000000000..20d5d0166 --- /dev/null +++ b/pkg/specgen/generate/ports_test.go @@ -0,0 +1,989 @@ +package generate + +import ( + "testing" + + "github.com/containers/podman/v3/libpod/network/types" + "github.com/stretchr/testify/assert" +) + +func TestParsePortMappingWithHostPort(t *testing.T) { + tests := []struct { + name string + arg []types.PortMapping + arg2 map[uint16][]string + want []types.PortMapping + }{ + { + name: "no ports", + arg: nil, + want: nil, + }, + { + name: "one tcp port", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "one tcp port no proto", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "one udp port", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp", + Range: 1, + }, + }, + }, + { + name: "one sctp port", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "sctp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "sctp", + Range: 1, + }, + }, + }, + { + name: "one port two protocols", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp,udp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp", + Range: 1, + }, + }, + }, + { + name: "one port three protocols", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp,udp,sctp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "sctp", + Range: 1, + }, + }, + }, + { + name: "one port with range 1", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "one port with range 5", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 5, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 5, + }, + }, + }, + { + name: "two ports joined", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + }, + }, + { + name: "two ports joined with range", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + { + HostPort: 8081, + ContainerPort: 81, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + }, + }, + { + name: "two ports with no overlapping range", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 9090, + ContainerPort: 9090, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 9090, + ContainerPort: 9090, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + }, + }, + { + name: "four ports with two overlapping ranges", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 8085, + ContainerPort: 85, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 100, + ContainerPort: 5, + Protocol: "tcp", + }, + { + HostPort: 101, + ContainerPort: 6, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 15, + }, + { + HostPort: 100, + ContainerPort: 5, + Protocol: "tcp", + Range: 2, + }, + }, + }, + { + name: "two overlapping ranges", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 8085, + ContainerPort: 85, + Protocol: "tcp", + Range: 2, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + }, + }, + { + name: "four overlapping ranges", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 8085, + ContainerPort: 85, + Protocol: "tcp", + Range: 2, + }, + { + HostPort: 8090, + ContainerPort: 90, + Protocol: "tcp", + Range: 7, + }, + { + HostPort: 8095, + ContainerPort: 95, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 17, + }, + }, + }, + { + name: "one port range overlaps 5 ports", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Range: 20, + }, + { + HostPort: 8085, + ContainerPort: 85, + Range: 2, + }, + { + HostPort: 8090, + ContainerPort: 90, + }, + { + HostPort: 8095, + ContainerPort: 95, + }, + { + HostPort: 8096, + ContainerPort: 96, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + Range: 20, + }, + }, + }, + { + name: "different host ip same port", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + HostIP: "192.168.1.1", + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + HostIP: "192.168.2.1", + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + HostIP: "192.168.1.1", + Range: 1, + }, + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "tcp", + HostIP: "192.168.2.1", + Range: 1, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := ParsePortMapping(tt.arg, tt.arg2) + assert.NoError(t, err, "error is not nil") + // use ElementsMatch instead of Equal because the order is not consistent + assert.ElementsMatch(t, tt.want, got, "got unexpected port mapping") + }) + } +} + +func TestParsePortMappingWithoutHostPort(t *testing.T) { + tests := []struct { + name string + arg []types.PortMapping + arg2 map[uint16][]string + want []types.PortMapping + }{ + { + name: "one tcp port", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "one port with two protocols", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp,udp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "udp", + Range: 1, + }, + }, + }, + { + name: "same port twice", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "neighbor ports are not joined", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + }, + { + HostPort: 0, + ContainerPort: 81, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 81, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "overlapping range ports are joined", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + { + HostPort: 0, + ContainerPort: 81, + Protocol: "tcp", + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 2, + }, + }, + }, + { + name: "four overlapping range ports are joined", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 3, + }, + { + HostPort: 0, + ContainerPort: 81, + Protocol: "tcp", + }, + { + HostPort: 0, + ContainerPort: 82, + Protocol: "tcp", + Range: 10, + }, + { + HostPort: 0, + ContainerPort: 90, + Protocol: "tcp", + Range: 5, + }, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 15, + }, + }, + }, + { + name: "expose one tcp port", + arg2: map[uint16][]string{ + 8080: {"tcp"}, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "expose already defined port", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + }, + }, + arg2: map[uint16][]string{ + 8080: {"tcp"}, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + }, + }, + { + name: "expose different proto", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + }, + }, + arg2: map[uint16][]string{ + 8080: {"udp"}, + }, + want: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 8080, + Protocol: "udp", + Range: 1, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := ParsePortMapping(tt.arg, tt.arg2) + assert.NoError(t, err, "error is not nil") + + // because we always get random host ports when it is set to 0 we cannot check that exactly + // check if it is not 0 and set to to 0 afterwards + for i := range got { + assert.Greater(t, got[i].HostPort, uint16(0), "host port is zero") + got[i].HostPort = 0 + } + + // use ElementsMatch instead of Equal because the order is not consistent + assert.ElementsMatch(t, tt.want, got, "got unexpected port mapping") + }) + } +} + +func TestParsePortMappingMixedHostPort(t *testing.T) { + tests := []struct { + name string + arg []types.PortMapping + want []types.PortMapping + resetHostPorts []int + }{ + { + name: "two ports one without a hostport set", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + }, + { + HostPort: 8080, + ContainerPort: 8080, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + resetHostPorts: []int{1}, + }, + { + name: "two ports one without a hostport set, inverted order", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + }, + { + HostPort: 0, + ContainerPort: 80, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + }, + resetHostPorts: []int{1}, + }, + { + name: "three ports without host ports, one with a hostport set, , inverted order", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + }, + { + HostPort: 0, + ContainerPort: 85, + }, + { + HostPort: 0, + ContainerPort: 90, + }, + { + HostPort: 8080, + ContainerPort: 8080, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 85, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 90, + Protocol: "tcp", + Range: 1, + }, + }, + resetHostPorts: []int{1, 2, 3}, + }, + { + name: "three ports without host ports, one with a hostport set", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + }, + { + HostPort: 0, + ContainerPort: 90, + }, + { + HostPort: 0, + ContainerPort: 85, + }, + { + HostPort: 0, + ContainerPort: 80, + }, + }, + want: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 8080, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 80, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 85, + Protocol: "tcp", + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 90, + Protocol: "tcp", + Range: 1, + }, + }, + resetHostPorts: []int{1, 2, 3}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := ParsePortMapping(tt.arg, nil) + assert.NoError(t, err, "error is not nil") + + // because we always get random host ports when it is set to 0 we cannot check that exactly + // use resetHostPorts to know which port element is 0 + for _, num := range tt.resetHostPorts { + assert.Greater(t, got[num].HostPort, uint16(0), "host port is zero") + got[num].HostPort = 0 + } + + assert.Equal(t, tt.want, got, "got unexpected port mapping") + }) + } +} + +func TestParsePortMappingError(t *testing.T) { + tests := []struct { + name string + arg []types.PortMapping + err string + }{ + { + name: "container port is 0", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 0, + Protocol: "tcp", + }, + }, + err: "container port number must be non-0", + }, + { + name: "container port range exceeds max", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 65000, + Protocol: "tcp", + Range: 10000, + }, + }, + err: "container port range exceeds maximum allowable port number", + }, + { + name: "host port range exceeds max", + arg: []types.PortMapping{ + { + HostPort: 60000, + ContainerPort: 1, + Protocol: "tcp", + Range: 10000, + }, + }, + err: "host port range exceeds maximum allowable port number", + }, + { + name: "invalid protocol", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "1", + }, + }, + err: "unrecognized protocol \"1\" in port mapping", + }, + { + name: "invalid protocol 2", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Protocol: "udp,u", + }, + }, + err: "unrecognized protocol \"u\" in port mapping", + }, + { + name: "invalid ip address", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + HostIP: "blah", + }, + }, + err: "invalid IP address \"blah\" in port mapping", + }, + { + name: "invalid overalpping range", + arg: []types.PortMapping{ + { + HostPort: 8080, + ContainerPort: 80, + Range: 5, + }, + { + HostPort: 8081, + ContainerPort: 60, + }, + }, + err: "conflicting port mappings for host port 8081 (protocol tcp)", + }, + { + name: "big port range with host port zero does not fit", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 1, + Range: 65535, + }, + }, + err: "failed to find an open port to expose container port 1 with range 65535 on the host", + }, + { + name: "big port range with host port zero does not fit", + arg: []types.PortMapping{ + { + HostPort: 0, + ContainerPort: 80, + Range: 1, + }, + { + HostPort: 0, + ContainerPort: 1000, + Range: 64535, + }, + }, + err: "failed to find an open port to expose container port 1000 with range 64535 on the host", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + _, err := ParsePortMapping(tt.arg, nil) + assert.EqualError(t, err, tt.err, "error does not match") + }) + } +} diff --git a/pkg/specgen/podspecgen.go b/pkg/specgen/podspecgen.go index 7713ea26c..948fb990c 100644 --- a/pkg/specgen/podspecgen.go +++ b/pkg/specgen/podspecgen.go @@ -99,7 +99,8 @@ type PodNetworkConfig struct { // Only available if NetNS is set to Bridge (the default for root). // As such, conflicts with NoInfra=true by proxy. // Optional. - StaticMAC *net.HardwareAddr `json:"static_mac,omitempty"` + // swagger:strfmt string + StaticMAC *types.HardwareAddr `json:"static_mac,omitempty"` // PortMappings is a set of ports to map into the infra container. // As, by default, containers share their network with the infra // container, this will forward the ports to the entire pod. diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 5a07af0f9..d777287d7 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -272,6 +272,10 @@ type ContainerStorageConfig struct { // If unset, the default, /, will be used. // Optional. WorkDir string `json:"work_dir,omitempty"` + // Create the working directory if it doesn't exist. + // If unset, it doesn't create it. + // Optional. + CreateWorkingDir bool `json:"create_working_dir,omitempty"` // StorageOpts is the container's storage options // Optional. StorageOpts map[string]string `json:"storage_opts,omitempty"` @@ -401,7 +405,8 @@ type ContainerNetworkConfig struct { // StaticMAC is a static MAC address to set in the container. // Only available if NetNS is set to bridge. // Optional. - StaticMAC *net.HardwareAddr `json:"static_mac,omitempty"` + // swagger:strfmt string + StaticMAC *nettypes.HardwareAddr `json:"static_mac,omitempty"` // PortBindings is a set of ports to map into the container. // Only available if NetNS is set to bridge or slirp. // Optional. diff --git a/pkg/specgenutil/specgen.go b/pkg/specgenutil/specgen.go index 683cd2918..4e8f954fb 100644 --- a/pkg/specgenutil/specgen.go +++ b/pkg/specgenutil/specgen.go @@ -11,6 +11,7 @@ import ( "github.com/containers/image/v5/manifest" "github.com/containers/podman/v3/cmd/podman/parse" "github.com/containers/podman/v3/libpod/define" + "github.com/containers/podman/v3/libpod/network/types" ann "github.com/containers/podman/v3/pkg/annotations" "github.com/containers/podman/v3/pkg/domain/entities" envLib "github.com/containers/podman/v3/pkg/env" @@ -457,7 +458,8 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *entities.ContainerCreateOptions s.DNSSearch = c.Net.DNSSearch s.DNSOptions = c.Net.DNSOptions s.StaticIP = c.Net.StaticIP - s.StaticMAC = c.Net.StaticMAC + // type cast to types.HardwareAddr + s.StaticMAC = (*types.HardwareAddr)(c.Net.StaticMAC) s.NetworkOptions = c.Net.NetworkOptions s.UseImageHosts = c.Net.NoHosts } |