diff --git a/cmd/crictl/container.go b/cmd/crictl/container.go index 1d925d1400..85742e51b0 100644 --- a/cmd/crictl/container.go +++ b/cmd/crictl/container.go @@ -517,12 +517,16 @@ var containerStatusCommand = &cli.Command{ return err } - for i := range c.NArg() { - containerID := c.Args().Get(i) - if err := ContainerStatus(runtimeClient, containerID, c.String("output"), c.String("template"), c.Bool("quiet")); err != nil { - return fmt.Errorf("getting the status of the container %q: %w", containerID, err) - } + if err := containerStatus( + runtimeClient, + c.Args().Slice(), + c.String("output"), + c.String("template"), + c.Bool("quiet"), + ); err != nil { + return fmt.Errorf("get the status of containers: %w", err) } + return nil }, } @@ -989,43 +993,49 @@ func marshalContainerStatus(cs *pb.ContainerStatus) (string, error) { return marshalMapInOrder(jsonMap, *cs) } -// ContainerStatus sends a ContainerStatusRequest to the server, and parses +// containerStatus sends a ContainerStatusRequest to the server, and parses // the returned ContainerStatusResponse. -func ContainerStatus(client internalapi.RuntimeService, id, output string, tmplStr string, quiet bool) error { +// nolint:dupl // pods and containers are similar, but still different +func containerStatus(client internalapi.RuntimeService, ids []string, output string, tmplStr string, quiet bool) error { verbose := !(quiet) if output == "" { // default to json output output = "json" } - if id == "" { + if len(ids) == 0 { return errors.New("ID cannot be empty") } - request := &pb.ContainerStatusRequest{ - ContainerId: id, - Verbose: verbose, - } - logrus.Debugf("ContainerStatusRequest: %v", request) - r, err := InterruptableRPC(nil, func(ctx context.Context) (*pb.ContainerStatusResponse, error) { - return client.ContainerStatus(ctx, id, verbose) - }) - logrus.Debugf("ContainerStatusResponse: %v", r) - if err != nil { - return err - } - status, err := marshalContainerStatus(r.Status) - if err != nil { - return err - } + statuses := []statusData{} + for _, id := range ids { + request := &pb.ContainerStatusRequest{ + ContainerId: id, + Verbose: verbose, + } + logrus.Debugf("ContainerStatusRequest: %v", request) + r, err := InterruptableRPC(nil, func(ctx context.Context) (*pb.ContainerStatusResponse, error) { + return client.ContainerStatus(ctx, id, verbose) + }) + logrus.Debugf("ContainerStatusResponse: %v", r) + if err != nil { + return fmt.Errorf("get container status: %w", err) + } - switch output { - case "json", "yaml", "go-template": - return outputStatusInfo(status, "", r.Info, output, tmplStr) - case "table": // table output is after this switch block - default: - return fmt.Errorf("output option cannot be %s", output) + statusJSON, err := marshalContainerStatus(r.Status) + if err != nil { + return fmt.Errorf("marshal container status: %w", err) + } + + if output == "table" { + outputContainerStatusTable(r, verbose) + } else { + statuses = append(statuses, statusData{json: statusJSON, info: r.Info}) + } } - // output in table format + return outputStatusData(statuses, output, tmplStr) +} + +func outputContainerStatusTable(r *pb.ContainerStatusResponse, verbose bool) { fmt.Printf("ID: %s\n", r.Status.Id) if r.Status.Metadata != nil { if r.Status.Metadata.Name != "" { @@ -1064,8 +1074,6 @@ func ContainerStatus(client internalapi.RuntimeService, id, output string, tmplS if verbose { fmt.Printf("Info: %v\n", r.GetInfo()) } - - return nil } // ListContainers sends a ListContainerRequest to the server, and parses diff --git a/cmd/crictl/image.go b/cmd/crictl/image.go index 9bedb0c653..f77832347b 100644 --- a/cmd/crictl/image.go +++ b/cmd/crictl/image.go @@ -323,6 +323,8 @@ var imageStatusCommand = &cli.Command{ output = "json" } tmplStr := c.String("template") + + statuses := []statusData{} for i := range c.NArg() { id := c.Args().Get(i) @@ -330,45 +332,43 @@ var imageStatusCommand = &cli.Command{ if err != nil { return fmt.Errorf("image status for %q request: %w", id, err) } - image := r.Image - if image == nil { + + if r.Image == nil { return fmt.Errorf("no such image %q present", id) } - status, err := protobufObjectToJSON(r.Image) + statusJSON, err := protobufObjectToJSON(r.Image) if err != nil { - return fmt.Errorf("marshal status to json for %q: %w", id, err) - } - switch output { - case "json", "yaml", "go-template": - if err := outputStatusInfo(status, "", r.Info, output, tmplStr); err != nil { - return fmt.Errorf("output status for %q: %w", id, err) - } - continue - case "table": // table output is after this switch block - default: - return fmt.Errorf("output option cannot be %s", output) + return fmt.Errorf("marshal status to JSON for %q: %w", id, err) } - // otherwise output in table format - fmt.Printf("ID: %s\n", image.Id) - for _, tag := range image.RepoTags { - fmt.Printf("Tag: %s\n", tag) - } - for _, digest := range image.RepoDigests { - fmt.Printf("Digest: %s\n", digest) - } - size := units.HumanSizeWithPrecision(float64(image.GetSize_()), 3) - fmt.Printf("Size: %s\n", size) - if verbose { - fmt.Printf("Info: %v\n", r.GetInfo()) + if output == "table" { + outputImageStatusTable(r, verbose) + } else { + statuses = append(statuses, statusData{json: statusJSON, info: r.Info}) } } - return nil + return outputStatusData(statuses, output, tmplStr) }, } +func outputImageStatusTable(r *pb.ImageStatusResponse, verbose bool) { + // otherwise output in table format + fmt.Printf("ID: %s\n", r.Image.Id) + for _, tag := range r.Image.RepoTags { + fmt.Printf("Tag: %s\n", tag) + } + for _, digest := range r.Image.RepoDigests { + fmt.Printf("Digest: %s\n", digest) + } + size := units.HumanSizeWithPrecision(float64(r.Image.GetSize_()), 3) + fmt.Printf("Size: %s\n", size) + if verbose { + fmt.Printf("Info: %v\n", r.GetInfo()) + } +} + var removeImageCommand = &cli.Command{ Name: "rmi", Usage: "Remove one or more images", @@ -541,34 +541,31 @@ var imageFsInfoCommand = &cli.Command{ return fmt.Errorf("marshal filesystem info to json: %w", err) } - switch output { - case "json", "yaml", "go-template": - if err := outputStatusInfo(status, "", nil, output, tmplStr); err != nil { - return fmt.Errorf("output filesystem info: %w", err) - } - return nil - case "table": // table output is after this switch block - default: - return fmt.Errorf("output option cannot be %s", output) - } - - tablePrintFileSystem := func(fileLabel string, filesystem []*pb.FilesystemUsage) { - fmt.Printf("%s Filesystem \n", fileLabel) - for i, val := range filesystem { - fmt.Printf("TimeStamp[%d]: %d\n", i, val.Timestamp) - fmt.Printf("Disk[%d]: %s\n", i, units.HumanSize(float64(val.UsedBytes.GetValue()))) - fmt.Printf("Inodes[%d]: %d\n", i, val.InodesUsed.GetValue()) - fmt.Printf("Mountpoint[%d]: %s\n", i, val.FsId.Mountpoint) - } + if output == "table" { + ouputImageFsInfoTable(r) + } else { + return outputStatusData([]statusData{{json: status}}, output, tmplStr) } - // otherwise output in table format - tablePrintFileSystem("Container", r.ContainerFilesystems) - tablePrintFileSystem("Image", r.ImageFilesystems) return nil }, } +func ouputImageFsInfoTable(r *pb.ImageFsInfoResponse) { + tablePrintFileSystem := func(fileLabel string, filesystem []*pb.FilesystemUsage) { + fmt.Printf("%s Filesystem \n", fileLabel) + for i, val := range filesystem { + fmt.Printf("TimeStamp[%d]: %d\n", i, val.Timestamp) + fmt.Printf("Disk[%d]: %s\n", i, units.HumanSize(float64(val.UsedBytes.GetValue()))) + fmt.Printf("Inodes[%d]: %d\n", i, val.InodesUsed.GetValue()) + fmt.Printf("Mountpoint[%d]: %s\n", i, val.FsId.Mountpoint) + } + } + // otherwise output in table format + tablePrintFileSystem("Container", r.ContainerFilesystems) + tablePrintFileSystem("Image", r.ImageFilesystems) +} + func parseCreds(creds string) (string, string, error) { if creds == "" { return "", "", errors.New("credentials can't be empty") diff --git a/cmd/crictl/info.go b/cmd/crictl/info.go index 233cebb925..1d3d460f9e 100644 --- a/cmd/crictl/info.go +++ b/cmd/crictl/info.go @@ -78,13 +78,14 @@ func Info(cliContext *cli.Context, client internalapi.RuntimeService) error { return err } - status, err := protobufObjectToJSON(r.Status) + statusJSON, err := protobufObjectToJSON(r.Status) if err != nil { - return err + return fmt.Errorf("create status JSON: %w", err) } handlers, err := json.Marshal(r.RuntimeHandlers) // protobufObjectToJSON cannot be used if err != nil { return err } - return outputStatusInfo(status, string(handlers), r.Info, cliContext.String("output"), cliContext.String("template")) + data := []statusData{{json: statusJSON, runtimeHandlers: string(handlers), info: r.Info}} + return outputStatusData(data, cliContext.String("output"), cliContext.String("template")) } diff --git a/cmd/crictl/sandbox.go b/cmd/crictl/sandbox.go index 00d68cc572..e20d8a8bc5 100644 --- a/cmd/crictl/sandbox.go +++ b/cmd/crictl/sandbox.go @@ -229,14 +229,17 @@ var podStatusCommand = &cli.Command{ if err != nil { return err } - for i := range c.NArg() { - id := c.Args().Get(i) - err := PodSandboxStatus(runtimeClient, id, c.String("output"), c.Bool("quiet"), c.String("template")) - if err != nil { - return fmt.Errorf("getting the pod sandbox status for %q: %w", id, err) - } + if err := podSandboxStatus( + runtimeClient, + c.Args().Slice(), + c.String("output"), + c.Bool("quiet"), + c.String("template"), + ); err != nil { + return fmt.Errorf("get the status of pod sandboxes: %w", err) } + return nil }, } @@ -397,42 +400,51 @@ func marshalPodSandboxStatus(ps *pb.PodSandboxStatus) (string, error) { return marshalMapInOrder(jsonMap, *ps) } -// PodSandboxStatus sends a PodSandboxStatusRequest to the server, and parses +// podSandboxStatus sends a PodSandboxStatusRequest to the server, and parses // the returned PodSandboxStatusResponse. -func PodSandboxStatus(client internalapi.RuntimeService, id, output string, quiet bool, tmplStr string) error { +// nolint:dupl // pods and containers are similar, but still different +func podSandboxStatus(client internalapi.RuntimeService, ids []string, output string, quiet bool, tmplStr string) error { verbose := !(quiet) if output == "" { // default to json output output = "json" } - if id == "" { + if len(ids) == 0 { return errors.New("ID cannot be empty") } - request := &pb.PodSandboxStatusRequest{ - PodSandboxId: id, - Verbose: verbose, - } - logrus.Debugf("PodSandboxStatusRequest: %v", request) - r, err := InterruptableRPC(nil, func(ctx context.Context) (*pb.PodSandboxStatusResponse, error) { - return client.PodSandboxStatus(ctx, id, verbose) - }) - logrus.Debugf("PodSandboxStatusResponse: %v", r) - if err != nil { - return err - } + statuses := []statusData{} + for _, id := range ids { + request := &pb.PodSandboxStatusRequest{ + PodSandboxId: id, + Verbose: verbose, + } + logrus.Debugf("PodSandboxStatusRequest: %v", request) + r, err := InterruptableRPC(nil, func(ctx context.Context) (*pb.PodSandboxStatusResponse, error) { + return client.PodSandboxStatus(ctx, id, verbose) + }) + + logrus.Debugf("PodSandboxStatusResponse: %v", r) + if err != nil { + return fmt.Errorf("get pod sandbox status: %w", err) + } + + statusJSON, err := marshalPodSandboxStatus(r.Status) + if err != nil { + return fmt.Errorf("marshal pod sandbox status: %w", err) + } + + if output == "table" { + outputPodSandboxStatusTable(r, verbose) + } else { + statuses = append(statuses, statusData{json: statusJSON, info: r.Info}) + } - status, err := marshalPodSandboxStatus(r.Status) - if err != nil { - return err - } - switch output { - case "json", "yaml", "go-template": - return outputStatusInfo(status, "", r.Info, output, tmplStr) - case "table": // table output is after this switch block - default: - return fmt.Errorf("output option cannot be %s", output) } + return outputStatusData(statuses, output, tmplStr) +} + +func outputPodSandboxStatusTable(r *pb.PodSandboxStatusResponse, verbose bool) { // output in table format by default. fmt.Printf("ID: %s\n", r.Status.Id) if r.Status.Metadata != nil { @@ -472,8 +484,6 @@ func PodSandboxStatus(client internalapi.RuntimeService, id, output string, quie if verbose { fmt.Printf("Info: %v\n", r.GetInfo()) } - - return nil } // ListPodSandboxes sends a ListPodSandboxRequest to the server, and parses diff --git a/cmd/crictl/util.go b/cmd/crictl/util.go index 5368c62b86..ec7a138b8d 100644 --- a/cmd/crictl/util.go +++ b/cmd/crictl/util.go @@ -281,69 +281,91 @@ func outputProtobufObjAsYAML(obj proto.Message) error { return nil } -func outputStatusInfo(status, handlers string, info map[string]string, format string, tmplStr string) error { - // Sort all keys - keys := []string{} - for k := range info { - keys = append(keys, k) - } - sort.Strings(keys) +type statusData struct { + json string + runtimeHandlers string + info map[string]string +} - infoMap := map[string]any{} +func outputStatusData(statuses []statusData, format string, tmplStr string) (err error) { + if len(statuses) == 0 { + return nil + } - if status != "" { - var statusVal map[string]any - err := json.Unmarshal([]byte(status), &statusVal) - if err != nil { - return err + result := []map[string]any{} + for _, status := range statuses { + // Sort all keys + keys := []string{} + for k := range status.info { + keys = append(keys, k) + } + sort.Strings(keys) + infoMap := map[string]any{} + + if status.json != "" { + var statusVal map[string]any + err := json.Unmarshal([]byte(status.json), &statusVal) + if err != nil { + return fmt.Errorf("unmarshal status JSON: %w", err) + } + infoMap["status"] = statusVal } - infoMap["status"] = statusVal - } - if handlers != "" { - var handlersVal []*any - err := json.Unmarshal([]byte(handlers), &handlersVal) - if err != nil { - return err + if status.runtimeHandlers != "" { + var handlersVal []*any + err := json.Unmarshal([]byte(status.runtimeHandlers), &handlersVal) + if err != nil { + return fmt.Errorf("unmarshal runtime handlers: %w", err) + } + if handlersVal != nil { + infoMap["runtimeHandlers"] = handlersVal + } } - if handlersVal != nil { - infoMap["runtimeHandlers"] = handlersVal + + for _, k := range keys { + var genericVal map[string]any + json.Unmarshal([]byte(status.info[k]), &genericVal) + infoMap[k] = genericVal } + + result = append(result, infoMap) } - for _, k := range keys { - var genericVal map[string]any - json.Unmarshal([]byte(info[k]), &genericVal) - infoMap[k] = genericVal + // Old behavior: single entries are not encapsulated within an array + var jsonResult []byte + if len(result) == 1 { + jsonResult, err = json.Marshal(result[0]) + } else { + jsonResult, err = json.Marshal(result) } - jsonInfo, err := json.Marshal(infoMap) if err != nil { - return err + return fmt.Errorf("marshal result: %w", err) } switch format { case "yaml": - yamlInfo, err := yaml.JSONToYAML(jsonInfo) + yamlInfo, err := yaml.JSONToYAML(jsonResult) if err != nil { - return err + return fmt.Errorf("JSON result to YAML: %w", err) } fmt.Println(string(yamlInfo)) case "json": var output bytes.Buffer - if err := json.Indent(&output, jsonInfo, "", " "); err != nil { - return err + if err := json.Indent(&output, jsonResult, "", " "); err != nil { + return fmt.Errorf("indent JSON result: %w", err) } fmt.Println(output.String()) case "go-template": - output, err := tmplExecuteRawJSON(tmplStr, string(jsonInfo)) + output, err := tmplExecuteRawJSON(tmplStr, string(jsonResult)) if err != nil { - return err + return fmt.Errorf("execute template: %w", err) } fmt.Println(output) default: - fmt.Printf("Don't support %q format\n", format) + return fmt.Errorf("unsupported format: %q", format) } + return nil } diff --git a/cmd/crictl/util_test.go b/cmd/crictl/util_test.go index c35385eefd..9ef1d7c49e 100644 --- a/cmd/crictl/util_test.go +++ b/cmd/crictl/util_test.go @@ -73,7 +73,7 @@ func TestNameFilterByRegex(t *testing.T) { } } -func TestOutputStatusInfo(t *testing.T) { +func TestOutputStatusData(t *testing.T) { const ( statusResponse = `{"conditions":[ { @@ -180,7 +180,8 @@ func TestOutputStatusInfo(t *testing.T) { } outStr, err := captureOutput(func() error { - err := outputStatusInfo(tc.status, tc.handlers, tc.info, tc.format, tc.tmplStr) + data := []statusData{{json: tc.status, runtimeHandlers: tc.handlers, info: tc.info}} + err := outputStatusData(data, tc.format, tc.tmplStr) if err != nil { t.Errorf("Unexpected error: %v", err) } diff --git a/pkg/validate/security_context_linux.go b/pkg/validate/security_context_linux.go index 344dac1a4c..b88f1fd480 100644 --- a/pkg/validate/security_context_linux.go +++ b/pkg/validate/security_context_linux.go @@ -1579,7 +1579,7 @@ func rootfsPath(info map[string]string) string { // The stateDir might have not been created yet. Let's use the parent directory that should // always exist. - return filepath.Join(cfg.StateDir, "../") + return filepath.Join(cfg.StateDir, "../") //nolint:gocritic } func hostUsernsContent() string { diff --git a/test/e2e/inspecti_test.go b/test/e2e/inspecti_test.go new file mode 100644 index 0000000000..38b70eaa5d --- /dev/null +++ b/test/e2e/inspecti_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "encoding/json" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +// The actual test suite +var _ = t.Describe("inspecti", func() { + const ( + imageSuccessText = "Image is up to date" + registry = "gcr.io/k8s-staging-cri-tools/" + image1 = registry + "test-image-2" + image2 = registry + "test-image-3" + ) + + BeforeEach(func() { + t.CrictlExpectSuccess("pull "+image1, imageSuccessText) + t.CrictlExpectSuccess("pull "+image2, imageSuccessText) + }) + + AfterEach(func() { + Expect(t.Crictl(fmt.Sprintf("rmi %s %s", image1, image2))).To(Exit(0)) + }) + + It("should succeed", func() { + // Single response + res := t.Crictl("inspecti " + image1) + Expect(res).To(Exit(0)) + contents := res.Out.Contents() + + // Should be no slice + singleResponse := map[string]any{} + Expect(json.Unmarshal(contents, &singleResponse)).NotTo(HaveOccurred()) + Expect(singleResponse).To(HaveKey("info")) + Expect(singleResponse).To(HaveKey("status")) + + // Multi response + res = t.Crictl(fmt.Sprintf("inspecti %s %s", image1, image2)) + Expect(res).To(Exit(0)) + contents = res.Out.Contents() + + // Should be a slice + multiResponse := []map[string]any{} + Expect(json.Unmarshal(contents, &multiResponse)).NotTo(HaveOccurred()) + const length = 2 + Expect(multiResponse).To(HaveLen(length)) + for i := range length { + Expect(multiResponse[i]).To(HaveKey("info")) + Expect(multiResponse[i]).To(HaveKey("status")) + } + }) +}) diff --git a/test/e2e/inspectp_test.go b/test/e2e/inspectp_test.go new file mode 100644 index 0000000000..9a1dbcb6a4 --- /dev/null +++ b/test/e2e/inspectp_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +// The actual test suite +var _ = t.Describe("inspectp", func() { + const sandboxesLength = 2 + sandboxes := []string{} + + BeforeEach(func() { + sandboxes = []string{} + + for i := range sandboxesLength { + f, err := os.CreateTemp("", "sandbox-") + Expect(err).NotTo(HaveOccurred()) + _, err = fmt.Fprintf(f, `{ "metadata": { "name": "sb-%d", "uid": "uid-%d", "namespace": "ns" }}`, i, i) + Expect(err).NotTo(HaveOccurred()) + + res := t.Crictl("runp " + f.Name()) + Expect(res).To(Exit(0)) + sandboxes = append(sandboxes, string(bytes.TrimSpace(res.Out.Contents()))) + } + }) + + AfterEach(func() { + for _, sb := range sandboxes { + Expect(os.RemoveAll(sb)).NotTo(HaveOccurred()) + res := t.Crictl("rmp -f " + sb) + Expect(res).To(Exit(0)) + } + Expect(t.Crictl("rmi registry.k8s.io/pause:3.9")).To(Exit(0)) + }) + + It("should succeed", func() { + // Single entry + res := t.Crictl("inspectp " + sandboxes[0]) + Expect(res).To(Exit(0)) + contents := res.Out.Contents() + + // Should be no slice + singleResponse := map[string]any{} + Expect(json.Unmarshal(contents, &singleResponse)).NotTo(HaveOccurred()) + Expect(singleResponse).To(HaveKey("info")) + Expect(singleResponse).To(HaveKey("status")) + + // Multiple entries + res = t.Crictl(fmt.Sprintf("inspectp %s %s", sandboxes[0], sandboxes[1])) + Expect(res).To(Exit(0)) + contents = res.Out.Contents() + + // Should be a slice + multiResponse := []map[string]any{} + Expect(json.Unmarshal(contents, &multiResponse)).NotTo(HaveOccurred()) + Expect(multiResponse).To(HaveLen(sandboxesLength)) + for i := range sandboxesLength { + Expect(multiResponse[i]).To(HaveKey("info")) + Expect(multiResponse[i]).To(HaveKey("status")) + } + }) +})