Skip to content

Commit 012ca11

Browse files
authored
feat: inject brick variable (#659)
1 parent 9946bef commit 012ca11

File tree

6 files changed

+163
-47
lines changed

6 files changed

+163
-47
lines changed

internal/orchestrator/bricksindex/bricks_index.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package bricksindex
22

33
import (
44
"io"
5+
"iter"
56
"slices"
67

78
"github.com/arduino/go-paths-helper"
@@ -56,6 +57,16 @@ func (b Brick) GetVariable(name string) (BrickVariable, bool) {
5657
return b.Variables[idx], true
5758
}
5859

60+
func (b Brick) GetDefaultVariables() iter.Seq2[string, string] {
61+
return func(yield func(string, string) bool) {
62+
for _, v := range b.Variables {
63+
if !yield(v.Name, v.DefaultValue) {
64+
return
65+
}
66+
}
67+
}
68+
}
69+
5970
func unmarshalBricksIndex(content io.Reader) (*BricksIndex, error) {
6071
var index BricksIndex
6172
if err := yaml.NewDecoder(content).Decode(&index); err != nil {

internal/orchestrator/orchestrator.go

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"iter"
99
"log/slog"
10+
"maps"
1011
"net"
1112
"os"
1213
"os/user"
@@ -34,6 +35,7 @@ import (
3435
"github.com/bcmi-labs/orchestrator/internal/store"
3536
"github.com/bcmi-labs/orchestrator/pkg/helpers"
3637
"github.com/bcmi-labs/orchestrator/pkg/micro"
38+
"github.com/bcmi-labs/orchestrator/pkg/x"
3739
"github.com/bcmi-labs/orchestrator/pkg/x/fatomic"
3840
)
3941

@@ -137,15 +139,14 @@ func StartApp(
137139
}
138140
}
139141
if app.MainPythonFile != nil {
140-
// Override the compose Variables with the app's variables and model configuration.
141-
envs, mapped_env := handleEnvironmentVariables(app, modelsIndex)
142+
envs := getAppEnvironmentVariables(app, bricksIndex, modelsIndex)
142143

143144
if !yield(StreamMessage{data: "Provisioning app..."}) {
144145
cancel()
145146
return
146147
}
147148

148-
if err := provisioner.App(ctx, bricksIndex, &app, cfg, mapped_env, staticStore); err != nil {
149+
if err := provisioner.App(ctx, bricksIndex, &app, cfg, envs, staticStore); err != nil {
149150
yield(StreamMessage{error: err})
150151
return
151152
}
@@ -177,7 +178,7 @@ func StartApp(
177178
}
178179
})
179180

180-
process, err := paths.NewProcess(envs, commands...)
181+
process, err := paths.NewProcess(envs.AsList(), commands...)
181182
if err != nil {
182183
yield(StreamMessage{error: err})
183184
return
@@ -197,35 +198,44 @@ func StartApp(
197198
}
198199
}
199200

200-
func handleEnvironmentVariables(app app.ArduinoApp, modelsIndex *modelsindex.ModelsIndex) ([]string, map[string]string) {
201-
envs := []string{}
202-
mapped_env := map[string]string{}
203-
addMapToEnv := func(m map[string]string) {
204-
for k, v := range m {
205-
envs = append(envs, fmt.Sprintf("%s=%s", k, v))
206-
mapped_env[k] = v
207-
}
208-
}
201+
// getAppEnvironmentVariables returns the environment variables for the app by merging variables and config in the following order:
202+
// - brick default variables (variables defined in the brick definition)
203+
// - brick instance variables (variables defined in the app.yaml for the brick instance)
204+
// - model configuration variables (variables defined in the model configuration)
205+
// In addition, it adds some useful environment variables like APP_HOME and HOST_IP.
206+
func getAppEnvironmentVariables(app app.ArduinoApp, brickIndex *bricksindex.BricksIndex, modelsIndex *modelsindex.ModelsIndex) x.EnvVars {
207+
envs := make(x.EnvVars)
208+
209209
for _, brick := range app.Descriptor.Bricks {
210-
addMapToEnv(brick.Variables)
210+
if brickDef, found := brickIndex.FindBrickByID(brick.ID); found {
211+
maps.Insert(envs, brickDef.GetDefaultVariables())
212+
}
213+
maps.Insert(envs, maps.All(brick.Variables))
214+
211215
if m, found := modelsIndex.GetModelByID(brick.Model); found {
212-
addMapToEnv(m.ModelConfiguration)
216+
maps.Insert(envs, maps.All(m.ModelConfiguration))
213217
}
214218
}
215219

216220
// Add the APP_HOME directory to the environment variables
217-
addMapToEnv(map[string]string{"APP_HOME": app.FullPath.String()})
218-
slog.Debug("Configuring app environment", slog.String("APP_HOME", app.FullPath.String()), slog.Any("envs", envs))
221+
envs["APP_HOME"] = app.FullPath.String()
219222

220223
// Pre-select default camera device if available. This can be overridden by the app environment variables (or in future by applab)
221224
// This is required because there are some video devices for HW acceleration that are auto registered in /dev but are not real cameras.
222225
if videoDevices := getVideoDevices(); len(videoDevices) > 0 {
223226
// VIDEO_DEVICE will be the first device in /dev/v4l/by-id
224-
addMapToEnv(map[string]string{"VIDEO_DEVICE": videoDevices[0]})
225-
slog.Info("Configuring default video device", slog.String("VIDEO_DEVICE", videoDevices[0]))
227+
envs["VIDEO_DEVICE"] = videoDevices[0]
226228
}
227229

228-
return envs, mapped_env
230+
if hostIP, err := helpers.GetHostIP(); err == nil {
231+
envs["HOST_IP"] = hostIP
232+
} else {
233+
slog.Warn("unable to get host IP", slog.String("error", err.Error()))
234+
}
235+
236+
slog.Debug("Current environment variables", slog.Any("envs", envs))
237+
238+
return envs
229239
}
230240

231241
func extractIndexFromVideoDeviceName(device string) (int, error) {

internal/orchestrator/orchestrator_test.go

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import (
1414
"go.bug.st/f"
1515

1616
"github.com/bcmi-labs/orchestrator/internal/orchestrator/app"
17+
"github.com/bcmi-labs/orchestrator/internal/orchestrator/bricksindex"
1718
"github.com/bcmi-labs/orchestrator/internal/orchestrator/config"
19+
"github.com/bcmi-labs/orchestrator/internal/orchestrator/modelsindex"
1820
)
1921

2022
func TestCloneApp(t *testing.T) {
@@ -355,7 +357,7 @@ func createApp(
355357
isExample bool,
356358
idProvider *app.IDProvider,
357359
cfg config.Configuration,
358-
) {
360+
) app.ID {
359361
t.Helper()
360362

361363
res, err := CreateApp(t.Context(), CreateAppRequest{
@@ -371,7 +373,10 @@ func createApp(
371373
newID, err := idProvider.IDFromPath(newPath)
372374
require.NoError(t, err)
373375
assert.Empty(t, gCmp.Diff(f.Must(idProvider.ParseID("examples:"+name)), newID))
376+
res.ID = newID
374377
}
378+
379+
return res.ID
375380
}
376381

377382
func TestSortV4LVideoDevices(t *testing.T) {
@@ -387,3 +392,88 @@ func TestSortV4LVideoDevices(t *testing.T) {
387392
assert.Equal(t, "usb-Generic_GENERAL_-_UVC-video-index1", devices[1])
388393
assert.Equal(t, "usb-046d_0825-video-index2", devices[2])
389394
}
395+
396+
func TestGetAppEnvironmentVariablesWithDefaults(t *testing.T) {
397+
cfg := setTestOrchestratorConfig(t)
398+
idProvider := app.NewAppIDProvider(cfg)
399+
400+
docker, err := dockerClient.NewClientWithOpts(
401+
dockerClient.FromEnv,
402+
dockerClient.WithAPIVersionNegotiation(),
403+
)
404+
require.NoError(t, err)
405+
dockerCli, err := command.NewDockerCli(
406+
command.WithAPIClient(docker),
407+
command.WithBaseContext(t.Context()),
408+
)
409+
require.NoError(t, err)
410+
411+
err = dockerCli.Initialize(&flags.ClientOptions{})
412+
require.NoError(t, err)
413+
414+
appId := createApp(t, "app1", false, idProvider, cfg)
415+
appDesc, err := app.Load(appId.ToPath().String())
416+
require.NoError(t, err)
417+
appDesc.Descriptor.Bricks = []app.Brick{
418+
{
419+
ID: "arduino:object_detection",
420+
Model: "", // use the default model
421+
Variables: map[string]string{}, // use the default variables
422+
},
423+
}
424+
425+
bricksIndexContent := []byte(`
426+
bricks:
427+
- id: arduino:object_detection
428+
name: Object Detection
429+
description: "Brick for object detection using a pre-trained model. It processes\
430+
\ images and returns the predicted class label, bounding-boxes and confidence\
431+
\ score.\nBrick is designed to work with pre-trained models provided by framework\
432+
\ or with custom object detection models trained on Edge Impulse platform. \n"
433+
require_container: true
434+
require_model: true
435+
require_devices: false
436+
ports: []
437+
category: video
438+
model_name: yolox-object-detection
439+
variables:
440+
- name: CUSTOM_MODEL_PATH
441+
default_value: /home/arduino/.arduino-bricks/ei-models
442+
description: path to the custom model directory
443+
- name: EI_OBJ_DETECTION_MODEL
444+
default_value: /models/ootb/ei/yolo-x-nano.eim
445+
description: path to the model file
446+
`)
447+
err = cfg.AssetsDir().Join("bricks-list.yaml").WriteFile(bricksIndexContent)
448+
require.NoError(t, err)
449+
bricksIndex, err := bricksindex.GenerateBricksIndexFromFile(cfg.AssetsDir())
450+
assert.NoError(t, err)
451+
452+
modelsIndexContent := []byte(`
453+
models:
454+
- yolox-object-detection:
455+
runner: brick
456+
name : "General purpose object detection - YoloX"
457+
description: "General purpose object detection model based on YoloX Nano. This model is trained on the COCO dataset and can detect 80 different object classes."
458+
model_configuration:
459+
"EI_OBJ_DETECTION_MODEL": "/models/ootb/ei/yolo-x-nano.eim"
460+
metadata:
461+
source: "edgeimpulse"
462+
ei-project-id: 717280
463+
source-model-id: "YOLOX-Nano"
464+
source-model-url: "https://github.com/Megvii-BaseDetection/YOLOX"
465+
bricks:
466+
- arduino:object_detection
467+
- arduino:video_object_detection
468+
`)
469+
err = cfg.AssetsDir().Join("models-list.yaml").WriteFile(modelsIndexContent)
470+
require.NoError(t, err)
471+
modelIndex, err := modelsindex.GenerateModelsIndexFromFile(cfg.AssetsDir())
472+
require.NoError(t, err)
473+
474+
env := getAppEnvironmentVariables(appDesc, bricksIndex, modelIndex)
475+
require.Equal(t, cfg.AppsDir().Join("app1").String(), env["APP_HOME"])
476+
require.Equal(t, "/models/ootb/ei/yolo-x-nano.eim", env["EI_OBJ_DETECTION_MODEL"])
477+
require.Equal(t, "/home/arduino/.arduino-bricks/ei-models", env["CUSTOM_MODEL_PATH"])
478+
// we ignore HOST_IP since it's dynamic
479+
}

internal/orchestrator/provision.go

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
"github.com/bcmi-labs/orchestrator/internal/orchestrator/bricksindex"
2222
"github.com/bcmi-labs/orchestrator/internal/orchestrator/config"
2323
"github.com/bcmi-labs/orchestrator/internal/store"
24-
"github.com/bcmi-labs/orchestrator/pkg/helpers"
24+
"github.com/bcmi-labs/orchestrator/pkg/x"
2525
)
2626

2727
type volume struct {
@@ -189,7 +189,7 @@ func generateMainComposeFile(
189189
bricksIndex *bricksindex.BricksIndex,
190190
pythonImage string,
191191
cfg config.Configuration,
192-
mapped_env map[string]string,
192+
envs x.EnvVars,
193193
staticStore *store.StaticStore,
194194
) error {
195195
slog.Debug("Generating main compose file for the App")
@@ -329,18 +329,7 @@ func generateMainComposeFile(
329329
DockerAppMainLabel: "true",
330330
DockerAppPathLabel: app.FullPath.String(),
331331
},
332-
// Add the host ip to the environment variables if we can retrieve it.
333-
// This is used to show the network ip address of the board in the apps.
334-
Environment: map[string]string{
335-
"HOST_IP": func() string {
336-
if hostIP, err := helpers.GetHostIP(); err != nil {
337-
slog.Warn("Failed to get host IP", slog.Any("error", err))
338-
return ""
339-
} else {
340-
return hostIP
341-
}
342-
}(),
343-
},
332+
Environment: envs,
344333
},
345334
}
346335

@@ -355,7 +344,7 @@ func generateMainComposeFile(
355344

356345
// If there are services that require devices, we need to generate an override compose file
357346
// Write additional file to override devices section in included compose files
358-
if e := generateServicesOverrideFile(app, slices.Collect(maps.Keys(services)), servicesThatRequireDevices, devices.devicePaths, getCurrentUser(), groups, overrideComposeFile); e != nil {
347+
if e := generateServicesOverrideFile(app, slices.Collect(maps.Keys(services)), servicesThatRequireDevices, devices.devicePaths, getCurrentUser(), groups, overrideComposeFile, envs); e != nil {
359348
return e
360349
}
361350

@@ -371,7 +360,7 @@ func generateMainComposeFile(
371360
slog.Warn("Failed to extract volumes from compose file", slog.String("compose_file", composeFilePath), slog.Any("error", err))
372361
continue
373362
}
374-
provisionComposeVolumes(composeFilePath, volumes, app, mapped_env)
363+
provisionComposeVolumes(composeFilePath, volumes, app, envs)
375364
}
376365

377366
// Done!
@@ -409,7 +398,7 @@ func extractServicesFromComposeFile(composeFile *paths.Path) (map[string]service
409398
return services, nil
410399
}
411400

412-
func generateServicesOverrideFile(arduinoApp *app.ArduinoApp, services []string, servicesThatRequireDevices []string, devices []string, user string, groups []string, overrideComposeFile *paths.Path) error {
401+
func generateServicesOverrideFile(arduinoApp *app.ArduinoApp, services []string, servicesThatRequireDevices []string, devices []string, user string, groups []string, overrideComposeFile *paths.Path, envs x.EnvVars) error {
413402
if overrideComposeFile.Exist() {
414403
if err := overrideComposeFile.Remove(); err != nil {
415404
return fmt.Errorf("failed to remove existing override compose file: %w", err)
@@ -422,10 +411,11 @@ func generateServicesOverrideFile(arduinoApp *app.ArduinoApp, services []string,
422411
}
423412

424413
type serviceOverride struct {
425-
User string `yaml:"user,omitempty"`
426-
Devices *[]string `yaml:"devices,omitempty"`
427-
GroupAdd *[]string `yaml:"group_add,omitempty"`
428-
Labels map[string]string `yaml:"labels,omitempty"`
414+
User string `yaml:"user,omitempty"`
415+
Devices *[]string `yaml:"devices,omitempty"`
416+
GroupAdd *[]string `yaml:"group_add,omitempty"`
417+
Labels map[string]string `yaml:"labels,omitempty"`
418+
Environment map[string]string `yaml:"environment,omitempty"`
429419
}
430420
var overrideCompose struct {
431421
Services map[string]serviceOverride `yaml:"services,omitempty"`
@@ -443,6 +433,7 @@ func generateServicesOverrideFile(arduinoApp *app.ArduinoApp, services []string,
443433
override.Devices = &devices
444434
override.GroupAdd = &groups
445435
}
436+
override.Environment = envs
446437
overrideCompose.Services[svc] = override
447438
}
448439
writeOverrideCompose := func() error {

internal/orchestrator/provision_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ func TestProvisionAppWithOverrides(t *testing.T) {
2020
cfg := setTestOrchestratorConfig(t)
2121
tempDirectory := t.TempDir()
2222

23-
// TODO: hack to skip the preEmbargo check
24-
cfg.UsedPythonImageTag = "latest"
25-
2623
staticStore := store.NewStaticStore(cfg.AssetsDir().String())
2724

2825
// Define a mock app with bricks that require overrides
@@ -33,6 +30,9 @@ func TestProvisionAppWithOverrides(t *testing.T) {
3330
{
3431
ID: "arduino:video_object_detection",
3532
Model: "yolox-object-detection",
33+
Variables: map[string]string{
34+
"CUSTOM_MODEL_PATH": "/models/custom/ei/",
35+
},
3636
},
3737
{
3838
ID: "arduino:web_ui",
@@ -98,7 +98,9 @@ bricks:
9898
require.Equal(t, "Object Detection", br.Name, "Brick name should match")
9999

100100
// Run the provision function to generate the main compose file
101-
env := map[string]string{}
101+
env := map[string]string{
102+
"FOO": "bar",
103+
}
102104
err = generateMainComposeFile(&app, bricksIndex, "app-bricks:python-apps-base:dev-latest", cfg, env, staticStore)
103105

104106
// Validate that the main compose file and overrides are created
@@ -110,7 +112,8 @@ bricks:
110112

111113
// Open override file and check for the expected override
112114
overridesContent, err := overridesFilePath.ReadFile()
113-
require.Nil(t, err, "Failed to read overrides file")
115+
require.NoError(t, err)
116+
114117
type services struct {
115118
Services map[string]map[string]interface{} `yaml:"services"`
116119
}
@@ -119,6 +122,7 @@ bricks:
119122
require.Nil(t, err, "Failed to unmarshal overrides content")
120123
require.NotNil(t, content.Services["ei-video-obj-detection-runner"], "Override for ei-video-obj-detection-runner should exist")
121124
require.NotNil(t, content.Services["ei-video-obj-detection-runner"]["devices"], "Override for ei-video-obj-detection-runner devices should exist")
125+
require.Equal(t, "bar", content.Services["ei-video-obj-detection-runner"]["environment"].(map[string]interface{})["FOO"])
122126
}
123127

124128
func TestVolumeParser(t *testing.T) {

pkg/x/x.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,13 @@ import "iter"
66
func EmptyIter[V any]() iter.Seq[V] {
77
return func(yield func(V) bool) {}
88
}
9+
10+
type EnvVars map[string]string
11+
12+
func (e EnvVars) AsList() []string {
13+
list := make([]string, 0, len(e))
14+
for k, v := range e {
15+
list = append(list, k+"="+v)
16+
}
17+
return list
18+
}

0 commit comments

Comments
 (0)