Skip to content

Commit fd9f50e

Browse files
pkosiecclaude
andauthored
List postgres databases during Lakebase plugin selection, add option to ignore fields in bundles (#4634)
## Changes - List postgres databases during Lakebase plugin selection - Add "bundleIgnore" field to exclude it from `databricks.yml` file (for Lakebase, we need a `LAKEBASE_ENDPOINT` env that shouldn't be listed in the DAB) - Fix the prompt title See also databricks/appkit#132 Resolves https://databricks.atlassian.net/browse/LKB-10074 ## Testing Test it together with databricks/appkit#132 -> check it out for the latest template 1. Build the CLI (`make build`) 2. Run the `databricks apps init` command with custom template (locally cloned one) - replace the path: `DATABRICKS_APPKIT_TEMPLATE_PATH="/Users/pawel.kosiec/repositories/databricks-os/appkit/template" dbx apps init` 3. Select Lakebase 4. Select your project, branch and database 5. DO NOT deploy your app yet 6. Move to the `databricks.yml` - Remove the `resources.apps.app.resources` and all unused variables (`postgres_branch`, `postgres_database`). 7. Deploy the app with `databricks apps deploy` 8. Bind the Lakebase project to your App, click "Deploy" button again 9. App should connect to Lakebase successfully ## Demo https://github.com/user-attachments/assets/29091cc2-62e3-4ece-aa50-8570d48d79a5 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b8dd8bf commit fd9f50e

File tree

8 files changed

+198
-14
lines changed

8 files changed

+198
-14
lines changed

cmd/apps/init.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,22 +227,30 @@ func parseSetValues(setValues []string, m *manifest.Manifest) (map[string]string
227227
rv[resourceKey+"."+fieldName] = value
228228
}
229229

230-
// Validate multi-field resources: if any field is set, all fields must be set.
230+
// Validate multi-field resources: if any non-bundleIgnore field is set, all non-bundleIgnore fields must be set.
231231
for _, p := range m.GetPlugins() {
232232
for _, r := range append(p.Resources.Required, p.Resources.Optional...) {
233233
if len(r.Fields) <= 1 {
234234
continue
235235
}
236236
names := r.FieldNames()
237237
setCount := 0
238+
totalCheckable := 0
238239
for _, fn := range names {
240+
if r.Fields[fn].BundleIgnore {
241+
continue
242+
}
243+
totalCheckable++
239244
if rv[r.Key()+"."+fn] != "" {
240245
setCount++
241246
}
242247
}
243-
if setCount > 0 && setCount < len(names) {
248+
if setCount > 0 && setCount < totalCheckable {
244249
var missing []string
245250
for _, fn := range names {
251+
if r.Fields[fn].BundleIgnore {
252+
continue
253+
}
246254
if rv[r.Key()+"."+fn] == "" {
247255
missing = append(missing, r.Key()+"."+fn)
248256
}

cmd/apps/init_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,58 @@ func TestParseSetValues(t *testing.T) {
500500
}
501501
}
502502

503+
func TestParseSetValuesBundleIgnoreSkipped(t *testing.T) {
504+
m := &manifest.Manifest{
505+
Plugins: map[string]manifest.Plugin{
506+
"lakebase": {
507+
Name: "lakebase",
508+
Resources: manifest.Resources{
509+
Required: []manifest.Resource{
510+
{
511+
Type: "postgres",
512+
Alias: "Postgres",
513+
ResourceKey: "postgres",
514+
Fields: map[string]manifest.ResourceField{
515+
"branch": {Description: "branch path"},
516+
"database": {Description: "database name"},
517+
"endpoint": {Env: "LAKEBASE_ENDPOINT", BundleIgnore: true},
518+
},
519+
},
520+
},
521+
},
522+
},
523+
},
524+
}
525+
526+
rv, err := parseSetValues([]string{
527+
"lakebase.postgres.branch=projects/p1/branches/main",
528+
"lakebase.postgres.database=mydb",
529+
}, m)
530+
require.NoError(t, err)
531+
assert.Equal(t, map[string]string{
532+
"postgres.branch": "projects/p1/branches/main",
533+
"postgres.database": "mydb",
534+
}, rv)
535+
536+
// Setting only one non-bundleIgnore field should still fail.
537+
_, err = parseSetValues([]string{"lakebase.postgres.branch=br"}, m)
538+
require.Error(t, err)
539+
assert.Contains(t, err.Error(), `incomplete resource "postgres"`)
540+
541+
// bundleIgnore field can still be set explicitly via --set.
542+
rv, err = parseSetValues([]string{
543+
"lakebase.postgres.branch=br",
544+
"lakebase.postgres.database=db",
545+
"lakebase.postgres.endpoint=ep",
546+
}, m)
547+
require.NoError(t, err)
548+
assert.Equal(t, map[string]string{
549+
"postgres.branch": "br",
550+
"postgres.database": "db",
551+
"postgres.endpoint": "ep",
552+
}, rv)
553+
}
554+
503555
func TestPluginHasResourceField(t *testing.T) {
504556
m := testManifest()
505557
p := m.GetPluginByName("analytics")

libs/apps/generator/generator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,10 @@ func variableNamesForResource(r manifest.Resource) []varInfo {
357357

358358
for _, fieldName := range r.FieldNames() {
359359
field := r.Fields[fieldName]
360+
if field.BundleIgnore {
361+
covered[fieldName] = true
362+
continue
363+
}
360364
desc := field.Description
361365
if desc == "" {
362366
desc = r.Description

libs/apps/generator/generator_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,3 +863,43 @@ func TestGenerateDotEnvSanitizesNewlines(t *testing.T) {
863863
assert.Equal(t, "WH_ID=safeEVIL_VAR=injected", result)
864864
assert.NotContains(t, result, "\n")
865865
}
866+
867+
func TestBundleIgnoreFieldSkippedInVariablesAndTargets(t *testing.T) {
868+
plugins := []manifest.Plugin{
869+
{
870+
Name: "test",
871+
Resources: manifest.Resources{
872+
Required: []manifest.Resource{
873+
{
874+
Type: "database", Alias: "Database", ResourceKey: "database",
875+
Fields: map[string]manifest.ResourceField{
876+
"instance_name": {Env: "DB_INSTANCE", Description: "Lakebase instance"},
877+
"database_name": {Env: "DB_NAME", Description: "Database name", BundleIgnore: true},
878+
},
879+
},
880+
},
881+
},
882+
},
883+
}
884+
cfg := generator.Config{ResourceValues: map[string]string{
885+
"database.instance_name": "my-inst",
886+
"database.database_name": "my-db",
887+
}}
888+
889+
vars := generator.GenerateBundleVariables(plugins, cfg)
890+
assert.Contains(t, vars, "database_instance_name:")
891+
assert.Contains(t, vars, " description: Lakebase instance")
892+
assert.NotContains(t, vars, "database_database_name:")
893+
894+
target := generator.GenerateTargetVariables(plugins, cfg)
895+
assert.Contains(t, target, "database_instance_name: my-inst")
896+
assert.NotContains(t, target, "database_database_name")
897+
898+
env := generator.GenerateDotEnv(plugins, cfg)
899+
assert.Contains(t, env, "DB_INSTANCE=my-inst")
900+
assert.Contains(t, env, "DB_NAME=my-db")
901+
902+
example := generator.GenerateDotEnvExample(plugins)
903+
assert.Contains(t, example, "DB_INSTANCE=your_database_instance_name")
904+
assert.Contains(t, example, "DB_NAME=your_database_database_name")
905+
}

libs/apps/manifest/manifest.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ const ManifestFileName = "appkit.plugins.json"
1414
// ResourceField describes a single field within a multi-field resource.
1515
// Multi-field resources (e.g., database, secret) need separate env vars and values per field.
1616
type ResourceField struct {
17-
Env string `json:"env"`
18-
Description string `json:"description"`
17+
Env string `json:"env"`
18+
Description string `json:"description"`
19+
BundleIgnore bool `json:"bundleIgnore,omitempty"`
1920
}
2021

2122
// Resource defines a Databricks resource required or optional for a plugin.

libs/apps/manifest/manifest_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,51 @@ func TestResourceFields(t *testing.T) {
308308
assert.Equal(t, []string{"database_name", "instance_name"}, r.FieldNames())
309309
}
310310

311+
func TestResourceFieldBundleIgnore(t *testing.T) {
312+
dir := t.TempDir()
313+
manifestPath := filepath.Join(dir, manifest.ManifestFileName)
314+
315+
content := `{
316+
"version": "1.0",
317+
"plugins": {
318+
"caching": {
319+
"name": "caching",
320+
"displayName": "Caching",
321+
"description": "DB caching",
322+
"package": "@databricks/appkit",
323+
"resources": {
324+
"required": [
325+
{
326+
"type": "database",
327+
"alias": "Database",
328+
"resourceKey": "database",
329+
"description": "Cache database",
330+
"fields": {
331+
"instance_name": {"env": "DB_INSTANCE", "bundleIgnore": true},
332+
"database_name": {"env": "DB_NAME"}
333+
}
334+
}
335+
],
336+
"optional": []
337+
}
338+
}
339+
}
340+
}`
341+
342+
err := os.WriteFile(manifestPath, []byte(content), 0o644)
343+
require.NoError(t, err)
344+
345+
m, err := manifest.Load(dir)
346+
require.NoError(t, err)
347+
348+
p := m.GetPluginByName("caching")
349+
require.NotNil(t, p)
350+
351+
r := p.Resources.Required[0]
352+
assert.True(t, r.Fields["instance_name"].BundleIgnore)
353+
assert.False(t, r.Fields["database_name"].BundleIgnore)
354+
}
355+
311356
func TestResourceHasFieldsFalse(t *testing.T) {
312357
r := manifest.Resource{Type: "sql_warehouse", Alias: "SQL Warehouse", ResourceKey: "sql-warehouse"}
313358
assert.False(t, r.HasFields())

libs/apps/prompt/listers.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,11 +392,44 @@ func ListPostgresBranches(ctx context.Context, projectName string) ([]ListItem,
392392
out := make([]ListItem, 0, len(branches))
393393
for _, b := range branches {
394394
label := extractIDFromName(b.Name, "branches")
395+
if b.Status != nil {
396+
if b.Status.Default {
397+
label += " (default)"
398+
}
399+
if b.Status.IsProtected {
400+
label += " (protected)"
401+
}
402+
if b.Status.CurrentState == postgres.BranchStatusStateArchived {
403+
label += " (archived)"
404+
}
405+
}
395406
out = append(out, ListItem{ID: b.Name, Label: label})
396407
}
397408
return out, nil
398409
}
399410

411+
// ListPostgresDatabases returns databases within a Lakebase Autoscaling branch as selectable items.
412+
func ListPostgresDatabases(ctx context.Context, branchName string) ([]ListItem, error) {
413+
w, err := workspaceClient(ctx)
414+
if err != nil {
415+
return nil, err
416+
}
417+
iter := w.Postgres.ListDatabases(ctx, postgres.ListDatabasesRequest{Parent: branchName})
418+
databases, err := listing.ToSlice(ctx, iter)
419+
if err != nil {
420+
return nil, err
421+
}
422+
out := make([]ListItem, 0, len(databases))
423+
for _, db := range databases {
424+
label := extractIDFromName(db.Name, "databases")
425+
if db.Status != nil && db.Status.PostgresDatabase != "" {
426+
label = db.Status.PostgresDatabase
427+
}
428+
out = append(out, ListItem{ID: db.Name, Label: label})
429+
}
430+
return out, nil
431+
}
432+
400433
// ListGenieSpaces returns Genie spaces as selectable items.
401434
func ListGenieSpaces(ctx context.Context) ([]ListItem, error) {
402435
w, err := workspaceClient(ctx)

libs/apps/prompt/prompt.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -562,22 +562,23 @@ func PromptForPostgres(ctx context.Context, r manifest.Resource, required bool)
562562
return nil, nil
563563
}
564564

565-
// Step 3: enter a database name (pre-filled with default)
566-
dbName := "databricks_postgres"
567-
theme := AppkitTheme()
568-
err = huh.NewInput().
569-
Title("Database name").
570-
Description("Enter the database name to connect to").
571-
Value(&dbName).
572-
WithTheme(theme).
573-
Run()
565+
// Step 3: pick a database within the branch
566+
var databases []ListItem
567+
err = RunWithSpinnerCtx(ctx, "Fetching databases...", func() error {
568+
var fetchErr error
569+
databases, fetchErr = ListPostgresDatabases(ctx, branchName)
570+
return fetchErr
571+
})
572+
if err != nil {
573+
return nil, err
574+
}
575+
dbName, err := PromptFromList(ctx, "Select Database", "no databases found in branch "+branchName, databases, required)
574576
if err != nil {
575577
return nil, err
576578
}
577579
if dbName == "" {
578580
return nil, nil
579581
}
580-
printAnswered(ctx, "Database", dbName)
581582

582583
return map[string]string{
583584
r.Key() + ".branch": branchName,

0 commit comments

Comments
 (0)