Skip to content

Commit 9da6e0b

Browse files
committed
add destroy command first part
1 parent 17f979f commit 9da6e0b

File tree

5 files changed

+184
-6
lines changed

5 files changed

+184
-6
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func NewAppCmd(cfg config.Configuration) *cobra.Command {
3535
appCmd.AddCommand(newCreateCmd(cfg))
3636
appCmd.AddCommand(newStartCmd(cfg))
3737
appCmd.AddCommand(newStopCmd(cfg))
38+
appCmd.AddCommand(newDestroyCmd(cfg))
3839
appCmd.AddCommand(newRestartCmd(cfg))
3940
appCmd.AddCommand(newLogsCmd(cfg))
4041
appCmd.AddCommand(newListCmd(cfg))

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// This file is part of arduino-app-cli.
2+
//
3+
// Copyright 2025 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-app-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to license@arduino.cc.
15+
16+
package app
17+
18+
import (
19+
"context"
20+
"fmt"
21+
22+
"github.com/spf13/cobra"
23+
24+
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/completion"
25+
"github.com/arduino/arduino-app-cli/cmd/feedback"
26+
"github.com/arduino/arduino-app-cli/internal/orchestrator"
27+
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
28+
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
29+
)
30+
31+
func newDestroyCmd(cfg config.Configuration) *cobra.Command {
32+
return &cobra.Command{
33+
Use: "destroy app_path",
34+
Short: "Destroy an Arduino App",
35+
Args: cobra.MaximumNArgs(1),
36+
RunE: func(cmd *cobra.Command, args []string) error {
37+
if len(args) == 0 {
38+
return cmd.Help()
39+
}
40+
app, err := Load(args[0])
41+
if err != nil {
42+
return err
43+
}
44+
return destroyHandler(cmd.Context(), app)
45+
},
46+
ValidArgsFunction: completion.ApplicationNamesWithFilterFunc(cfg, func(apps orchestrator.AppInfo) bool {
47+
return apps.Status != orchestrator.StatusUninitialized
48+
}),
49+
}
50+
}
51+
52+
func destroyHandler(ctx context.Context, app app.ArduinoApp) error {
53+
out, _, getResult := feedback.OutputStreams()
54+
55+
for message := range orchestrator.DestroyAndCleanApp(ctx, app) {
56+
switch message.GetType() {
57+
case orchestrator.ProgressType:
58+
fmt.Fprintf(out, "Progress[%s]: %.0f%%\n", message.GetProgress().Name, message.GetProgress().Progress)
59+
case orchestrator.InfoType:
60+
fmt.Fprintln(out, "[INFO]", message.GetData())
61+
case orchestrator.ErrorType:
62+
feedback.Fatal(message.GetError().Error(), feedback.ErrGeneric)
63+
return nil
64+
}
65+
}
66+
outputResult := getResult()
67+
68+
feedback.PrintResult(destroyAppResult{
69+
AppName: app.Name,
70+
Status: "uninitialized",
71+
Output: outputResult,
72+
})
73+
return nil
74+
}
75+
76+
type destroyAppResult struct {
77+
AppName string `json:"appName"`
78+
Status string `json:"status"`
79+
Output *feedback.OutputStreamsResult `json:"output,omitempty"`
80+
}
81+
82+
func (r destroyAppResult) String() string {
83+
return fmt.Sprintf("✓ App '%q destroyed successfully.", r.AppName)
84+
}
85+
86+
func (r destroyAppResult) Data() interface{} {
87+
return r
88+
}

internal/orchestrator/helpers.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,10 @@ func getAppStatusByPath(
150150
return nil, fmt.Errorf("failed to list containers: %w", err)
151151
}
152152
if len(containers) == 0 {
153-
return nil, nil
153+
return &AppStatusInfo{
154+
AppPath: paths.New(pathLabel),
155+
Status: StatusUninitialized,
156+
}, nil
154157
}
155158

156159
app := parseAppStatus(containers)

internal/orchestrator/orchestrator.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,91 @@ func StopAndDestroyApp(ctx context.Context, dockerClient command.Cli, app app.Ar
450450
return stopAppWithCmd(ctx, dockerClient, app, "down")
451451
}
452452

453+
func DestroyAndCleanApp(ctx context.Context, app app.ArduinoApp) iter.Seq[StreamMessage] {
454+
return func(yield func(StreamMessage) bool) {
455+
456+
for msg := range destroyAppContainers(ctx, app) {
457+
if !yield(msg) {
458+
return
459+
}
460+
}
461+
for msg := range cleanAppCacheFiles(app) {
462+
if !yield(msg) {
463+
return
464+
}
465+
}
466+
}
467+
}
468+
469+
func destroyAppContainers(ctx context.Context, app app.ArduinoApp) iter.Seq[StreamMessage] {
470+
return func(yield func(StreamMessage) bool) {
471+
ctx, cancel := context.WithCancel(ctx)
472+
defer cancel()
473+
474+
if !yield(StreamMessage{data: fmt.Sprintf("Destroying app %q containers and data...", app.Name)}) {
475+
return
476+
}
477+
callbackWriter := NewCallbackWriter(func(line string) {
478+
if !yield(StreamMessage{data: line}) {
479+
cancel()
480+
return
481+
}
482+
})
483+
if _, ok := app.GetSketchPath(); ok {
484+
if err := micro.Disable(); err != nil {
485+
slog.Debug("unable to disable micro (might be already stopped)", slog.String("error", err.Error()))
486+
}
487+
}
488+
if app.MainPythonFile != nil {
489+
mainCompose := app.AppComposeFilePath()
490+
if mainCompose.Exist() {
491+
process, err := paths.NewProcess(
492+
nil,
493+
"docker", "compose",
494+
"-f", mainCompose.String(),
495+
"down",
496+
"--volumes",
497+
"--remove-orphans",
498+
fmt.Sprintf("--timeout=%d", DefaultDockerStopTimeoutSeconds),
499+
)
500+
501+
if err != nil {
502+
yield(StreamMessage{error: err})
503+
return
504+
}
505+
506+
process.RedirectStderrTo(callbackWriter)
507+
process.RedirectStdoutTo(callbackWriter)
508+
if err := process.RunWithinContext(ctx); err != nil {
509+
yield(StreamMessage{error: fmt.Errorf("failed to destroy containers: %w", err)})
510+
return
511+
}
512+
}
513+
}
514+
yield(StreamMessage{data: "App containers and volumes removed."})
515+
}
516+
}
517+
518+
func cleanAppCacheFiles(app app.ArduinoApp) iter.Seq[StreamMessage] {
519+
return func(yield func(StreamMessage) bool) {
520+
cachePath := app.FullPath.Join(".cache")
521+
522+
if exists, _ := cachePath.ExistCheck(); !exists {
523+
yield(StreamMessage{data: "No cache to clean."})
524+
return
525+
}
526+
if !yield(StreamMessage{data: "Removing app cache files..."}) {
527+
return
528+
}
529+
slog.Debug("removing app cache", slog.String("path", cachePath.String()))
530+
if err := cachePath.RemoveAll(); err != nil {
531+
yield(StreamMessage{error: fmt.Errorf("unable to remove app cache: %w", err)})
532+
return
533+
}
534+
yield(StreamMessage{data: "Cache removed successfully."})
535+
}
536+
}
537+
453538
func RestartApp(
454539
ctx context.Context,
455540
docker command.Cli,

internal/orchestrator/status.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import (
2424
type Status string
2525

2626
const (
27-
StatusStarting Status = "starting"
28-
StatusRunning Status = "running"
29-
StatusStopping Status = "stopping"
30-
StatusStopped Status = "stopped"
31-
StatusFailed Status = "failed"
27+
StatusStarting Status = "starting"
28+
StatusRunning Status = "running"
29+
StatusStopping Status = "stopping"
30+
StatusStopped Status = "stopped"
31+
StatusFailed Status = "failed"
32+
StatusUninitialized Status = "uninitialized"
3233
)
3334

3435
func StatusFromDockerState(s container.ContainerState) Status {

0 commit comments

Comments
 (0)