Skip to content

Commit 5f35a3c

Browse files
intel352claude
andcommitted
feat: add env var support to App Platform driver
Create/Update now map env_vars and secret_env_vars from spec config to godo AppVariableDefinition. SECRET type is used for sensitive values so DigitalOcean stores them encrypted. Tests cover env var passthrough for both Create and Update paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d5ab83c commit 5f35a3c

File tree

2 files changed

+126
-4
lines changed

2 files changed

+126
-4
lines changed

internal/drivers/app_platform.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func (d *AppPlatformDriver) Create(ctx context.Context, spec interfaces.Resource
5252
Name: spec.Name,
5353
InstanceCount: int64(instanceCount),
5454
HTTPPort: int64(httpPort),
55+
Envs: envVarsFromConfig(spec.Config),
5556
Image: &godo.ImageSourceSpec{
5657
RegistryType: godo.ImageSourceSpecRegistryType_DOCR,
5758
Repository: imageRepo(image),
@@ -95,6 +96,7 @@ func (d *AppPlatformDriver) Update(ctx context.Context, ref interfaces.ResourceR
9596
Name: spec.Name,
9697
InstanceCount: int64(instanceCount),
9798
HTTPPort: int64(httpPort),
99+
Envs: envVarsFromConfig(spec.Config),
98100
Image: &godo.ImageSourceSpec{
99101
RegistryType: godo.ImageSourceSpecRegistryType_DOCR,
100102
Repository: imageRepo(image),
@@ -191,3 +193,32 @@ func imageTag(image string) string {
191193
}
192194
return "latest"
193195
}
196+
197+
// envVarsFromConfig converts the "env_vars" map in spec config to App Platform
198+
// environment variable definitions. Values listed under "secret_env_vars" are
199+
// marked as SECRET so DigitalOcean stores them encrypted.
200+
func envVarsFromConfig(cfg map[string]any) []*godo.AppVariableDefinition {
201+
raw, _ := cfg["env_vars"].(map[string]any)
202+
secrets, _ := cfg["secret_env_vars"].(map[string]any)
203+
if len(raw) == 0 && len(secrets) == 0 {
204+
return nil
205+
}
206+
envs := make([]*godo.AppVariableDefinition, 0, len(raw)+len(secrets))
207+
for k, v := range raw {
208+
envs = append(envs, &godo.AppVariableDefinition{
209+
Key: k,
210+
Value: fmt.Sprintf("%v", v),
211+
Type: godo.AppVariableType_General,
212+
Scope: godo.AppVariableScope_RunAndBuildTime,
213+
})
214+
}
215+
for k, v := range secrets {
216+
envs = append(envs, &godo.AppVariableDefinition{
217+
Key: k,
218+
Value: fmt.Sprintf("%v", v),
219+
Type: godo.AppVariableType_Secret,
220+
Scope: godo.AppVariableScope_RunAndBuildTime,
221+
})
222+
}
223+
return envs
224+
}

internal/drivers/app_platform_test.go

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,21 @@ import (
1212

1313
// mockAppClient is a mock implementation of AppPlatformClient.
1414
type mockAppClient struct {
15-
app *godo.App
16-
err error
15+
app *godo.App
16+
err error
17+
lastCreateReq *godo.AppCreateRequest
18+
lastUpdateReq *godo.AppUpdateRequest
1719
}
1820

19-
func (m *mockAppClient) Create(_ context.Context, _ *godo.AppCreateRequest) (*godo.App, *godo.Response, error) {
21+
func (m *mockAppClient) Create(_ context.Context, req *godo.AppCreateRequest) (*godo.App, *godo.Response, error) {
22+
m.lastCreateReq = req
2023
return m.app, nil, m.err
2124
}
2225
func (m *mockAppClient) Get(_ context.Context, _ string) (*godo.App, *godo.Response, error) {
2326
return m.app, nil, m.err
2427
}
25-
func (m *mockAppClient) Update(_ context.Context, _ string, _ *godo.AppUpdateRequest) (*godo.App, *godo.Response, error) {
28+
func (m *mockAppClient) Update(_ context.Context, _ string, req *godo.AppUpdateRequest) (*godo.App, *godo.Response, error) {
29+
m.lastUpdateReq = req
2630
return m.app, nil, m.err
2731
}
2832
func (m *mockAppClient) Delete(_ context.Context, _ string) (*godo.Response, error) {
@@ -217,6 +221,93 @@ func TestAppPlatformDriver_Diff_NoChanges(t *testing.T) {
217221
}
218222
}
219223

224+
func TestAppPlatformDriver_Create_EnvVars(t *testing.T) {
225+
mock := &mockAppClient{app: testApp()}
226+
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")
227+
228+
_, err := d.Create(context.Background(), interfaces.ResourceSpec{
229+
Name: "my-app",
230+
Config: map[string]any{
231+
"image": "registry.digitalocean.com/myrepo/myapp:v1",
232+
"env_vars": map[string]any{
233+
"SESSION_STORE": "pg",
234+
"GRPC_PORT": "8080",
235+
},
236+
"secret_env_vars": map[string]any{
237+
"DATABASE_URL": "postgres://user:pass@host/db",
238+
"JWT_SECRET": "s3cr3t",
239+
},
240+
},
241+
})
242+
if err != nil {
243+
t.Fatalf("Create: %v", err)
244+
}
245+
if mock.lastCreateReq == nil {
246+
t.Fatal("no create request captured")
247+
}
248+
svc := mock.lastCreateReq.Spec.Services[0]
249+
if len(svc.Envs) != 4 {
250+
t.Fatalf("expected 4 env vars, got %d", len(svc.Envs))
251+
}
252+
envMap := make(map[string]*godo.AppVariableDefinition, len(svc.Envs))
253+
for _, e := range svc.Envs {
254+
envMap[e.Key] = e
255+
}
256+
if envMap["SESSION_STORE"] == nil || envMap["SESSION_STORE"].Value != "pg" {
257+
t.Errorf("SESSION_STORE not set correctly")
258+
}
259+
if envMap["DATABASE_URL"] == nil || envMap["DATABASE_URL"].Type != godo.AppVariableType_Secret {
260+
t.Errorf("DATABASE_URL not marked as secret")
261+
}
262+
}
263+
264+
func TestAppPlatformDriver_Update_EnvVars(t *testing.T) {
265+
mock := &mockAppClient{app: testApp()}
266+
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")
267+
268+
_, err := d.Update(context.Background(), interfaces.ResourceRef{
269+
Name: "my-app", ProviderID: "app-123",
270+
}, interfaces.ResourceSpec{
271+
Name: "my-app",
272+
Config: map[string]any{
273+
"image": "registry.digitalocean.com/myrepo/myapp:v2",
274+
"env_vars": map[string]any{
275+
"HEALTH_PORT": "8080",
276+
},
277+
},
278+
})
279+
if err != nil {
280+
t.Fatalf("Update: %v", err)
281+
}
282+
if mock.lastUpdateReq == nil {
283+
t.Fatal("no update request captured")
284+
}
285+
svc := mock.lastUpdateReq.Spec.Services[0]
286+
if len(svc.Envs) != 1 {
287+
t.Fatalf("expected 1 env var, got %d", len(svc.Envs))
288+
}
289+
if svc.Envs[0].Key != "HEALTH_PORT" || svc.Envs[0].Value != "8080" {
290+
t.Errorf("HEALTH_PORT env var not set correctly")
291+
}
292+
}
293+
294+
func TestAppPlatformDriver_Create_NoEnvVars(t *testing.T) {
295+
mock := &mockAppClient{app: testApp()}
296+
d := drivers.NewAppPlatformDriverWithClient(mock, "nyc3")
297+
298+
_, err := d.Create(context.Background(), interfaces.ResourceSpec{
299+
Name: "my-app",
300+
Config: map[string]any{"image": "registry.digitalocean.com/myrepo/myapp:v1"},
301+
})
302+
if err != nil {
303+
t.Fatalf("Create: %v", err)
304+
}
305+
svc := mock.lastCreateReq.Spec.Services[0]
306+
if len(svc.Envs) != 0 {
307+
t.Errorf("expected no env vars when not specified, got %d", len(svc.Envs))
308+
}
309+
}
310+
220311
func TestAppPlatformDriver_HealthCheck_Unhealthy(t *testing.T) {
221312
app := &godo.App{
222313
ID: "app-123",

0 commit comments

Comments
 (0)