diff options
40 files changed, 622 insertions, 138 deletions
diff --git a/cmd/podman/machine/stop.go b/cmd/podman/machine/stop.go index 76ba85601..75666f734 100644 --- a/cmd/podman/machine/stop.go +++ b/cmd/podman/machine/stop.go @@ -3,6 +3,8 @@ package machine import ( + "fmt" + "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/pkg/machine" "github.com/containers/podman/v3/pkg/machine/qemu" @@ -46,5 +48,9 @@ func stop(cmd *cobra.Command, args []string) error { if err != nil { return err } - return vm.Stop(vmName, machine.StopOptions{}) + if err := vm.Stop(vmName, machine.StopOptions{}); err != nil { + return err + } + fmt.Printf("Machine %q stopped successfully\n", vmName) + return nil } diff --git a/libpod/container_config.go b/libpod/container_config.go index 33ea731fd..8f803d744 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -228,7 +228,7 @@ type ContainerNetworkConfig struct { // StaticMAC is a static MAC to request for the container. // This cannot be set unless CreateNetNS is set. // If not set, the container will be dynamically assigned a MAC by CNI. - StaticMAC net.HardwareAddr `json:"staticMAC"` + StaticMAC types.HardwareAddr `json:"staticMAC"` // PortMappings are the ports forwarded to the container's network // namespace // These are not used unless CreateNetNS is true diff --git a/libpod/network/cni/run.go b/libpod/network/cni/run.go index 99b2adce5..7795dfeeb 100644 --- a/libpod/network/cni/run.go +++ b/libpod/network/cni/run.go @@ -160,7 +160,7 @@ func CNIResultToStatus(res cnitypes.Result) (types.StatusBlock, error) { return result, err } interfaces[cniInt.Name] = types.NetInterface{ - MacAddress: mac, + MacAddress: types.HardwareAddr(mac), Networks: []types.NetAddress{{ Subnet: types.IPNet{IPNet: ip.Address}, Gateway: ip.Gateway, diff --git a/libpod/network/cni/run_test.go b/libpod/network/cni/run_test.go index 965203c2a..3169cd0eb 100644 --- a/libpod/network/cni/run_test.go +++ b/libpod/network/cni/run_test.go @@ -398,7 +398,7 @@ var _ = Describe("run CNI", func() { i, err := net.InterfaceByName(intName1) Expect(err).To(BeNil()) Expect(i.Name).To(Equal(intName1)) - Expect(i.HardwareAddr).To(Equal(macInt1)) + Expect(i.HardwareAddr).To(Equal((net.HardwareAddr)(macInt1))) addrs, err := i.Addrs() Expect(err).To(BeNil()) subnet := &net.IPNet{ @@ -448,7 +448,7 @@ var _ = Describe("run CNI", func() { i, err := net.InterfaceByName(intName1) Expect(err).To(BeNil()) Expect(i.Name).To(Equal(intName1)) - Expect(i.HardwareAddr).To(Equal(macInt1)) + Expect(i.HardwareAddr).To(Equal(net.HardwareAddr(macInt1))) addrs, err := i.Addrs() Expect(err).To(BeNil()) subnet := &net.IPNet{ @@ -460,7 +460,7 @@ var _ = Describe("run CNI", func() { i, err = net.InterfaceByName(intName2) Expect(err).To(BeNil()) Expect(i.Name).To(Equal(intName2)) - Expect(i.HardwareAddr).To(Equal(macInt2)) + Expect(i.HardwareAddr).To(Equal(net.HardwareAddr(macInt2))) addrs, err = i.Addrs() Expect(err).To(BeNil()) subnet = &net.IPNet{ @@ -600,7 +600,7 @@ var _ = Describe("run CNI", func() { i, err := net.InterfaceByName(intName1) Expect(err).To(BeNil()) Expect(i.Name).To(Equal(intName1)) - Expect(i.HardwareAddr).To(Equal(macInt1)) + Expect(i.HardwareAddr).To(Equal(net.HardwareAddr(macInt1))) addrs, err := i.Addrs() Expect(err).To(BeNil()) subnet := &net.IPNet{ @@ -612,7 +612,7 @@ var _ = Describe("run CNI", func() { i, err = net.InterfaceByName(intName2) Expect(err).To(BeNil()) Expect(i.Name).To(Equal(intName2)) - Expect(i.HardwareAddr).To(Equal(macInt2)) + Expect(i.HardwareAddr).To(Equal(net.HardwareAddr(macInt2))) addrs, err = i.Addrs() Expect(err).To(BeNil()) subnet = &net.IPNet{ @@ -690,7 +690,7 @@ var _ = Describe("run CNI", func() { netName: { InterfaceName: interfaceName, StaticIPs: []net.IP{ip1, ip2}, - StaticMAC: mac, + StaticMAC: types.HardwareAddr(mac), }, }, }, @@ -708,7 +708,7 @@ var _ = Describe("run CNI", func() { Expect(res[netName].Interfaces[interfaceName].Networks[1].Subnet.IP.String()).To(Equal(ip2.String())) Expect(res[netName].Interfaces[interfaceName].Networks[1].Subnet.Mask).To(Equal(subnet2.Mask)) Expect(res[netName].Interfaces[interfaceName].Networks[1].Gateway).To(Equal(net.ParseIP("fd41:0a75:2ca0:48a9::1"))) - Expect(res[netName].Interfaces[interfaceName].MacAddress).To(Equal(mac)) + Expect(res[netName].Interfaces[interfaceName].MacAddress).To(Equal(types.HardwareAddr(mac))) // default network has no dns Expect(res[netName].DNSServerIPs).To(BeEmpty()) Expect(res[netName].DNSSearchDomains).To(BeEmpty()) diff --git a/libpod/network/types/network.go b/libpod/network/types/network.go index 657c1ca6a..5cf523e20 100644 --- a/libpod/network/types/network.go +++ b/libpod/network/types/network.go @@ -1,6 +1,7 @@ package types import ( + "encoding/json" "net" "time" ) @@ -94,6 +95,50 @@ func (n *IPNet) UnmarshalText(text []byte) error { return nil } +// HardwareAddr is the same as net.HardwareAddr except +// that it adds the json marshal/unmarshal methods. +// This allows us to read the mac from a json string +// and a byte array. +type HardwareAddr net.HardwareAddr + +func (h *HardwareAddr) String() string { + return (*net.HardwareAddr)(h).String() +} + +func (h *HardwareAddr) MarshalText() ([]byte, error) { + return []byte((*net.HardwareAddr)(h).String()), nil +} + +func (h *HardwareAddr) UnmarshalJSON(text []byte) error { + if len(text) == 0 { + *h = nil + return nil + } + + // if the json string start with a quote we got a string + // unmarshal the string and parse the mac from this string + if string(text[0]) == `"` { + var macString string + err := json.Unmarshal(text, &macString) + if err == nil { + mac, err := net.ParseMAC(macString) + if err == nil { + *h = HardwareAddr(mac) + return nil + } + } + } + // not a string or got an error fallback to the normal parsing + mac := make(net.HardwareAddr, 0, 6) + // use the standard json unmarshal for backwards compat + err := json.Unmarshal(text, &mac) + if err != nil { + return err + } + *h = HardwareAddr(mac) + return nil +} + type Subnet struct { // Subnet for this Network in CIDR form. // swagger:strfmt string @@ -134,7 +179,7 @@ type NetInterface struct { // Networks list of assigned subnets with their gateway. Networks []NetAddress `json:"networks,omitempty"` // MacAddress for this Interface. - MacAddress net.HardwareAddr `json:"mac_address"` + MacAddress HardwareAddr `json:"mac_address"` } // NetAddress contains the subnet and gateway. @@ -157,7 +202,7 @@ type PerNetworkOptions struct { // Optional. Aliases []string `json:"aliases,omitempty"` // StaticMac for this container. Optional. - StaticMAC net.HardwareAddr `json:"static_mac,omitempty"` + StaticMAC HardwareAddr `json:"static_mac,omitempty"` // InterfaceName for this container. Required. InterfaceName string `json:"interface_name"` } diff --git a/libpod/network/types/network_test.go b/libpod/network/types/network_test.go new file mode 100644 index 000000000..91ee93692 --- /dev/null +++ b/libpod/network/types/network_test.go @@ -0,0 +1,82 @@ +package types_test + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/containers/podman/v3/libpod/network/types" +) + +func TestUnmarshalMacAddress(t *testing.T) { + tests := []struct { + name string + json string + want types.HardwareAddr + wantErr bool + }{ + { + name: "mac as string with colon", + json: `"52:54:00:1c:2e:46"`, + want: types.HardwareAddr{0x52, 0x54, 0x00, 0x1c, 0x2e, 0x46}, + }, + { + name: "mac as string with dash", + json: `"52-54-00-1c-2e-46"`, + want: types.HardwareAddr{0x52, 0x54, 0x00, 0x1c, 0x2e, 0x46}, + }, + { + name: "mac as byte array", + json: `[82, 84, 0, 28, 46, 70]`, + want: types.HardwareAddr{0x52, 0x54, 0x00, 0x1c, 0x2e, 0x46}, + }, + { + name: "null value", + json: `null`, + want: nil, + }, + { + name: "mac as base64", + json: `"qrvM3e7/"`, + want: types.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, + }, + { + name: "invalid string", + json: `"52:54:00:1c:2e`, + wantErr: true, + }, + { + name: "invalid array", + json: `[82, 84, 0, 28, 46`, + wantErr: true, + }, + + { + name: "invalid value", + json: `ab`, + wantErr: true, + }, + { + name: "invalid object", + json: `{}`, + wantErr: true, + }, + } + for _, tt := range tests { + test := tt + t.Run(test.name, func(t *testing.T) { + mac := types.HardwareAddr{} + err := json.Unmarshal([]byte(test.json), &mac) + if (err != nil) != test.wantErr { + t.Errorf("types.HardwareAddress Unmarshal() error = %v, wantErr %v", err, test.wantErr) + return + } + if test.wantErr { + return + } + if !reflect.DeepEqual(mac, test.want) { + t.Errorf("types.HardwareAddress Unmarshal() got = %v, want %v", mac, test.want) + } + }) + } +} diff --git a/libpod/options.go b/libpod/options.go index 135b2f363..f1cf015f8 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1104,7 +1104,7 @@ func WithNetworkOptions(options map[string][]string) CtrCreateOption { // It cannot be set unless WithNetNS has already been passed. // Further, it cannot be set if additional CNI networks to join have been // specified. -func WithStaticMAC(mac net.HardwareAddr) CtrCreateOption { +func WithStaticMAC(mac nettypes.HardwareAddr) CtrCreateOption { return func(ctr *Container) error { if ctr.valid { return define.ErrCtrFinalized 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..c63cf8f0e 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -42,9 +42,9 @@ 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"` + 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/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 e21cc2a33..38ceea271 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -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/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/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/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/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/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 8878bf128..7aa202334 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -57,7 +57,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/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 b8af2de68..e17f746a5 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -188,7 +188,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/specgen/podspecgen.go b/pkg/specgen/podspecgen.go index 7713ea26c..32d5be79a 100644 --- a/pkg/specgen/podspecgen.go +++ b/pkg/specgen/podspecgen.go @@ -99,7 +99,7 @@ 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"` + 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..593d91c64 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -401,7 +401,7 @@ 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"` + 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 } diff --git a/test/apiv2/10-images.at b/test/apiv2/10-images.at index d3fde9f9d..b7bcaf81d 100644 --- a/test/apiv2/10-images.at +++ b/test/apiv2/10-images.at @@ -218,6 +218,9 @@ if ! grep -q '400 Bad Request' "${TMPD}/headers.txt"; then BUILD_TEST_ERROR="1" fi +t POST libpod/images/prune 200 +t POST libpod/images/prune 200 length=0 [] + cleanBuildTest if [[ "${BUILD_TEST_ERROR}" ]]; then exit 1 diff --git a/test/apiv2/40-pods.at b/test/apiv2/40-pods.at index 985b26411..f45e85f61 100644 --- a/test/apiv2/40-pods.at +++ b/test/apiv2/40-pods.at @@ -110,11 +110,11 @@ t GET libpod/pods/fakename/top 404 \ .cause="no such pod" t GET libpod/pods/foo/top 200 \ - .Processes[0][-1]="/pause " \ + .Processes[0][-1]="/pause" \ .Titles[-1]="COMMAND" t GET libpod/pods/foo/top?ps_args=args,pid 200 \ - .Processes[0][0]="/pause " \ + .Processes[0][0]="/pause" \ .Processes[0][1]="1" \ .Titles[0]="COMMAND" \ .Titles[1]="PID" \ diff --git a/test/apiv2/python/rest_api/test_v2_0_0_container.py b/test/apiv2/python/rest_api/test_v2_0_0_container.py index 853e9da88..101044bbb 100644 --- a/test/apiv2/python/rest_api/test_v2_0_0_container.py +++ b/test/apiv2/python/rest_api/test_v2_0_0_container.py @@ -1,8 +1,11 @@ +import multiprocessing +import queue import random +import threading import unittest -import json import requests +import time from dateutil.parser import parse from .fixtures import APITestCase @@ -16,7 +19,10 @@ class ContainerTestCase(APITestCase): self.assertEqual(len(obj), 1) def test_list_filters(self): - r = requests.get(self.podman_url + "/v1.40/containers/json?filters%3D%7B%22status%22%3A%5B%22running%22%5D%7D") + r = requests.get( + self.podman_url + + "/v1.40/containers/json?filters%3D%7B%22status%22%3A%5B%22running%22%5D%7D" + ) self.assertEqual(r.status_code, 200, r.text) payload = r.json() containerAmnt = len(payload) @@ -33,18 +39,18 @@ class ContainerTestCase(APITestCase): self.assertId(r.content) _ = parse(r.json()["Created"]) - r = requests.post( self.podman_url + "/v1.40/containers/create?name=topcontainer", - json={"Cmd": ["top"], - "Image": "alpine:latest", - "Healthcheck": { - "Test": ["CMD", "pidof", "top"], - "Interval": 5000000000, - "Timeout": 2000000000, - "Retries": 3, - "StartPeriod": 5000000000 - } + json={ + "Cmd": ["top"], + "Image": "alpine:latest", + "Healthcheck": { + "Test": ["CMD", "pidof", "top"], + "Interval": 5000000000, + "Timeout": 2000000000, + "Retries": 3, + "StartPeriod": 5000000000, + }, }, ) self.assertEqual(r.status_code, 201, r.text) @@ -67,7 +73,7 @@ class ContainerTestCase(APITestCase): self.assertEqual(r.status_code, 200, r.text) self.assertId(r.content) out = r.json() - hc = out["Config"]["Healthcheck"]["Test"] + hc = out["Config"]["Healthcheck"]["Test"] self.assertListEqual(["CMD", "pidof", "top"], hc) r = requests.post(self.podman_url + f"/v1.40/containers/{container_id}/start") @@ -84,7 +90,9 @@ class ContainerTestCase(APITestCase): self.assertIn(r.status_code, (200, 409), r.text) if r.status_code == 200: self.assertId(r.content) - r = requests.get(self.uri(self.resolve_container("/containers/{}/stats?stream=false&one-shot=true"))) + r = requests.get( + self.uri(self.resolve_container("/containers/{}/stats?stream=false&one-shot=true")) + ) self.assertIn(r.status_code, (200, 409), r.text) if r.status_code == 200: self.assertId(r.content) @@ -136,9 +144,15 @@ class ContainerTestCase(APITestCase): payload = r.json() container_id = payload["Id"] self.assertIsNotNone(container_id) - r = requests.get(self.podman_url + f"/v1.40/containers/{payload['Id']}/logs?follow=false&stdout=true&until=0") + r = requests.get( + self.podman_url + + f"/v1.40/containers/{payload['Id']}/logs?follow=false&stdout=true&until=0" + ) self.assertEqual(r.status_code, 200, r.text) - r = requests.get(self.podman_url + f"/v1.40/containers/{payload['Id']}/logs?follow=false&stdout=true&until=1") + r = requests.get( + self.podman_url + + f"/v1.40/containers/{payload['Id']}/logs?follow=false&stdout=true&until=1" + ) self.assertEqual(r.status_code, 200, r.text) def test_commit(self): @@ -257,6 +271,63 @@ class ContainerTestCase(APITestCase): r = requests.delete(self.podman_url + f"/v1.40/containers/{container_id}") self.assertEqual(r.status_code, 204, r.text) + def test_top_no_stream(self): + uri = self.uri(self.resolve_container("/containers/{}/top")) + q = queue.Queue() + + def _impl(fifo): + fifo.put(requests.get(uri, params={"stream": False}, timeout=2)) + + top = threading.Thread(target=_impl, args=(q,)) + top.start() + time.sleep(2) + self.assertFalse(top.is_alive(), f"GET {uri} failed to return in 2s") + + qr = q.get(False) + self.assertEqual(qr.status_code, 200, qr.text) + + qr.close() + top.join() + + def test_top_stream(self): + uri = self.uri(self.resolve_container("/containers/{}/top")) + q = queue.Queue() + + stop_thread = False + + def _impl(fifo, stop): + try: + with requests.get(uri, params={"stream": True, "delay": 1}, stream=True) as r: + r.raise_for_status() + fifo.put(r) + for buf in r.iter_lines(chunk_size=None): + if stop(): + break + fifo.put(buf) + except Exception: + pass + + top = threading.Thread(target=_impl, args=(q, (lambda: stop_thread))) + top.start() + time.sleep(4) + self.assertTrue(top.is_alive(), f"GET {uri} exited too soon") + stop_thread = True + + for _ in range(10): + try: + qr = q.get_nowait() + if qr is not None: + self.assertEqual(qr.status_code, 200) + qr.close() + break + except queue.Empty: + pass + finally: + time.sleep(1) + else: + self.fail("Server failed to respond in 10s") + top.join() + if __name__ == "__main__": unittest.main() diff --git a/test/e2e/trust_test.go b/test/e2e/trust_test.go index 7f97f280a..b591e1c02 100644 --- a/test/e2e/trust_test.go +++ b/test/e2e/trust_test.go @@ -14,7 +14,8 @@ import ( var _ = Describe("Podman trust", func() { var ( - tempdir string + tempdir string + err error podmanTest *PodmanTestIntegration ) @@ -38,21 +39,17 @@ var _ = Describe("Podman trust", func() { }) It("podman image trust show", func() { - path, err := os.Getwd() - if err != nil { - os.Exit(1) - } - session := podmanTest.Podman([]string{"image", "trust", "show", "--registrypath", filepath.Dir(path), "--policypath", filepath.Join(filepath.Dir(path), "policy.json")}) + session := podmanTest.Podman([]string{"image", "trust", "show", "--registrypath", filepath.Join(INTEGRATION_ROOT, "test"), "--policypath", filepath.Join(INTEGRATION_ROOT, "test/policy.json")}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) outArray := session.OutputToStringArray() Expect(len(outArray)).To(Equal(3)) - // image order is not guaranteed. All we can do is check that - // these strings appear in output, we can't cross-check them. - Expect(session.OutputToString()).To(ContainSubstring("accept")) - Expect(session.OutputToString()).To(ContainSubstring("reject")) - Expect(session.OutputToString()).To(ContainSubstring("signed")) + // Repository order is not guaranteed. So, check that + // all expected lines appear in output; we also check total number of lines, so that handles all of them. + Expect(string(session.Out.Contents())).To(MatchRegexp(`(?m)^default\s+accept\s*$`)) + Expect(string(session.Out.Contents())).To(MatchRegexp(`(?m)^docker.io/library/hello-world\s+reject\s*$`)) + Expect(string(session.Out.Contents())).To(MatchRegexp(`(?m)^registry.access.redhat.com\s+signedBy\s+security@redhat.com, security@redhat.com\s+https://access.redhat.com/webassets/docker/content/sigstore\s*$`)) }) It("podman image trust set", func() { @@ -76,24 +73,52 @@ var _ = Describe("Podman trust", func() { }) It("podman image trust show --json", func() { - session := podmanTest.Podman([]string{"image", "trust", "show", "--json"}) + session := podmanTest.Podman([]string{"image", "trust", "show", "--registrypath", filepath.Join(INTEGRATION_ROOT, "test"), "--policypath", filepath.Join(INTEGRATION_ROOT, "test/policy.json"), "--json"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) Expect(session.IsJSONOutputValid()).To(BeTrue()) var teststruct []map[string]string json.Unmarshal(session.Out.Contents(), &teststruct) - Expect(teststruct[0]["name"]).To(Equal("* (default)")) - Expect(teststruct[0]["repo_name"]).To(Equal("default")) - Expect(teststruct[0]["type"]).To(Equal("accept")) - Expect(teststruct[1]["type"]).To(Equal("insecureAcceptAnything")) + Expect(len(teststruct)).To(Equal(3)) + // To ease comparison, group the unordered array of repos by repo (and we expect only one entry by repo, so order within groups doesn’t matter) + repoMap := map[string][]map[string]string{} + for _, e := range teststruct { + key := e["name"] + repoMap[key] = append(repoMap[key], e) + } + Expect(repoMap).To(Equal(map[string][]map[string]string{ + "* (default)": {{ + "name": "* (default)", + "repo_name": "default", + "sigstore": "", + "transport": "", + "type": "accept", + }}, + "docker.io/library/hello-world": {{ + "name": "docker.io/library/hello-world", + "repo_name": "docker.io/library/hello-world", + "sigstore": "", + "transport": "", + "type": "reject", + }}, + "registry.access.redhat.com": {{ + "name": "registry.access.redhat.com", + "repo_name": "registry.access.redhat.com", + "sigstore": "https://access.redhat.com/webassets/docker/content/sigstore", + "transport": "", + "type": "signedBy", + "gpg_id": "security@redhat.com, security@redhat.com", + }}, + })) }) It("podman image trust show --raw", func() { - session := podmanTest.Podman([]string{"image", "trust", "show", "--raw"}) + session := podmanTest.Podman([]string{"image", "trust", "show", "--policypath", filepath.Join(INTEGRATION_ROOT, "test/policy.json"), "--raw"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) + contents, err := ioutil.ReadFile(filepath.Join(INTEGRATION_ROOT, "test/policy.json")) + Expect(err).ShouldNot(HaveOccurred()) Expect(session.IsJSONOutputValid()).To(BeTrue()) - Expect(session.OutputToString()).To(ContainSubstring("default")) - Expect(session.OutputToString()).To(ContainSubstring("insecureAcceptAnything")) + Expect(string(session.Out.Contents())).To(Equal(string(contents) + "\n")) }) }) diff --git a/test/system/070-build.bats b/test/system/070-build.bats index 1d24eef15..3c47b1f5b 100644 --- a/test/system/070-build.bats +++ b/test/system/070-build.bats @@ -39,6 +39,7 @@ EOF cat >$dockerfile <<EOF FROM $IMAGE RUN echo $rand_content > /$rand_filename +VOLUME /a/b/c VOLUME ['/etc/foo', '/etc/bar'] EOF @@ -56,6 +57,25 @@ EOF /\[/etc/foo, /etc/bar]" "weird VOLUME gets converted to directories with brackets and comma" + # Now confirm that each volume got a unique device ID + run_podman run --rm build_test stat -c '%D' / /a /a/b /a/b/c /\[ /\[/etc /\[/etc/foo, /etc /etc/bar\] + # First, the non-volumes should all be the same... + is "${lines[0]}" "${lines[1]}" "devnum( / ) = devnum( /a )" + is "${lines[0]}" "${lines[2]}" "devnum( / ) = devnum( /a/b )" + is "${lines[0]}" "${lines[4]}" "devnum( / ) = devnum( /[ )" + is "${lines[0]}" "${lines[5]}" "devnum( / ) = devnum( /[etc )" + is "${lines[0]}" "${lines[7]}" "devnum( / ) = devnum( /etc )" + is "${lines[6]}" "${lines[8]}" "devnum( /[etc/foo, ) = devnum( /etc/bar] )" + # ...then, each volume should be different + if [[ "${lines[0]}" = "${lines[3]}" ]]; then + die "devnum( / ) (${lines[0]}) = devnum( volume0 ) (${lines[3]}) -- they should differ" + fi + if [[ "${lines[0]}" = "${lines[6]}" ]]; then + die "devnum( / ) (${lines[0]}) = devnum( volume1 ) (${lines[6]}) -- they should differ" + fi + # FIXME: is this expected? I thought /a/b/c and /[etc/foo, would differ + is "${lines[3]}" "${lines[6]}" "devnum( volume0 ) = devnum( volume1 )" + run_podman rmi -f build_test } diff --git a/test/system/500-networking.bats b/test/system/500-networking.bats index cb73cf24d..b3471b425 100644 --- a/test/system/500-networking.bats +++ b/test/system/500-networking.bats @@ -444,6 +444,14 @@ load helpers die "MAC address did not change after podman network disconnect/connect" fi + # FIXME FIXME FIXME: #11825: bodhi tests are failing, remote+rootless only, + # with "dnsmasq: failed to create inotify". This error has never occurred + # in CI, and Ed has been unable to reproduce it on 1minutetip. This next + # line is a suggestion from Paul Holzinger for trying to shed light on + # the system context before the failure. This output will be invisible + # if the test passes. + for foo in /proc/\*/fd/*; do readlink -f $foo; done |grep '^/proc/.*inotify' |cut -d/ -f3 | xargs -I '{}' -- ps --no-headers -o '%p %U %a' -p '{}' |uniq -c |sort -n + # connect a second network run_podman network connect $netname2 $cid diff --git a/test/upgrade/test-upgrade.bats b/test/upgrade/test-upgrade.bats index 5cb302a85..e9910f3d2 100644 --- a/test/upgrade/test-upgrade.bats +++ b/test/upgrade/test-upgrade.bats @@ -99,6 +99,7 @@ podman \$opts run -d --name myrunningcontainer --label mylabel=$LABEL_RUNNING \ -p $HOST_PORT:80 \ -v $pmroot/var/www:/var/www \ -w /var/www \ + --mac-address aa:bb:cc:dd:ee:ff \ $IMAGE /bin/busybox-extras httpd -f -p 80 podman \$opts pod create --name mypod |