Skip to content

Commit d396e9c

Browse files
feat(provision): add condition to dependsOn services list [WIRE-1091] (#584)
1 parent 0aafb0d commit d396e9c

File tree

3 files changed

+207
-28
lines changed

3 files changed

+207
-28
lines changed

internal/orchestrator/logs.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,11 @@ func AppLogs(
6565
continue
6666
}
6767

68-
services, err := extracServicesFromComposeFile(composeFilePath)
68+
services, err := extractServicesFromComposeFile(composeFilePath)
6969
if err != nil {
7070
return x.EmptyIter[LogMessage](), err
7171
}
72-
for _, s := range services {
72+
for s := range services {
7373
serviceToBrickMapping[s] = brick.ID
7474
}
7575
}

internal/orchestrator/provision.go

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,22 @@ type volume struct {
3030
Target string `yaml:"target"`
3131
}
3232

33+
type dependsOnCondition struct {
34+
Condition string `yaml:"condition"`
35+
}
36+
3337
type service struct {
34-
Image string `yaml:"image"`
35-
DependsOn []string `yaml:"depends_on,omitempty"`
36-
Volumes []volume `yaml:"volumes"`
37-
Devices []string `yaml:"devices"`
38-
Ports []string `yaml:"ports"`
39-
User string `yaml:"user"`
40-
GroupAdd []string `yaml:"group_add"`
41-
Entrypoint string `yaml:"entrypoint"`
42-
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
43-
Labels map[string]string `yaml:"labels,omitempty"`
44-
Environment map[string]string `yaml:"environment,omitempty"`
38+
Image string `yaml:"image"`
39+
DependsOn map[string]dependsOnCondition `yaml:"depends_on,omitempty"`
40+
Volumes []volume `yaml:"volumes"`
41+
Devices []string `yaml:"devices"`
42+
Ports []string `yaml:"ports"`
43+
User string `yaml:"user"`
44+
GroupAdd []string `yaml:"group_add"`
45+
Entrypoint string `yaml:"entrypoint"`
46+
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
47+
Labels map[string]string `yaml:"labels,omitempty"`
48+
Environment map[string]string `yaml:"environment,omitempty"`
4549
}
4650

4751
type Provision struct {
@@ -196,8 +200,8 @@ func generateMainComposeFile(
196200
}
197201

198202
var composeFiles paths.PathList
199-
services := []string{}
200-
servicesThatRequireDevices := []string{}
203+
services := make(map[string]serviceInfo)
204+
var servicesThatRequireDevices []string
201205
for _, brick := range app.Descriptor.Bricks {
202206
idxBrick, found := bricksIndex.FindBrickByID(brick.ID)
203207
slog.Debug("Processing brick", slog.String("brick_id", brick.ID), slog.Bool("found", found))
@@ -224,7 +228,7 @@ func generateMainComposeFile(
224228
}
225229

226230
// 3. Retrieve the compose services names.
227-
svcs, err := extracServicesFromComposeFile(composeFilePath)
231+
svcs, err := extractServicesFromComposeFile(composeFilePath)
228232
if err != nil {
229233
slog.Error("loading brick_compose", slog.String("brick_id", brick.ID), slog.String("path", composeFilePath.String()), slog.Any("error", err))
230234
continue
@@ -233,11 +237,11 @@ func generateMainComposeFile(
233237
// 4. Retrieve the required devices that we have to mount
234238
slog.Debug("Brick require Devices", slog.Bool("Devices", idxBrick.RequiresDevices), slog.Any("ports", ports))
235239
if idxBrick.RequiresDevices {
236-
servicesThatRequireDevices = append(servicesThatRequireDevices, svcs...)
240+
servicesThatRequireDevices = slices.AppendSeq(servicesThatRequireDevices, maps.Keys(svcs))
237241
}
238242

239243
composeFiles.Add(composeFilePath)
240-
services = append(services, svcs...)
244+
maps.Insert(services, maps.All(svcs))
241245
}
242246

243247
// Create a single docker-mainCompose that includes all the required services
@@ -291,14 +295,30 @@ func generateMainComposeFile(
291295

292296
groups := []string{"dialout", "video", "audio", "render"}
293297

298+
// Define depends_on conditions
299+
// Services with healthcheck will be started only when healthy
300+
// Services without healthcheck will be started as soon as the container is started
301+
dependsOn := make(map[string]dependsOnCondition, len(services))
302+
for name := range services {
303+
if services[name].hasHealthcheck {
304+
dependsOn[name] = dependsOnCondition{
305+
Condition: "service_healthy",
306+
}
307+
} else {
308+
dependsOn[name] = dependsOnCondition{
309+
Condition: "service_started",
310+
}
311+
}
312+
}
313+
294314
mainAppCompose.Services = &mainService{
295315
Main: service{
296316
Image: pythonImage,
297317
Volumes: volumes,
298318
Ports: slices.Collect(maps.Keys(ports)),
299319
Devices: devices.devicePaths,
300320
Entrypoint: "/run.sh",
301-
DependsOn: services,
321+
DependsOn: dependsOn,
302322
User: getCurrentUser(),
303323
GroupAdd: groups,
304324
ExtraHosts: []string{"msgpack-rpc-router:host-gateway"},
@@ -333,7 +353,7 @@ func generateMainComposeFile(
333353

334354
// If there are services that require devices, we need to generate an override compose file
335355
// Write additional file to override devices section in included compose files
336-
if e := generateServicesOverrideFile(app, services, servicesThatRequireDevices, devices.devicePaths, getCurrentUser(), groups, overrideComposeFile); e != nil {
356+
if e := generateServicesOverrideFile(app, slices.Collect(maps.Keys(services)), servicesThatRequireDevices, devices.devicePaths, getCurrentUser(), groups, overrideComposeFile); e != nil {
337357
return e
338358
}
339359

@@ -356,14 +376,21 @@ func generateMainComposeFile(
356376
return nil
357377
}
358378

359-
func extracServicesFromComposeFile(composeFile *paths.Path) ([]string, error) {
379+
type serviceInfo struct {
380+
hasHealthcheck bool
381+
}
382+
383+
func extractServicesFromComposeFile(composeFile *paths.Path) (map[string]serviceInfo, error) {
360384
content, err := os.ReadFile(composeFile.String())
361385
if err != nil {
362386
return nil, err
363387
}
364388

365389
type serviceMin struct {
366-
Image string `yaml:"image"`
390+
Image string `yaml:"image"`
391+
Healthcheck struct {
392+
Test []string `yaml:"test"`
393+
} `yaml:"healthcheck,omitempty"`
367394
}
368395
type composeServices struct {
369396
Services map[string]serviceMin `yaml:"services"`
@@ -372,11 +399,10 @@ func extracServicesFromComposeFile(composeFile *paths.Path) ([]string, error) {
372399
if err := yaml.Unmarshal(content, &index); err != nil {
373400
return nil, err
374401
}
375-
services := make([]string, len(index.Services))
376-
i := 0
377-
for svc := range maps.Keys(index.Services) {
378-
services[i] = svc
379-
i++
402+
services := make(map[string]serviceInfo, len(index.Services))
403+
for svc, svcDef := range index.Services {
404+
hasHealthcheck := len(svcDef.Healthcheck.Test) > 0
405+
services[svc] = serviceInfo{hasHealthcheck: hasHealthcheck}
380406
}
381407
return services, nil
382408
}

internal/orchestrator/provision_test.go

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/bcmi-labs/orchestrator/internal/orchestrator/bricksindex"
1212
"github.com/bcmi-labs/orchestrator/internal/store"
1313

14-
yaml "github.com/goccy/go-yaml"
14+
"github.com/goccy/go-yaml"
1515

1616
"github.com/stretchr/testify/require"
1717
)
@@ -251,3 +251,156 @@ services:
251251
})
252252

253253
}
254+
255+
func TestProvisionAppWithDependsOn(t *testing.T) {
256+
cfg := setTestOrchestratorConfig(t)
257+
staticStore := store.NewStaticStore(cfg.AssetsDir().String())
258+
tempDirectory := t.TempDir()
259+
var env = map[string]string{}
260+
type services struct {
261+
Services map[string]struct {
262+
Image string `yaml:"image"`
263+
DependsOn map[string]struct {
264+
Condition string `yaml:"condition"`
265+
} `yaml:"depends_on"`
266+
} `yaml:"services"`
267+
}
268+
269+
bricksIndexContent := []byte(`
270+
bricks:
271+
- id: arduino:dbstorage_tsstore
272+
name: Database Storage - Time Series Store
273+
description: Simplified time series database storage layer for Arduino sensor samples
274+
built on top of InfluxDB.
275+
require_container: true
276+
require_model: false
277+
ports: []
278+
category: storage
279+
variables:
280+
- name: APP_HOME
281+
default_value: .`)
282+
err := cfg.AssetsDir().Join("bricks-list.yaml").WriteFile(bricksIndexContent)
283+
require.NoError(t, err)
284+
285+
bricksIndex, err := bricksindex.GenerateBricksIndexFromFile(cfg.AssetsDir())
286+
require.Nil(t, err, "Failed to load bricks index with custom content")
287+
br, ok := bricksIndex.FindBrickByID("arduino:dbstorage_tsstore")
288+
require.True(t, ok, "Brick arduino:dbstorage_tsstore should exist in the index")
289+
require.NotNil(t, br, "Brick arduino:dbstorage_tsstore should not be nil")
290+
require.Equal(t, "Database Storage - Time Series Store", br.Name, "Brick name should match")
291+
292+
app := app.ArduinoApp{
293+
Name: "TestApp",
294+
Descriptor: app.AppDescriptor{
295+
Bricks: []app.Brick{
296+
{
297+
ID: "arduino:dbstorage_tsstore",
298+
},
299+
},
300+
},
301+
FullPath: paths.New(tempDirectory),
302+
}
303+
require.NoError(t, app.ProvisioningStateDir().MkdirAll())
304+
305+
t.Run("services with healthcheck", func(t *testing.T) {
306+
fileComposePath := cfg.AssetsDir().Join("compose", "arduino", "dbstorage_tsstore")
307+
require.NoError(t, fileComposePath.MkdirAll())
308+
dependsOnFromStrings := `
309+
services:
310+
dbstorage-influx:
311+
image: influxdb:2.7
312+
ports:
313+
- "${BIND_ADDRESS:-127.0.0.1}:${BIND_PORT:-8086}:8086"
314+
volumes:
315+
- "${APP_HOME:-.}/data/influx-data:/var/lib/influxdb2"
316+
environment:
317+
DOCKER_INFLUXDB_INIT_MODE: setup
318+
healthcheck:
319+
test: ["CMD", "curl", "-f", "http://localhost:8086/health"]`
320+
err := fileComposePath.Join("brick_compose.yaml").WriteFile([]byte(dependsOnFromStrings))
321+
require.NoError(t, err)
322+
323+
// Run the provision function to generate the main compose file
324+
err = generateMainComposeFile(&app, bricksIndex, "app-bricks:python-apps-base:dev-latest", cfg, env, staticStore)
325+
require.NoError(t, err, "Failed to generate main compose file")
326+
composeFilePath := paths.New(tempDirectory).Join(".cache").Join("app-compose.yaml")
327+
require.True(t, composeFilePath.Exist(), "Main compose file should exist")
328+
329+
// Open main compose file and check for the expected depends_on with service_healthy
330+
mainComposeFileContent, err := composeFilePath.ReadFile()
331+
require.Nil(t, err, "Failed to read compose file")
332+
var content services
333+
err = yaml.Unmarshal(mainComposeFileContent, &content)
334+
require.Nil(t, err, "Failed to unmarshal overrides content")
335+
exp := services{
336+
Services: map[string]struct {
337+
Image string `yaml:"image"`
338+
DependsOn map[string]struct {
339+
Condition string `yaml:"condition"`
340+
} `yaml:"depends_on"`
341+
}{
342+
"main": {
343+
Image: "app-bricks:python-apps-base:dev-latest",
344+
DependsOn: map[string]struct {
345+
Condition string `yaml:"condition"`
346+
}{
347+
"dbstorage-influx": {
348+
Condition: "service_healthy",
349+
},
350+
},
351+
},
352+
},
353+
}
354+
require.Equal(t, exp, content, "Main compose content should match the expected structure")
355+
})
356+
357+
t.Run("services without healthcheck", func(t *testing.T) {
358+
fileComposePath := cfg.AssetsDir().Join("compose", "arduino", "dbstorage_tsstore")
359+
require.NoError(t, fileComposePath.MkdirAll())
360+
dependsOnFromStrings := `
361+
services:
362+
dbstorage-influx:
363+
image: influxdb:2.7
364+
ports:
365+
- "${BIND_ADDRESS:-127.0.0.1}:${BIND_PORT:-8086}:8086"
366+
volumes:
367+
- "${APP_HOME:-.}/data/influx-data:/var/lib/influxdb2"
368+
environment:
369+
DOCKER_INFLUXDB_INIT_MODE: setup`
370+
err = fileComposePath.Join("brick_compose.yaml").WriteFile([]byte(dependsOnFromStrings))
371+
require.NoError(t, err)
372+
373+
// Run the provision function to generate the main compose file
374+
err = generateMainComposeFile(&app, bricksIndex, "app-bricks:python-apps-base:dev-latest", cfg, env, staticStore)
375+
require.NoError(t, err, "Failed to generate main compose file")
376+
composeFilePath := paths.New(tempDirectory).Join(".cache").Join("app-compose.yaml")
377+
require.True(t, composeFilePath.Exist(), "Main compose file should exist")
378+
379+
// Open main compose file and check for the expected depends_on with service_started
380+
mainComposeFileContent, err := composeFilePath.ReadFile()
381+
require.Nil(t, err, "Failed to read compose file")
382+
var content services
383+
err = yaml.Unmarshal(mainComposeFileContent, &content)
384+
require.Nil(t, err, "Failed to unmarshal overrides content")
385+
exp := services{
386+
Services: map[string]struct {
387+
Image string `yaml:"image"`
388+
DependsOn map[string]struct {
389+
Condition string `yaml:"condition"`
390+
} `yaml:"depends_on"`
391+
}{
392+
"main": {
393+
Image: "app-bricks:python-apps-base:dev-latest",
394+
DependsOn: map[string]struct {
395+
Condition string `yaml:"condition"`
396+
}{
397+
"dbstorage-influx": {
398+
Condition: "service_started",
399+
},
400+
},
401+
},
402+
},
403+
}
404+
require.Equal(t, exp, content, "Main compose content should match the expected structure")
405+
})
406+
}

0 commit comments

Comments
 (0)