Skip to content

Commit ec26728

Browse files
committed
Merge branch '371-dle-ui-container-starting' into 'master'
fix: add logic to detect and start existing UI container (#371) Closes #371 See merge request postgres-ai/database-lab!538
2 parents a43bf1a + 8f34615 commit ec26728

File tree

4 files changed

+179
-47
lines changed

4 files changed

+179
-47
lines changed

engine/internal/embeddedui/embedded_ui.go

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -91,53 +91,66 @@ func (ui *UIManager) isConfigChanged(cfg Config) bool {
9191

9292
// Run creates a new embedded UI container.
9393
func (ui *UIManager) Run(ctx context.Context) error {
94-
if err := docker.PrepareImage(ui.runner, ui.cfg.DockerImage); err != nil {
94+
if err := docker.PrepareImage(ctx, ui.docker, ui.cfg.DockerImage); err != nil {
9595
return fmt.Errorf("failed to prepare Docker image: %w", err)
9696
}
9797

98-
embeddedUI, err := ui.docker.ContainerCreate(ctx,
99-
&container.Config{
100-
Labels: map[string]string{
101-
cont.DBLabSatelliteLabel: cont.DBLabEmbeddedUILabel,
102-
cont.DBLabInstanceIDLabel: ui.engProps.InstanceID,
103-
cont.DBLabEngineNameLabel: ui.engProps.ContainerName,
104-
},
105-
Image: ui.cfg.DockerImage,
106-
Env: []string{
107-
EnvEngineName + "=" + ui.engProps.ContainerName,
108-
EnvEnginePort + "=" + strconv.FormatUint(uint64(ui.engProps.EnginePort), 10),
109-
},
110-
Healthcheck: &container.HealthConfig{
111-
Interval: healthCheckInterval,
112-
Timeout: healthCheckTimeout,
113-
Retries: healthCheckRetries,
98+
var containerID = ""
99+
100+
// try to fetch an existing UI container
101+
containerData, err := ui.docker.ContainerInspect(ctx, getEmbeddedUIName(ui.engProps.InstanceID))
102+
103+
if err == nil {
104+
containerID = containerData.ID
105+
}
106+
107+
if containerID == "" {
108+
embeddedUI, err := ui.docker.ContainerCreate(ctx,
109+
&container.Config{
110+
Labels: map[string]string{
111+
cont.DBLabSatelliteLabel: cont.DBLabEmbeddedUILabel,
112+
cont.DBLabInstanceIDLabel: ui.engProps.InstanceID,
113+
cont.DBLabEngineNameLabel: ui.engProps.ContainerName,
114+
},
115+
Image: ui.cfg.DockerImage,
116+
Env: []string{
117+
EnvEngineName + "=" + ui.engProps.ContainerName,
118+
EnvEnginePort + "=" + strconv.FormatUint(uint64(ui.engProps.EnginePort), 10),
119+
},
120+
Healthcheck: &container.HealthConfig{
121+
Interval: healthCheckInterval,
122+
Timeout: healthCheckTimeout,
123+
Retries: healthCheckRetries,
124+
},
114125
},
115-
},
116-
&container.HostConfig{
117-
PortBindings: map[nat.Port][]nat.PortBinding{
118-
"80/tcp": {
119-
{
120-
HostIP: ui.cfg.Host,
121-
HostPort: strconv.Itoa(ui.cfg.Port),
126+
&container.HostConfig{
127+
PortBindings: map[nat.Port][]nat.PortBinding{
128+
"80/tcp": {
129+
{
130+
HostIP: ui.cfg.Host,
131+
HostPort: strconv.Itoa(ui.cfg.Port),
132+
},
122133
},
123134
},
124135
},
125-
},
126-
&network.NetworkingConfig{},
127-
nil,
128-
getEmbeddedUIName(ui.engProps.InstanceID),
129-
)
130-
131-
if err != nil {
132-
return fmt.Errorf("failed to prepare Docker image for embedded UI: %w", err)
136+
&network.NetworkingConfig{},
137+
nil,
138+
getEmbeddedUIName(ui.engProps.InstanceID),
139+
)
140+
141+
if err != nil {
142+
return fmt.Errorf("failed to prepare Docker image for embedded UI: %w", err)
143+
}
144+
145+
containerID = embeddedUI.ID
133146
}
134147

135-
if err := networks.Connect(ctx, ui.docker, ui.engProps.InstanceID, embeddedUI.ID); err != nil {
148+
if err := networks.Connect(ctx, ui.docker, ui.engProps.InstanceID, containerID); err != nil {
136149
return fmt.Errorf("failed to connect UI container to the internal Docker network: %w", err)
137150
}
138151

139-
if err := ui.docker.ContainerStart(ctx, embeddedUI.ID, types.ContainerStartOptions{}); err != nil {
140-
return fmt.Errorf("failed to start container %q: %w", embeddedUI.ID, err)
152+
if err := ui.docker.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil {
153+
return fmt.Errorf("failed to start container %q: %w", containerID, err)
141154
}
142155

143156
reportLaunching(ui.cfg)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//go:build integration
2+
// +build integration
3+
4+
/*
5+
2021 © Postgres.ai
6+
*/
7+
8+
package embeddedui
9+
10+
import (
11+
"context"
12+
"testing"
13+
"time"
14+
15+
"github.com/docker/docker/api/types"
16+
"github.com/docker/docker/api/types/filters"
17+
"github.com/docker/docker/client"
18+
"github.com/stretchr/testify/assert"
19+
"github.com/stretchr/testify/require"
20+
21+
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/runners"
22+
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools"
23+
"gitlab.com/postgres-ai/database-lab/v3/pkg/config/global"
24+
"gitlab.com/postgres-ai/database-lab/v3/pkg/util/networks"
25+
)
26+
27+
func TestStartExistingContainer(t *testing.T) {
28+
t.Parallel()
29+
docker, err := client.NewClientWithOpts(client.FromEnv)
30+
require.NoError(t, err)
31+
32+
engProps := global.EngineProps{
33+
InstanceID: "testuistart",
34+
}
35+
36+
embeddedUI := New(
37+
Config{
38+
// "mock" UI image
39+
DockerImage: "gcr.io/google_containers/pause-amd64:3.0",
40+
},
41+
engProps,
42+
runners.NewLocalRunner(false),
43+
docker,
44+
)
45+
46+
ctx := context.TODO()
47+
48+
networks.Setup(ctx, docker, engProps.InstanceID, getEmbeddedUIName(engProps.InstanceID))
49+
50+
// clean test UI container
51+
defer tools.RemoveContainer(ctx, docker, getEmbeddedUIName(engProps.InstanceID), 30*time.Second)
52+
53+
// start UI container
54+
err = embeddedUI.Run(ctx)
55+
require.NoError(t, err)
56+
57+
// explicitly stop container
58+
tools.StopContainer(ctx, docker, getEmbeddedUIName(engProps.InstanceID), 30*time.Second)
59+
60+
// start UI container back
61+
err = embeddedUI.Run(ctx)
62+
require.NoError(t, err)
63+
64+
// list containers
65+
filterArgs := filters.NewArgs()
66+
filterArgs.Add("name", getEmbeddedUIName(engProps.InstanceID))
67+
68+
list, err := docker.ContainerList(
69+
ctx,
70+
types.ContainerListOptions{
71+
All: true,
72+
Filters: filterArgs,
73+
},
74+
)
75+
76+
require.NoError(t, err)
77+
assert.NotEmpty(t, list)
78+
// initial container
79+
assert.Equal(t, "running", list[0].State)
80+
}

engine/internal/provision/docker/docker.go

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,39 @@ import (
99
"context"
1010
"encoding/json"
1111
"fmt"
12+
"io"
1213
"os"
1314
"path"
1415
"strconv"
1516
"strings"
1617

1718
"github.com/docker/docker/api/types"
19+
"github.com/docker/docker/api/types/filters"
1820
"github.com/docker/docker/client"
1921
"github.com/pkg/errors"
2022
"github.com/shirou/gopsutil/host"
2123

2224
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources"
2325
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/runners"
2426
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools"
27+
"gitlab.com/postgres-ai/database-lab/v3/pkg/log"
2528
)
2629

2730
const (
2831
labelClone = "dblab_clone"
32+
33+
// referenceKey uses as a filtering key to identify image tag.
34+
referenceKey = "reference"
2935
)
3036

3137
var systemVolumes = []string{"/sys", "/lib", "/proc"}
3238

39+
// imagePullProgress describes the progress of pulling the container image.
40+
type imagePullProgress struct {
41+
Status string `json:"status"`
42+
Progress string `json:"progress"`
43+
}
44+
3345
// RunContainer runs specified container.
3446
func RunContainer(r runners.Runner, c *resources.AppConfig) error {
3547
hostInfo, err := host.Info()
@@ -221,8 +233,8 @@ func Exec(r runners.Runner, c *resources.AppConfig, cmd string) (string, error)
221233
}
222234

223235
// PrepareImage prepares a Docker image to use.
224-
func PrepareImage(runner runners.Runner, dockerImage string) error {
225-
imageExists, err := ImageExists(runner, dockerImage)
236+
func PrepareImage(ctx context.Context, docker *client.Client, dockerImage string) error {
237+
imageExists, err := ImageExists(ctx, docker, dockerImage)
226238
if err != nil {
227239
return fmt.Errorf("cannot check docker image existence: %w", err)
228240
}
@@ -231,31 +243,58 @@ func PrepareImage(runner runners.Runner, dockerImage string) error {
231243
return nil
232244
}
233245

234-
if err := PullImage(runner, dockerImage); err != nil {
246+
if err := PullImage(ctx, docker, dockerImage); err != nil {
235247
return fmt.Errorf("cannot pull docker image: %w", err)
236248
}
237249

238250
return nil
239251
}
240252

241253
// ImageExists checks existence of Docker image.
242-
func ImageExists(r runners.Runner, dockerImage string) (bool, error) {
243-
dockerListImagesCmd := "docker images " + dockerImage + " --quiet"
254+
func ImageExists(ctx context.Context, docker *client.Client, dockerImage string) (bool, error) {
255+
filterArgs := filters.NewArgs()
256+
filterArgs.Add(referenceKey, dockerImage)
257+
258+
list, err := docker.ImageList(ctx, types.ImageListOptions{
259+
All: false,
260+
Filters: filterArgs,
261+
})
244262

245-
out, err := r.Run(dockerListImagesCmd, true)
246263
if err != nil {
247264
return false, fmt.Errorf("failed to list images: %w", err)
248265
}
249266

250-
return len(strings.TrimSpace(out)) > 0, nil
267+
return len(list) > 0, nil
251268
}
252269

253270
// PullImage pulls Docker image from DockerHub registry.
254-
func PullImage(r runners.Runner, dockerImage string) error {
255-
dockerPullImageCmd := "docker pull " + dockerImage
271+
func PullImage(ctx context.Context, docker *client.Client, dockerImage string) error {
272+
pullResponse, err := docker.ImagePull(ctx, dockerImage, types.ImagePullOptions{})
273+
274+
if err != nil {
275+
return fmt.Errorf("failed to pull image: %w", err)
276+
}
277+
278+
// reading output of image pulling, without reading pull will not be performed
279+
decoder := json.NewDecoder(pullResponse)
256280

257-
if _, err := r.Run(dockerPullImageCmd, true); err != nil {
258-
return fmt.Errorf("failed to pull images: %w", err)
281+
for {
282+
var pullResult imagePullProgress
283+
if err := decoder.Decode(&pullResult); err != nil {
284+
if errors.Is(err, io.EOF) {
285+
break
286+
}
287+
288+
return fmt.Errorf("failed to pull image: %w", err)
289+
}
290+
291+
log.Dbg("Image pulling progress", pullResult.Status, pullResult.Progress)
292+
}
293+
294+
err = pullResponse.Close()
295+
296+
if err != nil {
297+
return fmt.Errorf("failed to pull image: %w", err)
259298
}
260299

261300
return nil

engine/internal/provision/mode_local.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func (p *Provisioner) Init() error {
125125
return fmt.Errorf("failed to revise port pool: %w", err)
126126
}
127127

128-
if err := docker.PrepareImage(p.runner, p.config.DockerImage); err != nil {
128+
if err := docker.PrepareImage(p.ctx, p.dockerClient, p.config.DockerImage); err != nil {
129129
return fmt.Errorf("cannot prepare docker image %s: %w", p.config.DockerImage, err)
130130
}
131131

0 commit comments

Comments
 (0)