From eb20678d7b22fbf3a1b888fd1e09a94a8eaeedbd Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 11:28:15 +0200 Subject: [PATCH 01/11] api: generate workspace-proxy deny-list from SDK source The next PR teaches `databricks api` to detect workspace-vs-account scope per call. That decision needs a deny-list of paths under accounts/ that the SDK builds without an account-ID slot (workspace proxies). Hand-maintaining that list drifts from the SDK; this commit generates it. genpaths walks every service/*/impl.go in the pinned SDK with go/ast, classifies each `path :=` assignment, and emits cmd/api/paths_generated.go with a closed allowlist on account-ID source spellings. Refuses to emit prefixes that would over-match, fails loudly on idioms it doesn't recognize, handles var/define/assign forms and rejects compound assignments. Hooked into ./task generate-paths and the existing generated-files staleness gate in CI. The generated tables are not yet referenced at runtime; the next PR wires them in. Generated-file lint exclusions (lax rules) cover the unused declarations until then. Co-authored-by: Isaac --- .github/workflows/push.yml | 7 + Taskfile.yml | 22 +- cmd/api/internal/genpaths/classify.go | 231 +++++++++++++++++++++ cmd/api/internal/genpaths/classify_test.go | 180 ++++++++++++++++ cmd/api/internal/genpaths/main.go | 220 ++++++++++++++++++++ cmd/api/paths_generated.go | 17 ++ 6 files changed, 676 insertions(+), 1 deletion(-) create mode 100644 cmd/api/internal/genpaths/classify.go create mode 100644 cmd/api/internal/genpaths/classify_test.go create mode 100644 cmd/api/internal/genpaths/main.go create mode 100644 cmd/api/paths_generated.go diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 06888d045ba..c827e567817 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -352,6 +352,13 @@ jobs: exit 1 fi + - name: Verify that cmd/api/paths_generated.go is up to date + run: | + if ! ( ./task --force generate-paths && git diff --exit-code ); then + echo "cmd/api/paths_generated.go is not up to date. Please run './task generate-paths' and commit the changes." + exit 1 + fi + validate-python-codegen: needs: cleanups runs-on: ubuntu-latest diff --git a/Taskfile.yml b/Taskfile.yml index 912b3f666a3..9db0c80bdc3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -57,6 +57,7 @@ tasks: - task: generate-validation - task: generate-docs - task: generate-direct + - task: generate-paths - task: pydabs-codegen - task: pydabs-lint - task: pydabs-test @@ -713,7 +714,7 @@ tasks: # can be invoked standalone. generate: - desc: Run all generators (genkit, refschema, schema, docs, validation, direct, pydabs) + desc: Run all generators (genkit, refschema, schema, docs, validation, direct, paths, pydabs) cmds: # Runs first: regenerates CLI command stubs from the OpenAPI spec at # .codegen/_openapi_sha. SDK version bumps (go.mod/go.sum) are a manual @@ -728,6 +729,7 @@ tasks: - task: generate-validation - task: generate-docs - task: generate-direct + - task: generate-paths - task: pydabs-codegen # Drives genkit from a universe checkout. Genkit writes CLI command files into @@ -857,6 +859,24 @@ tasks: desc: Generate direct engine config (apitypes + resources) deps: ['generate-direct-apitypes', 'generate-direct-resources'] + # Regenerates cmd/api/paths_generated.go: the deny-list of workspace-routed + # proxy paths that live under accounts/ in the SDK. Triggered by SDK pin + # changes (go.mod/go.sum) or generator edits. The temp-file-and-rename guards + # against truncating the checked-in file if the generator fails midway. + generate-paths: + desc: Generate cmd/api/paths_generated.go from the pinned SDK + sources: + - cmd/api/internal/genpaths/**/*.go + - exclude: cmd/api/internal/genpaths/**/*_test.go + - go.mod + - go.sum + generates: + - cmd/api/paths_generated.go + cmds: + # Ensure the SDK is in the module cache; the generator parses its source. + - go mod download github.com/databricks/databricks-sdk-go + - "sh -c 'go run ./cmd/api/internal/genpaths > cmd/api/paths_generated.go.tmp && mv cmd/api/paths_generated.go.tmp cmd/api/paths_generated.go'" + generate-direct-apitypes: desc: Generate direct engine API types YAML deps: ['generate-openapi-json'] diff --git a/cmd/api/internal/genpaths/classify.go b/cmd/api/internal/genpaths/classify.go new file mode 100644 index 00000000000..142ab5b63b2 --- /dev/null +++ b/cmd/api/internal/genpaths/classify.go @@ -0,0 +1,231 @@ +package main + +import ( + "errors" + "fmt" + "go/ast" + "go/token" + "strconv" + "strings" +) + +// pathClass labels how a `path := ...` expression in an SDK service impl.go +// should be treated by the deny-list generator. +type pathClass int + +const ( + // classNotAccount is used for paths that do not contain "accounts/". + classNotAccount pathClass = iota + // classAccountAPI is for fmt.Sprintf paths whose template contains + // "accounts/" and whose argument list includes a recognized account-ID + // source. These are account-routed and contribute nothing to the deny list. + classAccountAPI + // classWorkspaceProxyExact is for plain string literal paths containing + // "accounts/". They go into the exact-match map. + classWorkspaceProxyExact + // classWorkspaceProxyPrefix is for fmt.Sprintf paths whose template + // contains "accounts/" but whose argument list contains no account-ID + // source. The literal portion up to the first verb goes into the prefix + // list. + classWorkspaceProxyPrefix +) + +type classification struct { + class pathClass + value string +} + +// classify inspects the right-hand side of a `path := ` assignment in an +// SDK service impl.go and reports whether the path is account-routed, a +// workspace proxy under accounts/ (and which match flavor), or unrelated. +// +// The classifier is intentionally strict: any expression that looks +// account-related but doesn't match a recognized idiom returns an error so the +// generator fails loudly rather than silently producing a wrong deny-list. +func classify(expr ast.Expr) (classification, error) { + if lit, ok := expr.(*ast.BasicLit); ok && lit.Kind == token.STRING { + s, err := strconv.Unquote(lit.Value) + if err != nil { + return classification{}, fmt.Errorf("unquote string literal: %w", err) + } + if !strings.Contains(s, "accounts/") { + return classification{class: classNotAccount}, nil + } + return classification{class: classWorkspaceProxyExact, value: s}, nil + } + + if call, ok := expr.(*ast.CallExpr); ok && isFmtSprintf(call) { + return classifySprintf(call) + } + + // Fallback: an unrecognized idiom. If the subtree contains no "accounts/" + // literal, it can't be a path we care about. If it does, the generator + // won't be able to decide, so fail. + if hasAccountsLiteral(expr) { + return classification{}, errors.New( + "path expression mentions \"accounts/\" but uses an unrecognized construction idiom; " + + "either teach the classifier the new shape or extend the deny-list manually") + } + return classification{class: classNotAccount}, nil +} + +func classifySprintf(call *ast.CallExpr) (classification, error) { + if len(call.Args) == 0 { + return classification{}, errors.New("fmt.Sprintf with no template") + } + tmplLit, ok := call.Args[0].(*ast.BasicLit) + if !ok || tmplLit.Kind != token.STRING { + // Sprintf with a non-literal template. We can't reason about it. + return classification{}, errors.New("fmt.Sprintf with non-literal template") + } + template, err := strconv.Unquote(tmplLit.Value) + if err != nil { + return classification{}, fmt.Errorf("unquote Sprintf template: %w", err) + } + if !strings.Contains(template, "accounts/") { + return classification{class: classNotAccount}, nil + } + + for _, a := range call.Args[1:] { + if isAccountIDSource(a) { + return classification{class: classAccountAPI}, nil + } + } + + prefix := prefixUpToFirstVerb(template) + if prefix == "" { + return classification{}, fmt.Errorf("Sprintf template %q has no format verb", template) + } + // Guard: a prefix ending exactly at "accounts/" would match every account + // API under that family if it leaked into the deny-list. Refuse to emit + // rather than silently classify all of them as workspace-routed. + if strings.HasSuffix(prefix, "/accounts/") { + return classification{}, fmt.Errorf( + "path template %q has a format verb immediately after \"accounts/\" but no recognized account-ID source; "+ + "either teach the classifier a new account-ID spelling or extend the deny-list manually", + template) + } + // Guard: if the first format verb appears before the "accounts/" segment, + // the extracted prefix would not contain "accounts/" and would over-match + // unrelated APIs at runtime. Refuse to emit. + if !strings.Contains(prefix, "/accounts/") { + return classification{}, fmt.Errorf( + "path template %q has a format verb before the \"accounts/\" segment; "+ + "a prefix derived from this template would over-match unrelated APIs", + template) + } + return classification{class: classWorkspaceProxyPrefix, value: prefix}, nil +} + +func isFmtSprintf(call *ast.CallExpr) bool { + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return false + } + id, ok := sel.X.(*ast.Ident) + if !ok { + return false + } + return id.Name == "fmt" && sel.Sel.Name == "Sprintf" +} + +// isAccountIDSource reports whether the expression resolves to one of the +// recognized spellings of "the account ID for the current call". The list is +// an explicit closed allowlist: a new SDK spelling we don't recognize triggers +// a generator error rather than being silently classified as a workspace +// proxy. Currently allowed shapes: a.client.ConfiguredAccountID(), +// cfg.AccountID, a.client.Config.AccountID. +func isAccountIDSource(e ast.Expr) bool { + if call, ok := e.(*ast.CallExpr); ok { + if isClientConfiguredAccountID(call) { + return true + } + } + if sel, ok := e.(*ast.SelectorExpr); ok { + if isCfgAccountID(sel) || isClientConfigAccountID(sel) { + return true + } + } + return false +} + +// isClientConfiguredAccountID matches a.client.ConfiguredAccountID() with no +// arguments. This is the only spelling used in the SDK today. +func isClientConfiguredAccountID(call *ast.CallExpr) bool { + if len(call.Args) != 0 { + return false + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok || sel.Sel.Name != "ConfiguredAccountID" { + return false + } + receiver, ok := sel.X.(*ast.SelectorExpr) + if !ok || receiver.Sel.Name != "client" { + return false + } + id, ok := receiver.X.(*ast.Ident) + return ok && id.Name == "a" +} + +func isCfgAccountID(s *ast.SelectorExpr) bool { + if s.Sel.Name != "AccountID" { + return false + } + id, ok := s.X.(*ast.Ident) + return ok && id.Name == "cfg" +} + +func isClientConfigAccountID(s *ast.SelectorExpr) bool { + if s.Sel.Name != "AccountID" { + return false + } + inner, ok := s.X.(*ast.SelectorExpr) + if !ok || inner.Sel.Name != "Config" { + return false + } + inner2, ok := inner.X.(*ast.SelectorExpr) + if !ok || inner2.Sel.Name != "client" { + return false + } + id, ok := inner2.X.(*ast.Ident) + return ok && id.Name == "a" +} + +// prefixUpToFirstVerb returns the literal portion an fmt.Sprintf template +// would render before the first format verb, with "%%" escapes resolved to +// "%". The result is what runtime strings.HasPrefix compares against, so the +// escapes must match the rendered URL rather than the template source. +// Returns "" if the template has no real verb. +func prefixUpToFirstVerb(template string) string { + var b strings.Builder + for i := 0; i < len(template); i++ { + if template[i] != '%' { + b.WriteByte(template[i]) + continue + } + if i+1 < len(template) && template[i+1] == '%' { + b.WriteByte('%') + i++ + continue + } + return b.String() + } + return "" +} + +func hasAccountsLiteral(expr ast.Expr) bool { + found := false + ast.Inspect(expr, func(n ast.Node) bool { + lit, ok := n.(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + return true + } + s, err := strconv.Unquote(lit.Value) + if err == nil && strings.Contains(s, "accounts/") { + found = true + return false + } + return true + }) + return found +} diff --git a/cmd/api/internal/genpaths/classify_test.go b/cmd/api/internal/genpaths/classify_test.go new file mode 100644 index 00000000000..3def6567e99 --- /dev/null +++ b/cmd/api/internal/genpaths/classify_test.go @@ -0,0 +1,180 @@ +package main + +import ( + "go/parser" + "go/token" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClassify(t *testing.T) { + cases := []struct { + name string + src string + want classification + wantErr string + }{ + { + name: "literal proxy path -> exact match", + src: `"/api/2.0/preview/accounts/access-control/rule-sets"`, + want: classification{class: classWorkspaceProxyExact, value: "/api/2.0/preview/accounts/access-control/rule-sets"}, + }, + { + name: "literal path without accounts segment is ignored", + src: `"/api/2.0/clusters/list"`, + want: classification{class: classNotAccount}, + }, + { + name: "Sprintf without account-ID arg -> prefix", + src: `fmt.Sprintf("/api/2.0/accounts/servicePrincipals/%v/credentials/secrets", request.ServicePrincipalId)`, + want: classification{class: classWorkspaceProxyPrefix, value: "/api/2.0/accounts/servicePrincipals/"}, + }, + { + name: "Sprintf with ConfiguredAccountID -> account", + src: `fmt.Sprintf("/api/2.0/accounts/%v/scim/v2/Groups", a.client.ConfiguredAccountID())`, + want: classification{class: classAccountAPI}, + }, + { + name: "Sprintf with cfg.AccountID -> account", + src: `fmt.Sprintf("/api/2.0/accounts/%v/foo", cfg.AccountID)`, + want: classification{class: classAccountAPI}, + }, + { + name: "Sprintf with a.client.Config.AccountID -> account", + src: `fmt.Sprintf("/api/2.0/accounts/%v/foo", a.client.Config.AccountID)`, + want: classification{class: classAccountAPI}, + }, + { + name: "Sprintf multi-arg with ConfiguredAccountID -> account", + src: `fmt.Sprintf("/api/2.0/accounts/%v/workspaces/%v/permissionassignments", a.client.ConfiguredAccountID(), request.WorkspaceId)`, + want: classification{class: classAccountAPI}, + }, + { + name: "Sprintf without accounts segment is ignored", + src: `fmt.Sprintf("/api/2.0/clusters/%v/events", request.ClusterID)`, + want: classification{class: classNotAccount}, + }, + { + name: "string concatenation that mentions accounts/ -> error", + src: `"/api/2.0/preview/accounts/" + suffix`, + wantErr: "unrecognized construction idiom", + }, + { + name: "helper function call that mentions accounts/ -> error", + src: `buildPath("/api/2.0/preview/accounts/foo")`, + wantErr: "unrecognized construction idiom", + }, + { + name: "Sprintf with unrecognized account-ID source -> error (guard 1)", + src: `fmt.Sprintf("/api/foo/accounts/%v/bar", request.AccountId)`, + wantErr: `format verb immediately after "accounts/"`, + }, + { + name: "Sprintf with locally-aliased ConfiguredAccountID -> error (guard 1)", + src: `fmt.Sprintf("/api/2.0/accounts/%v/scim/v2/Groups", accID)`, + wantErr: `format verb immediately after "accounts/"`, + }, + { + name: "Sprintf with verb before accounts segment -> error", + src: `fmt.Sprintf("/api/2.0/%v/accounts/servicePrincipals/%v/credentials/secrets", request.Scope, request.ServicePrincipalId)`, + wantErr: `format verb before the "accounts/" segment`, + }, + { + name: "Sprintf with non-client receiver on ConfiguredAccountID -> error", + src: `fmt.Sprintf("/api/2.0/accounts/%v/foo", request.ConfiguredAccountID())`, + wantErr: `format verb immediately after "accounts/"`, + }, + { + name: "Sprintf with %% literal before real verb still classifies correctly", + src: `fmt.Sprintf("/api/2.0/accounts/servicePrincipals/%%marker/%v/credentials/secrets", request.ServicePrincipalId)`, + want: classification{class: classWorkspaceProxyPrefix, value: "/api/2.0/accounts/servicePrincipals/%marker/"}, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + expr, err := parser.ParseExpr(c.src) + require.NoError(t, err, "fixture must parse") + got, gotErr := classify(expr) + if c.wantErr != "" { + require.Error(t, gotErr) + assert.Contains(t, gotErr.Error(), c.wantErr) + return + } + require.NoError(t, gotErr) + assert.Equal(t, c.want, got) + }) + } +} + +// TestScanSDK runs the generator end-to-end against the pinned SDK. The +// expected sets are exact: a regression that adds an entry (overbroad prefix, +// new shape misclassified) is just as bad as one that drops an entry. +// +// When bumping the SDK pin, expected diffs go through `./task generate-paths`. +// Update this test only after confirming the new entries are intentional. +func TestScanSDK(t *testing.T) { + dir, err := resolveSDKDir() + require.NoError(t, err) + + prefixes, exacts, err := scanSDK(dir) + require.NoError(t, err) + + assert.Equal(t, []string{ + "/api/2.0/accounts/servicePrincipals/", + }, prefixes) + assert.Equal(t, []string{ + "/api/2.0/preview/accounts/access-control/assignable-roles", + "/api/2.0/preview/accounts/access-control/rule-sets", + }, exacts) +} + +// TestScanAST_VarPath verifies that `var path = ...` declarations are scanned +// the same way as `path := ...` assignments. This is a defensive case: the +// pinned SDK doesn't use the `var` form today, but if a future SDK introduces +// it, we want classification rather than silent skip. +func TestScanAST_VarPath(t *testing.T) { + src := `package svc +type api struct{} +func (a *api) F() { + var path = "/api/2.0/preview/accounts/access-control/rule-sets" + _ = path +} +` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, 0) + require.NoError(t, err) + + prefixSet := map[string]struct{}{} + exactSet := map[string]struct{}{} + require.NoError(t, scanAST(fset, f, prefixSet, exactSet)) + + assert.Empty(t, prefixSet) + assert.Equal(t, map[string]struct{}{ + "/api/2.0/preview/accounts/access-control/rule-sets": {}, + }, exactSet) +} + +// TestScanAST_CompoundAssign verifies that compound assignments to `path` +// (like +=) cause the generator to fail loudly. State across statements is +// outside the per-expression classifier's contract. +func TestScanAST_CompoundAssign(t *testing.T) { + src := `package svc +type api struct{} +func (a *api) F() { + path := "/api/2.0" + path += "/preview/accounts/access-control/rule-sets" + _ = path +} +` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, 0) + require.NoError(t, err) + + prefixSet := map[string]struct{}{} + exactSet := map[string]struct{}{} + err = scanAST(fset, f, prefixSet, exactSet) + require.Error(t, err) + assert.Contains(t, err.Error(), "compound assignment") +} diff --git a/cmd/api/internal/genpaths/main.go b/cmd/api/internal/genpaths/main.go new file mode 100644 index 00000000000..0e963076aed --- /dev/null +++ b/cmd/api/internal/genpaths/main.go @@ -0,0 +1,220 @@ +// genpaths emits cmd/api/paths_generated.go: the deny-list of workspace-routed +// proxy paths that live under accounts/ in the Databricks SDK. The runtime +// classifier in cmd/api uses it to avoid mis-routing those proxies as +// account-scope calls. +// +// The generator parses every service/*/impl.go from the pinned SDK module, +// finds `path :=` assignments, and classifies each as account-routed or as a +// workspace proxy. See classify.go for the per-expression rules. +// +// Output goes to stdout; the Taskfile target redirects it to the checked-in +// file. +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "log" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + "text/template" +) + +const sdkModule = "github.com/databricks/databricks-sdk-go" + +func main() { + if err := run(os.Stdout); err != nil { + log.Fatalf("genpaths: %v", err) + } +} + +func run(out *os.File) error { + dir, err := resolveSDKDir() + if err != nil { + return err + } + prefixes, exacts, err := scanSDK(dir) + if err != nil { + return err + } + return render(out, prefixes, exacts) +} + +func resolveSDKDir() (string, error) { + cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", sdkModule) + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("go list -m %s: %w", sdkModule, err) + } + dir := strings.TrimSpace(string(output)) + if dir == "" { + return "", fmt.Errorf("go list -m %s returned empty directory", sdkModule) + } + return dir, nil +} + +// scanSDK walks every service/*/impl.go under dir and returns the deny-list +// entries grouped by match flavor. Both slices are deduplicated and sorted. +func scanSDK(dir string) (prefixes, exacts []string, err error) { + pattern := filepath.Join(dir, "service", "*", "impl.go") + files, err := filepath.Glob(pattern) + if err != nil { + return nil, nil, fmt.Errorf("glob %s: %w", pattern, err) + } + if len(files) == 0 { + return nil, nil, fmt.Errorf("no impl.go files found under %s", pattern) + } + slices.Sort(files) + + prefixSet := map[string]struct{}{} + exactSet := map[string]struct{}{} + for _, f := range files { + if err := scanFile(f, prefixSet, exactSet); err != nil { + return nil, nil, err + } + } + + prefixes = setToSortedSlice(prefixSet) + exacts = setToSortedSlice(exactSet) + return prefixes, exacts, nil +} + +func scanFile(path string, prefixSet, exactSet map[string]struct{}) error { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + return fmt.Errorf("parse %s: %w", path, err) + } + return scanAST(fset, f, prefixSet, exactSet) +} + +// scanAST walks every function body in f and emits deny-list entries for any +// `path :=` or `var path = ...` assignment whose RHS is classified as a +// workspace proxy. Exposed separately from scanFile so tests can drive it +// with in-memory source. +func scanAST(fset *token.FileSet, f *ast.File, prefixSet, exactSet map[string]struct{}) error { + var classifyErr error + emit := func(expr ast.Expr, pos token.Pos) bool { + res, err := classify(expr) + if err != nil { + classifyErr = fmt.Errorf("%s: %w", fset.Position(pos), err) + return false + } + switch res.class { + case classWorkspaceProxyExact: + exactSet[res.value] = struct{}{} + case classWorkspaceProxyPrefix: + prefixSet[res.value] = struct{}{} + } + return true + } + for _, decl := range f.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Body == nil { + continue + } + ast.Inspect(fn.Body, func(n ast.Node) bool { + if classifyErr != nil { + return false + } + switch s := n.(type) { + case *ast.AssignStmt: + if len(s.Lhs) != 1 || len(s.Rhs) != 1 { + return true + } + lhs, ok := s.Lhs[0].(*ast.Ident) + if !ok || lhs.Name != "path" { + return true + } + // Compound assignments (+=, -=, etc.) imply state across + // statements that the per-expression classifier can't track. + // Reject them so a future SDK that introduces this idiom + // fails loudly instead of silently emitting a fragment as a + // deny-list entry. + if s.Tok != token.DEFINE && s.Tok != token.ASSIGN { + classifyErr = fmt.Errorf("%s: compound assignment %v to `path` is not supported by the classifier; "+ + "the SDK has changed path-construction idioms — extend the classifier or split into a single assignment", + fset.Position(s.Pos()), s.Tok) + return false + } + return emit(s.Rhs[0], s.Pos()) + case *ast.DeclStmt: + gen, ok := s.Decl.(*ast.GenDecl) + if !ok || gen.Tok != token.VAR { + return true + } + for _, spec := range gen.Specs { + vs, ok := spec.(*ast.ValueSpec) + if !ok || len(vs.Values) != 1 || len(vs.Names) != 1 { + continue + } + if vs.Names[0].Name != "path" { + continue + } + if !emit(vs.Values[0], vs.Pos()) { + return false + } + } + return true + } + return true + }) + if classifyErr != nil { + return classifyErr + } + } + return nil +} + +func setToSortedSlice(m map[string]struct{}) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + slices.Sort(out) + return out +} + +const fileTemplate = `// Code generated by genpaths. DO NOT EDIT. + +package api + +// workspaceProxyPrefixes lists path prefixes (Sprintf-derived, ending before +// the first verb) for SDK endpoints that live under accounts/ but route to +// the workspace gateway. Matched with strings.HasPrefix. +var workspaceProxyPrefixes = []string{ +{{range .Prefixes}} {{printf "%q" .}}, +{{end}}} + +// workspaceProxyExact lists literal paths for SDK endpoints that live under +// accounts/ but route to the workspace gateway. Matched with map equality. +var workspaceProxyExact = map[string]struct{}{ +{{range .Exacts}} {{printf "%q" .}}: {}, +{{end}}} +` + +func render(out *os.File, prefixes, exacts []string) error { + tmpl, err := template.New("paths").Parse(fileTemplate) + if err != nil { + return fmt.Errorf("parse template: %w", err) + } + var raw bytes.Buffer + if err := tmpl.Execute(&raw, struct { + Prefixes, Exacts []string + }{prefixes, exacts}); err != nil { + return fmt.Errorf("execute template: %w", err) + } + formatted, err := format.Source(raw.Bytes()) + if err != nil { + return fmt.Errorf("gofmt generated source: %w\n--- raw ---\n%s", err, raw.String()) + } + _, err = out.Write(formatted) + return err +} diff --git a/cmd/api/paths_generated.go b/cmd/api/paths_generated.go new file mode 100644 index 00000000000..044d1493348 --- /dev/null +++ b/cmd/api/paths_generated.go @@ -0,0 +1,17 @@ +// Code generated by genpaths. DO NOT EDIT. + +package api + +// workspaceProxyPrefixes lists path prefixes (Sprintf-derived, ending before +// the first verb) for SDK endpoints that live under accounts/ but route to +// the workspace gateway. Matched with strings.HasPrefix. +var workspaceProxyPrefixes = []string{ + "/api/2.0/accounts/servicePrincipals/", +} + +// workspaceProxyExact lists literal paths for SDK endpoints that live under +// accounts/ but route to the workspace gateway. Matched with map equality. +var workspaceProxyExact = map[string]struct{}{ + "/api/2.0/preview/accounts/access-control/assignable-roles": {}, + "/api/2.0/preview/accounts/access-control/rule-sets": {}, +} From a775d99344b1ed5684044a0e9bdd7cedc5567eb4 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 11:30:58 +0200 Subject: [PATCH 02/11] genpaths: fix lint findings - Drop stdlib log in favor of fmt.Fprintf+os.Exit for the generator binary (depguard rule against the stdlib log package). - Make the path-class switch exhaustive by listing the no-op cases. - Lowercase the format-verb error string (staticcheck ST1005). Co-authored-by: Isaac --- cmd/api/internal/genpaths/classify.go | 9 ++++----- cmd/api/internal/genpaths/main.go | 6 ++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/api/internal/genpaths/classify.go b/cmd/api/internal/genpaths/classify.go index 142ab5b63b2..10cf30472b4 100644 --- a/cmd/api/internal/genpaths/classify.go +++ b/cmd/api/internal/genpaths/classify.go @@ -5,6 +5,7 @@ import ( "fmt" "go/ast" "go/token" + "slices" "strconv" "strings" ) @@ -86,15 +87,13 @@ func classifySprintf(call *ast.CallExpr) (classification, error) { return classification{class: classNotAccount}, nil } - for _, a := range call.Args[1:] { - if isAccountIDSource(a) { - return classification{class: classAccountAPI}, nil - } + if slices.ContainsFunc(call.Args[1:], isAccountIDSource) { + return classification{class: classAccountAPI}, nil } prefix := prefixUpToFirstVerb(template) if prefix == "" { - return classification{}, fmt.Errorf("Sprintf template %q has no format verb", template) + return classification{}, fmt.Errorf("fmt.Sprintf template %q has no format verb", template) } // Guard: a prefix ending exactly at "accounts/" would match every account // API under that family if it leaked into the deny-list. Refuse to emit diff --git a/cmd/api/internal/genpaths/main.go b/cmd/api/internal/genpaths/main.go index 0e963076aed..5dd797d06a7 100644 --- a/cmd/api/internal/genpaths/main.go +++ b/cmd/api/internal/genpaths/main.go @@ -18,7 +18,6 @@ import ( "go/format" "go/parser" "go/token" - "log" "os" "os/exec" "path/filepath" @@ -31,7 +30,8 @@ const sdkModule = "github.com/databricks/databricks-sdk-go" func main() { if err := run(os.Stdout); err != nil { - log.Fatalf("genpaths: %v", err) + fmt.Fprintf(os.Stderr, "genpaths: %v\n", err) + os.Exit(1) } } @@ -112,6 +112,8 @@ func scanAST(fset *token.FileSet, f *ast.File, prefixSet, exactSet map[string]st exactSet[res.value] = struct{}{} case classWorkspaceProxyPrefix: prefixSet[res.value] = struct{}{} + case classNotAccount, classAccountAPI: + // nothing to record; not part of the deny-list } return true } From 28fbf3924593b7f628bca6b2d28ce225cce9200a Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 12:15:42 +0200 Subject: [PATCH 03/11] api: replace generated proxy paths with manual list Keep the unified-host routing support small for the initial PR by hand-maintaining the current workspace-proxy exceptions instead of parsing generated SDK source. --- .github/workflows/push.yml | 7 - Taskfile.yml | 22 +- cmd/api/internal/genpaths/classify.go | 230 --------------------- cmd/api/internal/genpaths/classify_test.go | 180 ---------------- cmd/api/internal/genpaths/main.go | 222 -------------------- cmd/api/paths.go | 29 +++ cmd/api/paths_generated.go | 17 -- cmd/api/paths_test.go | 52 +++++ 8 files changed, 82 insertions(+), 677 deletions(-) delete mode 100644 cmd/api/internal/genpaths/classify.go delete mode 100644 cmd/api/internal/genpaths/classify_test.go delete mode 100644 cmd/api/internal/genpaths/main.go create mode 100644 cmd/api/paths.go delete mode 100644 cmd/api/paths_generated.go create mode 100644 cmd/api/paths_test.go diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index c827e567817..06888d045ba 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -352,13 +352,6 @@ jobs: exit 1 fi - - name: Verify that cmd/api/paths_generated.go is up to date - run: | - if ! ( ./task --force generate-paths && git diff --exit-code ); then - echo "cmd/api/paths_generated.go is not up to date. Please run './task generate-paths' and commit the changes." - exit 1 - fi - validate-python-codegen: needs: cleanups runs-on: ubuntu-latest diff --git a/Taskfile.yml b/Taskfile.yml index 9db0c80bdc3..912b3f666a3 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -57,7 +57,6 @@ tasks: - task: generate-validation - task: generate-docs - task: generate-direct - - task: generate-paths - task: pydabs-codegen - task: pydabs-lint - task: pydabs-test @@ -714,7 +713,7 @@ tasks: # can be invoked standalone. generate: - desc: Run all generators (genkit, refschema, schema, docs, validation, direct, paths, pydabs) + desc: Run all generators (genkit, refschema, schema, docs, validation, direct, pydabs) cmds: # Runs first: regenerates CLI command stubs from the OpenAPI spec at # .codegen/_openapi_sha. SDK version bumps (go.mod/go.sum) are a manual @@ -729,7 +728,6 @@ tasks: - task: generate-validation - task: generate-docs - task: generate-direct - - task: generate-paths - task: pydabs-codegen # Drives genkit from a universe checkout. Genkit writes CLI command files into @@ -859,24 +857,6 @@ tasks: desc: Generate direct engine config (apitypes + resources) deps: ['generate-direct-apitypes', 'generate-direct-resources'] - # Regenerates cmd/api/paths_generated.go: the deny-list of workspace-routed - # proxy paths that live under accounts/ in the SDK. Triggered by SDK pin - # changes (go.mod/go.sum) or generator edits. The temp-file-and-rename guards - # against truncating the checked-in file if the generator fails midway. - generate-paths: - desc: Generate cmd/api/paths_generated.go from the pinned SDK - sources: - - cmd/api/internal/genpaths/**/*.go - - exclude: cmd/api/internal/genpaths/**/*_test.go - - go.mod - - go.sum - generates: - - cmd/api/paths_generated.go - cmds: - # Ensure the SDK is in the module cache; the generator parses its source. - - go mod download github.com/databricks/databricks-sdk-go - - "sh -c 'go run ./cmd/api/internal/genpaths > cmd/api/paths_generated.go.tmp && mv cmd/api/paths_generated.go.tmp cmd/api/paths_generated.go'" - generate-direct-apitypes: desc: Generate direct engine API types YAML deps: ['generate-openapi-json'] diff --git a/cmd/api/internal/genpaths/classify.go b/cmd/api/internal/genpaths/classify.go deleted file mode 100644 index 10cf30472b4..00000000000 --- a/cmd/api/internal/genpaths/classify.go +++ /dev/null @@ -1,230 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "go/ast" - "go/token" - "slices" - "strconv" - "strings" -) - -// pathClass labels how a `path := ...` expression in an SDK service impl.go -// should be treated by the deny-list generator. -type pathClass int - -const ( - // classNotAccount is used for paths that do not contain "accounts/". - classNotAccount pathClass = iota - // classAccountAPI is for fmt.Sprintf paths whose template contains - // "accounts/" and whose argument list includes a recognized account-ID - // source. These are account-routed and contribute nothing to the deny list. - classAccountAPI - // classWorkspaceProxyExact is for plain string literal paths containing - // "accounts/". They go into the exact-match map. - classWorkspaceProxyExact - // classWorkspaceProxyPrefix is for fmt.Sprintf paths whose template - // contains "accounts/" but whose argument list contains no account-ID - // source. The literal portion up to the first verb goes into the prefix - // list. - classWorkspaceProxyPrefix -) - -type classification struct { - class pathClass - value string -} - -// classify inspects the right-hand side of a `path := ` assignment in an -// SDK service impl.go and reports whether the path is account-routed, a -// workspace proxy under accounts/ (and which match flavor), or unrelated. -// -// The classifier is intentionally strict: any expression that looks -// account-related but doesn't match a recognized idiom returns an error so the -// generator fails loudly rather than silently producing a wrong deny-list. -func classify(expr ast.Expr) (classification, error) { - if lit, ok := expr.(*ast.BasicLit); ok && lit.Kind == token.STRING { - s, err := strconv.Unquote(lit.Value) - if err != nil { - return classification{}, fmt.Errorf("unquote string literal: %w", err) - } - if !strings.Contains(s, "accounts/") { - return classification{class: classNotAccount}, nil - } - return classification{class: classWorkspaceProxyExact, value: s}, nil - } - - if call, ok := expr.(*ast.CallExpr); ok && isFmtSprintf(call) { - return classifySprintf(call) - } - - // Fallback: an unrecognized idiom. If the subtree contains no "accounts/" - // literal, it can't be a path we care about. If it does, the generator - // won't be able to decide, so fail. - if hasAccountsLiteral(expr) { - return classification{}, errors.New( - "path expression mentions \"accounts/\" but uses an unrecognized construction idiom; " + - "either teach the classifier the new shape or extend the deny-list manually") - } - return classification{class: classNotAccount}, nil -} - -func classifySprintf(call *ast.CallExpr) (classification, error) { - if len(call.Args) == 0 { - return classification{}, errors.New("fmt.Sprintf with no template") - } - tmplLit, ok := call.Args[0].(*ast.BasicLit) - if !ok || tmplLit.Kind != token.STRING { - // Sprintf with a non-literal template. We can't reason about it. - return classification{}, errors.New("fmt.Sprintf with non-literal template") - } - template, err := strconv.Unquote(tmplLit.Value) - if err != nil { - return classification{}, fmt.Errorf("unquote Sprintf template: %w", err) - } - if !strings.Contains(template, "accounts/") { - return classification{class: classNotAccount}, nil - } - - if slices.ContainsFunc(call.Args[1:], isAccountIDSource) { - return classification{class: classAccountAPI}, nil - } - - prefix := prefixUpToFirstVerb(template) - if prefix == "" { - return classification{}, fmt.Errorf("fmt.Sprintf template %q has no format verb", template) - } - // Guard: a prefix ending exactly at "accounts/" would match every account - // API under that family if it leaked into the deny-list. Refuse to emit - // rather than silently classify all of them as workspace-routed. - if strings.HasSuffix(prefix, "/accounts/") { - return classification{}, fmt.Errorf( - "path template %q has a format verb immediately after \"accounts/\" but no recognized account-ID source; "+ - "either teach the classifier a new account-ID spelling or extend the deny-list manually", - template) - } - // Guard: if the first format verb appears before the "accounts/" segment, - // the extracted prefix would not contain "accounts/" and would over-match - // unrelated APIs at runtime. Refuse to emit. - if !strings.Contains(prefix, "/accounts/") { - return classification{}, fmt.Errorf( - "path template %q has a format verb before the \"accounts/\" segment; "+ - "a prefix derived from this template would over-match unrelated APIs", - template) - } - return classification{class: classWorkspaceProxyPrefix, value: prefix}, nil -} - -func isFmtSprintf(call *ast.CallExpr) bool { - sel, ok := call.Fun.(*ast.SelectorExpr) - if !ok { - return false - } - id, ok := sel.X.(*ast.Ident) - if !ok { - return false - } - return id.Name == "fmt" && sel.Sel.Name == "Sprintf" -} - -// isAccountIDSource reports whether the expression resolves to one of the -// recognized spellings of "the account ID for the current call". The list is -// an explicit closed allowlist: a new SDK spelling we don't recognize triggers -// a generator error rather than being silently classified as a workspace -// proxy. Currently allowed shapes: a.client.ConfiguredAccountID(), -// cfg.AccountID, a.client.Config.AccountID. -func isAccountIDSource(e ast.Expr) bool { - if call, ok := e.(*ast.CallExpr); ok { - if isClientConfiguredAccountID(call) { - return true - } - } - if sel, ok := e.(*ast.SelectorExpr); ok { - if isCfgAccountID(sel) || isClientConfigAccountID(sel) { - return true - } - } - return false -} - -// isClientConfiguredAccountID matches a.client.ConfiguredAccountID() with no -// arguments. This is the only spelling used in the SDK today. -func isClientConfiguredAccountID(call *ast.CallExpr) bool { - if len(call.Args) != 0 { - return false - } - sel, ok := call.Fun.(*ast.SelectorExpr) - if !ok || sel.Sel.Name != "ConfiguredAccountID" { - return false - } - receiver, ok := sel.X.(*ast.SelectorExpr) - if !ok || receiver.Sel.Name != "client" { - return false - } - id, ok := receiver.X.(*ast.Ident) - return ok && id.Name == "a" -} - -func isCfgAccountID(s *ast.SelectorExpr) bool { - if s.Sel.Name != "AccountID" { - return false - } - id, ok := s.X.(*ast.Ident) - return ok && id.Name == "cfg" -} - -func isClientConfigAccountID(s *ast.SelectorExpr) bool { - if s.Sel.Name != "AccountID" { - return false - } - inner, ok := s.X.(*ast.SelectorExpr) - if !ok || inner.Sel.Name != "Config" { - return false - } - inner2, ok := inner.X.(*ast.SelectorExpr) - if !ok || inner2.Sel.Name != "client" { - return false - } - id, ok := inner2.X.(*ast.Ident) - return ok && id.Name == "a" -} - -// prefixUpToFirstVerb returns the literal portion an fmt.Sprintf template -// would render before the first format verb, with "%%" escapes resolved to -// "%". The result is what runtime strings.HasPrefix compares against, so the -// escapes must match the rendered URL rather than the template source. -// Returns "" if the template has no real verb. -func prefixUpToFirstVerb(template string) string { - var b strings.Builder - for i := 0; i < len(template); i++ { - if template[i] != '%' { - b.WriteByte(template[i]) - continue - } - if i+1 < len(template) && template[i+1] == '%' { - b.WriteByte('%') - i++ - continue - } - return b.String() - } - return "" -} - -func hasAccountsLiteral(expr ast.Expr) bool { - found := false - ast.Inspect(expr, func(n ast.Node) bool { - lit, ok := n.(*ast.BasicLit) - if !ok || lit.Kind != token.STRING { - return true - } - s, err := strconv.Unquote(lit.Value) - if err == nil && strings.Contains(s, "accounts/") { - found = true - return false - } - return true - }) - return found -} diff --git a/cmd/api/internal/genpaths/classify_test.go b/cmd/api/internal/genpaths/classify_test.go deleted file mode 100644 index 3def6567e99..00000000000 --- a/cmd/api/internal/genpaths/classify_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -import ( - "go/parser" - "go/token" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestClassify(t *testing.T) { - cases := []struct { - name string - src string - want classification - wantErr string - }{ - { - name: "literal proxy path -> exact match", - src: `"/api/2.0/preview/accounts/access-control/rule-sets"`, - want: classification{class: classWorkspaceProxyExact, value: "/api/2.0/preview/accounts/access-control/rule-sets"}, - }, - { - name: "literal path without accounts segment is ignored", - src: `"/api/2.0/clusters/list"`, - want: classification{class: classNotAccount}, - }, - { - name: "Sprintf without account-ID arg -> prefix", - src: `fmt.Sprintf("/api/2.0/accounts/servicePrincipals/%v/credentials/secrets", request.ServicePrincipalId)`, - want: classification{class: classWorkspaceProxyPrefix, value: "/api/2.0/accounts/servicePrincipals/"}, - }, - { - name: "Sprintf with ConfiguredAccountID -> account", - src: `fmt.Sprintf("/api/2.0/accounts/%v/scim/v2/Groups", a.client.ConfiguredAccountID())`, - want: classification{class: classAccountAPI}, - }, - { - name: "Sprintf with cfg.AccountID -> account", - src: `fmt.Sprintf("/api/2.0/accounts/%v/foo", cfg.AccountID)`, - want: classification{class: classAccountAPI}, - }, - { - name: "Sprintf with a.client.Config.AccountID -> account", - src: `fmt.Sprintf("/api/2.0/accounts/%v/foo", a.client.Config.AccountID)`, - want: classification{class: classAccountAPI}, - }, - { - name: "Sprintf multi-arg with ConfiguredAccountID -> account", - src: `fmt.Sprintf("/api/2.0/accounts/%v/workspaces/%v/permissionassignments", a.client.ConfiguredAccountID(), request.WorkspaceId)`, - want: classification{class: classAccountAPI}, - }, - { - name: "Sprintf without accounts segment is ignored", - src: `fmt.Sprintf("/api/2.0/clusters/%v/events", request.ClusterID)`, - want: classification{class: classNotAccount}, - }, - { - name: "string concatenation that mentions accounts/ -> error", - src: `"/api/2.0/preview/accounts/" + suffix`, - wantErr: "unrecognized construction idiom", - }, - { - name: "helper function call that mentions accounts/ -> error", - src: `buildPath("/api/2.0/preview/accounts/foo")`, - wantErr: "unrecognized construction idiom", - }, - { - name: "Sprintf with unrecognized account-ID source -> error (guard 1)", - src: `fmt.Sprintf("/api/foo/accounts/%v/bar", request.AccountId)`, - wantErr: `format verb immediately after "accounts/"`, - }, - { - name: "Sprintf with locally-aliased ConfiguredAccountID -> error (guard 1)", - src: `fmt.Sprintf("/api/2.0/accounts/%v/scim/v2/Groups", accID)`, - wantErr: `format verb immediately after "accounts/"`, - }, - { - name: "Sprintf with verb before accounts segment -> error", - src: `fmt.Sprintf("/api/2.0/%v/accounts/servicePrincipals/%v/credentials/secrets", request.Scope, request.ServicePrincipalId)`, - wantErr: `format verb before the "accounts/" segment`, - }, - { - name: "Sprintf with non-client receiver on ConfiguredAccountID -> error", - src: `fmt.Sprintf("/api/2.0/accounts/%v/foo", request.ConfiguredAccountID())`, - wantErr: `format verb immediately after "accounts/"`, - }, - { - name: "Sprintf with %% literal before real verb still classifies correctly", - src: `fmt.Sprintf("/api/2.0/accounts/servicePrincipals/%%marker/%v/credentials/secrets", request.ServicePrincipalId)`, - want: classification{class: classWorkspaceProxyPrefix, value: "/api/2.0/accounts/servicePrincipals/%marker/"}, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - expr, err := parser.ParseExpr(c.src) - require.NoError(t, err, "fixture must parse") - got, gotErr := classify(expr) - if c.wantErr != "" { - require.Error(t, gotErr) - assert.Contains(t, gotErr.Error(), c.wantErr) - return - } - require.NoError(t, gotErr) - assert.Equal(t, c.want, got) - }) - } -} - -// TestScanSDK runs the generator end-to-end against the pinned SDK. The -// expected sets are exact: a regression that adds an entry (overbroad prefix, -// new shape misclassified) is just as bad as one that drops an entry. -// -// When bumping the SDK pin, expected diffs go through `./task generate-paths`. -// Update this test only after confirming the new entries are intentional. -func TestScanSDK(t *testing.T) { - dir, err := resolveSDKDir() - require.NoError(t, err) - - prefixes, exacts, err := scanSDK(dir) - require.NoError(t, err) - - assert.Equal(t, []string{ - "/api/2.0/accounts/servicePrincipals/", - }, prefixes) - assert.Equal(t, []string{ - "/api/2.0/preview/accounts/access-control/assignable-roles", - "/api/2.0/preview/accounts/access-control/rule-sets", - }, exacts) -} - -// TestScanAST_VarPath verifies that `var path = ...` declarations are scanned -// the same way as `path := ...` assignments. This is a defensive case: the -// pinned SDK doesn't use the `var` form today, but if a future SDK introduces -// it, we want classification rather than silent skip. -func TestScanAST_VarPath(t *testing.T) { - src := `package svc -type api struct{} -func (a *api) F() { - var path = "/api/2.0/preview/accounts/access-control/rule-sets" - _ = path -} -` - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "test.go", src, 0) - require.NoError(t, err) - - prefixSet := map[string]struct{}{} - exactSet := map[string]struct{}{} - require.NoError(t, scanAST(fset, f, prefixSet, exactSet)) - - assert.Empty(t, prefixSet) - assert.Equal(t, map[string]struct{}{ - "/api/2.0/preview/accounts/access-control/rule-sets": {}, - }, exactSet) -} - -// TestScanAST_CompoundAssign verifies that compound assignments to `path` -// (like +=) cause the generator to fail loudly. State across statements is -// outside the per-expression classifier's contract. -func TestScanAST_CompoundAssign(t *testing.T) { - src := `package svc -type api struct{} -func (a *api) F() { - path := "/api/2.0" - path += "/preview/accounts/access-control/rule-sets" - _ = path -} -` - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "test.go", src, 0) - require.NoError(t, err) - - prefixSet := map[string]struct{}{} - exactSet := map[string]struct{}{} - err = scanAST(fset, f, prefixSet, exactSet) - require.Error(t, err) - assert.Contains(t, err.Error(), "compound assignment") -} diff --git a/cmd/api/internal/genpaths/main.go b/cmd/api/internal/genpaths/main.go deleted file mode 100644 index 5dd797d06a7..00000000000 --- a/cmd/api/internal/genpaths/main.go +++ /dev/null @@ -1,222 +0,0 @@ -// genpaths emits cmd/api/paths_generated.go: the deny-list of workspace-routed -// proxy paths that live under accounts/ in the Databricks SDK. The runtime -// classifier in cmd/api uses it to avoid mis-routing those proxies as -// account-scope calls. -// -// The generator parses every service/*/impl.go from the pinned SDK module, -// finds `path :=` assignments, and classifies each as account-routed or as a -// workspace proxy. See classify.go for the per-expression rules. -// -// Output goes to stdout; the Taskfile target redirects it to the checked-in -// file. -package main - -import ( - "bytes" - "fmt" - "go/ast" - "go/format" - "go/parser" - "go/token" - "os" - "os/exec" - "path/filepath" - "slices" - "strings" - "text/template" -) - -const sdkModule = "github.com/databricks/databricks-sdk-go" - -func main() { - if err := run(os.Stdout); err != nil { - fmt.Fprintf(os.Stderr, "genpaths: %v\n", err) - os.Exit(1) - } -} - -func run(out *os.File) error { - dir, err := resolveSDKDir() - if err != nil { - return err - } - prefixes, exacts, err := scanSDK(dir) - if err != nil { - return err - } - return render(out, prefixes, exacts) -} - -func resolveSDKDir() (string, error) { - cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", sdkModule) - output, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("go list -m %s: %w", sdkModule, err) - } - dir := strings.TrimSpace(string(output)) - if dir == "" { - return "", fmt.Errorf("go list -m %s returned empty directory", sdkModule) - } - return dir, nil -} - -// scanSDK walks every service/*/impl.go under dir and returns the deny-list -// entries grouped by match flavor. Both slices are deduplicated and sorted. -func scanSDK(dir string) (prefixes, exacts []string, err error) { - pattern := filepath.Join(dir, "service", "*", "impl.go") - files, err := filepath.Glob(pattern) - if err != nil { - return nil, nil, fmt.Errorf("glob %s: %w", pattern, err) - } - if len(files) == 0 { - return nil, nil, fmt.Errorf("no impl.go files found under %s", pattern) - } - slices.Sort(files) - - prefixSet := map[string]struct{}{} - exactSet := map[string]struct{}{} - for _, f := range files { - if err := scanFile(f, prefixSet, exactSet); err != nil { - return nil, nil, err - } - } - - prefixes = setToSortedSlice(prefixSet) - exacts = setToSortedSlice(exactSet) - return prefixes, exacts, nil -} - -func scanFile(path string, prefixSet, exactSet map[string]struct{}) error { - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, path, nil, 0) - if err != nil { - return fmt.Errorf("parse %s: %w", path, err) - } - return scanAST(fset, f, prefixSet, exactSet) -} - -// scanAST walks every function body in f and emits deny-list entries for any -// `path :=` or `var path = ...` assignment whose RHS is classified as a -// workspace proxy. Exposed separately from scanFile so tests can drive it -// with in-memory source. -func scanAST(fset *token.FileSet, f *ast.File, prefixSet, exactSet map[string]struct{}) error { - var classifyErr error - emit := func(expr ast.Expr, pos token.Pos) bool { - res, err := classify(expr) - if err != nil { - classifyErr = fmt.Errorf("%s: %w", fset.Position(pos), err) - return false - } - switch res.class { - case classWorkspaceProxyExact: - exactSet[res.value] = struct{}{} - case classWorkspaceProxyPrefix: - prefixSet[res.value] = struct{}{} - case classNotAccount, classAccountAPI: - // nothing to record; not part of the deny-list - } - return true - } - for _, decl := range f.Decls { - fn, ok := decl.(*ast.FuncDecl) - if !ok || fn.Body == nil { - continue - } - ast.Inspect(fn.Body, func(n ast.Node) bool { - if classifyErr != nil { - return false - } - switch s := n.(type) { - case *ast.AssignStmt: - if len(s.Lhs) != 1 || len(s.Rhs) != 1 { - return true - } - lhs, ok := s.Lhs[0].(*ast.Ident) - if !ok || lhs.Name != "path" { - return true - } - // Compound assignments (+=, -=, etc.) imply state across - // statements that the per-expression classifier can't track. - // Reject them so a future SDK that introduces this idiom - // fails loudly instead of silently emitting a fragment as a - // deny-list entry. - if s.Tok != token.DEFINE && s.Tok != token.ASSIGN { - classifyErr = fmt.Errorf("%s: compound assignment %v to `path` is not supported by the classifier; "+ - "the SDK has changed path-construction idioms — extend the classifier or split into a single assignment", - fset.Position(s.Pos()), s.Tok) - return false - } - return emit(s.Rhs[0], s.Pos()) - case *ast.DeclStmt: - gen, ok := s.Decl.(*ast.GenDecl) - if !ok || gen.Tok != token.VAR { - return true - } - for _, spec := range gen.Specs { - vs, ok := spec.(*ast.ValueSpec) - if !ok || len(vs.Values) != 1 || len(vs.Names) != 1 { - continue - } - if vs.Names[0].Name != "path" { - continue - } - if !emit(vs.Values[0], vs.Pos()) { - return false - } - } - return true - } - return true - }) - if classifyErr != nil { - return classifyErr - } - } - return nil -} - -func setToSortedSlice(m map[string]struct{}) []string { - out := make([]string, 0, len(m)) - for k := range m { - out = append(out, k) - } - slices.Sort(out) - return out -} - -const fileTemplate = `// Code generated by genpaths. DO NOT EDIT. - -package api - -// workspaceProxyPrefixes lists path prefixes (Sprintf-derived, ending before -// the first verb) for SDK endpoints that live under accounts/ but route to -// the workspace gateway. Matched with strings.HasPrefix. -var workspaceProxyPrefixes = []string{ -{{range .Prefixes}} {{printf "%q" .}}, -{{end}}} - -// workspaceProxyExact lists literal paths for SDK endpoints that live under -// accounts/ but route to the workspace gateway. Matched with map equality. -var workspaceProxyExact = map[string]struct{}{ -{{range .Exacts}} {{printf "%q" .}}: {}, -{{end}}} -` - -func render(out *os.File, prefixes, exacts []string) error { - tmpl, err := template.New("paths").Parse(fileTemplate) - if err != nil { - return fmt.Errorf("parse template: %w", err) - } - var raw bytes.Buffer - if err := tmpl.Execute(&raw, struct { - Prefixes, Exacts []string - }{prefixes, exacts}); err != nil { - return fmt.Errorf("execute template: %w", err) - } - formatted, err := format.Source(raw.Bytes()) - if err != nil { - return fmt.Errorf("gofmt generated source: %w\n--- raw ---\n%s", err, raw.String()) - } - _, err = out.Write(formatted) - return err -} diff --git a/cmd/api/paths.go b/cmd/api/paths.go new file mode 100644 index 00000000000..67b301f84d7 --- /dev/null +++ b/cmd/api/paths.go @@ -0,0 +1,29 @@ +package api + +import "strings" + +// workspaceProxyPrefixes lists SDK endpoints that live under accounts/ but +// route to the workspace gateway. Keep this list in sync with workspace-routed +// proxy APIs in the pinned SDK. +var workspaceProxyPrefixes = []string{ + "/api/2.0/accounts/servicePrincipals/", +} + +// workspaceProxyExact lists literal SDK endpoints that live under accounts/ but +// route to the workspace gateway. +var workspaceProxyExact = map[string]struct{}{ + "/api/2.0/preview/accounts/access-control/assignable-roles": {}, + "/api/2.0/preview/accounts/access-control/rule-sets": {}, +} + +func isWorkspaceProxyPath(path string) bool { + if _, ok := workspaceProxyExact[path]; ok { + return true + } + for _, prefix := range workspaceProxyPrefixes { + if strings.HasPrefix(path, prefix) { + return true + } + } + return false +} diff --git a/cmd/api/paths_generated.go b/cmd/api/paths_generated.go deleted file mode 100644 index 044d1493348..00000000000 --- a/cmd/api/paths_generated.go +++ /dev/null @@ -1,17 +0,0 @@ -// Code generated by genpaths. DO NOT EDIT. - -package api - -// workspaceProxyPrefixes lists path prefixes (Sprintf-derived, ending before -// the first verb) for SDK endpoints that live under accounts/ but route to -// the workspace gateway. Matched with strings.HasPrefix. -var workspaceProxyPrefixes = []string{ - "/api/2.0/accounts/servicePrincipals/", -} - -// workspaceProxyExact lists literal paths for SDK endpoints that live under -// accounts/ but route to the workspace gateway. Matched with map equality. -var workspaceProxyExact = map[string]struct{}{ - "/api/2.0/preview/accounts/access-control/assignable-roles": {}, - "/api/2.0/preview/accounts/access-control/rule-sets": {}, -} diff --git a/cmd/api/paths_test.go b/cmd/api/paths_test.go new file mode 100644 index 00000000000..c14da6b3ccc --- /dev/null +++ b/cmd/api/paths_test.go @@ -0,0 +1,52 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsWorkspaceProxyPath(t *testing.T) { + cases := []struct { + name string + path string + want bool + }{ + { + name: "assignable roles proxy", + path: "/api/2.0/preview/accounts/access-control/assignable-roles", + want: true, + }, + { + name: "rule sets proxy", + path: "/api/2.0/preview/accounts/access-control/rule-sets", + want: true, + }, + { + name: "service principal secrets proxy", + path: "/api/2.0/accounts/servicePrincipals/spn-123/credentials/secrets", + want: true, + }, + { + name: "account service principal secrets path has account id segment", + path: "/api/2.0/accounts/abc-123/servicePrincipals/spn-123/credentials/secrets", + want: false, + }, + { + name: "rule sets child is not part of exact proxy entry", + path: "/api/2.0/preview/accounts/access-control/rule-sets/foo", + want: false, + }, + { + name: "workspace path", + path: "/api/2.0/clusters/list", + want: false, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + assert.Equal(t, c.want, isWorkspaceProxyPath(c.path)) + }) + } +} From cf65ef8e26a07e1a503c61b5bd9dac617fbd5267 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 11:28:34 +0200 Subject: [PATCH 04/11] api: route per-call against unified hosts `databricks api ` previously bypassed the generated SDK header logic and called client.Do directly, so on unified hosts where workspace-vs-account routing is decided per-call it had no way to distinguish the two. This wires up per-call detection using the deny-list from the prior PR, plus three explicit overrides: --account scope this call to the account API --workspace-id override the workspace routing identifier {account_id} literal substituted from the active profile Detection runs on URL.Path so query strings and fragments can't false-match. The CLI-only WorkspaceIDNone sentinel (workspace_id = none in .databrickscfg) is normalized to empty before the SDK's idiomatic check sees it, so the literal "none" never goes on the wire. Behavior change for classic workspace profiles that have workspace_id set: the routing identifier is now sent. Classic gateways ignore the header so this should be benign; called out in the manual smoke plan in case it surfaces. Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 2 + .../cmd/api/account-flag/out.requests.txt | 21 ++ acceptance/cmd/api/account-flag/out.test.toml | 5 + acceptance/cmd/api/account-flag/output.txt | 1 + acceptance/cmd/api/account-flag/script | 1 + .../api/account-id-missing/out.requests.txt | 9 + .../cmd/api/account-id-missing/out.test.toml | 5 + .../cmd/api/account-id-missing/output.txt | 5 + acceptance/cmd/api/account-id-missing/script | 5 + .../account-id-substitution/out.requests.txt | 21 ++ .../api/account-id-substitution/out.test.toml | 5 + .../api/account-id-substitution/output.txt | 1 + .../cmd/api/account-id-substitution/script | 2 + .../cmd/api/account-path/out.requests.txt | 21 ++ acceptance/cmd/api/account-path/out.test.toml | 5 + acceptance/cmd/api/account-path/output.txt | 1 + acceptance/cmd/api/account-path/script | 1 + acceptance/cmd/api/test.toml | 22 ++ .../api/workspace-id-flag/out.requests.txt | 24 ++ .../cmd/api/workspace-id-flag/out.test.toml | 5 + .../cmd/api/workspace-id-flag/output.txt | 1 + acceptance/cmd/api/workspace-id-flag/script | 1 + .../api/workspace-id-none/out.requests.txt | 21 ++ .../cmd/api/workspace-id-none/out.test.toml | 5 + .../cmd/api/workspace-id-none/output.txt | 1 + acceptance/cmd/api/workspace-id-none/script | 12 + .../cmd/api/workspace-id-none/test.toml | 3 + .../cmd/api/workspace-path/out.requests.txt | 24 ++ .../cmd/api/workspace-path/out.test.toml | 5 + acceptance/cmd/api/workspace-path/output.txt | 1 + acceptance/cmd/api/workspace-path/script | 1 + .../out.requests.txt | 24 ++ .../workspace-proxy-regression/out.test.toml | 5 + .../api/workspace-proxy-regression/output.txt | 1 + .../cmd/api/workspace-proxy-regression/script | 4 + cmd/api/api.go | 134 ++++++++++- cmd/api/api_test.go | 220 ++++++++++++++++++ 37 files changed, 623 insertions(+), 2 deletions(-) create mode 100644 acceptance/cmd/api/account-flag/out.requests.txt create mode 100644 acceptance/cmd/api/account-flag/out.test.toml create mode 100644 acceptance/cmd/api/account-flag/output.txt create mode 100644 acceptance/cmd/api/account-flag/script create mode 100644 acceptance/cmd/api/account-id-missing/out.requests.txt create mode 100644 acceptance/cmd/api/account-id-missing/out.test.toml create mode 100644 acceptance/cmd/api/account-id-missing/output.txt create mode 100644 acceptance/cmd/api/account-id-missing/script create mode 100644 acceptance/cmd/api/account-id-substitution/out.requests.txt create mode 100644 acceptance/cmd/api/account-id-substitution/out.test.toml create mode 100644 acceptance/cmd/api/account-id-substitution/output.txt create mode 100644 acceptance/cmd/api/account-id-substitution/script create mode 100644 acceptance/cmd/api/account-path/out.requests.txt create mode 100644 acceptance/cmd/api/account-path/out.test.toml create mode 100644 acceptance/cmd/api/account-path/output.txt create mode 100644 acceptance/cmd/api/account-path/script create mode 100644 acceptance/cmd/api/test.toml create mode 100644 acceptance/cmd/api/workspace-id-flag/out.requests.txt create mode 100644 acceptance/cmd/api/workspace-id-flag/out.test.toml create mode 100644 acceptance/cmd/api/workspace-id-flag/output.txt create mode 100644 acceptance/cmd/api/workspace-id-flag/script create mode 100644 acceptance/cmd/api/workspace-id-none/out.requests.txt create mode 100644 acceptance/cmd/api/workspace-id-none/out.test.toml create mode 100644 acceptance/cmd/api/workspace-id-none/output.txt create mode 100644 acceptance/cmd/api/workspace-id-none/script create mode 100644 acceptance/cmd/api/workspace-id-none/test.toml create mode 100644 acceptance/cmd/api/workspace-path/out.requests.txt create mode 100644 acceptance/cmd/api/workspace-path/out.test.toml create mode 100644 acceptance/cmd/api/workspace-path/output.txt create mode 100644 acceptance/cmd/api/workspace-path/script create mode 100644 acceptance/cmd/api/workspace-proxy-regression/out.requests.txt create mode 100644 acceptance/cmd/api/workspace-proxy-regression/out.test.toml create mode 100644 acceptance/cmd/api/workspace-proxy-regression/output.txt create mode 100644 acceptance/cmd/api/workspace-proxy-regression/script create mode 100644 cmd/api/api_test.go diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 00152d550ea..f8dbd68987f 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,6 +4,8 @@ ### CLI +* `databricks api` now works against unified hosts. Adds `--account` to scope a call to the account API, `--workspace-id` to override the workspace routing identifier per call, and `{account_id}` substitution from the active profile's `account_id`. + ### Bundles ### Dependency updates diff --git a/acceptance/cmd/api/account-flag/out.requests.txt b/acceptance/cmd/api/account-flag/out.requests.txt new file mode 100644 index 00000000000..0dd6d7022fc --- /dev/null +++ b/acceptance/cmd/api/account-flag/out.requests.txt @@ -0,0 +1,21 @@ +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + ] + }, + "method": "GET", + "path": "/.well-known/databricks-config" +} +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list" +} diff --git a/acceptance/cmd/api/account-flag/out.test.toml b/acceptance/cmd/api/account-flag/out.test.toml new file mode 100644 index 00000000000..d560f1de043 --- /dev/null +++ b/acceptance/cmd/api/account-flag/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/account-flag/output.txt b/acceptance/cmd/api/account-flag/output.txt new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/acceptance/cmd/api/account-flag/output.txt @@ -0,0 +1 @@ +{} diff --git a/acceptance/cmd/api/account-flag/script b/acceptance/cmd/api/account-flag/script new file mode 100644 index 00000000000..ed4dd74b0c7 --- /dev/null +++ b/acceptance/cmd/api/account-flag/script @@ -0,0 +1 @@ +$CLI api get /api/2.0/clusters/list --account diff --git a/acceptance/cmd/api/account-id-missing/out.requests.txt b/acceptance/cmd/api/account-id-missing/out.requests.txt new file mode 100644 index 00000000000..64b313d01e4 --- /dev/null +++ b/acceptance/cmd/api/account-id-missing/out.requests.txt @@ -0,0 +1,9 @@ +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + ] + }, + "method": "GET", + "path": "/.well-known/databricks-config" +} diff --git a/acceptance/cmd/api/account-id-missing/out.test.toml b/acceptance/cmd/api/account-id-missing/out.test.toml new file mode 100644 index 00000000000..d560f1de043 --- /dev/null +++ b/acceptance/cmd/api/account-id-missing/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/account-id-missing/output.txt b/acceptance/cmd/api/account-id-missing/output.txt new file mode 100644 index 00000000000..44af079ab0b --- /dev/null +++ b/acceptance/cmd/api/account-id-missing/output.txt @@ -0,0 +1,5 @@ + +>>> errcode [CLI] api get /api/2.0/accounts/{account_id}/oauth2/published-app-integrations +Error: path contains {account_id} but no account_id is set on profile "" (set account_id in ~/.databrickscfg, export DATABRICKS_ACCOUNT_ID, or replace {account_id} in the path) + +Exit code: 1 diff --git a/acceptance/cmd/api/account-id-missing/script b/acceptance/cmd/api/account-id-missing/script new file mode 100644 index 00000000000..eba2e79868c --- /dev/null +++ b/acceptance/cmd/api/account-id-missing/script @@ -0,0 +1,5 @@ +# No DATABRICKS_ACCOUNT_ID; the testserver also doesn't set account_id in +# .well-known. The CLI must fail with the actionable error before any target +# API request is sent. (Config resolution still hits .well-known earlier in +# the lifecycle, which is expected.) +trace errcode $CLI api get '/api/2.0/accounts/{account_id}/oauth2/published-app-integrations' diff --git a/acceptance/cmd/api/account-id-substitution/out.requests.txt b/acceptance/cmd/api/account-id-substitution/out.requests.txt new file mode 100644 index 00000000000..0d779ee1b50 --- /dev/null +++ b/acceptance/cmd/api/account-id-substitution/out.requests.txt @@ -0,0 +1,21 @@ +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + ] + }, + "method": "GET", + "path": "/.well-known/databricks-config" +} +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ] + }, + "method": "GET", + "path": "/api/2.0/accounts/abc-123/oauth2/published-app-integrations" +} diff --git a/acceptance/cmd/api/account-id-substitution/out.test.toml b/acceptance/cmd/api/account-id-substitution/out.test.toml new file mode 100644 index 00000000000..d560f1de043 --- /dev/null +++ b/acceptance/cmd/api/account-id-substitution/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/account-id-substitution/output.txt b/acceptance/cmd/api/account-id-substitution/output.txt new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/acceptance/cmd/api/account-id-substitution/output.txt @@ -0,0 +1 @@ +{} diff --git a/acceptance/cmd/api/account-id-substitution/script b/acceptance/cmd/api/account-id-substitution/script new file mode 100644 index 00000000000..8e509e74085 --- /dev/null +++ b/acceptance/cmd/api/account-id-substitution/script @@ -0,0 +1,2 @@ +export DATABRICKS_ACCOUNT_ID=abc-123 +$CLI api get '/api/2.0/accounts/{account_id}/oauth2/published-app-integrations' diff --git a/acceptance/cmd/api/account-path/out.requests.txt b/acceptance/cmd/api/account-path/out.requests.txt new file mode 100644 index 00000000000..d143b64be99 --- /dev/null +++ b/acceptance/cmd/api/account-path/out.requests.txt @@ -0,0 +1,21 @@ +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + ] + }, + "method": "GET", + "path": "/.well-known/databricks-config" +} +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ] + }, + "method": "GET", + "path": "/api/2.0/accounts/abc-123/network-policies" +} diff --git a/acceptance/cmd/api/account-path/out.test.toml b/acceptance/cmd/api/account-path/out.test.toml new file mode 100644 index 00000000000..d560f1de043 --- /dev/null +++ b/acceptance/cmd/api/account-path/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/account-path/output.txt b/acceptance/cmd/api/account-path/output.txt new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/acceptance/cmd/api/account-path/output.txt @@ -0,0 +1 @@ +{} diff --git a/acceptance/cmd/api/account-path/script b/acceptance/cmd/api/account-path/script new file mode 100644 index 00000000000..67e46484e43 --- /dev/null +++ b/acceptance/cmd/api/account-path/script @@ -0,0 +1 @@ +$CLI api get /api/2.0/accounts/abc-123/network-policies diff --git a/acceptance/cmd/api/test.toml b/acceptance/cmd/api/test.toml new file mode 100644 index 00000000000..69e31b128f1 --- /dev/null +++ b/acceptance/cmd/api/test.toml @@ -0,0 +1,22 @@ +RecordRequests = true +IncludeRequestHeaders = ["Authorization", "User-Agent", "X-Databricks-Org-Id"] + +# Common stubs for paths used across variants. Each returns an empty JSON +# object; the variants assert on the *recorded request* (out.requests.txt), +# not on the response body, so any well-formed JSON is fine. + +[[Server]] +Pattern = "GET /api/2.0/clusters/list" +Response.Body = '{}' + +[[Server]] +Pattern = "GET /api/2.0/accounts/abc-123/network-policies" +Response.Body = '{}' + +[[Server]] +Pattern = "GET /api/2.0/accounts/abc-123/oauth2/published-app-integrations" +Response.Body = '{}' + +[[Server]] +Pattern = "GET /api/2.0/preview/accounts/access-control/rule-sets" +Response.Body = '{}' diff --git a/acceptance/cmd/api/workspace-id-flag/out.requests.txt b/acceptance/cmd/api/workspace-id-flag/out.requests.txt new file mode 100644 index 00000000000..70362503692 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-flag/out.requests.txt @@ -0,0 +1,24 @@ +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + ] + }, + "method": "GET", + "path": "/.well-known/databricks-config" +} +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ], + "X-Databricks-Org-Id": [ + "999" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list" +} diff --git a/acceptance/cmd/api/workspace-id-flag/out.test.toml b/acceptance/cmd/api/workspace-id-flag/out.test.toml new file mode 100644 index 00000000000..d560f1de043 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-flag/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/workspace-id-flag/output.txt b/acceptance/cmd/api/workspace-id-flag/output.txt new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/acceptance/cmd/api/workspace-id-flag/output.txt @@ -0,0 +1 @@ +{} diff --git a/acceptance/cmd/api/workspace-id-flag/script b/acceptance/cmd/api/workspace-id-flag/script new file mode 100644 index 00000000000..741c4048c86 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-flag/script @@ -0,0 +1 @@ +$CLI api get /api/2.0/clusters/list --workspace-id 999 diff --git a/acceptance/cmd/api/workspace-id-none/out.requests.txt b/acceptance/cmd/api/workspace-id-none/out.requests.txt new file mode 100644 index 00000000000..0dd6d7022fc --- /dev/null +++ b/acceptance/cmd/api/workspace-id-none/out.requests.txt @@ -0,0 +1,21 @@ +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + ] + }, + "method": "GET", + "path": "/.well-known/databricks-config" +} +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list" +} diff --git a/acceptance/cmd/api/workspace-id-none/out.test.toml b/acceptance/cmd/api/workspace-id-none/out.test.toml new file mode 100644 index 00000000000..d560f1de043 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-none/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/workspace-id-none/output.txt b/acceptance/cmd/api/workspace-id-none/output.txt new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/acceptance/cmd/api/workspace-id-none/output.txt @@ -0,0 +1 @@ +{} diff --git a/acceptance/cmd/api/workspace-id-none/script b/acceptance/cmd/api/workspace-id-none/script new file mode 100644 index 00000000000..692f06ba971 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-none/script @@ -0,0 +1,12 @@ +# Profile with workspace_id = none overrides the host-metadata back-fill. +# The CLI must strip the sentinel before the header decision; the recorded +# request should not carry the routing identifier. +sethome "./home" +cat > "./home/.databrickscfg" < no identifier", + cfgWorkspaceID: "", + path: workspacePath, + want: "", + }, + { + name: "WorkspaceID set + workspace path -> sends identifier", + cfgWorkspaceID: resolvedWSID, + path: workspacePath, + want: resolvedWSID, + }, + { + name: "WorkspaceID set + account path -> no identifier (auto-detect)", + cfgWorkspaceID: resolvedWSID, + path: accountPath, + want: "", + }, + { + name: "WorkspaceID set + workspace-routed proxy under accounts/", + cfgWorkspaceID: resolvedWSID, + path: proxyPath, + want: resolvedWSID, + }, + { + name: "--account on workspace path", + forceAccount: true, + cfgWorkspaceID: resolvedWSID, + path: workspacePath, + want: "", + }, + { + name: "--workspace-id overrides resolved value", + workspaceIDFlag: flagWSID, + flagSet: true, + cfgWorkspaceID: resolvedWSID, + path: workspacePath, + want: flagWSID, + }, + { + name: "--workspace-id on account path still overrides", + workspaceIDFlag: flagWSID, + flagSet: true, + cfgWorkspaceID: resolvedWSID, + path: accountPath, + want: flagWSID, + }, + { + name: "--workspace-id empty value -> error", + workspaceIDFlag: "", + flagSet: true, + cfgWorkspaceID: resolvedWSID, + path: workspacePath, + wantErrSubstring: "--workspace-id requires a value", + }, + { + name: "--account and --workspace-id both set -> error", + forceAccount: true, + workspaceIDFlag: flagWSID, + flagSet: true, + cfgWorkspaceID: resolvedWSID, + path: workspacePath, + wantErrSubstring: "mutually exclusive", + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, err := resolveOrgID(c.forceAccount, c.workspaceIDFlag, c.flagSet, c.cfgWorkspaceID, c.path) + if c.wantErrSubstring != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), c.wantErrSubstring) + return + } + require.NoError(t, err) + assert.Equal(t, c.want, got) + }) + } +} + +// TestNormalizeWorkspaceID covers the helper that strips the CLI-only +// WorkspaceIDNone sentinel. RunE calls this directly before resolveOrgID, so +// a regression here would surface as the literal "none" being sent on the +// wire. +func TestNormalizeWorkspaceID(t *testing.T) { + cases := []struct { + name string + in string + want string + }{ + {"sentinel stripped to empty", auth.WorkspaceIDNone, ""}, + {"empty passes through", "", ""}, + {"normal value passes through", "900800700600", "900800700600"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + assert.Equal(t, c.want, normalizeWorkspaceID(c.in)) + }) + } +} + +func TestSubstituteAccountID(t *testing.T) { + cases := []struct { + name string + path string + accountID string + profile string + want string + wantErrSubstring string + }{ + { + name: "placeholder absent leaves path unchanged", + path: "/api/2.0/clusters/list", + accountID: "abc-123", + profile: "DEFAULT", + want: "/api/2.0/clusters/list", + }, + { + name: "placeholder present + account_id set", + path: "/api/2.0/accounts/{account_id}/oauth2/published-app-integrations", + accountID: "abc-123", + profile: "DEFAULT", + want: "/api/2.0/accounts/abc-123/oauth2/published-app-integrations", + }, + { + name: "multiple placeholders all replaced", + path: "/api/2.0/accounts/{account_id}/workspaces/123/foo?ref=accounts/{account_id}", + accountID: "abc-123", + profile: "DEFAULT", + want: "/api/2.0/accounts/abc-123/workspaces/123/foo?ref=accounts/abc-123", + }, + { + name: "placeholder present + account_id empty -> error", + path: "/api/2.0/accounts/{account_id}/oauth2/published-app-integrations", + accountID: "", + profile: "DEFAULT", + wantErrSubstring: `{account_id}`, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, err := substituteAccountID(c.path, c.accountID, c.profile) + if c.wantErrSubstring != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), c.wantErrSubstring) + assert.Contains(t, err.Error(), `profile "`+c.profile+`"`) + assert.Contains(t, err.Error(), "DATABRICKS_ACCOUNT_ID") + return + } + require.NoError(t, err) + assert.Equal(t, c.want, got) + }) + } +} From 4769fa7eb69026814f090243e74c2e634b0556fe Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 12:16:56 +0200 Subject: [PATCH 05/11] api: route account detection through proxy helper Keep the manual workspace-proxy list behind one helper so tests exercise the same path used by runtime account detection. --- cmd/api/api.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/cmd/api/api.go b/cmd/api/api.go index 2a3cfb7bfe3..7c6219cec5a 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -28,8 +28,8 @@ const ( // accountSegmentRe matches a non-empty segment immediately after "accounts/", // anchored at the start of the path or after a "/". Account-ID shape is -// deliberately opaque; the workspace-proxy lists in paths_generated.go carve -// out the SDK proxies that also live under /accounts/. +// deliberately opaque; the workspace-proxy list carves out SDK proxies that +// also live under /accounts/. var accountSegmentRe = regexp.MustCompile(`(^|/)accounts/[^/]+`) func New() *cobra.Command { @@ -154,21 +154,16 @@ func substituteAccountID(path, accountID, profile string) (string, error) { // hasAccountSegment reports whether path is an account-scope API. The match // runs on URL.Path, so query strings and fragments containing "/accounts/" // can't trigger a false positive. Returns false for paths that match a known -// workspace-routed proxy from the generated tables. +// workspace-routed proxy from the proxy path tables. func hasAccountSegment(rawPath string) (bool, error) { u, err := url.Parse(rawPath) if err != nil { return false, fmt.Errorf("parse path: %w", err) } p := u.Path - if _, ok := workspaceProxyExact[p]; ok { + if isWorkspaceProxyPath(p) { return false, nil } - for _, prefix := range workspaceProxyPrefixes { - if strings.HasPrefix(p, prefix) { - return false, nil - } - } return accountSegmentRe.MatchString(p), nil } From 116914e5694620df637e40efd0baf7f4cfe3f036 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 30 Apr 2026 12:38:58 +0200 Subject: [PATCH 06/11] acceptance: stabilize cmd/api recorded UA across CI and OS Add the standard OS and CI/cicd replacements to acceptance/cmd/api/test.toml and regenerate the recorded User-Agent strings to use os/[OS]. Without these, goldens generated locally on macOS contain os/darwin and no cicd/ segment, which fail on Linux + GitHub Actions where the SDK records os/linux ... cicd/github. Co-authored-by: Isaac --- .../cmd/api/account-flag/out.requests.txt | 4 ++-- .../api/account-id-missing/out.requests.txt | 2 +- .../account-id-substitution/out.requests.txt | 4 ++-- .../cmd/api/account-path/out.requests.txt | 4 ++-- acceptance/cmd/api/test.toml | 18 ++++++++++++++++++ .../cmd/api/workspace-id-flag/out.requests.txt | 4 ++-- .../cmd/api/workspace-id-none/out.requests.txt | 4 ++-- .../cmd/api/workspace-path/out.requests.txt | 4 ++-- .../out.requests.txt | 4 ++-- 9 files changed, 33 insertions(+), 15 deletions(-) diff --git a/acceptance/cmd/api/account-flag/out.requests.txt b/acceptance/cmd/api/account-flag/out.requests.txt index 0dd6d7022fc..f0aeed9b3c1 100644 --- a/acceptance/cmd/api/account-flag/out.requests.txt +++ b/acceptance/cmd/api/account-flag/out.requests.txt @@ -1,7 +1,7 @@ { "headers": { "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" ] }, "method": "GET", @@ -13,7 +13,7 @@ "Bearer [DATABRICKS_TOKEN]" ], "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ] }, "method": "GET", diff --git a/acceptance/cmd/api/account-id-missing/out.requests.txt b/acceptance/cmd/api/account-id-missing/out.requests.txt index 64b313d01e4..f42ffdcd34b 100644 --- a/acceptance/cmd/api/account-id-missing/out.requests.txt +++ b/acceptance/cmd/api/account-id-missing/out.requests.txt @@ -1,7 +1,7 @@ { "headers": { "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" ] }, "method": "GET", diff --git a/acceptance/cmd/api/account-id-substitution/out.requests.txt b/acceptance/cmd/api/account-id-substitution/out.requests.txt index 0d779ee1b50..59fa96c3dbc 100644 --- a/acceptance/cmd/api/account-id-substitution/out.requests.txt +++ b/acceptance/cmd/api/account-id-substitution/out.requests.txt @@ -1,7 +1,7 @@ { "headers": { "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" ] }, "method": "GET", @@ -13,7 +13,7 @@ "Bearer [DATABRICKS_TOKEN]" ], "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ] }, "method": "GET", diff --git a/acceptance/cmd/api/account-path/out.requests.txt b/acceptance/cmd/api/account-path/out.requests.txt index d143b64be99..3db1b27a54e 100644 --- a/acceptance/cmd/api/account-path/out.requests.txt +++ b/acceptance/cmd/api/account-path/out.requests.txt @@ -1,7 +1,7 @@ { "headers": { "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" ] }, "method": "GET", @@ -13,7 +13,7 @@ "Bearer [DATABRICKS_TOKEN]" ], "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ] }, "method": "GET", diff --git a/acceptance/cmd/api/test.toml b/acceptance/cmd/api/test.toml index 69e31b128f1..11d83c3f486 100644 --- a/acceptance/cmd/api/test.toml +++ b/acceptance/cmd/api/test.toml @@ -1,6 +1,24 @@ RecordRequests = true IncludeRequestHeaders = ["Authorization", "User-Agent", "X-Databricks-Org-Id"] +# Normalize OS-dependent and CI-only User-Agent segments so the recorded +# requests are stable across local macOS/Linux runs and GitHub Actions. +[[Repls]] +Old = '(linux|darwin|windows)' +New = '[OS]' + +[[Repls]] +Old = " cicd/[A-Za-z0-9.-]+" +New = "" + +[[Repls]] +Old = " upstream/[A-Za-z0-9.-]+" +New = "" + +[[Repls]] +Old = " upstream-version/[A-Za-z0-9.-]+" +New = "" + # Common stubs for paths used across variants. Each returns an empty JSON # object; the variants assert on the *recorded request* (out.requests.txt), # not on the response body, so any well-formed JSON is fine. diff --git a/acceptance/cmd/api/workspace-id-flag/out.requests.txt b/acceptance/cmd/api/workspace-id-flag/out.requests.txt index 70362503692..5f6cc8e1f2a 100644 --- a/acceptance/cmd/api/workspace-id-flag/out.requests.txt +++ b/acceptance/cmd/api/workspace-id-flag/out.requests.txt @@ -1,7 +1,7 @@ { "headers": { "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" ] }, "method": "GET", @@ -13,7 +13,7 @@ "Bearer [DATABRICKS_TOKEN]" ], "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ], "X-Databricks-Org-Id": [ "999" diff --git a/acceptance/cmd/api/workspace-id-none/out.requests.txt b/acceptance/cmd/api/workspace-id-none/out.requests.txt index 0dd6d7022fc..f0aeed9b3c1 100644 --- a/acceptance/cmd/api/workspace-id-none/out.requests.txt +++ b/acceptance/cmd/api/workspace-id-none/out.requests.txt @@ -1,7 +1,7 @@ { "headers": { "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" ] }, "method": "GET", @@ -13,7 +13,7 @@ "Bearer [DATABRICKS_TOKEN]" ], "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ] }, "method": "GET", diff --git a/acceptance/cmd/api/workspace-path/out.requests.txt b/acceptance/cmd/api/workspace-path/out.requests.txt index 79c097b67bd..45be4b47db9 100644 --- a/acceptance/cmd/api/workspace-path/out.requests.txt +++ b/acceptance/cmd/api/workspace-path/out.requests.txt @@ -1,7 +1,7 @@ { "headers": { "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" ] }, "method": "GET", @@ -13,7 +13,7 @@ "Bearer [DATABRICKS_TOKEN]" ], "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ], "X-Databricks-Org-Id": [ "[NUMID]" diff --git a/acceptance/cmd/api/workspace-proxy-regression/out.requests.txt b/acceptance/cmd/api/workspace-proxy-regression/out.requests.txt index 736d0604951..9e3a2cfd165 100644 --- a/acceptance/cmd/api/workspace-proxy-regression/out.requests.txt +++ b/acceptance/cmd/api/workspace-proxy-regression/out.requests.txt @@ -1,7 +1,7 @@ { "headers": { "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" ] }, "method": "GET", @@ -13,7 +13,7 @@ "Bearer [DATABRICKS_TOKEN]" ], "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/darwin cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" ], "X-Databricks-Org-Id": [ "[NUMID]" From 29a2fb634fdd3aaac8e507629ca319885a062b57 Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 4 May 2026 11:22:46 +0200 Subject: [PATCH 07/11] api: treat ?o= as per-call workspace override SPOG URLs from the Databricks UI carry the workspace ID as a query param (e.g. /api/2.2/jobs/list?o=7474644166319138). Recognize that param when present and use it as the routing identifier so pasted URLs route correctly without requiring --workspace-id. Precedence: --account > --workspace-id flag > ?o= > account-path auto-detect > profile workspace_id. Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 2 +- .../workspace-id-from-query/out.requests.txt | 27 +++++++ .../api/workspace-id-from-query/out.test.toml | 5 ++ .../api/workspace-id-from-query/output.txt | 1 + .../cmd/api/workspace-id-from-query/script | 1 + cmd/api/api.go | 24 +++++++ cmd/api/api_test.go | 70 +++++++++++++++++-- 7 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 acceptance/cmd/api/workspace-id-from-query/out.requests.txt create mode 100644 acceptance/cmd/api/workspace-id-from-query/out.test.toml create mode 100644 acceptance/cmd/api/workspace-id-from-query/output.txt create mode 100644 acceptance/cmd/api/workspace-id-from-query/script diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index f8dbd68987f..319fe320b2a 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,7 +4,7 @@ ### CLI -* `databricks api` now works against unified hosts. Adds `--account` to scope a call to the account API, `--workspace-id` to override the workspace routing identifier per call, and `{account_id}` substitution from the active profile's `account_id`. +* `databricks api` now works against unified hosts. Adds `--account` to scope a call to the account API, `--workspace-id` to override the workspace routing identifier per call, and `{account_id}` substitution from the active profile's `account_id`. A `?o=` query parameter on the path (the SPOG URL convention used by the Databricks UI) is also recognized as a per-call workspace override, so URLs pasted from the browser route correctly. ### Bundles diff --git a/acceptance/cmd/api/workspace-id-from-query/out.requests.txt b/acceptance/cmd/api/workspace-id-from-query/out.requests.txt new file mode 100644 index 00000000000..f94e6fe2317 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-query/out.requests.txt @@ -0,0 +1,27 @@ +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" + ] + }, + "method": "GET", + "path": "/.well-known/databricks-config" +} +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ], + "X-Databricks-Org-Id": [ + "999" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list", + "q": { + "o": "999" + } +} diff --git a/acceptance/cmd/api/workspace-id-from-query/out.test.toml b/acceptance/cmd/api/workspace-id-from-query/out.test.toml new file mode 100644 index 00000000000..d560f1de043 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-query/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/workspace-id-from-query/output.txt b/acceptance/cmd/api/workspace-id-from-query/output.txt new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-query/output.txt @@ -0,0 +1 @@ +{} diff --git a/acceptance/cmd/api/workspace-id-from-query/script b/acceptance/cmd/api/workspace-id-from-query/script new file mode 100644 index 00000000000..9357873fea3 --- /dev/null +++ b/acceptance/cmd/api/workspace-id-from-query/script @@ -0,0 +1 @@ +$CLI api get "/api/2.0/clusters/list?o=999" diff --git a/cmd/api/api.go b/cmd/api/api.go index 7c6219cec5a..6edd049b020 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -23,6 +23,13 @@ const ( // per-call when cfg.WorkspaceID is populated; we mirror the same idiom. orgIDHeader = "X-Databricks-Org-Id" + // orgIDQueryParam is the SPOG (single-page-of-glass) URL convention used + // by the Databricks UI: "?o=" identifies the workspace a URL + // targets. When present on the path, we treat it as a per-call override + // for the workspace routing identifier so that pasted SPOG URLs route + // correctly without requiring --workspace-id. + orgIDQueryParam = "o" + accountIDPlaceholder = "{account_id}" ) @@ -167,6 +174,16 @@ func hasAccountSegment(rawPath string) (bool, error) { return accountSegmentRe.MatchString(p), nil } +// extractOrgIDFromQuery returns the value of the "o" query parameter on path +// (the SPOG URL convention), or "" if absent or empty. +func extractOrgIDFromQuery(rawPath string) (string, error) { + u, err := url.Parse(rawPath) + if err != nil { + return "", fmt.Errorf("parse path: %w", err) + } + return u.Query().Get(orgIDQueryParam), nil +} + // resolveOrgID picks the value (if any) for the workspace routing identifier // based on flags, the resolved profile, and the path shape. Returns "" when // no header should be sent. @@ -189,6 +206,13 @@ func resolveOrgID( } return workspaceIDFlag, nil } + orgIDFromQuery, err := extractOrgIDFromQuery(path) + if err != nil { + return "", err + } + if orgIDFromQuery != "" { + return orgIDFromQuery, nil + } isAccount, err := hasAccountSegment(path) if err != nil { return "", err diff --git a/cmd/api/api_test.go b/cmd/api/api_test.go index 04f33c0bf95..427262e5449 100644 --- a/cmd/api/api_test.go +++ b/cmd/api/api_test.go @@ -46,13 +46,40 @@ func TestHasAccountSegment(t *testing.T) { } } +func TestExtractOrgIDFromQuery(t *testing.T) { + cases := []struct { + name string + path string + want string + }{ + {"no query string", "/api/2.0/clusters/list", ""}, + {"o param present", "/api/2.2/jobs/list?o=7474644166319138", "7474644166319138"}, + {"o param empty", "/api/2.0/clusters/list?o=", ""}, + {"o among other params first", "/api/2.0/clusters/list?o=123&foo=bar", "123"}, + {"o among other params last", "/api/2.0/clusters/list?foo=bar&o=123", "123"}, + {"unrelated o-prefixed param ignored", "/api/2.0/clusters/list?other=1", ""}, + {"absolute URL", "https://example.com/api/2.0/clusters/list?o=42", "42"}, + {"first value wins on duplicate", "/api/2.0/clusters/list?o=1&o=2", "1"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, err := extractOrgIDFromQuery(c.path) + require.NoError(t, err) + assert.Equal(t, c.want, got) + }) + } +} + func TestResolveOrgID(t *testing.T) { const ( - workspacePath = "/api/2.0/clusters/list" - accountPath = "/api/2.0/accounts/abc-123/network-policies" - proxyPath = "/api/2.0/preview/accounts/access-control/rule-sets" - resolvedWSID = "900800700600" - flagWSID = "999" + workspacePath = "/api/2.0/clusters/list" + accountPath = "/api/2.0/accounts/abc-123/network-policies" + proxyPath = "/api/2.0/preview/accounts/access-control/rule-sets" + spogPath = "/api/2.2/jobs/list?o=7474644166319138" + spogAccountPath = "/api/2.0/accounts/abc-123/network-policies?o=7474644166319138" + spogWorkspaceID = "7474644166319138" + resolvedWSID = "900800700600" + flagWSID = "999" ) cases := []struct { @@ -129,6 +156,39 @@ func TestResolveOrgID(t *testing.T) { path: workspacePath, wantErrSubstring: "mutually exclusive", }, + { + name: "?o= sets identifier when no flag and no profile WorkspaceID", + cfgWorkspaceID: "", + path: spogPath, + want: spogWorkspaceID, + }, + { + name: "?o= overrides profile WorkspaceID", + cfgWorkspaceID: resolvedWSID, + path: spogPath, + want: spogWorkspaceID, + }, + { + name: "--workspace-id wins over ?o=", + workspaceIDFlag: flagWSID, + flagSet: true, + cfgWorkspaceID: resolvedWSID, + path: spogPath, + want: flagWSID, + }, + { + name: "--account wins over ?o=", + forceAccount: true, + cfgWorkspaceID: resolvedWSID, + path: spogPath, + want: "", + }, + { + name: "?o= on /accounts/ path still routes to that workspace", + cfgWorkspaceID: "", + path: spogAccountPath, + want: spogWorkspaceID, + }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { From adf58b6b4035869210f792a4b9f392c3a92c299e Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 4 May 2026 13:58:12 +0200 Subject: [PATCH 08/11] acceptance: fix Windows path conversion and refresh out.test.toml cmd/api scripts pass POSIX paths like /api/2.0/clusters/list to the CLI. Git Bash on Windows converts a leading "/" arg to a Windows path, so the CLI sees /Program Files/Git/api/2.0/... and the testserver returns 404. Set MSYS_NO_PATHCONV=1 in the parent test.toml, matching the pattern used by cmd/workspace/export-dir-* and workspace/repos. Also regenerate out.test.toml with the diff-friendly inline format introduced by #5146. Co-authored-by: Isaac --- acceptance/cmd/api/account-flag/out.test.toml | 4 +--- acceptance/cmd/api/account-id-missing/out.test.toml | 4 +--- acceptance/cmd/api/account-id-substitution/out.test.toml | 4 +--- acceptance/cmd/api/account-path/out.test.toml | 4 +--- acceptance/cmd/api/test.toml | 6 ++++++ acceptance/cmd/api/workspace-id-flag/out.test.toml | 4 +--- acceptance/cmd/api/workspace-id-from-query/out.test.toml | 4 +--- acceptance/cmd/api/workspace-id-none/out.test.toml | 4 +--- acceptance/cmd/api/workspace-path/out.test.toml | 4 +--- acceptance/cmd/api/workspace-proxy-regression/out.test.toml | 4 +--- 10 files changed, 15 insertions(+), 27 deletions(-) diff --git a/acceptance/cmd/api/account-flag/out.test.toml b/acceptance/cmd/api/account-flag/out.test.toml index d560f1de043..f784a183258 100644 --- a/acceptance/cmd/api/account-flag/out.test.toml +++ b/acceptance/cmd/api/account-flag/out.test.toml @@ -1,5 +1,3 @@ Local = true Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/account-id-missing/out.test.toml b/acceptance/cmd/api/account-id-missing/out.test.toml index d560f1de043..f784a183258 100644 --- a/acceptance/cmd/api/account-id-missing/out.test.toml +++ b/acceptance/cmd/api/account-id-missing/out.test.toml @@ -1,5 +1,3 @@ Local = true Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/account-id-substitution/out.test.toml b/acceptance/cmd/api/account-id-substitution/out.test.toml index d560f1de043..f784a183258 100644 --- a/acceptance/cmd/api/account-id-substitution/out.test.toml +++ b/acceptance/cmd/api/account-id-substitution/out.test.toml @@ -1,5 +1,3 @@ Local = true Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/account-path/out.test.toml b/acceptance/cmd/api/account-path/out.test.toml index d560f1de043..f784a183258 100644 --- a/acceptance/cmd/api/account-path/out.test.toml +++ b/acceptance/cmd/api/account-path/out.test.toml @@ -1,5 +1,3 @@ Local = true Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/test.toml b/acceptance/cmd/api/test.toml index 11d83c3f486..35efdf107e7 100644 --- a/acceptance/cmd/api/test.toml +++ b/acceptance/cmd/api/test.toml @@ -1,6 +1,12 @@ RecordRequests = true IncludeRequestHeaders = ["Authorization", "User-Agent", "X-Databricks-Org-Id"] +[Env] +# MSYS2 (Git Bash on Windows) auto-converts a leading "/" argument to a +# Windows path (e.g. /api/2.0/... becomes C:/Program Files/Git/api/2.0/...). +# Disable that so the CLI receives the path verbatim. +MSYS_NO_PATHCONV = "1" + # Normalize OS-dependent and CI-only User-Agent segments so the recorded # requests are stable across local macOS/Linux runs and GitHub Actions. [[Repls]] diff --git a/acceptance/cmd/api/workspace-id-flag/out.test.toml b/acceptance/cmd/api/workspace-id-flag/out.test.toml index d560f1de043..f784a183258 100644 --- a/acceptance/cmd/api/workspace-id-flag/out.test.toml +++ b/acceptance/cmd/api/workspace-id-flag/out.test.toml @@ -1,5 +1,3 @@ Local = true Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/workspace-id-from-query/out.test.toml b/acceptance/cmd/api/workspace-id-from-query/out.test.toml index d560f1de043..f784a183258 100644 --- a/acceptance/cmd/api/workspace-id-from-query/out.test.toml +++ b/acceptance/cmd/api/workspace-id-from-query/out.test.toml @@ -1,5 +1,3 @@ Local = true Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/workspace-id-none/out.test.toml b/acceptance/cmd/api/workspace-id-none/out.test.toml index d560f1de043..f784a183258 100644 --- a/acceptance/cmd/api/workspace-id-none/out.test.toml +++ b/acceptance/cmd/api/workspace-id-none/out.test.toml @@ -1,5 +1,3 @@ Local = true Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/workspace-path/out.test.toml b/acceptance/cmd/api/workspace-path/out.test.toml index d560f1de043..f784a183258 100644 --- a/acceptance/cmd/api/workspace-path/out.test.toml +++ b/acceptance/cmd/api/workspace-path/out.test.toml @@ -1,5 +1,3 @@ Local = true Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/workspace-proxy-regression/out.test.toml b/acceptance/cmd/api/workspace-proxy-regression/out.test.toml index d560f1de043..f784a183258 100644 --- a/acceptance/cmd/api/workspace-proxy-regression/out.test.toml +++ b/acceptance/cmd/api/workspace-proxy-regression/out.test.toml @@ -1,5 +1,3 @@ Local = true Cloud = false - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] From 1ea4ffc858186b1e16f1616e543e601c97dda06a Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 4 May 2026 15:38:37 +0200 Subject: [PATCH 09/11] api: drop {account_id} path substitution Pull this out of the per-call routing PR per review feedback. The routing change does not need account-ID interpolation to land; if we want it later it can be a focused follow-up alongside any other path-substitution we decide to support. Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 2 +- .../api/account-id-missing/out.requests.txt | 9 ---- .../cmd/api/account-id-missing/out.test.toml | 3 -- .../cmd/api/account-id-missing/output.txt | 5 -- acceptance/cmd/api/account-id-missing/script | 5 -- .../account-id-substitution/out.requests.txt | 21 -------- .../api/account-id-substitution/out.test.toml | 3 -- .../api/account-id-substitution/output.txt | 1 - .../cmd/api/account-id-substitution/script | 2 - cmd/api/api.go | 24 --------- cmd/api/api_test.go | 54 ------------------- 11 files changed, 1 insertion(+), 128 deletions(-) delete mode 100644 acceptance/cmd/api/account-id-missing/out.requests.txt delete mode 100644 acceptance/cmd/api/account-id-missing/out.test.toml delete mode 100644 acceptance/cmd/api/account-id-missing/output.txt delete mode 100644 acceptance/cmd/api/account-id-missing/script delete mode 100644 acceptance/cmd/api/account-id-substitution/out.requests.txt delete mode 100644 acceptance/cmd/api/account-id-substitution/out.test.toml delete mode 100644 acceptance/cmd/api/account-id-substitution/output.txt delete mode 100644 acceptance/cmd/api/account-id-substitution/script diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 319fe320b2a..e992aa7ed9b 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -4,7 +4,7 @@ ### CLI -* `databricks api` now works against unified hosts. Adds `--account` to scope a call to the account API, `--workspace-id` to override the workspace routing identifier per call, and `{account_id}` substitution from the active profile's `account_id`. A `?o=` query parameter on the path (the SPOG URL convention used by the Databricks UI) is also recognized as a per-call workspace override, so URLs pasted from the browser route correctly. +* `databricks api` now works against unified hosts. Adds `--account` to scope a call to the account API and `--workspace-id` to override the workspace routing identifier per call. A `?o=` query parameter on the path (the SPOG URL convention used by the Databricks UI) is also recognized as a per-call workspace override, so URLs pasted from the browser route correctly. ### Bundles diff --git a/acceptance/cmd/api/account-id-missing/out.requests.txt b/acceptance/cmd/api/account-id-missing/out.requests.txt deleted file mode 100644 index f42ffdcd34b..00000000000 --- a/acceptance/cmd/api/account-id-missing/out.requests.txt +++ /dev/null @@ -1,9 +0,0 @@ -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" - ] - }, - "method": "GET", - "path": "/.well-known/databricks-config" -} diff --git a/acceptance/cmd/api/account-id-missing/out.test.toml b/acceptance/cmd/api/account-id-missing/out.test.toml deleted file mode 100644 index f784a183258..00000000000 --- a/acceptance/cmd/api/account-id-missing/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Local = true -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/account-id-missing/output.txt b/acceptance/cmd/api/account-id-missing/output.txt deleted file mode 100644 index 44af079ab0b..00000000000 --- a/acceptance/cmd/api/account-id-missing/output.txt +++ /dev/null @@ -1,5 +0,0 @@ - ->>> errcode [CLI] api get /api/2.0/accounts/{account_id}/oauth2/published-app-integrations -Error: path contains {account_id} but no account_id is set on profile "" (set account_id in ~/.databrickscfg, export DATABRICKS_ACCOUNT_ID, or replace {account_id} in the path) - -Exit code: 1 diff --git a/acceptance/cmd/api/account-id-missing/script b/acceptance/cmd/api/account-id-missing/script deleted file mode 100644 index eba2e79868c..00000000000 --- a/acceptance/cmd/api/account-id-missing/script +++ /dev/null @@ -1,5 +0,0 @@ -# No DATABRICKS_ACCOUNT_ID; the testserver also doesn't set account_id in -# .well-known. The CLI must fail with the actionable error before any target -# API request is sent. (Config resolution still hits .well-known earlier in -# the lifecycle, which is expected.) -trace errcode $CLI api get '/api/2.0/accounts/{account_id}/oauth2/published-app-integrations' diff --git a/acceptance/cmd/api/account-id-substitution/out.requests.txt b/acceptance/cmd/api/account-id-substitution/out.requests.txt deleted file mode 100644 index 59fa96c3dbc..00000000000 --- a/acceptance/cmd/api/account-id-substitution/out.requests.txt +++ /dev/null @@ -1,21 +0,0 @@ -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" - ] - }, - "method": "GET", - "path": "/.well-known/databricks-config" -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ], - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" - ] - }, - "method": "GET", - "path": "/api/2.0/accounts/abc-123/oauth2/published-app-integrations" -} diff --git a/acceptance/cmd/api/account-id-substitution/out.test.toml b/acceptance/cmd/api/account-id-substitution/out.test.toml deleted file mode 100644 index f784a183258..00000000000 --- a/acceptance/cmd/api/account-id-substitution/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Local = true -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/api/account-id-substitution/output.txt b/acceptance/cmd/api/account-id-substitution/output.txt deleted file mode 100644 index 0967ef424bc..00000000000 --- a/acceptance/cmd/api/account-id-substitution/output.txt +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/acceptance/cmd/api/account-id-substitution/script b/acceptance/cmd/api/account-id-substitution/script deleted file mode 100644 index 8e509e74085..00000000000 --- a/acceptance/cmd/api/account-id-substitution/script +++ /dev/null @@ -1,2 +0,0 @@ -export DATABRICKS_ACCOUNT_ID=abc-123 -$CLI api get '/api/2.0/accounts/{account_id}/oauth2/published-app-integrations' diff --git a/cmd/api/api.go b/cmd/api/api.go index 6edd049b020..823a6a4b663 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -29,8 +29,6 @@ const ( // for the workspace routing identifier so that pasted SPOG URLs route // correctly without requiring --workspace-id. orgIDQueryParam = "o" - - accountIDPlaceholder = "{account_id}" ) // accountSegmentRe matches a non-empty segment immediately after "accounts/", @@ -90,11 +88,6 @@ func makeCommand(method string) *cobra.Command { return err } - path, err = substituteAccountID(path, cfg.AccountID, cfg.Profile) - if err != nil { - return err - } - orgID, err := resolveOrgID( forceAccount, workspaceIDFlag, @@ -141,23 +134,6 @@ func normalizeWorkspaceID(workspaceID string) string { return workspaceID } -// substituteAccountID replaces "{account_id}" placeholders in path with the -// resolved account ID. Errors with an actionable message if the placeholder -// is present but no account_id is configured for the active profile. -func substituteAccountID(path, accountID, profile string) (string, error) { - if !strings.Contains(path, accountIDPlaceholder) { - return path, nil - } - if accountID == "" { - return "", fmt.Errorf( - "path contains %s but no account_id is set on profile %q "+ - "(set account_id in ~/.databrickscfg, export DATABRICKS_ACCOUNT_ID, "+ - "or replace %s in the path)", - accountIDPlaceholder, profile, accountIDPlaceholder) - } - return strings.ReplaceAll(path, accountIDPlaceholder, accountID), nil -} - // hasAccountSegment reports whether path is an account-scope API. The match // runs on URL.Path, so query strings and fragments containing "/accounts/" // can't trigger a false positive. Returns false for paths that match a known diff --git a/cmd/api/api_test.go b/cmd/api/api_test.go index 427262e5449..69cd28fe5fe 100644 --- a/cmd/api/api_test.go +++ b/cmd/api/api_test.go @@ -224,57 +224,3 @@ func TestNormalizeWorkspaceID(t *testing.T) { }) } } - -func TestSubstituteAccountID(t *testing.T) { - cases := []struct { - name string - path string - accountID string - profile string - want string - wantErrSubstring string - }{ - { - name: "placeholder absent leaves path unchanged", - path: "/api/2.0/clusters/list", - accountID: "abc-123", - profile: "DEFAULT", - want: "/api/2.0/clusters/list", - }, - { - name: "placeholder present + account_id set", - path: "/api/2.0/accounts/{account_id}/oauth2/published-app-integrations", - accountID: "abc-123", - profile: "DEFAULT", - want: "/api/2.0/accounts/abc-123/oauth2/published-app-integrations", - }, - { - name: "multiple placeholders all replaced", - path: "/api/2.0/accounts/{account_id}/workspaces/123/foo?ref=accounts/{account_id}", - accountID: "abc-123", - profile: "DEFAULT", - want: "/api/2.0/accounts/abc-123/workspaces/123/foo?ref=accounts/abc-123", - }, - { - name: "placeholder present + account_id empty -> error", - path: "/api/2.0/accounts/{account_id}/oauth2/published-app-integrations", - accountID: "", - profile: "DEFAULT", - wantErrSubstring: `{account_id}`, - }, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - got, err := substituteAccountID(c.path, c.accountID, c.profile) - if c.wantErrSubstring != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), c.wantErrSubstring) - assert.Contains(t, err.Error(), `profile "`+c.profile+`"`) - assert.Contains(t, err.Error(), "DATABRICKS_ACCOUNT_ID") - return - } - require.NoError(t, err) - assert.Equal(t, c.want, got) - }) - } -} From e9e0b4aabf35ee2737b25b84608a158e1196e49d Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 4 May 2026 15:38:44 +0200 Subject: [PATCH 10/11] acceptance/cmd/api: assert X-Databricks-Org-Id presence/absence explicitly Each test now uses print_requests.py | contains.py to assert the recorded request either does or does not carry the routing identifier. The intent (header sent or not, with what value) is now visible in the script and in output.txt, instead of being implied by the recorded out.requests.txt artifact alone. Co-authored-by: Isaac --- acceptance/cmd/api/account-flag/output.txt | 14 +++++++++++++ acceptance/cmd/api/account-flag/script | 1 + acceptance/cmd/api/account-path/output.txt | 14 +++++++++++++ acceptance/cmd/api/account-path/script | 1 + .../cmd/api/workspace-id-flag/output.txt | 17 ++++++++++++++++ acceptance/cmd/api/workspace-id-flag/script | 1 + .../api/workspace-id-from-query/output.txt | 20 +++++++++++++++++++ .../cmd/api/workspace-id-from-query/script | 1 + .../cmd/api/workspace-id-none/output.txt | 14 +++++++++++++ acceptance/cmd/api/workspace-id-none/script | 1 + acceptance/cmd/api/workspace-path/output.txt | 17 ++++++++++++++++ acceptance/cmd/api/workspace-path/script | 1 + .../api/workspace-proxy-regression/output.txt | 17 ++++++++++++++++ .../cmd/api/workspace-proxy-regression/script | 1 + 14 files changed, 120 insertions(+) diff --git a/acceptance/cmd/api/account-flag/output.txt b/acceptance/cmd/api/account-flag/output.txt index 0967ef424bc..3ef3a9900af 100644 --- a/acceptance/cmd/api/account-flag/output.txt +++ b/acceptance/cmd/api/account-flag/output.txt @@ -1 +1,15 @@ {} + +>>> print_requests.py --get --keep //api/2.0/clusters/list +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list" +} diff --git a/acceptance/cmd/api/account-flag/script b/acceptance/cmd/api/account-flag/script index ed4dd74b0c7..4c2c9f762cf 100644 --- a/acceptance/cmd/api/account-flag/script +++ b/acceptance/cmd/api/account-flag/script @@ -1 +1,2 @@ $CLI api get /api/2.0/clusters/list --account +trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "!X-Databricks-Org-Id" diff --git a/acceptance/cmd/api/account-path/output.txt b/acceptance/cmd/api/account-path/output.txt index 0967ef424bc..f7aba1b94bb 100644 --- a/acceptance/cmd/api/account-path/output.txt +++ b/acceptance/cmd/api/account-path/output.txt @@ -1 +1,15 @@ {} + +>>> print_requests.py --get --keep //api/2.0/accounts/abc-123/network-policies +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ] + }, + "method": "GET", + "path": "/api/2.0/accounts/abc-123/network-policies" +} diff --git a/acceptance/cmd/api/account-path/script b/acceptance/cmd/api/account-path/script index 67e46484e43..55f3fbdad52 100644 --- a/acceptance/cmd/api/account-path/script +++ b/acceptance/cmd/api/account-path/script @@ -1 +1,2 @@ $CLI api get /api/2.0/accounts/abc-123/network-policies +trace print_requests.py --get --keep //api/2.0/accounts/abc-123/network-policies | contains.py "!X-Databricks-Org-Id" diff --git a/acceptance/cmd/api/workspace-id-flag/output.txt b/acceptance/cmd/api/workspace-id-flag/output.txt index 0967ef424bc..d003944d561 100644 --- a/acceptance/cmd/api/workspace-id-flag/output.txt +++ b/acceptance/cmd/api/workspace-id-flag/output.txt @@ -1 +1,18 @@ {} + +>>> print_requests.py --get --keep //api/2.0/clusters/list +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ], + "X-Databricks-Org-Id": [ + "999" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list" +} diff --git a/acceptance/cmd/api/workspace-id-flag/script b/acceptance/cmd/api/workspace-id-flag/script index 741c4048c86..9e22dc301f8 100644 --- a/acceptance/cmd/api/workspace-id-flag/script +++ b/acceptance/cmd/api/workspace-id-flag/script @@ -1 +1,2 @@ $CLI api get /api/2.0/clusters/list --workspace-id 999 +trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" "999" diff --git a/acceptance/cmd/api/workspace-id-from-query/output.txt b/acceptance/cmd/api/workspace-id-from-query/output.txt index 0967ef424bc..21bd9507df2 100644 --- a/acceptance/cmd/api/workspace-id-from-query/output.txt +++ b/acceptance/cmd/api/workspace-id-from-query/output.txt @@ -1 +1,21 @@ {} + +>>> print_requests.py --get --keep //api/2.0/clusters/list +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ], + "X-Databricks-Org-Id": [ + "999" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list", + "q": { + "o": "999" + } +} diff --git a/acceptance/cmd/api/workspace-id-from-query/script b/acceptance/cmd/api/workspace-id-from-query/script index 9357873fea3..56f4d42827d 100644 --- a/acceptance/cmd/api/workspace-id-from-query/script +++ b/acceptance/cmd/api/workspace-id-from-query/script @@ -1 +1,2 @@ $CLI api get "/api/2.0/clusters/list?o=999" +trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" "999" diff --git a/acceptance/cmd/api/workspace-id-none/output.txt b/acceptance/cmd/api/workspace-id-none/output.txt index 0967ef424bc..3ef3a9900af 100644 --- a/acceptance/cmd/api/workspace-id-none/output.txt +++ b/acceptance/cmd/api/workspace-id-none/output.txt @@ -1 +1,15 @@ {} + +>>> print_requests.py --get --keep //api/2.0/clusters/list +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list" +} diff --git a/acceptance/cmd/api/workspace-id-none/script b/acceptance/cmd/api/workspace-id-none/script index 692f06ba971..bca43a87091 100644 --- a/acceptance/cmd/api/workspace-id-none/script +++ b/acceptance/cmd/api/workspace-id-none/script @@ -10,3 +10,4 @@ workspace_id = none EOF $CLI api get /api/2.0/clusters/list --profile spog-account +trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "!X-Databricks-Org-Id" diff --git a/acceptance/cmd/api/workspace-path/output.txt b/acceptance/cmd/api/workspace-path/output.txt index 0967ef424bc..a1aee4f0dc2 100644 --- a/acceptance/cmd/api/workspace-path/output.txt +++ b/acceptance/cmd/api/workspace-path/output.txt @@ -1 +1,18 @@ {} + +>>> print_requests.py --get --keep //api/2.0/clusters/list +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ], + "X-Databricks-Org-Id": [ + "[NUMID]" + ] + }, + "method": "GET", + "path": "/api/2.0/clusters/list" +} diff --git a/acceptance/cmd/api/workspace-path/script b/acceptance/cmd/api/workspace-path/script index 5edd0d6e6fb..fd982dc4740 100644 --- a/acceptance/cmd/api/workspace-path/script +++ b/acceptance/cmd/api/workspace-path/script @@ -1 +1,2 @@ $CLI api get /api/2.0/clusters/list +trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" diff --git a/acceptance/cmd/api/workspace-proxy-regression/output.txt b/acceptance/cmd/api/workspace-proxy-regression/output.txt index 0967ef424bc..2db4e469c1b 100644 --- a/acceptance/cmd/api/workspace-proxy-regression/output.txt +++ b/acceptance/cmd/api/workspace-proxy-regression/output.txt @@ -1 +1,18 @@ {} + +>>> print_requests.py --get --keep //api/2.0/preview/accounts/access-control/rule-sets +{ + "headers": { + "Authorization": [ + "Bearer [DATABRICKS_TOKEN]" + ], + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" + ], + "X-Databricks-Org-Id": [ + "[NUMID]" + ] + }, + "method": "GET", + "path": "/api/2.0/preview/accounts/access-control/rule-sets" +} diff --git a/acceptance/cmd/api/workspace-proxy-regression/script b/acceptance/cmd/api/workspace-proxy-regression/script index 686c15b59b2..9d35ce3850d 100644 --- a/acceptance/cmd/api/workspace-proxy-regression/script +++ b/acceptance/cmd/api/workspace-proxy-regression/script @@ -2,3 +2,4 @@ # being misclassified as account-scope, so the routing identifier should be # present on the recorded request. $CLI api get /api/2.0/preview/accounts/access-control/rule-sets +trace print_requests.py --get --keep //api/2.0/preview/accounts/access-control/rule-sets | contains.py "X-Databricks-Org-Id" From 556b5520213979085a8c926e298115dff8c16398 Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 4 May 2026 16:40:55 +0200 Subject: [PATCH 11/11] acceptance/cmd/api: drop --keep so the request lives only in output.txt print_requests.py --keep was preserving out.requests.txt alongside the new inline assertion in output.txt, duplicating the recorded request. Drop --keep so the helper consumes out.requests.txt and the assertion stays in output.txt only. Co-authored-by: Isaac --- .../cmd/api/account-flag/out.requests.txt | 21 --------------- acceptance/cmd/api/account-flag/output.txt | 2 +- acceptance/cmd/api/account-flag/script | 2 +- .../cmd/api/account-path/out.requests.txt | 21 --------------- acceptance/cmd/api/account-path/output.txt | 2 +- acceptance/cmd/api/account-path/script | 2 +- .../api/workspace-id-flag/out.requests.txt | 24 ----------------- .../cmd/api/workspace-id-flag/output.txt | 2 +- acceptance/cmd/api/workspace-id-flag/script | 2 +- .../workspace-id-from-query/out.requests.txt | 27 ------------------- .../api/workspace-id-from-query/output.txt | 2 +- .../cmd/api/workspace-id-from-query/script | 2 +- .../api/workspace-id-none/out.requests.txt | 21 --------------- .../cmd/api/workspace-id-none/output.txt | 2 +- acceptance/cmd/api/workspace-id-none/script | 2 +- .../cmd/api/workspace-path/out.requests.txt | 24 ----------------- acceptance/cmd/api/workspace-path/output.txt | 2 +- acceptance/cmd/api/workspace-path/script | 2 +- .../out.requests.txt | 24 ----------------- .../api/workspace-proxy-regression/output.txt | 2 +- .../cmd/api/workspace-proxy-regression/script | 2 +- 21 files changed, 14 insertions(+), 176 deletions(-) delete mode 100644 acceptance/cmd/api/account-flag/out.requests.txt delete mode 100644 acceptance/cmd/api/account-path/out.requests.txt delete mode 100644 acceptance/cmd/api/workspace-id-flag/out.requests.txt delete mode 100644 acceptance/cmd/api/workspace-id-from-query/out.requests.txt delete mode 100644 acceptance/cmd/api/workspace-id-none/out.requests.txt delete mode 100644 acceptance/cmd/api/workspace-path/out.requests.txt delete mode 100644 acceptance/cmd/api/workspace-proxy-regression/out.requests.txt diff --git a/acceptance/cmd/api/account-flag/out.requests.txt b/acceptance/cmd/api/account-flag/out.requests.txt deleted file mode 100644 index f0aeed9b3c1..00000000000 --- a/acceptance/cmd/api/account-flag/out.requests.txt +++ /dev/null @@ -1,21 +0,0 @@ -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" - ] - }, - "method": "GET", - "path": "/.well-known/databricks-config" -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ], - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" - ] - }, - "method": "GET", - "path": "/api/2.0/clusters/list" -} diff --git a/acceptance/cmd/api/account-flag/output.txt b/acceptance/cmd/api/account-flag/output.txt index 3ef3a9900af..c165bf2af88 100644 --- a/acceptance/cmd/api/account-flag/output.txt +++ b/acceptance/cmd/api/account-flag/output.txt @@ -1,6 +1,6 @@ {} ->>> print_requests.py --get --keep //api/2.0/clusters/list +>>> print_requests.py --get //api/2.0/clusters/list { "headers": { "Authorization": [ diff --git a/acceptance/cmd/api/account-flag/script b/acceptance/cmd/api/account-flag/script index 4c2c9f762cf..91a3114c19c 100644 --- a/acceptance/cmd/api/account-flag/script +++ b/acceptance/cmd/api/account-flag/script @@ -1,2 +1,2 @@ $CLI api get /api/2.0/clusters/list --account -trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "!X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "!X-Databricks-Org-Id" diff --git a/acceptance/cmd/api/account-path/out.requests.txt b/acceptance/cmd/api/account-path/out.requests.txt deleted file mode 100644 index 3db1b27a54e..00000000000 --- a/acceptance/cmd/api/account-path/out.requests.txt +++ /dev/null @@ -1,21 +0,0 @@ -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" - ] - }, - "method": "GET", - "path": "/.well-known/databricks-config" -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ], - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" - ] - }, - "method": "GET", - "path": "/api/2.0/accounts/abc-123/network-policies" -} diff --git a/acceptance/cmd/api/account-path/output.txt b/acceptance/cmd/api/account-path/output.txt index f7aba1b94bb..1afd36c01d3 100644 --- a/acceptance/cmd/api/account-path/output.txt +++ b/acceptance/cmd/api/account-path/output.txt @@ -1,6 +1,6 @@ {} ->>> print_requests.py --get --keep //api/2.0/accounts/abc-123/network-policies +>>> print_requests.py --get //api/2.0/accounts/abc-123/network-policies { "headers": { "Authorization": [ diff --git a/acceptance/cmd/api/account-path/script b/acceptance/cmd/api/account-path/script index 55f3fbdad52..df9bd9d6269 100644 --- a/acceptance/cmd/api/account-path/script +++ b/acceptance/cmd/api/account-path/script @@ -1,2 +1,2 @@ $CLI api get /api/2.0/accounts/abc-123/network-policies -trace print_requests.py --get --keep //api/2.0/accounts/abc-123/network-policies | contains.py "!X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/accounts/abc-123/network-policies | contains.py "!X-Databricks-Org-Id" diff --git a/acceptance/cmd/api/workspace-id-flag/out.requests.txt b/acceptance/cmd/api/workspace-id-flag/out.requests.txt deleted file mode 100644 index 5f6cc8e1f2a..00000000000 --- a/acceptance/cmd/api/workspace-id-flag/out.requests.txt +++ /dev/null @@ -1,24 +0,0 @@ -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" - ] - }, - "method": "GET", - "path": "/.well-known/databricks-config" -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ], - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" - ], - "X-Databricks-Org-Id": [ - "999" - ] - }, - "method": "GET", - "path": "/api/2.0/clusters/list" -} diff --git a/acceptance/cmd/api/workspace-id-flag/output.txt b/acceptance/cmd/api/workspace-id-flag/output.txt index d003944d561..5ff264fa554 100644 --- a/acceptance/cmd/api/workspace-id-flag/output.txt +++ b/acceptance/cmd/api/workspace-id-flag/output.txt @@ -1,6 +1,6 @@ {} ->>> print_requests.py --get --keep //api/2.0/clusters/list +>>> print_requests.py --get //api/2.0/clusters/list { "headers": { "Authorization": [ diff --git a/acceptance/cmd/api/workspace-id-flag/script b/acceptance/cmd/api/workspace-id-flag/script index 9e22dc301f8..ed76b842975 100644 --- a/acceptance/cmd/api/workspace-id-flag/script +++ b/acceptance/cmd/api/workspace-id-flag/script @@ -1,2 +1,2 @@ $CLI api get /api/2.0/clusters/list --workspace-id 999 -trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" "999" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" "999" diff --git a/acceptance/cmd/api/workspace-id-from-query/out.requests.txt b/acceptance/cmd/api/workspace-id-from-query/out.requests.txt deleted file mode 100644 index f94e6fe2317..00000000000 --- a/acceptance/cmd/api/workspace-id-from-query/out.requests.txt +++ /dev/null @@ -1,27 +0,0 @@ -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" - ] - }, - "method": "GET", - "path": "/.well-known/databricks-config" -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ], - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" - ], - "X-Databricks-Org-Id": [ - "999" - ] - }, - "method": "GET", - "path": "/api/2.0/clusters/list", - "q": { - "o": "999" - } -} diff --git a/acceptance/cmd/api/workspace-id-from-query/output.txt b/acceptance/cmd/api/workspace-id-from-query/output.txt index 21bd9507df2..7a72f8de473 100644 --- a/acceptance/cmd/api/workspace-id-from-query/output.txt +++ b/acceptance/cmd/api/workspace-id-from-query/output.txt @@ -1,6 +1,6 @@ {} ->>> print_requests.py --get --keep //api/2.0/clusters/list +>>> print_requests.py --get //api/2.0/clusters/list { "headers": { "Authorization": [ diff --git a/acceptance/cmd/api/workspace-id-from-query/script b/acceptance/cmd/api/workspace-id-from-query/script index 56f4d42827d..d61e06f70c4 100644 --- a/acceptance/cmd/api/workspace-id-from-query/script +++ b/acceptance/cmd/api/workspace-id-from-query/script @@ -1,2 +1,2 @@ $CLI api get "/api/2.0/clusters/list?o=999" -trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" "999" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" "999" diff --git a/acceptance/cmd/api/workspace-id-none/out.requests.txt b/acceptance/cmd/api/workspace-id-none/out.requests.txt deleted file mode 100644 index f0aeed9b3c1..00000000000 --- a/acceptance/cmd/api/workspace-id-none/out.requests.txt +++ /dev/null @@ -1,21 +0,0 @@ -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" - ] - }, - "method": "GET", - "path": "/.well-known/databricks-config" -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ], - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" - ] - }, - "method": "GET", - "path": "/api/2.0/clusters/list" -} diff --git a/acceptance/cmd/api/workspace-id-none/output.txt b/acceptance/cmd/api/workspace-id-none/output.txt index 3ef3a9900af..c165bf2af88 100644 --- a/acceptance/cmd/api/workspace-id-none/output.txt +++ b/acceptance/cmd/api/workspace-id-none/output.txt @@ -1,6 +1,6 @@ {} ->>> print_requests.py --get --keep //api/2.0/clusters/list +>>> print_requests.py --get //api/2.0/clusters/list { "headers": { "Authorization": [ diff --git a/acceptance/cmd/api/workspace-id-none/script b/acceptance/cmd/api/workspace-id-none/script index bca43a87091..23cbf69429b 100644 --- a/acceptance/cmd/api/workspace-id-none/script +++ b/acceptance/cmd/api/workspace-id-none/script @@ -10,4 +10,4 @@ workspace_id = none EOF $CLI api get /api/2.0/clusters/list --profile spog-account -trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "!X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "!X-Databricks-Org-Id" diff --git a/acceptance/cmd/api/workspace-path/out.requests.txt b/acceptance/cmd/api/workspace-path/out.requests.txt deleted file mode 100644 index 45be4b47db9..00000000000 --- a/acceptance/cmd/api/workspace-path/out.requests.txt +++ /dev/null @@ -1,24 +0,0 @@ -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" - ] - }, - "method": "GET", - "path": "/.well-known/databricks-config" -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ], - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" - ], - "X-Databricks-Org-Id": [ - "[NUMID]" - ] - }, - "method": "GET", - "path": "/api/2.0/clusters/list" -} diff --git a/acceptance/cmd/api/workspace-path/output.txt b/acceptance/cmd/api/workspace-path/output.txt index a1aee4f0dc2..9d17f66284f 100644 --- a/acceptance/cmd/api/workspace-path/output.txt +++ b/acceptance/cmd/api/workspace-path/output.txt @@ -1,6 +1,6 @@ {} ->>> print_requests.py --get --keep //api/2.0/clusters/list +>>> print_requests.py --get //api/2.0/clusters/list { "headers": { "Authorization": [ diff --git a/acceptance/cmd/api/workspace-path/script b/acceptance/cmd/api/workspace-path/script index fd982dc4740..0f46e69f022 100644 --- a/acceptance/cmd/api/workspace-path/script +++ b/acceptance/cmd/api/workspace-path/script @@ -1,2 +1,2 @@ $CLI api get /api/2.0/clusters/list -trace print_requests.py --get --keep //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/clusters/list | contains.py "X-Databricks-Org-Id" diff --git a/acceptance/cmd/api/workspace-proxy-regression/out.requests.txt b/acceptance/cmd/api/workspace-proxy-regression/out.requests.txt deleted file mode 100644 index 9e3a2cfd165..00000000000 --- a/acceptance/cmd/api/workspace-proxy-regression/out.requests.txt +++ /dev/null @@ -1,24 +0,0 @@ -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]" - ] - }, - "method": "GET", - "path": "/.well-known/databricks-config" -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ], - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/api_get cmd-exec-id/[UUID] interactive/none auth/pat" - ], - "X-Databricks-Org-Id": [ - "[NUMID]" - ] - }, - "method": "GET", - "path": "/api/2.0/preview/accounts/access-control/rule-sets" -} diff --git a/acceptance/cmd/api/workspace-proxy-regression/output.txt b/acceptance/cmd/api/workspace-proxy-regression/output.txt index 2db4e469c1b..c98486a15e1 100644 --- a/acceptance/cmd/api/workspace-proxy-regression/output.txt +++ b/acceptance/cmd/api/workspace-proxy-regression/output.txt @@ -1,6 +1,6 @@ {} ->>> print_requests.py --get --keep //api/2.0/preview/accounts/access-control/rule-sets +>>> print_requests.py --get //api/2.0/preview/accounts/access-control/rule-sets { "headers": { "Authorization": [ diff --git a/acceptance/cmd/api/workspace-proxy-regression/script b/acceptance/cmd/api/workspace-proxy-regression/script index 9d35ce3850d..3afcf48d895 100644 --- a/acceptance/cmd/api/workspace-proxy-regression/script +++ b/acceptance/cmd/api/workspace-proxy-regression/script @@ -2,4 +2,4 @@ # being misclassified as account-scope, so the routing identifier should be # present on the recorded request. $CLI api get /api/2.0/preview/accounts/access-control/rule-sets -trace print_requests.py --get --keep //api/2.0/preview/accounts/access-control/rule-sets | contains.py "X-Databricks-Org-Id" +trace print_requests.py --get //api/2.0/preview/accounts/access-control/rule-sets | contains.py "X-Databricks-Org-Id"