From c2c3b2c1de0e4a4227aa4a37ec92f63880af7df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= Date: Tue, 14 Jul 2020 20:17:40 +0200 Subject: [PATCH] cmd/list: Decode image/container JSON to own types Every time Podman changes their JSON API Toolbox breaks horribly. That is caused by the combination of decoding the JSON purely by hand and by the complete lack of type assertions to make the process stable. I previously didn't know that unmarshalling with Go works on the 'do the best job it can' logic (thank you Owen!). This makes the need to check for subtle changes in the names of fields go away and type checking a bit more bareable (marking the questioned field as interface{} and then immediatelly type asserting). If even now an existing field does not hold an expected value the field will remain blank. To take this a bit further, I created two types (toolboxImage and toolboxContainer) that are used by both functions for returning the list of images and containers. To make the maintenance easier, I added few methods for those properties that have a bit questionable history. --- src/cmd/list.go | 168 ++++++++++++++++++++++++++++++++++-------------- src/cmd/run.go | 2 +- 2 files changed, 120 insertions(+), 50 deletions(-) diff --git a/src/cmd/list.go b/src/cmd/list.go index 7188701b7..57a995845 100644 --- a/src/cmd/list.go +++ b/src/cmd/list.go @@ -17,17 +17,36 @@ package cmd import ( + "encoding/json" "errors" "fmt" "os" "text/tabwriter" + "time" "github.com/containers/toolbox/pkg/podman" "github.com/containers/toolbox/pkg/utils" + "github.com/docker/go-units" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) +type toolboxImage struct { + ID string + Names []string + Created interface{} +} + +type toolboxContainer struct { + ID string + Name string + Names []string + Status string + State string + Created interface{} + Image string +} + var ( listFlags struct { onlyContainers bool @@ -82,8 +101,8 @@ func list(cmd *cobra.Command, args []string) error { lsImages = false } - var images []map[string]interface{} - var containers []map[string]interface{} + var images []toolboxImage + var containers []toolboxContainer var err error if lsImages { @@ -104,7 +123,7 @@ func list(cmd *cobra.Command, args []string) error { return nil } -func listContainers() ([]map[string]interface{}, error) { +func listContainers() ([]toolboxContainer, error) { logrus.Debug("Fetching containers with label=com.redhat.component=fedora-toolbox") args := []string{"--all", "--filter", "label=com.redhat.component=fedora-toolbox"} containers_old, err := podman.GetContainers(args...) @@ -128,7 +147,24 @@ func listContainers() ([]map[string]interface{}, error) { containers = utils.SortJSON(containers, "Names", false) } - return containers, nil + var toolboxContainers []toolboxContainer + for _, container := range containers { + var c toolboxContainer + containerJSON, err := json.Marshal(container) + if err != nil { + logrus.Errorf("failed to marshal container: %v", err) + continue + } + + err = json.Unmarshal(containerJSON, &c) + if err != nil { + logrus.Errorf("failed to unmarshal container: %v", err) + continue + } + toolboxContainers = append(toolboxContainers, c) + } + + return toolboxContainers, nil } func listHelp(cmd *cobra.Command, args []string) { @@ -152,7 +188,7 @@ func listHelp(cmd *cobra.Command, args []string) { } } -func listImages() ([]map[string]interface{}, error) { +func listImages() ([]toolboxImage, error) { logrus.Debug("Fetching images with label=com.redhat.component=fedora-toolbox") args := []string{"--filter", "label=com.redhat.component=fedora-toolbox"} images_old, err := podman.GetImages(args...) @@ -179,33 +215,37 @@ func listImages() ([]map[string]interface{}, error) { images = utils.SortJSON(images, "names", true) } - return images, nil + var toolboxImages []toolboxImage + for _, image := range images { + var i toolboxImage + imageJSON, err := json.Marshal(image) + if err != nil { + logrus.Errorf("failed to marshal toolbox image: %v", err) + continue + } + + err = json.Unmarshal(imageJSON, &i) + if err != nil { + logrus.Errorf("failed to unmarshal toolbox image: %v", err) + continue + } + toolboxImages = append(toolboxImages, i) + } + + return toolboxImages, nil } -func listOutput(images, containers []map[string]interface{}) { +func listOutput(images []toolboxImage, containers []toolboxContainer) { if len(images) != 0 { writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintf(writer, "%s\t%s\t%s\n", "IMAGE ID", "IMAGE NAME", "CREATED") - var idKey, nameKey, createdKey string - if podman.CheckVersion("2.0.0") { - idKey = "Id" - nameKey = "Names" - createdKey = "Created" - } else if podman.CheckVersion("1.8.3") { - idKey = "ID" - nameKey = "Names" - createdKey = "Created" - } else { - idKey = "id" - nameKey = "names" - createdKey = "created" - } - for _, image := range images { - id := utils.ShortID(image[idKey].(string)) - name := image[nameKey].([]interface{})[0].(string) - created := image[createdKey].(string) + var id, name, created string + id = utils.ShortID(image.ID) + name = image.Names[0] + created = image.GetCreatedString() + fmt.Fprintf(writer, "%s\t%s\t%s\n", id, name, created) } @@ -226,35 +266,65 @@ func listOutput(images, containers []map[string]interface{}) { "STATUS", "IMAGE NAME") - var idKey, createdKey, statusKey string - if podman.CheckVersion("2.0.0") { - idKey = "Id" - createdKey = "CreatedAt" - statusKey = "State" - } else { - idKey = "ID" - createdKey = "Created" - statusKey = "Status" + for _, container := range containers { + var id, name, created, status, imageName string + id = utils.ShortID(container.ID) + name = container.GetName() + status = container.GetStatus() + created = container.GetCreatedString() + imageName = container.Image + + fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\n", id, name, created, status, imageName) } - for _, container := range containers { - id := utils.ShortID(container[idKey].(string)) + writer.Flush() + } +} - var nameString string - switch name := container["Names"].(type) { - case string: - nameString = name - case []interface{}: - nameString = name[0].(string) - } +// GetCreatedString returns a string with human-readable time when an image was +// created +func (i toolboxImage) GetCreatedString() string { + var created string + switch value := i.Created.(type) { + case string: + created = value + case float64: + created = units.HumanDuration(time.Since(time.Unix(int64(value), 0))) + " ago" + } + return created +} - created := container[createdKey].(string) - status := container[statusKey].(string) - imageName := container["Image"].(string) +// GetName returns the first name of a container +func (c toolboxContainer) GetName() string { + var name string + if c.Name != "" { + name = c.Name + } else { + name = c.Names[0] + } + return name +} - fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\n", id, nameString, created, status, imageName) - } +// GetStatus returns the status of a container +func (c toolboxContainer) GetStatus() string { + var status string + if c.Status != "" { + status = c.Status + } else { + status = c.State + } + return status +} - writer.Flush() +// GetCreatedString returns a string with human-readable time when a container +// was created +func (c toolboxContainer) GetCreatedString() string { + var created string + switch value := c.Created.(type) { + case string: + created = value + case float64: + created = units.HumanDuration(time.Since(time.Unix(int64(value), 0))) + " ago" } + return created } diff --git a/src/cmd/run.go b/src/cmd/run.go index 7a1364665..1530001d5 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -194,7 +194,7 @@ func runCommand(container string, } else if containersCount == 1 && defaultContainer { fmt.Fprintf(os.Stderr, "Error: container %s not found\n", container) - container = containers[0]["Names"].(string) + container = containers[0].GetName() fmt.Fprintf(os.Stderr, "Entering container %s instead.\n", container) fmt.Fprintf(os.Stderr, "Use the 'create' command to create a different toolbox.\n") fmt.Fprintf(os.Stderr, "Run '%s --help' for usage.\n", executableBase)