dataflow-core 2.1.30rc12__tar.gz → 2.1.32__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.
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/PKG-INFO +4 -2
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/environment.py +229 -73
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/__init__.py +1 -0
- dataflow_core-2.1.32/dataflow/models/dbml_diagram.py +58 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/environment.py +22 -1
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/project_details.py +16 -1
- dataflow_core-2.1.32/dataflow/scripts/build_from_upper.sh +103 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/scripts/clone_environment.sh +1 -1
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/scripts/create_environment.sh +1 -1
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/PKG-INFO +4 -2
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/SOURCES.txt +5 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/requires.txt +3 -1
- dataflow_core-2.1.32/dfmigration/versions/027_local_environment_workset_columns.py +46 -0
- dataflow_core-2.1.32/dfmigration/versions/028_create_diagram_table.py +60 -0
- dataflow_core-2.1.32/dfmigration/versions/029_project_studio_source.py +40 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/setup.py +4 -2
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/README.md +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/__init__.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/dataflowairflowauthenticator.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/dataflowhubauthenticator.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/dataflowsupersetauthenticator.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/utils/__init__.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/authenticator/utils/verify_email_domain.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/__init__.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/configuration.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/database_manager.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/dataflow.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/db.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/active_server_view.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/ai_provider.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/app_types.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/blacklist_library.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/connection.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/connection_type.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/container.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/container_session_history.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/dataflow_setting.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/dataflow_zone.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/environment_status.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/git_ssh.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/inference_service.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/onboarding_project.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/org_associations.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/organization.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/otp.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/pinned_projects.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/pod_activity.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/pod_session_history.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/recent_project_studio.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/recent_projects.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/role.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/role_server.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/role_zone.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/server_config.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/session.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/subscription.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/team.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/user.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/user_team.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/models/variables.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/__init__.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/ai_api_key.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/connection.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/git_ssh.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/schemas/secret.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/__init__.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/factory.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/interface.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/__init__.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/aws_manager.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/azure_manager.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/gcp_manager.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/infisical_manager.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/providers/ovh_manager.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/service.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/secrets_manager/utils.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/__init__.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/blocked_domains.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/exceptions.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/get_current_user.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/local_env_validation.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow/utils/logger.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/dependency_links.txt +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/entry_points.txt +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dataflow_core.egg-info/top_level.txt +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/__init__.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/env.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/001_initial_baseline_migration.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/002_user_onboarding_migration.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/003_runtime_visibility.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/004_runtime_custom_image.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/005_session_table_migration.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/006_super_admin_migration.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/007_show_walkthrough_and_org_email_migration.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/008_costing_migration.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/009_session_migration.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/010_add_environment_job_table.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/011_connection_type_migration.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/012_app_type_public_and_project_description.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/013_add_container_table.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/014_active_server_view_can_spawn.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/015_add_agent_enabled_org.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/016_org_server_visibility_controls.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/017_add_created_at_to_user.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/018_add_inference_service_table.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/019_user_onboarding_demo_selection.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/020_create_onboarding_project_table.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/021_create_ai_byok_tables.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/022_shared_airflow_tenancy.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/023_session_history_table_update_container_table.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/024_add_status_reason_to_local_environment.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/025_add_project_preview_image.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/026_runtime_costing.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/dfmigration/versions/__init__.py +0 -0
- {dataflow_core-2.1.30rc12 → dataflow_core-2.1.32}/setup.cfg +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataflow-core
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.32
|
|
4
4
|
Summary: Dataflow core package
|
|
5
5
|
Author: Dataflow
|
|
6
6
|
Author-email:
|
|
7
|
-
Requires-Dist: sqlalchemy
|
|
7
|
+
Requires-Dist: sqlalchemy<2.1
|
|
8
8
|
Requires-Dist: alembic
|
|
9
9
|
Requires-Dist: boto3
|
|
10
10
|
Requires-Dist: psycopg2-binary
|
|
11
11
|
Requires-Dist: pymysql
|
|
12
|
+
Requires-Dist: pymssql<3,>=2.2.8
|
|
13
|
+
Requires-Dist: sqlalchemy-bigquery<2,>=1.12.1
|
|
12
14
|
Requires-Dist: requests
|
|
13
15
|
Requires-Dist: azure-identity
|
|
14
16
|
Requires-Dist: azure-keyvault-secrets
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os, shutil, subprocess, datetime, yaml, re
|
|
2
|
-
from .models.environment import JobLogs, Environment, LocalEnvironment
|
|
2
|
+
from .models.environment import JobLogs, Environment, LocalEnvironment, ArchivedEnvironment
|
|
3
3
|
import json, asyncio, pkg_resources
|
|
4
4
|
from sqlalchemy.orm import Session
|
|
5
5
|
from .configuration import ConfigurationManager
|
|
@@ -74,7 +74,11 @@ class EnvironmentManager:
|
|
|
74
74
|
if db and log_file_location:
|
|
75
75
|
log_file_name = os.path.basename(log_file_location)
|
|
76
76
|
await self._update_job_status(log_file_name, build_status, log_file_location, db)
|
|
77
|
-
|
|
77
|
+
if build_status == "success":
|
|
78
|
+
env_path = os.path.join(self.env_build_path, env_name)
|
|
79
|
+
pip_libraries, conda_libraries = self.update_library_versions(env_path)
|
|
80
|
+
else:
|
|
81
|
+
pip_libraries, conda_libraries = None, None
|
|
78
82
|
self.update_environment_db(env_name, env_version, pip_libraries, conda_libraries, build_status, db)
|
|
79
83
|
|
|
80
84
|
return build_status
|
|
@@ -164,7 +168,13 @@ class EnvironmentManager:
|
|
|
164
168
|
os.symlink(old_yaml_path, new_yaml_path)
|
|
165
169
|
os.symlink(old_squashfs_path, new_squashfs_path)
|
|
166
170
|
|
|
167
|
-
|
|
171
|
+
archived = db.query(ArchivedEnvironment).filter(
|
|
172
|
+
ArchivedEnvironment.short_name == env_name,
|
|
173
|
+
ArchivedEnvironment.version == str(revert_version),
|
|
174
|
+
ArchivedEnvironment.org_id == self.org_id
|
|
175
|
+
).first()
|
|
176
|
+
pip_libraries = archived.pip_libraries if archived else None
|
|
177
|
+
conda_libraries = archived.conda_libraries if archived else None
|
|
168
178
|
|
|
169
179
|
self.update_environment_db(env_name, new_version, pip_libraries, conda_libraries, "success", db)
|
|
170
180
|
return "success"
|
|
@@ -172,30 +182,180 @@ class EnvironmentManager:
|
|
|
172
182
|
except Exception as e:
|
|
173
183
|
self.logger.critical(f"Failed to revert environment {env_name}: {e}", exc_info=True, extra={"status_code": 500, "environment": env_name})
|
|
174
184
|
return "failed"
|
|
175
|
-
|
|
185
|
+
|
|
186
|
+
async def build_env_from_upper(self, env_name, base_short_name, base_version, base_created_by,
|
|
187
|
+
tar_path, py_version, env_version, user_name=None, db: Session = None):
|
|
188
|
+
"""Builds a new squashfs environment from a captured overlay upperdir.
|
|
189
|
+
|
|
190
|
+
Reconstructs the overlay a user's activated environment ran with at
|
|
191
|
+
runtime (the base environment's squashfs as the read-only lowerdir,
|
|
192
|
+
plus a tar of the writable upperdir capturing their pip/conda
|
|
193
|
+
modifications) and repackages the merged result as a new versioned
|
|
194
|
+
squashfs. This is the "Save as Draft" build path -- as opposed to
|
|
195
|
+
create_env, which builds a fresh conda environment from a YAML spec.
|
|
196
|
+
|
|
197
|
+
A conda env bakes its absolute prefix into shebangs, conda-meta and
|
|
198
|
+
.pth files, so build_from_upper.sh branches on whether the prefix
|
|
199
|
+
changes: same prefix ("Save") squashes the merged tree verbatim, a new
|
|
200
|
+
prefix ("Save As" under a new short name) clones it first so conda
|
|
201
|
+
rewrites those paths.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
env_name (str): Short name for the new/target environment
|
|
205
|
+
base_short_name (str): Short name of the activated (base) environment
|
|
206
|
+
base_version (str): Version of the activated (base) environment
|
|
207
|
+
base_created_by (str): created_by of the base environment ("Dataflow"
|
|
208
|
+
for system environments, which live under the shared "dataflow"
|
|
209
|
+
sub path instead of an org-scoped one)
|
|
210
|
+
tar_path (str): Path to the tarred overlay upperdir
|
|
211
|
+
py_version (str): Python version, recorded in the conda YAML
|
|
212
|
+
env_version (str): Version to assign to the new environment
|
|
213
|
+
user_name (str): Username who initiated the save
|
|
214
|
+
db (Session): Database session
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
str: Build status ('success' or 'failed')
|
|
218
|
+
"""
|
|
219
|
+
log_file_location = None
|
|
220
|
+
if db:
|
|
221
|
+
log_file_location = self._setup_logging(env_name, env_version, user_name, db)
|
|
222
|
+
|
|
223
|
+
env_root_path = self.config.get_config_value('paths', 'env_path')
|
|
224
|
+
env_mount_root = self.config.get_config_value('paths', 'env_mount_path')
|
|
225
|
+
if base_created_by == "Dataflow":
|
|
226
|
+
base_squashfs_path = os.path.join(env_root_path, "dataflow", "python_envs", f"{base_short_name}_v{base_version}.squashfs")
|
|
227
|
+
base_sub_path = "dataflow"
|
|
228
|
+
else:
|
|
229
|
+
base_squashfs_path = os.path.join(self.env_base_path, f"{base_short_name}_v{base_version}.squashfs")
|
|
230
|
+
base_sub_path = self.env_sub_path
|
|
231
|
+
|
|
232
|
+
# Prefixes must match what dataflow-init mounts the squashfs at, since
|
|
233
|
+
# that is the path baked into the environment's own files:
|
|
234
|
+
# <env_mount_path>/{dataflow|org_id}/<short_name>.
|
|
235
|
+
base_prefix = os.path.join(env_mount_root, base_sub_path, base_short_name)
|
|
236
|
+
target_prefix = os.path.join(self.env_build_path, env_name)
|
|
237
|
+
|
|
238
|
+
yaml_path = os.path.join(self.env_version_path, f"{env_name}_v{env_version}.yaml")
|
|
239
|
+
scratch_dir = os.path.join(self.env_build_path, f"{env_name}_v{env_version}_scratch")
|
|
240
|
+
|
|
241
|
+
build_status = await self._execute_env_operation(
|
|
242
|
+
env_name=env_name,
|
|
243
|
+
status="draft",
|
|
244
|
+
mode="from_upper",
|
|
245
|
+
yaml_file_path=yaml_path,
|
|
246
|
+
log_file_location=log_file_location,
|
|
247
|
+
env_version=int(env_version),
|
|
248
|
+
py_version=py_version,
|
|
249
|
+
base_squashfs_path=base_squashfs_path,
|
|
250
|
+
tar_path=tar_path,
|
|
251
|
+
scratch_dir=scratch_dir,
|
|
252
|
+
base_prefix=base_prefix,
|
|
253
|
+
target_prefix=target_prefix,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Update job log status and library versions if db was provided
|
|
257
|
+
if db and log_file_location:
|
|
258
|
+
log_file_name = os.path.basename(log_file_location)
|
|
259
|
+
await self._update_job_status(log_file_name, build_status, log_file_location, db)
|
|
260
|
+
|
|
261
|
+
# build_from_upper.sh deliberately leaves the overlay mounted (and
|
|
262
|
+
# any clone in place) on success so we can introspect the built
|
|
263
|
+
# environment here before tearing it down -- unlike create_env's
|
|
264
|
+
# build prefix, base_prefix is a mount, not a plain directory, so
|
|
265
|
+
# it must be read before cleanup. On failure nothing was left
|
|
266
|
+
# behind (or it was torn down by the script's own failure trap),
|
|
267
|
+
# so the path is just an empty directory -- reading it would
|
|
268
|
+
# silently report zero packages and blank out this version's known
|
|
269
|
+
# library list. Leave pip_libraries/conda_libraries as None
|
|
270
|
+
# (update_environment_db then skips those columns) so a failed
|
|
271
|
+
# build doesn't clobber whatever the row already had.
|
|
272
|
+
if build_status == "success":
|
|
273
|
+
pip_libraries, conda_libraries = self.update_library_versions(target_prefix)
|
|
274
|
+
self.create_conda_yaml(
|
|
275
|
+
yaml_path=yaml_path,
|
|
276
|
+
env_name=env_name,
|
|
277
|
+
python_version=py_version,
|
|
278
|
+
conda_packages=conda_libraries,
|
|
279
|
+
pip_packages=pip_libraries,
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
pip_libraries, conda_libraries = None, None
|
|
283
|
+
self.update_environment_db(env_name, env_version, pip_libraries, conda_libraries, build_status, db)
|
|
284
|
+
|
|
285
|
+
self._cleanup_overlay_scratch(scratch_dir, base_prefix, target_prefix)
|
|
286
|
+
|
|
287
|
+
# The tar is a one-shot input; remove it regardless of outcome so a
|
|
288
|
+
# retry doesn't silently reuse a stale/partial capture, and so it
|
|
289
|
+
# doesn't linger on the user's NFS home.
|
|
290
|
+
try:
|
|
291
|
+
if os.path.exists(tar_path):
|
|
292
|
+
os.remove(tar_path)
|
|
293
|
+
except OSError as e:
|
|
294
|
+
self.logger.error(f"Failed to remove upper-layer tar {tar_path}: {e}")
|
|
295
|
+
|
|
296
|
+
return build_status
|
|
297
|
+
|
|
298
|
+
def _cleanup_overlay_scratch(self, scratch_dir: str, base_prefix: str, target_prefix: str):
|
|
299
|
+
"""Unmounts the overlay left mounted by build_from_upper.sh and removes
|
|
300
|
+
its scratch directory, plus the clone it may have built at
|
|
301
|
+
target_prefix. Safe to call even if nothing ended up mounted (e.g. the
|
|
302
|
+
build failed before the overlay mount step).
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
scratch_dir (str): The lower/upper/work scratch directory passed to
|
|
306
|
+
build_from_upper.sh
|
|
307
|
+
base_prefix (str): The base environment's prefix, where the merged
|
|
308
|
+
overlay was mounted
|
|
309
|
+
target_prefix (str): The new environment's prefix; equal to
|
|
310
|
+
base_prefix on a version bump, otherwise the cloned env
|
|
311
|
+
"""
|
|
312
|
+
lower_dir = os.path.join(scratch_dir, "lower")
|
|
313
|
+
for mount_dir in (base_prefix, lower_dir):
|
|
314
|
+
try:
|
|
315
|
+
if subprocess.run(["mountpoint", "-q", mount_dir]).returncode == 0:
|
|
316
|
+
subprocess.run(["umount", mount_dir], check=True)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
self.logger.error(f"Failed to unmount {mount_dir}: {e}")
|
|
319
|
+
shutil.rmtree(scratch_dir, ignore_errors=True)
|
|
320
|
+
shutil.rmtree(target_prefix, ignore_errors=True)
|
|
321
|
+
|
|
176
322
|
async def _execute_env_operation(
|
|
177
|
-
self,
|
|
178
|
-
env_name: str,
|
|
179
|
-
status: str,
|
|
180
|
-
mode: str,
|
|
181
|
-
yaml_file_path: str,
|
|
323
|
+
self,
|
|
324
|
+
env_name: str,
|
|
325
|
+
status: str,
|
|
326
|
+
mode: str,
|
|
327
|
+
yaml_file_path: str,
|
|
182
328
|
env_version: int,
|
|
183
|
-
source_path=None,
|
|
329
|
+
source_path=None,
|
|
184
330
|
log_file_location=None,
|
|
185
|
-
py_version=None
|
|
331
|
+
py_version=None,
|
|
332
|
+
base_squashfs_path=None,
|
|
333
|
+
tar_path=None,
|
|
334
|
+
scratch_dir=None,
|
|
335
|
+
base_prefix=None,
|
|
336
|
+
target_prefix=None,
|
|
186
337
|
):
|
|
187
|
-
"""Executes environment operations (create or
|
|
188
|
-
|
|
338
|
+
"""Executes environment operations (create, clone, or from_upper).
|
|
339
|
+
|
|
189
340
|
Args:
|
|
190
341
|
env_name (str): Name of the environment
|
|
191
342
|
status (str): Environment status ('draft' or 'published')
|
|
192
|
-
mode (str): Operation mode ('create' or '
|
|
343
|
+
mode (str): Operation mode ('create', 'clone', 'local_clone', or 'from_upper')
|
|
193
344
|
env_version (str): Version of the environment (for draft environments)
|
|
194
345
|
py_version (str): Python version to use (for create mode)
|
|
195
346
|
py_requirements (list): List of packages to install (for create mode)
|
|
196
347
|
source_path (str): Path to source environment (for clone mode)
|
|
197
348
|
log_file_location (str): Path to log file
|
|
198
|
-
|
|
349
|
+
base_squashfs_path (str): Path to the base squashfs to loop-mount as the
|
|
350
|
+
overlay lowerdir (for from_upper mode)
|
|
351
|
+
tar_path (str): Path to the tarred overlay upperdir (for from_upper mode)
|
|
352
|
+
scratch_dir (str): Scratch directory for the lower/upper/work
|
|
353
|
+
mounts (for from_upper mode)
|
|
354
|
+
base_prefix (str): Prefix the base environment was built at, where
|
|
355
|
+
the merged overlay gets mounted (for from_upper mode)
|
|
356
|
+
target_prefix (str): Prefix the new environment will be mounted at;
|
|
357
|
+
differing from base_prefix triggers the clone (for from_upper mode)
|
|
358
|
+
|
|
199
359
|
Returns:
|
|
200
360
|
str: Build status ('success' or 'failed')
|
|
201
361
|
"""
|
|
@@ -218,14 +378,18 @@ class EnvironmentManager:
|
|
|
218
378
|
elif mode == "clone":
|
|
219
379
|
clone_env_script_path = pkg_resources.resource_filename('dataflow', 'scripts/clone_environment.sh')
|
|
220
380
|
command = ["bash", clone_env_script_path, source_path, os.path.join(self.env_build_path, env_name), os.path.join(self.env_base_path, f"{env_name}_v{env_version}.squashfs"), yaml_file_path]
|
|
221
|
-
|
|
381
|
+
|
|
222
382
|
elif mode == "local_clone":
|
|
223
383
|
conda_cmd = f"conda create --name {env_name} --clone {source_path} --yes"
|
|
224
384
|
command = ["su", "-", os.environ.get("NB_USER"), "-c", conda_cmd]
|
|
225
385
|
self.logger.info(f"Cloning environment locally with command: {' '.join(command)}")
|
|
226
386
|
|
|
387
|
+
elif mode == "from_upper":
|
|
388
|
+
build_from_upper_script_path = pkg_resources.resource_filename('dataflow', 'scripts/build_from_upper.sh')
|
|
389
|
+
command = ["bash", build_from_upper_script_path, base_squashfs_path, tar_path, conda_env_path, scratch_dir, base_prefix, target_prefix]
|
|
390
|
+
|
|
227
391
|
else:
|
|
228
|
-
raise ValueError("Invalid mode. Use 'create' or '
|
|
392
|
+
raise ValueError("Invalid mode. Use 'create', 'clone', 'local_clone', or 'from_upper'.")
|
|
229
393
|
|
|
230
394
|
process = await asyncio.create_subprocess_exec(
|
|
231
395
|
*command,
|
|
@@ -439,8 +603,11 @@ class EnvironmentManager:
|
|
|
439
603
|
Args:
|
|
440
604
|
env_short_name (str): Short name of the environment
|
|
441
605
|
version (str): Version of the environment
|
|
442
|
-
pip_libraries (list): List of pip libraries
|
|
443
|
-
|
|
606
|
+
pip_libraries (list or None): List of pip libraries, or None to leave the
|
|
607
|
+
column untouched (e.g. a failed build never actually introspected
|
|
608
|
+
the new version's packages -- don't clobber whatever was there)
|
|
609
|
+
conda_libraries (list or None): List of conda libraries, or None to leave
|
|
610
|
+
the column untouched
|
|
444
611
|
status (str): Build status ('success' or 'failed')
|
|
445
612
|
db (Session): Database session
|
|
446
613
|
"""
|
|
@@ -452,13 +619,19 @@ class EnvironmentManager:
|
|
|
452
619
|
current_env = db.query(Environment).filter(Environment.short_name == env_short_name).first()
|
|
453
620
|
if not current_env:
|
|
454
621
|
raise ValueError(f"Environment with short name '{env_short_name}' does not exist.")
|
|
455
|
-
|
|
622
|
+
|
|
456
623
|
env_status = "Draft" if status == "success" else "Failed"
|
|
457
624
|
|
|
625
|
+
update_values = {"version": version, "status": env_status}
|
|
626
|
+
if pip_libraries is not None:
|
|
627
|
+
update_values["pip_libraries"] = pip_libraries
|
|
628
|
+
if conda_libraries is not None:
|
|
629
|
+
update_values["conda_libraries"] = conda_libraries
|
|
630
|
+
|
|
458
631
|
db.query(Environment).filter(
|
|
459
632
|
Environment.short_name == env_short_name,
|
|
460
633
|
Environment.org_id == self.org_id if self.org_id is not None else Environment.org_id.is_(None)
|
|
461
|
-
).update(
|
|
634
|
+
).update(update_values)
|
|
462
635
|
db.commit()
|
|
463
636
|
|
|
464
637
|
except Exception as e:
|
|
@@ -466,63 +639,46 @@ class EnvironmentManager:
|
|
|
466
639
|
db.rollback()
|
|
467
640
|
raise
|
|
468
641
|
|
|
469
|
-
def update_library_versions(self,
|
|
642
|
+
def update_library_versions(self, env_path: str):
|
|
470
643
|
"""
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
644
|
+
Reads pip and conda packages from a built environment directory.
|
|
645
|
+
|
|
646
|
+
Uses pip freeze via the env's own Python binary so packages installed
|
|
647
|
+
with --target (e.g. dataflow-airflow via airflow_custom.pth) are included.
|
|
648
|
+
Runs conda list for conda-channel packages.
|
|
649
|
+
|
|
476
650
|
Returns:
|
|
477
|
-
tuple:
|
|
651
|
+
tuple: (pip_libraries, conda_libraries) as lists of "name==version" / "name=version" strings.
|
|
478
652
|
"""
|
|
653
|
+
pip_libraries = []
|
|
479
654
|
try:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
yaml_content = yaml.safe_load(f)
|
|
492
|
-
|
|
493
|
-
# Extract conda and pip dependencies
|
|
494
|
-
dependencies = yaml_content.get('dependencies', [])
|
|
495
|
-
|
|
496
|
-
# Process conda libraries
|
|
497
|
-
conda_libraries = []
|
|
498
|
-
pip_libraries = []
|
|
499
|
-
|
|
500
|
-
for dep in dependencies:
|
|
501
|
-
if isinstance(dep, str):
|
|
502
|
-
if dep.startswith("python="):
|
|
503
|
-
continue
|
|
504
|
-
|
|
505
|
-
parts = dep.split('=')
|
|
506
|
-
package_name = parts[0].strip()
|
|
507
|
-
|
|
508
|
-
if package_name.lower() not in default_conda_packages:
|
|
509
|
-
if len(parts) >= 2:
|
|
510
|
-
package_with_version = f"{package_name}={parts[1]}"
|
|
511
|
-
conda_libraries.append(package_with_version)
|
|
512
|
-
else:
|
|
513
|
-
# No version specified, keep as is
|
|
514
|
-
conda_libraries.append(dep)
|
|
515
|
-
|
|
516
|
-
elif isinstance(dep, dict) and 'pip' in dep:
|
|
517
|
-
# This is the pip section
|
|
518
|
-
for pip_pkg in dep['pip']:
|
|
519
|
-
pip_libraries.append(pip_pkg)
|
|
520
|
-
|
|
521
|
-
return pip_libraries, conda_libraries
|
|
522
|
-
|
|
655
|
+
result = subprocess.run(
|
|
656
|
+
[f"{env_path}/bin/python", "-m", "pip", "freeze", "--disable-pip-version-check"],
|
|
657
|
+
capture_output=True, text=True, timeout=120
|
|
658
|
+
)
|
|
659
|
+
if result.returncode == 0:
|
|
660
|
+
for line in result.stdout.strip().splitlines():
|
|
661
|
+
line = line.strip()
|
|
662
|
+
if line and not line.startswith("#") and not line.startswith("-"):
|
|
663
|
+
pip_libraries.append(line)
|
|
664
|
+
else:
|
|
665
|
+
self.logger.error(f"pip freeze failed for {env_path}: {result.stderr.strip()}")
|
|
523
666
|
except Exception as e:
|
|
524
|
-
self.logger.
|
|
525
|
-
|
|
667
|
+
self.logger.error(f"Failed to get pip packages for {env_path}: {e}")
|
|
668
|
+
|
|
669
|
+
conda_libraries = []
|
|
670
|
+
try:
|
|
671
|
+
result = subprocess.run(
|
|
672
|
+
["conda", "list", "--prefix", env_path, "--json"],
|
|
673
|
+
capture_output=True, text=True, check=True
|
|
674
|
+
)
|
|
675
|
+
for pkg in json.loads(result.stdout):
|
|
676
|
+
if pkg.get("channel") not in ("pypi", "<develop>") and pkg["name"] != "python":
|
|
677
|
+
conda_libraries.append(f"{pkg['name']}={pkg['version']}")
|
|
678
|
+
except Exception as e:
|
|
679
|
+
self.logger.error(f"Failed to get conda packages for {env_path}: {e}")
|
|
680
|
+
|
|
681
|
+
return pip_libraries, conda_libraries
|
|
526
682
|
|
|
527
683
|
def create_conda_yaml(self, yaml_path, env_name, python_version, conda_packages, pip_packages):
|
|
528
684
|
"""
|
|
@@ -36,3 +36,4 @@ from .active_server_view import ActiveServerView
|
|
|
36
36
|
from .inference_service import InferenceService, InferenceStatus
|
|
37
37
|
from .onboarding_project import OnboardingProject
|
|
38
38
|
from .ai_provider import AIProvider, AIModel, AIUserProviderKey
|
|
39
|
+
from .dbml_diagram import Diagram
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from sqlalchemy import Column, Integer, String, ForeignKey, Text, DateTime, func, Enum, Index, text
|
|
2
|
+
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
|
3
|
+
from dataflow.db import Base
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Diagram(Base):
|
|
8
|
+
|
|
9
|
+
"""TABLE 'DBML_DIAGRAM'
|
|
10
|
+
|
|
11
|
+
Stores a user-authored database ER diagram. The DBML source is the single source of
|
|
12
|
+
truth for the schema itself (tables, columns, relationships, header colours); the
|
|
13
|
+
layout column only holds presentation state that DBML has no syntax for, i.e. where
|
|
14
|
+
each table sits on the canvas and the last viewport.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
id (int): Primary key for the diagram entry.
|
|
18
|
+
uid (UUID): Public identifier used in URLs, so the internal id is never exposed.
|
|
19
|
+
name (str): Display name of the diagram, unique per user within an organization.
|
|
20
|
+
description (str): Optional description of the diagram.
|
|
21
|
+
org_id (int): Foreign key referencing the ORGANIZATION table.
|
|
22
|
+
user_id (int): Foreign key referencing the USER table (the owner).
|
|
23
|
+
dbml_source (str): The DBML document defining the schema.
|
|
24
|
+
layout (dict): Canvas presentation state. Shape:
|
|
25
|
+
{"tables": {"public.users": {"x": 40, "y": 120}}, "viewport": {"x": 0, "y": 0, "zoom": 1}}
|
|
26
|
+
visibility (str): 'PRIVATE' or 'ORG'. Reserved for org sharing; currently always PRIVATE.
|
|
27
|
+
created_at (datetime): Timestamp of when the diagram was created.
|
|
28
|
+
updated_at (datetime): Timestamp of the last update to the diagram.
|
|
29
|
+
deleted_at (datetime): Soft delete timestamp; NULL while the diagram is active.
|
|
30
|
+
|
|
31
|
+
Constraints:
|
|
32
|
+
Index: Partial unique index ensuring a user cannot have two active diagrams
|
|
33
|
+
with the same name in the same organization.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
__tablename__ = 'DBML_DIAGRAM'
|
|
37
|
+
|
|
38
|
+
id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
|
|
39
|
+
uid = Column(UUID(as_uuid=True), default=uuid.uuid4, nullable=False, unique=True, server_default=text("gen_random_uuid()"))
|
|
40
|
+
name = Column(String, nullable=False)
|
|
41
|
+
description = Column(Text, nullable=True)
|
|
42
|
+
org_id = Column(Integer, ForeignKey("ORGANIZATION.id", ondelete="CASCADE"), index=True, nullable=False)
|
|
43
|
+
user_id = Column(Integer, ForeignKey("USER.user_id", ondelete="CASCADE"), index=True, nullable=False)
|
|
44
|
+
dbml_source = Column(Text, nullable=False, server_default='')
|
|
45
|
+
layout = Column(JSONB, nullable=True)
|
|
46
|
+
visibility = Column(Enum("PRIVATE", "ORG", name="diagram_visibility"), default="PRIVATE", server_default="PRIVATE", nullable=False)
|
|
47
|
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
48
|
+
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
49
|
+
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
|
50
|
+
|
|
51
|
+
__table_args__ = (
|
|
52
|
+
Index(
|
|
53
|
+
'uq_active_diagram_name',
|
|
54
|
+
'name', 'user_id', 'org_id',
|
|
55
|
+
unique=True,
|
|
56
|
+
postgresql_where=deleted_at.is_(None)
|
|
57
|
+
),
|
|
58
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from sqlalchemy import (
|
|
2
|
-
Column, Integer, String, Boolean, Text,
|
|
2
|
+
Column, Integer, BigInteger, String, Boolean, Text,
|
|
3
3
|
ForeignKey, DateTime, UniqueConstraint, CheckConstraint, Index
|
|
4
4
|
)
|
|
5
5
|
from sqlalchemy.orm import relationship, Session
|
|
@@ -181,6 +181,20 @@ class LocalEnvironment(Base):
|
|
|
181
181
|
cloned_from (str): Source from which the environment was cloned.
|
|
182
182
|
updated_at (datetime): Timestamp of when the local environment was last updated.
|
|
183
183
|
need_refresh (bool): Indicates if the local environment needs a refresh.
|
|
184
|
+
base_env_id (int): ID of the base (squashfs) ENVIRONMENT this workset's delta sits on.
|
|
185
|
+
base_version (str): Version of the base environment the delta is keyed to, mirrors the
|
|
186
|
+
on-disk `_worksets/{short_name}_v{version}/` directory.
|
|
187
|
+
pip_diff (str): Precomputed package diff (added/removed/modified) captured at the last
|
|
188
|
+
checkpoint, serialized as JSON. Computed while the env is live so it never needs to
|
|
189
|
+
be re-derived from the (otherwise meaningless without its base) delta tar.
|
|
190
|
+
workset_path (str): Path to the persisted delta directory, relative to the user's jovyan
|
|
191
|
+
home, e.g. `.dataflow/envs/_worksets/{short_name}_v{version}`.
|
|
192
|
+
delta_size_bytes (int): Size in bytes of the last checkpointed delta.tar, for UI display
|
|
193
|
+
and retention/cleanup decisions.
|
|
194
|
+
is_workset (bool): True for auto-persisted workset shadow rows (one per user/base
|
|
195
|
+
env/version, tracking the overlay upperdir delta), as opposed to genuine
|
|
196
|
+
user-created local/conda environments which also live in this table. Queries that
|
|
197
|
+
list a user's real local environments must filter is_workset = false.
|
|
184
198
|
"""
|
|
185
199
|
|
|
186
200
|
__tablename__ = "LOCAL_ENVIRONMENT"
|
|
@@ -197,6 +211,13 @@ class LocalEnvironment(Base):
|
|
|
197
211
|
cloned_from = Column(String, nullable=True)
|
|
198
212
|
updated_at = Column(DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc))
|
|
199
213
|
need_refresh = Column(Boolean, default=False, server_default='false')
|
|
214
|
+
# Auto-persisted private-environment workset tracking (overlay upperdir delta).
|
|
215
|
+
base_env_id = Column(Integer, nullable=True)
|
|
216
|
+
base_version = Column(String, nullable=True)
|
|
217
|
+
pip_diff = Column(Text, nullable=True)
|
|
218
|
+
workset_path = Column(String, nullable=True)
|
|
219
|
+
delta_size_bytes = Column(BigInteger, nullable=True)
|
|
220
|
+
is_workset = Column(Boolean, default=False, server_default='false', nullable=False)
|
|
200
221
|
|
|
201
222
|
class EnvType(str, Enum):
|
|
202
223
|
|
|
@@ -36,7 +36,13 @@ class ProjectDetails(Base):
|
|
|
36
36
|
exposed_port (int): Port exposed by the custom image.
|
|
37
37
|
private_repo (bool): Whether the image repository is private.
|
|
38
38
|
registry_secret_id (int): Foreign key referencing the VARIABLE table for registry credentials.
|
|
39
|
-
|
|
39
|
+
source_type (str): Where the project's code comes from: GIT (cloned from git_url) or STUDIO
|
|
40
|
+
(synced from the creator's Studio folder into the runtime's projects volume).
|
|
41
|
+
studio_source_path (str): For STUDIO projects, the source folder relative to the creator's
|
|
42
|
+
/home/jovyan.
|
|
43
|
+
last_synced_at (datetime): For STUDIO projects, when files were last synced from Studio.
|
|
44
|
+
synced_by (str): For STUDIO projects, the user who ran the last sync.
|
|
45
|
+
|
|
40
46
|
Relationships:
|
|
41
47
|
app_type: Relationship to the AppType model.
|
|
42
48
|
"""
|
|
@@ -87,6 +93,15 @@ class ProjectDetails(Base):
|
|
|
87
93
|
resource_config = Column(JSONB, nullable=True)
|
|
88
94
|
cost_incurred = Column(JSONB, nullable=True)
|
|
89
95
|
runtime_cost_state = Column(JSONB, nullable=True)
|
|
96
|
+
source_type = Column(
|
|
97
|
+
Enum("GIT", "STUDIO", name="project_source_type"),
|
|
98
|
+
nullable=False,
|
|
99
|
+
default="GIT",
|
|
100
|
+
server_default="GIT",
|
|
101
|
+
)
|
|
102
|
+
studio_source_path = Column(String, nullable=True)
|
|
103
|
+
last_synced_at = Column(DateTime(timezone=True), nullable=True)
|
|
104
|
+
synced_by = Column(String, nullable=True)
|
|
90
105
|
|
|
91
106
|
__table_args__ = (
|
|
92
107
|
Index(
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
# Reconstructs the overlayfs a user's activated environment ran with at
|
|
6
|
+
# runtime (read-only base squashfs as lowerdir + a captured upperdir tar of
|
|
7
|
+
# their pip/conda modifications), then repackages the merged result as a new
|
|
8
|
+
# squashfs. Requires CAP_SYS_ADMIN (loop + overlay mounts) and CAP_MKNOD
|
|
9
|
+
# (recreating whiteout char-devices on tar extract) -- run this in a
|
|
10
|
+
# privileged container.
|
|
11
|
+
|
|
12
|
+
base_squashfs_path=$1
|
|
13
|
+
tar_path=$2
|
|
14
|
+
squashfs_file_path=$3
|
|
15
|
+
scratch_dir=$4
|
|
16
|
+
base_prefix=$5
|
|
17
|
+
target_prefix=$6
|
|
18
|
+
|
|
19
|
+
if [ -z "$base_squashfs_path" ] || [ -z "$tar_path" ] || [ -z "$squashfs_file_path" ] \
|
|
20
|
+
|| [ -z "$scratch_dir" ] || [ -z "$base_prefix" ] || [ -z "$target_prefix" ]; then
|
|
21
|
+
echo "Error: Missing required parameters"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if [ ! -f "$base_squashfs_path" ]; then
|
|
26
|
+
echo "Error: base squashfs file does not exist: $base_squashfs_path"
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [ ! -f "$tar_path" ]; then
|
|
31
|
+
echo "Error: upper tar file does not exist: $tar_path"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
squashfs_file_name="$(basename "${squashfs_file_path}")"
|
|
36
|
+
env_name_with_version="${squashfs_file_name%.squashfs}"
|
|
37
|
+
|
|
38
|
+
lower_dir="${scratch_dir}/lower"
|
|
39
|
+
upper_dir="${scratch_dir}/upper"
|
|
40
|
+
work_dir="${scratch_dir}/work"
|
|
41
|
+
merged_dir="${base_prefix}"
|
|
42
|
+
|
|
43
|
+
# On failure, unwind whatever mounts were set up so we don't leak them. On
|
|
44
|
+
# success we deliberately leave lower/merged mounted, and leave a cloned
|
|
45
|
+
# target prefix in place -- the caller reads them (pip/conda introspection)
|
|
46
|
+
# before tearing everything down.
|
|
47
|
+
cleanup_on_failure() {
|
|
48
|
+
mountpoint -q "$merged_dir" 2>/dev/null && umount "$merged_dir" 2>/dev/null
|
|
49
|
+
mountpoint -q "$lower_dir" 2>/dev/null && umount "$lower_dir" 2>/dev/null
|
|
50
|
+
if [ "$target_prefix" != "$base_prefix" ]; then
|
|
51
|
+
rm -rf "$target_prefix"
|
|
52
|
+
fi
|
|
53
|
+
exit 1
|
|
54
|
+
}
|
|
55
|
+
trap cleanup_on_failure ERR
|
|
56
|
+
|
|
57
|
+
rm -rf "$scratch_dir"
|
|
58
|
+
mkdir -p "$lower_dir" "$upper_dir" "$work_dir"
|
|
59
|
+
|
|
60
|
+
# Defensive: a leftover mount here would silently become the clone source.
|
|
61
|
+
mountpoint -q "$merged_dir" 2>/dev/null && umount "$merged_dir"
|
|
62
|
+
rm -rf "$merged_dir"
|
|
63
|
+
mkdir -p "$merged_dir"
|
|
64
|
+
|
|
65
|
+
# lowerdir: loop-mount the base squashfs read-only -- identical to the
|
|
66
|
+
# lowerdir the user's pod ran against at runtime.
|
|
67
|
+
mount -t squashfs -o loop,ro "$base_squashfs_path" "$lower_dir"
|
|
68
|
+
|
|
69
|
+
# upperdir: extract the captured delta. --xattrs is mandatory -- an
|
|
70
|
+
# opaque-directory whiteout (a directory the user deleted and recreated) is
|
|
71
|
+
# recorded as the trusted.overlay.opaque xattr, not a regular tar entry, and
|
|
72
|
+
# silently vanishes on reconstruction without it. Character-device whiteout
|
|
73
|
+
# markers for deleted files require CAP_MKNOD to recreate on extract.
|
|
74
|
+
tar --xattrs --xattrs-include='*' --acls -xf "$tar_path" -C "$upper_dir"
|
|
75
|
+
|
|
76
|
+
# merged: reconstruct the overlay exactly as it was on the user's pod.
|
|
77
|
+
mount -t overlay overlay -o "lowerdir=${lower_dir},upperdir=${upper_dir},workdir=${work_dir}" "$merged_dir"
|
|
78
|
+
|
|
79
|
+
if [ "$base_prefix" = "$target_prefix" ]; then
|
|
80
|
+
# Same prefix: the merged view is already correct for where it will be
|
|
81
|
+
# mounted. Squash it as-is.
|
|
82
|
+
squashfs_source="$merged_dir"
|
|
83
|
+
else
|
|
84
|
+
echo "${base_prefix} to ${target_prefix}; cloning environment"
|
|
85
|
+
|
|
86
|
+
export PIP_NO_CACHE_DIR=1
|
|
87
|
+
export CONDA_PKGS_DIRS="${scratch_dir}/pkgs/${env_name_with_version}/"
|
|
88
|
+
mkdir -p "$CONDA_PKGS_DIRS"
|
|
89
|
+
conda config --set always_copy true
|
|
90
|
+
|
|
91
|
+
mkdir -p "$(dirname "$target_prefix")"
|
|
92
|
+
conda create --clone "$merged_dir" --prefix "$target_prefix" --yes
|
|
93
|
+
|
|
94
|
+
squashfs_source="$target_prefix"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Build the new squashfs from the merged (base + user modifications) view, or
|
|
98
|
+
# from the prefix-corrected clone of it.
|
|
99
|
+
mksquashfs "$squashfs_source" "${scratch_dir}/${squashfs_file_name}" -comp zstd -Xcompression-level 4 -force-uid 1000 -force-gid 1000
|
|
100
|
+
mkdir -p "$(dirname "$squashfs_file_path")"
|
|
101
|
+
cp -f "${scratch_dir}/${squashfs_file_name}" "$squashfs_file_path"
|
|
102
|
+
|
|
103
|
+
echo "Environment Creation Successful"
|
|
@@ -23,7 +23,7 @@ conda create --clone ${source_env_path} --prefix ${env_build_path} --yes
|
|
|
23
23
|
conda env export --prefix "${env_build_path}" > "$yaml_file_path"
|
|
24
24
|
|
|
25
25
|
# 2. Create squashfs file from the cloned environment
|
|
26
|
-
mksquashfs "${env_build_path}" "${squashfs_file_name}" -comp zstd -Xcompression-level 4
|
|
26
|
+
mksquashfs "${env_build_path}" "${squashfs_file_name}" -comp zstd -Xcompression-level 4 -force-uid 1000 -force-gid 1000
|
|
27
27
|
cp -f "${squashfs_file_name}" "${squashfs_file_path}"
|
|
28
28
|
# 3. Cleanup the temporary environment directory
|
|
29
29
|
rm -rf "${env_build_path}"
|