11package orchestrator
22
33import (
4- "bytes "
4+ "bufio "
55 "context"
66 "encoding/json"
77 "fmt"
8+ "io"
89 "os"
910 "slices"
10- "strconv"
1111 "strings"
1212
13- "github.com/arduino/go-paths-helper"
1413 "github.com/compose-spec/compose-go/v2/loader"
1514 "github.com/compose-spec/compose-go/v2/types"
1615 "github.com/docker/cli/cli/command"
16+ "github.com/docker/docker/api/types/container"
17+ "github.com/docker/docker/api/types/filters"
18+ "github.com/docker/docker/api/types/image"
19+ dockerClient "github.com/docker/docker/client"
1720 "go.bug.st/f"
1821
1922 "github.com/arduino/arduino-app-cli/cmd/feedback"
@@ -22,15 +25,15 @@ import (
2225)
2326
2427// SystemInit pulls necessary Docker images.
25- func SystemInit (ctx context.Context , cfg config.Configuration , staticStore * store.StaticStore ) error {
28+ func SystemInit (ctx context.Context , cfg config.Configuration , staticStore * store.StaticStore , docker * command. DockerCli ) error {
2629 containersToPreinstall := []string {cfg .PythonImage }
2730 additionalContainers , err := parseAllModelsRunnerImageTag (staticStore )
2831 if err != nil {
2932 return err
3033 }
3134 containersToPreinstall = append (containersToPreinstall , additionalContainers ... )
3235
33- pulledImages , err := listImagesAlreadyPulled (ctx )
36+ pulledImages , err := listImagesAlreadyPulled (ctx , docker . Client () )
3437 if err != nil {
3538 return err
3639 }
@@ -47,64 +50,73 @@ func SystemInit(ctx context.Context, cfg config.Configuration, staticStore *stor
4750 }
4851
4952 for _ , container := range containersToPreinstall {
53+ feedback .Printf ("Pulling container image %s ..." , container )
54+ if err := pullImage (ctx , stdout , docker .Client (), container ); err != nil {
55+ feedback .Printf ("Warning: failed to read image pull response - %v" , err )
56+ }
57+ }
5058
51- cmd , err := paths .NewProcess (nil , "docker" , "pull" , container )
52- if err != nil {
53- return err
59+ return nil
60+ }
61+
62+ func pullImage (ctx context.Context , stdout io.Writer , docker dockerClient.APIClient , imageName string ) error {
63+ out , err := docker .ImagePull (ctx , imageName , image.PullOptions {})
64+ if err != nil {
65+ return err
66+ }
67+ defer out .Close ()
68+
69+ scanner := bufio .NewScanner (out )
70+ for scanner .Scan () {
71+ type Payload struct {
72+ Status string `json:"status"`
73+ Progress string `json:"progress"`
74+ ID string `json:"id"`
5475 }
55- cmd .RedirectStderrTo (stdout )
56- cmd .RedirectStdoutTo (stdout )
57- if err := cmd .RunWithinContext (ctx ); err != nil {
58- return err
76+
77+ var payload Payload
78+ if err := json .Unmarshal (scanner .Bytes (), & payload ); err == nil {
79+ if payload .Status != "" {
80+ fmt .Fprintf (stdout , "%s" , payload .Status )
81+ }
82+ if payload .Progress != "" {
83+ fmt .Fprintf (stdout , "[%s] %s\r " , payload .ID , payload .Progress )
84+ } else {
85+ fmt .Fprintln (stdout )
86+ }
5987 }
6088 }
89+ if err := scanner .Err (); err != nil {
90+ return err
91+ }
6192 return nil
6293}
6394
95+ // Container images matching this list will be pulled by 'system init' and included in the Linux images.
96+ var imagePrefixes = []string {"ghcr.io/bcmi-labs/" , "public.ecr.aws/arduino/" , "influxdb" }
97+
6498// listImagesAlreadyPulled
6599// TODO make reference constant in a dedicated file as single source of truth
66- func listImagesAlreadyPulled (ctx context.Context ) ([]string , error ) {
67- cmd , err := paths .NewProcess (nil ,
68- "docker" , "images" , "--format" , "json" ,
69- "-f" , "reference=ghcr.io/bcmi-labs/*" ,
70- "-f" , "reference=public.ecr.aws/arduino/app-bricks/*" ,
71- "-f" , "reference=influxdb" ,
72- )
73- if err != nil {
74- return nil , err
75- }
76-
77- // Capture the output to check if the image exists
78- stdout , _ , err := cmd .RunAndCaptureOutput (ctx )
100+ func listImagesAlreadyPulled (ctx context.Context , docker dockerClient.APIClient ) ([]string , error ) {
101+ images , err := docker .ImageList (ctx , image.ListOptions {})
79102 if err != nil {
80103 return nil , err
81104 }
82105
83- type dockerImage struct {
84- Repository string `json:"Repository"`
85- Tag string `json:"Tag"`
86- }
87- var resp dockerImage
88- result := []string {}
89- for img := range bytes .Lines (stdout ) {
90- if len (img ) == 0 {
91- continue
92- }
93- if err := json .Unmarshal (img , & resp ); err != nil {
94- return nil , err
95- }
96- if resp .Tag == "<none>" {
97- continue
106+ result := make ([]string , 0 , len (images ))
107+ for _ , image := range images {
108+ for _ , tag := range image .RepoTags {
109+ for _ , prefix := range imagePrefixes {
110+ if strings .HasPrefix (tag , prefix ) {
111+ result = append (result , tag )
112+ }
113+ }
98114 }
99- result = append (result , resp .Repository + ":" + resp .Tag )
100115 }
101116
102117 return result , nil
103118}
104119
105- // Container images matching this list will be pulled by 'system init' and included in the Linux images.
106- var imagePrefixes = []string {"ghcr.io/bcmi-labs/" , "public.ecr.aws/arduino/" , "influxdb" }
107-
108120func parseAllModelsRunnerImageTag (staticStore * store.StaticStore ) ([]string , error ) {
109121 composePath := staticStore .GetComposeFolder ()
110122 brickNamespace := "arduino"
@@ -143,85 +155,84 @@ func parseAllModelsRunnerImageTag(staticStore *store.StaticStore) ([]string, err
143155 return f .Uniq (result ), nil
144156}
145157
146- func SystemCleanupSoft (ctx context.Context , cfg config.Configuration , staticStore * store.StaticStore , docker command.Cli ) (int64 , error ) {
147- totalCleaned := int64 (0 )
148-
149- containersMustStay , err := getRequiredImages (cfg , staticStore )
150- if err != nil {
151- return totalCleaned , err
152- }
153-
154- allImages , err := listImagesAlreadyPulled (ctx )
155- if err != nil {
156- return totalCleaned , err
157- }
158+ type SystemCleanupResult struct {
159+ ContainersRemoved int
160+ ImagesRemoved int
161+ RunningAppRemoved bool
162+ SpaceFreed int64 // in bytes
163+ }
158164
159- imagesToRemove := slices . DeleteFunc ( allImages , func ( v string ) bool {
160- return slices . Contains ( containersMustStay , v )
161- })
165+ func ( s SystemCleanupResult ) IsEmpty ( ) bool {
166+ return s == SystemCleanupResult {}
167+ }
162168
163- if len (imagesToRemove ) == 0 {
164- return totalCleaned , nil
165- }
169+ // SystemCleanup removes dangling containers and unused images.
170+ // Also running apps are stopped and removed.
171+ func SystemCleanup (ctx context.Context , cfg config.Configuration , staticStore * store.StaticStore , docker command.Cli ) (SystemCleanupResult , error ) {
172+ var result SystemCleanupResult
166173
174+ // Remove running app and dangling containers
167175 runningApp , err := getRunningApp (ctx , docker .Client ())
168176 if err != nil {
169- return totalCleaned , fmt . Errorf ( " failed to get running app: %w " , err )
177+ feedback . Printf ( "Warning: failed to get running app - %v " , err )
170178 }
171179 if runningApp != nil {
172180 for item := range StopAndDestroyApp (ctx , * runningApp ) {
173181 if item .GetType () == ErrorType {
174- return totalCleaned , item .GetError ()
182+ feedback .Printf ("Warning: failed to stop and destroy running app - %v" , item .GetError ())
183+ break
175184 }
176185 }
186+ result .RunningAppRemoved = true
177187 }
178-
179- for _ , container := range imagesToRemove {
180- imageSize , err := removeImage (ctx , container )
181- if err != nil {
182- feedback .Printf ("Warning: failed to remove image %s - %v" , container , err )
183- continue
184- }
185- totalCleaned += imageSize
188+ if count , err := removeDanglingContainers (ctx , docker .Client ()); err != nil {
189+ feedback .Printf ("Warning: failed to remove dangling containers - %v" , err )
190+ } else {
191+ result .ContainersRemoved = count
186192 }
187- return totalCleaned , nil
188- }
189193
190- func removeImage ( ctx context. Context , imageName string ) ( int64 , error ) {
191- imageSize , err := getImageSize ( imageName , ctx )
194+ // Remove unused images
195+ containersMustStay , err := getRequiredImages ( cfg , staticStore )
192196 if err != nil {
193- return 0 , fmt . Errorf ( "failed to get size of image %s: %w" , imageName , err )
197+ return result , err
194198 }
195-
196- cmd , err := paths .NewProcess (nil , "docker" , "rmi" , "-f" , imageName )
199+ allImages , err := listImagesAlreadyPulled (ctx , docker .Client ())
197200 if err != nil {
198- return 0 , fmt . Errorf ( "failed to create command to remove docker image %s: %w" , imageName , err )
201+ return result , err
199202 }
203+ imagesToRemove := slices .DeleteFunc (allImages , func (v string ) bool {
204+ return slices .Contains (containersMustStay , v )
205+ })
200206
201- if err := cmd .RunWithinContext (ctx ); err != nil {
202- return 0 , fmt .Errorf ("failed to remove image %s: %v" , imageName , err )
207+ for _ , image := range imagesToRemove {
208+ imageSize , err := removeImage (ctx , docker .Client (), image )
209+ if err != nil {
210+ feedback .Printf ("Warning: failed to remove image %s - %v" , image , err )
211+ continue
212+ }
213+ result .SpaceFreed += imageSize
214+ result .ImagesRemoved ++
203215 }
204216
205- return imageSize , nil
217+ return result , nil
206218}
207219
208- func getImageSize (container string , ctx context.Context ) (int64 , error ) {
209- cmdImageSize , err := paths .NewProcess (nil , "docker" , "image" , "inspect" , container , "--format" , "{{.Size}}" )
210- if err != nil {
211- return 0 , err
212- }
213- containersize , err := cmdImageSize .RunAndCaptureCombinedOutput (ctx )
214- if err != nil {
215- return 0 , err
220+ func removeImage (ctx context.Context , docker dockerClient.APIClient , imageName string ) (int64 , error ) {
221+ var size int64
222+ if info , err := docker .ImageInspect (ctx , imageName ); err != nil {
223+ feedback .Printf ("Warning: failed to inspect image %s - %v" , imageName , err )
224+ } else {
225+ size = info .Size
216226 }
217- trimmedOutput := bytes .TrimSpace (containersize )
218227
219- sizeInt64 , err := strconv .ParseInt (string (trimmedOutput ), 10 , 64 )
220- if err != nil {
221- return 0 , err
228+ if _ , err := docker .ImageRemove (ctx , imageName , image.RemoveOptions {
229+ Force : true ,
230+ PruneChildren : true ,
231+ }); err != nil {
232+ return 0 , fmt .Errorf ("failed to remove image %s: %w" , imageName , err )
222233 }
223234
224- return sizeInt64 , nil
235+ return size , nil
225236}
226237
227238// imgages required by the system
@@ -236,3 +247,25 @@ func getRequiredImages(cfg config.Configuration, staticStore *store.StaticStore)
236247 requiredImages = append (requiredImages , modelsRunnersContainers ... )
237248 return requiredImages , nil
238249}
250+
251+ func removeDanglingContainers (ctx context.Context , docker dockerClient.APIClient ) (int , error ) {
252+ containers , err := docker .ContainerList (ctx , container.ListOptions {
253+ All : true ,
254+ Filters : filters .NewArgs (filters .Arg ("label" , DockerAppLabel + "=true" )),
255+ })
256+ if err != nil {
257+ return 0 , fmt .Errorf ("failed to list containers: %w" , err )
258+ }
259+
260+ var counter int
261+ for _ , info := range containers {
262+ if err := docker .ContainerRemove (ctx , info .ID , container.RemoveOptions {
263+ Force : true ,
264+ RemoveVolumes : true ,
265+ }); err != nil {
266+ return 0 , fmt .Errorf ("failed to remove container %s: %w" , info .ID , err )
267+ }
268+ counter ++
269+ }
270+ return counter , nil
271+ }
0 commit comments