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

// Validate multi-field resources: if any non-bundleIgnore field is set, all non-bundleIgnore fields must be set.
// Validate multi-field resources: if any user-provided field is set, all user-provided fields must be set.
// Fields with BundleIgnore or LocalOnly are auto-populated and exempt from this check.
for _, p := range m.GetPlugins() {
for _, r := range append(p.Resources.Required, p.Resources.Optional...) {
if len(r.Fields) <= 1 {
Expand All @@ -237,7 +238,7 @@ func parseSetValues(setValues []string, m *manifest.Manifest) (map[string]string
setCount := 0
totalCheckable := 0
for _, fn := range names {
if r.Fields[fn].BundleIgnore {
if r.Fields[fn].BundleIgnore || r.Fields[fn].LocalOnly {
continue
}
totalCheckable++
Expand All @@ -248,7 +249,7 @@ func parseSetValues(setValues []string, m *manifest.Manifest) (map[string]string
if setCount > 0 && setCount < totalCheckable {
var missing []string
for _, fn := range names {
if r.Fields[fn].BundleIgnore {
if r.Fields[fn].BundleIgnore || r.Fields[fn].LocalOnly {
continue
}
if rv[r.Key()+"."+fn] == "" {
Expand Down
58 changes: 51 additions & 7 deletions cmd/apps/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,9 +512,9 @@ func TestParseSetValuesBundleIgnoreSkipped(t *testing.T) {
Alias: "Postgres",
ResourceKey: "postgres",
Fields: map[string]manifest.ResourceField{
"branch": {Description: "branch path"},
"database": {Description: "database name"},
"endpoint": {Env: "LAKEBASE_ENDPOINT", BundleIgnore: true},
"branch": {Description: "branch path"},
"database": {Description: "database name"},
"endpointPath": {Env: "LAKEBASE_ENDPOINT", BundleIgnore: true},
},
},
},
Expand Down Expand Up @@ -542,16 +542,60 @@ func TestParseSetValuesBundleIgnoreSkipped(t *testing.T) {
rv, err = parseSetValues([]string{
"lakebase.postgres.branch=br",
"lakebase.postgres.database=db",
"lakebase.postgres.endpoint=ep",
"lakebase.postgres.endpointPath=ep",
}, m)
require.NoError(t, err)
assert.Equal(t, map[string]string{
"postgres.branch": "br",
"postgres.database": "db",
"postgres.endpoint": "ep",
"postgres.branch": "br",
"postgres.database": "db",
"postgres.endpointPath": "ep",
}, rv)
}

func TestParseSetValuesLocalOnlySkipped(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"},
"host": {Env: "PGHOST", LocalOnly: true, Resolve: "postgres:host"},
"databaseName": {Env: "PGDATABASE", LocalOnly: true, Resolve: "postgres:databaseName"},
"endpointPath": {Env: "LAKEBASE_ENDPOINT", BundleIgnore: true, Resolve: "postgres:endpointPath"},
"port": {Env: "PGPORT", LocalOnly: true, Value: "5432"},
"sslmode": {Env: "PGSSLMODE", LocalOnly: true, Value: "require"},
},
},
},
},
},
},
}

// Setting only branch+database should succeed — localOnly and bundleIgnore fields are exempt.
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 branch should still fail (database is also required).
_, err = parseSetValues([]string{"lakebase.postgres.branch=br"}, m)
require.Error(t, err)
assert.Contains(t, err.Error(), `incomplete resource "postgres"`)
}

func TestPluginHasResourceField(t *testing.T) {
m := testManifest()
p := m.GetPluginByName("analytics")
Expand Down
15 changes: 12 additions & 3 deletions libs/apps/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,11 @@ func dotEnvActualLines(r manifest.Resource, cfg Config) []string {
if field.Env == "" || !validEnvVar.MatchString(field.Env) {
continue
}
value := sanitizeEnvValue(cfg.ResourceValues[r.Key()+"."+fieldName])
lines = append(lines, fmt.Sprintf("%s=%s", field.Env, value))
value := cfg.ResourceValues[r.Key()+"."+fieldName]
if value == "" && field.Value != "" {
value = field.Value
}
lines = append(lines, fmt.Sprintf("%s=%s", field.Env, sanitizeEnvValue(value)))
}
return lines
}
Expand All @@ -176,6 +179,9 @@ func dotEnvExampleLines(r manifest.Resource, commented bool) []string {
continue
}
placeholder := "your_" + r.VarPrefix() + "_" + fieldName
if field.Value != "" {
placeholder = field.Value
}
if commented {
lines = append(lines, fmt.Sprintf("# %s=%s", field.Env, placeholder))
} else {
Expand Down Expand Up @@ -251,6 +257,9 @@ func appEnvLines(r manifest.Resource) []string {
if field.Env == "" || !validEnvVar.MatchString(field.Env) {
continue
}
if field.LocalOnly {
continue
}
lines = append(lines,
" - name: "+field.Env,
" valueFrom: "+r.Key(),
Expand Down Expand Up @@ -357,7 +366,7 @@ func variableNamesForResource(r manifest.Resource) []varInfo {

for _, fieldName := range r.FieldNames() {
field := r.Fields[fieldName]
if field.BundleIgnore {
if field.BundleIgnore || field.LocalOnly {
covered[fieldName] = true
continue
}
Expand Down
137 changes: 137 additions & 0 deletions libs/apps/generator/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,143 @@ func TestGenerateDotEnvSanitizesNewlines(t *testing.T) {
assert.NotContains(t, result, "\n")
}

func TestLocalOnlyFieldSkippedInAppEnvAndVariables(t *testing.T) {
plugins := []manifest.Plugin{
{
Name: "test",
Resources: manifest.Resources{
Required: []manifest.Resource{
{
Type: "postgres", Alias: "Postgres", ResourceKey: "postgres",
Fields: map[string]manifest.ResourceField{
"branch": {Description: "Branch"},
"database": {Description: "Database"},
"host": {Env: "PGHOST", LocalOnly: true, Resolve: "postgres:host", Description: "Postgres host"},
"databaseName": {Env: "PGDATABASE", LocalOnly: true, Resolve: "postgres:databaseName", Description: "Postgres db name"},
"endpointPath": {Env: "LAKEBASE_ENDPOINT", BundleIgnore: true, Resolve: "postgres:endpointPath", Description: "Endpoint"},
"port": {Env: "PGPORT", LocalOnly: true, Value: "5432", Description: "Postgres port"},
"sslmode": {Env: "PGSSLMODE", LocalOnly: true, Value: "require", Description: "SSL mode"},
},
},
},
},
},
}
cfg := generator.Config{ResourceValues: map[string]string{
"postgres.branch": "projects/p1/branches/br-1",
"postgres.database": "projects/p1/branches/br-1/databases/db-1",
"postgres.host": "ep-test.database.cloud.databricks.com",
"postgres.databaseName": "mydb",
"postgres.endpointPath": "projects/p1/branches/br-1/endpoints/primary",
}}

// localOnly fields should be in .env with resolved values
env := generator.GenerateDotEnv(plugins, cfg)
assert.Contains(t, env, "PGHOST=ep-test.database.cloud.databricks.com")
assert.Contains(t, env, "PGDATABASE=mydb")
assert.Contains(t, env, "LAKEBASE_ENDPOINT=projects/p1/branches/br-1/endpoints/primary")
assert.Contains(t, env, "PGPORT=5432")
assert.Contains(t, env, "PGSSLMODE=require")

// localOnly fields should be in .env.example
example := generator.GenerateDotEnvExample(plugins)
assert.Contains(t, example, "PGHOST=your_postgres_host")
assert.Contains(t, example, "PGDATABASE=your_postgres_databaseName")
assert.Contains(t, example, "LAKEBASE_ENDPOINT=your_postgres_endpointPath")
assert.Contains(t, example, "PGPORT=5432")
assert.Contains(t, example, "PGSSLMODE=require")

// localOnly fields should NOT be in app.yaml (appEnv)
appEnv := generator.GenerateAppEnv(plugins, cfg)
assert.NotContains(t, appEnv, "PGHOST")
assert.NotContains(t, appEnv, "PGDATABASE")
assert.NotContains(t, appEnv, "PGPORT")
assert.NotContains(t, appEnv, "PGSSLMODE")
// bundleIgnore fields without localOnly SHOULD be in app.yaml
assert.Contains(t, appEnv, "LAKEBASE_ENDPOINT")
assert.Contains(t, appEnv, "valueFrom: postgres")

// localOnly fields should NOT be in bundle variables
vars := generator.GenerateBundleVariables(plugins, cfg)
assert.Contains(t, vars, "postgres_branch:")
assert.Contains(t, vars, "postgres_database:")
assert.NotContains(t, vars, "postgres_host:")
assert.NotContains(t, vars, "postgres_databaseName:")
assert.NotContains(t, vars, "postgres_endpointPath:")
assert.NotContains(t, vars, "postgres_port:")
assert.NotContains(t, vars, "postgres_sslmode:")

// localOnly fields should NOT be in target variables
target := generator.GenerateTargetVariables(plugins, cfg)
assert.Contains(t, target, "postgres_branch: projects/p1/branches/br-1")
assert.Contains(t, target, "postgres_database: projects/p1/branches/br-1/databases/db-1")
assert.NotContains(t, target, "postgres_host")
assert.NotContains(t, target, "postgres_databaseName")
assert.NotContains(t, target, "postgres_endpointPath")
assert.NotContains(t, target, "postgres_port")
assert.NotContains(t, target, "postgres_sslmode")
}

func TestValueFieldFallback(t *testing.T) {
plugins := []manifest.Plugin{
{
Name: "test",
Resources: manifest.Resources{
Required: []manifest.Resource{
{
Type: "postgres", Alias: "Postgres", ResourceKey: "postgres",
Fields: map[string]manifest.ResourceField{
"port": {Env: "PGPORT", LocalOnly: true, Value: "5432", Description: "Postgres port"},
"sslmode": {Env: "PGSSLMODE", LocalOnly: true, Value: "require", Description: "SSL mode"},
"host": {Env: "PGHOST", LocalOnly: true, Resolve: "postgres:host", Description: "Postgres host"},
},
},
},
},
},
}

// No ResourceValues provided — value fields should use static fallback, resolve fields should be empty
cfg := generator.Config{ResourceValues: map[string]string{}}

env := generator.GenerateDotEnv(plugins, cfg)
assert.Contains(t, env, "PGPORT=5432")
assert.Contains(t, env, "PGSSLMODE=require")
assert.Contains(t, env, "PGHOST=")

// value fields should show static value in example too
example := generator.GenerateDotEnvExample(plugins)
assert.Contains(t, example, "PGPORT=5432")
assert.Contains(t, example, "PGSSLMODE=require")
assert.Contains(t, example, "PGHOST=your_postgres_host")
}

func TestValueFieldNotOverriddenByResourceValues(t *testing.T) {
plugins := []manifest.Plugin{
{
Name: "test",
Resources: manifest.Resources{
Required: []manifest.Resource{
{
Type: "postgres", Alias: "Postgres", ResourceKey: "postgres",
Fields: map[string]manifest.ResourceField{
"port": {Env: "PGPORT", LocalOnly: true, Value: "5432", Description: "Postgres port"},
},
},
},
},
},
}

// Explicit ResourceValues should take precedence over static value
cfg := generator.Config{ResourceValues: map[string]string{
"postgres.port": "5433",
}}

env := generator.GenerateDotEnv(plugins, cfg)
assert.Contains(t, env, "PGPORT=5433")
}

func TestBundleIgnoreFieldSkippedInVariablesAndTargets(t *testing.T) {
plugins := []manifest.Plugin{
{
Expand Down
3 changes: 3 additions & 0 deletions libs/apps/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ type ResourceField struct {
Env string `json:"env"`
Description string `json:"description"`
BundleIgnore bool `json:"bundleIgnore,omitempty"`
LocalOnly bool `json:"localOnly,omitempty"`
Value string `json:"value,omitempty"`
Resolve string `json:"resolve,omitempty"`
}

// Resource defines a Databricks resource required or optional for a plugin.
Expand Down
11 changes: 11 additions & 0 deletions libs/apps/prompt/listers.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,17 @@ func ListPostgresDatabases(ctx context.Context, branchName string) ([]ListItem,
return out, nil
}

// ListPostgresEndpoints returns endpoints for a branch as raw Endpoint objects.
// Returns raw objects (not ListItem) since we need multiple fields (Name, Status.Hosts.Host).
func ListPostgresEndpoints(ctx context.Context, branchName string) ([]postgres.Endpoint, error) {
w, err := workspaceClient(ctx)
if err != nil {
return nil, err
}
iter := w.Postgres.ListEndpoints(ctx, postgres.ListEndpointsRequest{Parent: branchName})
return listing.ToSlice(ctx, iter)
}

// ListGenieSpaces returns Genie spaces as selectable items.
func ListGenieSpaces(ctx context.Context) ([]ListItem, error) {
w, err := workspaceClient(ctx)
Expand Down
Loading
Loading