Skip to content

Commit

Permalink
cmd/list: Decode image/container JSON to own types
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
HarryMichal committed Jul 14, 2020
1 parent 140c2d7 commit c2c3b2c
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 50 deletions.
168 changes: 119 additions & 49 deletions src/cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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...)
Expand All @@ -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) {
Expand All @@ -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...)
Expand All @@ -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)
}

Expand All @@ -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
}
2 changes: 1 addition & 1 deletion src/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit c2c3b2c

Please sign in to comment.