Skip to content

feat: provision mapping rules from a directory at startup#6544

Merged
talboren merged 3 commits into
keephq:mainfrom
manota01:feat/4487-provision-mapping-rules
May 31, 2026
Merged

feat: provision mapping rules from a directory at startup#6544
talboren merged 3 commits into
keephq:mainfrom
manota01:feat/4487-provision-mapping-rules

Conversation

@manota01
Copy link
Copy Markdown
Contributor

@manota01 manota01 commented May 28, 2026

Closes #4487.

What

Adds KEEP_MAPPINGS_DIRECTORY support so mapping rules can be GitOps-managed (loaded from YAML files on every backend start), mirroring the existing KEEP_WORKFLOWS_DIRECTORY / KEEP_PROVIDERS_DIRECTORY / KEEP_DEDUPLICATION_RULES patterns. Closes the last config gap that required UI-only management.

Manifest format:

# example: example-prometheus-mapping.yaml
name: example-prometheus-mapping
description: optional
priority: 0
type: csv
matchers:
  - [namespace]
rows:
  - { namespace: monitoring, team: platform }

Behavior

Mirrors provision_workflows and provision_deduplication_rules:

  • For each .yaml/.yml in KEEP_MAPPINGS_DIRECTORY: upsert by name. An existing rule with the same name (whether UI-created or previously provisioned) is adopted: is_provisioned=True, provisioned_file=<path>, contents overwritten from the manifest. Fields not in MappingRuleDtoIn (disabled, override, condition) are reset to model defaults on adoption — the manifest is the source of truth. DB id is preserved across re-runs.
  • Each manifest is committed in its own transaction, so a malformed manifest only skips itself; earlier successful manifests stay applied.
  • For DB rows with is_provisioned=True whose provisioned_file no longer exists or is outside the directory: delete (deprovision).
  • UI-created mapping rules (is_provisioned=False) whose name does not appear in any manifest are untouched.
  • If KEEP_MAPPINGS_DIRECTORY is unset and any provisioned rules exist in DB → all deprovisioned.
  • If env var set + dir doesn't exist → FileNotFoundError.

Changes

File What
keep/api/models/db/mapping.py Adds is_provisioned: bool + provisioned_file: Optional[str] to MappingRule (no max_length on the path — real GitOps mount paths can exceed 255 chars, mirrors Workflow.provisioned_file)
keep/api/models/db/migrations/versions/2026-05-28-16-50_67ff7efffed4.py Alembic migration for the two new columns (uses batch_alter_table for SQLite compatibility)
keep/api/bl/mapping_rules_provisioning.py New module — provision_mapping_rules_from_env. Validates each manifest with the existing MappingRuleDtoIn DTO (no new validator).
keep/api/config.py Wires provision_mapping_rules_from_env(SINGLE_TENANT_UUID) into provision_resources() after provision_deduplication_rules_from_env
tests/test_mapping_rules_provisioning.py 14 tests using db_session fixture pattern from tests/test_workflowstore.py
tests/provision/mapping_rules_{1,2,invalid,empty,same_name,with_noise}/... Fixture YAMLs (generic example names — example-prometheus-mapping etc.)
docs/deployment/provision/mapping.mdx New docs page describing the manifest format, behavior, and adoption semantics
docs/deployment/provision/overview.mdx Adds KEEP_MAPPINGS_DIRECTORY row to the env-var table + a 4th list item linking to the new page
.gitignore Adds !tests/provision/mapping_rules* (parallel to existing !tests/provision/workflows* exception)

Test coverage

test_creates_new_rule
test_provisions_multiple_rules
test_is_idempotent
test_adopts_existing_ui_rule_with_matching_name   # asserts disabled/override/condition reset
test_updates_existing_provisioned_rule
test_deprovisions_when_manifest_file_disappears
test_deprovisions_all_when_env_unset
test_leaves_unrelated_ui_rules_untouched
test_raises_when_directory_missing
test_invalid_manifest_does_not_break_valid_one   # bad file sorts AFTER good — proves per-manifest commit semantics
test_noop_when_env_unset_and_no_provisioned_rules
test_empty_directory_deprovisions_existing
test_same_name_manifests_do_not_create_duplicate  # guards against dup creation when autoflush is off
test_non_yaml_files_in_directory_are_ignored

Design choices worth a maintainer sanity-check

These are the bits where I made a call without explicit guidance — happy to switch:

  1. Manifest format: single YAML with inline rows (vs. metadata YAML + sibling CSV). Reasoning: file-per-mapping is the simplest GitOps shape, mirrors KEEP_WORKFLOWS_DIRECTORY ergonomics, and validation reuses the existing MappingRuleDtoIn (no new validator).
  2. Name match scope: across all rules (UI + provisioned), workflow-style adoption. The dedupe pattern restricts to already-provisioned rules; the workflow pattern matches by name regardless. Workflow pattern is friendlier for users migrating from UI to GitOps (no duplicates, no manual cleanup). Easy to swap to the stricter dedupe-style if preferred.
  3. is_provisioned + provisioned_file columns (workflow-style) vs. just is_provisioned (dedupe-style). The provisioned_file enables clean "manifest file disappeared → deprovision" detection, which dedupe doesn't need because dedupe rules live nested in KEEP_PROVIDERS env (no file-tracking).
  4. No KEEP_MAPPINGS single-env-var mode (workflows have both KEEP_WORKFLOWS_DIRECTORY and KEEP_WORKFLOW). Skipped because the directory mode covers every GitOps use case; a single inline mapping in an env var seems vanishingly rare. Easy to add later if needed.
  5. Reset disabled/override/condition on adoption (manifest is source of truth) rather than carrying UI state over. A UI rule that was disabled via the UI will be re-enabled when adopted. Documented in mapping.mdx. Could carry over instead — would require new fields on MappingRuleDtoIn.

Out of scope

  • Adding a provisioned indicator to the Keep UI for mapping rules (workflows show this; mappings could too). Separate UI concern.

Context

Production users of Keep adopting GitOps for providers, workflows, and team-routing configs have hit the same gap that motivated #4487 — mapping rules were the one config type that still required manual UI upload. Filing this so the same "I forgot to upload after merge" failure mode is structurally impossible going forward.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds GitOps-style provisioning for Mapping Rules by loading YAML manifests from a directory at backend startup (via KEEP_MAPPINGS_DIRECTORY), aligning mappings with the existing provisioning flows for workflows/providers/dedup rules.

Changes:

  • Add provisioning metadata to MappingRule (is_provisioned, provisioned_file) plus an Alembic migration.
  • Introduce provision_mapping_rules_from_env() to upsert mapping rules from YAML manifests and deprovision removed manifests.
  • Wire mapping provisioning into backend startup and add a dedicated test suite + fixture manifests.

Reviewed changes

Copilot reviewed 10 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
keep/api/models/db/mapping.py Adds provisioning-tracking columns to the MappingRule model.
keep/api/models/db/migrations/versions/2026-05-28-16-50_67ff7efffed4.py Creates DB columns for mapping rule provisioning metadata.
keep/api/bl/mapping_rules_provisioning.py Implements directory-based mapping rule provisioning + deprovisioning.
keep/api/config.py Calls mapping provisioning during provision_resources() startup flow.
tests/test_mapping_rules_provisioning.py Adds coverage for create/update/adopt/deprovision behaviors and error handling.
tests/provision/mapping_rules_1/* Fixture manifests for single-rule provisioning scenarios.
tests/provision/mapping_rules_2/* Fixture manifests for multi-rule provisioning scenarios.
tests/provision/mapping_rules_invalid/* Fixture manifests for partial-failure provisioning scenarios.
tests/provision/mapping_rules_empty/.gitkeep Fixture directory for “empty dir deprovisions existing” behavior.
.gitignore Ensures mapping provisioning fixtures remain tracked under tests/provision/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +97 to +104
_provision_one(session, tenant_id, path)
except Exception as exc:
logger.error(
"Failed to provision mapping rule from %s",
path,
extra={"exception": exc},
)
session.rollback()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in an earlier fix on this branch — the loop now commits per-manifest (current L105), so a late failure only rolls back its own work, not previously-applied manifests.

Comment on lines +108 to +116
def _collect_manifest_paths(mappings_dir: str) -> list[str]:
"""Return sorted absolute paths of YAML manifests in the directory."""
paths = []
for filename in sorted(os.listdir(mappings_dir)):
if filename.endswith((".yaml", ".yml")):
paths.append(os.path.join(mappings_dir, filename))
else:
logger.info("Skipping non-YAML file %s in mappings directory", filename)
return paths
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 78e74c3_collect_manifest_paths now normalizes via os.path.abspath(), and _provision_one stores that same path on provisioned_file. Set-membership comparison stays stable across cwd / env-var-form changes between runs. Added test_provisioned_file_is_absolute_and_stable_across_dir_form_changes to cover the relative-then-absolute case.

Comment on lines +99 to +102
logger.error(
"Failed to provision mapping rule from %s",
path,
extra={"exception": exc},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 78e74c3 — switched to logger.exception(...) so the traceback is captured. logger.exception is the dominant pattern across keep/api/bl/* (enrichments, incidents, maintenance_windows, etc.); aligning with that.

Comment thread keep/api/models/db/mapping.py Outdated
prefix_to_remove: Optional[str] = Field(max_length=255)
# Provisioning fields (set when the rule is loaded from KEEP_MAPPINGS_DIRECTORY at startup)
is_provisioned: bool = Field(default=False)
provisioned_file: Optional[str] = Field(max_length=255, default=None)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in an earlier fix on this branch — current model is provisioned_file: Optional[str] = Field(default=None) with no max_length, matching the migration's AutoString() (no length). Mirrors Workflow.provisioned_file since GitOps mount paths can exceed 255 chars.

assert len(rules) == 1
assert rules[0].name == "valid-mapping"


Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in an earlier fix — the invalid fixture is named zzz-bad-missing-rows.yaml (paired with good.yaml), so under sorted(os.listdir(...)) the invalid manifest is processed AFTER the valid one. The test does exercise the late-failure ordering you described.

@manota01 manota01 force-pushed the feat/4487-provision-mapping-rules branch 2 times, most recently from aae4abf to 3fb12bc Compare May 28, 2026 23:51
@manota01 manota01 changed the title feat: provision mapping rules from a directory at startup (#4487) feat: provision mapping rules from a directory at startup May 29, 2026
Adds KEEP_MAPPINGS_DIRECTORY support, mirroring the existing
KEEP_WORKFLOWS_DIRECTORY / KEEP_PROVIDERS_DIRECTORY / KEEP_DEDUPLICATION_RULES
provisioning. Closes the last GitOps gap — mapping rules can now be
maintained as YAML files in a repo and loaded by Keep on every backend start.

Each YAML manifest in the directory describes one mapping rule:

  name: example-prometheus-mapping
  description: optional
  priority: 0
  type: csv
  matchers:
    - [namespace]
  rows:
    - { namespace: monitoring, team: platform }

Behavior on startup (mirrors provision_workflows + provision_deduplication_rules):
  - For each .yaml in KEEP_MAPPINGS_DIRECTORY: upsert by `name`. An existing
    rule with the same name (whether UI-created or previously provisioned)
    is adopted: is_provisioned=True, provisioned_file=<path>, contents
    overwritten from the manifest.
  - For DB rows with is_provisioned=True whose provisioned_file no longer
    exists or is outside the directory: delete (deprovision).
  - UI-created mapping rules (is_provisioned=False) whose name does not
    appear in any manifest are untouched.

Changes:
  - Add `is_provisioned` and `provisioned_file` columns to MappingRule
    (with alembic migration)
  - New module keep/api/bl/mapping_rules_provisioning.py implementing
    provision_mapping_rules_from_env
  - Wire into provision_resources() in keep/api/config.py after
    provision_deduplication_rules_from_env
  - Tests in tests/test_mapping_rules_provisioning.py covering: create,
    multiple-manifest, idempotency, adoption of UI rules, update of
    provisioned rules, deprovision on file disappear, deprovision on
    env unset, leave-unrelated-UI-rules-untouched, invalid dir raises,
    malformed-manifest-doesn't-break-others, no-op when nothing to do,
    empty-dir-deprovisions

Closes keephq#4487
DELETE /mapping/{id} and PUT /mapping/{id} now return 409 when the
target rule has is_provisioned=True, mirroring the dedupe pattern at
alert_deduplicator.py:555,602.

Without this, a user can mutate or delete a GitOps-managed rule via
the UI/API; the next backend restart silently re-applies (or for
delete, the rule disappears until restart). The manifest in
KEEP_MAPPINGS_DIRECTORY must remain the source of truth.

Adds two route-level tests that seed a provisioned rule directly and
assert the guards short-circuit before the underlying mutation.
Two issues caught in Copilot review:

1. _collect_manifest_paths returned os.path.join(dir, filename) where dir
   could be relative. Between two runs (cwd change, env var switched from
   ./foo to /abs/foo) the set-membership comparison against stored
   provisioned_file would string-fail and spuriously deprovision a still-
   present rule. Normalize via os.path.abspath up-front so both collected
   and stored paths use the same form.

2. Per-manifest failure was logged with logger.error(...,
   extra={"exception": exc}) which drops the traceback. Switch to
   logger.exception(...) — the dominant pattern across keep/api/bl/* —
   so failures are debuggable.

Adds test_provisioned_file_is_absolute_and_stable_across_dir_form_changes
asserting (a) stored provisioned_file is absolute and (b) switching the
env var from relative to absolute form between runs does not deprovision.
@manota01 manota01 force-pushed the feat/4487-provision-mapping-rules branch from 78e74c3 to 80de72e Compare May 29, 2026 02:06
@manota01 manota01 marked this pull request as ready for review May 29, 2026 02:07
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. Documentation Improvements or additions to documentation Feature A new feature labels May 29, 2026
Copy link
Copy Markdown
Member

@talboren talboren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label May 31, 2026
@talboren talboren merged commit 2217c15 into keephq:main May 31, 2026
4 of 5 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

💪 Fantastic work @manota01! Your very first PR to keep has been merged! 🎉🥳

You've just taken your first step into open-source, and we couldn't be happier to have you onboard. 🙌
If you're feeling adventurous, why not dive into another issue and keep contributing? The community would love to see more from you! 🚀

For any support, feel free to reach out on the community: https://slack.keephq.dev. Happy coding! 👩‍💻👨‍💻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Documentation Improvements or additions to documentation Feature A new feature lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[➕ Feature]: Applying CSV mappings via Keep CLI so that they can be maintained in git

5 participants