composable-data-stack 0.13.0__tar.gz → 0.15.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {composable_data_stack-0.13.0/composable_data_stack.egg-info → composable_data_stack-0.15.0}/PKG-INFO +36 -3
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/README.md +33 -1
- composable_data_stack-0.15.0/cli/confinement.py +219 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/getter.py +624 -13
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/image_updates.py +12 -6
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/k8s_renderer.py +701 -6
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/k8s_runner.py +1 -1
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/k8s_security.py +96 -3
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/main.py +144 -30
- composable_data_stack-0.15.0/cli/maturity.py +271 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/planner.py +289 -15
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/preflight.py +10 -7
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/renderer.py +708 -21
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/resolver.py +19 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/resources/module.schema.json +152 -3
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/resources/profile.schema.json +161 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/resources/rule-schema.json +5 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/resources/rule-set.json +60 -7
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/secrets.py +4 -1
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/security.py +285 -20
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/security_common.py +22 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/up_runner.py +38 -4
- composable_data_stack-0.15.0/cli/utils.py +85 -0
- composable_data_stack-0.15.0/cli/validator.py +2054 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0/composable_data_stack.egg-info}/PKG-INFO +36 -3
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/composable_data_stack.egg-info/SOURCES.txt +39 -1
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/composable_data_stack.egg-info/requires.txt +2 -1
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/pyproject.toml +74 -2
- composable_data_stack-0.15.0/tests/test_agent_policy.py +49 -0
- composable_data_stack-0.15.0/tests/test_airflow_module.py +97 -0
- composable_data_stack-0.15.0/tests/test_audit_trivyignore.py +160 -0
- composable_data_stack-0.15.0/tests/test_cds_runtime.py +592 -0
- composable_data_stack-0.15.0/tests/test_cds_runtime_backends.py +153 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_cds_workflow.py +26 -43
- composable_data_stack-0.15.0/tests/test_certificates.py +224 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_completion.py +1 -2
- composable_data_stack-0.15.0/tests/test_contract_host_resolution.py +672 -0
- composable_data_stack-0.15.0/tests/test_dagster_entrypoint_backend_guard.py +442 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_environment_cli.py +15 -1
- composable_data_stack-0.15.0/tests/test_garage_module.py +342 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_getter.py +527 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_getter_extra.py +121 -0
- composable_data_stack-0.15.0/tests/test_gitops_fluxcd_module.py +302 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_image_digest_check.py +2 -2
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_image_updates.py +70 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_k3d_e2e_profile.py +15 -0
- composable_data_stack-0.15.0/tests/test_k8s_e2e_workflow.py +66 -0
- composable_data_stack-0.15.0/tests/test_k8s_renderer.py +1289 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_k8s_runner.py +18 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_k8s_security.py +156 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_keycloak_postgres_wiring.py +50 -0
- composable_data_stack-0.15.0/tests/test_local_dagster_postgres_superset_dbt_profile.py +92 -0
- composable_data_stack-0.15.0/tests/test_local_haven_reference_profile.py +83 -0
- composable_data_stack-0.15.0/tests/test_local_kubernetes_example_profile.py +81 -0
- composable_data_stack-0.15.0/tests/test_loki_module.py +153 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_main.py +143 -2
- composable_data_stack-0.15.0/tests/test_marquez_lineage_sink_host.py +140 -0
- composable_data_stack-0.15.0/tests/test_marquez_module.py +113 -0
- composable_data_stack-0.15.0/tests/test_maturity.py +363 -0
- composable_data_stack-0.15.0/tests/test_module_contract_schema.py +92 -0
- composable_data_stack-0.15.0/tests/test_object_storage_backup_module.py +323 -0
- composable_data_stack-0.15.0/tests/test_object_storage_backup_runtime.py +519 -0
- composable_data_stack-0.15.0/tests/test_otel_collector_module.py +252 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_planner.py +144 -6
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_planner_extra.py +80 -0
- composable_data_stack-0.15.0/tests/test_postgres_backup_restore_runtime.py +495 -0
- composable_data_stack-0.15.0/tests/test_preflight_rootless.py +50 -0
- composable_data_stack-0.15.0/tests/test_production_kubernetes_example_profile.py +97 -0
- composable_data_stack-0.15.0/tests/test_prompts_lint.py +407 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_publish_images_workflow.py +48 -0
- composable_data_stack-0.15.0/tests/test_redact_secrets.py +103 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_render_example_profile.py +64 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_renderer.py +410 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_report.py +21 -43
- composable_data_stack-0.15.0/tests/test_restic_backup_restore_runtime.py +558 -0
- composable_data_stack-0.15.0/tests/test_restic_backup_wiring.py +295 -0
- composable_data_stack-0.15.0/tests/test_reverse_proxy_contract.py +67 -0
- composable_data_stack-0.15.0/tests/test_reverse_proxy_routes.py +786 -0
- composable_data_stack-0.15.0/tests/test_runtime_confinement.py +234 -0
- composable_data_stack-0.15.0/tests/test_runtime_policy.py +244 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_secrets.py +20 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_security.py +217 -1
- composable_data_stack-0.15.0/tests/test_shipped_profiles.py +40 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_standalone_module_profile.py +5 -1
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_superset_hardening.py +8 -2
- composable_data_stack-0.15.0/tests/test_superset_oidc_wiring.py +266 -0
- composable_data_stack-0.15.0/tests/test_team_profile.py +139 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_trivyignore.py +3 -5
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_up_runner.py +110 -2
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_validator.py +167 -0
- composable_data_stack-0.15.0/tests/test_validator_extra.py +1453 -0
- composable_data_stack-0.15.0/tests/test_workdir_cds_runtime.py +154 -0
- composable_data_stack-0.13.0/cli/utils.py +0 -35
- composable_data_stack-0.13.0/cli/validator.py +0 -861
- composable_data_stack-0.13.0/tests/test_dagster_entrypoint_backend_guard.py +0 -164
- composable_data_stack-0.13.0/tests/test_k8s_renderer.py +0 -602
- composable_data_stack-0.13.0/tests/test_validator_extra.py +0 -559
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/LICENSE +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/__init__.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/audit_log.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/composer.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/constants.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/diagnostics.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/graph.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/image_digest_check.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/image_verification.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/loader.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/overlay.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/report.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/resources/__init__.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/resources/compatibility-registry.json +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/resources/compatibility-registry.schema.json +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/resources/contract.schema.json +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/secret_leak.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/state.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/cli/update_check.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/composable_data_stack.egg-info/dependency_links.txt +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/composable_data_stack.egg-info/entry_points.txt +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/composable_data_stack.egg-info/top_level.txt +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/setup.cfg +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_ai_profile_review.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_audit_log.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_build_cra_release_evidence_manifest.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_bump_changelog.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_check_cra_release_gate.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_check_roadmap_freshness.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_compose_runtime_smoke.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_compose_to_module.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_composer.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_composer_extra.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_cra_traceability.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_dagster_config_generation.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_dagster_definitions_uri.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_dagster_hardening.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_dbt_hardening.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_dlt_postgres_wiring.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_duckdb_hardening.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_environment_classification.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_generate_release_inventory.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_graph.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_image_locks.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_image_security_scan_workflow.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_image_verification.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_k3d_exposure.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_loader.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_main_extra.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_main_extra2.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_main_extra3.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_markdown_mermaid_blocks.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_module_isolation.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_modules_no_committed_secrets.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_mvp_proof_workflow.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_overlay.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_preflight.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_preflight_extra.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_python_version_alignment.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_release_notes_support.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_release_version_check.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_release_workflow.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_resolve_profile_path.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_resolver.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_secret_file_permissions.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_secret_output_scanning.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_smoke_example_profile.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_state.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_tender_analytics.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_tender_dashboard.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_test_runner_timeout.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_update_check.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_update_check_extra.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_upstream_image_hardening.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_utils.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_validate_cli_sbom.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_vault_init_db.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_vault_init_db_runtime.py +0 -0
- {composable_data_stack-0.13.0 → composable_data_stack-0.15.0}/tests/test_vendored_llm_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: composable-data-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0
|
|
4
4
|
Summary: CLI for validating and composing composable data stack profiles
|
|
5
5
|
Author: Ronald Hensbergen
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,11 +28,12 @@ Provides-Extra: completion
|
|
|
28
28
|
Requires-Dist: argcomplete<4,>=3.0.0; extra == "completion"
|
|
29
29
|
Provides-Extra: test
|
|
30
30
|
Requires-Dist: jinja2>=3.0; extra == "test"
|
|
31
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "test"
|
|
31
32
|
Provides-Extra: dev
|
|
32
33
|
Requires-Dist: coverage>=7.6; extra == "dev"
|
|
33
34
|
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
34
35
|
Requires-Dist: pip-audit>=2.7; extra == "dev"
|
|
35
|
-
Requires-Dist: ruff==0.17.
|
|
36
|
+
Requires-Dist: ruff==0.17.3; extra == "dev"
|
|
36
37
|
Dynamic: license-file
|
|
37
38
|
|
|
38
39
|
# 🚀 Composable Data Stack (CDS)
|
|
@@ -813,10 +814,38 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
|
|
|
813
814
|
| `[E081] ... Required secret "CDS_X_PASSWORD" not found in environment` | A secret marked `required: true` in the profile's `spec.secrets.values` is missing from the shell environment or the `.env` file in the current working directory. | Run `cds init <profile>` to generate `.env` in the project root, set the missing `CDS_*` variable, or export it directly before running the command. |
|
|
814
815
|
| `[E041] ... Contract ref "x.y" points to unknown module "x"` | A `consumes` binding's `contractRef` refers to a module ID that isn't defined in the profile. | Check `spec.modules` for the correct module `id`, and confirm the contract ref follows `<module-id>.<contract-name>`. |
|
|
815
816
|
| `[E041] ... but it does not provide "<contract-name>"` | The referenced module exists, but its `spec.provides` list doesn't expose that contract name. | Check the producing module's `module.yaml` for the contracts it actually provides, and fix the consumer's `contractRef` to match. |
|
|
817
|
+
| `[E129] ... Certificate ref ... is not defined in spec.certificates.values` (or `malformed`) | A module config value references `certificates.<alias>.cert`/`.key`, but the alias isn't declared under `spec.certificates.values` -- or the reference is bare/has an unknown trailing part instead of `.cert` or `.key`. | Declare the alias under `spec.certificates.values` with its `certEnv`/`keyEnv` names, or fix the reference to name one half of a declared pair. CDS only maps names; the certificate/key material itself is supplied at runtime by the external secret/cert provider. |
|
|
816
818
|
| `[E042] ... Contract kind mismatch` | The consumer expects one contract kind (e.g. `sql-database`) but the producer exposes a different kind. | Point the binding at a module that provides the expected contract kind, or update the consumer's expected kind if the mismatch is intentional. |
|
|
817
819
|
| `[E043] ... recorded as unsupported in the compatibility registry` | The binding's `kind` matches, but this exact provider/consumer pairing is explicitly recorded as `unsupported` in `cli/resources/compatibility-registry.json` -- it's known NOT to work together despite matching shape. | Use a different provider/consumer pairing for this contract, or if the pairing has since been fixed/verified, update its `status` to `tested` in the registry (see [docs/architecture.md § Secrets and contract resolution](docs/architecture.md#secrets-and-contract-resolution)). |
|
|
820
|
+
| `[E128] ... requires a TLS-protected endpoint (tls: required), but ... does not advertise one` | A module's `consumes` entry declares `tls: required` (issue #577), but the bound provider's contract spec does not set `tls: true` (absent, `false`, or a `${config.<path>}` reference that does not resolve to `true` against the producer's profile config). | Point the binding at a provider that advertises TLS, or have the provider set `tls: true` in its provide entry's contract spec. Drop `tls: required` only if the binding is genuinely allowed to run unencrypted. |
|
|
821
|
+
|
|
822
|
+
| `[E130] ... Duplicate backup policy for workload class "<class>"` | `spec.backup.policies` declares more than one policy for the same workload class. | Keep one policy per workload class. |
|
|
823
|
+
| `[E131] ... Backup encryption key ref ... is not defined in spec.secrets.values` | `spec.backup.encryption.keyFrom` references a `secrets.<alias>` that the profile does not declare. | Declare the alias under `spec.secrets.values` with its `env` name, or fix the reference. |
|
|
824
|
+
| `[E132] ... Backup target contractRef ...` | The pinned `spec.backup.target.contractRef` is malformed, points to an unknown module or contract, is not a `backup-target` provider, or (in production) the provider does not advertise `encrypted: true` / `supportsRestoreVerification: true`. | Point at a module providing a `backup-target` contract that advertises the required capabilities, or drop the pin to leave the storage provider open. |
|
|
825
|
+
| `[E133] ... Production profiles must declare spec.backup ...` / `... missing workload classes: ...` | A `metadata.environment: production` profile has no `spec.backup` block, or lacks a policy for a workload class it runs (warehouse, orchestration/BI/identity/secrets metadata, or configuration). | Declare encrypted, restore-verified backup policies for every workload class the profile runs; see `docs/backup-restore.md`. |
|
|
826
|
+
| `[E134] ... Production backups must be encrypted ...` / `... must be declared required: true` | A production profile has no `spec.backup.encryption.keyFrom`, or the referenced secret alias is declared `required: false` so the key can be missing at runtime. | Declare the encryption key alias with `required: true` so a missing key fails closed at startup. |
|
|
827
|
+
| `[E135] ... Production backup policy "<class>" must declare restoreVerification` | A production backup policy has no `restoreVerification` block. | Add `restoreVerification.frequencyDays` to every production backup policy.|
|
|
828
|
+
| `[E136] ... Reverse-proxy route host ...` (unknown target / no resolvable http-service endpoint / backend address not a rendered compose service / entrypoint not declared by the proxy / undeclared certificate alias / duplicate route host) | A `config.routes` entry on a reverse-proxy provider names a module that is missing, does not provide an `http-service` contract with host/port (or a port outside 1-65535), advertises a backend address that matches no rendered compose service (multi-service modules must advertise `serviceName`), names a `<module-id>.<contract-name>` contract the target does not provide, names an entrypoint the proxy's `config.entryPoints` does not declare, references an undeclared certificate alias, or duplicates the host of an earlier route on the same proxy (#581). | Fix the route's `target`/`certificate`/`host`/`entryPoints` (a bare module id routes to its first http-service contract; `<module-id>.<contract-name>` picks a specific one; entrypoints must match the proxy's declared `config.entryPoints` keys), or advertise the serving compose service via `serviceName` in the target's http-service provide entry. |
|
|
829
|
+
| `[E150] ... Provided contract ... advertises network host ...` | A module's provided `http-service`, `sql-database`, `cache-service`, `object-storage`, `lineage-sink`, `log-sink`, `metrics-sink`, or `trace-sink` contract advertises a network host (the `host` field, the `endpoint`'s or `connectionUri`'s hostname part, or a `serviceName` mapped through the renderer's service-naming rule; a scheme-less `host:port` value contributes its host part, #1036) that matches no service the render produced for that module -- no compose service on the compose target, no rendered Kubernetes Service on the helm target -- so consumers would resolve a hostname nothing answers (#1010, #1029, #1035). | Compose: instantiate the module with an id matching its compose service name, advertise the serving service via a `serviceName` field in the provide entry, or build the endpoint from `${service.dns}` or `${service.services.<name>}`. Helm: render the serving workload with a `service` block (or as a `StatefulSet`, which gets a governing Service), advertise the serving workload via a `serviceName` field, or build the endpoint from `${service.dns}`. |
|
|
830
|
+
| `[E151] ... Raw manifest ... must be an object with apiVersion, kind, and metadata.name` / `... must declare non-empty string apiVersion, kind, and metadata.name` | A raw manifest under a module's `implementation.kubernetes.manifests` is malformed -- a plan loaded from a file bypasses the module schema, so the Helm renderer fails closed instead of shipping a broken manifest (issue #767). | Fix the manifest shape in the module (or the saved plan): every entry needs string `apiVersion`/`kind` and a `metadata.name`. |
|
|
818
831
|
| `[E103] ... config.image.tag is required ... when config.image.source is "registry"` | A module instance sets `config.image.source: registry` without also setting `config.image.tag`. | Set `config.image.tag` to a version, or switch back to `config.image.source: build`. |
|
|
832
|
+
| `[E139] ... without an explicit host authentication advertisement` / `... host authentication method "trust"` | A `metadata.environment: production` profile runs a `sql-database` provider whose contract does not advertise a host authentication method, or advertises `trust` (issue #206). | Configure the provider's authentication explicitly (e.g. postgres `config.authentication.hostAuth: scram-sha-256`); trust authentication is prohibited in production profiles. |
|
|
833
|
+
| `[E140] ... configures webServer.mode "development"` | A production profile sets an HTTP-serving module's `config.webServer.mode` to `development`, selecting its built-in development web server (issue #216). | Set `config.webServer.mode` to `production` (or remove the override) so the service runs behind its production process manager; see `docs/serving-modes.md`. |
|
|
834
|
+
| `[E137] ... accepts unauthenticated clients` | A production profile runs a `cache-service` provider that does not advertise `authentication: required` (no password credential configured). | Supply a cache password through a `secrets.<alias>` reference (e.g. keydb `config.passwordFrom`), declared `required: true` in `spec.secrets.values`. |
|
|
835
|
+
| `[E138] ... Credential field ... must reference a secret alias` / `... is empty` / `... uses a ${CDS_*} profile input` / `... contains a plaintext value` | A credential-named config field of a production database/cache provider is not a `secrets.<alias>` reference, or its alias is not declared `required: true` (issue #206). | Supply the credential through a secret reference declared `required: true` so a missing value fails closed at startup. |
|
|
836
|
+
| `[W105] ... legacy md5 host authentication` | A production `sql-database` provider advertises `md5`, which validates but is a deprecated password method. | Prefer `scram-sha-256` for production profiles. |
|
|
837
|
+
| `[E148] ... RBAC rule ... must be a mapping with non-empty "apiGroups", "resources", and "verbs" lists` / `... The "serviceAccount" block ... must be a mapping ...` | A module's `implementation.kubernetes.serviceAccount` block (or one of its `rbac` rules) is malformed -- a plan loaded from a file bypasses the module schema, so the Helm renderer fails closed instead of shipping a broken Role (#75). | Fix the `serviceAccount`/`rbac` shape in the module (or the saved plan). |
|
|
838
|
+
| `[E149] ... The "networkPolicy"/... block ... must be a mapping with optional "ingress"/"egress" lists ...` | A module's custom NetworkPolicy allow rules are malformed (plan-file bypass of the module schema) (#75). | Fix the `networkPolicy` shape; each rule must be a mapping. |
|
|
839
|
+
| `[W128] ... carries no numeric port and provider module ... advertises no service ports ...` | A contract binding carries no port evidence, so its generated NetworkPolicy egress rule allows all ports to the provider (#75). | Declare the port in the contract so the rule stays port-scoped. |
|
|
819
840
|
| `[W097] ... config.image.tag is "latest" with config.image.source "registry"` | `config.image.tag: latest` under `source: registry` still validates, but drifts silently between deploys instead of pinning a reproducible version. | Pin an explicit tag from `publish-images.yml`'s output (or `tests/fixtures/signed-images.json`) instead of `latest`. |
|
|
841
|
+
| `[E127] ... Unknown confinement key ...` / `... must be 'default', 'unconfined' or 'file:<path>' ...` / `... Invalid SELinux label(s) ...` | A `security.runtime.confinement` block (or a module instance's confinement) is malformed: an unsupported key, an invalid `seccomp`/`apparmor`/`selinux` value, or a `file:` seccomp profile path that is missing or resolves outside the profile directory. | Use `seccomp: default`/`unconfined`/`file:<relative path inside the profile directory>`, `apparmor: runtime/default`/`unconfined`/`<host profile name>`, and `selinux: disable` or a `label: value` list; see `docs/runtime-confinement.md`. |
|
|
842
|
+
| `[W071] ... has kind "docker-compose" but no "compose" definition` | A module implementation declares the `docker-compose` kind without an `implementation.compose` block, so the renderer skips the module. | Add the `implementation.compose` block (services, volumes) to the module's `module.yaml`, or set a supported implementation `kind`. |
|
|
843
|
+
| `[W072] ... Container "<name>" in Kubernetes workload "<module>/<workload>" has no resource requests or limits` | Rendering the Helm target: a container or init container listed in a Kubernetes workload has no matching entry under the workload's `resources`. | Add a `resources.<container>` entry with `requests` and `limits` to the workload in the module's Kubernetes implementation (see `docs/kubernetes.md`). |
|
|
844
|
+
| `[W090] ... Invalid .env line format: "<line>" (expected KEY=VALUE)` | A line in the `.env` file is neither blank, a `#` comment, nor in `KEY=VALUE` form, so it is ignored. | Fix the line to `KEY=VALUE` form, comment it out with `#`, or remove it. |
|
|
845
|
+
| `[W098] ... Applied plaintext endpoint exposure waiver for production profile (modules: ...)` | A production profile's `spec.security.waivers.plaintextEndpointExposure` waived the CDS-SEC-074 plaintext-endpoint findings; CDS reports the waiver so it stays visible in security output. | Confirm the waiver is still justified and its `reason` is accurate; see `docs/security-rules.md` (Warnings). |
|
|
846
|
+
| `[W099] ... plaintextEndpointExposure is set but has no effect for profile class ...` | `spec.security.waivers.plaintextEndpointExposure` is declared on a non-prod profile, where CDS-SEC-074 and its waiver only apply to prod-class profiles. | Remove the waiver from the non-prod profile, or move it to the prod profile (overlay) it is meant for. |
|
|
847
|
+
| `[W103] ... Applied runtime policy waiver for production profile (checks: ...)` | A production profile's `spec.security.waivers.runtimePolicy` suppressed CDS-SEC-090 violations; CDS reports the waiver so it stays visible in security output. | Confirm the waiver is still justified and its `reason` is accurate; see `docs/security-rules.md` (Warnings). |
|
|
848
|
+
| `[W104] ... runtimePolicy is set but has no effect for profile class ...` | `spec.security.waivers.runtimePolicy` is declared on a non-prod profile, where CDS-SEC-090 and its waiver only apply to prod-class profiles. | Remove the waiver from the non-prod profile, or move it to the prod profile (overlay) it is meant for. |
|
|
820
849
|
|
|
821
850
|
All diagnostics print with their error code and YAML path (e.g. `spec.modules[1].config`), so search the profile file for that path to find the exact line to fix.
|
|
822
851
|
|
|
@@ -957,6 +986,8 @@ Please read these first:
|
|
|
957
986
|
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
958
987
|
- [docs/maintainer-merge-policy.md](docs/maintainer-merge-policy.md)
|
|
959
988
|
- [SECURITY.md](SECURITY.md) — how to report a vulnerability
|
|
989
|
+
- [ACCESSIBILITY.md](ACCESSIBILITY.md) — our accessibility commitment and how
|
|
990
|
+
to report a barrier
|
|
960
991
|
- [LEGAL.md](LEGAL.md) — license, code of conduct, security, support, and
|
|
961
992
|
regulatory-compliance documentation index
|
|
962
993
|
- [CHANGELOG.md](CHANGELOG.md)
|
|
@@ -972,10 +1003,12 @@ Development helper tools are located in the gitignored `tools/` directory. See
|
|
|
972
1003
|
- [Quickstart](README.md#-quickstart) — get running in 5 minutes
|
|
973
1004
|
- [CLI Reference](docs/cli-reference.md) — full command/flag table and `[profile]` resolution rules
|
|
974
1005
|
- [How CDS Works](docs/how-it-works.md) — C4 + sequence diagrams of the validate → plan → render engine
|
|
975
|
-
- [Kubernetes Target](docs/kubernetes.md): Helm rendering and isolated local k3s workflow
|
|
1006
|
+
- [Kubernetes Target](docs/kubernetes.md): Helm rendering, Docker Compose migration guide, and isolated local k3s workflow — with ready-made example profiles (`profiles/local-kubernetes-example`, `profiles/production-kubernetes-example` with its `prod` overlay)
|
|
976
1007
|
- [From Docker Compose to CDS Profile](docs/from-docker-to-cds-profile.md) — complete transformation guide
|
|
977
1008
|
- [Architecture](docs/architecture.md) — design and core concepts
|
|
978
1009
|
- [Modules](docs/modules.md) — how to structure reusable components
|
|
1010
|
+
- [Workload Runtime API](docs/runtime-api.md) — `cds-runtime`: named database connections for Python workloads
|
|
1011
|
+
- [Production Hardening Checklist](docs/production-hardening-checklist.md) — control-to-owner mapping and the operator verification procedure for a production profile
|
|
979
1012
|
- [Roadmap](docs/roadmap.md) — planned features and milestones
|
|
980
1013
|
|
|
981
1014
|
---
|
|
@@ -776,10 +776,38 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
|
|
|
776
776
|
| `[E081] ... Required secret "CDS_X_PASSWORD" not found in environment` | A secret marked `required: true` in the profile's `spec.secrets.values` is missing from the shell environment or the `.env` file in the current working directory. | Run `cds init <profile>` to generate `.env` in the project root, set the missing `CDS_*` variable, or export it directly before running the command. |
|
|
777
777
|
| `[E041] ... Contract ref "x.y" points to unknown module "x"` | A `consumes` binding's `contractRef` refers to a module ID that isn't defined in the profile. | Check `spec.modules` for the correct module `id`, and confirm the contract ref follows `<module-id>.<contract-name>`. |
|
|
778
778
|
| `[E041] ... but it does not provide "<contract-name>"` | The referenced module exists, but its `spec.provides` list doesn't expose that contract name. | Check the producing module's `module.yaml` for the contracts it actually provides, and fix the consumer's `contractRef` to match. |
|
|
779
|
+
| `[E129] ... Certificate ref ... is not defined in spec.certificates.values` (or `malformed`) | A module config value references `certificates.<alias>.cert`/`.key`, but the alias isn't declared under `spec.certificates.values` -- or the reference is bare/has an unknown trailing part instead of `.cert` or `.key`. | Declare the alias under `spec.certificates.values` with its `certEnv`/`keyEnv` names, or fix the reference to name one half of a declared pair. CDS only maps names; the certificate/key material itself is supplied at runtime by the external secret/cert provider. |
|
|
779
780
|
| `[E042] ... Contract kind mismatch` | The consumer expects one contract kind (e.g. `sql-database`) but the producer exposes a different kind. | Point the binding at a module that provides the expected contract kind, or update the consumer's expected kind if the mismatch is intentional. |
|
|
780
781
|
| `[E043] ... recorded as unsupported in the compatibility registry` | The binding's `kind` matches, but this exact provider/consumer pairing is explicitly recorded as `unsupported` in `cli/resources/compatibility-registry.json` -- it's known NOT to work together despite matching shape. | Use a different provider/consumer pairing for this contract, or if the pairing has since been fixed/verified, update its `status` to `tested` in the registry (see [docs/architecture.md § Secrets and contract resolution](docs/architecture.md#secrets-and-contract-resolution)). |
|
|
782
|
+
| `[E128] ... requires a TLS-protected endpoint (tls: required), but ... does not advertise one` | A module's `consumes` entry declares `tls: required` (issue #577), but the bound provider's contract spec does not set `tls: true` (absent, `false`, or a `${config.<path>}` reference that does not resolve to `true` against the producer's profile config). | Point the binding at a provider that advertises TLS, or have the provider set `tls: true` in its provide entry's contract spec. Drop `tls: required` only if the binding is genuinely allowed to run unencrypted. |
|
|
783
|
+
|
|
784
|
+
| `[E130] ... Duplicate backup policy for workload class "<class>"` | `spec.backup.policies` declares more than one policy for the same workload class. | Keep one policy per workload class. |
|
|
785
|
+
| `[E131] ... Backup encryption key ref ... is not defined in spec.secrets.values` | `spec.backup.encryption.keyFrom` references a `secrets.<alias>` that the profile does not declare. | Declare the alias under `spec.secrets.values` with its `env` name, or fix the reference. |
|
|
786
|
+
| `[E132] ... Backup target contractRef ...` | The pinned `spec.backup.target.contractRef` is malformed, points to an unknown module or contract, is not a `backup-target` provider, or (in production) the provider does not advertise `encrypted: true` / `supportsRestoreVerification: true`. | Point at a module providing a `backup-target` contract that advertises the required capabilities, or drop the pin to leave the storage provider open. |
|
|
787
|
+
| `[E133] ... Production profiles must declare spec.backup ...` / `... missing workload classes: ...` | A `metadata.environment: production` profile has no `spec.backup` block, or lacks a policy for a workload class it runs (warehouse, orchestration/BI/identity/secrets metadata, or configuration). | Declare encrypted, restore-verified backup policies for every workload class the profile runs; see `docs/backup-restore.md`. |
|
|
788
|
+
| `[E134] ... Production backups must be encrypted ...` / `... must be declared required: true` | A production profile has no `spec.backup.encryption.keyFrom`, or the referenced secret alias is declared `required: false` so the key can be missing at runtime. | Declare the encryption key alias with `required: true` so a missing key fails closed at startup. |
|
|
789
|
+
| `[E135] ... Production backup policy "<class>" must declare restoreVerification` | A production backup policy has no `restoreVerification` block. | Add `restoreVerification.frequencyDays` to every production backup policy.|
|
|
790
|
+
| `[E136] ... Reverse-proxy route host ...` (unknown target / no resolvable http-service endpoint / backend address not a rendered compose service / entrypoint not declared by the proxy / undeclared certificate alias / duplicate route host) | A `config.routes` entry on a reverse-proxy provider names a module that is missing, does not provide an `http-service` contract with host/port (or a port outside 1-65535), advertises a backend address that matches no rendered compose service (multi-service modules must advertise `serviceName`), names a `<module-id>.<contract-name>` contract the target does not provide, names an entrypoint the proxy's `config.entryPoints` does not declare, references an undeclared certificate alias, or duplicates the host of an earlier route on the same proxy (#581). | Fix the route's `target`/`certificate`/`host`/`entryPoints` (a bare module id routes to its first http-service contract; `<module-id>.<contract-name>` picks a specific one; entrypoints must match the proxy's declared `config.entryPoints` keys), or advertise the serving compose service via `serviceName` in the target's http-service provide entry. |
|
|
791
|
+
| `[E150] ... Provided contract ... advertises network host ...` | A module's provided `http-service`, `sql-database`, `cache-service`, `object-storage`, `lineage-sink`, `log-sink`, `metrics-sink`, or `trace-sink` contract advertises a network host (the `host` field, the `endpoint`'s or `connectionUri`'s hostname part, or a `serviceName` mapped through the renderer's service-naming rule; a scheme-less `host:port` value contributes its host part, #1036) that matches no service the render produced for that module -- no compose service on the compose target, no rendered Kubernetes Service on the helm target -- so consumers would resolve a hostname nothing answers (#1010, #1029, #1035). | Compose: instantiate the module with an id matching its compose service name, advertise the serving service via a `serviceName` field in the provide entry, or build the endpoint from `${service.dns}` or `${service.services.<name>}`. Helm: render the serving workload with a `service` block (or as a `StatefulSet`, which gets a governing Service), advertise the serving workload via a `serviceName` field, or build the endpoint from `${service.dns}`. |
|
|
792
|
+
| `[E151] ... Raw manifest ... must be an object with apiVersion, kind, and metadata.name` / `... must declare non-empty string apiVersion, kind, and metadata.name` | A raw manifest under a module's `implementation.kubernetes.manifests` is malformed -- a plan loaded from a file bypasses the module schema, so the Helm renderer fails closed instead of shipping a broken manifest (issue #767). | Fix the manifest shape in the module (or the saved plan): every entry needs string `apiVersion`/`kind` and a `metadata.name`. |
|
|
781
793
|
| `[E103] ... config.image.tag is required ... when config.image.source is "registry"` | A module instance sets `config.image.source: registry` without also setting `config.image.tag`. | Set `config.image.tag` to a version, or switch back to `config.image.source: build`. |
|
|
794
|
+
| `[E139] ... without an explicit host authentication advertisement` / `... host authentication method "trust"` | A `metadata.environment: production` profile runs a `sql-database` provider whose contract does not advertise a host authentication method, or advertises `trust` (issue #206). | Configure the provider's authentication explicitly (e.g. postgres `config.authentication.hostAuth: scram-sha-256`); trust authentication is prohibited in production profiles. |
|
|
795
|
+
| `[E140] ... configures webServer.mode "development"` | A production profile sets an HTTP-serving module's `config.webServer.mode` to `development`, selecting its built-in development web server (issue #216). | Set `config.webServer.mode` to `production` (or remove the override) so the service runs behind its production process manager; see `docs/serving-modes.md`. |
|
|
796
|
+
| `[E137] ... accepts unauthenticated clients` | A production profile runs a `cache-service` provider that does not advertise `authentication: required` (no password credential configured). | Supply a cache password through a `secrets.<alias>` reference (e.g. keydb `config.passwordFrom`), declared `required: true` in `spec.secrets.values`. |
|
|
797
|
+
| `[E138] ... Credential field ... must reference a secret alias` / `... is empty` / `... uses a ${CDS_*} profile input` / `... contains a plaintext value` | A credential-named config field of a production database/cache provider is not a `secrets.<alias>` reference, or its alias is not declared `required: true` (issue #206). | Supply the credential through a secret reference declared `required: true` so a missing value fails closed at startup. |
|
|
798
|
+
| `[W105] ... legacy md5 host authentication` | A production `sql-database` provider advertises `md5`, which validates but is a deprecated password method. | Prefer `scram-sha-256` for production profiles. |
|
|
799
|
+
| `[E148] ... RBAC rule ... must be a mapping with non-empty "apiGroups", "resources", and "verbs" lists` / `... The "serviceAccount" block ... must be a mapping ...` | A module's `implementation.kubernetes.serviceAccount` block (or one of its `rbac` rules) is malformed -- a plan loaded from a file bypasses the module schema, so the Helm renderer fails closed instead of shipping a broken Role (#75). | Fix the `serviceAccount`/`rbac` shape in the module (or the saved plan). |
|
|
800
|
+
| `[E149] ... The "networkPolicy"/... block ... must be a mapping with optional "ingress"/"egress" lists ...` | A module's custom NetworkPolicy allow rules are malformed (plan-file bypass of the module schema) (#75). | Fix the `networkPolicy` shape; each rule must be a mapping. |
|
|
801
|
+
| `[W128] ... carries no numeric port and provider module ... advertises no service ports ...` | A contract binding carries no port evidence, so its generated NetworkPolicy egress rule allows all ports to the provider (#75). | Declare the port in the contract so the rule stays port-scoped. |
|
|
782
802
|
| `[W097] ... config.image.tag is "latest" with config.image.source "registry"` | `config.image.tag: latest` under `source: registry` still validates, but drifts silently between deploys instead of pinning a reproducible version. | Pin an explicit tag from `publish-images.yml`'s output (or `tests/fixtures/signed-images.json`) instead of `latest`. |
|
|
803
|
+
| `[E127] ... Unknown confinement key ...` / `... must be 'default', 'unconfined' or 'file:<path>' ...` / `... Invalid SELinux label(s) ...` | A `security.runtime.confinement` block (or a module instance's confinement) is malformed: an unsupported key, an invalid `seccomp`/`apparmor`/`selinux` value, or a `file:` seccomp profile path that is missing or resolves outside the profile directory. | Use `seccomp: default`/`unconfined`/`file:<relative path inside the profile directory>`, `apparmor: runtime/default`/`unconfined`/`<host profile name>`, and `selinux: disable` or a `label: value` list; see `docs/runtime-confinement.md`. |
|
|
804
|
+
| `[W071] ... has kind "docker-compose" but no "compose" definition` | A module implementation declares the `docker-compose` kind without an `implementation.compose` block, so the renderer skips the module. | Add the `implementation.compose` block (services, volumes) to the module's `module.yaml`, or set a supported implementation `kind`. |
|
|
805
|
+
| `[W072] ... Container "<name>" in Kubernetes workload "<module>/<workload>" has no resource requests or limits` | Rendering the Helm target: a container or init container listed in a Kubernetes workload has no matching entry under the workload's `resources`. | Add a `resources.<container>` entry with `requests` and `limits` to the workload in the module's Kubernetes implementation (see `docs/kubernetes.md`). |
|
|
806
|
+
| `[W090] ... Invalid .env line format: "<line>" (expected KEY=VALUE)` | A line in the `.env` file is neither blank, a `#` comment, nor in `KEY=VALUE` form, so it is ignored. | Fix the line to `KEY=VALUE` form, comment it out with `#`, or remove it. |
|
|
807
|
+
| `[W098] ... Applied plaintext endpoint exposure waiver for production profile (modules: ...)` | A production profile's `spec.security.waivers.plaintextEndpointExposure` waived the CDS-SEC-074 plaintext-endpoint findings; CDS reports the waiver so it stays visible in security output. | Confirm the waiver is still justified and its `reason` is accurate; see `docs/security-rules.md` (Warnings). |
|
|
808
|
+
| `[W099] ... plaintextEndpointExposure is set but has no effect for profile class ...` | `spec.security.waivers.plaintextEndpointExposure` is declared on a non-prod profile, where CDS-SEC-074 and its waiver only apply to prod-class profiles. | Remove the waiver from the non-prod profile, or move it to the prod profile (overlay) it is meant for. |
|
|
809
|
+
| `[W103] ... Applied runtime policy waiver for production profile (checks: ...)` | A production profile's `spec.security.waivers.runtimePolicy` suppressed CDS-SEC-090 violations; CDS reports the waiver so it stays visible in security output. | Confirm the waiver is still justified and its `reason` is accurate; see `docs/security-rules.md` (Warnings). |
|
|
810
|
+
| `[W104] ... runtimePolicy is set but has no effect for profile class ...` | `spec.security.waivers.runtimePolicy` is declared on a non-prod profile, where CDS-SEC-090 and its waiver only apply to prod-class profiles. | Remove the waiver from the non-prod profile, or move it to the prod profile (overlay) it is meant for. |
|
|
783
811
|
|
|
784
812
|
All diagnostics print with their error code and YAML path (e.g. `spec.modules[1].config`), so search the profile file for that path to find the exact line to fix.
|
|
785
813
|
|
|
@@ -920,6 +948,8 @@ Please read these first:
|
|
|
920
948
|
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
921
949
|
- [docs/maintainer-merge-policy.md](docs/maintainer-merge-policy.md)
|
|
922
950
|
- [SECURITY.md](SECURITY.md) — how to report a vulnerability
|
|
951
|
+
- [ACCESSIBILITY.md](ACCESSIBILITY.md) — our accessibility commitment and how
|
|
952
|
+
to report a barrier
|
|
923
953
|
- [LEGAL.md](LEGAL.md) — license, code of conduct, security, support, and
|
|
924
954
|
regulatory-compliance documentation index
|
|
925
955
|
- [CHANGELOG.md](CHANGELOG.md)
|
|
@@ -935,10 +965,12 @@ Development helper tools are located in the gitignored `tools/` directory. See
|
|
|
935
965
|
- [Quickstart](README.md#-quickstart) — get running in 5 minutes
|
|
936
966
|
- [CLI Reference](docs/cli-reference.md) — full command/flag table and `[profile]` resolution rules
|
|
937
967
|
- [How CDS Works](docs/how-it-works.md) — C4 + sequence diagrams of the validate → plan → render engine
|
|
938
|
-
- [Kubernetes Target](docs/kubernetes.md): Helm rendering and isolated local k3s workflow
|
|
968
|
+
- [Kubernetes Target](docs/kubernetes.md): Helm rendering, Docker Compose migration guide, and isolated local k3s workflow — with ready-made example profiles (`profiles/local-kubernetes-example`, `profiles/production-kubernetes-example` with its `prod` overlay)
|
|
939
969
|
- [From Docker Compose to CDS Profile](docs/from-docker-to-cds-profile.md) — complete transformation guide
|
|
940
970
|
- [Architecture](docs/architecture.md) — design and core concepts
|
|
941
971
|
- [Modules](docs/modules.md) — how to structure reusable components
|
|
972
|
+
- [Workload Runtime API](docs/runtime-api.md) — `cds-runtime`: named database connections for Python workloads
|
|
973
|
+
- [Production Hardening Checklist](docs/production-hardening-checklist.md) — control-to-owner mapping and the operator verification procedure for a production profile
|
|
942
974
|
- [Roadmap](docs/roadmap.md) — planned features and milestones
|
|
943
975
|
|
|
944
976
|
---
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Declarative runtime confinement (seccomp, AppArmor, SELinux) for Compose services.
|
|
2
|
+
|
|
3
|
+
A profile declares a default under `spec.security.runtime.confinement`; a
|
|
4
|
+
module instance may override individual keys under `security.confinement`.
|
|
5
|
+
The effective value is rendered into each service's `security_opt`.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .diagnostics import Diagnostic
|
|
15
|
+
|
|
16
|
+
INVALID_CONFINEMENT_CODE = "E127"
|
|
17
|
+
CONFINEMENT_KEYS = ("seccomp", "apparmor", "selinux")
|
|
18
|
+
|
|
19
|
+
_APPARMOR_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$")
|
|
20
|
+
_DRIVE_RE = re.compile(r"^[A-Za-z]:")
|
|
21
|
+
_SELINUX_LABEL_RE = re.compile(r"^(user|role|type|level):[A-Za-z0-9_.,:-]+$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def profile_confinement(profile_or_plan: dict[str, Any] | None) -> dict[str, Any]:
|
|
25
|
+
"""Return the profile-wide confinement block from a profile or plan."""
|
|
26
|
+
if not isinstance(profile_or_plan, dict):
|
|
27
|
+
return {}
|
|
28
|
+
security = profile_or_plan.get("security")
|
|
29
|
+
if security is None:
|
|
30
|
+
security = (profile_or_plan.get("spec") or {}).get("security")
|
|
31
|
+
runtime = (security or {}).get("runtime") if isinstance(security, dict) else None
|
|
32
|
+
block = (runtime or {}).get("confinement") if isinstance(runtime, dict) else None
|
|
33
|
+
return block if isinstance(block, dict) else {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def plan_declares_confinement(plan: dict[str, Any] | None) -> bool:
|
|
37
|
+
"""True when the profile or any module instance sets confinement."""
|
|
38
|
+
if profile_confinement(plan):
|
|
39
|
+
return True
|
|
40
|
+
return any(
|
|
41
|
+
isinstance((module.get("security") or {}).get("confinement"), dict)
|
|
42
|
+
and (module.get("security") or {}).get("confinement")
|
|
43
|
+
for module in (plan or {}).get("modules") or []
|
|
44
|
+
if isinstance(module, dict)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def effective_confinement(
|
|
49
|
+
defaults: dict[str, Any] | None, override: dict[str, Any] | None
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
"""Profile defaults with the module instance's keys layered on top."""
|
|
52
|
+
merged = {key: value for key, value in (defaults or {}).items() if key in CONFINEMENT_KEYS}
|
|
53
|
+
for key, value in (override or {}).items():
|
|
54
|
+
if key in CONFINEMENT_KEYS:
|
|
55
|
+
merged[key] = value
|
|
56
|
+
return merged
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_confinement(
|
|
60
|
+
block: Any, path: str, base_dir: Path | None = None
|
|
61
|
+
) -> list[Diagnostic]:
|
|
62
|
+
"""Check one confinement block; `base_dir` anchors `file:` seccomp profiles."""
|
|
63
|
+
if block is None:
|
|
64
|
+
return []
|
|
65
|
+
if not isinstance(block, dict):
|
|
66
|
+
return [_error(f"{path} must be an object.", path)]
|
|
67
|
+
diagnostics: list[Diagnostic] = []
|
|
68
|
+
for key in block:
|
|
69
|
+
if key not in CONFINEMENT_KEYS:
|
|
70
|
+
diagnostics.append(_error(
|
|
71
|
+
f"Unknown confinement key '{key}'; supported keys: {', '.join(CONFINEMENT_KEYS)}.",
|
|
72
|
+
f"{path}.{key}",
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
seccomp = block.get("seccomp")
|
|
76
|
+
if seccomp is not None:
|
|
77
|
+
diagnostics.extend(_validate_seccomp(seccomp, f"{path}.seccomp", base_dir))
|
|
78
|
+
apparmor = block.get("apparmor")
|
|
79
|
+
if apparmor is not None and not (
|
|
80
|
+
isinstance(apparmor, str)
|
|
81
|
+
and (apparmor in {"runtime/default", "unconfined"} or _APPARMOR_NAME_RE.match(apparmor))
|
|
82
|
+
):
|
|
83
|
+
diagnostics.append(_error(
|
|
84
|
+
"apparmor must be 'runtime/default', 'unconfined' or the name of a profile "
|
|
85
|
+
"already loaded on the host (letters, digits, '.', '_', '-', '/').",
|
|
86
|
+
f"{path}.apparmor",
|
|
87
|
+
))
|
|
88
|
+
selinux = block.get("selinux")
|
|
89
|
+
if selinux is not None:
|
|
90
|
+
diagnostics.extend(_validate_selinux(selinux, f"{path}.selinux"))
|
|
91
|
+
return diagnostics
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_seccomp(value: Any, path: str, base_dir: Path | None) -> list[Diagnostic]:
|
|
95
|
+
if value in {"default", "unconfined"}:
|
|
96
|
+
return []
|
|
97
|
+
if not isinstance(value, str) or not value.startswith("file:") or len(value) == 5:
|
|
98
|
+
return [_error(
|
|
99
|
+
"seccomp must be 'default', 'unconfined' or 'file:<path to a JSON profile "
|
|
100
|
+
"relative to the profile directory>'.",
|
|
101
|
+
path,
|
|
102
|
+
)]
|
|
103
|
+
relative = value[5:]
|
|
104
|
+
if base_dir is None:
|
|
105
|
+
return [_error(
|
|
106
|
+
"seccomp file profiles need a known profile directory to resolve against.", path
|
|
107
|
+
)]
|
|
108
|
+
# Reject rooted and drive-qualified paths explicitly: they are not
|
|
109
|
+
# "absolute" to pathlib on every platform (e.g. "/x" on Windows).
|
|
110
|
+
if (
|
|
111
|
+
Path(relative).is_absolute()
|
|
112
|
+
or relative.startswith(("/", "\\"))
|
|
113
|
+
or _DRIVE_RE.match(relative)
|
|
114
|
+
or ".." in Path(relative).parts
|
|
115
|
+
):
|
|
116
|
+
return [_error(
|
|
117
|
+
"seccomp file profiles must be relative paths inside the profile directory.", path
|
|
118
|
+
)]
|
|
119
|
+
target = (base_dir / relative).resolve()
|
|
120
|
+
if not target.is_relative_to(base_dir.resolve()):
|
|
121
|
+
return [_error(
|
|
122
|
+
"seccomp file profile resolves outside the profile directory.", path
|
|
123
|
+
)]
|
|
124
|
+
if not target.is_file():
|
|
125
|
+
return [_error(f"seccomp profile '{relative}' does not exist.", path)]
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _validate_selinux(value: Any, path: str) -> list[Diagnostic]:
|
|
130
|
+
if value == "disable":
|
|
131
|
+
return []
|
|
132
|
+
if not isinstance(value, list) or not value or not all(isinstance(v, str) for v in value):
|
|
133
|
+
return [_error(
|
|
134
|
+
"selinux must be 'disable' or a non-empty list of labels such as "
|
|
135
|
+
"'type:container_t' or 'level:s0:c100,c200'.",
|
|
136
|
+
path,
|
|
137
|
+
)]
|
|
138
|
+
bad = [label for label in value if not _SELINUX_LABEL_RE.match(label)]
|
|
139
|
+
if bad:
|
|
140
|
+
return [_error(
|
|
141
|
+
f"Invalid SELinux label(s): {', '.join(bad)}; expected user:, role:, type: or level: values.",
|
|
142
|
+
path,
|
|
143
|
+
)]
|
|
144
|
+
kinds = [label.split(":", 1)[0] for label in value]
|
|
145
|
+
duplicates = sorted({k for k in kinds if kinds.count(k) > 1})
|
|
146
|
+
if duplicates:
|
|
147
|
+
return [_error(
|
|
148
|
+
f"Duplicate SELinux label kind(s): {', '.join(duplicates)}; "
|
|
149
|
+
"each of user:, role:, type: and level: may appear at most once.",
|
|
150
|
+
path,
|
|
151
|
+
)]
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def confinement_security_opts(
|
|
156
|
+
confinement: dict[str, Any], compose_dir: Path, base_dir: Path | None
|
|
157
|
+
) -> list[str]:
|
|
158
|
+
"""Translate a validated confinement block into Compose `security_opt` entries.
|
|
159
|
+
|
|
160
|
+
Raises ValueError when a seccomp file cannot be expressed relative to
|
|
161
|
+
`compose_dir` (for example on a different Windows drive).
|
|
162
|
+
"""
|
|
163
|
+
opts: list[str] = []
|
|
164
|
+
seccomp = confinement.get("seccomp")
|
|
165
|
+
if seccomp == "unconfined":
|
|
166
|
+
opts.append("seccomp=unconfined")
|
|
167
|
+
elif isinstance(seccomp, str) and seccomp.startswith("file:"):
|
|
168
|
+
target = (base_dir or compose_dir) / seccomp[5:]
|
|
169
|
+
opts.append(f"seccomp:{Path(os.path.relpath(target, compose_dir)).as_posix()}")
|
|
170
|
+
apparmor = confinement.get("apparmor")
|
|
171
|
+
if apparmor == "runtime/default":
|
|
172
|
+
opts.append("apparmor=docker-default")
|
|
173
|
+
elif isinstance(apparmor, str):
|
|
174
|
+
opts.append(f"apparmor={apparmor}")
|
|
175
|
+
selinux = confinement.get("selinux")
|
|
176
|
+
if selinux == "disable":
|
|
177
|
+
opts.append("label=disable")
|
|
178
|
+
elif isinstance(selinux, list):
|
|
179
|
+
opts.extend(f"label={label}" for label in selinux)
|
|
180
|
+
return opts
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def apply_confinement(
|
|
184
|
+
service: dict[str, Any],
|
|
185
|
+
confinement: dict[str, Any],
|
|
186
|
+
compose_dir: Path,
|
|
187
|
+
base_dir: Path | None,
|
|
188
|
+
path: str = "security.confinement",
|
|
189
|
+
) -> list[Diagnostic]:
|
|
190
|
+
"""Set the declared confinement on a service, replacing module-set values per key."""
|
|
191
|
+
if not confinement:
|
|
192
|
+
return []
|
|
193
|
+
try:
|
|
194
|
+
new_opts = confinement_security_opts(confinement, compose_dir, base_dir)
|
|
195
|
+
except ValueError:
|
|
196
|
+
return [_error(
|
|
197
|
+
"seccomp file profile cannot be referenced relative to the generated "
|
|
198
|
+
"docker-compose.yml (different drive); render into the profile's drive.",
|
|
199
|
+
f"{path}.seccomp",
|
|
200
|
+
)]
|
|
201
|
+
existing = [str(opt) for opt in service.get("security_opt") or []]
|
|
202
|
+
replaced = {
|
|
203
|
+
"seccomp": ("seccomp=", "seccomp:"),
|
|
204
|
+
"apparmor": ("apparmor=", "apparmor:"),
|
|
205
|
+
"selinux": ("label=", "label:"),
|
|
206
|
+
}
|
|
207
|
+
prefixes = tuple(p for key in confinement for p in replaced.get(key, ()))
|
|
208
|
+
kept = [opt for opt in existing if not opt.startswith(prefixes)] if prefixes else existing
|
|
209
|
+
# `default` seccomp means "engine default", so it only clears module-set values.
|
|
210
|
+
merged = kept + new_opts
|
|
211
|
+
if merged:
|
|
212
|
+
service["security_opt"] = merged
|
|
213
|
+
else:
|
|
214
|
+
service.pop("security_opt", None)
|
|
215
|
+
return []
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _error(message: str, path: str) -> Diagnostic:
|
|
219
|
+
return Diagnostic(level="error", code=INVALID_CONFINEMENT_CODE, message=message, path=path)
|