Skip to content
Open
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
12 changes: 10 additions & 2 deletions cmd/apps/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,22 +227,30 @@ func parseSetValues(setValues []string, m *manifest.Manifest) (map[string]string
rv[resourceKey+"."+fieldName] = value
}

// Validate multi-field resources: if any field is set, all fields must be set.
// Validate multi-field resources: if any non-bundleIgnore field is set, all non-bundleIgnore fields must be set.
for _, p := range m.GetPlugins() {
for _, r := range append(p.Resources.Required, p.Resources.Optional...) {
if len(r.Fields) <= 1 {
continue
}
names := r.FieldNames()
setCount := 0
totalCheckable := 0
for _, fn := range names {
if r.Fields[fn].BundleIgnore {
continue
}
totalCheckable++
if rv[r.Key()+"."+fn] != "" {
setCount++
}
}
if setCount > 0 && setCount < len(names) {
if setCount > 0 && setCount < totalCheckable {
var missing []string
for _, fn := range names {
if r.Fields[fn].BundleIgnore {
continue
}
if rv[r.Key()+"."+fn] == "" {
missing = append(missing, r.Key()+"."+fn)
}
Expand Down
52 changes: 52 additions & 0 deletions cmd/apps/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,58 @@ func TestParseSetValues(t *testing.T) {
}
}

func TestParseSetValuesBundleIgnoreSkipped(t *testing.T) {
m := &manifest.Manifest{
Plugins: map[string]manifest.Plugin{
"lakebase": {
Name: "lakebase",
Resources: manifest.Resources{
Required: []manifest.Resource{
{
Type: "postgres",
Alias: "Postgres",
ResourceKey: "postgres",
Fields: map[string]manifest.ResourceField{
"branch": {Description: "branch path"},
"database": {Description: "database name"},
"endpoint": {Env: "LAKEBASE_ENDPOINT", BundleIgnore: true},
},
},
},
},
},
},
}

rv, err := parseSetValues([]string{
"lakebase.postgres.branch=projects/p1/branches/main",
"lakebase.postgres.database=mydb",
}, m)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"postgres.branch": "projects/p1/branches/main",
"postgres.database": "mydb",
}, rv)

// Setting only one non-bundleIgnore field should still fail.
_, err = parseSetValues([]string{"lakebase.postgres.branch=br"}, m)
require.Error(t, err)
assert.Contains(t, err.Error(), `incomplete resource "postgres"`)

// bundleIgnore field can still be set explicitly via --set.
rv, err = parseSetValues([]string{
"lakebase.postgres.branch=br",
"lakebase.postgres.database=db",
"lakebase.postgres.endpoint=ep",
}, m)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"postgres.branch": "br",
"postgres.database": "db",
"postgres.endpoint": "ep",
}, rv)
}

func TestPluginHasResourceField(t *testing.T) {
m := testManifest()
p := m.GetPluginByName("analytics")
Expand Down
4 changes: 4 additions & 0 deletions libs/apps/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ func variableNamesForResource(r manifest.Resource) []varInfo {

for _, fieldName := range r.FieldNames() {
field := r.Fields[fieldName]
if field.BundleIgnore {
covered[fieldName] = true
continue
}
desc := field.Description
if desc == "" {
desc = r.Description
Expand Down
40 changes: 40 additions & 0 deletions libs/apps/generator/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,3 +863,43 @@ func TestGenerateDotEnvSanitizesNewlines(t *testing.T) {
assert.Equal(t, "WH_ID=safeEVIL_VAR=injected", result)
assert.NotContains(t, result, "\n")
}

func TestBundleIgnoreFieldSkippedInVariablesAndTargets(t *testing.T) {
plugins := []manifest.Plugin{
{
Name: "test",
Resources: manifest.Resources{
Required: []manifest.Resource{
{
Type: "database", Alias: "Database", ResourceKey: "database",
Fields: map[string]manifest.ResourceField{
"instance_name": {Env: "DB_INSTANCE", Description: "Lakebase instance"},
"database_name": {Env: "DB_NAME", Description: "Database name", BundleIgnore: true},
},
},
},
},
},
}
cfg := generator.Config{ResourceValues: map[string]string{
"database.instance_name": "my-inst",
"database.database_name": "my-db",
}}

vars := generator.GenerateBundleVariables(plugins, cfg)
assert.Contains(t, vars, "database_instance_name:")
assert.Contains(t, vars, " description: Lakebase instance")
assert.NotContains(t, vars, "database_database_name:")

target := generator.GenerateTargetVariables(plugins, cfg)
assert.Contains(t, target, "database_instance_name: my-inst")
assert.NotContains(t, target, "database_database_name")

env := generator.GenerateDotEnv(plugins, cfg)
assert.Contains(t, env, "DB_INSTANCE=my-inst")
assert.Contains(t, env, "DB_NAME=my-db")

example := generator.GenerateDotEnvExample(plugins)
assert.Contains(t, example, "DB_INSTANCE=your_database_instance_name")
assert.Contains(t, example, "DB_NAME=your_database_database_name")
}
5 changes: 3 additions & 2 deletions libs/apps/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ const ManifestFileName = "appkit.plugins.json"
// ResourceField describes a single field within a multi-field resource.
// Multi-field resources (e.g., database, secret) need separate env vars and values per field.
type ResourceField struct {
Env string `json:"env"`
Description string `json:"description"`
Env string `json:"env"`
Description string `json:"description"`
BundleIgnore bool `json:"bundleIgnore,omitempty"`
}

// Resource defines a Databricks resource required or optional for a plugin.
Expand Down
45 changes: 45 additions & 0 deletions libs/apps/manifest/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,51 @@ func TestResourceFields(t *testing.T) {
assert.Equal(t, []string{"database_name", "instance_name"}, r.FieldNames())
}

func TestResourceFieldBundleIgnore(t *testing.T) {
dir := t.TempDir()
manifestPath := filepath.Join(dir, manifest.ManifestFileName)

content := `{
"version": "1.0",
"plugins": {
"caching": {
"name": "caching",
"displayName": "Caching",
"description": "DB caching",
"package": "@databricks/appkit",
"resources": {
"required": [
{
"type": "database",
"alias": "Database",
"resourceKey": "database",
"description": "Cache database",
"fields": {
"instance_name": {"env": "DB_INSTANCE", "bundleIgnore": true},
"database_name": {"env": "DB_NAME"}
}
}
],
"optional": []
}
}
}
}`

err := os.WriteFile(manifestPath, []byte(content), 0o644)
require.NoError(t, err)

m, err := manifest.Load(dir)
require.NoError(t, err)

p := m.GetPluginByName("caching")
require.NotNil(t, p)

r := p.Resources.Required[0]
assert.True(t, r.Fields["instance_name"].BundleIgnore)
assert.False(t, r.Fields["database_name"].BundleIgnore)
}

func TestResourceHasFieldsFalse(t *testing.T) {
r := manifest.Resource{Type: "sql_warehouse", Alias: "SQL Warehouse", ResourceKey: "sql-warehouse"}
assert.False(t, r.HasFields())
Expand Down
33 changes: 33 additions & 0 deletions libs/apps/prompt/listers.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,11 +392,44 @@ func ListPostgresBranches(ctx context.Context, projectName string) ([]ListItem,
out := make([]ListItem, 0, len(branches))
for _, b := range branches {
label := extractIDFromName(b.Name, "branches")
if b.Status != nil {
if b.Status.Default {
label += " (default)"
}
if b.Status.IsProtected {
label += " (protected)"
}
if b.Status.CurrentState == postgres.BranchStatusStateArchived {
label += " (archived)"
}
}
out = append(out, ListItem{ID: b.Name, Label: label})
}
return out, nil
}

// ListPostgresDatabases returns databases within a Lakebase Autoscaling branch as selectable items.
func ListPostgresDatabases(ctx context.Context, branchName string) ([]ListItem, error) {
w, err := workspaceClient(ctx)
if err != nil {
return nil, err
}
iter := w.Postgres.ListDatabases(ctx, postgres.ListDatabasesRequest{Parent: branchName})
databases, err := listing.ToSlice(ctx, iter)
if err != nil {
return nil, err
}
out := make([]ListItem, 0, len(databases))
for _, db := range databases {
label := extractIDFromName(db.Name, "databases")
if db.Status != nil && db.Status.PostgresDatabase != "" {
label = db.Status.PostgresDatabase
}
out = append(out, ListItem{ID: db.Name, Label: label})
}
return out, nil
}

// ListGenieSpaces returns Genie spaces as selectable items.
func ListGenieSpaces(ctx context.Context) ([]ListItem, error) {
w, err := workspaceClient(ctx)
Expand Down
21 changes: 11 additions & 10 deletions libs/apps/prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,22 +562,23 @@ func PromptForPostgres(ctx context.Context, r manifest.Resource, required bool)
return nil, nil
}

// Step 3: enter a database name (pre-filled with default)
dbName := "databricks_postgres"
theme := AppkitTheme()
err = huh.NewInput().
Title("Database name").
Description("Enter the database name to connect to").
Value(&dbName).
WithTheme(theme).
Run()
// Step 3: pick a database within the branch
var databases []ListItem
err = RunWithSpinnerCtx(ctx, "Fetching databases...", func() error {
var fetchErr error
databases, fetchErr = ListPostgresDatabases(ctx, branchName)
return fetchErr
})
if err != nil {
return nil, err
}
dbName, err := PromptFromList(ctx, "Select Database", "no databases found in branch "+branchName, databases, required)
if err != nil {
return nil, err
}
if dbName == "" {
return nil, nil
}
printAnswered(ctx, "Database", dbName)

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