Merge component 'engine' from git@github.com:moby/moby master
This commit is contained in:
@@ -6173,8 +6173,19 @@ paths:
|
||||
type: "integer"
|
||||
- name: "buildargs"
|
||||
in: "query"
|
||||
description: "JSON map of string pairs for build-time variables. Users pass these values at build-time. Docker uses the buildargs as the environment context for commands run via the `Dockerfile` RUN instruction, or for variable expansion in other `Dockerfile` instructions. This is not meant for passing secret values. [Read more about the buildargs instruction.](https://docs.docker.com/engine/reference/builder/#arg)"
|
||||
type: "integer"
|
||||
description: >
|
||||
JSON map of string pairs for build-time variables. Users pass these values at build-time. Docker
|
||||
uses the buildargs as the environment context for commands run via the `Dockerfile` RUN
|
||||
instruction, or for variable expansion in other `Dockerfile` instructions. This is not meant for
|
||||
passing secret values.
|
||||
|
||||
|
||||
For example, the build arg `FOO=bar` would become `{"FOO":"bar"}` in JSON. This would result in the
|
||||
the query parameter `buildargs={"FOO":"bar"}`. Note that `{"FOO":"bar"}` should be URI component encoded.
|
||||
|
||||
|
||||
[Read more about the buildargs instruction.](https://docs.docker.com/engine/reference/builder/#arg)
|
||||
type: "string"
|
||||
- name: "shmsize"
|
||||
in: "query"
|
||||
description: "Size of `/dev/shm` in bytes. The size must be greater than 0. If omitted the system uses 64MB."
|
||||
|
||||
@@ -133,23 +133,15 @@ func FromEnv(c *Client) error {
|
||||
},
|
||||
CheckRedirect: CheckRedirect,
|
||||
}
|
||||
WithHTTPClient(httpClient)(c)
|
||||
}
|
||||
|
||||
host := os.Getenv("DOCKER_HOST")
|
||||
if host != "" {
|
||||
var err error
|
||||
// WithHost will create an API client if it doesn't exist
|
||||
if err := WithHost(host)(c); err != nil {
|
||||
return err
|
||||
}
|
||||
httpClient, err = defaultHTTPClient(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if httpClient != nil {
|
||||
if err := WithHTTPClient(httpClient)(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
version := os.Getenv("DOCKER_API_VERSION")
|
||||
if version != "" {
|
||||
@@ -167,7 +159,8 @@ func WithVersion(version string) func(*Client) error {
|
||||
}
|
||||
}
|
||||
|
||||
// WithHost overrides the client host with the specified one
|
||||
// WithHost overrides the client host with the specified one, creating a new
|
||||
// http client if one doesn't exist
|
||||
func WithHost(host string) func(*Client) error {
|
||||
return func(c *Client) error {
|
||||
hostURL, err := ParseHostURL(host)
|
||||
@@ -178,11 +171,17 @@ func WithHost(host string) func(*Client) error {
|
||||
c.proto = hostURL.Scheme
|
||||
c.addr = hostURL.Host
|
||||
c.basePath = hostURL.Path
|
||||
client, err := defaultHTTPClient(host)
|
||||
if err != nil {
|
||||
return err
|
||||
if c.client == nil {
|
||||
client, err := defaultHTTPClient(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WithHTTPClient(client)(c)
|
||||
}
|
||||
return WithHTTPClient(client)(c)
|
||||
if transport, ok := c.client.Transport.(*http.Transport); ok {
|
||||
return sockets.ConfigureTransport(transport, c.proto, c.addr)
|
||||
}
|
||||
return fmt.Errorf("cannot apply host to http transport")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func (s *DockerSuite) TestContainersAPICreateMountsBindNamedPipe(c *check.C) {
|
||||
testRequires(c, SameHostDaemon, DaemonIsWindowsAtLeastBuild(16210)) // Named pipe support was added in RS3
|
||||
testRequires(c, SameHostDaemon, DaemonIsWindowsAtLeastBuild(16299)) // Named pipe support was added in RS3
|
||||
|
||||
// Create a host pipe to map into the container
|
||||
hostPipeName := fmt.Sprintf(`\\.\pipe\docker-cli-test-pipe-%x`, rand.Uint64())
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/integration-cli/checker"
|
||||
"github.com/go-check/check"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmConfigsEmptyList(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
configs := d.ListConfigs(c)
|
||||
c.Assert(configs, checker.NotNil)
|
||||
c.Assert(len(configs), checker.Equals, 0, check.Commentf("configs: %#v", configs))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmConfigsCreate(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
configs := d.ListConfigs(c)
|
||||
c.Assert(len(configs), checker.Equals, 1, check.Commentf("configs: %#v", configs))
|
||||
name := configs[0].Spec.Annotations.Name
|
||||
c.Assert(name, checker.Equals, testName, check.Commentf("configs: %s", name))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmConfigsDelete(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
config := d.GetConfig(c, id)
|
||||
c.Assert(config.ID, checker.Equals, id, check.Commentf("config: %v", config))
|
||||
|
||||
d.DeleteConfig(c, config.ID)
|
||||
|
||||
cli, err := d.NewClient()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cli.Close()
|
||||
|
||||
_, _, err = cli.ConfigInspectWithRaw(context.Background(), id)
|
||||
c.Assert(err.Error(), checker.Contains, "No such config")
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmConfigsUpdate(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_config"
|
||||
id := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
Labels: map[string]string{
|
||||
"test": "test1",
|
||||
},
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id))
|
||||
|
||||
config := d.GetConfig(c, id)
|
||||
c.Assert(config.ID, checker.Equals, id, check.Commentf("config: %v", config))
|
||||
|
||||
// test UpdateConfig with full ID
|
||||
d.UpdateConfig(c, id, func(s *swarm.Config) {
|
||||
s.Spec.Labels = map[string]string{
|
||||
"test": "test1",
|
||||
}
|
||||
})
|
||||
|
||||
config = d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Labels["test"], checker.Equals, "test1", check.Commentf("config: %v", config))
|
||||
|
||||
// test UpdateConfig with full name
|
||||
d.UpdateConfig(c, config.Spec.Name, func(s *swarm.Config) {
|
||||
s.Spec.Labels = map[string]string{
|
||||
"test": "test2",
|
||||
}
|
||||
})
|
||||
|
||||
config = d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Labels["test"], checker.Equals, "test2", check.Commentf("config: %v", config))
|
||||
|
||||
// test UpdateConfig with prefix ID
|
||||
d.UpdateConfig(c, id[:1], func(s *swarm.Config) {
|
||||
s.Spec.Labels = map[string]string{
|
||||
"test": "test3",
|
||||
}
|
||||
})
|
||||
|
||||
config = d.GetConfig(c, id)
|
||||
c.Assert(config.Spec.Labels["test"], checker.Equals, "test3", check.Commentf("config: %v", config))
|
||||
|
||||
// test UpdateConfig in updating Data which is not supported in daemon
|
||||
// this test will produce an error in func UpdateConfig
|
||||
config = d.GetConfig(c, id)
|
||||
config.Spec.Data = []byte("TESTINGDATA2")
|
||||
|
||||
cli, err := d.NewClient()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cli.Close()
|
||||
|
||||
expected := "only updates to Labels are allowed"
|
||||
|
||||
err = cli.ConfigUpdate(context.Background(), config.ID, config.Version, config.Spec)
|
||||
c.Assert(err.Error(), checker.Contains, expected)
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/integration-cli/checker"
|
||||
"github.com/go-check/check"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmSecretsEmptyList(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
secrets := d.ListSecrets(c)
|
||||
c.Assert(secrets, checker.NotNil)
|
||||
c.Assert(len(secrets), checker.Equals, 0, check.Commentf("secrets: %#v", secrets))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmSecretsCreate(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_secret"
|
||||
secretSpec := swarm.SecretSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
}
|
||||
|
||||
id := d.CreateSecret(c, secretSpec)
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
|
||||
secrets := d.ListSecrets(c)
|
||||
c.Assert(len(secrets), checker.Equals, 1, check.Commentf("secrets: %#v", secrets))
|
||||
name := secrets[0].Spec.Annotations.Name
|
||||
c.Assert(name, checker.Equals, testName, check.Commentf("secret: %s", name))
|
||||
|
||||
// create an already existing secret, daemon should return a status code of 409
|
||||
status, out, err := d.SockRequest("POST", "/secrets/create", secretSpec)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(status, checker.Equals, http.StatusConflict, check.Commentf("secret create: %s", string(out)))
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmSecretsDelete(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_secret"
|
||||
id := d.CreateSecret(c, swarm.SecretSpec{Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
|
||||
secret := d.GetSecret(c, id)
|
||||
c.Assert(secret.ID, checker.Equals, id, check.Commentf("secret: %v", secret))
|
||||
|
||||
d.DeleteSecret(c, secret.ID)
|
||||
|
||||
cli, err := d.NewClient()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cli.Close()
|
||||
|
||||
_, _, err = cli.SecretInspectWithRaw(context.Background(), id)
|
||||
c.Assert(err.Error(), checker.Contains, "No such secret")
|
||||
|
||||
id = "non-existing"
|
||||
err = cli.SecretRemove(context.Background(), id)
|
||||
c.Assert(err.Error(), checker.Contains, "No such secret: non-existing")
|
||||
}
|
||||
|
||||
func (s *DockerSwarmSuite) TestAPISwarmSecretsUpdate(c *check.C) {
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName := "test_secret"
|
||||
id := d.CreateSecret(c, swarm.SecretSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName,
|
||||
Labels: map[string]string{
|
||||
"test": "test1",
|
||||
},
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
c.Assert(id, checker.Not(checker.Equals), "", check.Commentf("secrets: %s", id))
|
||||
|
||||
secret := d.GetSecret(c, id)
|
||||
c.Assert(secret.ID, checker.Equals, id, check.Commentf("secret: %v", secret))
|
||||
|
||||
// test UpdateSecret with full ID
|
||||
d.UpdateSecret(c, id, func(s *swarm.Secret) {
|
||||
s.Spec.Labels = map[string]string{
|
||||
"test": "test1",
|
||||
}
|
||||
})
|
||||
|
||||
secret = d.GetSecret(c, id)
|
||||
c.Assert(secret.Spec.Labels["test"], checker.Equals, "test1", check.Commentf("secret: %v", secret))
|
||||
|
||||
// test UpdateSecret with full name
|
||||
d.UpdateSecret(c, secret.Spec.Name, func(s *swarm.Secret) {
|
||||
s.Spec.Labels = map[string]string{
|
||||
"test": "test2",
|
||||
}
|
||||
})
|
||||
|
||||
secret = d.GetSecret(c, id)
|
||||
c.Assert(secret.Spec.Labels["test"], checker.Equals, "test2", check.Commentf("secret: %v", secret))
|
||||
|
||||
// test UpdateSecret with prefix ID
|
||||
d.UpdateSecret(c, id[:1], func(s *swarm.Secret) {
|
||||
s.Spec.Labels = map[string]string{
|
||||
"test": "test3",
|
||||
}
|
||||
})
|
||||
|
||||
secret = d.GetSecret(c, id)
|
||||
c.Assert(secret.Spec.Labels["test"], checker.Equals, "test3", check.Commentf("secret: %v", secret))
|
||||
|
||||
// test UpdateSecret in updating Data which is not supported in daemon
|
||||
// this test will produce an error in func UpdateSecret
|
||||
secret = d.GetSecret(c, id)
|
||||
secret.Spec.Data = []byte("TESTINGDATA2")
|
||||
|
||||
cli, err := d.NewClient()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cli.Close()
|
||||
|
||||
expected := "only updates to Labels are allowed"
|
||||
|
||||
err = cli.SecretUpdate(context.Background(), secret.ID, secret.Version, secret.Spec)
|
||||
c.Assert(err.Error(), checker.Contains, expected)
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/integration-cli/checker"
|
||||
"github.com/go-check/check"
|
||||
)
|
||||
|
||||
func (s *DockerSwarmSuite) TestConfigList(c *check.C) {
|
||||
testRequires(c, SameHostDaemon)
|
||||
d := s.AddDaemon(c, true, true)
|
||||
|
||||
testName0 := "test0"
|
||||
testName1 := "test1"
|
||||
|
||||
// create config test0
|
||||
id0 := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName0,
|
||||
Labels: map[string]string{"type": "test"},
|
||||
},
|
||||
Data: []byte("TESTINGDATA0"),
|
||||
})
|
||||
c.Assert(id0, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id0))
|
||||
|
||||
config := d.GetConfig(c, id0)
|
||||
c.Assert(config.Spec.Name, checker.Equals, testName0)
|
||||
|
||||
// create config test1
|
||||
id1 := d.CreateConfig(c, swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: testName1,
|
||||
Labels: map[string]string{"type": "production"},
|
||||
},
|
||||
Data: []byte("TESTINGDATA1"),
|
||||
})
|
||||
c.Assert(id1, checker.Not(checker.Equals), "", check.Commentf("configs: %s", id1))
|
||||
|
||||
config = d.GetConfig(c, id1)
|
||||
c.Assert(config.Spec.Name, checker.Equals, testName1)
|
||||
|
||||
// test by command `docker config ls`
|
||||
out, err := d.Cmd("config", "ls")
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
|
||||
|
||||
// test filter by name `docker config ls --filter name=xxx`
|
||||
args := []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"name=test0",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName1)
|
||||
|
||||
// test filter by id `docker config ls --filter id=xxx`
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"id=" + id1,
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
|
||||
|
||||
// test filter by label `docker config ls --filter label=xxx`
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"label=type",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
|
||||
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"label=type=test",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName1)
|
||||
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"label=type=production",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.IsNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Not(checker.Contains), testName0)
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, testName1)
|
||||
|
||||
// test invalid filter `docker config ls --filter noexisttype=xxx`
|
||||
args = []string{
|
||||
"config",
|
||||
"ls",
|
||||
"--filter",
|
||||
"noexisttype=test0",
|
||||
}
|
||||
out, err = d.Cmd(args...)
|
||||
c.Assert(err, checker.NotNil, check.Commentf(out))
|
||||
|
||||
c.Assert(strings.TrimSpace(out), checker.Contains, "Error response from daemon: Invalid filter 'noexisttype'")
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/integration-cli/checker"
|
||||
"github.com/go-check/check"
|
||||
)
|
||||
|
||||
func (s *DockerSuite) TestInspectOomKilledTrue(c *check.C) {
|
||||
testRequires(c, DaemonIsLinux, memoryLimitSupport, swapMemorySupport)
|
||||
|
||||
name := "testoomkilled"
|
||||
_, exitCode, _ := dockerCmdWithError("run", "--name", name, "--memory", "32MB", "busybox", "sh", "-c", "x=a; while true; do x=$x$x$x$x; done")
|
||||
|
||||
c.Assert(exitCode, checker.Equals, 137, check.Commentf("OOM exit should be 137"))
|
||||
|
||||
oomKilled := inspectField(c, name, "State.OOMKilled")
|
||||
c.Assert(oomKilled, checker.Equals, "true")
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TestInspectOomKilledFalse(c *check.C) {
|
||||
testRequires(c, DaemonIsLinux, memoryLimitSupport, swapMemorySupport)
|
||||
|
||||
name := "testoomkilled"
|
||||
dockerCmd(c, "run", "--name", name, "--memory", "32MB", "busybox", "sh", "-c", "echo hello world")
|
||||
|
||||
oomKilled := inspectField(c, name, "State.OOMKilled")
|
||||
c.Assert(oomKilled, checker.Equals, "false")
|
||||
}
|
||||
@@ -4002,7 +4002,7 @@ func (s *DockerSuite) TestRunAttachFailedNoLeak(c *check.C) {
|
||||
v, err := kernel.GetKernelVersion()
|
||||
c.Assert(err, checker.IsNil)
|
||||
build, _ := strconv.Atoi(strings.Split(strings.SplitN(v.String(), " ", 3)[2][1:], ".")[0])
|
||||
if build >= 16292 { // @jhowardmsft TODO - replace with final RS3 build and ==
|
||||
if build == 16299 {
|
||||
c.Skip("Temporarily disabled on RS3 builds")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/integration-cli/checker"
|
||||
"github.com/docker/docker/integration-cli/cli"
|
||||
"github.com/go-check/check"
|
||||
"github.com/gotestyourself/gotestyourself/icmd"
|
||||
)
|
||||
|
||||
func (s *DockerSuite) TestUpdateRestartPolicy(c *check.C) {
|
||||
out := cli.DockerCmd(c, "run", "-d", "--restart=on-failure:3", "busybox", "sh", "-c", "sleep 1 && false").Combined()
|
||||
timeout := 60 * time.Second
|
||||
if testEnv.OSType == "windows" {
|
||||
timeout = 180 * time.Second
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(string(out))
|
||||
|
||||
// update restart policy to on-failure:5
|
||||
cli.DockerCmd(c, "update", "--restart=on-failure:5", id)
|
||||
|
||||
cli.WaitExited(c, id, timeout)
|
||||
|
||||
count := inspectField(c, id, "RestartCount")
|
||||
c.Assert(count, checker.Equals, "5")
|
||||
|
||||
maximumRetryCount := inspectField(c, id, "HostConfig.RestartPolicy.MaximumRetryCount")
|
||||
c.Assert(maximumRetryCount, checker.Equals, "5")
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TestUpdateRestartWithAutoRemoveFlag(c *check.C) {
|
||||
out := runSleepingContainer(c, "--rm")
|
||||
id := strings.TrimSpace(out)
|
||||
|
||||
// update restart policy for an AutoRemove container
|
||||
cli.Docker(cli.Args("update", "--restart=always", id)).Assert(c, icmd.Expected{
|
||||
ExitCode: 1,
|
||||
Err: "Restart policy cannot be updated because AutoRemove is enabled for the container",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/integration/internal/swarm"
|
||||
"github.com/docker/docker/internal/testutil"
|
||||
"github.com/gotestyourself/gotestyourself/skip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestConfigList(t *testing.T) {
|
||||
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
|
||||
|
||||
defer setupTest(t)()
|
||||
d := swarm.NewSwarm(t, testEnv)
|
||||
defer d.Stop(t)
|
||||
client, err := client.NewClientWithOpts(client.WithHost((d.Sock())))
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// This test case is ported from the original TestConfigsEmptyList
|
||||
configs, err := client.ConfigList(ctx, types.ConfigListOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(configs), 0)
|
||||
|
||||
testName0 := "test0"
|
||||
testName1 := "test1"
|
||||
testNames := []string{testName0, testName1}
|
||||
sort.Strings(testNames)
|
||||
|
||||
// create config test0
|
||||
createConfig(ctx, t, client, testName0, []byte("TESTINGDATA0"), map[string]string{"type": "test"})
|
||||
|
||||
config1ID := createConfig(ctx, t, client, testName1, []byte("TESTINGDATA1"), map[string]string{"type": "production"})
|
||||
|
||||
names := func(entries []swarmtypes.Config) []string {
|
||||
values := []string{}
|
||||
for _, entry := range entries {
|
||||
values = append(values, entry.Spec.Name)
|
||||
}
|
||||
sort.Strings(values)
|
||||
return values
|
||||
}
|
||||
|
||||
// test by `config ls`
|
||||
entries, err := client.ConfigList(ctx, types.ConfigListOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, names(entries), testNames)
|
||||
|
||||
testCases := []struct {
|
||||
filters filters.Args
|
||||
expected []string
|
||||
}{
|
||||
// test filter by name `config ls --filter name=xxx`
|
||||
{
|
||||
filters: filters.NewArgs(filters.Arg("name", testName0)),
|
||||
expected: []string{testName0},
|
||||
},
|
||||
// test filter by id `config ls --filter id=xxx`
|
||||
{
|
||||
filters: filters.NewArgs(filters.Arg("id", config1ID)),
|
||||
expected: []string{testName1},
|
||||
},
|
||||
// test filter by label `config ls --filter label=xxx`
|
||||
{
|
||||
filters: filters.NewArgs(filters.Arg("label", "type")),
|
||||
expected: testNames,
|
||||
},
|
||||
{
|
||||
filters: filters.NewArgs(filters.Arg("label", "type=test")),
|
||||
expected: []string{testName0},
|
||||
},
|
||||
{
|
||||
filters: filters.NewArgs(filters.Arg("label", "type=production")),
|
||||
expected: []string{testName1},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
entries, err = client.ConfigList(ctx, types.ConfigListOptions{
|
||||
Filters: tc.filters,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, names(entries), tc.expected)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func createConfig(ctx context.Context, t *testing.T, client client.APIClient, name string, data []byte, labels map[string]string) string {
|
||||
config, err := client.ConfigCreate(ctx, swarmtypes.ConfigSpec{
|
||||
Annotations: swarmtypes.Annotations{
|
||||
Name: name,
|
||||
Labels: labels,
|
||||
},
|
||||
Data: data,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, config.ID, "")
|
||||
return config.ID
|
||||
}
|
||||
|
||||
func TestConfigsCreateAndDelete(t *testing.T) {
|
||||
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
|
||||
|
||||
defer setupTest(t)()
|
||||
d := swarm.NewSwarm(t, testEnv)
|
||||
defer d.Stop(t)
|
||||
client, err := client.NewClientWithOpts(client.WithHost((d.Sock())))
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testName := "test_config"
|
||||
|
||||
// This test case is ported from the original TestConfigsCreate
|
||||
configID := createConfig(ctx, t, client, testName, []byte("TESTINGDATA"), nil)
|
||||
|
||||
insp, _, err := client.ConfigInspectWithRaw(ctx, configID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.Spec.Name, testName)
|
||||
|
||||
// This test case is ported from the original TestConfigsDelete
|
||||
err = client.ConfigRemove(ctx, configID)
|
||||
require.NoError(t, err)
|
||||
|
||||
insp, _, err = client.ConfigInspectWithRaw(ctx, configID)
|
||||
testutil.ErrorContains(t, err, "No such config")
|
||||
}
|
||||
|
||||
func TestConfigsUpdate(t *testing.T) {
|
||||
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
|
||||
|
||||
defer setupTest(t)()
|
||||
d := swarm.NewSwarm(t, testEnv)
|
||||
defer d.Stop(t)
|
||||
client, err := client.NewClientWithOpts(client.WithHost((d.Sock())))
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testName := "test_config"
|
||||
|
||||
// This test case is ported from the original TestConfigsCreate
|
||||
configID := createConfig(ctx, t, client, testName, []byte("TESTINGDATA"), nil)
|
||||
|
||||
insp, _, err := client.ConfigInspectWithRaw(ctx, configID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.ID, configID)
|
||||
|
||||
// test UpdateConfig with full ID
|
||||
insp.Spec.Labels = map[string]string{"test": "test1"}
|
||||
err = client.ConfigUpdate(ctx, configID, insp.Version, insp.Spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
insp, _, err = client.ConfigInspectWithRaw(ctx, configID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.Spec.Labels["test"], "test1")
|
||||
|
||||
// test UpdateConfig with full name
|
||||
insp.Spec.Labels = map[string]string{"test": "test2"}
|
||||
err = client.ConfigUpdate(ctx, testName, insp.Version, insp.Spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
insp, _, err = client.ConfigInspectWithRaw(ctx, configID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.Spec.Labels["test"], "test2")
|
||||
|
||||
// test UpdateConfig with prefix ID
|
||||
insp.Spec.Labels = map[string]string{"test": "test3"}
|
||||
err = client.ConfigUpdate(ctx, configID[:1], insp.Version, insp.Spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
insp, _, err = client.ConfigInspectWithRaw(ctx, configID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.Spec.Labels["test"], "test3")
|
||||
|
||||
// test UpdateConfig in updating Data which is not supported in daemon
|
||||
// this test will produce an error in func UpdateConfig
|
||||
insp.Spec.Data = []byte("TESTINGDATA2")
|
||||
err = client.ConfigUpdate(ctx, configID, insp.Version, insp.Spec)
|
||||
testutil.ErrorContains(t, err, "only updates to Labels are allowed")
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/internal/test/environment"
|
||||
)
|
||||
|
||||
var testEnv *environment.Execution
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var err error
|
||||
testEnv, err = environment.New()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
err = environment.EnsureFrozenImagesLinux(testEnv)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
testEnv.Print()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func setupTest(t *testing.T) func() {
|
||||
environment.ProtectAll(t, testEnv)
|
||||
return func() { testEnv.Clean(t) }
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/docker/docker/integration/internal/request"
|
||||
"github.com/gotestyourself/gotestyourself/poll"
|
||||
"github.com/gotestyourself/gotestyourself/skip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -146,3 +147,39 @@ func TestKillDifferentUserContainer(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
poll.WaitOn(t, containerIsInState(ctx, client, id, "exited"), poll.WithDelay(100*time.Millisecond))
|
||||
}
|
||||
|
||||
func TestInspectOomKilledTrue(t *testing.T) {
|
||||
skip.If(t, testEnv.DaemonInfo.OSType != "linux" || !testEnv.DaemonInfo.MemoryLimit || !testEnv.DaemonInfo.SwapLimit)
|
||||
|
||||
defer setupTest(t)()
|
||||
ctx := context.Background()
|
||||
client := request.NewAPIClient(t)
|
||||
|
||||
name := "testoomkilled"
|
||||
cID := container.Run(t, ctx, client, container.WithName(name), container.WithCmd("sh", "-c", "x=a; while true; do x=$x$x$x$x; done"), func(c *container.TestContainerConfig) {
|
||||
c.HostConfig.Resources.Memory = 32 * 1024 * 1024
|
||||
})
|
||||
|
||||
poll.WaitOn(t, containerIsInState(ctx, client, cID, "exited"), poll.WithDelay(100*time.Millisecond))
|
||||
|
||||
inspect, err := client.ContainerInspect(ctx, cID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, inspect.State.OOMKilled, true)
|
||||
}
|
||||
|
||||
func TestInspectOomKilledFalse(t *testing.T) {
|
||||
skip.If(t, testEnv.DaemonInfo.OSType != "linux" || !testEnv.DaemonInfo.MemoryLimit || !testEnv.DaemonInfo.SwapLimit)
|
||||
|
||||
defer setupTest(t)()
|
||||
ctx := context.Background()
|
||||
client := request.NewAPIClient(t)
|
||||
|
||||
name := "testoomkilled"
|
||||
cID := container.Run(t, ctx, client, container.WithName(name), container.WithCmd("sh", "-c", "echo hello world"))
|
||||
|
||||
poll.WaitOn(t, containerIsInState(ctx, client, cID, "exited"), poll.WithDelay(100*time.Millisecond))
|
||||
|
||||
inspect, err := client.ContainerInspect(ctx, cID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, inspect.State.OOMKilled, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package container // import "github.com/docker/docker/integration/container"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/integration/internal/container"
|
||||
"github.com/docker/docker/integration/internal/request"
|
||||
"github.com/docker/docker/internal/testutil"
|
||||
"github.com/gotestyourself/gotestyourself/poll"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdateRestartPolicy(t *testing.T) {
|
||||
defer setupTest(t)()
|
||||
client := request.NewAPIClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
cID := container.Run(t, ctx, client, container.WithCmd("sh", "-c", "sleep 1 && false"), func(c *container.TestContainerConfig) {
|
||||
c.HostConfig.RestartPolicy = containertypes.RestartPolicy{
|
||||
Name: "on-failure",
|
||||
MaximumRetryCount: 3,
|
||||
}
|
||||
})
|
||||
|
||||
_, err := client.ContainerUpdate(ctx, cID, containertypes.UpdateConfig{
|
||||
RestartPolicy: containertypes.RestartPolicy{
|
||||
Name: "on-failure",
|
||||
MaximumRetryCount: 5,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
timeout := 60 * time.Second
|
||||
if testEnv.OSType == "windows" {
|
||||
timeout = 180 * time.Second
|
||||
}
|
||||
|
||||
poll.WaitOn(t, containerIsInState(ctx, client, cID, "exited"), poll.WithDelay(100*time.Millisecond), poll.WithTimeout(timeout))
|
||||
|
||||
inspect, err := client.ContainerInspect(ctx, cID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, inspect.RestartCount, 5)
|
||||
assert.Equal(t, inspect.HostConfig.RestartPolicy.MaximumRetryCount, 5)
|
||||
}
|
||||
|
||||
func TestUpdateRestartWithAutoRemove(t *testing.T) {
|
||||
defer setupTest(t)()
|
||||
client := request.NewAPIClient(t)
|
||||
ctx := context.Background()
|
||||
|
||||
cID := container.Run(t, ctx, client, func(c *container.TestContainerConfig) {
|
||||
c.HostConfig.AutoRemove = true
|
||||
})
|
||||
|
||||
_, err := client.ContainerUpdate(ctx, cID, containertypes.UpdateConfig{
|
||||
RestartPolicy: containertypes.RestartPolicy{
|
||||
Name: "always",
|
||||
},
|
||||
})
|
||||
testutil.ErrorContains(t, err, "Restart policy cannot be updated because AutoRemove is enabled for the container")
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
swarmtypes "github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/integration/internal/swarm"
|
||||
"github.com/docker/docker/internal/testutil"
|
||||
"github.com/gotestyourself/gotestyourself/skip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -35,7 +36,7 @@ func TestSecretInspect(t *testing.T) {
|
||||
|
||||
secret, _, err = client.SecretInspectWithRaw(context.Background(), testName)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, secret.ID, secretID)
|
||||
assert.Equal(t, secretID, secretID)
|
||||
}
|
||||
|
||||
func TestSecretList(t *testing.T) {
|
||||
@@ -124,3 +125,110 @@ func createSecret(ctx context.Context, t *testing.T, client client.APIClient, na
|
||||
assert.NotEqual(t, secret.ID, "")
|
||||
return secret.ID
|
||||
}
|
||||
|
||||
func TestSecretsCreate(t *testing.T) {
|
||||
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
|
||||
|
||||
defer setupTest(t)()
|
||||
d := swarm.NewSwarm(t, testEnv)
|
||||
defer d.Stop(t)
|
||||
client, err := client.NewClientWithOpts(client.WithHost((d.Sock())))
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testName := "test_secret"
|
||||
createSecret(ctx, t, client, testName, []byte("TESTINGDATA"), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// create an already existin secret, daemon should return a status code of 409
|
||||
_, err = client.SecretCreate(ctx, swarmtypes.SecretSpec{
|
||||
Annotations: swarmtypes.Annotations{
|
||||
Name: testName,
|
||||
},
|
||||
Data: []byte("TESTINGDATA"),
|
||||
})
|
||||
testutil.ErrorContains(t, err, "already exists")
|
||||
}
|
||||
|
||||
func TestSecretsDelete(t *testing.T) {
|
||||
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
|
||||
|
||||
defer setupTest(t)()
|
||||
d := swarm.NewSwarm(t, testEnv)
|
||||
defer d.Stop(t)
|
||||
client, err := client.NewClientWithOpts(client.WithHost((d.Sock())))
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testName := "test_secret"
|
||||
secretID := createSecret(ctx, t, client, testName, []byte("TESTINGDATA"), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
insp, _, err := client.SecretInspectWithRaw(ctx, secretID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.ID, secretID)
|
||||
|
||||
err = client.SecretRemove(ctx, secretID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = client.SecretInspectWithRaw(ctx, secretID)
|
||||
testutil.ErrorContains(t, err, "No such secret")
|
||||
|
||||
err = client.SecretRemove(ctx, "non-existin")
|
||||
testutil.ErrorContains(t, err, "No such secret: non-existin")
|
||||
}
|
||||
|
||||
func TestSecretsUpdate(t *testing.T) {
|
||||
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
|
||||
|
||||
defer setupTest(t)()
|
||||
d := swarm.NewSwarm(t, testEnv)
|
||||
defer d.Stop(t)
|
||||
client, err := client.NewClientWithOpts(client.WithHost((d.Sock())))
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testName := "test_secret"
|
||||
secretID := createSecret(ctx, t, client, testName, []byte("TESTINGDATA"), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
insp, _, err := client.SecretInspectWithRaw(ctx, secretID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.ID, secretID)
|
||||
|
||||
// test UpdateSecret with full ID
|
||||
insp.Spec.Labels = map[string]string{"test": "test1"}
|
||||
err = client.SecretUpdate(ctx, secretID, insp.Version, insp.Spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
insp, _, err = client.SecretInspectWithRaw(ctx, secretID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.Spec.Labels["test"], "test1")
|
||||
|
||||
// test UpdateSecret with full name
|
||||
insp.Spec.Labels = map[string]string{"test": "test2"}
|
||||
err = client.SecretUpdate(ctx, testName, insp.Version, insp.Spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
insp, _, err = client.SecretInspectWithRaw(ctx, secretID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.Spec.Labels["test"], "test2")
|
||||
|
||||
// test UpdateSecret with prefix ID
|
||||
insp.Spec.Labels = map[string]string{"test": "test3"}
|
||||
err = client.SecretUpdate(ctx, secretID[:1], insp.Version, insp.Spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
insp, _, err = client.SecretInspectWithRaw(ctx, secretID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, insp.Spec.Labels["test"], "test3")
|
||||
|
||||
// test UpdateSecret in updating Data which is not supported in daemon
|
||||
// this test will produce an error in func UpdateSecret
|
||||
insp.Spec.Data = []byte("TESTINGDATA2")
|
||||
err = client.SecretUpdate(ctx, secretID, insp.Version, insp.Spec)
|
||||
testutil.ErrorContains(t, err, "only updates to Labels are allowed")
|
||||
}
|
||||
|
||||
@@ -223,7 +223,7 @@ func (c *client) createWindows(id string, spec *specs.Spec, runtimeOptions inter
|
||||
|
||||
if configuration.HvPartition {
|
||||
// We don't currently support setting the utility VM image explicitly.
|
||||
// TODO @swernli/jhowardmsft circa RS3/4, this may be re-locatable.
|
||||
// TODO @swernli/jhowardmsft circa RS5, this may be re-locatable.
|
||||
if spec.Windows.HyperV.UtilityVMPath != "" {
|
||||
return errors.New("runtime does not support an explicit utility VM path for Hyper-V containers")
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func (c *client) createWindows(id string, spec *specs.Spec, runtimeOptions inter
|
||||
}
|
||||
}
|
||||
configuration.MappedDirectories = mds
|
||||
if len(mps) > 0 && system.GetOSVersion().Build < 16210 { // replace with Win10 RS3 build number at RTM
|
||||
if len(mps) > 0 && system.GetOSVersion().Build < 16299 { // RS3
|
||||
return errors.New("named pipe mounts are not supported on this version of Windows")
|
||||
}
|
||||
configuration.MappedPipes = mps
|
||||
|
||||
@@ -35,7 +35,7 @@ func DefaultPathEnv(os string) string {
|
||||
// This is used, for example, when validating a user provided path in docker cp.
|
||||
// If a drive letter is supplied, it must be the system drive. The drive letter
|
||||
// is always removed. Also, it translates it to OS semantics (IOW / to \). We
|
||||
// need the path in this syntax so that it can ultimately be contatenated with
|
||||
// need the path in this syntax so that it can ultimately be concatenated with
|
||||
// a Windows long-path which doesn't support drive-letters. Examples:
|
||||
// C: --> Fail
|
||||
// C:\ --> \
|
||||
|
||||
@@ -16,7 +16,7 @@ var eol = []byte("\n")
|
||||
// ErrNonPositiveLinesNumber is an error returned if the lines number was negative.
|
||||
var ErrNonPositiveLinesNumber = errors.New("The number of lines to extract from the file must be positive")
|
||||
|
||||
//TailFile returns last n lines of reader f (could be a fil).
|
||||
//TailFile returns last n lines of reader f (could be a nil).
|
||||
func TailFile(f io.ReadSeeker, n int) ([][]byte, error) {
|
||||
if n <= 0 {
|
||||
return nil, ErrNonPositiveLinesNumber
|
||||
|
||||
Reference in New Issue
Block a user