Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/arduino-app-cli/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func NewAppCmd(cfg config.Configuration) *cobra.Command {
appCmd.AddCommand(newCreateCmd(cfg))
appCmd.AddCommand(newStartCmd(cfg))
appCmd.AddCommand(newStopCmd(cfg))
appCmd.AddCommand(newDestroyCmd(cfg))
appCmd.AddCommand(newRestartCmd(cfg))
appCmd.AddCommand(newLogsCmd(cfg))
appCmd.AddCommand(newListCmd(cfg))
Expand Down
88 changes: 88 additions & 0 deletions cmd/arduino-app-cli/app/destroy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// This file is part of arduino-app-cli.
//
// Copyright 2025 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-app-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to [email protected].

package app

import (
"context"
"fmt"

"github.com/spf13/cobra"

"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/completion"
"github.com/arduino/arduino-app-cli/cmd/feedback"
"github.com/arduino/arduino-app-cli/internal/orchestrator"
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
)

func newDestroyCmd(cfg config.Configuration) *cobra.Command {
return &cobra.Command{
Use: "destroy app_path",
Short: "Destroy an Arduino App",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
app, err := Load(args[0])
if err != nil {
return err
}
return destroyHandler(cmd.Context(), app)
},
ValidArgsFunction: completion.ApplicationNamesWithFilterFunc(cfg, func(apps orchestrator.AppInfo) bool {
return apps.Status != orchestrator.StatusUninitialized
}),
}
}

func destroyHandler(ctx context.Context, app app.ArduinoApp) error {
out, _, getResult := feedback.OutputStreams()

for message := range orchestrator.DestroyAndCleanApp(ctx, app) {
switch message.GetType() {
case orchestrator.ProgressType:
fmt.Fprintf(out, "Progress[%s]: %.0f%%\n", message.GetProgress().Name, message.GetProgress().Progress)
case orchestrator.InfoType:
fmt.Fprintln(out, "[INFO]", message.GetData())
case orchestrator.ErrorType:
feedback.Fatal(message.GetError().Error(), feedback.ErrGeneric)
return nil
}
}
outputResult := getResult()

feedback.PrintResult(destroyAppResult{
AppName: app.Name,
Status: "uninitialized",
Output: outputResult,
})
return nil
}

type destroyAppResult struct {
AppName string `json:"appName"`
Status string `json:"status"`
Output *feedback.OutputStreamsResult `json:"output,omitempty"`
}

func (r destroyAppResult) String() string {
return fmt.Sprintf("✓ App '%q destroyed successfully.", r.AppName)
}

func (r destroyAppResult) Data() interface{} {
return r
}
5 changes: 4 additions & 1 deletion internal/orchestrator/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@ func getAppStatusByPath(
return nil, fmt.Errorf("failed to list containers: %w", err)
}
if len(containers) == 0 {
return nil, nil
return &AppStatusInfo{
AppPath: paths.New(pathLabel),
Status: StatusUninitialized,
}, nil
}

app := parseAppStatus(containers)
Expand Down
85 changes: 85 additions & 0 deletions internal/orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,91 @@ func StopAndDestroyApp(ctx context.Context, dockerClient command.Cli, app app.Ar
return stopAppWithCmd(ctx, dockerClient, app, "down")
}

func DestroyAndCleanApp(ctx context.Context, app app.ArduinoApp) iter.Seq[StreamMessage] {
return func(yield func(StreamMessage) bool) {

for msg := range destroyAppContainers(ctx, app) {
if !yield(msg) {
return
}
}
for msg := range cleanAppCacheFiles(app) {
if !yield(msg) {
return
}
}
}
}

func destroyAppContainers(ctx context.Context, app app.ArduinoApp) iter.Seq[StreamMessage] {
return func(yield func(StreamMessage) bool) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()

if !yield(StreamMessage{data: fmt.Sprintf("Destroying app %q containers and data...", app.Name)}) {
return
}
callbackWriter := NewCallbackWriter(func(line string) {
if !yield(StreamMessage{data: line}) {
cancel()
return
}
})
if _, ok := app.GetSketchPath(); ok {
if err := micro.Disable(); err != nil {
slog.Debug("unable to disable micro (might be already stopped)", slog.String("error", err.Error()))
}
}
if app.MainPythonFile != nil {
mainCompose := app.AppComposeFilePath()
if mainCompose.Exist() {
process, err := paths.NewProcess(
nil,
"docker", "compose",
"-f", mainCompose.String(),
"down",
"--volumes",
"--remove-orphans",
fmt.Sprintf("--timeout=%d", DefaultDockerStopTimeoutSeconds),
)

if err != nil {
yield(StreamMessage{error: err})
return
}

process.RedirectStderrTo(callbackWriter)
process.RedirectStdoutTo(callbackWriter)
if err := process.RunWithinContext(ctx); err != nil {
yield(StreamMessage{error: fmt.Errorf("failed to destroy containers: %w", err)})
return
}
}
}
yield(StreamMessage{data: "App containers and volumes removed."})
}
}

func cleanAppCacheFiles(app app.ArduinoApp) iter.Seq[StreamMessage] {
return func(yield func(StreamMessage) bool) {
cachePath := app.FullPath.Join(".cache")

if exists, _ := cachePath.ExistCheck(); !exists {
yield(StreamMessage{data: "No cache to clean."})
return
}
if !yield(StreamMessage{data: "Removing app cache files..."}) {
return
}
slog.Debug("removing app cache", slog.String("path", cachePath.String()))
if err := cachePath.RemoveAll(); err != nil {
yield(StreamMessage{error: fmt.Errorf("unable to remove app cache: %w", err)})
return
}
yield(StreamMessage{data: "Cache removed successfully."})
}
}

func RestartApp(
ctx context.Context,
docker command.Cli,
Expand Down
11 changes: 6 additions & 5 deletions internal/orchestrator/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import (
type Status string

const (
StatusStarting Status = "starting"
StatusRunning Status = "running"
StatusStopping Status = "stopping"
StatusStopped Status = "stopped"
StatusFailed Status = "failed"
StatusStarting Status = "starting"
StatusRunning Status = "running"
StatusStopping Status = "stopping"
StatusStopped Status = "stopped"
StatusFailed Status = "failed"
StatusUninitialized Status = "uninitialized"
)

func StatusFromDockerState(s container.ContainerState) Status {
Expand Down