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.
Files changed (203) hide show
  1. {composable_data_stack-0.14.0/composable_data_stack.egg-info → composable_data_stack-0.15.1}/PKG-INFO +40 -4
  2. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/README.md +37 -2
  3. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/audit_log.py +21 -9
  4. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/composer.py +17 -5
  5. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/confinement.py +83 -47
  6. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/constants.py +1 -0
  7. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/diagnostics.py +1 -1
  8. composable_data_stack-0.15.1/cli/getter.py +1806 -0
  9. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/graph.py +3 -1
  10. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/image_digest_check.py +21 -8
  11. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/image_updates.py +46 -14
  12. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/image_verification.py +146 -118
  13. composable_data_stack-0.15.1/cli/k8s_provision.py +504 -0
  14. composable_data_stack-0.15.1/cli/k8s_renderer.py +3376 -0
  15. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/k8s_runner.py +72 -4
  16. composable_data_stack-0.15.1/cli/k8s_security.py +407 -0
  17. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/loader.py +5 -1
  18. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/main.py +1453 -314
  19. composable_data_stack-0.15.1/cli/maturity.py +265 -0
  20. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/overlay.py +274 -24
  21. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/planner.py +644 -133
  22. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/preflight.py +116 -39
  23. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/renderer.py +995 -256
  24. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/report.py +109 -36
  25. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resolver.py +36 -3
  26. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/module.schema.json +340 -7
  27. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/profile.schema.json +58 -1
  28. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/rule-schema.json +5 -0
  29. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/rule-set.json +10 -7
  30. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/secret_leak.py +32 -23
  31. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/secrets.py +47 -25
  32. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/security.py +533 -255
  33. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/security_common.py +101 -9
  34. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/state.py +2 -1
  35. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/up_runner.py +85 -17
  36. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/update_check.py +30 -17
  37. composable_data_stack-0.15.1/cli/utils.py +94 -0
  38. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/validator.py +1166 -188
  39. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1/composable_data_stack.egg-info}/PKG-INFO +40 -4
  40. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/SOURCES.txt +48 -1
  41. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/requires.txt +2 -1
  42. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/pyproject.toml +74 -2
  43. composable_data_stack-0.15.1/tests/test_agent_policy.py +66 -0
  44. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_ai_profile_review.py +30 -4
  45. composable_data_stack-0.15.1/tests/test_airflow_module.py +98 -0
  46. composable_data_stack-0.15.1/tests/test_assign_project_order.py +102 -0
  47. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_audit_log.py +63 -7
  48. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_audit_trivyignore.py +50 -3
  49. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_build_cra_release_evidence_manifest.py +12 -4
  50. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_bump_changelog.py +3 -1
  51. composable_data_stack-0.15.1/tests/test_cds_runtime.py +668 -0
  52. composable_data_stack-0.15.1/tests/test_cds_runtime_backends.py +162 -0
  53. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_cds_workflow.py +37 -22
  54. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_certificates.py +57 -23
  55. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_check_cra_release_gate.py +22 -6
  56. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_check_roadmap_freshness.py +55 -18
  57. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_completion.py +3 -1
  58. composable_data_stack-0.15.1/tests/test_compose_profile_lifecycle_smoke.py +423 -0
  59. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_compose_runtime_smoke.py +55 -5
  60. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_compose_to_module.py +201 -73
  61. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_composer.py +26 -9
  62. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_composer_extra.py +54 -17
  63. composable_data_stack-0.15.1/tests/test_config_module_overrides.py +608 -0
  64. composable_data_stack-0.15.1/tests/test_contract_host_resolution.py +777 -0
  65. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_cra_traceability.py +1 -2
  66. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dagster_config_generation.py +19 -9
  67. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dagster_definitions_uri.py +3 -1
  68. composable_data_stack-0.15.1/tests/test_dagster_entrypoint_backend_guard.py +464 -0
  69. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dagster_hardening.py +58 -21
  70. composable_data_stack-0.15.1/tests/test_data_sharing.py +634 -0
  71. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dbt_hardening.py +33 -10
  72. composable_data_stack-0.15.1/tests/test_dlt_duckdb_wiring.py +236 -0
  73. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_dlt_postgres_wiring.py +13 -7
  74. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_duckdb_hardening.py +23 -16
  75. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_environment_classification.py +58 -14
  76. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_environment_cli.py +131 -33
  77. composable_data_stack-0.15.1/tests/test_garage_module.py +386 -0
  78. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_generate_release_inventory.py +16 -5
  79. composable_data_stack-0.15.1/tests/test_getter.py +2509 -0
  80. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_getter_extra.py +320 -52
  81. composable_data_stack-0.15.1/tests/test_gitops_fluxcd_module.py +418 -0
  82. composable_data_stack-0.15.1/tests/test_grafana_module.py +461 -0
  83. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_graph.py +4 -4
  84. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_digest_check.py +15 -4
  85. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_locks.py +6 -2
  86. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_security_scan_workflow.py +10 -6
  87. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_updates.py +127 -17
  88. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_image_verification.py +102 -35
  89. composable_data_stack-0.15.1/tests/test_k3d_e2e_profile.py +144 -0
  90. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_k3d_exposure.py +5 -3
  91. composable_data_stack-0.15.1/tests/test_k8s_e2e_workflow.py +64 -0
  92. composable_data_stack-0.15.1/tests/test_k8s_provision.py +622 -0
  93. composable_data_stack-0.15.1/tests/test_k8s_renderer.py +2184 -0
  94. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_k8s_runner.py +34 -7
  95. composable_data_stack-0.15.1/tests/test_k8s_security.py +488 -0
  96. composable_data_stack-0.15.1/tests/test_keycloak_oidc_clients.py +356 -0
  97. composable_data_stack-0.15.1/tests/test_keycloak_postgres_wiring.py +591 -0
  98. composable_data_stack-0.15.1/tests/test_kubernetes_dns_docs.py +27 -0
  99. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_loader.py +31 -10
  100. composable_data_stack-0.15.1/tests/test_local_dagster_postgres_superset_dbt_profile.py +102 -0
  101. composable_data_stack-0.15.1/tests/test_local_haven_reference_profile.py +128 -0
  102. composable_data_stack-0.15.1/tests/test_local_kubernetes_example_profile.py +88 -0
  103. composable_data_stack-0.15.1/tests/test_local_loki_otel_collector_profile.py +115 -0
  104. composable_data_stack-0.15.1/tests/test_local_postgres_grafana_profile.py +95 -0
  105. composable_data_stack-0.15.1/tests/test_loki_module.py +275 -0
  106. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_main.py +2520 -753
  107. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_main_extra.py +115 -36
  108. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_main_extra2.py +236 -83
  109. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_main_extra3.py +486 -143
  110. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_markdown_mermaid_blocks.py +1 -5
  111. composable_data_stack-0.15.1/tests/test_marquez_lineage_sink_host.py +160 -0
  112. composable_data_stack-0.15.1/tests/test_marquez_module.py +114 -0
  113. composable_data_stack-0.15.1/tests/test_maturity.py +367 -0
  114. composable_data_stack-0.15.1/tests/test_module_contract_schema.py +98 -0
  115. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_module_isolation.py +52 -37
  116. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_modules_no_committed_secrets.py +6 -2
  117. composable_data_stack-0.15.1/tests/test_mutation_tests_workflow.py +316 -0
  118. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_mvp_proof_workflow.py +3 -1
  119. composable_data_stack-0.15.1/tests/test_object_storage_backup_module.py +337 -0
  120. composable_data_stack-0.15.1/tests/test_object_storage_backup_runtime.py +590 -0
  121. composable_data_stack-0.15.1/tests/test_observability_runtime_smoke.py +413 -0
  122. composable_data_stack-0.15.1/tests/test_otel_collector_module.py +437 -0
  123. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_overlay.py +213 -59
  124. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_planner.py +545 -83
  125. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_planner_extra.py +194 -33
  126. composable_data_stack-0.15.1/tests/test_postgres_backup_restore_runtime.py +722 -0
  127. composable_data_stack-0.15.1/tests/test_postgres_operator_module.py +621 -0
  128. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_preflight.py +106 -31
  129. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_preflight_extra.py +145 -8
  130. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_preflight_rootless.py +36 -2
  131. composable_data_stack-0.15.1/tests/test_production_kubernetes_example_profile.py +128 -0
  132. composable_data_stack-0.15.1/tests/test_profile_matrix.py +39 -0
  133. composable_data_stack-0.15.1/tests/test_prompts_lint.py +971 -0
  134. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_publish_images_workflow.py +135 -23
  135. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_python_version_alignment.py +15 -5
  136. composable_data_stack-0.15.1/tests/test_redact_secrets.py +122 -0
  137. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_release_workflow.py +12 -3
  138. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_render_example_profile.py +81 -32
  139. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_renderer.py +431 -48
  140. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_report.py +126 -51
  141. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_resolve_profile_path.py +25 -11
  142. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_resolver.py +22 -5
  143. composable_data_stack-0.15.1/tests/test_restic_backup_restore_runtime.py +704 -0
  144. composable_data_stack-0.15.1/tests/test_restic_backup_wiring.py +314 -0
  145. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_reverse_proxy_contract.py +11 -2
  146. composable_data_stack-0.15.1/tests/test_reverse_proxy_routes.py +1405 -0
  147. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_runtime_confinement.py +33 -13
  148. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_runtime_policy.py +173 -34
  149. composable_data_stack-0.15.1/tests/test_sample_data_parameter.py +236 -0
  150. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_secret_file_permissions.py +22 -5
  151. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_secret_output_scanning.py +31 -10
  152. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_secrets.py +29 -4
  153. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_security.py +593 -101
  154. composable_data_stack-0.15.1/tests/test_service_mesh_istio_module.py +543 -0
  155. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_shipped_profiles.py +11 -3
  156. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_smoke_example_profile.py +40 -11
  157. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_standalone_module_profile.py +9 -2
  158. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_state.py +28 -11
  159. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_superset_hardening.py +8 -2
  160. composable_data_stack-0.15.1/tests/test_superset_oidc_helm.py +345 -0
  161. composable_data_stack-0.15.1/tests/test_superset_oidc_wiring.py +267 -0
  162. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_team_profile.py +34 -5
  163. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_test_runner_timeout.py +3 -2
  164. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_trivyignore.py +3 -5
  165. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_up_runner.py +192 -26
  166. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_update_check.py +18 -6
  167. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_update_check_extra.py +37 -18
  168. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_upstream_image_hardening.py +14 -4
  169. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_utils.py +6 -2
  170. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_validate_cli_sbom.py +33 -7
  171. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_validator.py +493 -52
  172. composable_data_stack-0.15.1/tests/test_validator_extra.py +1869 -0
  173. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_vault_init_db.py +25 -8
  174. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_vault_init_db_runtime.py +93 -24
  175. composable_data_stack-0.15.1/tests/test_vendored_llm_client.py +204 -0
  176. composable_data_stack-0.15.1/tests/test_workdir_cds_runtime.py +175 -0
  177. composable_data_stack-0.14.0/cli/getter.py +0 -1000
  178. composable_data_stack-0.14.0/cli/k8s_renderer.py +0 -1546
  179. composable_data_stack-0.14.0/cli/k8s_security.py +0 -138
  180. composable_data_stack-0.14.0/cli/utils.py +0 -35
  181. composable_data_stack-0.14.0/tests/test_dagster_entrypoint_backend_guard.py +0 -210
  182. composable_data_stack-0.14.0/tests/test_getter.py +0 -1336
  183. composable_data_stack-0.14.0/tests/test_k3d_e2e_profile.py +0 -52
  184. composable_data_stack-0.14.0/tests/test_k8s_renderer.py +0 -632
  185. composable_data_stack-0.14.0/tests/test_k8s_security.py +0 -155
  186. composable_data_stack-0.14.0/tests/test_keycloak_postgres_wiring.py +0 -213
  187. composable_data_stack-0.14.0/tests/test_reverse_proxy_routes.py +0 -786
  188. composable_data_stack-0.14.0/tests/test_validator_extra.py +0 -955
  189. composable_data_stack-0.14.0/tests/test_vendored_llm_client.py +0 -89
  190. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/LICENSE +0 -0
  191. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/__init__.py +0 -0
  192. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/__init__.py +0 -0
  193. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/compatibility-registry.json +0 -0
  194. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/compatibility-registry.schema.json +0 -0
  195. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/cli/resources/contract.schema.json +0 -0
  196. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/dependency_links.txt +0 -0
  197. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/entry_points.txt +0 -0
  198. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/top_level.txt +0 -0
  199. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/setup.cfg +0 -0
  200. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_release_notes_support.py +0 -0
  201. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_release_version_check.py +0 -0
  202. {composable_data_stack-0.14.0 → composable_data_stack-0.15.1}/tests/test_tender_analytics.py +0 -0
  203. {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.14.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.1; extra == "dev"
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 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. |
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 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. |
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
- No `cli.main` call site currently passes an explicit `project_root`, so
40
- this defaults to `Path.cwd()` rather than the `resolve_project_root
41
- (profile_path)` convention used elsewhere (e.g. for rendered Compose
42
- output). That means audit entries land in whatever directory the user
43
- happened to invoke `cds` from, not necessarily the profile's own
44
- project root (e.g. when `CDS_PROFILE_PATH` points at a separate
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(f"WARNING Could not write audit log entry to {log_path}: {exc}", file=sys.stderr)
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(profile, profile_dir, modules_root)
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 = module_def.get("metadata") if isinstance(module_def.get("metadata"), dict) else {}
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(ref for ref, kind in providers.items() if kind == contract_kind)
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((profile.get("spec", {}).get("secrets") or {}).get("values") or {})
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("secrets", {"provider": {"type": "env"}})
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 = {key: value for key, value in (defaults or {}).items() if key in CONFINEMENT_KEYS}
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(_error(
71
- f"Unknown confinement key '{key}'; supported keys: {', '.join(CONFINEMENT_KEYS)}.",
72
- f"{path}.{key}",
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 (apparmor in {"runtime/default", "unconfined"} or _APPARMOR_NAME_RE.match(apparmor))
86
+ and (
87
+ apparmor in {"runtime/default", "unconfined"}
88
+ or _APPARMOR_NAME_RE.match(apparmor)
89
+ )
82
90
  ):
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
- ))
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 [_error(
99
- "seccomp must be 'default', 'unconfined' or 'file:<path to a JSON profile "
100
- "relative to the profile directory>'.",
101
- path,
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 [_error(
106
- "seccomp file profiles need a known profile directory to resolve against.", path
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 [_error(
117
- "seccomp file profiles must be relative paths inside the profile directory.", path
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 [_error(
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 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
- )]
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 [_error(
141
- f"Invalid SELinux label(s): {', '.join(bad)}; expected user:, role:, type: or level: values.",
142
- path,
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 [_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
- )]
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 [_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
- )]
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 = [opt for opt in existing if not opt.startswith(prefixes)] if prefixes else existing
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(level="error", code=INVALID_CONFINEMENT_CODE, message=message, path=path)
253
+ return Diagnostic(
254
+ level="error", code=INVALID_CONFINEMENT_CODE, message=message, path=path
255
+ )
@@ -4,5 +4,6 @@
4
4
  # RecursionError / stack overflow.
5
5
  MAX_NESTING_DEPTH = 100
6
6
 
7
+
7
8
  class MaxNestingDepthExceeded(Exception):
8
9
  """Raised when a recursive structure exceeds MAX_NESTING_DEPTH."""
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
 
5
5
  @dataclass
6
6
  class Diagnostic:
7
- level: str # "error" | "warning"
7
+ level: str # "error" | "warning"
8
8
  code: str
9
9
  message: str
10
10
  path: str