mirror of https://github.com/docker/compose.git
ACI : when following logs (actually polling logs), stop polling when the container is not running anymore.
Signed-off-by: Guillaume Tardif <guillaume.tardif@docker.com>
This commit is contained in:
parent
e9a575c4be
commit
7138ecc899
23
aci/aci.go
23
aci/aci.go
|
@ -259,6 +259,8 @@ func getACIContainerLogs(ctx context.Context, aciContext store.AciContext, conta
|
||||||
|
|
||||||
func streamLogs(ctx context.Context, aciContext store.AciContext, containerGroupName, containerName string, req containers.LogsRequest) error {
|
func streamLogs(ctx context.Context, aciContext store.AciContext, containerGroupName, containerName string, req containers.LogsRequest) error {
|
||||||
numLines := 0
|
numLines := 0
|
||||||
|
previousLogLines := ""
|
||||||
|
firstDisplay := true // optimization to exit sooner in cases like docker run hello-world, do not wait another 2 secs.
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -285,6 +287,12 @@ func streamLogs(ctx context.Context, aciContext store.AciContext, containerGroup
|
||||||
fmt.Fprintln(req.Writer, logLines[i])
|
fmt.Fprintln(req.Writer, logLines[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (firstDisplay || previousLogLines == logs) && !isContainerRunning(ctx, aciContext, containerGroupName, containerName) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
firstDisplay = false
|
||||||
|
previousLogLines = logs
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
|
@ -294,6 +302,21 @@ func streamLogs(ctx context.Context, aciContext store.AciContext, containerGroup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isContainerRunning(ctx context.Context, aciContext store.AciContext, containerGroupName, containerName string) bool {
|
||||||
|
group, err := getACIContainerGroup(ctx, aciContext, containerGroupName)
|
||||||
|
if err != nil {
|
||||||
|
return false // group has disappeared
|
||||||
|
}
|
||||||
|
for _, container := range *group.Containers {
|
||||||
|
if *container.Name == containerName {
|
||||||
|
if convert.GetStatus(container, group) == convert.StatusRunning {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func getBacktrackLines(lines []string, terminalWidth int) int {
|
func getBacktrackLines(lines []string, terminalWidth int) int {
|
||||||
if terminalWidth == 0 { // no terminal width has been set, do not divide by zero
|
if terminalWidth == 0 { // no terminal width has been set, do not divide by zero
|
||||||
return len(lines)
|
return len(lines)
|
||||||
|
|
|
@ -48,7 +48,6 @@ const (
|
||||||
singleContainerTag = "docker-single-container"
|
singleContainerTag = "docker-single-container"
|
||||||
composeContainerTag = "docker-compose-application"
|
composeContainerTag = "docker-compose-application"
|
||||||
composeContainerSeparator = "_"
|
composeContainerSeparator = "_"
|
||||||
statusRunning = "Running"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContextParams options for creating ACI context
|
// ContextParams options for creating ACI context
|
||||||
|
@ -183,7 +182,7 @@ func getContainerID(group containerinstance.ContainerGroup, container containeri
|
||||||
}
|
}
|
||||||
|
|
||||||
func isContainerVisible(container containerinstance.Container, group containerinstance.ContainerGroup, showAll bool) bool {
|
func isContainerVisible(container containerinstance.Container, group containerinstance.ContainerGroup, showAll bool) bool {
|
||||||
return *container.Name == convert.ComposeDNSSidecarName || (!showAll && convert.GetStatus(container, group) != statusRunning)
|
return *container.Name == convert.ComposeDNSSidecarName || (!showAll && convert.GetStatus(container, group) != convert.StatusRunning)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
|
func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
|
||||||
|
@ -349,7 +348,7 @@ func (cs *aciContainerService) Delete(ctx context.Context, containerID string, r
|
||||||
for _, container := range *cg.Containers {
|
for _, container := range *cg.Containers {
|
||||||
status := convert.GetStatus(container, cg)
|
status := convert.GetStatus(container, cg)
|
||||||
|
|
||||||
if status == statusRunning {
|
if status == convert.StatusRunning {
|
||||||
return errdefs.ErrForbidden
|
return errdefs.ErrForbidden
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// StatusRunning name of the ACI running status
|
||||||
|
StatusRunning = "Running"
|
||||||
// ComposeDNSSidecarName name of the dns sidecar container
|
// ComposeDNSSidecarName name of the dns sidecar container
|
||||||
ComposeDNSSidecarName = "aci--dns--sidecar"
|
ComposeDNSSidecarName = "aci--dns--sidecar"
|
||||||
dnsSidecarImage = "busybox:1.31.1"
|
dnsSidecarImage = "busybox:1.31.1"
|
||||||
|
@ -391,7 +393,7 @@ func bytesToGb(b types.UnitBytes) float64 {
|
||||||
// ContainerGroupToServiceStatus convert from an ACI container definition to service status
|
// ContainerGroupToServiceStatus convert from an ACI container definition to service status
|
||||||
func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container) compose.ServiceStatus {
|
func ContainerGroupToServiceStatus(containerID string, group containerinstance.ContainerGroup, container containerinstance.Container) compose.ServiceStatus {
|
||||||
var replicas = 1
|
var replicas = 1
|
||||||
if GetStatus(container, group) != "Running" {
|
if GetStatus(container, group) != StatusRunning {
|
||||||
replicas = 0
|
replicas = 0
|
||||||
}
|
}
|
||||||
return compose.ServiceStatus{
|
return compose.ServiceStatus{
|
||||||
|
|
|
@ -31,6 +31,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/compose-cli/aci/convert"
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/icmd"
|
"gotest.tools/v3/icmd"
|
||||||
|
@ -294,6 +296,7 @@ func TestContainerRunAttached(t *testing.T) {
|
||||||
|
|
||||||
container = "test-container"
|
container = "test-container"
|
||||||
|
|
||||||
|
var followLogsProcess *icmd.Result
|
||||||
t.Run("run attached limits", func(t *testing.T) {
|
t.Run("run attached limits", func(t *testing.T) {
|
||||||
cmd := c.NewDockerCmd(
|
cmd := c.NewDockerCmd(
|
||||||
"run",
|
"run",
|
||||||
|
@ -303,7 +306,7 @@ func TestContainerRunAttached(t *testing.T) {
|
||||||
"-p", "80:80",
|
"-p", "80:80",
|
||||||
"nginx",
|
"nginx",
|
||||||
)
|
)
|
||||||
runRes := icmd.StartCmd(cmd)
|
followLogsProcess = icmd.StartCmd(cmd)
|
||||||
|
|
||||||
checkRunning := func(t poll.LogT) poll.Result {
|
checkRunning := func(t poll.LogT) poll.Result {
|
||||||
res := c.RunDockerOrExitError("inspect", container)
|
res := c.RunDockerOrExitError("inspect", container)
|
||||||
|
@ -330,7 +333,7 @@ func TestContainerRunAttached(t *testing.T) {
|
||||||
assert.Equal(t, port.HostPort, uint32(80))
|
assert.Equal(t, port.HostPort, uint32(80))
|
||||||
endpoint = fmt.Sprintf("http://%s:%d", port.HostIP, port.HostPort)
|
endpoint = fmt.Sprintf("http://%s:%d", port.HostIP, port.HostPort)
|
||||||
|
|
||||||
assert.Assert(t, !strings.Contains(runRes.Stdout(), "/test"))
|
assert.Assert(t, !strings.Contains(followLogsProcess.Stdout(), "/test"))
|
||||||
checkRequest := func(t poll.LogT) poll.Result {
|
checkRequest := func(t poll.LogT) poll.Result {
|
||||||
r, _ := http.Get(endpoint + "/test")
|
r, _ := http.Get(endpoint + "/test")
|
||||||
if r != nil && r.StatusCode == http.StatusNotFound {
|
if r != nil && r.StatusCode == http.StatusNotFound {
|
||||||
|
@ -341,7 +344,7 @@ func TestContainerRunAttached(t *testing.T) {
|
||||||
poll.WaitOn(t, checkRequest, poll.WithDelay(1*time.Second), poll.WithTimeout(60*time.Second))
|
poll.WaitOn(t, checkRequest, poll.WithDelay(1*time.Second), poll.WithTimeout(60*time.Second))
|
||||||
|
|
||||||
checkLog := func(t poll.LogT) poll.Result {
|
checkLog := func(t poll.LogT) poll.Result {
|
||||||
if strings.Contains(runRes.Stdout(), "/test") {
|
if strings.Contains(followLogsProcess.Stdout(), "/test") {
|
||||||
return poll.Success()
|
return poll.Success()
|
||||||
}
|
}
|
||||||
return poll.Continue("waiting for logs to contain /test")
|
return poll.Continue("waiting for logs to contain /test")
|
||||||
|
@ -360,6 +363,13 @@ func TestContainerRunAttached(t *testing.T) {
|
||||||
t.Run("stop container", func(t *testing.T) {
|
t.Run("stop container", func(t *testing.T) {
|
||||||
res := c.RunDockerCmd("stop", container)
|
res := c.RunDockerCmd("stop", container)
|
||||||
res.Assert(t, icmd.Expected{Out: container})
|
res.Assert(t, icmd.Expected{Out: container})
|
||||||
|
waitForStatus(t, c, container, "Terminated", "Node Stopped")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check we stoppped following logs", func(t *testing.T) {
|
||||||
|
// nolint errcheck
|
||||||
|
followLogsStopped := waitWithTimeout(func() { followLogsProcess.Cmd.Process.Wait() }, 10*time.Second)
|
||||||
|
assert.NilError(t, followLogsStopped, "Follow logs process did not stop after container is stopped")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ps stopped container with --all", func(t *testing.T) {
|
t.Run("ps stopped container with --all", func(t *testing.T) {
|
||||||
|
@ -372,14 +382,14 @@ func TestContainerRunAttached(t *testing.T) {
|
||||||
assert.Assert(t, is.Len(out, 2))
|
assert.Assert(t, is.Len(out, 2))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("start container", func(t *testing.T) {
|
t.Run("restart container", func(t *testing.T) {
|
||||||
res := c.RunDockerCmd("start", container)
|
res := c.RunDockerCmd("start", container)
|
||||||
res.Assert(t, icmd.Expected{Out: container})
|
res.Assert(t, icmd.Expected{Out: container})
|
||||||
waitForStatus(t, c, container, "Running")
|
waitForStatus(t, c, container, convert.StatusRunning)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("rm stopped container", func(t *testing.T) {
|
t.Run("kill & rm stopped container", func(t *testing.T) {
|
||||||
res := c.RunDockerCmd("stop", container)
|
res := c.RunDockerCmd("kill", container)
|
||||||
res.Assert(t, icmd.Expected{Out: container})
|
res.Assert(t, icmd.Expected{Out: container})
|
||||||
waitForStatus(t, c, container, "Terminated", "Node Stopped")
|
waitForStatus(t, c, container, "Terminated", "Node Stopped")
|
||||||
|
|
||||||
|
@ -675,3 +685,17 @@ func waitForStatus(t *testing.T, c *E2eCLI, containerID string, statuses ...stri
|
||||||
|
|
||||||
poll.WaitOn(t, checkStopped, poll.WithDelay(5*time.Second), poll.WithTimeout(90*time.Second))
|
poll.WaitOn(t, checkStopped, poll.WithDelay(5*time.Second), poll.WithTimeout(90*time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func waitWithTimeout(blockingCall func(), timeout time.Duration) error {
|
||||||
|
c := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(c)
|
||||||
|
blockingCall()
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-c:
|
||||||
|
return nil
|
||||||
|
case <-time.After(timeout):
|
||||||
|
return fmt.Errorf("Timed out after %s", timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue