WIP: Fix docker config restore in corner case

Store Docker config per location to fix corner case (when encrypted
and unencrypted backup get consolidated).
This commit is contained in:
Stefan Agner 2025-02-04 16:54:14 +00:00
parent 9164d35615
commit aa0313f1b9
No known key found for this signature in database
GPG Key ID: AE01353D1E44747D
4 changed files with 47 additions and 23 deletions

View File

@ -202,12 +202,12 @@ class Backup(JobGroup):
@property @property
def docker(self) -> dict[str, Any]: def docker(self) -> dict[str, Any]:
"""Return backup Docker config data.""" """Return backup Docker config data."""
return self._data.get(ATTR_DOCKER, {}) return self._locations[self.location].get(ATTR_DOCKER, {})
@docker.setter @docker.setter
def docker(self, value: dict[str, Any]) -> None: def docker(self, value: dict[str, Any]) -> None:
"""Set the Docker config data.""" """Set the Docker config data."""
self._data[ATTR_DOCKER] = value self._locations[self.location][ATTR_DOCKER] = value
@property @property
def location(self) -> str | None: def location(self) -> str | None:
@ -389,7 +389,7 @@ class Backup(JobGroup):
decrypt = self._aes.decryptor() decrypt = self._aes.decryptor()
padder = padding.PKCS7(128).unpadder() padder = padding.PKCS7(128).unpadder()
_LOGGER.info("Decrypting data: %s", data)
data = padder.update(decrypt.update(b64decode(data))) + padder.finalize() data = padder.update(decrypt.update(b64decode(data))) + padder.finalize()
return data.decode() return data.decode()
@ -486,6 +486,8 @@ class Backup(JobGroup):
if self._data[ATTR_PROTECTED]: if self._data[ATTR_PROTECTED]:
self._locations[self.location][ATTR_PROTECTED] = True self._locations[self.location][ATTR_PROTECTED] = True
if self._data[ATTR_DOCKER]:
self._locations[self.location][ATTR_DOCKER] = self._data[ATTR_DOCKER]
return True return True

View File

@ -12,6 +12,7 @@ from shutil import copy
from ..addons.addon import Addon from ..addons.addon import Addon
from ..const import ( from ..const import (
ATTR_DAYS_UNTIL_STALE, ATTR_DAYS_UNTIL_STALE,
ATTR_DOCKER,
ATTR_PATH, ATTR_PATH,
ATTR_PROTECTED, ATTR_PROTECTED,
FILE_HASSIO_BACKUPS, FILE_HASSIO_BACKUPS,
@ -368,7 +369,11 @@ class BackupManager(FileConfiguration, JobGroup):
backup.all_locations.update( backup.all_locations.update(
{ {
loc: {ATTR_PATH: path, ATTR_PROTECTED: backup.protected} loc: {
ATTR_PATH: path,
ATTR_PROTECTED: backup.protected,
ATTR_DOCKER: backup.docker,
}
for loc, path in all_new_locations.items() for loc, path in all_new_locations.items()
} }
) )

View File

@ -486,7 +486,9 @@ async def test_restore_immediate_errors(
with ( with (
patch.object( patch.object(
Backup, "all_locations", new={None: {"path": None, "protected": True}} Backup,
"all_locations",
new={None: {"path": None, "protected": True, "docker": {}}},
), ),
patch.object(Backup, "validate_password", return_value=False), patch.object(Backup, "validate_password", return_value=False),
): ):
@ -638,8 +640,12 @@ async def test_backup_to_multiple_locations(
assert orig_backup.exists() assert orig_backup.exists()
assert copy_backup.exists() assert copy_backup.exists()
assert coresys.backups.get(slug).all_locations == { assert coresys.backups.get(slug).all_locations == {
None: {"path": orig_backup, "protected": False}, None: {"path": orig_backup, "protected": False, "docker": {"registries": {}}},
".cloud_backup": {"path": copy_backup, "protected": False}, ".cloud_backup": {
"path": copy_backup,
"protected": False,
"docker": {"registries": {}},
},
} }
assert coresys.backups.get(slug).location is None assert coresys.backups.get(slug).location is None
@ -699,8 +705,16 @@ async def test_upload_to_multiple_locations(
assert orig_backup.exists() assert orig_backup.exists()
assert copy_backup.exists() assert copy_backup.exists()
assert coresys.backups.get("7fed74c8").all_locations == { assert coresys.backups.get("7fed74c8").all_locations == {
None: {"path": orig_backup, "protected": False}, None: {
".cloud_backup": {"path": copy_backup, "protected": False}, "path": orig_backup,
"protected": False,
"docker": {"registries": {}},
},
".cloud_backup": {
"path": copy_backup,
"protected": False,
"docker": {"registries": {}},
},
} }
assert coresys.backups.get("7fed74c8").location is None assert coresys.backups.get("7fed74c8").location is None
@ -714,7 +728,7 @@ async def test_upload_duplicate_backup_new_location(
orig_backup = Path(copy(backup_file, coresys.config.path_backup)) orig_backup = Path(copy(backup_file, coresys.config.path_backup))
await coresys.backups.reload() await coresys.backups.reload()
assert coresys.backups.get("7fed74c8").all_locations == { assert coresys.backups.get("7fed74c8").all_locations == {
None: {"path": orig_backup, "protected": False} None: {"path": orig_backup, "protected": False, "docker": {"registries": {}}}
} }
with backup_file.open("rb") as file, MultipartWriter("form-data") as mp: with backup_file.open("rb") as file, MultipartWriter("form-data") as mp:
@ -731,8 +745,12 @@ async def test_upload_duplicate_backup_new_location(
assert orig_backup.exists() assert orig_backup.exists()
assert copy_backup.exists() assert copy_backup.exists()
assert coresys.backups.get("7fed74c8").all_locations == { assert coresys.backups.get("7fed74c8").all_locations == {
None: {"path": orig_backup, "protected": False}, None: {"path": orig_backup, "protected": False, "docker": {"registries": {}}},
".cloud_backup": {"path": copy_backup, "protected": False}, ".cloud_backup": {
"path": copy_backup,
"protected": False,
"docker": {"registries": {}},
},
} }
assert coresys.backups.get("7fed74c8").location is None assert coresys.backups.get("7fed74c8").location is None
@ -769,7 +787,7 @@ async def test_upload_with_filename(
orig_backup = coresys.config.path_backup / filename orig_backup = coresys.config.path_backup / filename
assert orig_backup.exists() assert orig_backup.exists()
assert coresys.backups.get("7fed74c8").all_locations == { assert coresys.backups.get("7fed74c8").all_locations == {
None: {"path": orig_backup, "protected": False} None: {"path": orig_backup, "protected": False, "docker": {"registries": {}}}
} }
assert coresys.backups.get("7fed74c8").location is None assert coresys.backups.get("7fed74c8").location is None
@ -802,8 +820,12 @@ async def test_remove_backup_from_location(api_client: TestClient, coresys: Core
await coresys.backups.reload() await coresys.backups.reload()
assert (backup := coresys.backups.get("7fed74c8")) assert (backup := coresys.backups.get("7fed74c8"))
assert backup.all_locations == { assert backup.all_locations == {
None: {"path": location_1, "protected": False}, None: {"path": location_1, "protected": False, "docker": {"registries": {}}},
".cloud_backup": {"path": location_2, "protected": False}, ".cloud_backup": {
"path": location_2,
"protected": False,
"docker": {"registries": {}},
},
} }
resp = await api_client.delete( resp = await api_client.delete(
@ -814,7 +836,9 @@ async def test_remove_backup_from_location(api_client: TestClient, coresys: Core
assert location_1.exists() assert location_1.exists()
assert not location_2.exists() assert not location_2.exists()
assert coresys.backups.get("7fed74c8") assert coresys.backups.get("7fed74c8")
assert backup.all_locations == {None: {"path": location_1, "protected": False}} assert backup.all_locations == {
None: {"path": location_1, "protected": False, "docker": {"registries": {}}}
}
@pytest.mark.usefixtures("tmp_supervisor_data") @pytest.mark.usefixtures("tmp_supervisor_data")
@ -973,11 +997,6 @@ async def test_restore_backup_unencrypted_after_encrypted(
".cloud_backup": {"path": Path(unc_tar), "protected": False}, ".cloud_backup": {"path": Path(unc_tar), "protected": False},
} }
# pylint: disable=fixme
# TODO: There is a bug in the restore code that causes the restore to fail
# if the backup contains a Docker registry configuration and one location
# is encrypted and the other is not (just like our test fixture).
# We punt the ball on this one for this PR since this is a rare edge case.
backup.restore_dockerconfig = MagicMock() backup.restore_dockerconfig = MagicMock()
coresys.core.state = CoreState.RUNNING coresys.core.state = CoreState.RUNNING

View File

@ -584,7 +584,6 @@ async def mock_full_backup(coresys: CoreSys, tmp_path) -> Backup:
mock_backup = Backup(coresys, Path(tmp_path, "test_backup.tar"), "test", None) mock_backup = Backup(coresys, Path(tmp_path, "test_backup.tar"), "test", None)
mock_backup.new("Test", utcnow().isoformat(), BackupType.FULL) mock_backup.new("Test", utcnow().isoformat(), BackupType.FULL)
mock_backup.repositories = ["https://github.com/awesome-developer/awesome-repo"] mock_backup.repositories = ["https://github.com/awesome-developer/awesome-repo"]
mock_backup.docker = {}
mock_backup._data[ATTR_ADDONS] = [ mock_backup._data[ATTR_ADDONS] = [
{ {
ATTR_SLUG: "local_ssh", ATTR_SLUG: "local_ssh",
@ -609,7 +608,6 @@ async def mock_partial_backup(coresys: CoreSys, tmp_path) -> Backup:
mock_backup = Backup(coresys, Path(tmp_path, "test_backup.tar"), "test", None) mock_backup = Backup(coresys, Path(tmp_path, "test_backup.tar"), "test", None)
mock_backup.new("Test", utcnow().isoformat(), BackupType.PARTIAL) mock_backup.new("Test", utcnow().isoformat(), BackupType.PARTIAL)
mock_backup.repositories = ["https://github.com/awesome-developer/awesome-repo"] mock_backup.repositories = ["https://github.com/awesome-developer/awesome-repo"]
mock_backup.docker = {}
mock_backup._data[ATTR_ADDONS] = [ mock_backup._data[ATTR_ADDONS] = [
{ {
ATTR_SLUG: "local_ssh", ATTR_SLUG: "local_ssh",