composable-data-stack 0.14.0__tar.gz → 0.15.1__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.14.0/composable_data_stack.egg-info → composable_data_stack-0.15.1}/PKG-INFO +40 -4
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/README.md +37 -2
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/audit_log.py +21 -9
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/composer.py +17 -5
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/confinement.py +83 -47
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/constants.py +1 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/diagnostics.py +1 -1
- composable_data_stack-0.15.1/cli/getter.py +1806 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/graph.py +3 -1
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/image_digest_check.py +21 -8
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/image_updates.py +46 -14
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/image_verification.py +146 -118
- composable_data_stack-0.15.1/cli/k8s_provision.py +504 -0
- composable_data_stack-0.15.1/cli/k8s_renderer.py +3376 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/k8s_runner.py +72 -4
- composable_data_stack-0.15.1/cli/k8s_security.py +407 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/loader.py +5 -1
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/main.py +1453 -314
- composable_data_stack-0.15.1/cli/maturity.py +265 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/overlay.py +274 -24
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/planner.py +644 -133
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/preflight.py +116 -39
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/renderer.py +995 -256
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/report.py +109 -36
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resolver.py +36 -3
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/module.schema.json +340 -7
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/profile.schema.json +58 -1
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/rule-schema.json +5 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/rule-set.json +10 -7
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/secret_leak.py +32 -23
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/secrets.py +47 -25
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/security.py +533 -255
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/security_common.py +101 -9
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/state.py +2 -1
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/up_runner.py +85 -17
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/update_check.py +30 -17
- composable_data_stack-0.15.1/cli/utils.py +94 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/validator.py +1166 -188
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1/composable_data_stack.egg-info}/PKG-INFO +40 -4
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/SOURCES.txt +48 -1
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/requires.txt +2 -1
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/pyproject.toml +74 -2
- composable_data_stack-0.15.1/tests/test_agent_policy.py +66 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_ai_profile_review.py +30 -4
- composable_data_stack-0.15.1/tests/test_airflow_module.py +98 -0
- composable_data_stack-0.15.1/tests/test_assign_project_order.py +102 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_audit_log.py +63 -7
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_audit_trivyignore.py +50 -3
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_build_cra_release_evidence_manifest.py +12 -4
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_bump_changelog.py +3 -1
- composable_data_stack-0.15.1/tests/test_cds_runtime.py +668 -0
- composable_data_stack-0.15.1/tests/test_cds_runtime_backends.py +162 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_cds_workflow.py +37 -22
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_certificates.py +57 -23
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_check_cra_release_gate.py +22 -6
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_check_roadmap_freshness.py +55 -18
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_completion.py +3 -1
- composable_data_stack-0.15.1/tests/test_compose_profile_lifecycle_smoke.py +423 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_compose_runtime_smoke.py +55 -5
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_compose_to_module.py +201 -73
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_composer.py +26 -9
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_composer_extra.py +54 -17
- composable_data_stack-0.15.1/tests/test_config_module_overrides.py +608 -0
- composable_data_stack-0.15.1/tests/test_contract_host_resolution.py +777 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_cra_traceability.py +1 -2
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dagster_config_generation.py +19 -9
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dagster_definitions_uri.py +3 -1
- composable_data_stack-0.15.1/tests/test_dagster_entrypoint_backend_guard.py +464 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dagster_hardening.py +58 -21
- composable_data_stack-0.15.1/tests/test_data_sharing.py +634 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dbt_hardening.py +33 -10
- composable_data_stack-0.15.1/tests/test_dlt_duckdb_wiring.py +236 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dlt_postgres_wiring.py +13 -7
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_duckdb_hardening.py +23 -16
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_environment_classification.py +58 -14
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_environment_cli.py +131 -33
- composable_data_stack-0.15.1/tests/test_garage_module.py +386 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_generate_release_inventory.py +16 -5
- composable_data_stack-0.15.1/tests/test_getter.py +2509 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_getter_extra.py +320 -52
- composable_data_stack-0.15.1/tests/test_gitops_fluxcd_module.py +418 -0
- composable_data_stack-0.15.1/tests/test_grafana_module.py +461 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_graph.py +4 -4
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_digest_check.py +15 -4
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_locks.py +6 -2
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_security_scan_workflow.py +10 -6
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_updates.py +127 -17
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_verification.py +102 -35
- composable_data_stack-0.15.1/tests/test_k3d_e2e_profile.py +144 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_k3d_exposure.py +5 -3
- composable_data_stack-0.15.1/tests/test_k8s_e2e_workflow.py +64 -0
- composable_data_stack-0.15.1/tests/test_k8s_provision.py +622 -0
- composable_data_stack-0.15.1/tests/test_k8s_renderer.py +2184 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_k8s_runner.py +34 -7
- composable_data_stack-0.15.1/tests/test_k8s_security.py +488 -0
- composable_data_stack-0.15.1/tests/test_keycloak_oidc_clients.py +356 -0
- composable_data_stack-0.15.1/tests/test_keycloak_postgres_wiring.py +591 -0
- composable_data_stack-0.15.1/tests/test_kubernetes_dns_docs.py +27 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_loader.py +31 -10
- composable_data_stack-0.15.1/tests/test_local_dagster_postgres_superset_dbt_profile.py +102 -0
- composable_data_stack-0.15.1/tests/test_local_haven_reference_profile.py +128 -0
- composable_data_stack-0.15.1/tests/test_local_kubernetes_example_profile.py +88 -0
- composable_data_stack-0.15.1/tests/test_local_loki_otel_collector_profile.py +115 -0
- composable_data_stack-0.15.1/tests/test_local_postgres_grafana_profile.py +95 -0
- composable_data_stack-0.15.1/tests/test_loki_module.py +275 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_main.py +2520 -753
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_main_extra.py +115 -36
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_main_extra2.py +236 -83
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_main_extra3.py +486 -143
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_markdown_mermaid_blocks.py +1 -5
- composable_data_stack-0.15.1/tests/test_marquez_lineage_sink_host.py +160 -0
- composable_data_stack-0.15.1/tests/test_marquez_module.py +114 -0
- composable_data_stack-0.15.1/tests/test_maturity.py +367 -0
- composable_data_stack-0.15.1/tests/test_module_contract_schema.py +98 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_module_isolation.py +52 -37
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_modules_no_committed_secrets.py +6 -2
- composable_data_stack-0.15.1/tests/test_mutation_tests_workflow.py +316 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_mvp_proof_workflow.py +3 -1
- composable_data_stack-0.15.1/tests/test_object_storage_backup_module.py +337 -0
- composable_data_stack-0.15.1/tests/test_object_storage_backup_runtime.py +590 -0
- composable_data_stack-0.15.1/tests/test_observability_runtime_smoke.py +413 -0
- composable_data_stack-0.15.1/tests/test_otel_collector_module.py +437 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_overlay.py +213 -59
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_planner.py +545 -83
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_planner_extra.py +194 -33
- composable_data_stack-0.15.1/tests/test_postgres_backup_restore_runtime.py +722 -0
- composable_data_stack-0.15.1/tests/test_postgres_operator_module.py +621 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_preflight.py +106 -31
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_preflight_extra.py +145 -8
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_preflight_rootless.py +36 -2
- composable_data_stack-0.15.1/tests/test_production_kubernetes_example_profile.py +128 -0
- composable_data_stack-0.15.1/tests/test_profile_matrix.py +39 -0
- composable_data_stack-0.15.1/tests/test_prompts_lint.py +971 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_publish_images_workflow.py +135 -23
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_python_version_alignment.py +15 -5
- composable_data_stack-0.15.1/tests/test_redact_secrets.py +122 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_release_workflow.py +12 -3
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_render_example_profile.py +81 -32
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_renderer.py +431 -48
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_report.py +126 -51
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_resolve_profile_path.py +25 -11
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_resolver.py +22 -5
- composable_data_stack-0.15.1/tests/test_restic_backup_restore_runtime.py +704 -0
- composable_data_stack-0.15.1/tests/test_restic_backup_wiring.py +314 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_reverse_proxy_contract.py +11 -2
- composable_data_stack-0.15.1/tests/test_reverse_proxy_routes.py +1405 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_runtime_confinement.py +33 -13
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_runtime_policy.py +173 -34
- composable_data_stack-0.15.1/tests/test_sample_data_parameter.py +236 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_secret_file_permissions.py +22 -5
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_secret_output_scanning.py +31 -10
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_secrets.py +29 -4
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_security.py +593 -101
- composable_data_stack-0.15.1/tests/test_service_mesh_istio_module.py +543 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_shipped_profiles.py +11 -3
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_smoke_example_profile.py +40 -11
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_standalone_module_profile.py +9 -2
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_state.py +28 -11
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_superset_hardening.py +8 -2
- composable_data_stack-0.15.1/tests/test_superset_oidc_helm.py +345 -0
- composable_data_stack-0.15.1/tests/test_superset_oidc_wiring.py +267 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_team_profile.py +34 -5
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_test_runner_timeout.py +3 -2
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_trivyignore.py +3 -5
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_up_runner.py +192 -26
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_update_check.py +18 -6
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_update_check_extra.py +37 -18
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_upstream_image_hardening.py +14 -4
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_utils.py +6 -2
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_validate_cli_sbom.py +33 -7
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_validator.py +493 -52
- composable_data_stack-0.15.1/tests/test_validator_extra.py +1869 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_vault_init_db.py +25 -8
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_vault_init_db_runtime.py +93 -24
- composable_data_stack-0.15.1/tests/test_vendored_llm_client.py +204 -0
- composable_data_stack-0.15.1/tests/test_workdir_cds_runtime.py +175 -0
- composable_data_stack-0.14.0/cli/getter.py +0 -1000
- composable_data_stack-0.14.0/cli/k8s_renderer.py +0 -1546
- composable_data_stack-0.14.0/cli/k8s_security.py +0 -138
- composable_data_stack-0.14.0/cli/utils.py +0 -35
- composable_data_stack-0.14.0/tests/test_dagster_entrypoint_backend_guard.py +0 -210
- composable_data_stack-0.14.0/tests/test_getter.py +0 -1336
- composable_data_stack-0.14.0/tests/test_k3d_e2e_profile.py +0 -52
- composable_data_stack-0.14.0/tests/test_k8s_renderer.py +0 -632
- composable_data_stack-0.14.0/tests/test_k8s_security.py +0 -155
- composable_data_stack-0.14.0/tests/test_keycloak_postgres_wiring.py +0 -213
- composable_data_stack-0.14.0/tests/test_reverse_proxy_routes.py +0 -786
- composable_data_stack-0.14.0/tests/test_validator_extra.py +0 -955
- composable_data_stack-0.14.0/tests/test_vendored_llm_client.py +0 -89
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/LICENSE +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/__init__.py +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/__init__.py +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/compatibility-registry.json +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/compatibility-registry.schema.json +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/contract.schema.json +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/dependency_links.txt +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/entry_points.txt +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/top_level.txt +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/setup.cfg +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_release_notes_support.py +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_release_version_check.py +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_tender_analytics.py +0 -0
- {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_tender_dashboard.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.1
|
|
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)
|
|
@@ -531,6 +532,8 @@ This runs `validate` → `plan` → `render` → `docker compose build` → `doc
|
|
|
531
532
|
|
|
532
533
|
`docker compose build` output is streamed to the terminal (grouped by image) and also persisted to a log file (default: `.cds/logs/up-<profile>-<timestamp>.log`; override with `--log-file`). `docker compose up` always runs detached: in the default mode its output goes only to the log file while the terminal shows the live state view; with `--detach` it is streamed to the terminal like the build output. Once the stack starts, `cds up` polls `docker compose ps` and redraws the same grouped/colored view as `cds state` until every service is healthy/running/exited or a timeout is hit (default 180s; override with `--timeout`; disable colored labels with `--no-color`). Container logs from that point on go only to the log file, not the terminal, since the terminal is showing the live state view. `cds up` exits `0` once the stack settles, or `1` on timeout or an unhealthy service.
|
|
533
534
|
|
|
535
|
+
The rendered `docker-compose.yml` is a standard Compose file, so the stack also runs without `cds` — `docker compose -f docker-compose.yml up -d` works on its own from the project root. See [docs/underlying-commands.md](docs/underlying-commands.md) for the full mapping of every `cds` lifecycle command (`render` / `up` / `state` / `down`, Compose and Helm) to its underlying `docker compose` / `helm` / `kubectl` commands.
|
|
536
|
+
|
|
534
537
|
Add `--detach` (or `-d`) to skip the live state view and return as soon as the stack starts, e.g. for scripting or CI:
|
|
535
538
|
|
|
536
539
|
```bash
|
|
@@ -817,6 +820,7 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
|
|
|
817
820
|
| `[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. |
|
|
818
821
|
| `[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)). |
|
|
819
822
|
| `[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. |
|
|
823
|
+
| `[E154] ... expects database engine ... but contract ref ... advertises engine ...` / `... advertises no "engine" in its spec` | A module's `consumes` entry declares `engineFrom: spec.config.<path>` (issue #682), but the bound provider's `sql-database` contract advertises a different `engine`, or none at all -- the consumer would speak the wrong wire protocol to the bound database (e.g. Keycloak's `KC_DB` vs the bound provider). The expected engine is read from the profile config at that path, falling back to the `configSchema` default when unset. | Set the consumer's config field (e.g. keycloak `metadataDatabase.driver`) to the provider's advertised engine, bind a provider advertising the expected engine, or have the provider set `engine` in its provide entry's contract spec. Drop `engineFrom` only if the consumer genuinely accepts any engine. |
|
|
820
824
|
|
|
821
825
|
| `[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. |
|
|
822
826
|
| `[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,13 +828,39 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
|
|
|
824
828
|
| `[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`. |
|
|
825
829
|
| `[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. |
|
|
826
830
|
| `[E135] ... Production backup policy "<class>" must declare restoreVerification` | A production backup policy has no `restoreVerification` block. | Add `restoreVerification.frequencyDays` to every production backup policy.|
|
|
827
|
-
| `[E136] ... Reverse-proxy route
|
|
831
|
+
| `[E136] ... Reverse-proxy route ...` (malformed or unknown contractRef / no resolvable http-service endpoint / backend address not a rendered compose service / entrypoint not declared by the proxy / undeclared certificate alias / route with neither host nor pathPrefix / duplicate route rule) | A `config.routes` entry on a reverse-proxy provider has a `contractRef` that is not `<module-id>.<contract-name>` or names a module that is missing, or that module does not provide the named `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 an entrypoint the proxy's `config.entryPoints` does not declare, references an undeclared certificate alias, names neither `host` nor `pathPrefix`, or duplicates an earlier route's rule on the same proxy (#581, #883). | Fix the route's `contractRef`/`certificate`/`host`/`pathPrefix`/`entryPoints` (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. |
|
|
832
|
+
| `[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}`. |
|
|
833
|
+
| `[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`. |
|
|
828
834
|
| `[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`. |
|
|
829
835
|
| `[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. |
|
|
836
|
+
| `[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`. |
|
|
830
837
|
| `[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`. |
|
|
831
838
|
| `[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. |
|
|
832
839
|
| `[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. |
|
|
840
|
+
| `[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). |
|
|
841
|
+
| `[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. |
|
|
842
|
+
| `[E152] ... waitFor gate of workload "<module>/<workload>" names host "<host>", which the module's default-deny NetworkPolicy does not allow egress to ...` | On the Helm target, a `waitFor` gate names a host that is neither a Service of the same module emitted by this render (a workload disabled via `enabledFrom` gets no Service), the endpoint of a provider the module consumes, nor the target of one of the module's reverse-proxy routes, and no custom `networkPolicy.egress` ipBlock rule or declared `implementation.kubernetes.egress` entry covers the host:port, so the default-deny NetworkPolicy blocks the init container's connection and it would only hang until its timeout (#1031, #1033, #1023). | Bind the host's module through a `consumes` contract, route to it via `config.routes`, gate on a same-module `${k8s.service.*}` Service, declare the destination under `implementation.kubernetes.egress` (an IP/CIDR with the gate's port), or add a custom `networkPolicy.egress` ipBlock rule covering the host and the gate's port (a DNS-name host cannot be expressed in a NetworkPolicy and stays denied). |
|
|
843
|
+
| `[E153] ... External egress entry of module \"<module>\" at ... is invalid ...` | A declared external egress entry under `implementation.kubernetes.egress` (or `workloads.<name>.egress`) is malformed -- plan-file bypass of the module schema (#1023): exactly one of `host` (IP literal; DNS names are rejected) / `cidr` (network form, no host bits), `except` CIDRs inside the `cidr`, `port`/`ports` within 1-65535, `protocol` TCP/UDP/SCTP, and all `${config.*}` expressions must resolve. | Fix the entry shape in the module (or the saved plan); see `docs/kubernetes.md` ("Declaring external egress"). |
|
|
844
|
+
| `[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. |
|
|
845
|
+
| `[W129] ... Reverse-proxy route ... cannot be derived as a NetworkPolicy edge ...` | On the Helm target, a `config.routes` entry of a reverse-proxy module has a `contractRef` naming an unknown module, a module without the named `http-service` provide, or a target whose http-service contract carries no usable port evidence (absent, or outside 1-65535, and the target advertises no service ports), so no port-scoped NetworkPolicy edge can be derived and the proxy's default-deny policy keeps the target unreachable (#1033, #883). | Fix the route's `contractRef` (`<module-id>.<contract-name>` naming an existing http-service provide) and declare a valid `port` in that contract, or add a custom `networkPolicy.egress` rule for the target. |
|
|
846
|
+
| `[W130] ... renders resources for operator "<name>" (CRD <crd>), which CDS does not install itself ...` | On the Helm target, a module declares an out-of-band operator under `implementation.kubernetes.operators` (e.g. the CloudNativePG operator of `modules-experimental/warehouse/postgres-operator`): CDS renders the CRs but never installs the operator, so the chart only reconciles once the operator is on the cluster (#768). The warning names the install command; the same note is in the chart's `NOTES.txt`. | Run the install command from the warning (e.g. `helm repo add cnpg https://cloudnative-pg.github.io/charts && helm upgrade --install cnpg --namespace cnpg-system --create-namespace cnpg/cloudnative-pg`), verify with `kubectl get crd clusters.postgresql.cnpg.io`, then re-run; see `docs/kubernetes.md` ("Operator-managed modules"). |
|
|
847
|
+
| `[W131] ... provides open-data-provider contract ... with classification ... which is outside the recommended DCAT-AP-NL data-theme vocabulary ...` | A module provides an `open-data-provider` contract whose `classification` is free text outside the recommended standard vocabulary (the DCAT-AP-NL data themes: AGRI, ECON, EDUC, ENER, ENVI, GEOJ, GOVE, HEAH, INTR, JUST, REGI, SOCI, TRAN, or the full EU data-theme authority URI), so the dataset is harder to compare and find across domains (issue #845). The value still renders; this is a warning only. | Set `classification` to a theme from the DCAT-AP-NL data-theme vocabulary (e.g. in the postgres module's `config.dataProvider.classification`); see `shared/contracts/open-data-provider.yaml` and `docs/fds-alignment.md`. |
|
|
848
|
+
| `[E155] ... needs operator "<name>", but its CRD "<crd>" is not present on the cluster ...` | `cds up --target helm` checked the cluster (`kubectl get crd <crd>`) and the operator a module depends on is confirmed absent, so `helm upgrade --install` would fail with "no matches for kind" (#768). | Install the operator with the command named in the error (e.g. `helm upgrade --install cnpg --namespace cnpg-system --create-namespace cnpg/cloudnative-pg` after `helm repo add cnpg https://cloudnative-pg.github.io/charts`), then re-run `cds up --target helm`. |
|
|
849
|
+
| `[E156] ... forEach marker of manifest ... resolved ... to nothing / to a <type>, but fan-out needs a list / produced the duplicate metadata.name ...` | A raw manifest under `implementation.kubernetes.manifests` declares `forEach: <expr>`, but the expression does not resolve to a list in the plan, or two fanned-out copies resolve to the same `metadata.name` (they would collide in the release namespace and in the chart's template file names) (issue #1170). | Point `forEach` at a list-valued config path (e.g. `config.services`), and reference `${item...}` in the manifest's `metadata.name` so every fanned-out copy is uniquely named. |
|
|
850
|
+
| `[E157] ... Duplicate datasetId ...` | `spec.dataSharing.datasets` declares the same `datasetId` more than once; each disclosed dataset must be declared once (issue #844). | Remove the duplicate entry, or give each entry a distinct `datasetId`. |
|
|
851
|
+
| `[E158] ... Dataset ... names module ... which is not an enabled module instance ...` | A `spec.dataSharing.datasets` entry's `module` names a module id that is not an enabled module instance in the profile (issue #844). | Point `module` at an existing, enabled module instance id from `spec.modules`, or drop the field. |
|
|
852
|
+
| `[E159] ... enrolls service "<name>" in its service-mesh config (config.services[<i>].name), but no module instance with that id exists in the profile ...` | At plan time, a module providing the `service-mesh` contract has a `config.services[].name` that matches no module instance id of the profile (a typo such as `warehose` for `warehouse`, or a disabled module) (issue #1179). The rendered per-service `AuthorizationPolicy` would select zero pods via the `cds.dev/module` label -- a silent no-op allow policy. | Fix the enrollment to name an existing module instance id (the error lists the planned ids), add the module with that id, or remove the entry from `config.services`; enable a disabled module you want to enroll. |
|
|
853
|
+
| `[E161] ... Unknown module id '<id>' ...` / `... addresses/traverses a non-scalar value ...` / `... matches a resolved secret value ...` | A persisted `cds config set module.<id>.config.<path> <value>` override in `.cds/config.json` or the shared `cds.shared.json` (issue #1220) names a module id that is not in the resolved profile, points at a dict/list instead of a scalar leaf, or tries to store a resolved secret value. The same code is emitted at load time when a hand-edited `config.json` breaks, so `validate`/`plan`/`render` fail loudly instead of silently ignoring the override. | Fix the id or path (only `module.<id>.config.<scalar leaf>` keys are accepted), remove the override with `cds config unset [--shared] module.<id>.config.<path>`, or store a `secrets.<alias>` reference instead of a secret value; see [docs/cli-reference.md](docs/cli-reference.md#local-module-config-overrides-moduleidconfigpath). |
|
|
854
|
+
| `[W132] ... enrolls service "<name>" in its service-mesh config, but the module renders no Kubernetes workload pods ...` | On the Helm target, a known enrollment names a module with no rendered workloads, such as a manifest-only module or one whose workloads are all disabled by `enabledFrom` (issue #1198). If enabled, the per-service `AuthorizationPolicy` would select zero pods via the `cds.dev/module` label. | Enroll a module that renders at least one Kubernetes workload, or enable one of its gated workloads; remove the enrollment if no pod workload is intended. |
|
|
833
855
|
| `[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`. |
|
|
856
|
+
| `[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`. |
|
|
857
|
+
| `[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`. |
|
|
858
|
+
| `[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`). |
|
|
859
|
+
| `[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. |
|
|
860
|
+
| `[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). |
|
|
861
|
+
| `[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. |
|
|
862
|
+
| `[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). |
|
|
863
|
+
| `[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. |
|
|
834
864
|
|
|
835
865
|
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.
|
|
836
866
|
|
|
@@ -971,6 +1001,8 @@ Please read these first:
|
|
|
971
1001
|
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
972
1002
|
- [docs/maintainer-merge-policy.md](docs/maintainer-merge-policy.md)
|
|
973
1003
|
- [SECURITY.md](SECURITY.md) — how to report a vulnerability
|
|
1004
|
+
- [ACCESSIBILITY.md](ACCESSIBILITY.md) — our accessibility commitment and how
|
|
1005
|
+
to report a barrier
|
|
974
1006
|
- [LEGAL.md](LEGAL.md) — license, code of conduct, security, support, and
|
|
975
1007
|
regulatory-compliance documentation index
|
|
976
1008
|
- [CHANGELOG.md](CHANGELOG.md)
|
|
@@ -985,11 +1017,15 @@ Development helper tools are located in the gitignored `tools/` directory. See
|
|
|
985
1017
|
|
|
986
1018
|
- [Quickstart](README.md#-quickstart) — get running in 5 minutes
|
|
987
1019
|
- [CLI Reference](docs/cli-reference.md) — full command/flag table and `[profile]` resolution rules
|
|
1020
|
+
- [Underlying Commands](docs/underlying-commands.md) — the exact `docker compose` / `helm` / `kubectl` commands behind `cds render` / `up` / `state` / `down`, for running the stack without the `cds` CLI
|
|
988
1021
|
- [How CDS Works](docs/how-it-works.md) — C4 + sequence diagrams of the validate → plan → render engine
|
|
989
|
-
- [Kubernetes Target](docs/kubernetes.md): Helm rendering and isolated local k3s workflow
|
|
1022
|
+
- [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)
|
|
990
1023
|
- [From Docker Compose to CDS Profile](docs/from-docker-to-cds-profile.md) — complete transformation guide
|
|
991
1024
|
- [Architecture](docs/architecture.md) — design and core concepts
|
|
992
1025
|
- [Modules](docs/modules.md) — how to structure reusable components
|
|
1026
|
+
- [Repository Layout](docs/repo-layout.md) — best practice for keeping platform assets (modules, profiles) separate from project code (Dagster pipelines, dbt/dlt, database scripts)
|
|
1027
|
+
- [Workload Runtime API](docs/runtime-api.md) — `cds-runtime`: named database connections for Python workloads
|
|
1028
|
+
- [Production Hardening Checklist](docs/production-hardening-checklist.md) — control-to-owner mapping and the operator verification procedure for a production profile
|
|
993
1029
|
- [Roadmap](docs/roadmap.md) — planned features and milestones
|
|
994
1030
|
|
|
995
1031
|
---
|
|
@@ -494,6 +494,8 @@ This runs `validate` → `plan` → `render` → `docker compose build` → `doc
|
|
|
494
494
|
|
|
495
495
|
`docker compose build` output is streamed to the terminal (grouped by image) and also persisted to a log file (default: `.cds/logs/up-<profile>-<timestamp>.log`; override with `--log-file`). `docker compose up` always runs detached: in the default mode its output goes only to the log file while the terminal shows the live state view; with `--detach` it is streamed to the terminal like the build output. Once the stack starts, `cds up` polls `docker compose ps` and redraws the same grouped/colored view as `cds state` until every service is healthy/running/exited or a timeout is hit (default 180s; override with `--timeout`; disable colored labels with `--no-color`). Container logs from that point on go only to the log file, not the terminal, since the terminal is showing the live state view. `cds up` exits `0` once the stack settles, or `1` on timeout or an unhealthy service.
|
|
496
496
|
|
|
497
|
+
The rendered `docker-compose.yml` is a standard Compose file, so the stack also runs without `cds` — `docker compose -f docker-compose.yml up -d` works on its own from the project root. See [docs/underlying-commands.md](docs/underlying-commands.md) for the full mapping of every `cds` lifecycle command (`render` / `up` / `state` / `down`, Compose and Helm) to its underlying `docker compose` / `helm` / `kubectl` commands.
|
|
498
|
+
|
|
497
499
|
Add `--detach` (or `-d`) to skip the live state view and return as soon as the stack starts, e.g. for scripting or CI:
|
|
498
500
|
|
|
499
501
|
```bash
|
|
@@ -780,6 +782,7 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
|
|
|
780
782
|
| `[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. |
|
|
781
783
|
| `[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
784
|
| `[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. |
|
|
785
|
+
| `[E154] ... expects database engine ... but contract ref ... advertises engine ...` / `... advertises no "engine" in its spec` | A module's `consumes` entry declares `engineFrom: spec.config.<path>` (issue #682), but the bound provider's `sql-database` contract advertises a different `engine`, or none at all -- the consumer would speak the wrong wire protocol to the bound database (e.g. Keycloak's `KC_DB` vs the bound provider). The expected engine is read from the profile config at that path, falling back to the `configSchema` default when unset. | Set the consumer's config field (e.g. keycloak `metadataDatabase.driver`) to the provider's advertised engine, bind a provider advertising the expected engine, or have the provider set `engine` in its provide entry's contract spec. Drop `engineFrom` only if the consumer genuinely accepts any engine. |
|
|
783
786
|
|
|
784
787
|
| `[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
788
|
| `[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. |
|
|
@@ -787,13 +790,39 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
|
|
|
787
790
|
| `[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
791
|
| `[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
792
|
| `[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
|
|
793
|
+
| `[E136] ... Reverse-proxy route ...` (malformed or unknown contractRef / no resolvable http-service endpoint / backend address not a rendered compose service / entrypoint not declared by the proxy / undeclared certificate alias / route with neither host nor pathPrefix / duplicate route rule) | A `config.routes` entry on a reverse-proxy provider has a `contractRef` that is not `<module-id>.<contract-name>` or names a module that is missing, or that module does not provide the named `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 an entrypoint the proxy's `config.entryPoints` does not declare, references an undeclared certificate alias, names neither `host` nor `pathPrefix`, or duplicates an earlier route's rule on the same proxy (#581, #883). | Fix the route's `contractRef`/`certificate`/`host`/`pathPrefix`/`entryPoints` (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. |
|
|
794
|
+
| `[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}`. |
|
|
795
|
+
| `[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`. |
|
|
791
796
|
| `[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`. |
|
|
792
797
|
| `[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. |
|
|
798
|
+
| `[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`. |
|
|
793
799
|
| `[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`. |
|
|
794
800
|
| `[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. |
|
|
795
801
|
| `[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. |
|
|
802
|
+
| `[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). |
|
|
803
|
+
| `[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. |
|
|
804
|
+
| `[E152] ... waitFor gate of workload "<module>/<workload>" names host "<host>", which the module's default-deny NetworkPolicy does not allow egress to ...` | On the Helm target, a `waitFor` gate names a host that is neither a Service of the same module emitted by this render (a workload disabled via `enabledFrom` gets no Service), the endpoint of a provider the module consumes, nor the target of one of the module's reverse-proxy routes, and no custom `networkPolicy.egress` ipBlock rule or declared `implementation.kubernetes.egress` entry covers the host:port, so the default-deny NetworkPolicy blocks the init container's connection and it would only hang until its timeout (#1031, #1033, #1023). | Bind the host's module through a `consumes` contract, route to it via `config.routes`, gate on a same-module `${k8s.service.*}` Service, declare the destination under `implementation.kubernetes.egress` (an IP/CIDR with the gate's port), or add a custom `networkPolicy.egress` ipBlock rule covering the host and the gate's port (a DNS-name host cannot be expressed in a NetworkPolicy and stays denied). |
|
|
805
|
+
| `[E153] ... External egress entry of module \"<module>\" at ... is invalid ...` | A declared external egress entry under `implementation.kubernetes.egress` (or `workloads.<name>.egress`) is malformed -- plan-file bypass of the module schema (#1023): exactly one of `host` (IP literal; DNS names are rejected) / `cidr` (network form, no host bits), `except` CIDRs inside the `cidr`, `port`/`ports` within 1-65535, `protocol` TCP/UDP/SCTP, and all `${config.*}` expressions must resolve. | Fix the entry shape in the module (or the saved plan); see `docs/kubernetes.md` ("Declaring external egress"). |
|
|
806
|
+
| `[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. |
|
|
807
|
+
| `[W129] ... Reverse-proxy route ... cannot be derived as a NetworkPolicy edge ...` | On the Helm target, a `config.routes` entry of a reverse-proxy module has a `contractRef` naming an unknown module, a module without the named `http-service` provide, or a target whose http-service contract carries no usable port evidence (absent, or outside 1-65535, and the target advertises no service ports), so no port-scoped NetworkPolicy edge can be derived and the proxy's default-deny policy keeps the target unreachable (#1033, #883). | Fix the route's `contractRef` (`<module-id>.<contract-name>` naming an existing http-service provide) and declare a valid `port` in that contract, or add a custom `networkPolicy.egress` rule for the target. |
|
|
808
|
+
| `[W130] ... renders resources for operator "<name>" (CRD <crd>), which CDS does not install itself ...` | On the Helm target, a module declares an out-of-band operator under `implementation.kubernetes.operators` (e.g. the CloudNativePG operator of `modules-experimental/warehouse/postgres-operator`): CDS renders the CRs but never installs the operator, so the chart only reconciles once the operator is on the cluster (#768). The warning names the install command; the same note is in the chart's `NOTES.txt`. | Run the install command from the warning (e.g. `helm repo add cnpg https://cloudnative-pg.github.io/charts && helm upgrade --install cnpg --namespace cnpg-system --create-namespace cnpg/cloudnative-pg`), verify with `kubectl get crd clusters.postgresql.cnpg.io`, then re-run; see `docs/kubernetes.md` ("Operator-managed modules"). |
|
|
809
|
+
| `[W131] ... provides open-data-provider contract ... with classification ... which is outside the recommended DCAT-AP-NL data-theme vocabulary ...` | A module provides an `open-data-provider` contract whose `classification` is free text outside the recommended standard vocabulary (the DCAT-AP-NL data themes: AGRI, ECON, EDUC, ENER, ENVI, GEOJ, GOVE, HEAH, INTR, JUST, REGI, SOCI, TRAN, or the full EU data-theme authority URI), so the dataset is harder to compare and find across domains (issue #845). The value still renders; this is a warning only. | Set `classification` to a theme from the DCAT-AP-NL data-theme vocabulary (e.g. in the postgres module's `config.dataProvider.classification`); see `shared/contracts/open-data-provider.yaml` and `docs/fds-alignment.md`. |
|
|
810
|
+
| `[E155] ... needs operator "<name>", but its CRD "<crd>" is not present on the cluster ...` | `cds up --target helm` checked the cluster (`kubectl get crd <crd>`) and the operator a module depends on is confirmed absent, so `helm upgrade --install` would fail with "no matches for kind" (#768). | Install the operator with the command named in the error (e.g. `helm upgrade --install cnpg --namespace cnpg-system --create-namespace cnpg/cloudnative-pg` after `helm repo add cnpg https://cloudnative-pg.github.io/charts`), then re-run `cds up --target helm`. |
|
|
811
|
+
| `[E156] ... forEach marker of manifest ... resolved ... to nothing / to a <type>, but fan-out needs a list / produced the duplicate metadata.name ...` | A raw manifest under `implementation.kubernetes.manifests` declares `forEach: <expr>`, but the expression does not resolve to a list in the plan, or two fanned-out copies resolve to the same `metadata.name` (they would collide in the release namespace and in the chart's template file names) (issue #1170). | Point `forEach` at a list-valued config path (e.g. `config.services`), and reference `${item...}` in the manifest's `metadata.name` so every fanned-out copy is uniquely named. |
|
|
812
|
+
| `[E157] ... Duplicate datasetId ...` | `spec.dataSharing.datasets` declares the same `datasetId` more than once; each disclosed dataset must be declared once (issue #844). | Remove the duplicate entry, or give each entry a distinct `datasetId`. |
|
|
813
|
+
| `[E158] ... Dataset ... names module ... which is not an enabled module instance ...` | A `spec.dataSharing.datasets` entry's `module` names a module id that is not an enabled module instance in the profile (issue #844). | Point `module` at an existing, enabled module instance id from `spec.modules`, or drop the field. |
|
|
814
|
+
| `[E159] ... enrolls service "<name>" in its service-mesh config (config.services[<i>].name), but no module instance with that id exists in the profile ...` | At plan time, a module providing the `service-mesh` contract has a `config.services[].name` that matches no module instance id of the profile (a typo such as `warehose` for `warehouse`, or a disabled module) (issue #1179). The rendered per-service `AuthorizationPolicy` would select zero pods via the `cds.dev/module` label -- a silent no-op allow policy. | Fix the enrollment to name an existing module instance id (the error lists the planned ids), add the module with that id, or remove the entry from `config.services`; enable a disabled module you want to enroll. |
|
|
815
|
+
| `[E161] ... Unknown module id '<id>' ...` / `... addresses/traverses a non-scalar value ...` / `... matches a resolved secret value ...` | A persisted `cds config set module.<id>.config.<path> <value>` override in `.cds/config.json` or the shared `cds.shared.json` (issue #1220) names a module id that is not in the resolved profile, points at a dict/list instead of a scalar leaf, or tries to store a resolved secret value. The same code is emitted at load time when a hand-edited `config.json` breaks, so `validate`/`plan`/`render` fail loudly instead of silently ignoring the override. | Fix the id or path (only `module.<id>.config.<scalar leaf>` keys are accepted), remove the override with `cds config unset [--shared] module.<id>.config.<path>`, or store a `secrets.<alias>` reference instead of a secret value; see [docs/cli-reference.md](docs/cli-reference.md#local-module-config-overrides-moduleidconfigpath). |
|
|
816
|
+
| `[W132] ... enrolls service "<name>" in its service-mesh config, but the module renders no Kubernetes workload pods ...` | On the Helm target, a known enrollment names a module with no rendered workloads, such as a manifest-only module or one whose workloads are all disabled by `enabledFrom` (issue #1198). If enabled, the per-service `AuthorizationPolicy` would select zero pods via the `cds.dev/module` label. | Enroll a module that renders at least one Kubernetes workload, or enable one of its gated workloads; remove the enrollment if no pod workload is intended. |
|
|
796
817
|
| `[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`. |
|
|
818
|
+
| `[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`. |
|
|
819
|
+
| `[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`. |
|
|
820
|
+
| `[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`). |
|
|
821
|
+
| `[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. |
|
|
822
|
+
| `[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). |
|
|
823
|
+
| `[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. |
|
|
824
|
+
| `[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). |
|
|
825
|
+
| `[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. |
|
|
797
826
|
|
|
798
827
|
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.
|
|
799
828
|
|
|
@@ -934,6 +963,8 @@ Please read these first:
|
|
|
934
963
|
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
935
964
|
- [docs/maintainer-merge-policy.md](docs/maintainer-merge-policy.md)
|
|
936
965
|
- [SECURITY.md](SECURITY.md) — how to report a vulnerability
|
|
966
|
+
- [ACCESSIBILITY.md](ACCESSIBILITY.md) — our accessibility commitment and how
|
|
967
|
+
to report a barrier
|
|
937
968
|
- [LEGAL.md](LEGAL.md) — license, code of conduct, security, support, and
|
|
938
969
|
regulatory-compliance documentation index
|
|
939
970
|
- [CHANGELOG.md](CHANGELOG.md)
|
|
@@ -948,11 +979,15 @@ Development helper tools are located in the gitignored `tools/` directory. See
|
|
|
948
979
|
|
|
949
980
|
- [Quickstart](README.md#-quickstart) — get running in 5 minutes
|
|
950
981
|
- [CLI Reference](docs/cli-reference.md) — full command/flag table and `[profile]` resolution rules
|
|
982
|
+
- [Underlying Commands](docs/underlying-commands.md) — the exact `docker compose` / `helm` / `kubectl` commands behind `cds render` / `up` / `state` / `down`, for running the stack without the `cds` CLI
|
|
951
983
|
- [How CDS Works](docs/how-it-works.md) — C4 + sequence diagrams of the validate → plan → render engine
|
|
952
|
-
- [Kubernetes Target](docs/kubernetes.md): Helm rendering and isolated local k3s workflow
|
|
984
|
+
- [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)
|
|
953
985
|
- [From Docker Compose to CDS Profile](docs/from-docker-to-cds-profile.md) — complete transformation guide
|
|
954
986
|
- [Architecture](docs/architecture.md) — design and core concepts
|
|
955
987
|
- [Modules](docs/modules.md) — how to structure reusable components
|
|
988
|
+
- [Repository Layout](docs/repo-layout.md) — best practice for keeping platform assets (modules, profiles) separate from project code (Dagster pipelines, dbt/dlt, database scripts)
|
|
989
|
+
- [Workload Runtime API](docs/runtime-api.md) — `cds-runtime`: named database connections for Python workloads
|
|
990
|
+
- [Production Hardening Checklist](docs/production-hardening-checklist.md) — control-to-owner mapping and the operator verification procedure for a production profile
|
|
956
991
|
- [Roadmap](docs/roadmap.md) — planned features and milestones
|
|
957
992
|
|
|
958
993
|
---
|
|
@@ -16,6 +16,7 @@ recorded (e.g. "CDS_DB_PASSWORD"), the same `${CDS_*}` placeholder
|
|
|
16
16
|
convention used by the planner/renderer (see `cli/secrets.py`) -- never
|
|
17
17
|
secret values.
|
|
18
18
|
"""
|
|
19
|
+
|
|
19
20
|
from __future__ import annotations
|
|
20
21
|
|
|
21
22
|
import json
|
|
@@ -25,6 +26,8 @@ from datetime import UTC, datetime
|
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
from typing import Any
|
|
27
28
|
|
|
29
|
+
from .security_common import SymlinkedWriteError, ensure_no_symlinked_parent
|
|
30
|
+
|
|
28
31
|
_LOG_FILE = Path(".cds") / "audit-log.jsonl"
|
|
29
32
|
|
|
30
33
|
# Set to disable the audit log without touching project config, e.g. for a
|
|
@@ -36,13 +39,12 @@ _TRUTHY = {"1", "true", "yes", "on"}
|
|
|
36
39
|
def audit_log_path(project_root: Path | None = None) -> Path:
|
|
37
40
|
"""Return the path audit entries are appended to for `project_root`.
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
profiles checkout). Tracked by #822.
|
|
42
|
+
`cli.main` call sites pass the profile-anchored project root
|
|
43
|
+
(`resolve_project_root(profile_path)` -- the same resolution that
|
|
44
|
+
decides where rendered Compose output goes), so entries for a given
|
|
45
|
+
profile consistently land in one `.cds/audit-log.jsonl` regardless of
|
|
46
|
+
the cwd `cds` was invoked from (#822). With no `project_root` (direct
|
|
47
|
+
programmatic use), this falls back to `Path.cwd()`.
|
|
46
48
|
"""
|
|
47
49
|
root = (project_root or Path.cwd()).expanduser().resolve()
|
|
48
50
|
return root / _LOG_FILE
|
|
@@ -141,6 +143,13 @@ def record_entry(
|
|
|
141
143
|
entry["details"] = details
|
|
142
144
|
|
|
143
145
|
try:
|
|
146
|
+
# Never append through a symlinked .cds directory: mkdir() and
|
|
147
|
+
# open() both follow the link, so the entry would land outside the
|
|
148
|
+
# project tree (issue #1225, same write-through-symlink class as
|
|
149
|
+
# the lifecycle-log guard in cli/main.py and the tracking manifest
|
|
150
|
+
# in cli/getter.py). audit_log_path() always returns the default
|
|
151
|
+
# .cds/audit-log.jsonl, so this guard covers the default path only.
|
|
152
|
+
ensure_no_symlinked_parent(log_path, what="audit log directory")
|
|
144
153
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
145
154
|
if log_path.is_symlink():
|
|
146
155
|
# Never follow/overwrite-through a symlink planted at the log
|
|
@@ -148,8 +157,11 @@ def record_entry(
|
|
|
148
157
|
log_path.unlink()
|
|
149
158
|
with open(log_path, "a", encoding="utf-8") as f:
|
|
150
159
|
f.write(json.dumps(entry, sort_keys=True) + "\n")
|
|
151
|
-
except OSError as exc:
|
|
152
|
-
print(
|
|
160
|
+
except (OSError, SymlinkedWriteError) as exc:
|
|
161
|
+
print(
|
|
162
|
+
f"WARNING Could not write audit log entry to {log_path}: {exc}",
|
|
163
|
+
file=sys.stderr,
|
|
164
|
+
)
|
|
153
165
|
return None
|
|
154
166
|
|
|
155
167
|
return log_path
|
|
@@ -186,12 +186,18 @@ def compose_profile(
|
|
|
186
186
|
)
|
|
187
187
|
return None, diagnostics
|
|
188
188
|
|
|
189
|
-
providers, existing_ids, provider_diags = _collect_provided_contracts(
|
|
189
|
+
providers, existing_ids, provider_diags = _collect_provided_contracts(
|
|
190
|
+
profile, profile_dir, modules_root
|
|
191
|
+
)
|
|
190
192
|
diagnostics.extend(provider_diags)
|
|
191
193
|
if any(d.level == "error" for d in diagnostics):
|
|
192
194
|
return None, diagnostics
|
|
193
195
|
|
|
194
|
-
metadata =
|
|
196
|
+
metadata = (
|
|
197
|
+
module_def.get("metadata")
|
|
198
|
+
if isinstance(module_def.get("metadata"), dict)
|
|
199
|
+
else {}
|
|
200
|
+
)
|
|
195
201
|
resolved_id = instance_id or metadata.get("name")
|
|
196
202
|
if not isinstance(resolved_id, str) or not resolved_id.strip():
|
|
197
203
|
diagnostics.append(
|
|
@@ -275,7 +281,9 @@ def compose_profile(
|
|
|
275
281
|
)
|
|
276
282
|
continue
|
|
277
283
|
else:
|
|
278
|
-
candidates = sorted(
|
|
284
|
+
candidates = sorted(
|
|
285
|
+
ref for ref, kind in providers.items() if kind == contract_kind
|
|
286
|
+
)
|
|
279
287
|
if len(candidates) == 1:
|
|
280
288
|
chosen = candidates[0]
|
|
281
289
|
elif not candidates:
|
|
@@ -321,7 +329,9 @@ def compose_profile(
|
|
|
321
329
|
if any(d.level == "error" for d in diagnostics):
|
|
322
330
|
return None, diagnostics
|
|
323
331
|
|
|
324
|
-
secret_values = deepcopy(
|
|
332
|
+
secret_values = deepcopy(
|
|
333
|
+
(profile.get("spec", {}).get("secrets") or {}).get("values") or {}
|
|
334
|
+
)
|
|
325
335
|
for alias in sorted(set(_iter_secret_aliases(config))):
|
|
326
336
|
if alias in secret_values:
|
|
327
337
|
continue
|
|
@@ -359,7 +369,9 @@ def compose_profile(
|
|
|
359
369
|
merged["spec"]["modules"].append(new_instance)
|
|
360
370
|
|
|
361
371
|
if secret_values:
|
|
362
|
-
secrets_block = merged["spec"].setdefault(
|
|
372
|
+
secrets_block = merged["spec"].setdefault(
|
|
373
|
+
"secrets", {"provider": {"type": "env"}}
|
|
374
|
+
)
|
|
363
375
|
secrets_block.setdefault("provider", {"type": "env"})
|
|
364
376
|
secrets_block["values"] = secret_values
|
|
365
377
|
|
|
@@ -4,6 +4,7 @@ A profile declares a default under `spec.security.runtime.confinement`; a
|
|
|
4
4
|
module instance may override individual keys under `security.confinement`.
|
|
5
5
|
The effective value is rendered into each service's `security_opt`.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import os
|
|
@@ -49,7 +50,9 @@ def effective_confinement(
|
|
|
49
50
|
defaults: dict[str, Any] | None, override: dict[str, Any] | None
|
|
50
51
|
) -> dict[str, Any]:
|
|
51
52
|
"""Profile defaults with the module instance's keys layered on top."""
|
|
52
|
-
merged = {
|
|
53
|
+
merged = {
|
|
54
|
+
key: value for key, value in (defaults or {}).items() if key in CONFINEMENT_KEYS
|
|
55
|
+
}
|
|
53
56
|
for key, value in (override or {}).items():
|
|
54
57
|
if key in CONFINEMENT_KEYS:
|
|
55
58
|
merged[key] = value
|
|
@@ -67,10 +70,12 @@ def validate_confinement(
|
|
|
67
70
|
diagnostics: list[Diagnostic] = []
|
|
68
71
|
for key in block:
|
|
69
72
|
if key not in CONFINEMENT_KEYS:
|
|
70
|
-
diagnostics.append(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
diagnostics.append(
|
|
74
|
+
_error(
|
|
75
|
+
f"Unknown confinement key '{key}'; supported keys: {', '.join(CONFINEMENT_KEYS)}.",
|
|
76
|
+
f"{path}.{key}",
|
|
77
|
+
)
|
|
78
|
+
)
|
|
74
79
|
|
|
75
80
|
seccomp = block.get("seccomp")
|
|
76
81
|
if seccomp is not None:
|
|
@@ -78,13 +83,18 @@ def validate_confinement(
|
|
|
78
83
|
apparmor = block.get("apparmor")
|
|
79
84
|
if apparmor is not None and not (
|
|
80
85
|
isinstance(apparmor, str)
|
|
81
|
-
and (
|
|
86
|
+
and (
|
|
87
|
+
apparmor in {"runtime/default", "unconfined"}
|
|
88
|
+
or _APPARMOR_NAME_RE.match(apparmor)
|
|
89
|
+
)
|
|
82
90
|
):
|
|
83
|
-
diagnostics.append(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
diagnostics.append(
|
|
92
|
+
_error(
|
|
93
|
+
"apparmor must be 'runtime/default', 'unconfined' or the name of a profile "
|
|
94
|
+
"already loaded on the host (letters, digits, '.', '_', '-', '/').",
|
|
95
|
+
f"{path}.apparmor",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
88
98
|
selinux = block.get("selinux")
|
|
89
99
|
if selinux is not None:
|
|
90
100
|
diagnostics.extend(_validate_selinux(selinux, f"{path}.selinux"))
|
|
@@ -95,16 +105,21 @@ def _validate_seccomp(value: Any, path: str, base_dir: Path | None) -> list[Diag
|
|
|
95
105
|
if value in {"default", "unconfined"}:
|
|
96
106
|
return []
|
|
97
107
|
if not isinstance(value, str) or not value.startswith("file:") or len(value) == 5:
|
|
98
|
-
return [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
return [
|
|
109
|
+
_error(
|
|
110
|
+
"seccomp must be 'default', 'unconfined' or 'file:<path to a JSON profile "
|
|
111
|
+
"relative to the profile directory>'.",
|
|
112
|
+
path,
|
|
113
|
+
)
|
|
114
|
+
]
|
|
103
115
|
relative = value[5:]
|
|
104
116
|
if base_dir is None:
|
|
105
|
-
return [
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
return [
|
|
118
|
+
_error(
|
|
119
|
+
"seccomp file profiles need a known profile directory to resolve against.",
|
|
120
|
+
path,
|
|
121
|
+
)
|
|
122
|
+
]
|
|
108
123
|
# Reject rooted and drive-qualified paths explicitly: they are not
|
|
109
124
|
# "absolute" to pathlib on every platform (e.g. "/x" on Windows).
|
|
110
125
|
if (
|
|
@@ -113,14 +128,17 @@ def _validate_seccomp(value: Any, path: str, base_dir: Path | None) -> list[Diag
|
|
|
113
128
|
or _DRIVE_RE.match(relative)
|
|
114
129
|
or ".." in Path(relative).parts
|
|
115
130
|
):
|
|
116
|
-
return [
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
return [
|
|
132
|
+
_error(
|
|
133
|
+
"seccomp file profiles must be relative paths inside the profile directory.",
|
|
134
|
+
path,
|
|
135
|
+
)
|
|
136
|
+
]
|
|
119
137
|
target = (base_dir / relative).resolve()
|
|
120
138
|
if not target.is_relative_to(base_dir.resolve()):
|
|
121
|
-
return [
|
|
122
|
-
"seccomp file profile resolves outside the profile directory.", path
|
|
123
|
-
|
|
139
|
+
return [
|
|
140
|
+
_error("seccomp file profile resolves outside the profile directory.", path)
|
|
141
|
+
]
|
|
124
142
|
if not target.is_file():
|
|
125
143
|
return [_error(f"seccomp profile '{relative}' does not exist.", path)]
|
|
126
144
|
return []
|
|
@@ -129,26 +147,36 @@ def _validate_seccomp(value: Any, path: str, base_dir: Path | None) -> list[Diag
|
|
|
129
147
|
def _validate_selinux(value: Any, path: str) -> list[Diagnostic]:
|
|
130
148
|
if value == "disable":
|
|
131
149
|
return []
|
|
132
|
-
if
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
if (
|
|
151
|
+
not isinstance(value, list)
|
|
152
|
+
or not value
|
|
153
|
+
or not all(isinstance(v, str) for v in value)
|
|
154
|
+
):
|
|
155
|
+
return [
|
|
156
|
+
_error(
|
|
157
|
+
"selinux must be 'disable' or a non-empty list of labels such as "
|
|
158
|
+
"'type:container_t' or 'level:s0:c100,c200'.",
|
|
159
|
+
path,
|
|
160
|
+
)
|
|
161
|
+
]
|
|
138
162
|
bad = [label for label in value if not _SELINUX_LABEL_RE.match(label)]
|
|
139
163
|
if bad:
|
|
140
|
-
return [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
164
|
+
return [
|
|
165
|
+
_error(
|
|
166
|
+
f"Invalid SELinux label(s): {', '.join(bad)}; expected user:, role:, type: or level: values.",
|
|
167
|
+
path,
|
|
168
|
+
)
|
|
169
|
+
]
|
|
144
170
|
kinds = [label.split(":", 1)[0] for label in value]
|
|
145
171
|
duplicates = sorted({k for k in kinds if kinds.count(k) > 1})
|
|
146
172
|
if duplicates:
|
|
147
|
-
return [
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
return [
|
|
174
|
+
_error(
|
|
175
|
+
f"Duplicate SELinux label kind(s): {', '.join(duplicates)}; "
|
|
176
|
+
"each of user:, role:, type: and level: may appear at most once.",
|
|
177
|
+
path,
|
|
178
|
+
)
|
|
179
|
+
]
|
|
152
180
|
return []
|
|
153
181
|
|
|
154
182
|
|
|
@@ -193,11 +221,13 @@ def apply_confinement(
|
|
|
193
221
|
try:
|
|
194
222
|
new_opts = confinement_security_opts(confinement, compose_dir, base_dir)
|
|
195
223
|
except ValueError:
|
|
196
|
-
return [
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
224
|
+
return [
|
|
225
|
+
_error(
|
|
226
|
+
"seccomp file profile cannot be referenced relative to the generated "
|
|
227
|
+
"docker-compose.yml (different drive); render into the profile's drive.",
|
|
228
|
+
f"{path}.seccomp",
|
|
229
|
+
)
|
|
230
|
+
]
|
|
201
231
|
existing = [str(opt) for opt in service.get("security_opt") or []]
|
|
202
232
|
replaced = {
|
|
203
233
|
"seccomp": ("seccomp=", "seccomp:"),
|
|
@@ -205,7 +235,11 @@ def apply_confinement(
|
|
|
205
235
|
"selinux": ("label=", "label:"),
|
|
206
236
|
}
|
|
207
237
|
prefixes = tuple(p for key in confinement for p in replaced.get(key, ()))
|
|
208
|
-
kept =
|
|
238
|
+
kept = (
|
|
239
|
+
[opt for opt in existing if not opt.startswith(prefixes)]
|
|
240
|
+
if prefixes
|
|
241
|
+
else existing
|
|
242
|
+
)
|
|
209
243
|
# `default` seccomp means "engine default", so it only clears module-set values.
|
|
210
244
|
merged = kept + new_opts
|
|
211
245
|
if merged:
|
|
@@ -216,4 +250,6 @@ def apply_confinement(
|
|
|
216
250
|
|
|
217
251
|
|
|
218
252
|
def _error(message: str, path: str) -> Diagnostic:
|
|
219
|
-
return Diagnostic(
|
|
253
|
+
return Diagnostic(
|
|
254
|
+
level="error", code=INVALID_CONFINEMENT_CODE, message=message, path=path
|
|
255
|
+
)
|