Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func restartHandler(ctx context.Context, cfg config.Configuration, app app.Ardui
app,
cfg,
servicelocator.GetStaticStore(),
servicelocator.GetAppIDProvider(),
)
for message := range stream {
switch message.GetType() {
Expand Down
1 change: 1 addition & 0 deletions cmd/arduino-app-cli/app/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func startHandler(ctx context.Context, cfg config.Configuration, app app.Arduino
app,
cfg,
servicelocator.GetStaticStore(),
servicelocator.GetAppIDProvider(),
)
for message := range stream {
switch message.GetType() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var (
return f.Must(orchestrator.NewProvision(
GetDockerClient(),
globalConfig,
GetAppIDProvider(),
))
})

Expand Down
4 changes: 4 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ func NewHTTPRouter(
mux.Handle("PATCH /v1/apps/{appID}/bricks/{brickID}", handlers.HandleBrickUpdates(brickService, idProvider))
mux.Handle("DELETE /v1/apps/{appID}/bricks/{brickID}", handlers.HandleBrickDelete(brickService, idProvider))

mux.Handle("GET /v1/apps/{appID}/secrets", handlers.HandleSecretsList(cfg, idProvider))
mux.Handle("PUT /v1/apps/{appID}/secrets/{secretName}", handlers.HandleSecretsUpdate(cfg, idProvider))
mux.Handle("DELETE /v1/apps/{appID}/secrets/{secretName}", handlers.HandleSecretsDelete(cfg, idProvider))

mux.Handle("GET /v1/docs/", http.StripPrefix("/v1/docs/", handlers.DocsServer(docsFS)))

mux.Handle("GET /v1/monitor/ws", handlers.HandleMonitorWS(allowedOrigins))
Expand Down
2 changes: 1 addition & 1 deletion internal/api/handlers/app_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func HandleAppStart(
type log struct {
Message string `json:"message"`
}
for item := range orchestrator.StartApp(r.Context(), dockerCli, provisioner, modelsIndex, bricksIndex, app, cfg, staticStore) {
for item := range orchestrator.StartApp(r.Context(), dockerCli, provisioner, modelsIndex, bricksIndex, app, cfg, staticStore, idProvider) {
switch item.GetType() {
case orchestrator.ProgressType:
sseStream.Send(render.SSEEvent{Type: "progress", Data: progress(*item.GetProgress())})
Expand Down
79 changes: 79 additions & 0 deletions internal/api/handlers/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package handlers

import (
"io"
"log/slog"
"net/http"

"github.com/arduino/arduino-app-cli/internal/api/models"
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
"github.com/arduino/arduino-app-cli/internal/orchestrator/secrets"
"github.com/arduino/arduino-app-cli/internal/render"
)

func HandleSecretsList(cfg config.Configuration, idProvider *app.IDProvider) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
appID, err := idProvider.IDFromBase64(r.PathValue("appID"))
if err != nil {
render.EncodeResponse(w, http.StatusPreconditionFailed, models.ErrorResponse{Details: "invalid app id"})
return
}

secrets, err := secrets.ListSecrets(cfg, appID)
if err != nil {
slog.Error("Unable to list secrets", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to list secret"})
return
}

render.EncodeResponse(w, http.StatusOK, models.SecretListResponse{Secrets: secrets})
})
}

func HandleSecretsUpdate(cfg config.Configuration, idProvider *app.IDProvider) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
appID, err := idProvider.IDFromBase64(r.PathValue("appID"))
if err != nil {
render.EncodeResponse(w, http.StatusPreconditionFailed, models.ErrorResponse{Details: "invalid app id"})
return
}
name := r.PathValue("secretName")

value, err := io.ReadAll(r.Body)
if err != nil {
slog.Warn("Failed to read request body", "error", err.Error())
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "invalid body"})
return
}

err = secrets.UpdateSecret(cfg, appID, name, value)
if err != nil {
slog.Error("Unable to update secret", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to update secret"})
return
}

render.EncodeResponse(w, http.StatusNoContent, nil)
})
}

func HandleSecretsDelete(cfg config.Configuration, idProvider *app.IDProvider) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
appID, err := idProvider.IDFromBase64(r.PathValue("appID"))
if err != nil {
render.EncodeResponse(w, http.StatusPreconditionFailed, models.ErrorResponse{Details: "invalid app id"})
return
}
name := r.PathValue("secretName")

err = secrets.RemoveSecret(cfg, appID, name)
if err != nil {
slog.Error("Unable to remove secret", slog.String("error", err.Error()))
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to remove secret"})
return
}

render.EncodeResponse(w, http.StatusNoContent, nil)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ package models
type PropertyKeysResponse struct {
Keys []string `json:"keys"`
}

type SecretListResponse struct {
Secrets []string `json:"secrets"`
}
7 changes: 7 additions & 0 deletions internal/orchestrator/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ func (c *Configuration) init() error {
if err := c.AssetsDir().MkdirAll(); err != nil {
return err
}
if err := c.SecretsDir().MkdirAll(); err != nil {
return err
}
return nil
}

Expand All @@ -152,6 +155,10 @@ func (c *Configuration) RouterSocketPath() *paths.Path {
return c.routerSocketPath
}

func (c *Configuration) SecretsDir() *paths.Path {
return c.dataDir.Join("secrets")
}

func (c *Configuration) AssetsDir() *paths.Path {
return c.dataDir.Join("assets")
}
Expand Down
8 changes: 5 additions & 3 deletions internal/orchestrator/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func StartApp(
appToStart app.ArduinoApp,
cfg config.Configuration,
staticStore *store.StaticStore,
idProvider *app.IDProvider,
) iter.Seq[StreamMessage] {
return func(yield func(StreamMessage) bool) {
ctx, cancel := context.WithCancel(ctx)
Expand Down Expand Up @@ -183,7 +184,7 @@ func StartApp(
return
}

if err := provisioner.App(ctx, bricksIndex, &appToStart, cfg, envs, staticStore); err != nil {
if err := provisioner.App(ctx, bricksIndex, &appToStart, cfg, envs, staticStore, idProvider); err != nil {
yield(StreamMessage{error: err})
return
}
Expand Down Expand Up @@ -458,6 +459,7 @@ func RestartApp(
appToStart app.ArduinoApp,
cfg config.Configuration,
staticStore *store.StaticStore,
idProvider *app.IDProvider,
) iter.Seq[StreamMessage] {
return func(yield func(StreamMessage) bool) {
ctx, cancel := context.WithCancel(ctx)
Expand All @@ -484,7 +486,7 @@ func RestartApp(
}
}
}
startStream := StartApp(ctx, docker, provisioner, modelsIndex, bricksIndex, appToStart, cfg, staticStore)
startStream := StartApp(ctx, docker, provisioner, modelsIndex, bricksIndex, appToStart, cfg, staticStore, idProvider)
startStream(yield)
}
}
Expand Down Expand Up @@ -517,7 +519,7 @@ func StartDefaultApp(
}

// TODO: we need to stop all other running app before starting the default app.
for msg := range StartApp(ctx, docker, provisioner, modelsIndex, bricksIndex, *app, cfg, staticStore) {
for msg := range StartApp(ctx, docker, provisioner, modelsIndex, bricksIndex, *app, cfg, staticStore, idProvider) {
if msg.IsError() {
return fmt.Errorf("failed to start app: %w", msg.GetError())
}
Expand Down
36 changes: 32 additions & 4 deletions internal/orchestrator/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
"github.com/arduino/arduino-app-cli/internal/orchestrator/secrets"
"github.com/arduino/arduino-app-cli/internal/store"
)

Expand Down Expand Up @@ -67,11 +68,13 @@ type service struct {
Labels map[string]string `yaml:"labels,omitempty"`
Environment map[string]string `yaml:"environment,omitempty"`
Logging *logging `yaml:"logging,omitempty"`
Secrets []string `yaml:"secrets,omitempty"`
}

type Provision struct {
docker command.Cli
pythonImage string
idProvider *app.IDProvider
}

func isDevelopmentMode(cfg config.Configuration) bool {
Expand All @@ -81,10 +84,12 @@ func isDevelopmentMode(cfg config.Configuration) bool {
func NewProvision(
docker command.Cli,
cfg config.Configuration,
idProvider *app.IDProvider,
) (*Provision, error) {
provision := &Provision{
docker: docker,
pythonImage: cfg.PythonImage,
idProvider: idProvider,
}

dynamicProvisionDir := cfg.AssetsDir().Join(cfg.UsedPythonImageTag)
Expand Down Expand Up @@ -119,6 +124,7 @@ func (p *Provision) App(
cfg config.Configuration,
mapped_env map[string]string,
staticStore *store.StaticStore,
idProvider *app.IDProvider,
) error {
if arduinoApp == nil {
return fmt.Errorf("provisioning failed: arduinoApp is nil")
Expand All @@ -130,7 +136,7 @@ func (p *Provision) App(
}
}

return generateMainComposeFile(arduinoApp, bricksIndex, p.pythonImage, cfg, mapped_env, staticStore)
return generateMainComposeFile(arduinoApp, idProvider, bricksIndex, p.pythonImage, cfg, mapped_env, staticStore)
}

func (p *Provision) init(
Expand Down Expand Up @@ -207,6 +213,7 @@ const (

func generateMainComposeFile(
app *app.ArduinoApp,
idProvider *app.IDProvider,
bricksIndex *bricksindex.BricksIndex,
pythonImage string,
cfg config.Configuration,
Expand Down Expand Up @@ -288,10 +295,14 @@ func generateMainComposeFile(
type mainService struct {
Main service `yaml:"main"`
}
type secretObj struct {
File string `yaml:"file"`
}
var mainAppCompose struct {
Name string `yaml:"name"`
Include []string `yaml:"include,omitempty"`
Services *mainService `yaml:"services,omitempty"`
Name string `yaml:"name"`
Include []string `yaml:"include,omitempty"`
Services *mainService `yaml:"services,omitempty"`
Secrets map[string]secretObj `yaml:"secrets,omitempty"`
}
// Merge compose
composeProjectName, err := getAppComposeProjectNameFromApp(*app, cfg)
Expand Down Expand Up @@ -356,6 +367,22 @@ func generateMainComposeFile(
}
}

// Add secrets (if defined)
appID, err := idProvider.IDFromPath(app.FullPath)
if err != nil {
return fmt.Errorf("failed to retrieve app id from path %s: %w", app.FullPath.String(), err)
}
secrets, err := secrets.GetSecrets(cfg, appID)
if err != nil {
slog.Error("Failed to retrieve secrets for app", slog.String("app_path", app.FullPath.String()), slog.String("app_id", appID.String()), slog.Any("error", err))
}
secretsList := make([]string, 0, len(secrets))
mainAppCompose.Secrets = make(map[string]secretObj, len(secrets))
for _, secret := range secrets {
secretsList = append(secretsList, secret.Name)
mainAppCompose.Secrets[secret.Name] = secretObj{File: secret.Path}
}

mainAppCompose.Services = &mainService{
Main: service{
Image: pythonImage,
Expand All @@ -380,6 +407,7 @@ func generateMainComposeFile(
"max-file": "2",
},
},
Secrets: secretsList,
},
}

Expand Down
8 changes: 5 additions & 3 deletions internal/orchestrator/provision_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func TestProvisionAppWithOverrides(t *testing.T) {
tempDirectory := t.TempDir()

staticStore := store.NewStaticStore(cfg.AssetsDir().String())
idProvider := app.NewAppIDProvider(cfg)

// Define a mock app with bricks that require overrides
app := app.ArduinoApp{
Expand Down Expand Up @@ -116,7 +117,7 @@ bricks:
env := map[string]string{
"FOO": "bar",
}
err = generateMainComposeFile(&app, bricksIndex, "app-bricks:python-apps-base:dev-latest", cfg, env, staticStore)
err = generateMainComposeFile(&app, idProvider, bricksIndex, "app-bricks:python-apps-base:dev-latest", cfg, env, staticStore)

// Validate that the main compose file and overrides are created
require.NoError(t, err, "Failed to generate main compose file")
Expand Down Expand Up @@ -274,6 +275,7 @@ services:
func TestProvisionAppWithDependsOn(t *testing.T) {
cfg := setTestOrchestratorConfig(t)
staticStore := store.NewStaticStore(cfg.AssetsDir().String())
idProvider := app.NewAppIDProvider(cfg)
tempDirectory := t.TempDir()
var env = map[string]string{}
type services struct {
Expand Down Expand Up @@ -340,7 +342,7 @@ services:
require.NoError(t, err)

// Run the provision function to generate the main compose file
err = generateMainComposeFile(&app, bricksIndex, "app-bricks:python-apps-base:dev-latest", cfg, env, staticStore)
err = generateMainComposeFile(&app, idProvider, bricksIndex, "app-bricks:python-apps-base:dev-latest", cfg, env, staticStore)
require.NoError(t, err, "Failed to generate main compose file")
composeFilePath := paths.New(tempDirectory).Join(".cache").Join("app-compose.yaml")
require.True(t, composeFilePath.Exist(), "Main compose file should exist")
Expand Down Expand Up @@ -390,7 +392,7 @@ services:
require.NoError(t, err)

// Run the provision function to generate the main compose file
err = generateMainComposeFile(&app, bricksIndex, "app-bricks:python-apps-base:dev-latest", cfg, env, staticStore)
err = generateMainComposeFile(&app, idProvider, bricksIndex, "app-bricks:python-apps-base:dev-latest", cfg, env, staticStore)
require.NoError(t, err, "Failed to generate main compose file")
composeFilePath := paths.New(tempDirectory).Join(".cache").Join("app-compose.yaml")
require.True(t, composeFilePath.Exist(), "Main compose file should exist")
Expand Down
Loading