Skip to content

bundle: add genie_space resource for direct deploy#5282

Draft
janniklasrose wants to merge 19 commits into
mainfrom
janniklasrose/genie
Draft

bundle: add genie_space resource for direct deploy#5282
janniklasrose wants to merge 19 commits into
mainfrom
janniklasrose/genie

Conversation

@janniklasrose
Copy link
Copy Markdown
Contributor

Summary

Introduces a new genie_space bundle resource (direct-engine only) that mirrors the existing dashboard pattern. Highlights:

  • New resource type with full direct-engine CRUD plus mutators for serialized_space normalization, path translation, and direct-only enforcement.
  • bundle generate genie-space command supporting --existing-path, --existing-id, and --resource [--watch] flows.
  • Etag-based drift detection mirroring dashboards: etag is persisted in state and OverrideChangeDesc skips updates when the stored etag matches the remote.
  • Permissions wired up through the standard /permissions/genie/{id} endpoint.
  • Test server genie_spaces.go with etag bumping, file-entry cleanup on trash, and a full acceptance test suite (simple, inline, complex, file-path conflicts, parent-path recreate, permissions).

Review-cycle addressed

This branch consolidates feedback from three independent reviews. Notable items:

  • Removed ValidateGenieSpacePermissions after confirming the /permissions/genie/{id} API exists and the plumbing was already in place.
  • Rejects non-string/non-structural serialized_space inputs early with a diag.Error (previously silently accepted).
  • generateForExisting now returns on saveConfiguration failure rather than continuing into --bind.
  • Local file extension .genie.json.geniespace.json (and matching testserver workspace path suffix .geniespace).
  • Both genie and dashboard --watch loops are now ctx-aware (select { <-ctx.Done() ; <-time.After(...) }).
  • dstate.ExportState now exports etag for both dashboards and genie_spaces (was previously dashboards-only).
  • Test fixture updated to the v2 export schema (enable_format_assistance / enable_entity_matching) since the v1 format is now rejected by the backend.
  • Added acceptance tests for permissions, parent_path recreate, and a command-level test for --resource sync/watch with ctx cancellation.

Test plan

  • go test ./bundle/... ./libs/testserver ./libs/workspaceurls ./cmd/bundle/...
  • go test ./acceptance -run 'TestAccept/bundle/(validate|resources|generate)/(genie|permissions/genie)'
  • ./task lint, ./task fmt, ./task ws all clean
  • Schema and docs regenerated via ./task generate-schema and ./task generate-docs
  • Live workspace verification (etag drift detection, permissions PUT, .geniespace.json round-trip, --watch Ctrl-C)

This pull request and its description were written by Isaac.

Adds first-class support for Genie spaces as a bundle resource,
complete with CRUD via direct-mode deploy, `bundle generate genie-space`
to import an existing space, permissions handling, and acceptance tests.

The resource configuration follows the dashboards pattern: a `file_path`
field points to a local `.genie.json` file whose contents are inlined
into `serialized_space` during deployment. The parent_path defaults to
`${workspace.resource_path}` and is normalized to the `/Workspace`
prefix, matching the API's expected form.

Co-authored-by: Isaac
…ng-parent-path errors

Genie surfaces a missing parent folder inconsistently across
environments: some workspaces return a standard 404 missing-resource
error, while others return 400 INVALID_PARAMETER_VALUE with a NOT_FOUND
"Tree node ... does not exist" message embedded in the text. Treat both
forms as "create the parent directory and retry once".

Co-authored-by: Isaac
…sitor

VisitGenieSpacePaths existed but was never called by VisitPaths, so
NormalizePaths did not rewrite genie_space file_path values from
"relative to YAML location" to "relative to bundle root" before
applyGenieSpaceTranslations resolved them. The result was that
generator output like "../src/<name>.genie.json" failed on deploy
with "path ... is not contained in sync root path".

Co-authored-by: Isaac
Inline YAML serialized_space stayed as a structured value (map[string]any
with int leaves) in the config struct, while state held the JSON string
that was sent to the API. structdiff compared an `any` field with
reflect.DeepEqual, which reports map != string, so every plan after
deploy showed a false update for the genie_space.

Marshal inline serialized_space to its JSON string in
ConfigureGenieSpaceSerializedSpace, mirroring the file_path code path,
so config-side and state-side carry the same type. The
genie_space_complex validate test is updated to reflect that
serialized_space is now a string regardless of input form, and a new
acceptance test under resources/genie_spaces/inline asserts that a
deploy + plan cycle is drift-free for inline serialized_space.

Co-authored-by: Isaac
Databricks workspaces do not expose a permissions endpoint for Genie
Spaces (PUT /permissions/genie/spaces/<id> returns 404
ENDPOINT_NOT_FOUND). Without an upfront check the deploy creates the
space first and then errors when applying permissions, leaving partial
state behind.

Add ValidateGenieSpacePermissions to the PreDeployChecks pipeline so
both per-resource permissions and bundle-level permissions propagated
by ApplyBundlePermissions surface a clear validation error before any
API call is made.

Co-authored-by: Isaac
Two minor follow-ups to the genie_spaces work:

- ConfigureGenieSpaceSerializedSpace silently let file_path win when a
  user also set serialized_space inline. Emit a warning that points at
  the inline block so the user knows their YAML is being dropped on
  the floor.

- ValidateDirectOnlyResources only mentioned the
  DATABRICKS_BUNDLE_ENGINE env var as a way to opt into direct mode,
  even though 'bundle.engine: direct' in databricks.yml is the more
  common entry point. Mention both.

Co-authored-by: Isaac
When the Genie API returns parent_path on GetSpace, propagate it
through bundle generate genie-space so the produced YAML deploys back
to the same workspace folder. The testserver is updated to mirror
that response shape so the acceptance fixture exercises the new path.

Filter ParentPath out of ForceSendFields in DoRead and
responseToGenieSpaceConfig: we deliberately clear ParentPath in the
returned GenieSpaceConfig because the GET API does not reliably
include it, but the SDK still surfaces it in ForceSendFields when the
field appeared on the wire. Without this filter, deploy state
serialization force-emits parent_path: "" even though the field is
logically unset, producing spurious output diffs.

Co-authored-by: Isaac
- Replace switch-with-fallthrough on dyn.Kind with a guard clause to
  satisfy the exhaustive linter without listing every Kind variant.
- Use http.StatusBadRequest in isMissingGenieParentPathError instead
  of a magic 400 (auto-fix from golangci-lint).

Co-authored-by: Isaac
…cally

serialized_space is in ignore_remote_changes because we cannot diff a
structured local YAML body against a remote JSON string. That makes UI
edits invisible at plan time, but the unconditional UpdateSpace request
was still sending the local body, so any later update to title or
description would silently overwrite UI changes.

Use the plan entry to detect whether the user actually changed
serialized_space locally; only include it in the update request when the
change is an Update action (not a Skip from ignore_remote_changes).

Co-authored-by: Isaac
The previous implementation polled w.Workspace.GetStatusByPath using
resource.FilePath, which is the local relative path (e.g.
"src/foo.genie.json"). Both lookups (with and without the "/Workspace"
prefix) were invalid for the workspace API, so currentModified stayed at
0 and the file never updated past the first iteration.

Genie has no remote modification timestamp on the response, so use
content comparison instead: canonicalize the just-fetched
serialized_space and compare against the on-disk body, re-saving only
when they differ. The first iteration still always saves, preserving
the prior unconditional initial sync.

Co-authored-by: Isaac
The parent generate command exposes --key as a persistent flag, but the
genie-space subcommand was always deriving the key from the remote
title. Read the flag value and fall back to the title-derived key only
when not provided.

Co-authored-by: Isaac
Calling json.Unmarshal on an empty serialized_space surfaces a confusing
"unexpected end of JSON input" error and writes nothing useful. Bail
out early with a clear message that names the target file.

Co-authored-by: Isaac
DoRead duplicated the field copy and the ParentPath-drop comment that
already lives in responseToGenieSpaceConfig. Reuse the helper directly
so the two stay in sync.

Co-authored-by: Isaac
The user-facing fields (title, description, warehouse_id, parent_path,
file_path, serialized_space) had PLACEHOLDER descriptions, leaving the
generated reference and resources docs blank. Fill them in with short
descriptions and regenerate the schema and docs output.

Co-authored-by: Isaac
Lowercase the genie_space error message to satisfy ST1005 and let the
linter convert an empty []string{} to a nil slice.

Co-authored-by: Isaac
The simple acceptance test fixture was a v1 serialized_space sample that
the Genie backend now rejects with 409 ABORTED ("The export format has
changed since this export was taken"). Bumps version to 2 and replaces
get_example_values / build_value_dictionary with the v2-equivalent
enable_format_assistance / enable_entity_matching, matching the format
that bundle generate genie-space now produces.

Co-authored-by: Isaac
The state DB API gained context, withRecovery and withWrite arguments
on origin/main; mirror the dashboard generate command and use the same
arguments. Also regenerates the simple acceptance plan output to pick
up the WAL-implementation serial increment.

Co-authored-by: Isaac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant