diff --git a/src/cmd/list.go b/src/cmd/list.go index 269761a30..5ac3e7f36 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,96 @@ 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 interface{} + } + + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + i.ID = raw.ID + i.Names = raw.Names + // 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 +} + +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) 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= 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)