databricks-tellr 0.3.13.dev17__tar.gz → 0.3.13.dev19__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr
3
- Version: 0.3.13.dev17
3
+ Version: 0.3.13.dev19
4
4
  Summary: Tellr deployment tooling for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -67,4 +67,3 @@ env:
67
67
  # Set to "0" if you want to force-disable the fast path entirely.
68
68
  - name: HUASHU_PIPELINE_ENABLED
69
69
  value: "1"
70
- $ENCRYPTION_KEY_BLOCK
@@ -287,9 +287,9 @@ def update(
287
287
  reset_database: If True, drop and recreate the schema (tables recreated on app startup)
288
288
  client: External WorkspaceClient (optional)
289
289
  profile: Databricks CLI profile name (optional)
290
- encryption_key: Override for the legacy Fernet key to carry forward
291
- into the regenerated app.yaml (boot then migrates it into the
292
- encryption_keys table). Default: read from the deployed app.yaml.
290
+ encryption_key: Override for the legacy Fernet key to relocate into
291
+ the encryption_keys table. Default: read from the deployed
292
+ app.yaml. The key is no longer written to app.yaml.
293
293
  mlflow_tracing: Optional overrides for UC tracing env vars (same keys as ``create``).
294
294
  Values from deployment YAML are not loaded on update; use this argument or
295
295
  ``TELLR_DEPLOY_MLFLOW_*`` environment variables.
@@ -504,9 +504,9 @@ def _update_databricks(
504
504
  client: External WorkspaceClient (optional)
505
505
  profile: Databricks CLI profile name (optional)
506
506
  seed_databricks_defaults: If True, seed Databricks-specific content on startup
507
- encryption_key: Override for the legacy Fernet key to carry forward
508
- into the regenerated app.yaml (boot then migrates it into the
509
- encryption_keys table). Default: read from the deployed app.yaml.
507
+ encryption_key: Override for the legacy Fernet key to relocate into
508
+ the encryption_keys table. Default: read from the deployed
509
+ app.yaml. The key is no longer written to app.yaml.
510
510
  mlflow_tracing: Optional overrides for UC tracing placeholders in ``app.yaml``.
511
511
 
512
512
  Returns:
@@ -524,7 +524,7 @@ def _update_databricks(
524
524
  overrides=mlflow_tracing,
525
525
  )
526
526
 
527
- # Preserve the existing key so boot can migrate it (carry-forward).
527
+ # Preserve the existing encryption key so we don't invalidate encrypted data
528
528
  if not encryption_key:
529
529
  encryption_key = _read_existing_encryption_key(ws, app_file_workspace_path)
530
530
 
@@ -545,6 +545,28 @@ def _update_databricks(
545
545
  print(f" Schema '{schema_name}' reset (tables will be recreated on app startup)")
546
546
  print()
547
547
 
548
+ # CRITICAL-3 migration: relocate the legacy app.yaml key into the
549
+ # encryption_keys table BEFORE the new (keyless) app.yaml overwrites
550
+ # it. Runs at most once per install: after it succeeds, the deployed
551
+ # app.yaml has no key entry and encryption_key is None on re-runs.
552
+ if encryption_key:
553
+ print("Relocating encryption key into Lakebase (encryption_keys)...")
554
+ app_for_grant = ws.apps.get(name=app_name)
555
+ grant_client_id = _get_app_client_id(app_for_grant)
556
+ if not grant_client_id:
557
+ print(" Warning: no app client ID — table grant will be skipped")
558
+ mig_conn, _ = _get_lakebase_connection(
559
+ ws, lakebase_name, lakebase_result=lakebase_result
560
+ )
561
+ try:
562
+ with mig_conn.cursor() as cur:
563
+ _migrate_encryption_key_to_lakebase(
564
+ cur, schema_name, grant_client_id, encryption_key
565
+ )
566
+ finally:
567
+ mig_conn.close()
568
+ print(" Key relocated (relocate, not rotate — no re-encryption)")
569
+
548
570
  # Generate and upload updated files
549
571
  with _staging_dir() as staging:
550
572
  _write_requirements(staging, app_version)
@@ -555,7 +577,6 @@ def _update_databricks(
555
577
  seed_databricks_defaults=seed_databricks_defaults,
556
578
  lakebase_result=lakebase_result,
557
579
  mlflow_tracing=mlflow_subs,
558
- encryption_key=encryption_key,
559
580
  )
560
581
  _upload_files(ws, staging, app_file_workspace_path)
561
582
  print(" Files updated")
@@ -767,6 +788,65 @@ def _read_existing_encryption_key(
767
788
  return None
768
789
 
769
790
 
791
+ def _migrate_encryption_key_to_lakebase(
792
+ cur: Any,
793
+ schema_name: str,
794
+ client_id: str | None,
795
+ encryption_key: str,
796
+ ) -> None:
797
+ """Relocate the legacy app.yaml Fernet key into <schema>.encryption_keys.
798
+
799
+ SDR-4437 CRITICAL-3, deploy-time migration. Relocate, not rotate: no
800
+ re-encryption of existing rows. Idempotent: re-running with the same key
801
+ is a no-op. A *different* pre-existing key is a hard error — silently
802
+ keeping either key would orphan the ciphertext encrypted under the other.
803
+
804
+ The GRANT is unconditional: the original deployer's ALTER DEFAULT
805
+ PRIVILEGES attaches to the creating role, so when a different identity
806
+ runs the upgrade this explicit grant is the only one that applies.
807
+ INSERT is included so the app's boot seed (insert-when-missing branch)
808
+ can never be privilege-blocked.
809
+
810
+ Takes an open cursor so callers control the endpoint (prod branch,
811
+ devloop fork, ...). Caller owns the connection lifecycle.
812
+ """
813
+ validate_schema_name(schema_name)
814
+ if client_id is not None:
815
+ validate_client_id(client_id)
816
+
817
+ # Matches the SQLAlchemy model (src/database/models/encryption_key.py):
818
+ # id INTEGER PK (no autoincrement), key_value TEXT, created_at TIMESTAMP.
819
+ cur.execute(
820
+ f'CREATE TABLE IF NOT EXISTS "{schema_name}".encryption_keys ('
821
+ "id INTEGER PRIMARY KEY, "
822
+ "key_value TEXT NOT NULL, "
823
+ "created_at TIMESTAMP NOT NULL)"
824
+ )
825
+ cur.execute(
826
+ f'INSERT INTO "{schema_name}".encryption_keys (id, key_value, created_at) '
827
+ "VALUES (1, %s, CURRENT_TIMESTAMP) ON CONFLICT (id) DO NOTHING",
828
+ (encryption_key,),
829
+ )
830
+ cur.execute(
831
+ f'SELECT key_value FROM "{schema_name}".encryption_keys WHERE id = 1'
832
+ )
833
+ row = cur.fetchone()
834
+ if not row or row[0] != encryption_key:
835
+ raise DeploymentError(
836
+ "encryption_keys already holds a DIFFERENT key than the deployed "
837
+ "app.yaml. Refusing to continue: proceeding would orphan ciphertext "
838
+ "encrypted under one of the two keys. Determine which key decrypts "
839
+ "the existing google_oauth_tokens/google_global_credentials rows, "
840
+ "fix the encryption_keys row (or the app.yaml) to match, then "
841
+ "re-run the update. See SDR-4437 PR-3 design."
842
+ )
843
+ if client_id:
844
+ cur.execute(
845
+ f'GRANT SELECT, INSERT ON "{schema_name}".encryption_keys '
846
+ f'TO "{client_id}"'
847
+ )
848
+
849
+
770
850
  def _load_deployment_config(config_yaml_path: str) -> dict[str, str]:
771
851
  """Load deployment settings from config/deployment.yaml-style files.
772
852
 
@@ -1287,14 +1367,11 @@ def _write_app_yaml(
1287
1367
  seed_databricks_defaults: bool = False,
1288
1368
  lakebase_result: dict[str, Any] | None = None,
1289
1369
  mlflow_tracing: dict[str, str] | None = None,
1290
- encryption_key: str | None = None,
1291
1370
  ) -> None:
1292
1371
  """Generate app.yaml with environment variables.
1293
1372
 
1294
- The Fernet encryption key is written ONLY when ``encryption_key`` is
1295
- provided (the one-time legacy→table carry-forward). Boot seeds the
1296
- encryption_keys table from it and then scrubs it from app.yaml. When
1297
- ``encryption_key`` is None the app.yaml is keyless (steady state).
1373
+ The Fernet encryption key is deliberately NOT written here (SDR-4437
1374
+ CRITICAL-3): the app reads/seeds it from the encryption_keys table.
1298
1375
 
1299
1376
  Args:
1300
1377
  staging_dir: Directory to write the app.yaml file
@@ -1320,17 +1397,8 @@ def _write_app_yaml(
1320
1397
  if mlflow_tracing is None:
1321
1398
  mlflow_tracing = _mlflow_substitutions_for_app_yaml()
1322
1399
 
1323
- if encryption_key:
1324
- key_block = (
1325
- " - name: GOOGLE_OAUTH_ENCRYPTION_KEY\n"
1326
- f' value: "{encryption_key}"'
1327
- )
1328
- else:
1329
- key_block = ""
1330
-
1331
1400
  template_content = _load_template("app.yaml.template")
1332
1401
  content = Template(template_content).substitute(
1333
- ENCRYPTION_KEY_BLOCK=key_block,
1334
1402
  LAKEBASE_INSTANCE=lakebase_name,
1335
1403
  LAKEBASE_SCHEMA=schema_name,
1336
1404
  INIT_DATABASE_CALL=init_call,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr
3
- Version: 0.3.13.dev17
3
+ Version: 0.3.13.dev19
4
4
  Summary: Tellr deployment tooling for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "databricks-tellr"
7
- version = "0.3.13.dev17"
7
+ version = "0.3.13.dev19"
8
8
  description = "Tellr deployment tooling for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"