From 0e5c30d9713ac13419241dcb480fb2e8a71327e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= Date: Tue, 14 Jul 2020 22:59:09 +0200 Subject: [PATCH 1/4] build: Add github.com/docker/go-units This package will be used for generating a human-readable representation of elapsed time (e.g., "5 minutes"). https://github.com/containers/toolbox/pull/503 --- src/go.mod | 1 + src/go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/go.mod b/src/go.mod index 3c11a5d15..2b1ac3018 100644 --- a/src/go.mod +++ b/src/go.mod @@ -6,6 +6,7 @@ require ( github.com/HarryMichal/go-version v1.0.0 github.com/acobaugh/osrelease v0.0.0-20181218015638-a93a0a55a249 github.com/briandowns/spinner v1.10.0 + github.com/docker/go-units v0.4.0 github.com/godbus/dbus/v5 v5.0.3 github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.5 diff --git a/src/go.sum b/src/go.sum index d91ae7906..4d6a0034d 100644 --- a/src/go.sum +++ b/src/go.sum @@ -12,6 +12,8 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= From 4028c3ae3f56585c9af5b8f5dc9442562ee3a44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= Date: Wed, 22 Jul 2020 11:34:42 +0200 Subject: [PATCH 2/4] pkg/utils: Add function creating human-readable duration strings https://github.com/containers/toolbox/pull/503 --- src/pkg/utils/utils.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index 6d38b709f..634d981e8 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -28,9 +28,11 @@ import ( "strconv" "strings" "syscall" + "time" "github.com/acobaugh/osrelease" "github.com/containers/toolbox/pkg/shell" + "github.com/docker/go-units" "github.com/godbus/dbus/v5" "github.com/sirupsen/logrus" "golang.org/x/sys/unix" @@ -333,6 +335,14 @@ func GetMountOptions(target string) (string, error) { return mountOptions, nil } +// HumanDuration accepts a Unix time value and converts it into a human readable +// string. +// +// Examples: "5 minutes ago", "2 hours ago", "3 days ago" +func HumanDuration(duration int64) string { + return units.HumanDuration(time.Since(time.Unix(duration, 0))) + " ago" +} + // ImageReferenceCanBeID checks if 'image' might be the ID of an image func ImageReferenceCanBeID(image string) (bool, error) { matched, err := regexp.MatchString("^[a-f0-9]\\{6,64\\}$", image) From e7722078310a79b08ad0095b1980c95db2094197 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 3/4] cmd/list, cmd/run: Decode image/container JSONs to typed structs 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 bearable (marking the questioned field as interface{} and then immediatelly type switch). 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. Instead of using getters for properties that need some checks or adjustments this uses a custom unmarshaling function (Owen's idea; thanks!). The work around populating the two new types is a bit hacky (marshalling and already unmarshalled JSON - that was unmarshalled to []map[string]interface{} - that is then unmarshalled to either toolboxImage or toolboxContainer. This is done to prevent a major refactoring before the 0.1.0 release. This should be changed for the 0.2.0 release. https://github.com/containers/toolbox/pull/503 --- src/cmd/list.go | 204 ++++++++++++++++++++++++++++++++++-------------- src/cmd/run.go | 8 +- 2 files changed, 146 insertions(+), 66 deletions(-) diff --git a/src/cmd/list.go b/src/cmd/list.go index 269761a30..c2e8c6417 100644 --- a/src/cmd/list.go +++ b/src/cmd/list.go @@ -17,6 +17,7 @@ package cmd import ( + "encoding/json" "errors" "fmt" "os" @@ -28,6 +29,20 @@ import ( "github.com/spf13/cobra" ) +type toolboxImage struct { + ID string + Names []string + Created string +} + +type toolboxContainer struct { + ID string + Names []string + Status string + Created string + Image string +} + var ( listFlags struct { onlyContainers bool @@ -82,8 +97,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 +119,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 +143,27 @@ func listContainers() ([]map[string]interface{}, error) { containers = utils.SortJSON(containers, "Names", false) } - return containers, nil + // This section is a temporary solution that is here to prevent a major + // redesign of the way how toolbox containers are fetched. + // Remove this in Toolbox v0.2.0 + 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 = c.UnmarshalJSON(containerJSON) + 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 +187,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,34 +214,39 @@ func listImages() ([]map[string]interface{}, error) { images = utils.SortJSON(images, "names", true) } - return images, nil + // This section is a temporary solution that is here to prevent a major + // redesign of the way how toolbox images are fetched. + // Remove this in Toolbox v0.2.0 + 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 = i.UnmarshalJSON(imageJSON) + 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) - fmt.Fprintf(writer, "%s\t%s\t%s\n", id, name, created) + fmt.Fprintf(writer, "%s\t%s\t%s\n", + utils.ShortID(image.ID), + image.Names[0], + image.Created) } writer.Flush() @@ -232,35 +272,10 @@ func listOutput(images, containers []map[string]interface{}) { "IMAGE NAME", resetColor) - 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 { - id := utils.ShortID(container[idKey].(string)) - - var nameString string - switch name := container["Names"].(type) { - case string: - nameString = name - case []interface{}: - nameString = name[0].(string) - } - - created := container[createdKey].(string) - status := container[statusKey].(string) - imageName := container["Image"].(string) - isRunning := false if podman.CheckVersion("2.0.0") { - isRunning = status == "running" + isRunning = container.Status == "running" } var color string @@ -270,17 +285,88 @@ func listOutput(images, containers []map[string]interface{}) { color = defaultColor } - fmt.Fprintf(writer, - "%s%s\t%s\t%s\t%s\t%s%s\n", + fmt.Fprintf(writer, "%s%s\t%s\t%s\t%s\t%s%s\n", color, - id, - nameString, - created, - status, - imageName, + utils.ShortID(container.ID), + container.Names[0], + container.Created, + container.Status, + container.Image, resetColor) } writer.Flush() } } + +func (i *toolboxImage) UnmarshalJSON(data []byte) error { + var raw struct { + ID string + Names []string + Created string + } + + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + i.ID = raw.ID + i.Names = raw.Names + i.Created = raw.Created + + return nil +} + +func (c *toolboxContainer) UnmarshalJSON(data []byte) error { + var raw struct { + ID string + Names interface{} + Status string + State interface{} + Created interface{} + Image string + } + + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + c.ID = raw.ID + // In Podman V1 the field 'Names' held a single string but since Podman V2 the + // field holds an array of strings + switch value := raw.Names.(type) { + case string: + c.Names = append(c.Names, value) + case []interface{}: + for _, v := range value { + c.Names = append(c.Names, v.(string)) + } + } + + // In Podman V1 the field holding a string about the container's state was + // called 'Status' and field 'State' held a number representing the state. In + // Podman V2 the string was moved to 'State' and field 'Status' was dropped. + switch value := raw.State.(type) { + case string: + c.Status = value + case float64: + c.Status = raw.Status + } + + // In Podman V1 the field 'Created' held a human-readable string in format + // "5 minutes ago". Since Podman V2 the field holds an integer with Unix time. + // After a discussion in https://github.com/containers/podman/issues/6594 the + // previous value was moved to field 'CreatedAt'. Since we're already using + // the 'github.com/docker/go-units' library, we'll stop using the provided + // human-readable string and assemble it ourselves. Go interprets numbers in + // JSON as float64. + switch value := raw.Created.(type) { + case string: + c.Created = value + case float64: + c.Created = utils.HumanDuration(int64(value)) + } + c.Image = raw.Image + + return nil +} diff --git a/src/cmd/run.go b/src/cmd/run.go index 31d394fb3..80d46cf8f 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -194,13 +194,7 @@ func runCommand(container string, } else if containersCount == 1 && defaultContainer { fmt.Fprintf(os.Stderr, "Error: container %s not found\n", container) - switch value := containers[0]["Names"].(type) { - case string: - container = value - case []interface{}: - container = value[0].(string) - } - + container = containers[0].Names[0] 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) From 8168e0f58ce7bc13d4328a172e62f3142faeceba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= Date: Thu, 23 Jul 2020 13:40:08 +0200 Subject: [PATCH 4/4] cmd/list: Handle API change of Podman 2.1 in `podman images` Since Podman 2.1 the field 'Created' of `podman images --format json` no longer holds a string with human-readable string but holds a Unix time integer[0]. [0] https://github.com/containers/podman/pull/6815 https://github.com/containers/toolbox/pull/503 --- src/cmd/list.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cmd/list.go b/src/cmd/list.go index c2e8c6417..5ac3e7f36 100644 --- a/src/cmd/list.go +++ b/src/cmd/list.go @@ -303,7 +303,7 @@ func (i *toolboxImage) UnmarshalJSON(data []byte) error { var raw struct { ID string Names []string - Created string + Created interface{} } if err := json.Unmarshal(data, &raw); err != nil { @@ -312,7 +312,15 @@ func (i *toolboxImage) UnmarshalJSON(data []byte) error { i.ID = raw.ID i.Names = raw.Names - i.Created = raw.Created + // Until Podman 2.0.x the field 'Created' held a human-readable string in + // format "5 minutes ago". Since Podman 2.1 the field holds an integer with + // Unix time. Go interprets numbers in JSON as float64. + switch value := raw.Created.(type) { + case string: + i.Created = value + case float64: + i.Created = utils.HumanDuration(int64(value)) + } return nil }