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
93 changes: 93 additions & 0 deletions cmd/workspace/postgres/overrides.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package postgres

import (
"encoding/json"
"errors"
"strings"

"github.com/databricks/cli/libs/flags"
"github.com/databricks/databricks-sdk-go/service/postgres"
"github.com/spf13/cobra"
)

// createRoleOverride appends an example body to the auto-generated help and
// rejects wrapped {"role": ...} bodies with a clear client-side error.
// The --json flag binds to the inner Role object (CreateRoleRequest.Role,
// JSON-tagged "role"), so users supply spec/name/etc. directly. Without an
// example, the auto-generated `// TODO: complex arg: spec` flags leave no
// hint about the body shape and the API's "Field 'role' is required" error
// is unhelpful when the request body is wrong.
func createRoleOverride(createRoleCmd *cobra.Command, _ *postgres.CreateRoleRequest) {
prevPreRunE := createRoleCmd.PreRunE
createRoleCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
if err := rejectWrappedRoleJSON(cmd); err != nil {
return err
}
if prevPreRunE != nil {
return prevPreRunE(cmd, args)
}
return nil
}

createRoleCmd.Long += `

Body shape (passed via --json): fields go directly on the Role object.
Do not wrap them in '{"role": ...}' — the CLI rejects wrapped bodies
client-side with a hint pointing to the right shape.

Example — create a service-principal-backed role:

databricks postgres create-role projects/<PROJECT_ID>/branches/<BRANCH_ID> \
--role-id <SP_CLIENT_ID> \
--json '{"spec": {"identity_type": "SERVICE_PRINCIPAL", "postgres_role": "<SP_CLIENT_ID>", "auth_method": "LAKEBASE_OAUTH_V1"}}'

The example omits 'membership_roles' so the role starts with default
privileges only — grant database/schema/table access separately via
SQL, following least privilege. Set 'membership_roles' (e.g.
["DATABRICKS_SUPERUSER"]) only when broad administrative access is
intentional.

See databricks-sdk-go/service/postgres.RoleRoleSpec for the full set of
spec fields.`
}

// rejectWrappedRoleJSON returns a clear error when --json is a top-level
// object containing a "role" key. Without this guard the generated unmarshal
// strips the unknown outer "role" field with a warning and ships an empty
// body, and the server rejects with a confusing "Field 'role' is required"
// message.
func rejectWrappedRoleJSON(cmd *cobra.Command) error {
flag := cmd.Flags().Lookup("json")
if flag == nil {
return nil
}
jf, ok := flag.Value.(*flags.JsonFlag)
if !ok {
return nil
}
raw := jf.Raw()
if len(raw) == 0 {
return nil
}
var top map[string]json.RawMessage
if err := json.Unmarshal(raw, &top); err != nil {
return nil //nolint:nilerr // defer non-object inputs to the generated unmarshal so its diagnostics render
}
if _, hasRole := top["role"]; hasRole {
return errors.New(strings.TrimSpace(`
--json should NOT be wrapped in '{"role": ...}'.

The flag binds to the inner Role object — supply 'spec'/'name'/etc.
directly. Example:

databricks postgres create-role projects/<PROJECT_ID>/branches/<BRANCH_ID> \
--role-id <SP_CLIENT_ID> \
--json '{"spec": {"identity_type": "SERVICE_PRINCIPAL", "postgres_role": "<SP_CLIENT_ID>", "auth_method": "LAKEBASE_OAUTH_V1"}}'
`))
}
return nil
}

func init() {
createRoleOverrides = append(createRoleOverrides, createRoleOverride)
}
50 changes: 50 additions & 0 deletions cmd/workspace/postgres/overrides_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package postgres

import (
"testing"

"github.com/databricks/cli/libs/flags"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func cmdWithJSON(t *testing.T, raw string) *cobra.Command {
t.Helper()
cmd := &cobra.Command{}
var jf flags.JsonFlag
cmd.Flags().Var(&jf, "json", "JSON body")
if raw != "" {
require.NoError(t, jf.Set(raw))
}
return cmd
}

func TestRejectWrappedRoleJSON(t *testing.T) {
t.Run("rejects wrapped {role: ...}", func(t *testing.T) {
cmd := cmdWithJSON(t, `{"role":{"spec":{"identity_type":"SERVICE_PRINCIPAL"}}}`)
err := rejectWrappedRoleJSON(cmd)
require.Error(t, err)
assert.Contains(t, err.Error(), "should NOT be wrapped")
assert.Contains(t, err.Error(), `databricks postgres create-role`)
})

t.Run("passes when body has spec at top level", func(t *testing.T) {
cmd := cmdWithJSON(t, `{"spec":{"identity_type":"SERVICE_PRINCIPAL"}}`)
assert.NoError(t, rejectWrappedRoleJSON(cmd))
})

t.Run("passes when --json was not provided", func(t *testing.T) {
cmd := cmdWithJSON(t, "")
assert.NoError(t, rejectWrappedRoleJSON(cmd))
})

t.Run("passes through non-object JSON to the generated diagnostics path", func(t *testing.T) {
cmd := cmdWithJSON(t, `"not-an-object"`)
assert.NoError(t, rejectWrappedRoleJSON(cmd))
})

t.Run("passes when --json flag is absent on the command", func(t *testing.T) {
assert.NoError(t, rejectWrappedRoleJSON(&cobra.Command{}))
})
}
7 changes: 7 additions & 0 deletions libs/flags/json_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,10 @@ func (j *JsonFlag) Unmarshal(v any) diag.Diagnostics {
func (j *JsonFlag) Type() string {
return "JSON"
}

// Raw returns the raw JSON bytes the flag was set to (or nil when the flag
// was not provided). Exposed so command overrides can do shape-level
// validation before the generated Unmarshal call.
func (j *JsonFlag) Raw() []byte {
return j.raw
}
Loading