Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **`fluid validate` and `fluid plan` warn about an id the build phase refuses.** The build
phase puts `contract.id` and each `builds[].id` in directory names and generated code, so they
must start with a letter or underscore, use only letters, digits, `_`, `.` and `-`, and be at
most 128 characters. The contract schema allows more (`1orders`, or a longer id), and
`fluid apply` in a build mode refuses such an id before it applies anything, but validate and
plan said nothing. For a contract with builds, both now warn and name the id: plain
`fluid validate` passes and `--strict` fails, and `fluid plan` still writes the plan, whatever
its `--mode`. A contract without builds is not checked, as in `fluid apply`.

- **IaC plugins load when a contract needs their platform, and cannot take a built-in's name
unless the operator says so.** A package that ships an OpenTofu emitter through the
`fluid_build.iac_providers` entry-point group used to be imported whenever `fluid_build.iac`
Expand Down Expand Up @@ -539,6 +548,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
trailing newline from an environment variable or a file; strip the value where it is set.
### Upgrade notes

- New validate warning, so `--strict` fails: a contract with builds whose `contract.id` or a
`builds[].id` the build phase refuses (`1orders`, or more than 128 characters). `fluid apply`
in a build mode already refuses it; rename the id to fit.
- A contract with a column typed `array`, `struct`, `map`, `object`, `record` or `row` (or
`array(...)`) on an `aws` Glue table or a `gcp` BigQuery table now fails `fluid validate`,
`fluid generate iac` and `fluid apply` with `column-type`, where it validated and wrote a
Expand Down
30 changes: 30 additions & 0 deletions fluid_build/build_runners/_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@
# can never traverse out of the directory it is joined into.
_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.\-]{0,127}\Z")

# :data:`_IDENT_RE` in words, for a message that tells an author what to change.
IDENT_RULE = (
"start with a letter or underscore, use only letters, digits, '_', '.' and '-', "
"and be at most 128 characters"
)


class IdentifierViolation(ValueError):
"""Raised when a contract identifier fails validation.
Expand Down Expand Up @@ -103,3 +109,27 @@ def validate_build_ids(contract: Mapping[str, Any]) -> None:
for build in builds:
if isinstance(build, Mapping) and build.get("id"):
validate_identifier(build["id"], kind="build.id")


def build_id_warning(contract: Mapping[str, Any]) -> Optional[str]:
"""The warning ``fluid validate`` and ``fluid plan`` give for an id the build phase refuses.

The schema's ``$defs.identifier`` admits more than :data:`_IDENT_RE` (a
leading digit, any length), and the two stay different by decision. So
validate and plan accept such an id and warn, and ``fluid apply`` in a
build mode refuses it. The check is :func:`validate_build_ids`, so the
warning names the id apply would name. Returns ``None`` when it passes.

The text does not say the schema accepted the id: validate also runs this
on a contract the schema refused, such as one whose ``id`` is ``123``.
"""
try:
validate_build_ids(contract)
except IdentifierViolation as exc:
return (
f"{exc.kind} {exc.value!r} cannot be used by the build phase, which puts it in "
f"directory names and generated code, so it must {IDENT_RULE}. fluid apply in a "
"build mode (amend-and-build, replace-and-build) refuses this id before it "
"applies anything. Rename it to fit."
)
return None
15 changes: 9 additions & 6 deletions fluid_build/cli/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@ def _gate_build_ids_for_apply(contract: Dict[str, Any]) -> None:

The build phase turns ``contract.id`` and ``builds[].id`` into directory
names, so it takes a narrower grammar than the schema: ``1orders``, or an
id of 129 characters, passes ``fluid validate`` and ``fluid plan``. The
id of 129 characters, passes ``fluid validate`` and ``fluid plan``, which
only warn (``build_runners._ids.build_id_warning``). The
build runner checks the ids itself, but on the OpenTofu engine it runs
after ``tofu apply``, so such an id was refused once the product was
provisioned, as an "Unexpected error". This is the runner's own check
Expand All @@ -323,7 +324,11 @@ def _gate_build_ids_for_apply(contract: Dict[str, Any]) -> None:
Raises:
CLIError: ``apply_build_identifier_invalid``.
"""
from fluid_build.build_runners._ids import IdentifierViolation, validate_build_ids
from fluid_build.build_runners._ids import (
IDENT_RULE,
IdentifierViolation,
validate_build_ids,
)

try:
validate_build_ids(contract)
Expand All @@ -337,10 +342,8 @@ def _gate_build_ids_for_apply(contract: Dict[str, Any]) -> None:
"hint": (
"The build phase keeps each build's run records, cursors and "
"watermarks under .fluid/runs/<contract.id>/<build.id>/, so both ids "
"must start with a letter or underscore, use only letters, digits, "
"'_', '.' and '-', and be at most 128 characters. The contract schema "
"allows more, so fluid validate and fluid plan accept it. Nothing was "
"applied."
f"must {IDENT_RULE}. The contract schema allows more, so fluid "
"validate and fluid plan only warn about it. Nothing was applied."
),
},
) from exc
Expand Down
24 changes: 23 additions & 1 deletion fluid_build/cli/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from pathlib import Path
from typing import Any, Dict, List, Optional

from fluid_build.cli.console import cprint, warning
from fluid_build.cli.console import cprint, cprint_literal, warning

# 11-stage pipeline: stage-6 → stage-7 cryptographic plan-binding. ``plan.json``
# carries ``bundleDigest`` (pins input bundle) + ``planDigest`` (catches
Expand Down Expand Up @@ -305,6 +305,7 @@ def run(args, logger: logging.Logger) -> int:
# plan generation or write. A failure raises ``CLIError`` →
# non-zero exit, no plan.json written.
_gate_contract_for_plan_or_apply(contract, logger, command="plan")
_warn_unusable_build_ids(contract)

# --- --provider gate -------------------------------------------------
# ``--provider`` reaches ``build_provider`` only on the legacy planner
Expand Down Expand Up @@ -761,6 +762,27 @@ def _gate_contract_for_plan_or_apply(
)


def _warn_unusable_build_ids(contract: Dict[str, Any]) -> None:
"""Warn when the build phase will refuse ``contract.id`` or a ``builds[].id``.

The schema admits ids the build phase refuses (``1orders``, or one of 129
characters), and ``fluid apply`` in a build mode refuses such an id before
it applies anything. Plan only warns, whatever ``--mode`` it plans for, and
writes the plan. Not part of :func:`_gate_contract_for_plan_or_apply`:
apply shares that gate and refuses the id with a typed error of its own
(``cli/apply.py::_gate_build_ids_for_apply``). Printed, not logged: a
``warn()`` event would print the same text again as a JSON line.
"""
from fluid_build.build_runners._ids import build_id_warning

message = build_id_warning(contract)
if message is not None:
# As validate prints its warnings (validate._cprint_literal): literal,
# because the id is contract data, not markup; the terminal wraps the
# line, so Rich never breaks one inside the id; and no highlighter.
cprint_literal(f"\n⚠️ {message}", highlight=False, soft_wrap=True)


def _validate_plan_actions(plan: Dict[str, Any], logger: logging.Logger) -> None:
"""Run SDK ``validate_actions()`` over the plan's action list.

Expand Down
29 changes: 29 additions & 0 deletions fluid_build/cli/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,9 @@ def _run_contract_rules(
"the contract was NOT checked for a continuous trigger on a batch build"
)

# --- An id the build phase refuses (WARNING) -----------------------
_build_id_check(validation_result, contract)

# --- Packaging checks (RFC-packaging-modes.md file 9) ---------------
# ``fluid validate`` had NO packaging awareness: a `shared` block with
# no `pool` validated clean and then failed at `fluid plan` with an
Expand Down Expand Up @@ -1306,6 +1309,32 @@ def _databricks_binding_check(validation_result, contract: Mapping[str, Any]) ->
validation_result.add_warning(msg)


def _build_id_check(validation_result, contract: Mapping[str, Any]) -> None:
"""A ``contract.id`` or ``builds[].id`` the build phase refuses, as a warning.

The schema's ``$defs.identifier`` admits ids the build phase refuses
(``1orders``, or more than 128 characters), and the two grammars stay
different by decision. ``fluid apply`` in a build mode refuses such an id
before it applies anything, with the same rule
(``build_runners._ids.validate_build_ids``). A warning, not an error: plain
validate passes and ``--strict`` fails. A crash in the check is reported,
never swallowed.
"""
try:
from fluid_build.build_runners._ids import build_id_warning

message = build_id_warning(contract)
except Exception as exc: # noqa: BLE001 — reported, never swallowed
validation_result.add_warning(
f"Build-id check could not run ({type(exc).__name__}: {exc}); "
"contract.id and builds[].id were NOT checked against the ids the build "
"phase accepts"
)
return
if message is not None:
validation_result.add_warning(message)


def _duplicate_contract_id_errors(contract: Mapping[str, Any]) -> List[str]:
"""Refuse two exposes whose output ports would be published under one
contract id.
Expand Down
Loading
Loading