odoo-devops-tools 1.24.2__tar.gz → 1.24.3__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: odoo-devops-tools
3
- Version: 1.24.2
3
+ Version: 1.24.3
4
4
  Summary: Build reproducible Odoo workspaces from a single project definition.
5
5
  Author: Roman Lacko
6
6
  License-Expression: MIT
@@ -349,7 +349,7 @@ Restore a database dump with `pg_restore`:
349
349
  ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
350
350
  ```
351
351
 
352
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
352
+ Database restores use copy semantics by default.
353
353
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
354
354
  database can coexist with the source database:
355
355
 
@@ -787,7 +787,7 @@ Restore a database dump with `pg_restore`:
787
787
  ./odoo-scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
788
788
  ```
789
789
 
790
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
790
+ Database restores use copy semantics by default.
791
791
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
792
792
  database can coexist with the source database:
793
793
 
@@ -321,7 +321,7 @@ Restore a database dump with `pg_restore`:
321
321
  ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
322
322
  ```
323
323
 
324
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
324
+ Database restores use copy semantics by default.
325
325
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
326
326
  database can coexist with the source database:
327
327
 
@@ -759,7 +759,7 @@ Restore a database dump with `pg_restore`:
759
759
  ./odoo-scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
760
760
  ```
761
761
 
762
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
762
+ Database restores use copy semantics by default.
763
763
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
764
764
  database can coexist with the source database:
765
765
 
@@ -0,0 +1 @@
1
+ __version__ = "1.24.3"
@@ -1049,10 +1049,27 @@ def _parse_odoo_version(odoo_version: str) -> int:
1049
1049
 
1050
1050
  def _validate_docker_image(value: str, option: str) -> str:
1051
1051
  image = (value or "").strip()
1052
- if not image:
1052
+ # This value is embedded directly into Dockerfile FROM instructions.
1053
+ # Require a single Docker image reference (registry, tag and digest allowed).
1054
+ component = r"[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*"
1055
+ label = r"[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?"
1056
+ registry = rf"(?:{label}(?:\.{label})*(?::[0-9]+)?|\[[0-9A-Fa-f:.]+\](?::[0-9]+)?)"
1057
+ tag = r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}"
1058
+ digest = r"[A-Za-z][A-Za-z0-9_+.-]*:[A-Za-z0-9=_-]{32,}"
1059
+ if (
1060
+ not image
1061
+ or len(image) > 512
1062
+ or "\n" in (value or "")
1063
+ or "\r" in (value or "")
1064
+ or re.fullmatch(
1065
+ rf"(?:{registry}/)?{component}(?:/{component})*(?::{tag})?(?:@{digest})?",
1066
+ image,
1067
+ flags=re.ASCII,
1068
+ ) is None
1069
+ ):
1053
1070
  raise Exception(
1054
1071
  f"Invalid option '{option}' in section [docker] "
1055
- "(expected non-empty Docker image name/tag)."
1072
+ "(expected a single Docker image reference, optionally with registry, tag or digest)."
1056
1073
  )
1057
1074
 
1058
1075
  return image
@@ -1271,6 +1288,13 @@ def load_project_config(
1271
1288
  for sec in cp.sections():
1272
1289
  if sec.startswith("addons."):
1273
1290
  name = sec.split(".", 1)[1]
1291
+ # Git addon destinations are ROOT/odoo-addons/<name>. Keep the
1292
+ # section name a single, portable path component (also on Windows).
1293
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*", name) or name.endswith("."):
1294
+ raise Exception(
1295
+ f"Invalid addon section name [{sec}]: use letters, digits, '.', '_' or '-' "
1296
+ "without path separators or traversal components."
1297
+ )
1274
1298
 
1275
1299
  has_path = cp.has_option(sec, "path")
1276
1300
  has_repo = cp.has_option(sec, "repo")
@@ -2151,7 +2175,22 @@ def _validate_local_odoo_path(layout: Layout, spec: OdooSpec) -> Path:
2151
2175
 
2152
2176
 
2153
2177
  def _resolve_addon_path(layout: Layout, addon_name: str, spec: AddonSpec) -> Path:
2154
- return _resolve_workspace_path(layout, spec.path or "", layout.addons_root / addon_name)
2178
+ if spec.is_local:
2179
+ return _resolve_workspace_path(layout, spec.path or "", layout.addons_root / addon_name)
2180
+ dest = layout.addons_root / addon_name
2181
+ # Implicit Git destinations must not follow symlinks into another repo,
2182
+ # outside the workspace, or back onto the addons root itself.
2183
+ if (
2184
+ not _is_path_within(layout.addons_root, layout.root)
2185
+ or dest.is_symlink()
2186
+ or not _is_path_within(dest, layout.addons_root)
2187
+ or dest.resolve() == layout.addons_root.resolve()
2188
+ ):
2189
+ raise Exception(
2190
+ f"Unsafe addon destination for [addons.{addon_name}]: {dest} "
2191
+ f"must resolve inside {layout.addons_root}. Use an explicit 'path' for local addons."
2192
+ )
2193
+ return dest
2155
2194
 
2156
2195
 
2157
2196
  def _validate_local_addon_path(layout: Layout, addon_name: str, spec: AddonSpec) -> Path:
@@ -2498,11 +2537,44 @@ def _native_default_db_name(cfg: ProjectConfig) -> str:
2498
2537
  db_name = str(cfg.config.get("db_name") or "").strip()
2499
2538
  if not db_name:
2500
2539
  raise Exception("Native database helpers require [config].db_name.")
2501
- if db_name in (".", "..") or any(c in db_name for c in ("/", "\\", "\x00", "\r", "\n")):
2502
- raise Exception("Database helpers require a single database name, not a path.")
2540
+ # These names become PostgreSQL identifiers, filesystem path components and
2541
+ # Windows batch variables. Reject traversal, CMD metacharacters and names
2542
+ # that PostgreSQL could silently truncate to another database name.
2543
+ if len(db_name) > 63 or not re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.-]*", db_name):
2544
+ raise Exception(
2545
+ "Invalid [config].db_name: expected 1-63 ASCII letters, digits, "
2546
+ "underscores, dots or hyphens, starting with a letter, digit or underscore."
2547
+ )
2503
2548
  return db_name
2504
2549
 
2505
2550
 
2551
+ def _native_filestore_data_dir_sh(layout: Layout) -> str:
2552
+ # Preserve workspace-relative paths when the workspace is moved.
2553
+ try:
2554
+ relative = layout.data_dir.relative_to(layout.root)
2555
+ except ValueError:
2556
+ return shlex.quote(str(layout.data_dir))
2557
+ return f'"${{ROOT_DIR}}"/{shlex.quote(relative.as_posix())}'
2558
+
2559
+
2560
+ def _native_filestore_data_dir_bat(layout: Layout) -> str:
2561
+ # Paths cannot contain double quotes on Windows; reject control characters
2562
+ # rather than generating unsafe batch assignments.
2563
+ if any(char in str(layout.data_dir) for char in ('"', '\r', '\n', '\x00')):
2564
+ raise Exception("Native filestore data_dir contains invalid Windows path characters.")
2565
+ try:
2566
+ relative = layout.data_dir.relative_to(layout.root)
2567
+ except ValueError:
2568
+ value = str(layout.data_dir)
2569
+ else:
2570
+ suffix = '' if str(relative) == '.' else '\\' + str(relative)
2571
+ value = '%ROOT_DIR%' + suffix
2572
+ # Keep %ROOT_DIR% expandable while escaping literal percent signs in paths.
2573
+ if value.startswith('%ROOT_DIR%'):
2574
+ return '%ROOT_DIR%' + value[len('%ROOT_DIR%'):].replace('%', '%%').replace('^', '^^')
2575
+ return value.replace('%', '%%').replace('^', '^^')
2576
+
2577
+
2506
2578
  def _native_pg_env(cfg: ProjectConfig) -> dict[str, str]:
2507
2579
  # libpq reads these environment variables, keeping passwords out of command
2508
2580
  # arguments. Missing/False options leave libpq defaults and .pgpass intact.
@@ -2857,6 +2929,7 @@ echo "INFO: Database neutralization completed: ${{DB_NAME}}"
2857
2929
 
2858
2930
  def write_native_backup_filestore_sh(layout: Layout, cfg: ProjectConfig) -> Path:
2859
2931
  configured_db_name = shlex.quote(_native_default_db_name(cfg))
2932
+ configured_data_dir = _native_filestore_data_dir_sh(layout)
2860
2933
  content = fr"""#!/usr/bin/env bash
2861
2934
  set -euo pipefail
2862
2935
 
@@ -2865,7 +2938,8 @@ ROOT_DIR="$(cd "${{SCRIPT_DIR}}/.." && pwd)"
2865
2938
 
2866
2939
  BACKUPS_DIR="${{ROOT_DIR}}/odoo-backups"
2867
2940
  DB_NAME={configured_db_name}
2868
- FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{DB_NAME}}"
2941
+ DATA_DIR={configured_data_dir}
2942
+ FILESTORE_PATH="${{DATA_DIR}}/filestore/${{DB_NAME}}"
2869
2943
  TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
2870
2944
  OUTPUT="${{1:-${{BACKUPS_DIR}}/${{DB_NAME}}_${{TIMESTAMP}}_filestore.tar.gz}}"
2871
2945
  TMP_OUTPUT="${{OUTPUT}}.tmp"
@@ -2887,6 +2961,7 @@ echo "INFO: Filestore backup created: ${{OUTPUT}}"
2887
2961
 
2888
2962
  def write_native_restore_filestore_sh(layout: Layout, cfg: ProjectConfig) -> Path:
2889
2963
  configured_db_name = shlex.quote(_native_default_db_name(cfg))
2964
+ configured_data_dir = _native_filestore_data_dir_sh(layout)
2890
2965
  content = fr"""#!/usr/bin/env bash
2891
2966
  set -euo pipefail
2892
2967
 
@@ -2894,7 +2969,8 @@ SCRIPT_DIR="$(cd "$(dirname "${{BASH_SOURCE[0]}}")" && pwd)"
2894
2969
  ROOT_DIR="$(cd "${{SCRIPT_DIR}}/.." && pwd)"
2895
2970
 
2896
2971
  DB_NAME={configured_db_name}
2897
- FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{DB_NAME}}"
2972
+ DATA_DIR={configured_data_dir}
2973
+ FILESTORE_PATH="${{DATA_DIR}}/filestore/${{DB_NAME}}"
2898
2974
  FORCE=false
2899
2975
  SOURCE=""
2900
2976
 
@@ -2959,6 +3035,7 @@ echo "INFO: Filestore restore completed: ${{DB_NAME}}"
2959
3035
 
2960
3036
  def write_native_backup_filestore_restic_sh(layout: Layout, cfg: ProjectConfig) -> Path:
2961
3037
  configured_db_name = shlex.quote(_native_default_db_name(cfg))
3038
+ configured_data_dir = _native_filestore_data_dir_sh(layout)
2962
3039
  content = fr'''#!/usr/bin/env bash
2963
3040
  set -euo pipefail
2964
3041
 
@@ -2967,7 +3044,8 @@ ROOT_DIR="$(cd "${{SCRIPT_DIR}}/.." && pwd)"
2967
3044
  RESTIC="${{SCRIPT_DIR}}/restic.sh"
2968
3045
  DB_NAME={configured_db_name}
2969
3046
  RESTIC_HOST_NAME="${{RESTIC_HOST:-$(hostname -f 2>/dev/null || hostname)}}"
2970
- FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{DB_NAME}}"
3047
+ DATA_DIR={configured_data_dir}
3048
+ FILESTORE_PATH="${{DATA_DIR}}/filestore/${{DB_NAME}}"
2971
3049
 
2972
3050
  if [[ ! -x "${{RESTIC}}" ]]; then
2973
3051
  echo "ERROR: restic helper not found: ${{RESTIC}}" >&2
@@ -2994,6 +3072,7 @@ echo "INFO: Backing up filestore for database '${{DB_NAME}}' with restic."
2994
3072
 
2995
3073
  def write_native_restore_filestore_restic_sh(layout: Layout, cfg: ProjectConfig) -> Path:
2996
3074
  configured_db_name = shlex.quote(_native_default_db_name(cfg))
3075
+ configured_data_dir = _native_filestore_data_dir_sh(layout)
2997
3076
  restic_command = _native_restic_command_sh()
2998
3077
  content = fr'''#!/usr/bin/env bash
2999
3078
  set -euo pipefail
@@ -3008,8 +3087,9 @@ RESTIC_HOST_NAME="${{RESTIC_HOST:-$(hostname -f 2>/dev/null || hostname)}}"
3008
3087
  FORCE=false
3009
3088
  SNAPSHOT="latest"
3010
3089
  SNAPSHOT_SET=false
3011
- SOURCE_FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{SOURCE_DB_NAME}}"
3012
- TARGET_FILESTORE_PATH="${{ROOT_DIR}}/odoo-data/filestore/${{DB_NAME}}"
3090
+ DATA_DIR={configured_data_dir}
3091
+ SOURCE_FILESTORE_PATH="${{DATA_DIR}}/filestore/${{SOURCE_DB_NAME}}"
3092
+ TARGET_FILESTORE_PATH="${{DATA_DIR}}/filestore/${{DB_NAME}}"
3013
3093
 
3014
3094
  usage() {{
3015
3095
  echo "Usage: $(basename "$0") [--force] [SNAPSHOT]" >&2
@@ -3371,6 +3451,7 @@ exit /b 0
3371
3451
 
3372
3452
  def write_native_backup_filestore_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3373
3453
  configured_db_name = _native_default_db_name(cfg).replace("%", "%%")
3454
+ configured_data_dir = _native_filestore_data_dir_bat(layout)
3374
3455
  content = rf"""@echo off
3375
3456
  setlocal DisableDelayedExpansion
3376
3457
 
@@ -3379,7 +3460,8 @@ if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
3379
3460
  for %%I in ("%SCRIPT_DIR%\..") do set "ROOT_DIR=%%~fI"
3380
3461
  set "BACKUPS_DIR=%ROOT_DIR%\odoo-backups"
3381
3462
  set "DB_NAME={configured_db_name}"
3382
- set "FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%DB_NAME%"
3463
+ set "DATA_DIR={configured_data_dir}"
3464
+ set "FILESTORE_PATH=%DATA_DIR%\filestore\%DB_NAME%"
3383
3465
 
3384
3466
  for /f %%I in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') do set "TIMESTAMP=%%I"
3385
3467
  if "%~1"=="" (
@@ -3412,6 +3494,7 @@ endlocal
3412
3494
 
3413
3495
  def write_native_restore_filestore_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3414
3496
  configured_db_name = _native_default_db_name(cfg).replace("%", "%%")
3497
+ configured_data_dir = _native_filestore_data_dir_bat(layout)
3415
3498
  content = rf"""@echo off
3416
3499
  setlocal DisableDelayedExpansion
3417
3500
 
@@ -3419,7 +3502,8 @@ set "SCRIPT_DIR=%~dp0"
3419
3502
  if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%"
3420
3503
  for %%I in ("%SCRIPT_DIR%\..") do set "ROOT_DIR=%%~fI"
3421
3504
  set "DB_NAME={configured_db_name}"
3422
- set "FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%DB_NAME%"
3505
+ set "DATA_DIR={configured_data_dir}"
3506
+ set "FILESTORE_PATH=%DATA_DIR%\filestore\%DB_NAME%"
3423
3507
  set "FORCE=false"
3424
3508
  set "SOURCE="
3425
3509
 
@@ -3484,6 +3568,7 @@ exit /b 2
3484
3568
 
3485
3569
  def write_native_backup_filestore_restic_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3486
3570
  configured_db_name = _native_default_db_name(cfg).replace("%", "%%")
3571
+ configured_data_dir = _native_filestore_data_dir_bat(layout)
3487
3572
  content = rf"""@echo off
3488
3573
  setlocal DisableDelayedExpansion
3489
3574
 
@@ -3497,7 +3582,8 @@ if defined RESTIC_HOST (
3497
3582
  ) else (
3498
3583
  for /f "delims=" %%H in ('hostname') do set "RESTIC_HOST_NAME=%%H"
3499
3584
  )
3500
- set "FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%DB_NAME%"
3585
+ set "DATA_DIR={configured_data_dir}"
3586
+ set "FILESTORE_PATH=%DATA_DIR%\filestore\%DB_NAME%"
3501
3587
 
3502
3588
  if not exist "%RESTIC%" (
3503
3589
  echo ERROR: restic helper not found: %RESTIC%
@@ -3519,6 +3605,7 @@ exit /b %ERRORLEVEL%
3519
3605
 
3520
3606
  def write_native_restore_filestore_restic_bat(layout: Layout, cfg: ProjectConfig) -> Path:
3521
3607
  configured_db_name = _native_default_db_name(cfg).replace("%", "%%")
3608
+ configured_data_dir = _native_filestore_data_dir_bat(layout)
3522
3609
  restic_command = _native_restic_command_bat()
3523
3610
  content = rf"""@echo off
3524
3611
  setlocal DisableDelayedExpansion
@@ -3541,8 +3628,9 @@ if defined RESTIC_HOST (
3541
3628
  set "FORCE=false"
3542
3629
  set "SNAPSHOT=latest"
3543
3630
  set "SNAPSHOT_SET=false"
3544
- set "SOURCE_FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%SOURCE_DB_NAME%"
3545
- set "TARGET_FILESTORE_PATH=%ROOT_DIR%\odoo-data\filestore\%DB_NAME%"
3631
+ set "DATA_DIR={configured_data_dir}"
3632
+ set "SOURCE_FILESTORE_PATH=%DATA_DIR%\filestore\%SOURCE_DB_NAME%"
3633
+ set "TARGET_FILESTORE_PATH=%DATA_DIR%\filestore\%DB_NAME%"
3546
3634
 
3547
3635
  goto parse_args
3548
3636
 
@@ -4308,9 +4396,16 @@ def write_docker_compose(layout: Layout, cfg: ProjectConfig) -> Path:
4308
4396
 
4309
4397
  def _docker_default_db_name(cfg: ProjectConfig) -> str:
4310
4398
  db_name = cfg.config.get("db_name")
4311
- if isinstance(db_name, str) and db_name.strip():
4312
- return db_name.strip()
4313
- return "odoo"
4399
+ if not isinstance(db_name, str) or not db_name.strip():
4400
+ return "odoo"
4401
+ db_name = db_name.strip()
4402
+ # The same name is used by database and destructive filestore helpers.
4403
+ if len(db_name) > 63 or not re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.-]*", db_name):
4404
+ raise Exception(
4405
+ "Invalid [config].db_name: expected 1-63 ASCII letters, digits, "
4406
+ "underscores, dots or hyphens, starting with a letter, digit or underscore."
4407
+ )
4408
+ return db_name
4314
4409
 
4315
4410
 
4316
4411
  def _write_docker_local_script(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: odoo-devops-tools
3
- Version: 1.24.2
3
+ Version: 1.24.3
4
4
  Summary: Build reproducible Odoo workspaces from a single project definition.
5
5
  Author: Roman Lacko
6
6
  License-Expression: MIT
@@ -349,7 +349,7 @@ Restore a database dump with `pg_restore`:
349
349
  ./docker/local/scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
350
350
  ```
351
351
 
352
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
352
+ Database restores use copy semantics by default.
353
353
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
354
354
  database can coexist with the source database:
355
355
 
@@ -787,7 +787,7 @@ Restore a database dump with `pg_restore`:
787
787
  ./odoo-scripts/restore-db.sh ./odoo-backups/pre-upgrade.dump
788
788
  ```
789
789
 
790
- Database restores use copy semantics by default, matching `click-odoo-restoredb --copy`.
790
+ Database restores use copy semantics by default.
791
791
  After `pg_restore`, Odoo database identity parameters are regenerated so the restored
792
792
  database can coexist with the source database:
793
793
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "odoo-devops-tools"
7
- version = "1.24.2"
7
+ version = "1.24.3"
8
8
  description = "Build reproducible Odoo workspaces from a single project definition."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "1.24.2"