@tdk-landscape/tdk-cli-core 1.3.15 → 1.3.22
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.
- package/Tiltfile +144 -0
- package/discovery/AGENTS.md +362 -0
- package/discovery/TILT_DISCOVERY.star +424 -0
- package/discovery/Tiltfile +58 -0
- package/discovery/config.star +258 -0
- package/discovery/constants.star +127 -0
- package/discovery/deterministic.star +211 -0
- package/discovery/discovery_daemon.star +191 -0
- package/discovery/discovery_orchestrator.star +460 -0
- package/discovery/filtering.star +5 -0
- package/discovery/identity-management-backend/service.yaml +26 -0
- package/discovery/identity-management-frontend/service.yaml +29 -0
- package/discovery/identity-sdk/service.yaml +9 -0
- package/discovery/incremental_discovery.star +213 -0
- package/discovery/indexing.star +7 -0
- package/discovery/json_manifest_scanner.star +51 -0
- package/discovery/libraries.star +46 -0
- package/discovery/loading.star +23 -0
- package/discovery/manifest/constants.star +104 -0
- package/discovery/manifest/loading.star +386 -0
- package/discovery/manifest/normalize.star +281 -0
- package/discovery/manifest/validation.star +108 -0
- package/discovery/normalization.star +11 -0
- package/discovery/registry.star +544 -0
- package/discovery/resource_snapshot.py +382 -0
- package/discovery/resource_snapshot.star +143 -0
- package/discovery/schemas.star +1 -0
- package/discovery/services/platform/cdc/debezium/nats-connector/service.yaml +9 -0
- package/discovery/services/platform/cdc/nats-http-bridge/service.yaml +9 -0
- package/discovery/services/platform/orchestrator/orchestrator-glue-backend/service.yaml +26 -0
- package/discovery/services/platform/synthetic-monitor/service.yaml +9 -0
- package/discovery/test_resource_snapshot.py +251 -0
- package/discovery/validation.star +78 -0
- package/discovery/versioning.star +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +11 -0
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +1 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +36 -41
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/resource.d.ts.map +1 -1
- package/dist/commands/resource.js +51 -55
- package/dist/commands/resource.js.map +1 -1
- package/dist/commands/stack.d.ts.map +1 -1
- package/dist/commands/stack.js +22 -24
- package/dist/commands/stack.js.map +1 -1
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +1 -2
- package/dist/commands/ui.js.map +1 -1
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +1 -2
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/upgrade.d.ts +8 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +59 -18
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/generator/extension-fetch.d.ts +19 -0
- package/dist/generator/extension-fetch.d.ts.map +1 -1
- package/dist/generator/extension-fetch.js +71 -5
- package/dist/generator/extension-fetch.js.map +1 -1
- package/dist/generator/template-engine.d.ts +1 -0
- package/dist/generator/template-engine.d.ts.map +1 -1
- package/dist/generator/template-engine.js +50 -6
- package/dist/generator/template-engine.js.map +1 -1
- package/dist/utils/command-helpers.d.ts.map +1 -1
- package/dist/utils/command-helpers.js +7 -9
- package/dist/utils/command-helpers.js.map +1 -1
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +1 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/errors.d.ts +2 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +6 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/project-features.d.ts.map +1 -1
- package/dist/utils/project-features.js +1 -1
- package/dist/utils/project-features.js.map +1 -1
- package/dist/utils/resource-features.d.ts.map +1 -1
- package/dist/utils/resource-features.js +2 -2
- package/dist/utils/resource-features.js.map +1 -1
- package/dist/utils/tar.js +4 -4
- package/dist/utils/tar.js.map +1 -1
- package/dist/utils/validation.d.ts +7 -0
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +9 -0
- package/dist/utils/validation.js.map +1 -1
- package/engine/AGENTS.md +705 -0
- package/engine/README.md +29 -0
- package/engine/REFACTORING_MANIFEST_SYSTEM.md +974 -0
- package/engine/Tiltfile +80 -0
- package/engine/assets/docker/golden-layers.Dockerfile +221 -0
- package/engine/assets/scripts/cleanup-completed-migrator-images.sh +37 -0
- package/engine/assets/scripts/cleanup-tilt-images.sh +120 -0
- package/engine/assets/scripts/mac-m1-resource-monitor.sh +121 -0
- package/engine/config.star +3 -0
- package/engine/docs/AUTODISCOVERY_TEST.md +182 -0
- package/engine/docs/C4-ARCHITECTURE-operations.md +396 -0
- package/engine/docs/C4-ARCHITECTURE-overview.md +284 -0
- package/engine/docs/C4-ARCHITECTURE.md +17 -0
- package/engine/docs/GOLDEN_IMAGE_INTEGRATION.md +349 -0
- package/engine/docs/MANIFEST_DRIVEN_ARCHITECTURE.md +357 -0
- package/engine/docs/README.md +284 -0
- package/engine/entrypoint.star +97 -0
- package/engine/lifecycle/orchestrator.star +260 -0
- package/engine/paths.star +717 -0
- package/engine/schemas/manifest-schema.json +301 -0
- package/engine/schemas/service-schema.json +301 -0
- package/engine/scripts/README.md +89 -0
- package/engine/scripts/build-and-publish-lib.sh +205 -0
- package/engine/scripts/check-lib-publish.sh +85 -0
- package/engine/scripts/debug-container.sh +258 -0
- package/engine/scripts/generate-manifest.sh +197 -0
- package/engine/spec.master +702 -0
- package/engine/spec.master.example +341 -0
- package/engine/topologies/README.md +34 -0
- package/engine/topologies/platform/docker/AGENTS.md +59 -0
- package/engine/topologies/platform/docker/auth/auth.star +167 -0
- package/engine/topologies/platform/docker/build/golden_image.star +37 -0
- package/engine/topologies/platform/docker/build/golden_image_build.star +82 -0
- package/engine/topologies/platform/docker/build/golden_image_constants.star +93 -0
- package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +291 -0
- package/engine/topologies/platform/docker/compose/compose.star +636 -0
- package/engine/topologies/platform/docker/compose/compose_helpers.star +52 -0
- package/engine/topologies/platform/docker/compose/traefik_standalone.star +53 -0
- package/engine/topologies/platform/docker/config/bunignore.star +142 -0
- package/engine/topologies/platform/docker/config/dockerignore.star +150 -0
- package/engine/topologies/platform/docker/constants.star +170 -0
- package/engine/topologies/platform/docker/dockerfile/dockerfile.star +54 -0
- package/engine/topologies/platform/docker/generators/golden_docker_generator_v2.star +73 -0
- package/engine/topologies/platform/docker/index.star +27 -0
- package/engine/topologies/platform/docker/layers/bun_helpers.star +23 -0
- package/engine/topologies/platform/docker/layers/infisical/index.star +40 -0
- package/engine/topologies/platform/docker/layers/infisical/infisical_docker.star +262 -0
- package/engine/topologies/platform/docker/layers/l1_base_layers.star +87 -0
- package/engine/topologies/platform/docker/layers/l2_dependency_layers.star +125 -0
- package/engine/topologies/platform/docker/layers/l3_builder_layers.star +209 -0
- package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +214 -0
- package/engine/topologies/platform/docker/layers/prisma/constants.star +135 -0
- package/engine/topologies/platform/docker/layers/prisma/db_readiness.star +96 -0
- package/engine/topologies/platform/docker/layers/prisma/filesystem_setup.star +109 -0
- package/engine/topologies/platform/docker/layers/prisma/migrate_script.star +98 -0
- package/engine/topologies/platform/docker/layers/prisma/prisma_build.star +126 -0
- package/engine/topologies/platform/docker/layers/prisma/prisma_runtime.star +161 -0
- package/engine/topologies/platform/docker/layers/prisma/secret_resolution.star +119 -0
- package/engine/topologies/platform/docker/networking/api_path_constants.star +230 -0
- package/engine/topologies/platform/docker/networking/sablier_container_cycle.star +27 -0
- package/engine/topologies/platform/docker/networking/traefik.star +232 -0
- package/engine/topologies/platform/docker/networking/traefik_constants.star +146 -0
- package/engine/topologies/platform/docker/networking/traefik_helpers.star +160 -0
- package/engine/topologies/platform/docker/secrets/infisical.star +135 -0
- package/engine/topologies/platform/net/proxy.star +4 -0
- package/engine/topologies/platform/registries/local.star +4 -0
- package/engine/topologies/platform/security/secrets.star +239 -0
- package/engine/topologies/platform/state/relational.star +4 -0
- package/engine/topologies/tilt/common/AGENTS.md +59 -0
- package/engine/topologies/tilt/common/utils.star +364 -0
- package/engine/topologies/tilt/common/utils_debug.star +69 -0
- package/engine/topologies/tilt/common/utils_deterministic.star +168 -0
- package/engine/topologies/tilt/common/utils_docker_networks.star +46 -0
- package/engine/topologies/tilt/common/utils_env.star +43 -0
- package/engine/topologies/tilt/common/utils_output.star +97 -0
- package/engine/topologies/tilt/config/AGENTS.md +68 -0
- package/engine/topologies/tilt/config/defaults.star +3 -0
- package/engine/topologies/tilt/config/feature_flags.star +10 -0
- package/engine/topologies/tilt/config/global.star +55 -0
- package/engine/topologies/tilt/config/profiles.star +382 -0
- package/engine/topologies/tilt/generators/agents_md.star +62 -0
- package/engine/topologies/tilt/generators/api_gateway.star +145 -0
- package/engine/topologies/tilt/generators/index.star +124 -0
- package/engine/topologies/tilt/generators/infisical/README.md +424 -0
- package/engine/topologies/tilt/generators/infisical/examples.star +312 -0
- package/engine/topologies/tilt/generators/infisical/index.star +57 -0
- package/engine/topologies/tilt/generators/infisical/machine_identity.star +420 -0
- package/engine/topologies/tilt/generators/infisical/organization.star +534 -0
- package/engine/topologies/tilt/generators/infisical/path_manager.star +476 -0
- package/engine/topologies/tilt/generators/infisical/secrets_generator.star +420 -0
- package/engine/topologies/tilt/generators/nginx_conf.star +143 -0
- package/engine/topologies/tilt/generators/npmrc.star +111 -0
- package/engine/topologies/tilt/generators/tsconfig.star +23 -0
- package/engine/topologies/tilt/generators/typescript/backend_library_tsconfig.star +71 -0
- package/engine/topologies/tilt/generators/typescript/backend_tsconfig.star +124 -0
- package/engine/topologies/tilt/generators/typescript/common.star +7 -0
- package/engine/topologies/tilt/generators/typescript/frontend_library_tsconfig.star +76 -0
- package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +110 -0
- package/engine/topologies/tilt/generators/typescript/include.star +23 -0
- package/engine/topologies/tilt/generators/typescript/playwright_config.star +40 -0
- package/engine/topologies/tilt/generators/typescript/prisma_config.star +49 -0
- package/engine/topologies/tilt/generators/validators.star +853 -0
- package/engine/topologies/tilt/generators/vite/backend.star +78 -0
- package/engine/topologies/tilt/generators/vite/frontend.star +98 -0
- package/engine/topologies/tilt/generators/vite/generators.star +43 -0
- package/engine/topologies/tilt/generators/vite/helpers.star +244 -0
- package/engine/topologies/tilt/generators/vite/library.star +65 -0
- package/engine/topologies/tilt/generators/vite/sdk.star +73 -0
- package/engine/topologies/tilt/generators/vite/templates.star +737 -0
- package/engine/topologies/tilt/generators/vite_config.star +22 -0
- package/engine/topologies/tilt/manifest/AGENTS.md +61 -0
- package/engine/topologies/tilt/manifest/README.md +693 -0
- package/engine/topologies/tilt/manifest/__init__.star +218 -0
- package/engine/topologies/tilt/manifest/constants.star +252 -0
- package/engine/topologies/tilt/manifest/errors.star +266 -0
- package/engine/topologies/tilt/manifest/integration.star +339 -0
- package/engine/topologies/tilt/manifest/json_to_yaml.py +54 -0
- package/engine/topologies/tilt/manifest/loader.star +214 -0
- package/engine/topologies/tilt/manifest/mapper.star +227 -0
- package/engine/topologies/tilt/manifest/parser.star +400 -0
- package/engine/topologies/tilt/manifest/schema.star +462 -0
- package/engine/topologies/tilt/manifest/tests/test_integration.star +276 -0
- package/engine/topologies/tilt/manifest/tests/test_loader_parser.star +397 -0
- package/engine/topologies/tilt/manifest/tests/test_manifest.star +316 -0
- package/engine/topologies/tilt/manifest/tests/test_validator.star +470 -0
- package/engine/topologies/tilt/manifest/validator.star +717 -0
- package/engine/topologies/tilt/manifest/yaml_to_json.py +18 -0
- package/engine/topologies/tilt/resources/AGENTS.md +71 -0
- package/engine/topologies/tilt/resources/conditions.star +3 -0
- package/engine/topologies/tilt/resources/databases.star +244 -0
- package/engine/topologies/tilt/resources/declaration.star +3 -0
- package/engine/topologies/tilt/resources/deps.star +3 -0
- package/engine/topologies/tilt/resources/infra-loader.star +437 -0
- package/engine/topologies/tilt/resources/libs.star +30 -0
- package/engine/topologies/tilt/resources/libs_discovery.star +102 -0
- package/engine/topologies/tilt/resources/libs_paths.star +79 -0
- package/engine/topologies/tilt/resources/libs_resource_definition.star +151 -0
- package/engine/topologies/tilt/resources/libs_setup.star +164 -0
- package/engine/topologies/tilt/resources/libs_tsconfig.star +37 -0
- package/engine/topologies/tilt/resources/orchestrator/apply.star +77 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +431 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_manifest_orchestration.star +81 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_migrator_orchestration.star +148 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_resource_validation.star +36 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_runtime_flags.star +44 -0
- package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +164 -0
- package/engine/topologies/tilt/resources/orchestrator/generators/dependencies/AGENTS.md +75 -0
- package/engine/topologies/tilt/resources/orchestrator/generators/dependencies/index.star +21 -0
- package/engine/topologies/tilt/resources/orchestrator/generators/dependencies/resolver.star +134 -0
- package/engine/topologies/tilt/resources/orchestrator/generators/env.star +258 -0
- package/engine/topologies/tilt/resources/orchestrator/generators/frontend.star +169 -0
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +524 -0
- package/engine/topologies/tilt/resources/orchestrator/helpers.star +33 -0
- package/engine/topologies/tilt/resources/orchestrator.star +54 -0
- package/engine/topologies/tilt/resources/ordering.star +3 -0
- package/engine/topologies/tilt/resources/triggers.star +3 -0
- package/engine/topologies/tilt/resources/watchers.star +299 -0
- package/engine/where-things-live.star +717 -0
- package/ext/Tiltfile +66 -0
- package/ext/ide-components/README.md +219 -0
- package/ext/ide-components/code_executor/server.py +268 -0
- package/ext/ide-components/code_viewer/server.py +206 -0
- package/ext/ide-components/config_inspector/server.py +1050 -0
- package/ext/ide-components/file_browser/server.py +285 -0
- package/ext/ide-components/shared/config_service.py +469 -0
- package/ext/ide-components/shared/file_utils.py +298 -0
- package/ext/ide-components/shared/http_utils.py +232 -0
- package/ext/ide-components/shared/static/styles.css +1334 -0
- package/ext/ide-components/shared/syntax_highlighter.py +140 -0
- package/ext/ide-components/shared/template_engine.py +123 -0
- package/ext/ide-components/shared/templates/base.html +121 -0
- package/ext/ide-components/shared/test_config_service.py +326 -0
- package/ext/ide-components/shared/tilt_integration.py +158 -0
- package/ext/ui-enhancements/cron-status-server.sh +49 -0
- package/ext/ui-enhancements/cron-status-tracker.sh +146 -0
- package/ext/ui-enhancements/extension.yaml +36 -0
- package/ext/ui-enhancements/tiltfile.py +99 -0
- package/ext/ui-enhancements/ui/cron-jobs-tab.css +396 -0
- package/ext/ui-enhancements/ui/help-panel.css +279 -0
- package/ext/ui-enhancements/ui/icons.css +125 -0
- package/ext/ui-enhancements/ui/tooltips.css +114 -0
- package/package.json +13 -9
- package/specs/specs/TILT_SERVICE_DEFAULTS.star +205 -0
- package/specs/specs/TILT_TECH_STACK.star +159 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# 📋 MANIFEST MODULE - VALIDATOR
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Path: .tilt/topologies/tilt/manifest/validator.star
|
|
5
|
+
# Purpose: Comprehensive manifest validation with schema, values, and dependencies
|
|
6
|
+
# Status: Phase 3 of manifest system refactoring
|
|
7
|
+
# =============================================================================
|
|
8
|
+
|
|
9
|
+
# === INLINED CONSTANTS for pure extension loading ===
|
|
10
|
+
VALID_STACKS = () # Stacks are project-specific, discovered dynamically
|
|
11
|
+
VALID_FEATURES = "api-client", "env-config", "api-index", "prisma", "nats", "redis", "infisical", "vitest", "traefik", "websocket", "graphql", "grpc", "vite-node", "maintenance", "sablier"
|
|
12
|
+
PORT_RANGES = {"frontend": {"min": 3000, "max": 5999}, "backend": {"min": 4000, "max": 5999}, "worker": {"min": 6000, "max": 6999}, "migrator": {"min": 7000, "max": 7999}, "sdk": {"min": 3000, "max": 9999}, "library": {"min": 3000, "max": 9999}}
|
|
13
|
+
DEFAULTS = {}
|
|
14
|
+
|
|
15
|
+
# === END INLINED CONSTANTS ===
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
load("./constants.star",
|
|
19
|
+
"VALID_APP_TYPES",
|
|
20
|
+
"MANIFEST_DEFAULTS",
|
|
21
|
+
"VALIDATION_THRESHOLDS",
|
|
22
|
+
"MANIFEST_FILENAME",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
load("./schema.star", "ManifestSchema")
|
|
26
|
+
load("./errors.star", "ManifestErrors")
|
|
27
|
+
load("./parser.star", "extract_stack_from_path")
|
|
28
|
+
|
|
29
|
+
def validate(manifest, context=None, level='all'):
|
|
30
|
+
"""
|
|
31
|
+
Comprehensive manifest validation at specified level.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
manifest: Manifest dict to validate
|
|
35
|
+
context: Optional context dict:
|
|
36
|
+
- all_manifests: List of all manifests for cross-validation
|
|
37
|
+
- resource_path: Path for error context
|
|
38
|
+
- strict: Boolean for strict mode (treats warnings as errors)
|
|
39
|
+
level: Validation level
|
|
40
|
+
- 'schema': Structure and types only
|
|
41
|
+
- 'values': Value constraints
|
|
42
|
+
- 'cross_field': Cross-field dependencies
|
|
43
|
+
- 'cross_resource': Cross-resource dependencies (requires all_manifests)
|
|
44
|
+
- 'all': All validations (default)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
struct(
|
|
48
|
+
valid=True/False,
|
|
49
|
+
errors=[], # Critical errors
|
|
50
|
+
warnings=[], # Non-critical issues
|
|
51
|
+
stats={ # Validation statistics
|
|
52
|
+
'fields_checked': N,
|
|
53
|
+
'rules_passed': N,
|
|
54
|
+
'rules_failed': N,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
"""
|
|
58
|
+
context = context or {}
|
|
59
|
+
errors = []
|
|
60
|
+
warnings = []
|
|
61
|
+
stats = {'fields_checked': 0, 'rules_passed': 0, 'rules_failed': 0}
|
|
62
|
+
|
|
63
|
+
levels = ['schema', 'values', 'cross_field', 'cross_service']
|
|
64
|
+
if level == 'all':
|
|
65
|
+
levels_to_run = levels
|
|
66
|
+
elif level in levels:
|
|
67
|
+
levels_to_run = [level]
|
|
68
|
+
else:
|
|
69
|
+
return struct(
|
|
70
|
+
valid=False,
|
|
71
|
+
errors=[ManifestErrors.new(
|
|
72
|
+
message="Invalid validation level: " + level,
|
|
73
|
+
category=ManifestErrors.CATEGORY['CONFIG'],
|
|
74
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
75
|
+
)],
|
|
76
|
+
warnings=[],
|
|
77
|
+
stats=stats,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Run schema validation
|
|
81
|
+
if 'schema' in levels_to_run:
|
|
82
|
+
schema_result = _validate_schema(manifest, context)
|
|
83
|
+
errors.extend(schema_result.errors)
|
|
84
|
+
warnings.extend(schema_result.warnings)
|
|
85
|
+
stats['fields_checked'] += schema_result.stats['fields_checked']
|
|
86
|
+
stats['rules_passed'] += schema_result.stats['rules_passed']
|
|
87
|
+
stats['rules_failed'] += schema_result.stats['rules_failed']
|
|
88
|
+
|
|
89
|
+
# Run value validation
|
|
90
|
+
if 'values' in levels_to_run:
|
|
91
|
+
values_result = _validate_values(manifest, context)
|
|
92
|
+
errors.extend(values_result.errors)
|
|
93
|
+
warnings.extend(values_result.warnings)
|
|
94
|
+
stats['fields_checked'] += values_result.stats['fields_checked']
|
|
95
|
+
stats['rules_passed'] += values_result.stats['rules_passed']
|
|
96
|
+
stats['rules_failed'] += values_result.stats['rules_failed']
|
|
97
|
+
|
|
98
|
+
# Run cross-field validation
|
|
99
|
+
if 'cross_field' in levels_to_run:
|
|
100
|
+
cross_field_result = _validate_cross_field(manifest, context)
|
|
101
|
+
errors.extend(cross_field_result.errors)
|
|
102
|
+
warnings.extend(cross_field_result.warnings)
|
|
103
|
+
stats['fields_checked'] += cross_field_result.stats['fields_checked']
|
|
104
|
+
stats['rules_passed'] += cross_field_result.stats['rules_passed']
|
|
105
|
+
stats['rules_failed'] += cross_field_result.stats['rules_failed']
|
|
106
|
+
|
|
107
|
+
# Run cross-resource validation
|
|
108
|
+
if 'cross_resource' in levels_to_run and context.get('all_manifests'):
|
|
109
|
+
cross_resource_result = _validate_cross_resource(manifest, context)
|
|
110
|
+
errors.extend(cross_resource_result.errors)
|
|
111
|
+
warnings.extend(cross_resource_result.warnings)
|
|
112
|
+
stats['fields_checked'] += cross_resource_result.stats['fields_checked']
|
|
113
|
+
stats['rules_passed'] += cross_resource_result.stats['rules_passed']
|
|
114
|
+
stats['rules_failed'] += cross_resource_result.stats['rules_failed']
|
|
115
|
+
|
|
116
|
+
# In strict mode, treat warnings as errors
|
|
117
|
+
if context.get('strict'):
|
|
118
|
+
errors.extend([ManifestErrors.new(
|
|
119
|
+
message=w.message,
|
|
120
|
+
category=w.category,
|
|
121
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
122
|
+
context=w.context,
|
|
123
|
+
) for w in warnings])
|
|
124
|
+
warnings = []
|
|
125
|
+
|
|
126
|
+
valid = len([e for e in errors if e.severity == ManifestErrors.SEVERITY['CRITICAL'] or
|
|
127
|
+
e.severity == ManifestErrors.SEVERITY['ERROR']]) == 0
|
|
128
|
+
|
|
129
|
+
return struct(
|
|
130
|
+
valid=valid,
|
|
131
|
+
errors=errors,
|
|
132
|
+
warnings=warnings,
|
|
133
|
+
stats=stats,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _validate_schema(manifest, context):
|
|
137
|
+
"""
|
|
138
|
+
Level 1: Schema Validation
|
|
139
|
+
- Check all required fields present
|
|
140
|
+
- Validate field types
|
|
141
|
+
- Check field formats (regex patterns)
|
|
142
|
+
- Validate enum values
|
|
143
|
+
"""
|
|
144
|
+
errors = []
|
|
145
|
+
warnings = []
|
|
146
|
+
stats = {'fields_checked': 0, 'rules_passed': 0, 'rules_failed': 0}
|
|
147
|
+
|
|
148
|
+
if not manifest:
|
|
149
|
+
errors.append(ManifestErrors.new(
|
|
150
|
+
message="Manifest is empty or None",
|
|
151
|
+
category=ManifestErrors.CATEGORY['SCHEMA'],
|
|
152
|
+
severity=ManifestErrors.SEVERITY['CRITICAL'],
|
|
153
|
+
context={'path': context.get('resource_path', '')},
|
|
154
|
+
))
|
|
155
|
+
return struct(errors=errors, warnings=warnings, stats=stats)
|
|
156
|
+
|
|
157
|
+
# Check required fields
|
|
158
|
+
required_fields = ManifestSchema.get_required_fields(manifest)
|
|
159
|
+
for field_name in required_fields:
|
|
160
|
+
stats['fields_checked'] += 1
|
|
161
|
+
|
|
162
|
+
if field_name not in manifest or manifest[field_name] == None or manifest[field_name] == '':
|
|
163
|
+
errors.append(ManifestErrors.new(
|
|
164
|
+
message="Missing required field: " + field_name,
|
|
165
|
+
category=ManifestErrors.CATEGORY['SCHEMA'],
|
|
166
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
167
|
+
context={'field': field_name, 'path': context.get('resource_path', '')},
|
|
168
|
+
))
|
|
169
|
+
stats['rules_failed'] += 1
|
|
170
|
+
else:
|
|
171
|
+
stats['rules_passed'] += 1
|
|
172
|
+
|
|
173
|
+
# Validate field types and constraints
|
|
174
|
+
for field_name, value in manifest.items():
|
|
175
|
+
if field_name.startswith('_'): # Skip internal fields
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
field_schema = ManifestSchema.get_field(field_name)
|
|
179
|
+
if not field_schema:
|
|
180
|
+
# Unknown field - could be a warning
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
stats['fields_checked'] += 1
|
|
184
|
+
|
|
185
|
+
# Check field type
|
|
186
|
+
field_type = field_schema.get('type')
|
|
187
|
+
type_valid = _check_field_type(field_name, value, field_type, field_schema)
|
|
188
|
+
|
|
189
|
+
if not type_valid:
|
|
190
|
+
errors.append(ManifestErrors.new(
|
|
191
|
+
message="Field {} has invalid type".format(field_name),
|
|
192
|
+
category=ManifestErrors.CATEGORY['SCHEMA'],
|
|
193
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
194
|
+
context={'field': field_name, 'value': value},
|
|
195
|
+
))
|
|
196
|
+
stats['rules_failed'] += 1
|
|
197
|
+
else:
|
|
198
|
+
# Check constraints
|
|
199
|
+
constraints = field_schema.get('constraints', {})
|
|
200
|
+
constraint_result = _check_constraints(field_name, value, constraints)
|
|
201
|
+
|
|
202
|
+
if constraint_result:
|
|
203
|
+
errors.append(constraint_result)
|
|
204
|
+
stats['rules_failed'] += 1
|
|
205
|
+
else:
|
|
206
|
+
stats['rules_passed'] += 1
|
|
207
|
+
|
|
208
|
+
return struct(errors=errors, warnings=warnings, stats=stats)
|
|
209
|
+
|
|
210
|
+
def _validate_values(manifest, context):
|
|
211
|
+
"""
|
|
212
|
+
Level 2: Value Validation
|
|
213
|
+
- Port range validation
|
|
214
|
+
- Name format validation
|
|
215
|
+
- String length constraints
|
|
216
|
+
- Numeric range constraints
|
|
217
|
+
- Pattern matching
|
|
218
|
+
"""
|
|
219
|
+
errors = []
|
|
220
|
+
warnings = []
|
|
221
|
+
stats = {'fields_checked': 0, 'rules_passed': 0, 'rules_failed': 0}
|
|
222
|
+
|
|
223
|
+
# Validate port
|
|
224
|
+
if 'port' in manifest:
|
|
225
|
+
stats['fields_checked'] += 1
|
|
226
|
+
port = manifest['port']
|
|
227
|
+
app_type = manifest.get('appType', 'backend')
|
|
228
|
+
|
|
229
|
+
# Check basic range
|
|
230
|
+
if port < 1024 or port > 65535:
|
|
231
|
+
errors.append(ManifestErrors.new(
|
|
232
|
+
message="Port {} is outside valid range [1024, 65535]".format(port),
|
|
233
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
234
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
235
|
+
context={'field': 'port', 'value': port},
|
|
236
|
+
))
|
|
237
|
+
stats['rules_failed'] += 1
|
|
238
|
+
else:
|
|
239
|
+
# Check app type specific range
|
|
240
|
+
port_range = PORT_RANGES.get(app_type)
|
|
241
|
+
if port_range:
|
|
242
|
+
if port < port_range['min'] or port > port_range['max']:
|
|
243
|
+
warnings.append(ManifestErrors.new(
|
|
244
|
+
message="Port {} is outside recommended range for {} [{}-{}]".format(
|
|
245
|
+
port, app_type, port_range['min'], port_range['max']
|
|
246
|
+
),
|
|
247
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
248
|
+
severity=ManifestErrors.SEVERITY['WARNING'],
|
|
249
|
+
context={'field': 'port', 'value': port, 'appType': app_type},
|
|
250
|
+
))
|
|
251
|
+
stats['rules_passed'] += 1
|
|
252
|
+
else:
|
|
253
|
+
stats['rules_passed'] += 1
|
|
254
|
+
else:
|
|
255
|
+
stats['rules_passed'] += 1
|
|
256
|
+
|
|
257
|
+
# Validate appName format
|
|
258
|
+
if 'appName' in manifest:
|
|
259
|
+
stats['fields_checked'] += 1
|
|
260
|
+
app_name = manifest['appName']
|
|
261
|
+
|
|
262
|
+
# Check kebab-case pattern
|
|
263
|
+
if not _matches_pattern(app_name, r'^[a-z][a-z0-9-]*$'):
|
|
264
|
+
errors.append(ManifestErrors.new(
|
|
265
|
+
message="Invalid appName format: {} (must be kebab-case)".format(app_name),
|
|
266
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
267
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
268
|
+
context={'field': 'appName', 'value': app_name},
|
|
269
|
+
))
|
|
270
|
+
stats['rules_failed'] += 1
|
|
271
|
+
else:
|
|
272
|
+
# Check length
|
|
273
|
+
if len(app_name) < 3:
|
|
274
|
+
errors.append(ManifestErrors.new(
|
|
275
|
+
message="appName too short (min 3 characters): {}".format(app_name),
|
|
276
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
277
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
278
|
+
context={'field': 'appName', 'value': app_name, 'min_length': 3},
|
|
279
|
+
))
|
|
280
|
+
stats['rules_failed'] += 1
|
|
281
|
+
elif len(app_name) > 50:
|
|
282
|
+
errors.append(ManifestErrors.new(
|
|
283
|
+
message="appName too long (max 50 characters): {}".format(app_name),
|
|
284
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
285
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
286
|
+
context={'field': 'appName', 'value': app_name, 'max_length': 50},
|
|
287
|
+
))
|
|
288
|
+
stats['rules_failed'] += 1
|
|
289
|
+
else:
|
|
290
|
+
stats['rules_passed'] += 1
|
|
291
|
+
|
|
292
|
+
# Validate replicas
|
|
293
|
+
if 'replicas' in manifest:
|
|
294
|
+
stats['fields_checked'] += 1
|
|
295
|
+
replicas = manifest['replicas']
|
|
296
|
+
|
|
297
|
+
if replicas < 1:
|
|
298
|
+
errors.append(ManifestErrors.new(
|
|
299
|
+
message="replicas must be at least 1: {}".format(replicas),
|
|
300
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
301
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
302
|
+
context={'field': 'replicas', 'value': replicas},
|
|
303
|
+
))
|
|
304
|
+
stats['rules_failed'] += 1
|
|
305
|
+
elif replicas > VALIDATION_THRESHOLDS['max_replicas']:
|
|
306
|
+
errors.append(ManifestErrors.new(
|
|
307
|
+
message="replicas exceeds maximum ({}): {}".format(
|
|
308
|
+
VALIDATION_THRESHOLDS['max_replicas'], replicas
|
|
309
|
+
),
|
|
310
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
311
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
312
|
+
context={'field': 'replicas', 'value': replicas, 'max': VALIDATION_THRESHOLDS['max_replicas']},
|
|
313
|
+
))
|
|
314
|
+
stats['rules_failed'] += 1
|
|
315
|
+
else:
|
|
316
|
+
stats['rules_passed'] += 1
|
|
317
|
+
|
|
318
|
+
# Validate features length
|
|
319
|
+
if 'features' in manifest:
|
|
320
|
+
stats['fields_checked'] += 1
|
|
321
|
+
features = manifest['features']
|
|
322
|
+
|
|
323
|
+
if len(features) > VALIDATION_THRESHOLDS['max_features']:
|
|
324
|
+
warnings.append(ManifestErrors.new(
|
|
325
|
+
message="Too many features ({}), consider splitting resource".format(len(features)),
|
|
326
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
327
|
+
severity=ManifestErrors.SEVERITY['WARNING'],
|
|
328
|
+
context={'field': 'features', 'count': len(features), 'max': VALIDATION_THRESHOLDS['max_features']},
|
|
329
|
+
))
|
|
330
|
+
else:
|
|
331
|
+
stats['rules_passed'] += 1
|
|
332
|
+
|
|
333
|
+
return struct(errors=errors, warnings=warnings, stats=stats)
|
|
334
|
+
|
|
335
|
+
def _validate_cross_field(manifest, context):
|
|
336
|
+
"""
|
|
337
|
+
Level 3: Cross-Field Validation
|
|
338
|
+
- Frontend requires backend
|
|
339
|
+
- Prisma requires database
|
|
340
|
+
- Port conflicts with app type
|
|
341
|
+
- Dependencies valid
|
|
342
|
+
"""
|
|
343
|
+
errors = []
|
|
344
|
+
warnings = []
|
|
345
|
+
stats = {'fields_checked': 0, 'rules_passed': 0, 'rules_failed': 0}
|
|
346
|
+
|
|
347
|
+
app_type = manifest.get('appType', '')
|
|
348
|
+
|
|
349
|
+
# Frontend requires backendName
|
|
350
|
+
if app_type == 'frontend':
|
|
351
|
+
stats['fields_checked'] += 1
|
|
352
|
+
|
|
353
|
+
if not manifest.get('backendName'):
|
|
354
|
+
errors.append(ManifestErrors.new(
|
|
355
|
+
message="Frontend resource must specify backendName",
|
|
356
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
357
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
358
|
+
context={'field': 'backendName', 'appType': 'frontend'},
|
|
359
|
+
))
|
|
360
|
+
stats['rules_failed'] += 1
|
|
361
|
+
else:
|
|
362
|
+
stats['rules_passed'] += 1
|
|
363
|
+
|
|
364
|
+
# Frontend should have basePath
|
|
365
|
+
stats['fields_checked'] += 1
|
|
366
|
+
if not manifest.get('basePath'):
|
|
367
|
+
warnings.append(ManifestErrors.new(
|
|
368
|
+
message="Frontend resource should specify basePath for routing",
|
|
369
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
370
|
+
severity=ManifestErrors.SEVERITY['WARNING'],
|
|
371
|
+
context={'field': 'basePath', 'appType': 'frontend'},
|
|
372
|
+
))
|
|
373
|
+
else:
|
|
374
|
+
stats['rules_passed'] += 1
|
|
375
|
+
|
|
376
|
+
# Prisma requires databaseName
|
|
377
|
+
features = manifest.get('features', [])
|
|
378
|
+
if 'prisma' in features:
|
|
379
|
+
stats['fields_checked'] += 1
|
|
380
|
+
|
|
381
|
+
if not manifest.get('databaseName'):
|
|
382
|
+
warnings.append(ManifestErrors.new(
|
|
383
|
+
message="Resource with 'prisma' feature should specify databaseName",
|
|
384
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
385
|
+
severity=ManifestErrors.SEVERITY['WARNING'],
|
|
386
|
+
context={'field': 'databaseName', 'features': features},
|
|
387
|
+
))
|
|
388
|
+
else:
|
|
389
|
+
stats['rules_passed'] += 1
|
|
390
|
+
|
|
391
|
+
# Backend should have Traefik config
|
|
392
|
+
if app_type == 'backend':
|
|
393
|
+
stats['fields_checked'] += 1
|
|
394
|
+
|
|
395
|
+
if not manifest.get('traefik'):
|
|
396
|
+
warnings.append(ManifestErrors.new(
|
|
397
|
+
message="Backend resource should have Traefik configuration for routing",
|
|
398
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
399
|
+
severity=ManifestErrors.SEVERITY['WARNING'],
|
|
400
|
+
context={'field': 'traefik', 'appType': 'backend'},
|
|
401
|
+
))
|
|
402
|
+
else:
|
|
403
|
+
stats['rules_passed'] += 1
|
|
404
|
+
|
|
405
|
+
return struct(errors=errors, warnings=warnings, stats=stats)
|
|
406
|
+
|
|
407
|
+
def _validate_cross_resource(manifest, context):
|
|
408
|
+
"""
|
|
409
|
+
Level 4: Cross-Resource Validation
|
|
410
|
+
- dependsOn exist as resources
|
|
411
|
+
- No circular dependencies
|
|
412
|
+
- Backend references are valid
|
|
413
|
+
- Port uniqueness across resources
|
|
414
|
+
"""
|
|
415
|
+
errors = []
|
|
416
|
+
warnings = []
|
|
417
|
+
stats = {'fields_checked': 0, 'rules_passed': 0, 'rules_failed': 0}
|
|
418
|
+
|
|
419
|
+
all_manifests = context.get('all_manifests', [])
|
|
420
|
+
resource_path = context.get('resource_path', '')
|
|
421
|
+
|
|
422
|
+
if not all_manifests:
|
|
423
|
+
return struct(errors=errors, warnings=warnings, stats=stats)
|
|
424
|
+
|
|
425
|
+
# Build lookup maps
|
|
426
|
+
app_names = {}
|
|
427
|
+
ports = {}
|
|
428
|
+
|
|
429
|
+
for m in all_manifests:
|
|
430
|
+
name = m.get('appName', '')
|
|
431
|
+
if name:
|
|
432
|
+
app_names[name] = m
|
|
433
|
+
|
|
434
|
+
port = m.get('port')
|
|
435
|
+
if port:
|
|
436
|
+
if port not in ports:
|
|
437
|
+
ports[port] = []
|
|
438
|
+
ports[port].append(name)
|
|
439
|
+
|
|
440
|
+
current_name = manifest.get('appName', '')
|
|
441
|
+
current_port = manifest.get('port')
|
|
442
|
+
|
|
443
|
+
# Check dependsOn exist
|
|
444
|
+
internal_deps = manifest.get('dependsOn', [])
|
|
445
|
+
if internal_deps:
|
|
446
|
+
stats['fields_checked'] += 1
|
|
447
|
+
|
|
448
|
+
missing_deps = []
|
|
449
|
+
for dep in internal_deps:
|
|
450
|
+
if dep not in app_names:
|
|
451
|
+
missing_deps.append(dep)
|
|
452
|
+
|
|
453
|
+
if missing_deps:
|
|
454
|
+
errors.append(ManifestErrors.new(
|
|
455
|
+
message="Missing resource dependencies: {}".format(', '.join(missing_deps)),
|
|
456
|
+
category=ManifestErrors.CATEGORY['DEPENDENCY'],
|
|
457
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
458
|
+
context={'field': 'dependsOn', 'missing': missing_deps},
|
|
459
|
+
))
|
|
460
|
+
stats['rules_failed'] += 1
|
|
461
|
+
else:
|
|
462
|
+
stats['rules_passed'] += 1
|
|
463
|
+
|
|
464
|
+
# Check backendName exists (for frontends)
|
|
465
|
+
if manifest.get('appType') == 'frontend':
|
|
466
|
+
backend_name = manifest.get('backendName', '')
|
|
467
|
+
if backend_name:
|
|
468
|
+
stats['fields_checked'] += 1
|
|
469
|
+
|
|
470
|
+
if backend_name not in app_names:
|
|
471
|
+
errors.append(ManifestErrors.new(
|
|
472
|
+
message="Referenced backend '{}' does not exist".format(backend_name),
|
|
473
|
+
category=ManifestErrors.CATEGORY['DEPENDENCY'],
|
|
474
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
475
|
+
context={'field': 'backendName', 'value': backend_name},
|
|
476
|
+
))
|
|
477
|
+
stats['rules_failed'] += 1
|
|
478
|
+
else:
|
|
479
|
+
# Check that backend is actually a backend
|
|
480
|
+
backend_manifest = app_names[backend_name]
|
|
481
|
+
if backend_manifest.get('appType') != 'backend':
|
|
482
|
+
errors.append(ManifestErrors.new(
|
|
483
|
+
message="Referenced backend '{}' is not a backend (type: {})".format(
|
|
484
|
+
backend_name, backend_manifest.get('appType')
|
|
485
|
+
),
|
|
486
|
+
category=ManifestErrors.CATEGORY['DEPENDENCY'],
|
|
487
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
488
|
+
context={'field': 'backendName', 'value': backend_name},
|
|
489
|
+
))
|
|
490
|
+
stats['rules_failed'] += 1
|
|
491
|
+
else:
|
|
492
|
+
stats['rules_passed'] += 1
|
|
493
|
+
|
|
494
|
+
# Check port uniqueness
|
|
495
|
+
if current_port and current_port in ports:
|
|
496
|
+
resources_with_port = ports[current_port]
|
|
497
|
+
other_resources = [r for r in resources_with_port if r != current_name]
|
|
498
|
+
|
|
499
|
+
if other_resources:
|
|
500
|
+
stats['fields_checked'] += 1
|
|
501
|
+
|
|
502
|
+
errors.append(ManifestErrors.new(
|
|
503
|
+
message="Port {} is already used by: {}".format(current_port, ', '.join(other_resources)),
|
|
504
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
505
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
506
|
+
context={'field': 'port', 'value': current_port, 'conflicts': other_resources},
|
|
507
|
+
))
|
|
508
|
+
stats['rules_failed'] += 1
|
|
509
|
+
else:
|
|
510
|
+
stats['rules_passed'] += 1
|
|
511
|
+
|
|
512
|
+
return struct(errors=errors, warnings=warnings, stats=stats)
|
|
513
|
+
|
|
514
|
+
def _check_field_type(field_name, value, expected_type, field_schema):
|
|
515
|
+
"""Check if value matches expected type."""
|
|
516
|
+
if expected_type == 'string':
|
|
517
|
+
return type(value) == "string"
|
|
518
|
+
elif expected_type == 'integer':
|
|
519
|
+
return type(value) == "int"
|
|
520
|
+
elif expected_type == 'boolean':
|
|
521
|
+
return type(value) == "bool"
|
|
522
|
+
elif expected_type == 'list':
|
|
523
|
+
return type(value) == "list"
|
|
524
|
+
elif expected_type == 'dict':
|
|
525
|
+
return type(value) == "dict"
|
|
526
|
+
elif expected_type == 'enum':
|
|
527
|
+
valid_values = field_schema.get('constraints', {}).get('values', [])
|
|
528
|
+
return value in valid_values
|
|
529
|
+
return True
|
|
530
|
+
|
|
531
|
+
def _check_constraints(field_name, value, constraints):
|
|
532
|
+
"""Check value against constraints."""
|
|
533
|
+
# Check pattern
|
|
534
|
+
if 'pattern' in constraints:
|
|
535
|
+
pattern = constraints['pattern']
|
|
536
|
+
if not _matches_pattern(value, pattern):
|
|
537
|
+
return ManifestErrors.new(
|
|
538
|
+
message="Field {} value does not match pattern: {}".format(field_name, pattern),
|
|
539
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
540
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
541
|
+
context={'field': field_name, 'value': value, 'pattern': pattern},
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
# Check length (for strings and lists)
|
|
545
|
+
if 'min_length' in constraints:
|
|
546
|
+
if len(value) < constraints['min_length']:
|
|
547
|
+
return ManifestErrors.new(
|
|
548
|
+
message="Field {} value too short (min {}): {}".format(
|
|
549
|
+
field_name, constraints['min_length'], len(value)
|
|
550
|
+
),
|
|
551
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
552
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
553
|
+
context={'field': field_name, 'value': value, 'min_length': constraints['min_length']},
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
if 'max_length' in constraints:
|
|
557
|
+
if len(value) > constraints['max_length']:
|
|
558
|
+
return ManifestErrors.new(
|
|
559
|
+
message="Field {} value too long (max {}): {}".format(
|
|
560
|
+
field_name, constraints['max_length'], len(value)
|
|
561
|
+
),
|
|
562
|
+
category=ManifestErrors.CATEGORY['VALIDATION'],
|
|
563
|
+
severity=ManifestErrors.SEVERITY['ERROR'],
|
|
564
|
+
context={'field': field_name, 'value': value, 'max_length': constraints['max_length']},
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
return None
|
|
568
|
+
|
|
569
|
+
def _matches_pattern(value, pattern):
|
|
570
|
+
"""Simple pattern matching (Starlark doesn't have regex)."""
|
|
571
|
+
# For now, do basic checks
|
|
572
|
+
if pattern == r'^[a-z][a-z0-9-]*$':
|
|
573
|
+
# Kebab-case: starts with letter, then letters/digits/hyphens
|
|
574
|
+
if not value:
|
|
575
|
+
return False
|
|
576
|
+
if not (value[0] >= 'a' and value[0] <= 'z'):
|
|
577
|
+
return False
|
|
578
|
+
for c in value:
|
|
579
|
+
if not ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '-'):
|
|
580
|
+
return False
|
|
581
|
+
return True
|
|
582
|
+
|
|
583
|
+
if pattern == r'^[a-z][a-z0-9_]*$':
|
|
584
|
+
# Snake-case for database names
|
|
585
|
+
if not value:
|
|
586
|
+
return False
|
|
587
|
+
if not (value[0] >= 'a' and value[0] <= 'z'):
|
|
588
|
+
return False
|
|
589
|
+
for c in value:
|
|
590
|
+
if not ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_'):
|
|
591
|
+
return False
|
|
592
|
+
return True
|
|
593
|
+
|
|
594
|
+
if pattern == r'^/[a-z][a-z0-9/-]*$':
|
|
595
|
+
# Path prefix: starts with /, then letters/digits/hyphens/slashes
|
|
596
|
+
if not value or value[0] != '/':
|
|
597
|
+
return False
|
|
598
|
+
for c in value[1:]:
|
|
599
|
+
if not ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '-' or c == '/'):
|
|
600
|
+
return False
|
|
601
|
+
return True
|
|
602
|
+
|
|
603
|
+
# Default: accept
|
|
604
|
+
return True
|
|
605
|
+
|
|
606
|
+
def is_valid(manifest, context=None):
|
|
607
|
+
"""
|
|
608
|
+
Quick check if manifest is valid.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
manifest: Manifest dict to check
|
|
612
|
+
context: Optional context for validation
|
|
613
|
+
|
|
614
|
+
Returns:
|
|
615
|
+
True if valid (no critical errors)
|
|
616
|
+
"""
|
|
617
|
+
result = validate(manifest, context, level='all')
|
|
618
|
+
return result.valid
|
|
619
|
+
|
|
620
|
+
def get_validation_report(manifest, context=None):
|
|
621
|
+
"""
|
|
622
|
+
Generate detailed validation report.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
manifest: Manifest dict to validate
|
|
626
|
+
context: Optional context for validation
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
Human-readable report string
|
|
630
|
+
"""
|
|
631
|
+
result = validate(manifest, context, level='all')
|
|
632
|
+
|
|
633
|
+
lines = []
|
|
634
|
+
lines.append("=" * 70)
|
|
635
|
+
lines.append("📋 MANIFEST VALIDATION REPORT")
|
|
636
|
+
lines.append("=" * 70)
|
|
637
|
+
|
|
638
|
+
# Summary
|
|
639
|
+
status = "✅ PASSED" if result.valid else "❌ FAILED"
|
|
640
|
+
lines.append("Status: " + status)
|
|
641
|
+
lines.append("")
|
|
642
|
+
|
|
643
|
+
# Statistics
|
|
644
|
+
lines.append("Fields Checked: {}".format(result.stats['fields_checked']))
|
|
645
|
+
lines.append("Rules Passed: {}".format(result.stats['rules_passed']))
|
|
646
|
+
lines.append("Rules Failed: {}".format(result.stats['rules_failed']))
|
|
647
|
+
lines.append("")
|
|
648
|
+
|
|
649
|
+
# Errors
|
|
650
|
+
if result.errors:
|
|
651
|
+
lines.append("ERRORS ({}):".format(len(result.errors)))
|
|
652
|
+
for error in result.errors:
|
|
653
|
+
formatted = ManifestErrors.format(error)
|
|
654
|
+
lines.append(" - " + formatted)
|
|
655
|
+
lines.append("")
|
|
656
|
+
|
|
657
|
+
# Warnings
|
|
658
|
+
if result.warnings:
|
|
659
|
+
lines.append("WARNINGS ({}):".format(len(result.warnings)))
|
|
660
|
+
for warning in result.warnings:
|
|
661
|
+
formatted = ManifestErrors.format(warning)
|
|
662
|
+
lines.append(" - " + formatted)
|
|
663
|
+
lines.append("")
|
|
664
|
+
|
|
665
|
+
lines.append("=" * 70)
|
|
666
|
+
|
|
667
|
+
return "\n".join(lines)
|
|
668
|
+
|
|
669
|
+
def validate_schema(manifest):
|
|
670
|
+
"""
|
|
671
|
+
Quick schema-only validation.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
manifest: Manifest dict
|
|
675
|
+
|
|
676
|
+
Returns:
|
|
677
|
+
Validation result struct
|
|
678
|
+
"""
|
|
679
|
+
return validate(manifest, level='schema')
|
|
680
|
+
|
|
681
|
+
def validate_values(manifest):
|
|
682
|
+
"""
|
|
683
|
+
Quick values-only validation.
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
manifest: Manifest dict
|
|
687
|
+
|
|
688
|
+
Returns:
|
|
689
|
+
Validation result struct
|
|
690
|
+
"""
|
|
691
|
+
return validate(manifest, level='values')
|
|
692
|
+
|
|
693
|
+
def validate_dependencies(manifest, all_manifests):
|
|
694
|
+
"""
|
|
695
|
+
Validate dependencies against all resources.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
manifest: Manifest to validate
|
|
699
|
+
all_manifests: List of all manifests for reference
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
Validation result struct
|
|
703
|
+
"""
|
|
704
|
+
context = {'all_manifests': all_manifests}
|
|
705
|
+
return validate(manifest, context, level='cross_resource')
|
|
706
|
+
|
|
707
|
+
# Export validator functions
|
|
708
|
+
ManifestValidator = struct(
|
|
709
|
+
validate=validate,
|
|
710
|
+
is_valid=is_valid,
|
|
711
|
+
get_report=get_validation_report,
|
|
712
|
+
validate_schema=validate_schema,
|
|
713
|
+
validate_values=validate_values,
|
|
714
|
+
validate_dependencies=validate_dependencies,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
VALIDATION = {}
|