toolbox/src/cmd/list.go

411 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright © 2019 2021 Red Hat Inc.
*
* 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 cmd
import (
"encoding/json"
"errors"
"fmt"
"os"
"text/tabwriter"
"github.com/containers/toolbox/pkg/podman"
"github.com/containers/toolbox/pkg/utils"
"github.com/mattn/go-isatty"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type toolboxImage struct {
ID string
Names []string
Created string
Labels map[string]string
}
type toolboxContainer struct {
ID string
Names []string
Status string
Created string
Image string
Labels map[string]string
}
var (
listFlags struct {
onlyContainers bool
onlyImages bool
}
// toolboxImageLabels holds labels used by images that mark them as compatible with Toolbox
toolboxImageLabels = map[string]string{
"com.github.debarshiray.toolbox": "true",
"com.github.containers.toolbox": "true",
"com.redhat.component": "ubi8-container",
}
// toolboxContainerLabels holds labels used by container that mark them as compatible with Toolbox
toolboxContainerLabels = map[string]string{
"com.github.debarshiray.toolbox": "true",
"com.github.containrs.toolbox": "true",
}
)
var listCmd = &cobra.Command{
Use: "list",
Short: "List existing toolbox containers and images",
RunE: list,
}
func init() {
flags := listCmd.Flags()
flags.BoolVarP(&listFlags.onlyContainers,
"containers",
"c",
false,
"List only toolbox containers, not images")
flags.BoolVarP(&listFlags.onlyImages,
"images",
"i",
false,
"List only toolbox images, not containers")
listCmd.SetHelpFunc(listHelp)
rootCmd.AddCommand(listCmd)
}
func list(cmd *cobra.Command, args []string) error {
if utils.IsInsideContainer() {
if !utils.IsInsideToolboxContainer() {
return errors.New("this is not a toolbox container")
}
if _, err := utils.ForwardToHost(); err != nil {
return err
}
return nil
}
lsContainers := true
lsImages := true
if !listFlags.onlyContainers && listFlags.onlyImages {
lsContainers = false
} else if listFlags.onlyContainers && !listFlags.onlyImages {
lsImages = false
}
var images []toolboxImage
var containers []toolboxContainer
var err error
if lsImages {
images, err = getImages()
if err != nil {
return err
}
}
if lsContainers {
containers, err = getContainers()
if err != nil {
return err
}
}
listOutput(images, containers)
return nil
}
func getContainers() ([]toolboxContainer, error) {
var err error
var args []string
var containers []map[string]interface{}
var toolboxContainers []toolboxContainer
logrus.Debug("Fetching all containers")
args = []string{"--all", "--sort", "names"}
containers, err = podman.GetContainers(args...)
if err != nil {
return nil, fmt.Errorf("failed to get containers: %w", err)
}
for _, container := range containers {
var c toolboxContainer
var isToolboxContainer bool = false
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
}
for label := range toolboxContainerLabels {
if _, ok := c.Labels[label]; ok {
isToolboxContainer = true
break
}
}
if isToolboxContainer {
toolboxContainers = append(toolboxContainers, c)
}
}
return toolboxContainers, nil
}
func listHelp(cmd *cobra.Command, args []string) {
if utils.IsInsideContainer() {
if !utils.IsInsideToolboxContainer() {
fmt.Fprintf(os.Stderr, "Error: this is not a toolbox container\n")
return
}
if _, err := utils.ForwardToHost(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return
}
return
}
if err := utils.ShowManual("toolbox-list"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return
}
}
func getImages() ([]toolboxImage, error) {
var err error
var args []string
var images []map[string]interface{}
var toolboxImages []toolboxImage
logrus.Debug("Fetching all images")
args = []string{"--sort", "repository"}
images, err = podman.GetImages(args...)
if err != nil {
return nil, fmt.Errorf("failed to get images: %w", err)
}
for _, image := range images {
var i toolboxImage
var isToolboxImage bool = false
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
}
for label := range toolboxImageLabels {
if _, ok := i.Labels[label]; ok {
isToolboxImage = true
break
}
}
if isToolboxImage {
toolboxImages = append(toolboxImages, i)
}
}
return toolboxImages, nil
}
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")
for _, image := range images {
fmt.Fprintf(writer, "%s\t%s\t%s\n",
utils.ShortID(image.ID),
image.Names[0],
image.Created)
}
writer.Flush()
}
if len(images) != 0 && len(containers) != 0 {
fmt.Println()
}
if len(containers) != 0 {
const boldGreenColor = "\033[1;32m"
const defaultColor = "\033[0;00m" // identical to resetColor, but same length as boldGreenColor
const resetColor = "\033[0m"
stdoutFd := os.Stdout.Fd()
writer := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
if isatty.IsTerminal(stdoutFd) {
fmt.Fprintf(writer, "%s", defaultColor)
}
fmt.Fprintf(writer,
"%s\t%s\t%s\t%s\t%s",
"CONTAINER ID",
"CONTAINER NAME",
"CREATED",
"STATUS",
"IMAGE NAME")
if isatty.IsTerminal(stdoutFd) {
fmt.Fprintf(writer, "%s", resetColor)
}
fmt.Fprintf(writer, "\n")
for _, container := range containers {
isRunning := false
if podman.CheckVersion("2.0.0") {
isRunning = container.Status == "running"
}
if isatty.IsTerminal(stdoutFd) {
var color string
if isRunning {
color = boldGreenColor
} else {
color = defaultColor
}
fmt.Fprintf(writer, "%s", color)
}
fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s",
utils.ShortID(container.ID),
container.Names[0],
container.Created,
container.Status,
container.Image)
if isatty.IsTerminal(stdoutFd) {
fmt.Fprintf(writer, "%s", resetColor)
}
fmt.Fprintf(writer, "\n")
}
writer.Flush()
}
}
func (i *toolboxImage) UnmarshalJSON(data []byte) error {
var raw struct {
ID string
Names []string
Created interface{}
Labels map[string]string
}
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))
}
i.Labels = raw.Labels
return nil
}
func (c *toolboxContainer) UnmarshalJSON(data []byte) error {
var raw struct {
ID string
Names interface{}
Status string
State interface{}
Created interface{}
Image string
Labels map[string]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
c.Labels = raw.Labels
return nil
}