88 "maps"
99 "os"
1010 "path/filepath"
11+ "regexp"
1112 "slices"
1213 "strings"
1314 "time"
@@ -48,13 +49,14 @@ func ProvisionApp(
4849 ctx context.Context ,
4950 provisioner * Provision ,
5051 bricksIndex * bricksindex.BricksIndex ,
52+ mapped_env map [string ]string ,
5153 app * app.ArduinoApp ,
5254) error {
5355 start := time .Now ()
5456 defer func () {
5557 slog .Info ("Provisioning took" , "duration" , time .Since (start ).String ())
5658 }()
57- return provisioner .App (ctx , bricksIndex , app )
59+ return provisioner .App (ctx , bricksIndex , app , mapped_env )
5860}
5961
6062type Provision struct {
@@ -88,6 +90,7 @@ func (p *Provision) App(
8890 ctx context.Context ,
8991 bricksIndex * bricksindex.BricksIndex ,
9092 arduinoApp * app.ArduinoApp ,
93+ mapped_env map [string ]string ,
9194) error {
9295 if arduinoApp == nil {
9396 return fmt .Errorf ("provisioning failed: arduinoApp is nil" )
@@ -108,7 +111,7 @@ func (p *Provision) App(
108111 }
109112 }
110113
111- return generateMainComposeFile (arduinoApp , bricksIndex , p .pythonImage )
114+ return generateMainComposeFile (arduinoApp , bricksIndex , p .pythonImage , mapped_env )
112115}
113116
114117func (p * Provision ) IsUsingDynamicProvision () bool {
@@ -197,6 +200,7 @@ func generateMainComposeFile(
197200 app * app.ArduinoApp ,
198201 bricksIndex * bricksindex.BricksIndex ,
199202 pythonImage string ,
203+ mapped_env map [string ]string ,
200204) error {
201205 slog .Debug ("Generating main compose file for the App" )
202206
@@ -291,6 +295,7 @@ func generateMainComposeFile(
291295 Target : "/app" ,
292296 },
293297 }
298+
294299 slog .Debug ("Adding UNIX socket" , slog .Any ("sock" , orchestratorConfig .RouterSocketPath ().String ()), slog .Bool ("exists" , orchestratorConfig .RouterSocketPath ().Exist ()))
295300 if orchestratorConfig .RouterSocketPath ().Exist () {
296301 volumes = append (volumes , volume {
@@ -301,6 +306,7 @@ func generateMainComposeFile(
301306 }
302307
303308 devices := getDevices ()
309+ groups := []string {"dialout" , "video" , "audio" }
304310
305311 mainAppCompose .Services = & mainService {
306312 Main : service {
@@ -311,7 +317,7 @@ func generateMainComposeFile(
311317 Entrypoint : "/run.sh" ,
312318 DependsOn : services ,
313319 User : getCurrentUser (),
314- GroupAdd : [] string { "dialout" , "video" , "audio" } ,
320+ GroupAdd : groups ,
315321 ExtraHosts : []string {"msgpack-rpc-router:host-gateway" },
316322 Labels : map [string ]string {
317323 DockerAppLabel : "true" ,
@@ -324,18 +330,28 @@ func generateMainComposeFile(
324330 if e := writeMainCompose (); e != nil {
325331 return e
326332 }
333+
327334 // If there are services that require devices, we need to generate an override compose file
328- if overrideComposeFile .Exist () {
329- if err := overrideComposeFile .Remove (); err != nil {
330- return fmt .Errorf ("failed to remove existing override compose file: %w" , err )
331- }
335+ // Write additional file to override devices section in included compose files
336+ if e := generateServicesOverrideFile (services , servicesThatRequireDevices , devices , getCurrentUser (), groups , overrideComposeFile ); e != nil {
337+ return e
332338 }
333- if len (servicesThatRequireDevices ) > 0 {
334- // Write additiona file to override devices section in included compose files
335- if e := generateServicesOverrideFile (servicesThatRequireDevices , devices , overrideComposeFile ); e != nil {
336- return e
339+
340+ // Pre-provision containers required paths, if they do not exist.
341+ // This is required to preserve the host directory access rights for arduino user.
342+ // Otherwise, paths created by the container will have root:root ownership
343+ for _ , additionalComposeFile := range composeFiles {
344+ composeFilePath := additionalComposeFile .String ()
345+ slog .Debug ("Pre-provisioning volumes from compose file" , slog .String ("compose_file" , composeFilePath ))
346+
347+ volumes , err := extractVolumesFromComposeFile (composeFilePath )
348+ if err != nil {
349+ slog .Warn ("Failed to extract volumes from compose file" , slog .String ("compose_file" , composeFilePath ), slog .Any ("error" , err ))
350+ continue
337351 }
352+ provisionComposeVolumes (composeFilePath , volumes , app , mapped_env )
338353 }
354+
339355 // Done!
340356 return nil
341357}
@@ -364,20 +380,37 @@ func extracServicesFromComposeFile(composeFile *paths.Path) ([]string, error) {
364380 }
365381}
366382
367- func generateServicesOverrideFile (servicesThatRequireDevices []string , devices []string , overrideComposeFile * paths.Path ) error {
383+ func generateServicesOverrideFile (services []string , servicesThatRequireDevices []string , devices []string , user string , groups []string , overrideComposeFile * paths.Path ) error {
384+ if overrideComposeFile .Exist () {
385+ if err := overrideComposeFile .Remove (); err != nil {
386+ return fmt .Errorf ("failed to remove existing override compose file: %w" , err )
387+ }
388+ }
389+
390+ if len (services ) == 0 {
391+ slog .Debug ("No services to override, skipping override compose file generation" )
392+ return nil
393+ }
394+
368395 type serviceOverride struct {
369- Devices []string `yaml:"devices"`
396+ Devices * []string `yaml:"devices,omitempty"`
397+ User string `yaml:"user"`
398+ GroupAdd []string `yaml:"group_add"`
370399 }
371400 var overrideCompose struct {
372401 Services map [string ]serviceOverride `yaml:"services,omitempty"`
373402 }
374- overrideCompose .Services = make (map [string ]serviceOverride , len (servicesThatRequireDevices ))
375- for _ , svc := range servicesThatRequireDevices {
376- overrideCompose .Services [svc ] = serviceOverride {
377- Devices : devices ,
403+ overrideCompose .Services = make (map [string ]serviceOverride , len (services ))
404+ for _ , svc := range services {
405+ override := serviceOverride {
406+ User : user ,
407+ GroupAdd : groups ,
378408 }
409+ if slices .Contains (servicesThatRequireDevices , svc ) {
410+ override .Devices = & devices
411+ }
412+ overrideCompose .Services [svc ] = override
379413 }
380- slog .Debug ("Generating override compose file for devices" , slog .Any ("overrideCompose" , overrideCompose ), slog .Any ("devices" , devices ))
381414 writeOverrideCompose := func () error {
382415 data , err := yaml .Marshal (overrideCompose )
383416 if err != nil {
@@ -393,3 +426,105 @@ func generateServicesOverrideFile(servicesThatRequireDevices []string, devices [
393426 }
394427 return nil
395428}
429+
430+ var (
431+ // Regular expression to split on the first colon that is not followed by a hyphen
432+ volumeColonSplitRE = regexp .MustCompile (`:[^-]` )
433+ volumeAppHomeReplaceRE = regexp .MustCompile (`\$\{APP_HOME(:-\.)?\}` )
434+ volumePathReplaceRE = regexp .MustCompile (`\$\{([A-Z_-]+)(:-)?([\/a-zA-Z0-9._-]+)?\}` )
435+ )
436+
437+ // provisionComposeVolumes ensure we create the parent folder with the correct owner.
438+ // By default docker if it doesn't find the folder, it will create it as root.
439+ // We do not want that, to make sure to have it as `arduino:arduino` we have
440+ // to manually parse the volumes, and make sure to create the target dirs ourself.
441+ func provisionComposeVolumes (additionalComposeFile string , volumes []string , app * app.ArduinoApp , mapped_env map [string ]string ) {
442+ if len (volumes ) == 0 {
443+ slog .Debug ("No volumes to provision from compose file" , slog .String ("compose_file" , additionalComposeFile ))
444+ return
445+ }
446+
447+ slog .Debug ("Extracted volumes from compose file" , slog .String ("compose_file" , additionalComposeFile ), slog .Any ("volumes" , volumes ))
448+ for _ , volume := range volumes {
449+ volume = replaceDockerMacros (volume , app , mapped_env , additionalComposeFile )
450+ hostDirectory := paths .New (volume )
451+ if strings .Contains (volume , ":" ) {
452+ volumes := volumeColonSplitRE .Split (volume , - 1 )
453+ hostDirectory = paths .New (volumes [0 ])
454+ }
455+ if ! hostDirectory .Exist () {
456+ if err := hostDirectory .MkdirAll (); err != nil {
457+ slog .Warn ("Failed to create host directory for compose file" , slog .String ("compose_file" , additionalComposeFile ), slog .String ("host_directory" , hostDirectory .String ()), slog .Any ("error" , err ))
458+ } else {
459+ slog .Debug ("Pre-provisioning host directory for compose file" , slog .String ("compose_file" , additionalComposeFile ), slog .String ("host_directory" , hostDirectory .String ()))
460+ }
461+ }
462+ }
463+ }
464+
465+ func replaceDockerMacros (volume string , app * app.ArduinoApp , mapped_env map [string ]string , additionalComposeFile string ) string {
466+ // Replace ${APP_HOME} with the actual app path
467+ volume = volumeAppHomeReplaceRE .ReplaceAllString (volume , app .FullPath .String ())
468+ // Replace host volume directory with the actual path
469+ if volumePathReplaceRE .MatchString (volume ) {
470+ groups := volumePathReplaceRE .FindStringSubmatch (volume )
471+ // idx 0 is the full match, idx 1 is the variable name, idx 2 is the optional `:-` and idx 3 is the default value
472+ switch len (groups ) {
473+ case 2 :
474+ // Check if the environment variable is set
475+ if value , ok := mapped_env [groups [1 ]]; ok {
476+ volume = volumePathReplaceRE .ReplaceAllString (volume , value )
477+ } else {
478+ slog .Warn ("Environment variable not found for volume replacement" , slog .String ("variable" , groups [1 ]), slog .String ("compose_file" , additionalComposeFile ))
479+ }
480+ case 4 :
481+ // If the variable is not set, use the default value
482+ if value , ok := mapped_env [groups [1 ]]; ok {
483+ volume = volumePathReplaceRE .ReplaceAllString (volume , value )
484+ } else {
485+ volume = volumePathReplaceRE .ReplaceAllString (volume , groups [3 ])
486+ }
487+ default :
488+ slog .Warn ("Unexpected format for volume replacement" , slog .String ("volume" , volume ), slog .String ("compose_file" , additionalComposeFile ))
489+ }
490+ }
491+ return volume
492+ }
493+
494+ func extractVolumesFromComposeFile (additionalComposeFile string ) ([]string , error ) {
495+ content , err := os .ReadFile (additionalComposeFile )
496+ if err != nil {
497+ slog .Error ("Failed to read compose file" , slog .String ("compose_file" , additionalComposeFile ), slog .Any ("error" , err ))
498+ return nil , err
499+ }
500+ // Try with string syntax first
501+ type composeServices [T any ] struct {
502+ Services map [string ]struct {
503+ Volumes []T `yaml:"volumes"`
504+ } `yaml:"services"`
505+ }
506+ var index composeServices [string ]
507+ if err := yaml .Unmarshal (content , & index ); err != nil {
508+ var index composeServices [volume ]
509+ if err := yaml .Unmarshal (content , & index ); err != nil {
510+ return nil , fmt .Errorf ("failed to unmarshal compose file %s: %w" , additionalComposeFile , err )
511+ }
512+ volumes := make ([]string , 0 , len (index .Services ))
513+ for _ , svc := range index .Services {
514+ for _ , v := range svc .Volumes {
515+ if v .Type == "bind" {
516+ volumes = append (volumes , v .Source )
517+ } else {
518+ volumes = append (volumes , v .Target )
519+ }
520+ }
521+ }
522+ return volumes , nil
523+ }
524+
525+ volumes := make ([]string , 0 , len (index .Services ))
526+ for _ , svc := range index .Services {
527+ volumes = append (volumes , svc .Volumes ... )
528+ }
529+ return volumes , nil
530+ }
0 commit comments