Skip to content

Commit 74ec746

Browse files
mirkoCrobumirkoCrobu
andauthored
Feat/327 cleanup operation (#616)
Co-authored-by: mirkoCrobu <mirkocrobu@NB-0531.localdomain>
1 parent 941085a commit 74ec746

File tree

4 files changed

+183
-5
lines changed

4 files changed

+183
-5
lines changed

cmd/arduino-app-cli/system/system.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"strings"
66

7+
"github.com/docker/cli/cli/command"
78
"github.com/spf13/cobra"
89

910
"github.com/bcmi-labs/orchestrator/cmd/arduino-app-cli/internal/servicelocator"
@@ -22,6 +23,7 @@ func NewSystemCmd(cfg config.Configuration) *cobra.Command {
2223

2324
cmd.AddCommand(newDownloadImage(cfg))
2425
cmd.AddCommand(newUpdateCmd())
26+
cmd.AddCommand(newCleanUpCmd(cfg, servicelocator.GetDockerClient()))
2527

2628
return cmd
2729
}
@@ -116,3 +118,29 @@ func getFilterFunc(onlyArduino bool) func(p update.UpgradablePackage) bool {
116118
}
117119
return update.MatchAllPackages
118120
}
121+
122+
func newCleanUpCmd(cfg config.Configuration, docker command.Cli) *cobra.Command {
123+
cmd := &cobra.Command{
124+
Use: "cleanup",
125+
Short: "Removes unused and obsolete application images to free up disk space.",
126+
Args: cobra.ExactArgs(0),
127+
RunE: func(cmd *cobra.Command, _ []string) error {
128+
129+
staticStore := servicelocator.GetStaticStore()
130+
131+
feedback.Printf("Running cleanup...")
132+
result, err := orchestrator.SystemCleanupSoft(cmd.Context(), cfg, staticStore, docker)
133+
if err != nil {
134+
return err
135+
}
136+
137+
if result > 0 {
138+
feedback.Printf("Cleanup successful. Freed up %d bytes of disk space.", result)
139+
} else {
140+
feedback.Printf("No removable images found.")
141+
}
142+
return nil
143+
},
144+
}
145+
return cmd
146+
}

internal/orchestrator/orchestrator.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ func getVideoDevices() map[int]string {
297297
return deviceMap
298298
}
299299

300-
func StopApp(ctx context.Context, app app.ArduinoApp) iter.Seq[StreamMessage] {
300+
func stopAppWithCmd(ctx context.Context, app app.ArduinoApp, cmd string) iter.Seq[StreamMessage] {
301301
return func(yield func(StreamMessage) bool) {
302302
ctx, cancel := context.WithCancel(ctx)
303303
defer cancel()
@@ -324,7 +324,7 @@ func StopApp(ctx context.Context, app app.ArduinoApp) iter.Seq[StreamMessage] {
324324
mainCompose := app.AppComposeFilePath()
325325
// In case the app was never started
326326
if mainCompose.Exist() {
327-
process, err := paths.NewProcess(nil, "docker", "compose", "-f", mainCompose.String(), "stop", fmt.Sprintf("--timeout=%d", DefaultDockerStopTimeoutSeconds))
327+
process, err := paths.NewProcess(nil, "docker", "compose", "-f", mainCompose.String(), cmd, fmt.Sprintf("--timeout=%d", DefaultDockerStopTimeoutSeconds))
328328
if err != nil {
329329
yield(StreamMessage{error: err})
330330
return
@@ -341,6 +341,14 @@ func StopApp(ctx context.Context, app app.ArduinoApp) iter.Seq[StreamMessage] {
341341
}
342342
}
343343

344+
func StopApp(ctx context.Context, app app.ArduinoApp) iter.Seq[StreamMessage] {
345+
return stopAppWithCmd(ctx, app, "stop")
346+
}
347+
348+
func StopAndDestroyApp(ctx context.Context, app app.ArduinoApp) iter.Seq[StreamMessage] {
349+
return stopAppWithCmd(ctx, app, "down")
350+
}
351+
344352
func StartDefaultApp(
345353
ctx context.Context,
346354
docker command.Cli,

internal/orchestrator/system.go

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import (
44
"bytes"
55
"context"
66
"encoding/json"
7+
"fmt"
78
"os"
89
"slices"
10+
"strconv"
911
"strings"
1012

1113
"github.com/arduino/go-paths-helper"
1214
"github.com/compose-spec/compose-go/v2/loader"
1315
"github.com/compose-spec/compose-go/v2/types"
16+
"github.com/docker/cli/cli/command"
1417
"go.bug.st/f"
1518

1619
"github.com/bcmi-labs/orchestrator/cmd/feedback"
@@ -42,7 +45,9 @@ func SystemInit(ctx context.Context, cfg config.Configuration, staticStore *stor
4245
feedback.Fatal(err.Error(), feedback.ErrBadArgument)
4346
return nil
4447
}
48+
4549
for _, container := range containersToPreinstall {
50+
4651
cmd, err := paths.NewProcess(nil, "docker", "pull", container)
4752
if err != nil {
4853
return err
@@ -57,11 +62,13 @@ func SystemInit(ctx context.Context, cfg config.Configuration, staticStore *stor
5762
}
5863

5964
// listImagesAlreadyPulled
65+
// TODO make reference constant in a dedicated file as single source of truth
6066
func listImagesAlreadyPulled(ctx context.Context) ([]string, error) {
6167
cmd, err := paths.NewProcess(nil,
6268
"docker", "images", "--format", "json",
6369
"-f", "reference=ghcr.io/bcmi-labs/*",
64-
"-f", "reference=public.ecr.aws/*",
70+
"-f", "reference=public.ecr.aws/arduino/app-bricks/*",
71+
"-f", "reference=influxdb",
6572
)
6673
if err != nil {
6774
return nil, err
@@ -135,3 +142,97 @@ func parseAllModelsRunnerImageTag(staticStore *store.StaticStore) ([]string, err
135142

136143
return f.Uniq(result), nil
137144
}
145+
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+
159+
imagesToRemove := slices.DeleteFunc(allImages, func(v string) bool {
160+
return slices.Contains(containersMustStay, v)
161+
})
162+
163+
if len(imagesToRemove) == 0 {
164+
return totalCleaned, nil
165+
}
166+
167+
runningApp, err := getRunningApp(ctx, docker.Client())
168+
if err != nil {
169+
return totalCleaned, fmt.Errorf("failed to get running app: %w", err)
170+
}
171+
if runningApp != nil {
172+
for item := range StopAndDestroyApp(ctx, *runningApp) {
173+
if item.GetType() == ErrorType {
174+
return totalCleaned, item.GetError()
175+
}
176+
}
177+
}
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
186+
}
187+
return totalCleaned, nil
188+
}
189+
190+
func removeImage(ctx context.Context, imageName string) (int64, error) {
191+
imageSize, err := getImageSize(imageName, ctx)
192+
if err != nil {
193+
return 0, fmt.Errorf("failed to get size of image %s: %w", imageName, err)
194+
}
195+
196+
cmd, err := paths.NewProcess(nil, "docker", "rmi", "-f", imageName)
197+
if err != nil {
198+
return 0, fmt.Errorf("failed to create command to remove docker image %s: %w", imageName, err)
199+
}
200+
201+
if err := cmd.RunWithinContext(ctx); err != nil {
202+
return 0, fmt.Errorf("failed to remove image %s: %v", imageName, err)
203+
}
204+
205+
return imageSize, nil
206+
}
207+
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
216+
}
217+
trimmedOutput := bytes.TrimSpace(containersize)
218+
219+
sizeInt64, err := strconv.ParseInt(string(trimmedOutput), 10, 64)
220+
if err != nil {
221+
return 0, err
222+
}
223+
224+
return sizeInt64, nil
225+
}
226+
227+
// imgages required by the system
228+
func getRequiredImages(cfg config.Configuration, staticStore *store.StaticStore) ([]string, error) {
229+
requiredImages := []string{cfg.PythonImage}
230+
231+
modelsRunnersContainers, err := parseAllModelsRunnerImageTag(staticStore)
232+
if err != nil {
233+
return nil, fmt.Errorf("failed to parse models runner images: %w", err)
234+
}
235+
236+
requiredImages = append(requiredImages, modelsRunnersContainers...)
237+
return requiredImages, nil
238+
}

internal/update/apt/service.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ func (s *Service) UpgradePackages(ctx context.Context, names []string) (<-chan u
7272
defer cancel()
7373

7474
eventsCh <- update.Event{Type: update.StartEvent, Data: "Upgrade is starting"}
75-
7675
stream := runUpgradeCommand(ctx, names)
7776
for line, err := range stream {
7877
if err != nil {
@@ -86,6 +85,19 @@ func (s *Service) UpgradePackages(ctx context.Context, names []string) (<-chan u
8685
}
8786
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
8887
}
88+
// TEMPORARY PATCH: stopping and destroying docker containers and images since IDE does not implement it yet.
89+
// TODO: Remove this workaround once IDE implements it.
90+
// Tracking issue: https://github.com/bcmi-labs/orchestrator/issues/623
91+
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: "Stop and destroy docker containers and images ..."}
92+
streamCleanup := cleanupDockerContainers(ctx)
93+
for line, err := range streamCleanup {
94+
if err != nil {
95+
// TODO: maybe we should retun an error or a better feedback to the user?
96+
// currently, we just log the error and continue considenring not blocking
97+
slog.Error("Error stopping and destroying docker containers", "error", err)
98+
}
99+
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
100+
}
89101

90102
// TEMPORARY PATCH: Install the latest docker images and show the logs to the users.
91103
// TODO: Remove this workaround once docker image versions are no longer hardcoded in arduino-app-cli.
@@ -106,7 +118,6 @@ func (s *Service) UpgradePackages(ctx context.Context, names []string) (<-chan u
106118
}
107119
eventsCh <- update.Event{Type: update.UpgradeLineEvent, Data: line}
108120
}
109-
110121
eventsCh <- update.Event{Type: update.RestartEvent, Data: "Upgrade completed. Restarting ..."}
111122

112123
err := restartServices(ctx)
@@ -203,6 +214,36 @@ func pullDockerImages(ctx context.Context) iter.Seq2[string, error] {
203214
}
204215
}
205216

217+
// Remove all stopped containers
218+
func cleanupDockerContainers(ctx context.Context) iter.Seq2[string, error] {
219+
return func(yield func(string, error) bool) {
220+
cmd, err := paths.NewProcess(nil, "arduino-app-cli", "system", "cleanup")
221+
if err != nil {
222+
_ = yield("", err)
223+
return
224+
}
225+
226+
stdout := orchestrator.NewCallbackWriter(func(line string) {
227+
if !yield(line, nil) {
228+
err := cmd.Kill()
229+
if err != nil {
230+
slog.Error("Failed to kill 'arduino-app-cli system cleanup' command", slog.String("error", err.Error()))
231+
}
232+
return
233+
}
234+
})
235+
236+
cmd.RedirectStderrTo(stdout)
237+
cmd.RedirectStdoutTo(stdout)
238+
239+
err = cmd.RunWithinContext(ctx)
240+
if err != nil {
241+
_ = yield("", err)
242+
return
243+
}
244+
}
245+
}
246+
206247
// RestartServices restarts services that need to be restarted after an upgrade.
207248
// It uses the `needrestart` command to determine which services need to be restarted.
208249
// It returns an error if the command fails to start or if it fails to wait for the command to finish.

0 commit comments

Comments
 (0)