Skip to content

Commit 00fcb06

Browse files
committed
feat: add secret management
1 parent 86853c9 commit 00fcb06

File tree

12 files changed

+212
-11
lines changed

12 files changed

+212
-11
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ func restartHandler(ctx context.Context, cfg config.Configuration, app app.Ardui
6363
app,
6464
cfg,
6565
servicelocator.GetStaticStore(),
66+
servicelocator.GetAppIDProvider(),
6667
)
6768
for message := range stream {
6869
switch message.GetType() {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func startHandler(ctx context.Context, cfg config.Configuration, app app.Arduino
6565
app,
6666
cfg,
6767
servicelocator.GetStaticStore(),
68+
servicelocator.GetAppIDProvider(),
6869
)
6970
for message := range stream {
7071
switch message.GetType() {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ var (
5353
return f.Must(orchestrator.NewProvision(
5454
GetDockerClient(),
5555
globalConfig,
56+
GetAppIDProvider(),
5657
))
5758
})
5859

internal/api/api.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ func NewHTTPRouter(
9393
mux.Handle("PATCH /v1/apps/{appID}/bricks/{brickID}", handlers.HandleBrickUpdates(brickService, idProvider))
9494
mux.Handle("DELETE /v1/apps/{appID}/bricks/{brickID}", handlers.HandleBrickDelete(brickService, idProvider))
9595

96+
mux.Handle("GET /v1/apps/{appID}/secrets", handlers.HandleSecretsList(cfg, idProvider))
97+
mux.Handle("PUT /v1/apps/{appID}/secrets/{secretName}", handlers.HandleSecretsUpdate(cfg, idProvider))
98+
mux.Handle("DELETE /v1/apps/{appID}/secrets/{secretName}", handlers.HandleSecretsDelete(cfg, idProvider))
99+
96100
mux.Handle("GET /v1/docs/", http.StripPrefix("/v1/docs/", handlers.DocsServer(docsFS)))
97101

98102
mux.Handle("GET /v1/monitor/ws", handlers.HandleMonitorWS(allowedOrigins))

internal/api/handlers/app_start.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func HandleAppStart(
6969
type log struct {
7070
Message string `json:"message"`
7171
}
72-
for item := range orchestrator.StartApp(r.Context(), dockerCli, provisioner, modelsIndex, bricksIndex, app, cfg, staticStore) {
72+
for item := range orchestrator.StartApp(r.Context(), dockerCli, provisioner, modelsIndex, bricksIndex, app, cfg, staticStore, idProvider) {
7373
switch item.GetType() {
7474
case orchestrator.ProgressType:
7575
sseStream.Send(render.SSEEvent{Type: "progress", Data: progress(*item.GetProgress())})

internal/api/handlers/secrets.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package handlers
2+
3+
import (
4+
"io"
5+
"log/slog"
6+
"net/http"
7+
8+
"github.com/arduino/arduino-app-cli/internal/api/models"
9+
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
10+
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
11+
"github.com/arduino/arduino-app-cli/internal/orchestrator/secrets"
12+
"github.com/arduino/arduino-app-cli/internal/render"
13+
)
14+
15+
func HandleSecretsList(cfg config.Configuration, idProvider *app.IDProvider) http.Handler {
16+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
appID, err := idProvider.IDFromBase64(r.PathValue("appID"))
18+
if err != nil {
19+
render.EncodeResponse(w, http.StatusPreconditionFailed, models.ErrorResponse{Details: "invalid app id"})
20+
return
21+
}
22+
23+
secrets, err := secrets.ListSecrets(cfg, appID)
24+
if err != nil {
25+
slog.Error("Unable to list secrets", slog.String("error", err.Error()))
26+
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to list secret"})
27+
return
28+
}
29+
30+
render.EncodeResponse(w, http.StatusOK, models.SecretListResponse{Secrets: secrets})
31+
})
32+
}
33+
34+
func HandleSecretsUpdate(cfg config.Configuration, idProvider *app.IDProvider) http.Handler {
35+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
36+
appID, err := idProvider.IDFromBase64(r.PathValue("appID"))
37+
if err != nil {
38+
render.EncodeResponse(w, http.StatusPreconditionFailed, models.ErrorResponse{Details: "invalid app id"})
39+
return
40+
}
41+
name := r.PathValue("secretName")
42+
43+
value, err := io.ReadAll(r.Body)
44+
if err != nil {
45+
slog.Warn("Failed to read request body", "error", err.Error())
46+
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "invalid body"})
47+
return
48+
}
49+
50+
err = secrets.UpdateSecret(cfg, appID, name, value)
51+
if err != nil {
52+
slog.Error("Unable to update secret", slog.String("error", err.Error()))
53+
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to update secret"})
54+
return
55+
}
56+
57+
render.EncodeResponse(w, http.StatusNoContent, nil)
58+
})
59+
}
60+
61+
func HandleSecretsDelete(cfg config.Configuration, idProvider *app.IDProvider) http.Handler {
62+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
63+
appID, err := idProvider.IDFromBase64(r.PathValue("appID"))
64+
if err != nil {
65+
render.EncodeResponse(w, http.StatusPreconditionFailed, models.ErrorResponse{Details: "invalid app id"})
66+
return
67+
}
68+
name := r.PathValue("secretName")
69+
70+
err = secrets.RemoveSecret(cfg, appID, name)
71+
if err != nil {
72+
slog.Error("Unable to remove secret", slog.String("error", err.Error()))
73+
render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to remove secret"})
74+
return
75+
}
76+
77+
render.EncodeResponse(w, http.StatusNoContent, nil)
78+
})
79+
}

internal/api/models/properties.go renamed to internal/api/models/models.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ package models
1818
type PropertyKeysResponse struct {
1919
Keys []string `json:"keys"`
2020
}
21+
22+
type SecretListResponse struct {
23+
Secrets []string `json:"secrets"`
24+
}

internal/orchestrator/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ func (c *Configuration) init() error {
133133
if err := c.AssetsDir().MkdirAll(); err != nil {
134134
return err
135135
}
136+
if err := c.SecretsDir().MkdirAll(); err != nil {
137+
return err
138+
}
136139
return nil
137140
}
138141

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

158+
func (c *Configuration) SecretsDir() *paths.Path {
159+
return c.dataDir.Join("secrets")
160+
}
161+
155162
func (c *Configuration) AssetsDir() *paths.Path {
156163
return c.dataDir.Join("assets")
157164
}

internal/orchestrator/orchestrator.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func StartApp(
117117
appToStart app.ArduinoApp,
118118
cfg config.Configuration,
119119
staticStore *store.StaticStore,
120+
idProvider *app.IDProvider,
120121
) iter.Seq[StreamMessage] {
121122
return func(yield func(StreamMessage) bool) {
122123
ctx, cancel := context.WithCancel(ctx)
@@ -183,7 +184,7 @@ func StartApp(
183184
return
184185
}
185186

186-
if err := provisioner.App(ctx, bricksIndex, &appToStart, cfg, envs, staticStore); err != nil {
187+
if err := provisioner.App(ctx, bricksIndex, &appToStart, cfg, envs, staticStore, idProvider); err != nil {
187188
yield(StreamMessage{error: err})
188189
return
189190
}
@@ -458,6 +459,7 @@ func RestartApp(
458459
appToStart app.ArduinoApp,
459460
cfg config.Configuration,
460461
staticStore *store.StaticStore,
462+
idProvider *app.IDProvider,
461463
) iter.Seq[StreamMessage] {
462464
return func(yield func(StreamMessage) bool) {
463465
ctx, cancel := context.WithCancel(ctx)
@@ -484,7 +486,7 @@ func RestartApp(
484486
}
485487
}
486488
}
487-
startStream := StartApp(ctx, docker, provisioner, modelsIndex, bricksIndex, appToStart, cfg, staticStore)
489+
startStream := StartApp(ctx, docker, provisioner, modelsIndex, bricksIndex, appToStart, cfg, staticStore, idProvider)
488490
startStream(yield)
489491
}
490492
}
@@ -517,7 +519,7 @@ func StartDefaultApp(
517519
}
518520

519521
// TODO: we need to stop all other running app before starting the default app.
520-
for msg := range StartApp(ctx, docker, provisioner, modelsIndex, bricksIndex, *app, cfg, staticStore) {
522+
for msg := range StartApp(ctx, docker, provisioner, modelsIndex, bricksIndex, *app, cfg, staticStore, idProvider) {
521523
if msg.IsError() {
522524
return fmt.Errorf("failed to start app: %w", msg.GetError())
523525
}

internal/orchestrator/provision.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
3737
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
3838
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
39+
"github.com/arduino/arduino-app-cli/internal/orchestrator/secrets"
3940
"github.com/arduino/arduino-app-cli/internal/store"
4041
)
4142

@@ -67,11 +68,13 @@ type service struct {
6768
Labels map[string]string `yaml:"labels,omitempty"`
6869
Environment map[string]string `yaml:"environment,omitempty"`
6970
Logging *logging `yaml:"logging,omitempty"`
71+
Secrets []string `yaml:"secrets,omitempty"`
7072
}
7173

7274
type Provision struct {
7375
docker command.Cli
7476
pythonImage string
77+
idProvider *app.IDProvider
7578
}
7679

7780
func isDevelopmentMode(cfg config.Configuration) bool {
@@ -81,10 +84,12 @@ func isDevelopmentMode(cfg config.Configuration) bool {
8184
func NewProvision(
8285
docker command.Cli,
8386
cfg config.Configuration,
87+
idProvider *app.IDProvider,
8488
) (*Provision, error) {
8589
provision := &Provision{
8690
docker: docker,
8791
pythonImage: cfg.PythonImage,
92+
idProvider: idProvider,
8893
}
8994

9095
dynamicProvisionDir := cfg.AssetsDir().Join(cfg.UsedPythonImageTag)
@@ -119,6 +124,7 @@ func (p *Provision) App(
119124
cfg config.Configuration,
120125
mapped_env map[string]string,
121126
staticStore *store.StaticStore,
127+
idProvider *app.IDProvider,
122128
) error {
123129
if arduinoApp == nil {
124130
return fmt.Errorf("provisioning failed: arduinoApp is nil")
@@ -130,7 +136,7 @@ func (p *Provision) App(
130136
}
131137
}
132138

133-
return generateMainComposeFile(arduinoApp, bricksIndex, p.pythonImage, cfg, mapped_env, staticStore)
139+
return generateMainComposeFile(arduinoApp, idProvider, bricksIndex, p.pythonImage, cfg, mapped_env, staticStore)
134140
}
135141

136142
func (p *Provision) init(
@@ -207,6 +213,7 @@ const (
207213

208214
func generateMainComposeFile(
209215
app *app.ArduinoApp,
216+
idProvider *app.IDProvider,
210217
bricksIndex *bricksindex.BricksIndex,
211218
pythonImage string,
212219
cfg config.Configuration,
@@ -288,10 +295,14 @@ func generateMainComposeFile(
288295
type mainService struct {
289296
Main service `yaml:"main"`
290297
}
298+
type secretObj struct {
299+
File string `yaml:"file"`
300+
}
291301
var mainAppCompose struct {
292-
Name string `yaml:"name"`
293-
Include []string `yaml:"include,omitempty"`
294-
Services *mainService `yaml:"services,omitempty"`
302+
Name string `yaml:"name"`
303+
Include []string `yaml:"include,omitempty"`
304+
Services *mainService `yaml:"services,omitempty"`
305+
Secrets map[string]secretObj `yaml:"secrets,omitempty"`
295306
}
296307
// Merge compose
297308
composeProjectName, err := getAppComposeProjectNameFromApp(*app, cfg)
@@ -356,6 +367,22 @@ func generateMainComposeFile(
356367
}
357368
}
358369

370+
// Add secrets (if defined)
371+
appID, err := idProvider.IDFromPath(app.FullPath)
372+
if err != nil {
373+
return fmt.Errorf("failed to retrieve app id from path %s: %w", app.FullPath.String(), err)
374+
}
375+
secrets, err := secrets.GetSecrets(cfg, appID)
376+
if err != nil {
377+
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))
378+
}
379+
secretsList := make([]string, 0, len(secrets))
380+
mainAppCompose.Secrets = make(map[string]secretObj, len(secrets))
381+
for _, secret := range secrets {
382+
secretsList = append(secretsList, secret.Name)
383+
mainAppCompose.Secrets[secret.Name] = secretObj{File: secret.Path}
384+
}
385+
359386
mainAppCompose.Services = &mainService{
360387
Main: service{
361388
Image: pythonImage,
@@ -380,6 +407,7 @@ func generateMainComposeFile(
380407
"max-file": "2",
381408
},
382409
},
410+
Secrets: secretsList,
383411
},
384412
}
385413

0 commit comments

Comments
 (0)