From ae5d34775f0b4eac1f4c96c7b62fbc54b48efef4 Mon Sep 17 00:00:00 2001 From: Speculator55005 <50082482+fas89@users.noreply.github.com> Date: Thu, 12 Nov 2026 23:58:24 +0100 Subject: [PATCH] docs(databricks): correct three claims about data quality monitoring Found while documenting #864 for forge_docs. - Alerts: provider 1.140.0 does have notification_settings, but only under data_profiling_config (on_failure); anomaly_detection_config has only excluded_table_full_names. HONESTLY_TESTED said the provider had none. - A refused monitor's hints do not come first: CLIError's three generic suggestions come before them, and they go ahead of the engine's own tofu state list hint. CHANGELOG, iac/base.py and the _apply_failure docstring now say that. - The Free Edition quota covering the scans was assumed, not measured: the CHANGELOG now says to expect them to count against it. --- CHANGELOG.md | 15 ++++++++------- HONESTLY_TESTED.md | 4 ++-- fluid_build/cli/_apply_opentofu_engine.py | 4 ++-- fluid_build/iac/base.py | 6 +++--- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08743b2a..74914c21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,13 +63,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 bounds it checks; `fluid apply` says so, and `fluid validate` warns about a `dq.monitoring.window` other than one day. A monitor turned on outside forge-cli is adopted, not created over. If Databricks refuses the monitor (no serverless compute, or no MANAGE on the schema), the apply - fails after the tables are created and names the opt-out first. Removing one (opting out, dropping - the SLOs, removing the expose) deletes its hidden scan job and what it has learned, never table - data, so it is not gated, and is printed. **Cost:** scans run on serverless compute billed as - `DATA_QUALITY_MONITORING`, once when monitoring is turned on and then as often as each table is - updated. Databricks Free Edition runs it (proven 2026-11-12), and its daily compute quota covers - these scans too: a workspace over the quota stops all compute for the rest of the day, so on Free - Edition opt out the products you do not need watched. + fails after the tables are created, and its suggestions say what the monitor needs and name the + opt-out. Removing one (opting out, dropping the SLOs, removing the expose) deletes its hidden scan + job and what it has learned, never table data, so it is not gated, and is printed. **Cost:** scans + run on serverless compute billed as `DATA_QUALITY_MONITORING`, once when monitoring is turned on + and then as often as each table is updated. Databricks Free Edition runs it (proven 2026-11-12). + Expect the scans to count against its daily compute quota (their cost was not measured): a + workspace over the quota stops all compute for the rest of the day, so on Free Edition opt out the + products you do not need watched. ### Changed diff --git a/HONESTLY_TESTED.md b/HONESTLY_TESTED.md index 51ff5cba..0541cb15 100644 --- a/HONESTLY_TESTED.md +++ b/HONESTLY_TESTED.md @@ -91,11 +91,11 @@ Markers declared in `pyproject.toml`. | Stage 1 real tofu, stand-in workspace: dbt jobs | `tests/iac/test_iac_databricks_jobs_fake_e2e.py` | ✅ 6. A local dbt build applies as one serverless job plus the workspace files dbt reads, each in a folder created before it (no upload meets a missing parent) (never `profiles.yml`, `target/` or a hidden file), and a second plan is empty. A changed model updates its file in place. A GIT build runs its tag on a continuous trigger and uploads nothing. Turning a job build local removes the job, its files and their folders without `--allow-data-loss`, and says so; a folder holding a file the apply did not put there is not deleted. The table a dbt build writes keeps what dbt's CREATE OR REPLACE made of it, and the apply reports the column drift instead of failing on a type change. | Whether Databricks accepts and runs the job: serverless environment version 2, the `dbt-databricks>=1.8.0,<2.0.0` range, the commands, a WORKSPACE `project_directory` under `/Workspace`, `run_as` permissions, a Git credential for a private repository, `dbt deps` with a local package (not uploaded). The manual live row proves the first four. Whether the jobs API echoes a server default the stand-in does not (the fake's builder found `performance_target` and an all-false `notification_settings` would each plan an update), which would make a second plan non-empty (live, it was empty). What dbt's CREATE OR REPLACE does to a table with column mapping (the module sets none on a dbt-written table). Not bound by a plan: an apply uploads the dbt project as it is when it runs (`planDigest` and `bundleDigest` cover the contract, not the project's files, as for a local dbt run), and a bundle does not carry the project. Known gap, by design: a job is not adopted. Its import id is a job id, which neither the contract nor `tofu` can look up by name, so an apply that lost its state creates a second job. `fluid verify` flags the duplicate (a stubbed client proves it). Workspace files need no import: the provider's create overwrites. | | Stage 1 unit: Databricks retention (`lifecycle.expire`) | `tests/iac/test_iac_databricks_retention.py`, `tests/cli/test_verify_databricks_retention.py` | ✅ 195 + 18. ISO 8601 durations (years and months at their shortest, P1Y = 365 and P1M = 28 days; a fraction, a sign, a lower-case letter, `PT`, `P1DT`, out-of-order and over-long numbers refused), Delta `interval` properties (rounded up, months refused) and the history as the larger of the two properties with Delta's defaults. A dbt model's `materialized` resolved from `config()` in the SQL, the model's YAML `config:` and `dbt_project.yml` (the `+` prefix or none, root, project, folder and model-name levels, the most specific wins, SQL over YAML over project), the default `view`, the model found by alias or file name, commented-out and `raw` configs ignored, a `config()` inside a Jinja block, `var()`, `**`, a custom schema or database, a Python model, a versioned or twin model, a linked file and a Git repository refused as undeterminable. The build's `--select` must include the resolved model (name, `fqn`, `path:`, `*`, unions and intersections; a graph operator on another node, a tag, a config or run state is refused as not evaluable). Accepted: a dbt `table` model and a DuckDB acquisition with P30D, P365D, P6Y; refused: P7D, P29D, P4W, P1M, a history set longer than the retention, `incremental`, `snapshot`, `view`, another acquisition engine, no writer, a mixed set of writers. `fluid validate` and the emitter agree on kind and message. `fluid verify`'s retention dimension against a stubbed client and, over HTTP, the stand-in workspace (pass, fail with the `ALTER TABLE`, the runtime property over the declared one, skip, predictive optimization off as a warning). | Live behaviour: no real Databricks endpoint was called. Whether Unity Catalog's GetTable serves a table's `delta.logRetentionDuration` and `delta.deletedFileRetentionDuration` in `properties` or `delta_runtime_properties_kvpairs` (read from databricks-sdk-go v0.190.0 `model.go`, not observed), whether `effective_predictive_optimization_flag` is filled for a managed table, that predictive optimization runs `VACUUM` within the retention (a Databricks guarantee, not tested), and that dbt-databricks `table` replaces the table each run. Delta history is only half the promise: the acquisition acceptance rests on the landing being an `INSERT OVERWRITE` of the whole table in the one mode it runs (`full_refresh`, read from the landing's own `LOAD_MODES`; another source mode is refused as `incremental`, none as `unmapped`), which the bronze landing row covers against the stand-in, not live. A GetTable answer with no properties is a verify warning (Delta's defaults assumed). Embedded-logic DuckDB builds, `dlt`, `airbyte` and the other acquisition engines are refused. Selection is matched on names and paths only, not on dbt's graph: `+other+` is refused even if `other` is upstream of the model. | | Stage 1: Databricks Unity Catalog metadata (comments, product tags, column tags; live: the 2026-11-04 row) | `tests/iac/test_iac_databricks_metadata.py` (the emitter), `tests/iac/test_iac_databricks_metadata_fake_e2e.py` (real `fluid` + `tofu` + databricks/databricks 1.140.0 against `tests/iac/_fake_databricks.py`), `tests/cli/test_verify_databricks.py` (the comment and column-tag cases) | ✅ 43 + 5 + 7. The table's `comment` is the expose's description, else the product's (its `purpose` when structured), each described column's `comment` its schema description, stripped; none on a table a dbt build writes (dbt's CREATE OR REPLACE owns them; `fluid validate` warns). The product's identity and owner team are tags on the table only (`fluid_product_id`, `fluid_product_name`, `fluid_domain`, `fluid_layer`, `fluid_product_type`, `fluid_product_version`, `fluid_version`, `fluid_owner_team`), an unstated field makes none, and they count with the governance labels toward 50; `lifecycle.state: deprecated` (the expose's, else the product's) sets `system.certification_status = deprecated`, else the label `certification: certified` (the expose's, else the product's) sets `certified` and is no plain tag (a product-level label certifies every expose's table, as intended; an expose-level one only its own); another label value is `databricks-certification-invalid`; a plan notice says the identity needs ASSIGN on that governed tag. Against the stand-in: certified, then deprecated in place, then removed. A classified column (`sensitivity`) carries `fluid_sensitivity` as a `columns` tag named `catalog.schema.table.column`, imported best-effort by the provider's id. Refused through `validate` and `emit` alike: a backslash in any comment (`databricks-comment-unsafe`; the provider's `parseComment` escapes only quotes) and a `${` (Spark variable substitution), a label meeting a product key (`databricks-tag-collision`), a value over 256 characters (`databricks-tag-value-too-long`), over 1,000 column tags (`databricks-tags-too-many`); a `${...}` in a tag value is rendered as a literal. Against the stand-in: apply writes the comments (an apostrophe included, through the provider's escaping), the product tags and the column tag, never an `OWNER TO`; a second plan is empty; a changed description is an in-place `COMMENT ON TABLE`, never a replace; removing a classification destroys only its column tag; a table made before this is adopted and gains its comments in place (`+5 ~1` for the product tags and the table update). `fluid verify` passes after the apply and fails a comment or column tag changed by hand; against a stubbed client it reads each column's tag list to its last page, ignores a tag or comment someone else added, and checks no comment on a dbt-written table. | **Not run live.** What real Unity Catalog stores and returns for a table or column comment (whitespace, newlines, length limits, which the docs do not state), whether it accepts a tag value with `, . : / = -` (the docs restrict only keys), whether `entity-tag-assignments/columns/{catalog.schema.table.column}` resolves as the SDK documents, and whether a dbt CREATE OR REPLACE keeps column tags (undocumented; `fluid verify` is the gate), and whether the Entity Tag Assignments API sets the system tag `system.certification_status` (its docs show the UI and `SET TAG`; the SDK documents governed tags through the API, needing ASSIGN; the stand-in checks no permission). A contract that adds a column while comments change for the first time hits the provider's mixed-change refusal (its fix-it says to apply in two steps). | -| Stage 1: Databricks data quality monitoring (live: the 2026-11-12 row) | `tests/iac/test_iac_databricks_quality.py` (what asks for it, the monitor per schema, the derived import, the notice, the engine), `tests/iac/test_iac_databricks_quality_fake_e2e.py` (real `fluid` + `tofu` + databricks/databricks 1.140.0 against `tests/iac/_fake_databricks.py`), `tests/iac/test_iac_tofu_validate.py::test_a_databricks_data_quality_monitor_passes_tofu_validate` | ✅ 37 + 6 + 1. A freshness or completeness SLO (`qos.freshnessSLO`, `qos.completenessTarget`, an `observability.defaultSLIs` freshness or completeness block, a `contract.dq.rules` entry of type `freshness`, `completeness` or `anomaly_detection`) puts one `databricks_data_quality_monitor` on the owned schema, watching it by `schema_id`, created after the schema's tables, the product's other tables there in `excluded_table_full_names`; `contract.dq.monitoring.enabled: false` removes only the monitor; a `dq.monitoring.window` other than one day is a `fluid validate` warning. Against the stand-in: a second plan is empty; a monitor turned on outside forge-cli is adopted by `schema,` taken from state after the schema's import (no create); the last exclusion leaving is a PATCH whose `update_mask` names the whole config (the stand-in honours the mask), and the plan converges; a monitor Databricks refuses fails the apply after the tables, with the opt-out first among the suggestions (`apply_failure_hints`); opting out deletes it, not gated; `tofu destroy` deletes it before the schema. | What Databricks scans and reports (the live row); the stand-in serves schema monitors only. | +| Stage 1: Databricks data quality monitoring (live: the 2026-11-12 row) | `tests/iac/test_iac_databricks_quality.py` (what asks for it, the monitor per schema, the derived import, the notice, the engine), `tests/iac/test_iac_databricks_quality_fake_e2e.py` (real `fluid` + `tofu` + databricks/databricks 1.140.0 against `tests/iac/_fake_databricks.py`), `tests/iac/test_iac_tofu_validate.py::test_a_databricks_data_quality_monitor_passes_tofu_validate` | ✅ 37 + 6 + 1. A freshness or completeness SLO (`qos.freshnessSLO`, `qos.completenessTarget`, an `observability.defaultSLIs` freshness or completeness block, a `contract.dq.rules` entry of type `freshness`, `completeness` or `anomaly_detection`) puts one `databricks_data_quality_monitor` on the owned schema, watching it by `schema_id`, created after the schema's tables, the product's other tables there in `excluded_table_full_names`; `contract.dq.monitoring.enabled: false` removes only the monitor; a `dq.monitoring.window` other than one day is a `fluid validate` warning. Against the stand-in: a second plan is empty; a monitor turned on outside forge-cli is adopted by `schema,` taken from state after the schema's import (no create); the last exclusion leaving is a PATCH whose `update_mask` names the whole config (the stand-in honours the mask), and the plan converges; a monitor Databricks refuses fails the apply after the tables, its suggestions saying what it needs and naming the opt-out (`apply_failure_hints`, after the three generic ones); opting out deletes it, not gated; `tofu destroy` deletes it before the schema. | What Databricks scans and reports (the live row); the stand-in serves schema monitors only. | | Manual live, Free Edition (founder-approved) | the real `fluid` CLI, no test file: run 2026-11-01 from main dfd6f145, databricks-sdk 0.158.0, every `fluid_iactest_*` object swept after (1 schema, 1 table, 1 folder; 0 left) | ✅ validate, `generate iac --validate`, plan, dry-run and apply `+9`; a second plan is EMPTY against the real Jobs API; Databricks accepts the Quartz schedule, serverless environment 2 with `dbt-databricks>=1.8.0,<2.0.0` (dbt 1.12.6, adapter 1.13.0), the WORKSPACE `project_directory` and the commands as emitted; one `run-now` ends SUCCESS and dbt builds the table; `fluid verify` with the real SDK (columns, types, one job); `fluid diff`'s state check (its live check has no Databricks inspector); a model change `~1`; the build made local removes job, files and folders `-5` without `--allow-data-loss` (the empty `.fluid//` folders above `files/` stay: only `files/` and below are managed). It found the USE CATALOG check reading a group-conveyed privilege as none. After the fix, `fluid verify` on a fresh one-table product (`+4`) passes every dimension, catalog access included (1 schema and 1 table swept, 0 left). | A GIT source, profile or OAuth auth, a run-as service principal, table adoption and a larger project, live; the region residency check (the metastore precondition and `fluid verify`'s residency dimension), which was added after this run and not run live; dbt's CREATE OR REPLACE drops a contract's NOT NULL, so verify flags a `required` column a dbt model does not constrain itself. | | Manual live, Free Edition (founder-approved), masks and row filters | the real `fluid` CLI, no test file: run 2026-11-03 at the tip of #785, a real workspace with an AWS us-west-2 metastore; every object swept after (0 left) | ✅ 10 steps passed. Functions are created and read back (owner is the user's email; `routine_definition` is the body without `RETURN`, backticks kept; `full_data_type` is STRING or BOOLEAN; a zero-parameter function's FunctionInfo has no `input_params`). The row filter and masks are bound and enforced on the restricted user's own SELECT: only the consented rows, `msisdn` as `XXXXX4567`, a 3-character value hashed, the denied column NULL. `SET ROW FILTER ... ON ()` is accepted and a `SET` replaces a live filter. Tags are present, `fluid verify` passes every dimension, a second apply is `+0 ~0 -0`, the residency precondition fails the plan and names both regions, `generate iac` refuses, and the sweep left 0. | Still open, as this run showed: superseded functions were left behind (orphans; the follow-up that drops them after a successful apply is not run live); `tofu destroy` of the module fails at a schema that still holds functions, so a teardown must drop them first; and the residency failure text was forge-cli's 800-character tail of the tofu JSON, which cut off the sentence naming the live region (the one-line explanation is not run live). A metastore in another cloud or region, a service principal as the run identity, and a group-conveyed grant on the live workspace are still not run. | | Manual live, Free Edition (founder-approved), 2026-11-04 | the real `fluid` CLI, no test file: #801 + #831 at a7169202, run by the demo session, every object swept back to the baseline after | ✅ Unity Catalog metadata read back live: the table and column comments, the product tags (`fluid_product_id` and the rest), the owner-team tag, the `fluid_sensitivity` column tags and `system.certification_status = certified` (accepted for an admin personal access token). The bronze landing: 5 rows loaded, the email column hashed, the staged files deleted from the volume; a second run overwrote the table (the Delta history shows `numRemovedFiles=1`). The column masks, the row filter and a scoped grant were enforced after the `INSERT OVERWRITE`. `lifecycle.retention: P30D` with `expire: true` was accepted, `fluid verify` passed, and predictive optimization was on. A second apply planned `+0 ~0 -0`. A changed row filter bound the new `fluid_rf_` function and dropped the superseded one. Findings fixed in the follow-up PR: the landing left the empty `///` directories and the local masked copy under `.fluid/staging//`, and a dry run did not list the superseded function drops. | Not tested live: `system.certification_status` set by a non-admin identity holding only ASSIGN on the governed tag (only an admin token was used). Open: `facets.databricks_load.rows` counts the rows loaded, which a row-filtered `SELECT COUNT(*)` by a restricted reader does not match (by design of the filter, not reconciled in the run record). | -| Manual live, Free Edition (founder-approved), data quality monitoring, 2026-11-12 | the real `fluid` CLI, no test file: this branch, provider 1.140.0, scratch catalog `fluid_iactest_dqm` (dropped CASCADE after, 0 left) | ✅ Free Edition runs the feature: `POST /api/data-quality/v1/monitors` creates a hidden anomaly-detection workflow whose first run ended SUCCESS after about 8 minutes. `fluid apply` of a two-table product with the pack's smart-meter SLOs: `+14`, a second plan empty; `system.data_quality_monitoring.table_results` then held `readings` as `Training` (freshness and completeness `Unknown`, `RECENTLY_CREATED`) and nothing for the excluded `notes`; Catalog Explorer showed Quality `Training` on `readings` and `Excluded` (Reason: User Configured) on `notes`. A schema whose monitor was made over REST: `adopted 2 pre-existing resource(s)`, then an empty plan. `dq.monitoring.enabled: false`: `-1`, printed as not gated, and the monitor GET is 404. The last excluded table starting to ask: `~1` (provider 1.140.0 PATCHes with `update_mask=anomaly_detection_config,data_profiling_config` and an empty config), the live monitor's exclusion list is gone, and the next plan is empty. A second create on one schema is `400 RESOURCE_ALREADY_EXISTS`; a missing monitor `404 RESOURCE_DOES_NOT_EXIST`. | A scan beyond `Training` (the model needs days of history); the serverless cost (no `DATA_QUALITY_MONITORING` row in `system.billing.usage` within the session: its rows lagged the scans by hours); a workspace or identity Databricks refuses (the stand-in row); alerts (`notification_settings` is not in provider 1.140.0). | +| Manual live, Free Edition (founder-approved), data quality monitoring, 2026-11-12 | the real `fluid` CLI, no test file: this branch, provider 1.140.0, scratch catalog `fluid_iactest_dqm` (dropped CASCADE after, 0 left) | ✅ Free Edition runs the feature: `POST /api/data-quality/v1/monitors` creates a hidden anomaly-detection workflow whose first run ended SUCCESS after about 8 minutes. `fluid apply` of a two-table product with the pack's smart-meter SLOs: `+14`, a second plan empty; `system.data_quality_monitoring.table_results` then held `readings` as `Training` (freshness and completeness `Unknown`, `RECENTLY_CREATED`) and nothing for the excluded `notes`; Catalog Explorer showed Quality `Training` on `readings` and `Excluded` (Reason: User Configured) on `notes`. A schema whose monitor was made over REST: `adopted 2 pre-existing resource(s)`, then an empty plan. `dq.monitoring.enabled: false`: `-1`, printed as not gated, and the monitor GET is 404. The last excluded table starting to ask: `~1` (provider 1.140.0 PATCHes with `update_mask=anomaly_detection_config,data_profiling_config` and an empty config), the live monitor's exclusion list is gone, and the next plan is empty. A second create on one schema is `400 RESOURCE_ALREADY_EXISTS`; a missing monitor `404 RESOURCE_DOES_NOT_EXIST`. | A scan beyond `Training` (the model needs days of history); the serverless cost (no `DATA_QUALITY_MONITORING` row in `system.billing.usage` within the session: its rows lagged the scans by hours); a workspace or identity Databricks refuses (the stand-in row); alerts (provider 1.140.0's `anomaly_detection_config` has no `notification_settings`; its only one is `data_profiling_config`'s `on_failure`). | | Stage 1 unit: Databricks Genie space (`extensions.databricksGenie`) | `tests/iac/test_iac_databricks_genie.py` (the compile, the plugin's `converge_output_ports` against the stand-in's Genie and Permissions APIs, the engine hook) | ✅ 90. What the space holds (sample questions split on `\|`, 32-hex ids sorted, column descriptions with allowed values, range, example and synonyms, one instruction with the metrics' SQL, `agentPolicy` and the access statement), prompt matching never on, a sensitive column's example left out, the refusals (`fluid validate` and before `tofu`), create + share then no change, an update with the `etag`, a 409 not overwritten, a hand edit set back, removal trashes, a stranger's space untouched, a space planted with the marker in another folder never adopted, shared or trashed, a marker quoted inside a description no match, two markers refused, an apply straight after a create finding the space (the list lags), a title another space has kept as the service suffixed it and a rename onto one said as such, a transient failure to read a space's folder failing the apply instead of making a second space, a new space outside the folder the next apply looks in trashed and said, a policy that allows `analysis` but not `qa` accepted and one that denies `analysis` refused, its lines printed literally (Rich read the marker and a `[/x]` title as markup), a principal granted `manage` (`ALL_PRIVILEGES`) counted as a reader, metric views counted toward the 50-entry cap, remediation by HTTP status, a missing SDK stopping the apply before `tofu`, the native plan refusing what `emit` refuses, one snippet per metric name, a principal the service keeps in another case not shared again, a dry run writes nothing, a metric view used once it exists and sent where the service keeps it, readers' `CAN_RUN` with editors kept, the real SDK's error classes read as 403/404/409, no credential in a message, the stand-in refusing what the service refuses, and measure SQL identical to the governed MCP query path's | Genie's answers (only the live row below asks one) | | Stage 1 real tofu, stand-in workspace: Genie space | `tests/iac/test_iac_databricks_genie_fake_e2e.py` | ✅ 3. The real `fluid` CLI and `tofu`: validate, dry-run, apply (the space after the tables, shared with the readers), a second apply that changes nothing, the block removed (trashed); a Genie failure fails the apply after the tables with its fix and the next apply converges; an invalid block refused before `tofu` | The stand-in is modelled on the measured API, not the service itself | | Manual live, Free Edition, Genie space, 2026-11-12 | the real `fluid` CLI, no test file: branch `feat/databricks-genie-space`, databricks-sdk 0.162.0, a copy of industry-models tower-management `dp019_site_360` in the scratch catalog `fluid_iactest_genie` (no dbt build, no MENA sovereignty block: the workspace is in us-west-2), 6 synthetic rows | ✅ dry-run (would create, would share), apply `+73` then the space created and shared `CAN_RUN` with two persona service principals, a second apply `+0 ~0 -0` and "no change", the question "Which on-air sites in Amman are colocation-ready with hybrid power and no standing risk?" answered through the Genie Conversation API with the 2 expected sites (its SQL took the latest `snapshot_date` and the inverse of the `at_risk_site_count` filter from the instruction), a metric view created by hand picked up ("updated: metric views", then "no change"), the block removed (trashed). Two defects found and fixed in the run: the metric-view read failed on a schema not created yet, and the service keeps a metric view among the tables. A re-check after the security review (the plugin's own hook over `samples.nyctaxi.trips`, nothing created in Unity Catalog) found the list shows a new space only ~9 s later (a run straight after made a second space) and that a taken title is suffixed on create and 409 on PATCH; fixed, then create, an immediate second run ("no change", same space), and the block removed (trashed, via the folder check) passed live. Swept: the space trashed, `DROP CATALOG fluid_iactest_genie CASCADE`, 0 left | A reader without `SELECT` asking (no persona credential here; the access model is Genie's documented one); a service principal as the deploying identity (its folder is `/Users/`, the one Databricks Asset Bundles build from `current_user.userName`; a create that lands anywhere else is trashed and stops the apply); a space over several products | diff --git a/fluid_build/cli/_apply_opentofu_engine.py b/fluid_build/cli/_apply_opentofu_engine.py index 12c5b91e..d0c7310f 100644 --- a/fluid_build/cli/_apply_opentofu_engine.py +++ b/fluid_build/cli/_apply_opentofu_engine.py @@ -1693,8 +1693,8 @@ def _apply_failure(result: Any, workdir: str, plugin: Any = None) -> CLIError: none (a crash, a timeout) falls back to the first ``Error:``, else the exit code. Tofu records state as it goes, so what was created before the failure is normally tracked and a re-run converges: the suggestions say so. A plugin's - optional ``apply_failure_hints(address, detail)`` puts its own way on first, for the - resource that failed. + optional ``apply_failure_hints(address, detail)`` adds its way on for the resource that + failed, ahead of the engine's own ``tofu state list`` hint. """ diagnostics = _diagnostics(getattr(result, "events", None)) context: Dict[str, Any] = {} diff --git a/fluid_build/iac/base.py b/fluid_build/iac/base.py index 70a9744a..c83ddb11 100644 --- a/fluid_build/iac/base.py +++ b/fluid_build/iac/base.py @@ -66,9 +66,9 @@ class IacProviderPlugin(Protocol): * ``explain_plan_failure(output, contract, state)``: an :class:`UnsupportedBindingError` naming the fix for a failed ``tofu`` run, or ``None``. - * ``apply_failure_hints(address, detail)``: suggestions, first in line, for - a ``tofu apply`` that failed on the resource at ``address``; ``[]`` when - the plugin has none. + * ``apply_failure_hints(address, detail)``: suggestions for a ``tofu apply`` + that failed on the resource at ``address``, added ahead of the engine's own + ``tofu state list`` hint; ``[]`` when the plugin has none. * ``recorded_regions(state)``: the regions the state's resources live in, so an apply pinned to another region is refused, not re-created there. * ``suppressed_drift(contract, prior_state)``: drift the module hides with