composable-data-stack 0.15.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 (197) hide show
  1. {composable_data_stack-0.15.0/composable_data_stack.egg-info → composable_data_stack-0.15.1}/PKG-INFO +19 -2
  2. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/README.md +18 -1
  3. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/audit_log.py +21 -9
  4. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/composer.py +17 -5
  5. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/confinement.py +83 -47
  6. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/constants.py +1 -0
  7. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/diagnostics.py +1 -1
  8. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/getter.py +269 -74
  9. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/graph.py +3 -1
  10. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/image_digest_check.py +21 -8
  11. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/image_updates.py +34 -8
  12. {composable_data_stack-0.15.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.0 → composable_data_stack-0.15.1}/cli/k8s_renderer.py +1378 -185
  15. {composable_data_stack-0.15.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.15.0 → composable_data_stack-0.15.1}/cli/loader.py +5 -1
  18. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/main.py +1315 -290
  19. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/maturity.py +14 -20
  20. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/overlay.py +274 -24
  21. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/planner.py +435 -129
  22. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/preflight.py +116 -39
  23. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/renderer.py +789 -263
  24. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/report.py +109 -36
  25. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resolver.py +36 -3
  26. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/module.schema.json +196 -7
  27. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/profile.schema.json +47 -0
  28. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/secret_leak.py +32 -23
  29. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/secrets.py +43 -24
  30. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/security.py +425 -240
  31. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/security_common.py +89 -19
  32. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/state.py +2 -1
  33. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/up_runner.py +47 -13
  34. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/update_check.py +30 -17
  35. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/utils.py +13 -4
  36. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/validator.py +821 -345
  37. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1/composable_data_stack.egg-info}/PKG-INFO +19 -2
  38. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/SOURCES.txt +19 -0
  39. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/pyproject.toml +1 -1
  40. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_agent_policy.py +23 -6
  41. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_ai_profile_review.py +30 -4
  42. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_airflow_module.py +1 -0
  43. composable_data_stack-0.15.1/tests/test_assign_project_order.py +102 -0
  44. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_audit_log.py +63 -7
  45. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_audit_trivyignore.py +12 -4
  46. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_build_cra_release_evidence_manifest.py +12 -4
  47. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_bump_changelog.py +3 -1
  48. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_cds_runtime.py +108 -32
  49. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_cds_runtime_backends.py +13 -4
  50. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_cds_workflow.py +30 -16
  51. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_certificates.py +57 -23
  52. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_check_cra_release_gate.py +22 -6
  53. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_check_roadmap_freshness.py +55 -18
  54. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_completion.py +3 -1
  55. composable_data_stack-0.15.1/tests/test_compose_profile_lifecycle_smoke.py +423 -0
  56. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_compose_runtime_smoke.py +55 -5
  57. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_compose_to_module.py +201 -73
  58. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_composer.py +26 -9
  59. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_composer_extra.py +54 -17
  60. composable_data_stack-0.15.1/tests/test_config_module_overrides.py +608 -0
  61. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_contract_host_resolution.py +140 -35
  62. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_cra_traceability.py +1 -2
  63. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dagster_config_generation.py +19 -9
  64. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dagster_definitions_uri.py +3 -1
  65. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dagster_entrypoint_backend_guard.py +46 -24
  66. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dagster_hardening.py +58 -21
  67. composable_data_stack-0.15.1/tests/test_data_sharing.py +634 -0
  68. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dbt_hardening.py +33 -10
  69. composable_data_stack-0.15.1/tests/test_dlt_duckdb_wiring.py +236 -0
  70. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dlt_postgres_wiring.py +13 -7
  71. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_duckdb_hardening.py +23 -16
  72. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_environment_classification.py +58 -14
  73. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_environment_cli.py +131 -33
  74. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_garage_module.py +63 -19
  75. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_generate_release_inventory.py +16 -5
  76. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_getter.py +799 -153
  77. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_getter_extra.py +200 -53
  78. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_gitops_fluxcd_module.py +126 -10
  79. composable_data_stack-0.15.1/tests/test_grafana_module.py +461 -0
  80. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_graph.py +4 -4
  81. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_digest_check.py +15 -4
  82. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_locks.py +6 -2
  83. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_security_scan_workflow.py +10 -6
  84. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_updates.py +60 -20
  85. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_verification.py +102 -35
  86. composable_data_stack-0.15.1/tests/test_k3d_e2e_profile.py +144 -0
  87. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k3d_exposure.py +5 -3
  88. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k8s_e2e_workflow.py +1 -3
  89. composable_data_stack-0.15.1/tests/test_k8s_provision.py +622 -0
  90. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k8s_renderer.py +970 -75
  91. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k8s_runner.py +16 -7
  92. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k8s_security.py +213 -36
  93. composable_data_stack-0.15.1/tests/test_keycloak_oidc_clients.py +356 -0
  94. composable_data_stack-0.15.1/tests/test_keycloak_postgres_wiring.py +591 -0
  95. composable_data_stack-0.15.1/tests/test_kubernetes_dns_docs.py +27 -0
  96. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_loader.py +31 -10
  97. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_local_dagster_postgres_superset_dbt_profile.py +12 -2
  98. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_local_haven_reference_profile.py +45 -0
  99. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_local_kubernetes_example_profile.py +8 -1
  100. composable_data_stack-0.15.1/tests/test_local_loki_otel_collector_profile.py +115 -0
  101. composable_data_stack-0.15.1/tests/test_local_postgres_grafana_profile.py +95 -0
  102. composable_data_stack-0.15.1/tests/test_loki_module.py +275 -0
  103. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_main.py +2384 -758
  104. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_main_extra.py +115 -36
  105. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_main_extra2.py +236 -83
  106. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_main_extra3.py +486 -143
  107. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_markdown_mermaid_blocks.py +1 -5
  108. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_marquez_lineage_sink_host.py +28 -8
  109. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_marquez_module.py +1 -0
  110. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_maturity.py +9 -5
  111. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_module_contract_schema.py +9 -3
  112. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_module_isolation.py +52 -37
  113. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_modules_no_committed_secrets.py +6 -2
  114. composable_data_stack-0.15.1/tests/test_mutation_tests_workflow.py +316 -0
  115. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_mvp_proof_workflow.py +3 -1
  116. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_object_storage_backup_module.py +32 -18
  117. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_object_storage_backup_runtime.py +121 -50
  118. composable_data_stack-0.15.1/tests/test_observability_runtime_smoke.py +413 -0
  119. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_otel_collector_module.py +204 -19
  120. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_overlay.py +213 -59
  121. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_planner.py +407 -83
  122. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_planner_extra.py +117 -36
  123. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_postgres_backup_restore_runtime.py +330 -103
  124. composable_data_stack-0.15.1/tests/test_postgres_operator_module.py +621 -0
  125. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_preflight.py +106 -31
  126. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_preflight_extra.py +145 -8
  127. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_preflight_rootless.py +36 -2
  128. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_production_kubernetes_example_profile.py +32 -1
  129. composable_data_stack-0.15.1/tests/test_profile_matrix.py +39 -0
  130. composable_data_stack-0.15.1/tests/test_prompts_lint.py +971 -0
  131. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_publish_images_workflow.py +89 -25
  132. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_python_version_alignment.py +15 -5
  133. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_redact_secrets.py +24 -5
  134. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_release_workflow.py +12 -3
  135. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_render_example_profile.py +81 -32
  136. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_renderer.py +202 -73
  137. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_report.py +126 -51
  138. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_resolve_profile_path.py +25 -11
  139. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_resolver.py +22 -5
  140. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_restic_backup_restore_runtime.py +218 -72
  141. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_restic_backup_wiring.py +32 -13
  142. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_reverse_proxy_contract.py +11 -2
  143. composable_data_stack-0.15.1/tests/test_reverse_proxy_routes.py +1405 -0
  144. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_runtime_confinement.py +33 -13
  145. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_runtime_policy.py +134 -51
  146. composable_data_stack-0.15.1/tests/test_sample_data_parameter.py +236 -0
  147. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_secret_file_permissions.py +22 -5
  148. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_secret_output_scanning.py +31 -10
  149. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_secrets.py +9 -4
  150. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_security.py +407 -129
  151. composable_data_stack-0.15.1/tests/test_service_mesh_istio_module.py +543 -0
  152. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_shipped_profiles.py +11 -3
  153. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_smoke_example_profile.py +40 -11
  154. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_standalone_module_profile.py +4 -1
  155. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_state.py +28 -11
  156. composable_data_stack-0.15.1/tests/test_superset_oidc_helm.py +345 -0
  157. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_superset_oidc_wiring.py +19 -18
  158. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_team_profile.py +19 -6
  159. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_test_runner_timeout.py +3 -2
  160. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_up_runner.py +86 -28
  161. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_update_check.py +18 -6
  162. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_update_check_extra.py +37 -18
  163. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_upstream_image_hardening.py +14 -4
  164. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_utils.py +6 -2
  165. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_validate_cli_sbom.py +33 -7
  166. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_validator.py +493 -52
  167. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_validator_extra.py +564 -148
  168. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_vault_init_db.py +25 -8
  169. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_vault_init_db_runtime.py +93 -24
  170. composable_data_stack-0.15.1/tests/test_vendored_llm_client.py +204 -0
  171. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_workdir_cds_runtime.py +34 -13
  172. composable_data_stack-0.15.0/cli/k8s_security.py +0 -231
  173. composable_data_stack-0.15.0/tests/test_k3d_e2e_profile.py +0 -67
  174. composable_data_stack-0.15.0/tests/test_keycloak_postgres_wiring.py +0 -263
  175. composable_data_stack-0.15.0/tests/test_loki_module.py +0 -153
  176. composable_data_stack-0.15.0/tests/test_prompts_lint.py +0 -407
  177. composable_data_stack-0.15.0/tests/test_reverse_proxy_routes.py +0 -786
  178. composable_data_stack-0.15.0/tests/test_vendored_llm_client.py +0 -89
  179. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/LICENSE +0 -0
  180. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/__init__.py +0 -0
  181. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/__init__.py +0 -0
  182. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/compatibility-registry.json +0 -0
  183. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/compatibility-registry.schema.json +0 -0
  184. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/contract.schema.json +0 -0
  185. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/rule-schema.json +0 -0
  186. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/rule-set.json +0 -0
  187. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/dependency_links.txt +0 -0
  188. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/entry_points.txt +0 -0
  189. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/requires.txt +0 -0
  190. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/top_level.txt +0 -0
  191. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/setup.cfg +0 -0
  192. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_release_notes_support.py +0 -0
  193. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_release_version_check.py +0 -0
  194. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_superset_hardening.py +0 -0
  195. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_tender_analytics.py +0 -0
  196. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_tender_dashboard.py +0 -0
  197. {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_trivyignore.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: composable-data-stack
3
- Version: 0.15.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
@@ -532,6 +532,8 @@ This runs `validate` → `plan` → `render` → `docker compose build` → `doc
532
532
 
533
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.
534
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
+
535
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:
536
538
 
537
539
  ```bash
@@ -818,6 +820,7 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
818
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. |
819
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)). |
820
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. |
821
824
 
822
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. |
823
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. |
@@ -825,7 +828,7 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
825
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`. |
826
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. |
827
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.|
828
- | `[E136] ... Reverse-proxy route host ...` (unknown target / no resolvable http-service endpoint / backend address not a rendered compose service / entrypoint not declared by the proxy / undeclared certificate alias / duplicate route host) | A `config.routes` entry on a reverse-proxy provider names a module that is missing, does not provide an `http-service` contract with host/port (or a port outside 1-65535), advertises a backend address that matches no rendered compose service (multi-service modules must advertise `serviceName`), names a `<module-id>.<contract-name>` contract the target does not provide, names an entrypoint the proxy's `config.entryPoints` does not declare, references an undeclared certificate alias, or duplicates the host of an earlier route on the same proxy (#581). | Fix the route's `target`/`certificate`/`host`/`entryPoints` (a bare module id routes to its first http-service contract; `<module-id>.<contract-name>` picks a specific one; entrypoints must match the proxy's declared `config.entryPoints` keys), or advertise the serving compose service via `serviceName` in the target's http-service provide entry. |
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. |
829
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}`. |
830
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`. |
831
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`. |
@@ -836,7 +839,19 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
836
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. |
837
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). |
838
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"). |
839
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. |
840
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`. |
841
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`. |
842
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`. |
@@ -1002,11 +1017,13 @@ Development helper tools are located in the gitignored `tools/` directory. See
1002
1017
 
1003
1018
  - [Quickstart](README.md#-quickstart) — get running in 5 minutes
1004
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
1005
1021
  - [How CDS Works](docs/how-it-works.md) — C4 + sequence diagrams of the validate → plan → render engine
1006
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)
1007
1023
  - [From Docker Compose to CDS Profile](docs/from-docker-to-cds-profile.md) — complete transformation guide
1008
1024
  - [Architecture](docs/architecture.md) — design and core concepts
1009
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)
1010
1027
  - [Workload Runtime API](docs/runtime-api.md) — `cds-runtime`: named database connections for Python workloads
1011
1028
  - [Production Hardening Checklist](docs/production-hardening-checklist.md) — control-to-owner mapping and the operator verification procedure for a production profile
1012
1029
  - [Roadmap](docs/roadmap.md) — planned features and milestones
@@ -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,7 +790,7 @@ 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. |
791
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}`. |
792
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`. |
793
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`. |
@@ -798,7 +801,19 @@ Common errors from `cds validate`, `cds plan`, and `cds render`, and how to fix
798
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. |
799
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). |
800
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"). |
801
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. |
802
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`. |
803
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`. |
804
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`. |
@@ -964,11 +979,13 @@ Development helper tools are located in the gitignored `tools/` directory. See
964
979
 
965
980
  - [Quickstart](README.md#-quickstart) — get running in 5 minutes
966
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
967
983
  - [How CDS Works](docs/how-it-works.md) — C4 + sequence diagrams of the validate → plan → render engine
968
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)
969
985
  - [From Docker Compose to CDS Profile](docs/from-docker-to-cds-profile.md) — complete transformation guide
970
986
  - [Architecture](docs/architecture.md) — design and core concepts
971
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)
972
989
  - [Workload Runtime API](docs/runtime-api.md) — `cds-runtime`: named database connections for Python workloads
973
990
  - [Production Hardening Checklist](docs/production-hardening-checklist.md) — control-to-owner mapping and the operator verification procedure for a production profile
974
991
  - [Roadmap](docs/roadmap.md) — planned features and milestones
@@ -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