@tdk-landscape/tdk-cli-core 1.3.14 → 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/README.md +3 -3
- 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 +62 -21
- 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/paths.js +1 -1
- package/dist/utils/paths.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,469 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ConfigService - File operations for Config Inspector
|
|
4
|
+
Handles safe file reading/writing with validation, backups, and conflict detection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import hashlib
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Optional, Tuple, NamedTuple
|
|
13
|
+
|
|
14
|
+
from file_utils import PROJECT_ROOT, FileUtils
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SaveResult(NamedTuple):
|
|
18
|
+
"""Result of a save operation"""
|
|
19
|
+
success: bool
|
|
20
|
+
message: str
|
|
21
|
+
backup_path: Optional[Path] = None
|
|
22
|
+
error_details: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigService:
|
|
26
|
+
"""
|
|
27
|
+
Service layer for config file operations.
|
|
28
|
+
Separates file operations from HTTP handling.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# Allowed paths for write operations
|
|
32
|
+
# NOTE: These are paths within the TDK CLI installation
|
|
33
|
+
ALLOWED_PATH_PREFIXES = [
|
|
34
|
+
'engine/topologies/',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
# Master configs allowed at project root
|
|
38
|
+
ALLOWED_ROOT_CONFIGS = [
|
|
39
|
+
'TILT_RESOURCE_DEFAULTS.star',
|
|
40
|
+
'spec.master',
|
|
41
|
+
'engine/spec.master',
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
# Allowed file extensions
|
|
45
|
+
ALLOWED_EXTENSIONS = ['.star']
|
|
46
|
+
|
|
47
|
+
# Special filenames that are allowed without extension check
|
|
48
|
+
ALLOWED_FILENAMES = ['spec.master']
|
|
49
|
+
|
|
50
|
+
# File size limits
|
|
51
|
+
MAX_FILE_SIZE = 1024 * 1024 # 1MB
|
|
52
|
+
MAX_CONTENT_LENGTH = 1024 * 1024 # 1MB for request body
|
|
53
|
+
|
|
54
|
+
# Backup settings
|
|
55
|
+
BACKUP_DIR_NAME = '.backups'
|
|
56
|
+
MAX_BACKUPS = 3
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def validate_write_path(path: str) -> Optional[Path]:
|
|
60
|
+
"""
|
|
61
|
+
Validate that a file path is safe for writing.
|
|
62
|
+
|
|
63
|
+
Security: Prevents path traversal attacks by using os.path.realpath()
|
|
64
|
+
and ensuring the resolved path is within allowed directories.
|
|
65
|
+
|
|
66
|
+
Allowed paths:
|
|
67
|
+
- Within engine/topologies/
|
|
68
|
+
- Project root master configs: TILT_RESOURCE_DEFAULTS.star, spec.master, engine/spec.master
|
|
69
|
+
|
|
70
|
+
Returns: Absolute Path if valid, None if invalid.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
# Handle relative paths
|
|
74
|
+
if not path.startswith('/'):
|
|
75
|
+
target = PROJECT_ROOT / path
|
|
76
|
+
else:
|
|
77
|
+
target = Path(path)
|
|
78
|
+
|
|
79
|
+
# Resolve to absolute path using realpath (follows symlinks, removes ..)
|
|
80
|
+
resolved = Path(os.path.realpath(target))
|
|
81
|
+
project_root_real = Path(os.path.realpath(PROJECT_ROOT))
|
|
82
|
+
|
|
83
|
+
# Check if path is within project root (prevent path traversal)
|
|
84
|
+
try:
|
|
85
|
+
resolved.relative_to(project_root_real)
|
|
86
|
+
except ValueError:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
# Get the relative path from project root
|
|
90
|
+
rel_path = resolved.relative_to(project_root_real)
|
|
91
|
+
rel_path_str = str(rel_path)
|
|
92
|
+
|
|
93
|
+
# Check if it's one of the allowed root configs
|
|
94
|
+
if rel_path_str in ConfigService.ALLOWED_ROOT_CONFIGS:
|
|
95
|
+
return resolved
|
|
96
|
+
|
|
97
|
+
# Check if it's within allowed prefixes
|
|
98
|
+
for prefix in ConfigService.ALLOWED_PATH_PREFIXES:
|
|
99
|
+
if rel_path_str.startswith(prefix):
|
|
100
|
+
return resolved
|
|
101
|
+
|
|
102
|
+
# Path is not in any allowed location
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
except (ValueError, OSError, RuntimeError):
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def validate_file_type(path: str) -> Tuple[bool, str]:
|
|
110
|
+
"""
|
|
111
|
+
Validate that a file has an allowed extension or filename.
|
|
112
|
+
|
|
113
|
+
Allowed:
|
|
114
|
+
- Files with .star extension
|
|
115
|
+
- Files named exactly 'spec.master'
|
|
116
|
+
|
|
117
|
+
Returns: (is_valid, message)
|
|
118
|
+
"""
|
|
119
|
+
path_obj = Path(path)
|
|
120
|
+
filename = path_obj.name
|
|
121
|
+
extension = path_obj.suffix.lower()
|
|
122
|
+
|
|
123
|
+
# Check if filename is in allowed list (spec.master)
|
|
124
|
+
if filename in ConfigService.ALLOWED_FILENAMES:
|
|
125
|
+
return True, "Valid"
|
|
126
|
+
|
|
127
|
+
# Check extension
|
|
128
|
+
if extension in ConfigService.ALLOWED_EXTENSIONS:
|
|
129
|
+
return True, "Valid"
|
|
130
|
+
|
|
131
|
+
return False, f"Invalid file type: only {', '.join(ConfigService.ALLOWED_EXTENSIONS)} and {', '.join(ConfigService.ALLOWED_FILENAMES)} are allowed"
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def check_file_size(path: Path) -> Tuple[bool, str]:
|
|
135
|
+
"""
|
|
136
|
+
Check if a file size is within the allowed limit.
|
|
137
|
+
|
|
138
|
+
Returns: (is_valid, message)
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
if not path.exists():
|
|
142
|
+
# For new files, this is ok
|
|
143
|
+
return True, "Valid"
|
|
144
|
+
|
|
145
|
+
size = path.stat().st_size
|
|
146
|
+
if size > ConfigService.MAX_FILE_SIZE:
|
|
147
|
+
return False, f"File too large: {size / (1024*1024):.1f}MB (max {ConfigService.MAX_FILE_SIZE / (1024*1024):.1f}MB)"
|
|
148
|
+
|
|
149
|
+
return True, "Valid"
|
|
150
|
+
except (OSError, IOError) as e:
|
|
151
|
+
return False, f"Cannot check file size: {e}"
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def compute_content_hash(content: str) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Compute SHA-256 hash of content for conflict detection.
|
|
157
|
+
|
|
158
|
+
Returns: Hex digest of the hash.
|
|
159
|
+
"""
|
|
160
|
+
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def check_conflict(path: Path, original_hash: str) -> Tuple[bool, str]:
|
|
164
|
+
"""
|
|
165
|
+
Check if file has been modified since it was loaded (conflict detection).
|
|
166
|
+
|
|
167
|
+
Uses SHA-256 content hash comparison instead of timestamps to avoid TOCTOU race conditions.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
path: Path to the file
|
|
171
|
+
original_hash: SHA-256 hash of the content when it was loaded
|
|
172
|
+
|
|
173
|
+
Returns: (has_conflict, message)
|
|
174
|
+
- has_conflict: True if file has changed, False if it matches original_hash
|
|
175
|
+
- message: Human-readable description
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
if not path.exists():
|
|
179
|
+
return True, "File has been deleted"
|
|
180
|
+
|
|
181
|
+
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
|
182
|
+
current_content = f.read()
|
|
183
|
+
|
|
184
|
+
current_hash = ConfigService.compute_content_hash(current_content)
|
|
185
|
+
|
|
186
|
+
if current_hash != original_hash:
|
|
187
|
+
return True, "File has been modified by another process"
|
|
188
|
+
|
|
189
|
+
return False, "No conflict detected"
|
|
190
|
+
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return True, f"Cannot check for conflicts: {e}"
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def create_backup_with_rotation(file_path: Path, max_backups: int = MAX_BACKUPS) -> Tuple[Optional[Path], str]:
|
|
196
|
+
"""
|
|
197
|
+
Create a backup of a file with rotation (keep only last N backups).
|
|
198
|
+
|
|
199
|
+
Backups are stored in a .backups/ subdirectory next to the file.
|
|
200
|
+
Old backups are automatically deleted when exceeding max_backups.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
file_path: Path to the file to backup
|
|
204
|
+
max_backups: Maximum number of backups to keep (default: 3)
|
|
205
|
+
|
|
206
|
+
Returns: (backup_path, message)
|
|
207
|
+
- backup_path: Path to the created backup, or None if backup failed
|
|
208
|
+
- message: Description of what happened
|
|
209
|
+
"""
|
|
210
|
+
try:
|
|
211
|
+
if not file_path.exists():
|
|
212
|
+
return None, "File does not exist, no backup needed"
|
|
213
|
+
|
|
214
|
+
# Create backup directory
|
|
215
|
+
backup_dir = file_path.parent / ConfigService.BACKUP_DIR_NAME
|
|
216
|
+
backup_dir.mkdir(exist_ok=True)
|
|
217
|
+
|
|
218
|
+
# Generate backup filename with timestamp
|
|
219
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
220
|
+
backup_name = f"{file_path.name}.{timestamp}.backup"
|
|
221
|
+
backup_path = backup_dir / backup_name
|
|
222
|
+
|
|
223
|
+
# Copy file to backup
|
|
224
|
+
import shutil
|
|
225
|
+
shutil.copy2(file_path, backup_path)
|
|
226
|
+
|
|
227
|
+
# Clean up old backups
|
|
228
|
+
existing_backups = sorted(
|
|
229
|
+
backup_dir.glob(f"{file_path.name}.*.backup"),
|
|
230
|
+
key=lambda p: p.stat().st_mtime,
|
|
231
|
+
reverse=True # Newest first
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Remove excess backups
|
|
235
|
+
removed_count = 0
|
|
236
|
+
for old_backup in existing_backups[max_backups:]:
|
|
237
|
+
old_backup.unlink()
|
|
238
|
+
removed_count += 1
|
|
239
|
+
|
|
240
|
+
msg = f"Backup created: {backup_path.name}"
|
|
241
|
+
if removed_count > 0:
|
|
242
|
+
msg += f" (removed {removed_count} old backup(s))"
|
|
243
|
+
|
|
244
|
+
return backup_path, msg
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
return None, f"Failed to create backup: {e}"
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def atomic_write(file_path: Path, content: str, original_hash: str = None,
|
|
251
|
+
create_backup: bool = True) -> SaveResult:
|
|
252
|
+
"""
|
|
253
|
+
Atomically write content to a file with backup and conflict detection.
|
|
254
|
+
|
|
255
|
+
Uses write-to-temp-then-rename pattern for atomic writes.
|
|
256
|
+
If the write fails, the temp file is preserved for recovery.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
file_path: Path to write to
|
|
260
|
+
content: Content to write
|
|
261
|
+
original_hash: If provided, check for conflicts before writing (SHA-256 hash)
|
|
262
|
+
create_backup: Whether to create a backup before writing (default: True)
|
|
263
|
+
|
|
264
|
+
Returns: SaveResult with success status, message, and details
|
|
265
|
+
"""
|
|
266
|
+
temp_file = None
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
# Validate file size
|
|
270
|
+
content_bytes = content.encode('utf-8')
|
|
271
|
+
if len(content_bytes) > ConfigService.MAX_FILE_SIZE:
|
|
272
|
+
return SaveResult(
|
|
273
|
+
success=False,
|
|
274
|
+
message=f"Content too large: {len(content_bytes) / (1024*1024):.1f}MB",
|
|
275
|
+
error_details=f"Maximum file size is {ConfigService.MAX_FILE_SIZE / (1024*1024):.1f}MB"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Check for conflicts if hash provided
|
|
279
|
+
if original_hash and file_path.exists():
|
|
280
|
+
has_conflict, conflict_msg = ConfigService.check_conflict(file_path, original_hash)
|
|
281
|
+
if has_conflict:
|
|
282
|
+
return SaveResult(
|
|
283
|
+
success=False,
|
|
284
|
+
message=f"Conflict detected: {conflict_msg}",
|
|
285
|
+
error_details="The file has been modified since you loaded it. Please reload and try again."
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Create backup if requested and file exists
|
|
289
|
+
backup_path = None
|
|
290
|
+
backup_msg = ""
|
|
291
|
+
if create_backup and file_path.exists():
|
|
292
|
+
backup_path, backup_msg = ConfigService.create_backup_with_rotation(file_path)
|
|
293
|
+
if backup_path is None:
|
|
294
|
+
# Backup failed but we can still try to write
|
|
295
|
+
backup_msg = "Warning: Backup creation failed, but continuing with write"
|
|
296
|
+
|
|
297
|
+
# Atomic write: write to temp file, then rename
|
|
298
|
+
temp_fd, temp_path = tempfile.mkstemp(
|
|
299
|
+
dir=file_path.parent,
|
|
300
|
+
prefix=f".tmp_{file_path.name}_",
|
|
301
|
+
suffix='.tmp'
|
|
302
|
+
)
|
|
303
|
+
temp_file = Path(temp_path)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
# Write content to temp file
|
|
307
|
+
with os.fdopen(temp_fd, 'w', encoding='utf-8') as f:
|
|
308
|
+
f.write(content)
|
|
309
|
+
|
|
310
|
+
# Ensure data is written to disk
|
|
311
|
+
os.fsync(temp_fd)
|
|
312
|
+
|
|
313
|
+
# Atomic rename (POSIX guarantees this is atomic)
|
|
314
|
+
temp_file.rename(file_path)
|
|
315
|
+
|
|
316
|
+
# Sync parent directory to ensure rename is persisted
|
|
317
|
+
dir_fd = os.open(file_path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
|
318
|
+
try:
|
|
319
|
+
os.fsync(dir_fd)
|
|
320
|
+
finally:
|
|
321
|
+
os.close(dir_fd)
|
|
322
|
+
|
|
323
|
+
success_msg = "File saved successfully"
|
|
324
|
+
if backup_msg:
|
|
325
|
+
success_msg += f". {backup_msg}"
|
|
326
|
+
|
|
327
|
+
return SaveResult(
|
|
328
|
+
success=True,
|
|
329
|
+
message=success_msg,
|
|
330
|
+
backup_path=backup_path
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
except Exception as e:
|
|
334
|
+
# If rename failed, temp file still exists for recovery
|
|
335
|
+
return SaveResult(
|
|
336
|
+
success=False,
|
|
337
|
+
message=f"Failed to write file: {e}",
|
|
338
|
+
error_details=f"Temp file preserved at: {temp_file}" if temp_file.exists() else None
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
return SaveResult(
|
|
343
|
+
success=False,
|
|
344
|
+
message=f"Unexpected error during save: {e}",
|
|
345
|
+
error_details=str(e)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
finally:
|
|
349
|
+
# Clean up temp file if it still exists (shouldn't happen on success)
|
|
350
|
+
if temp_file and temp_file.exists():
|
|
351
|
+
try:
|
|
352
|
+
temp_file.unlink()
|
|
353
|
+
except (OSError, PermissionError):
|
|
354
|
+
pass # Best effort cleanup
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def validate_request_content_length(content_length: Optional[str]) -> Tuple[bool, str]:
|
|
358
|
+
"""
|
|
359
|
+
Validate Content-Length header to prevent DoS attacks.
|
|
360
|
+
|
|
361
|
+
Returns: (is_valid, message)
|
|
362
|
+
"""
|
|
363
|
+
if content_length is None:
|
|
364
|
+
return False, "Missing Content-Length header"
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
length = int(content_length)
|
|
368
|
+
if length < 0:
|
|
369
|
+
return False, "Invalid Content-Length (negative)"
|
|
370
|
+
if length > ConfigService.MAX_CONTENT_LENGTH:
|
|
371
|
+
return False, f"Content too large: {length} bytes (max {ConfigService.MAX_CONTENT_LENGTH})"
|
|
372
|
+
return True, "Valid"
|
|
373
|
+
except ValueError:
|
|
374
|
+
return False, "Invalid Content-Length header"
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def validate_csrf_token(token: str, referer: Optional[str], origin: Optional[str]) -> Tuple[bool, str]:
|
|
378
|
+
"""
|
|
379
|
+
Validate CSRF protection token.
|
|
380
|
+
|
|
381
|
+
Checks:
|
|
382
|
+
- Token is non-empty
|
|
383
|
+
- Referer or Origin header is from localhost
|
|
384
|
+
|
|
385
|
+
Returns: (is_valid, message)
|
|
386
|
+
"""
|
|
387
|
+
if not token or len(token) < 8:
|
|
388
|
+
return False, "Invalid or missing CSRF token"
|
|
389
|
+
|
|
390
|
+
# Check that request comes from localhost
|
|
391
|
+
allowed_hosts = ['localhost', '127.0.0.1', '[::1]']
|
|
392
|
+
|
|
393
|
+
referer_valid = False
|
|
394
|
+
if referer:
|
|
395
|
+
referer_lower = referer.lower()
|
|
396
|
+
if any(host in referer_lower for host in allowed_hosts):
|
|
397
|
+
referer_valid = True
|
|
398
|
+
|
|
399
|
+
origin_valid = False
|
|
400
|
+
if origin:
|
|
401
|
+
origin_lower = origin.lower()
|
|
402
|
+
if any(host in origin_lower for host in allowed_hosts):
|
|
403
|
+
origin_valid = True
|
|
404
|
+
|
|
405
|
+
if not (referer_valid or origin_valid):
|
|
406
|
+
return False, "Invalid origin - request must come from localhost"
|
|
407
|
+
|
|
408
|
+
return True, "Valid"
|
|
409
|
+
|
|
410
|
+
@staticmethod
|
|
411
|
+
def generate_csrf_token() -> str:
|
|
412
|
+
"""Generate a new CSRF token."""
|
|
413
|
+
import secrets
|
|
414
|
+
return secrets.token_urlsafe(32)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# Backwards compatibility: extend FileUtils with ConfigService methods
|
|
418
|
+
FileUtils.validate_write_path = staticmethod(ConfigService.validate_write_path)
|
|
419
|
+
FileUtils.validate_file_type = staticmethod(ConfigService.validate_file_type)
|
|
420
|
+
FileUtils.atomic_write = staticmethod(ConfigService.atomic_write)
|
|
421
|
+
FileUtils.create_backup_with_rotation = staticmethod(ConfigService.create_backup_with_rotation)
|
|
422
|
+
FileUtils.check_conflict = staticmethod(ConfigService.check_conflict)
|
|
423
|
+
FileUtils.compute_content_hash = staticmethod(ConfigService.compute_content_hash)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
if __name__ == '__main__':
|
|
427
|
+
# Quick tests
|
|
428
|
+
print(f"Project root: {PROJECT_ROOT}")
|
|
429
|
+
|
|
430
|
+
# Test validate_write_path
|
|
431
|
+
test_paths = [
|
|
432
|
+
'engine/topologies/tilt/discovery/registry.star',
|
|
433
|
+
'TILT_RESOURCE_DEFAULTS.star',
|
|
434
|
+
'spec.master',
|
|
435
|
+
'engine/spec.master',
|
|
436
|
+
'../outside', # Should fail
|
|
437
|
+
'/etc/passwd', # Should fail
|
|
438
|
+
'some/random/file.star', # Should fail - not in allowed paths
|
|
439
|
+
'README.md', # Should fail - wrong extension
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
print("\n=== Testing validate_write_path ===")
|
|
443
|
+
for path in test_paths:
|
|
444
|
+
result = ConfigService.validate_write_path(path)
|
|
445
|
+
status = "✅ VALID" if result else "❌ INVALID"
|
|
446
|
+
print(f"{status}: {path}")
|
|
447
|
+
|
|
448
|
+
# Test validate_file_type
|
|
449
|
+
print("\n=== Testing validate_file_type ===")
|
|
450
|
+
test_files = [
|
|
451
|
+
'engine/topologies/tilt.star',
|
|
452
|
+
'spec.master',
|
|
453
|
+
'README.md',
|
|
454
|
+
'file.txt',
|
|
455
|
+
]
|
|
456
|
+
for path in test_files:
|
|
457
|
+
is_valid, msg = ConfigService.validate_file_type(path)
|
|
458
|
+
status = "✅" if is_valid else "❌"
|
|
459
|
+
print(f"{status}: {path} - {msg}")
|
|
460
|
+
|
|
461
|
+
# Test hash computation
|
|
462
|
+
print("\n=== Testing compute_content_hash ===")
|
|
463
|
+
test_content = "Hello, World!"
|
|
464
|
+
hash1 = ConfigService.compute_content_hash(test_content)
|
|
465
|
+
hash2 = ConfigService.compute_content_hash(test_content)
|
|
466
|
+
print(f"Content: '{test_content}'")
|
|
467
|
+
print(f"Hash 1: {hash1[:16]}...")
|
|
468
|
+
print(f"Hash 2: {hash2[:16]}...")
|
|
469
|
+
print(f"Hashes match: {hash1 == hash2}")
|