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.
- {composable_data_stack-0.15.0/composable_data_stack.egg-info → composable_data_stack-0.15.1}/PKG-INFO +19 -2
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/README.md +18 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/audit_log.py +21 -9
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/composer.py +17 -5
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/confinement.py +83 -47
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/constants.py +1 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/diagnostics.py +1 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/getter.py +269 -74
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/graph.py +3 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/image_digest_check.py +21 -8
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/image_updates.py +34 -8
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/image_verification.py +146 -118
- composable_data_stack-0.15.1/cli/k8s_provision.py +504 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/k8s_renderer.py +1378 -185
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/k8s_runner.py +72 -4
- composable_data_stack-0.15.1/cli/k8s_security.py +407 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/loader.py +5 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/main.py +1315 -290
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/maturity.py +14 -20
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/overlay.py +274 -24
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/planner.py +435 -129
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/preflight.py +116 -39
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/renderer.py +789 -263
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/report.py +109 -36
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resolver.py +36 -3
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/module.schema.json +196 -7
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/profile.schema.json +47 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/secret_leak.py +32 -23
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/secrets.py +43 -24
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/security.py +425 -240
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/security_common.py +89 -19
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/state.py +2 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/up_runner.py +47 -13
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/update_check.py +30 -17
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/utils.py +13 -4
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/validator.py +821 -345
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1/composable_data_stack.egg-info}/PKG-INFO +19 -2
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/SOURCES.txt +19 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/pyproject.toml +1 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_agent_policy.py +23 -6
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_ai_profile_review.py +30 -4
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_airflow_module.py +1 -0
- composable_data_stack-0.15.1/tests/test_assign_project_order.py +102 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_audit_log.py +63 -7
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_audit_trivyignore.py +12 -4
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_build_cra_release_evidence_manifest.py +12 -4
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_bump_changelog.py +3 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_cds_runtime.py +108 -32
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_cds_runtime_backends.py +13 -4
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_cds_workflow.py +30 -16
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_certificates.py +57 -23
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_check_cra_release_gate.py +22 -6
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_check_roadmap_freshness.py +55 -18
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_completion.py +3 -1
- composable_data_stack-0.15.1/tests/test_compose_profile_lifecycle_smoke.py +423 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_compose_runtime_smoke.py +55 -5
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_compose_to_module.py +201 -73
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_composer.py +26 -9
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_composer_extra.py +54 -17
- composable_data_stack-0.15.1/tests/test_config_module_overrides.py +608 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_contract_host_resolution.py +140 -35
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_cra_traceability.py +1 -2
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dagster_config_generation.py +19 -9
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dagster_definitions_uri.py +3 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dagster_entrypoint_backend_guard.py +46 -24
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dagster_hardening.py +58 -21
- composable_data_stack-0.15.1/tests/test_data_sharing.py +634 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dbt_hardening.py +33 -10
- composable_data_stack-0.15.1/tests/test_dlt_duckdb_wiring.py +236 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_dlt_postgres_wiring.py +13 -7
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_duckdb_hardening.py +23 -16
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_environment_classification.py +58 -14
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_environment_cli.py +131 -33
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_garage_module.py +63 -19
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_generate_release_inventory.py +16 -5
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_getter.py +799 -153
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_getter_extra.py +200 -53
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_gitops_fluxcd_module.py +126 -10
- composable_data_stack-0.15.1/tests/test_grafana_module.py +461 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_graph.py +4 -4
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_digest_check.py +15 -4
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_locks.py +6 -2
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_security_scan_workflow.py +10 -6
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_updates.py +60 -20
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_image_verification.py +102 -35
- composable_data_stack-0.15.1/tests/test_k3d_e2e_profile.py +144 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k3d_exposure.py +5 -3
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k8s_e2e_workflow.py +1 -3
- composable_data_stack-0.15.1/tests/test_k8s_provision.py +622 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k8s_renderer.py +970 -75
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k8s_runner.py +16 -7
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_k8s_security.py +213 -36
- composable_data_stack-0.15.1/tests/test_keycloak_oidc_clients.py +356 -0
- composable_data_stack-0.15.1/tests/test_keycloak_postgres_wiring.py +591 -0
- composable_data_stack-0.15.1/tests/test_kubernetes_dns_docs.py +27 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_loader.py +31 -10
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_local_dagster_postgres_superset_dbt_profile.py +12 -2
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_local_haven_reference_profile.py +45 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_local_kubernetes_example_profile.py +8 -1
- composable_data_stack-0.15.1/tests/test_local_loki_otel_collector_profile.py +115 -0
- composable_data_stack-0.15.1/tests/test_local_postgres_grafana_profile.py +95 -0
- composable_data_stack-0.15.1/tests/test_loki_module.py +275 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_main.py +2384 -758
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_main_extra.py +115 -36
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_main_extra2.py +236 -83
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_main_extra3.py +486 -143
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_markdown_mermaid_blocks.py +1 -5
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_marquez_lineage_sink_host.py +28 -8
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_marquez_module.py +1 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_maturity.py +9 -5
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_module_contract_schema.py +9 -3
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_module_isolation.py +52 -37
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_modules_no_committed_secrets.py +6 -2
- composable_data_stack-0.15.1/tests/test_mutation_tests_workflow.py +316 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_mvp_proof_workflow.py +3 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_object_storage_backup_module.py +32 -18
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_object_storage_backup_runtime.py +121 -50
- composable_data_stack-0.15.1/tests/test_observability_runtime_smoke.py +413 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_otel_collector_module.py +204 -19
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_overlay.py +213 -59
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_planner.py +407 -83
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_planner_extra.py +117 -36
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_postgres_backup_restore_runtime.py +330 -103
- composable_data_stack-0.15.1/tests/test_postgres_operator_module.py +621 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_preflight.py +106 -31
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_preflight_extra.py +145 -8
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_preflight_rootless.py +36 -2
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_production_kubernetes_example_profile.py +32 -1
- composable_data_stack-0.15.1/tests/test_profile_matrix.py +39 -0
- composable_data_stack-0.15.1/tests/test_prompts_lint.py +971 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_publish_images_workflow.py +89 -25
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_python_version_alignment.py +15 -5
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_redact_secrets.py +24 -5
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_release_workflow.py +12 -3
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_render_example_profile.py +81 -32
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_renderer.py +202 -73
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_report.py +126 -51
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_resolve_profile_path.py +25 -11
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_resolver.py +22 -5
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_restic_backup_restore_runtime.py +218 -72
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_restic_backup_wiring.py +32 -13
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_reverse_proxy_contract.py +11 -2
- composable_data_stack-0.15.1/tests/test_reverse_proxy_routes.py +1405 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_runtime_confinement.py +33 -13
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_runtime_policy.py +134 -51
- composable_data_stack-0.15.1/tests/test_sample_data_parameter.py +236 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_secret_file_permissions.py +22 -5
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_secret_output_scanning.py +31 -10
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_secrets.py +9 -4
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_security.py +407 -129
- composable_data_stack-0.15.1/tests/test_service_mesh_istio_module.py +543 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_shipped_profiles.py +11 -3
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_smoke_example_profile.py +40 -11
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_standalone_module_profile.py +4 -1
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_state.py +28 -11
- composable_data_stack-0.15.1/tests/test_superset_oidc_helm.py +345 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_superset_oidc_wiring.py +19 -18
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_team_profile.py +19 -6
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_test_runner_timeout.py +3 -2
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_up_runner.py +86 -28
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_update_check.py +18 -6
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_update_check_extra.py +37 -18
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_upstream_image_hardening.py +14 -4
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_utils.py +6 -2
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_validate_cli_sbom.py +33 -7
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_validator.py +493 -52
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_validator_extra.py +564 -148
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_vault_init_db.py +25 -8
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_vault_init_db_runtime.py +93 -24
- composable_data_stack-0.15.1/tests/test_vendored_llm_client.py +204 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_workdir_cds_runtime.py +34 -13
- composable_data_stack-0.15.0/cli/k8s_security.py +0 -231
- composable_data_stack-0.15.0/tests/test_k3d_e2e_profile.py +0 -67
- composable_data_stack-0.15.0/tests/test_keycloak_postgres_wiring.py +0 -263
- composable_data_stack-0.15.0/tests/test_loki_module.py +0 -153
- composable_data_stack-0.15.0/tests/test_prompts_lint.py +0 -407
- composable_data_stack-0.15.0/tests/test_reverse_proxy_routes.py +0 -786
- composable_data_stack-0.15.0/tests/test_vendored_llm_client.py +0 -89
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/LICENSE +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/__init__.py +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/__init__.py +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/compatibility-registry.json +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/compatibility-registry.schema.json +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/contract.schema.json +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/rule-schema.json +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/cli/resources/rule-set.json +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/dependency_links.txt +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/entry_points.txt +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/requires.txt +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/composable_data_stack.egg-info/top_level.txt +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/setup.cfg +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_release_notes_support.py +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_release_version_check.py +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_superset_hardening.py +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_tender_analytics.py +0 -0
- {composable_data_stack-0.15.0 → composable_data_stack-0.15.1}/tests/test_tender_dashboard.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
profiles checkout). Tracked by #822.
|
|
42
|
+
`cli.main` call sites pass the profile-anchored project root
|
|
43
|
+
(`resolve_project_root(profile_path)` -- the same resolution that
|
|
44
|
+
decides where rendered Compose output goes), so entries for a given
|
|
45
|
+
profile consistently land in one `.cds/audit-log.jsonl` regardless of
|
|
46
|
+
the cwd `cds` was invoked from (#822). With no `project_root` (direct
|
|
47
|
+
programmatic use), this falls back to `Path.cwd()`.
|
|
46
48
|
"""
|
|
47
49
|
root = (project_root or Path.cwd()).expanduser().resolve()
|
|
48
50
|
return root / _LOG_FILE
|
|
@@ -141,6 +143,13 @@ def record_entry(
|
|
|
141
143
|
entry["details"] = details
|
|
142
144
|
|
|
143
145
|
try:
|
|
146
|
+
# Never append through a symlinked .cds directory: mkdir() and
|
|
147
|
+
# open() both follow the link, so the entry would land outside the
|
|
148
|
+
# project tree (issue #1225, same write-through-symlink class as
|
|
149
|
+
# the lifecycle-log guard in cli/main.py and the tracking manifest
|
|
150
|
+
# in cli/getter.py). audit_log_path() always returns the default
|
|
151
|
+
# .cds/audit-log.jsonl, so this guard covers the default path only.
|
|
152
|
+
ensure_no_symlinked_parent(log_path, what="audit log directory")
|
|
144
153
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
145
154
|
if log_path.is_symlink():
|
|
146
155
|
# Never follow/overwrite-through a symlink planted at the log
|
|
@@ -148,8 +157,11 @@ def record_entry(
|
|
|
148
157
|
log_path.unlink()
|
|
149
158
|
with open(log_path, "a", encoding="utf-8") as f:
|
|
150
159
|
f.write(json.dumps(entry, sort_keys=True) + "\n")
|
|
151
|
-
except OSError as exc:
|
|
152
|
-
print(
|
|
160
|
+
except (OSError, SymlinkedWriteError) as exc:
|
|
161
|
+
print(
|
|
162
|
+
f"WARNING Could not write audit log entry to {log_path}: {exc}",
|
|
163
|
+
file=sys.stderr,
|
|
164
|
+
)
|
|
153
165
|
return None
|
|
154
166
|
|
|
155
167
|
return log_path
|
|
@@ -186,12 +186,18 @@ def compose_profile(
|
|
|
186
186
|
)
|
|
187
187
|
return None, diagnostics
|
|
188
188
|
|
|
189
|
-
providers, existing_ids, provider_diags = _collect_provided_contracts(
|
|
189
|
+
providers, existing_ids, provider_diags = _collect_provided_contracts(
|
|
190
|
+
profile, profile_dir, modules_root
|
|
191
|
+
)
|
|
190
192
|
diagnostics.extend(provider_diags)
|
|
191
193
|
if any(d.level == "error" for d in diagnostics):
|
|
192
194
|
return None, diagnostics
|
|
193
195
|
|
|
194
|
-
metadata =
|
|
196
|
+
metadata = (
|
|
197
|
+
module_def.get("metadata")
|
|
198
|
+
if isinstance(module_def.get("metadata"), dict)
|
|
199
|
+
else {}
|
|
200
|
+
)
|
|
195
201
|
resolved_id = instance_id or metadata.get("name")
|
|
196
202
|
if not isinstance(resolved_id, str) or not resolved_id.strip():
|
|
197
203
|
diagnostics.append(
|
|
@@ -275,7 +281,9 @@ def compose_profile(
|
|
|
275
281
|
)
|
|
276
282
|
continue
|
|
277
283
|
else:
|
|
278
|
-
candidates = sorted(
|
|
284
|
+
candidates = sorted(
|
|
285
|
+
ref for ref, kind in providers.items() if kind == contract_kind
|
|
286
|
+
)
|
|
279
287
|
if len(candidates) == 1:
|
|
280
288
|
chosen = candidates[0]
|
|
281
289
|
elif not candidates:
|
|
@@ -321,7 +329,9 @@ def compose_profile(
|
|
|
321
329
|
if any(d.level == "error" for d in diagnostics):
|
|
322
330
|
return None, diagnostics
|
|
323
331
|
|
|
324
|
-
secret_values = deepcopy(
|
|
332
|
+
secret_values = deepcopy(
|
|
333
|
+
(profile.get("spec", {}).get("secrets") or {}).get("values") or {}
|
|
334
|
+
)
|
|
325
335
|
for alias in sorted(set(_iter_secret_aliases(config))):
|
|
326
336
|
if alias in secret_values:
|
|
327
337
|
continue
|
|
@@ -359,7 +369,9 @@ def compose_profile(
|
|
|
359
369
|
merged["spec"]["modules"].append(new_instance)
|
|
360
370
|
|
|
361
371
|
if secret_values:
|
|
362
|
-
secrets_block = merged["spec"].setdefault(
|
|
372
|
+
secrets_block = merged["spec"].setdefault(
|
|
373
|
+
"secrets", {"provider": {"type": "env"}}
|
|
374
|
+
)
|
|
363
375
|
secrets_block.setdefault("provider", {"type": "env"})
|
|
364
376
|
secrets_block["values"] = secret_values
|
|
365
377
|
|
|
@@ -4,6 +4,7 @@ A profile declares a default under `spec.security.runtime.confinement`; a
|
|
|
4
4
|
module instance may override individual keys under `security.confinement`.
|
|
5
5
|
The effective value is rendered into each service's `security_opt`.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import os
|
|
@@ -49,7 +50,9 @@ def effective_confinement(
|
|
|
49
50
|
defaults: dict[str, Any] | None, override: dict[str, Any] | None
|
|
50
51
|
) -> dict[str, Any]:
|
|
51
52
|
"""Profile defaults with the module instance's keys layered on top."""
|
|
52
|
-
merged = {
|
|
53
|
+
merged = {
|
|
54
|
+
key: value for key, value in (defaults or {}).items() if key in CONFINEMENT_KEYS
|
|
55
|
+
}
|
|
53
56
|
for key, value in (override or {}).items():
|
|
54
57
|
if key in CONFINEMENT_KEYS:
|
|
55
58
|
merged[key] = value
|
|
@@ -67,10 +70,12 @@ def validate_confinement(
|
|
|
67
70
|
diagnostics: list[Diagnostic] = []
|
|
68
71
|
for key in block:
|
|
69
72
|
if key not in CONFINEMENT_KEYS:
|
|
70
|
-
diagnostics.append(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
diagnostics.append(
|
|
74
|
+
_error(
|
|
75
|
+
f"Unknown confinement key '{key}'; supported keys: {', '.join(CONFINEMENT_KEYS)}.",
|
|
76
|
+
f"{path}.{key}",
|
|
77
|
+
)
|
|
78
|
+
)
|
|
74
79
|
|
|
75
80
|
seccomp = block.get("seccomp")
|
|
76
81
|
if seccomp is not None:
|
|
@@ -78,13 +83,18 @@ def validate_confinement(
|
|
|
78
83
|
apparmor = block.get("apparmor")
|
|
79
84
|
if apparmor is not None and not (
|
|
80
85
|
isinstance(apparmor, str)
|
|
81
|
-
and (
|
|
86
|
+
and (
|
|
87
|
+
apparmor in {"runtime/default", "unconfined"}
|
|
88
|
+
or _APPARMOR_NAME_RE.match(apparmor)
|
|
89
|
+
)
|
|
82
90
|
):
|
|
83
|
-
diagnostics.append(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
diagnostics.append(
|
|
92
|
+
_error(
|
|
93
|
+
"apparmor must be 'runtime/default', 'unconfined' or the name of a profile "
|
|
94
|
+
"already loaded on the host (letters, digits, '.', '_', '-', '/').",
|
|
95
|
+
f"{path}.apparmor",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
88
98
|
selinux = block.get("selinux")
|
|
89
99
|
if selinux is not None:
|
|
90
100
|
diagnostics.extend(_validate_selinux(selinux, f"{path}.selinux"))
|
|
@@ -95,16 +105,21 @@ def _validate_seccomp(value: Any, path: str, base_dir: Path | None) -> list[Diag
|
|
|
95
105
|
if value in {"default", "unconfined"}:
|
|
96
106
|
return []
|
|
97
107
|
if not isinstance(value, str) or not value.startswith("file:") or len(value) == 5:
|
|
98
|
-
return [
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
return [
|
|
109
|
+
_error(
|
|
110
|
+
"seccomp must be 'default', 'unconfined' or 'file:<path to a JSON profile "
|
|
111
|
+
"relative to the profile directory>'.",
|
|
112
|
+
path,
|
|
113
|
+
)
|
|
114
|
+
]
|
|
103
115
|
relative = value[5:]
|
|
104
116
|
if base_dir is None:
|
|
105
|
-
return [
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
return [
|
|
118
|
+
_error(
|
|
119
|
+
"seccomp file profiles need a known profile directory to resolve against.",
|
|
120
|
+
path,
|
|
121
|
+
)
|
|
122
|
+
]
|
|
108
123
|
# Reject rooted and drive-qualified paths explicitly: they are not
|
|
109
124
|
# "absolute" to pathlib on every platform (e.g. "/x" on Windows).
|
|
110
125
|
if (
|
|
@@ -113,14 +128,17 @@ def _validate_seccomp(value: Any, path: str, base_dir: Path | None) -> list[Diag
|
|
|
113
128
|
or _DRIVE_RE.match(relative)
|
|
114
129
|
or ".." in Path(relative).parts
|
|
115
130
|
):
|
|
116
|
-
return [
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
return [
|
|
132
|
+
_error(
|
|
133
|
+
"seccomp file profiles must be relative paths inside the profile directory.",
|
|
134
|
+
path,
|
|
135
|
+
)
|
|
136
|
+
]
|
|
119
137
|
target = (base_dir / relative).resolve()
|
|
120
138
|
if not target.is_relative_to(base_dir.resolve()):
|
|
121
|
-
return [
|
|
122
|
-
"seccomp file profile resolves outside the profile directory.", path
|
|
123
|
-
|
|
139
|
+
return [
|
|
140
|
+
_error("seccomp file profile resolves outside the profile directory.", path)
|
|
141
|
+
]
|
|
124
142
|
if not target.is_file():
|
|
125
143
|
return [_error(f"seccomp profile '{relative}' does not exist.", path)]
|
|
126
144
|
return []
|
|
@@ -129,26 +147,36 @@ def _validate_seccomp(value: Any, path: str, base_dir: Path | None) -> list[Diag
|
|
|
129
147
|
def _validate_selinux(value: Any, path: str) -> list[Diagnostic]:
|
|
130
148
|
if value == "disable":
|
|
131
149
|
return []
|
|
132
|
-
if
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
150
|
+
if (
|
|
151
|
+
not isinstance(value, list)
|
|
152
|
+
or not value
|
|
153
|
+
or not all(isinstance(v, str) for v in value)
|
|
154
|
+
):
|
|
155
|
+
return [
|
|
156
|
+
_error(
|
|
157
|
+
"selinux must be 'disable' or a non-empty list of labels such as "
|
|
158
|
+
"'type:container_t' or 'level:s0:c100,c200'.",
|
|
159
|
+
path,
|
|
160
|
+
)
|
|
161
|
+
]
|
|
138
162
|
bad = [label for label in value if not _SELINUX_LABEL_RE.match(label)]
|
|
139
163
|
if bad:
|
|
140
|
-
return [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
164
|
+
return [
|
|
165
|
+
_error(
|
|
166
|
+
f"Invalid SELinux label(s): {', '.join(bad)}; expected user:, role:, type: or level: values.",
|
|
167
|
+
path,
|
|
168
|
+
)
|
|
169
|
+
]
|
|
144
170
|
kinds = [label.split(":", 1)[0] for label in value]
|
|
145
171
|
duplicates = sorted({k for k in kinds if kinds.count(k) > 1})
|
|
146
172
|
if duplicates:
|
|
147
|
-
return [
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
173
|
+
return [
|
|
174
|
+
_error(
|
|
175
|
+
f"Duplicate SELinux label kind(s): {', '.join(duplicates)}; "
|
|
176
|
+
"each of user:, role:, type: and level: may appear at most once.",
|
|
177
|
+
path,
|
|
178
|
+
)
|
|
179
|
+
]
|
|
152
180
|
return []
|
|
153
181
|
|
|
154
182
|
|
|
@@ -193,11 +221,13 @@ def apply_confinement(
|
|
|
193
221
|
try:
|
|
194
222
|
new_opts = confinement_security_opts(confinement, compose_dir, base_dir)
|
|
195
223
|
except ValueError:
|
|
196
|
-
return [
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
224
|
+
return [
|
|
225
|
+
_error(
|
|
226
|
+
"seccomp file profile cannot be referenced relative to the generated "
|
|
227
|
+
"docker-compose.yml (different drive); render into the profile's drive.",
|
|
228
|
+
f"{path}.seccomp",
|
|
229
|
+
)
|
|
230
|
+
]
|
|
201
231
|
existing = [str(opt) for opt in service.get("security_opt") or []]
|
|
202
232
|
replaced = {
|
|
203
233
|
"seccomp": ("seccomp=", "seccomp:"),
|
|
@@ -205,7 +235,11 @@ def apply_confinement(
|
|
|
205
235
|
"selinux": ("label=", "label:"),
|
|
206
236
|
}
|
|
207
237
|
prefixes = tuple(p for key in confinement for p in replaced.get(key, ()))
|
|
208
|
-
kept =
|
|
238
|
+
kept = (
|
|
239
|
+
[opt for opt in existing if not opt.startswith(prefixes)]
|
|
240
|
+
if prefixes
|
|
241
|
+
else existing
|
|
242
|
+
)
|
|
209
243
|
# `default` seccomp means "engine default", so it only clears module-set values.
|
|
210
244
|
merged = kept + new_opts
|
|
211
245
|
if merged:
|
|
@@ -216,4 +250,6 @@ def apply_confinement(
|
|
|
216
250
|
|
|
217
251
|
|
|
218
252
|
def _error(message: str, path: str) -> Diagnostic:
|
|
219
|
-
return Diagnostic(
|
|
253
|
+
return Diagnostic(
|
|
254
|
+
level="error", code=INVALID_CONFINEMENT_CODE, message=message, path=path
|
|
255
|
+
)
|