411 lines
9.2 KiB
Go
411 lines
9.2 KiB
Go
/*
|
||
* 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
|
||
}
|