Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cli/command/container/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
}
}

if err := command.ValidateOutputPath(dstPath); err != nil {
return err
}

client := dockerCli.Client()
// if client requests to follow symbol link, then must decide target file to be copied
var rebaseName string
Expand Down Expand Up @@ -209,6 +213,11 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
}

// Validate the destination path
if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath)
}

// Ignore any error and assume that the parent directory of the destination
// path exists, in which case the copy may still succeed. If there is any
// type of conflict (e.g., non-directory overwriting an existing directory
Expand Down
9 changes: 9 additions & 0 deletions cli/command/container/cp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,12 @@ func TestSplitCpArg(t *testing.T) {
})
}
}

func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
options := copyOptions{source: "container:/dev/null", destination: "/dev/random"}
cli := test.NewFakeCli(nil)
err := runCopy(cli, options)
assert.Assert(t, err != nil)
expected := `"/dev/random" must be a directory or a regular file`
assert.ErrorContains(t, err, expected)
}
4 changes: 4 additions & 0 deletions cli/command/container/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func runExport(dockerCli command.Cli, opts exportOptions) error {
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
}

if err := command.ValidateOutputPath(opts.output); err != nil {
return errors.Wrap(err, "failed to export container")
}

clnt := dockerCli.Client()

responseBody, err := clnt.ContainerExport(context.Background(), opts.container)
Expand Down
16 changes: 16 additions & 0 deletions cli/command/container/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,19 @@ func TestContainerExportOutputToFile(t *testing.T) {

assert.Assert(t, fs.Equal(dir.Path(), expected))
}

func TestContainerExportOutputToIrregularFile(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
containerExportFunc: func(container string) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("foo")), nil
},
})
cmd := NewExportCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs([]string{"-o", "/dev/random", "container"})

err := cmd.Execute()
assert.Assert(t, err != nil)
expected := `"/dev/random" must be a directory or a regular file`
assert.ErrorContains(t, err, expected)
}
99 changes: 90 additions & 9 deletions cli/command/container/formatter_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package container

import (
"fmt"
"strconv"
"sync"

"github.com/docker/cli/cli/command/formatter"
Expand All @@ -13,11 +14,21 @@ const (
winOSType = "windows"
defaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"
winDefaultStatsTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
autoRangeStatsTableFormat = "table {{.ID}}\t{{.CurrentMemoryMin}}\t{{.CurrentMemoryMax}}\t{{.OptiMemoryMin}}\t{{.OptiMemoryMax}}\t{{.OptiCPUNumber}}\t{{.UsedCPUPerc}}\t{{.OptiCPUTime}}"

currentMemoryMinHeader = "CURRENT MIN"
currentMemoryMaxHeader = "CURRENT MAX"
optiMemoryMinHeader = "OPTI MIN"
optiMemoryMaxHeader = "OPTI MAX"
optiCPUNumberHeader = "OPTI CPU"
usedCPUPercHeader = "USED %"
optiCPUTimeHeader = "OPTI TIME"

containerHeader = "CONTAINER"
cpuPercHeader = "CPU %"
netIOHeader = "NET I/O"
blockIOHeader = "BLOCK I/O"

memPercHeader = "MEM %" // Used only on Linux
winMemUseHeader = "PRIV WORKING SET" // Used only on Windows
memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux
Expand All @@ -26,6 +37,14 @@ const (

// StatsEntry represents represents the statistics data collected from a container
type StatsEntry struct {
CurrentMemoryMin string
CurrentMemoryMax string
OptiMemoryMin string
OptiMemoryMax string
OptiCPUNumber string
UsedCPUPerc string
OptiCPUTime string

Container string
Name string
ID string
Expand Down Expand Up @@ -72,6 +91,14 @@ func (cs *Stats) SetErrorAndReset(err error) {
cs.PidsCurrent = 0
cs.err = err
cs.IsInvalid = true
cs.CurrentMemoryMin = ""
cs.CurrentMemoryMax = ""
cs.OptiMemoryMin = ""
cs.OptiMemoryMax = ""
cs.OptiCPUNumber = ""
cs.UsedCPUPerc = ""
cs.OptiCPUTime = ""

}

// SetError sets container statistics error
Expand Down Expand Up @@ -106,6 +133,8 @@ func NewStatsFormat(source, osType string) formatter.Format {
return formatter.Format(winDefaultStatsTableFormat)
}
return formatter.Format(defaultStatsTableFormat)
} else if source == formatter.AutoRangeFormatKey {
return formatter.Format(autoRangeStatsTableFormat)
}
return formatter.Format(source)
}
Expand Down Expand Up @@ -136,15 +165,22 @@ func statsFormatWrite(ctx formatter.Context, Stats []StatsEntry, osType string,
}
statsCtx := statsContext{}
statsCtx.Header = formatter.SubHeaderContext{
"Container": containerHeader,
"Name": formatter.NameHeader,
"ID": formatter.ContainerIDHeader,
"CPUPerc": cpuPercHeader,
"MemUsage": memUsage,
"MemPerc": memPercHeader,
"NetIO": netIOHeader,
"BlockIO": blockIOHeader,
"PIDs": pidsHeader,
"Container": containerHeader,
"Name": formatter.NameHeader,
"ID": formatter.ContainerIDHeader,
"CPUPerc": cpuPercHeader,
"MemUsage": memUsage,
"MemPerc": memPercHeader,
"NetIO": netIOHeader,
"BlockIO": blockIOHeader,
"PIDs": pidsHeader,
"CurrentMemoryMin": currentMemoryMinHeader,
"CurrentMemoryMax": currentMemoryMaxHeader,
"OptiMemoryMin": optiMemoryMinHeader,
"OptiMemoryMax": optiMemoryMaxHeader,
"OptiCPUNumber": optiCPUNumberHeader,
"UsedCPUPerc": usedCPUPercHeader,
"OptiCPUTime": optiCPUTimeHeader,
}
statsCtx.os = osType
return ctx.Write(&statsCtx, render)
Expand All @@ -157,10 +193,55 @@ type statsContext struct {
trunc bool
}

func dashOrConverted(value string, isInvalid bool) string {
val, err := strconv.ParseFloat(value, 32)
if err != nil || isInvalid {
return "--"
}
return units.BytesSize(val)
}

func (c *statsContext) MarshalJSON() ([]byte, error) {
return formatter.MarshalJSON(c)
}

func (c *statsContext) CurrentMemoryMin() string {
return dashOrConverted(c.s.CurrentMemoryMin, c.s.IsInvalid)
}

func (c *statsContext) CurrentMemoryMax() string {
return dashOrConverted(c.s.CurrentMemoryMax, c.s.IsInvalid)
}

func (c *statsContext) OptiMemoryMin() string {
return dashOrConverted(c.s.OptiMemoryMin, c.s.IsInvalid)
}

func (c *statsContext) OptiMemoryMax() string {
return dashOrConverted(c.s.OptiMemoryMax, c.s.IsInvalid)
}

func (c *statsContext) OptiCPUNumber() string {
if c.s.IsInvalid {
return fmt.Sprintf("--")
}
return c.s.OptiCPUNumber
}

func (c *statsContext) UsedCPUPerc() string {
if c.s.IsInvalid {
return fmt.Sprintf("--")
}
return c.s.UsedCPUPerc
}

func (c *statsContext) OptiCPUTime() string {
if c.s.IsInvalid {
return fmt.Sprintf("--")
}
return c.s.OptiCPUTime
}

func (c *statsContext) Container() string {
return c.s.Container
}
Expand Down
28 changes: 28 additions & 0 deletions cli/command/container/formatter_stats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ func TestContainerStatsContext(t *testing.T) {
{StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs},
{StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs},
{StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs},
{StatsEntry{CurrentMemoryMin: "746123"}, "", "728.6KiB", currentMemoryMinHeader, ctx.CurrentMemoryMin},
{StatsEntry{CurrentMemoryMin: "746123", IsInvalid: true}, "", "--", currentMemoryMinHeader, ctx.CurrentMemoryMin},
{StatsEntry{CurrentMemoryMax: "758123"}, "", "740.4KiB", currentMemoryMaxHeader, ctx.CurrentMemoryMax},
{StatsEntry{CurrentMemoryMax: "758123", IsInvalid: true}, "", "--", currentMemoryMaxHeader, ctx.CurrentMemoryMax},
{StatsEntry{OptiMemoryMin: "600000000"}, "", "572.2MiB", optiMemoryMinHeader, ctx.OptiMemoryMin},
{StatsEntry{OptiMemoryMin: "600000000", IsInvalid: true}, "", "--", optiMemoryMinHeader, ctx.OptiMemoryMin},
{StatsEntry{OptiMemoryMax: "900000000"}, "", "858.3MiB", optiMemoryMaxHeader, ctx.OptiMemoryMax},
{StatsEntry{OptiMemoryMax: "900000000", IsInvalid: true}, "", "--", optiMemoryMaxHeader, ctx.OptiMemoryMax},
{StatsEntry{OptiCPUNumber: "4"}, "", "4", optiCPUNumberHeader, ctx.OptiCPUNumber},
{StatsEntry{OptiCPUNumber: "4", IsInvalid: true}, "", "--", optiCPUNumberHeader, ctx.OptiCPUNumber},
{StatsEntry{OptiCPUTime: "123456"}, "", "123456", optiCPUTimeHeader, ctx.OptiCPUTime},
{StatsEntry{OptiCPUTime: "123456", IsInvalid: true}, "", "--", optiCPUTimeHeader, ctx.OptiCPUTime},
{StatsEntry{UsedCPUPerc: "342%"}, "", "342%", usedCPUPercHeader, ctx.UsedCPUPerc},
{StatsEntry{UsedCPUPerc: "342%", IsInvalid: true}, "", "--", usedCPUPercHeader, ctx.UsedCPUPerc},
}

for _, te := range tt {
Expand Down Expand Up @@ -219,6 +233,20 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
},
"CONTAINER CPU %\n",
},
{
formatter.Context{
Format: "table {{.CurrentMemoryMin}}\t{{.CurrentMemoryMax}}\t{{.OptiMemoryMin}}\t{{.OptiMemoryMax}}",
Output: &out,
},
"CURRENT MIN CURRENT MAX OPTI MIN OPTI MAX\n",
},
{
formatter.Context{
Format: "table {{.OptiCPUNumber}}\t{{.UsedCPUPerc}}\t{{.OptiCPUTime}}",
Output: &out,
},
"OPTI CPU USED % OPTI TIME\n",
},
}

for _, context := range contexts {
Expand Down
18 changes: 17 additions & 1 deletion cli/command/container/stats_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ func collect(ctx context.Context, s *Stats, cli client.APIClient, streamStats bo
var (
v *types.StatsJSON
memPercent, cpuPercent float64
blkRead, blkWrite uint64 // Only used on Linux
mem, memLimit float64
blkRead, blkWrite uint64 // Only used on Linux
pidsStatsCurrent uint64
)

Expand Down Expand Up @@ -126,8 +126,16 @@ func collect(ctx context.Context, s *Stats, cli client.APIClient, streamStats bo
BlockRead: float64(blkRead),
BlockWrite: float64(blkWrite),
PidsCurrent: pidsStatsCurrent,
CurrentMemoryMin: getAutoRangeValue(v.AutoRange["memoryAR"], "nmin"),
CurrentMemoryMax: getAutoRangeValue(v.AutoRange["memoryAR"], "nmax"),
OptiMemoryMin: getAutoRangeValue(v.AutoRange["memoryAR"], "sugmin"),
OptiMemoryMax: getAutoRangeValue(v.AutoRange["memoryAR"], "sugmax"),
OptiCPUNumber: getAutoRangeValue(v.AutoRange["cpuAR"], "numCPU"),
UsedCPUPerc: getAutoRangeValue(v.AutoRange["cpuAR"], "percentOpti"),
OptiCPUTime: getAutoRangeValue(v.AutoRange["cpuAR"], "usageOpti"),
})
u <- nil

if !streamStats {
return
}
Expand Down Expand Up @@ -164,6 +172,14 @@ func collect(ctx context.Context, s *Stats, cli client.APIClient, streamStats bo
}
}

func getAutoRangeValue(array map[string]string, value string) string {
v, exist := array[value]
if !exist {
v = "--"
}
return v
}

func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 {
var (
cpuPercent = 0.0
Expand Down
15 changes: 15 additions & 0 deletions cli/command/container/stats_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ func TestCalculateMemUsageUnixNoCache(t *testing.T) {
assert.Assert(t, inDelta(100.0, result, 1e-6))
}

func TestGetAutoRangeValue(t *testing.T) {
testMap := map[string]string{
"testTrue": "true",
"testFalse": "false",
}
testValues := []string{"testTrue", "testFalse", "test"}

results := []string{"true", "false", "--"}

for index := range testValues {
v := getAutoRangeValue(testMap, testValues[index])
assert.Assert(t, v == results[index])
}
}

func TestCalculateMemPercentUnixNoCache(t *testing.T) {
// Given
someLimit := float64(100.0)
Expand Down
7 changes: 4 additions & 3 deletions cli/command/formatter/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import (

// Format keys used to specify certain kinds of output formats
const (
TableFormatKey = "table"
RawFormatKey = "raw"
PrettyFormatKey = "pretty"
TableFormatKey = "table"
RawFormatKey = "raw"
PrettyFormatKey = "pretty"
AutoRangeFormatKey = "autorange"

DefaultQuietFormat = "{{.ID}}"
)
Expand Down
14 changes: 1 addition & 13 deletions cli/command/image/save.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package image
import (
"context"
"io"
"os"
"path/filepath"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
Expand Down Expand Up @@ -44,7 +42,7 @@ func RunSave(dockerCli command.Cli, opts saveOptions) error {
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
}

if err := validateOutputPath(opts.output); err != nil {
if err := command.ValidateOutputPath(opts.output); err != nil {
return errors.Wrap(err, "failed to save image")
}

Expand All @@ -61,13 +59,3 @@ func RunSave(dockerCli command.Cli, opts saveOptions) error {

return command.CopyToFile(opts.output, responseBody)
}

func validateOutputPath(path string) error {
dir := filepath.Dir(path)
if dir != "" && dir != "." {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return errors.Errorf("unable to validate output path: directory %q does not exist", dir)
}
}
return nil
}
7 changes: 6 additions & 1 deletion cli/command/image/save_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ func TestNewSaveCommandErrors(t *testing.T) {
{
name: "output directory does not exist",
args: []string{"-o", "fakedir/out.tar", "arg1"},
expectedError: "failed to save image: unable to validate output path: directory \"fakedir\" does not exist",
expectedError: "failed to save image: invalid output path: directory \"fakedir\" does not exist",
},
{
name: "output file is irregular",
args: []string{"-o", "/dev/null", "arg1"},
expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file",
},
}
for _, tc := range testCases {
Expand Down
Loading