Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions podman/domain/volumes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any, Literal, Optional, Union

import requests
import pathlib

from podman import api
from podman.domain.manager import Manager, PodmanResource
Expand Down Expand Up @@ -173,3 +174,49 @@ def remove(self, name: Union[Volume, str], force: Optional[bool] = None) -> None
name = name.name
response = self.client.delete(f"/volumes/{name}", params={"force": force})
response.raise_for_status()

def export_archive(self, name: Union[Volume, str]) -> bytes:
"""Export a podman volume, returns the exported archive as bytes.

Args:
name: Identifier for Volume to be exported.

Raises:
APIError: when service reports an error
"""
if isinstance(name, Volume):
name = name.name
response = self.client.get(f"/volumes/{name}/export")
response.raise_for_status()
return response._content

def import_archive(
self, name: Union[Volume, str], data: Optional[bytes] = None, path: Optional[str] = None
):
"""Import a podman volume from tar.
The podman volume archive must be provided either as bytes or as a path to the archive.

Args:
name: Identifier for Volume to be imported.
data: Uncompressed tar archive as bytes.
path: Path to uncompressed tar archive.

Raises:
APIError: when service reports an error
"""
if isinstance(name, Volume):
name = name.name

if data is None and path is None:
raise RuntimeError("Either data or path must be provided !")
elif data is not None and path is not None:
raise RuntimeError("Data and path must not be set at the same time !")

if data is None:
file = pathlib.Path(path)
if not file.exists():
raise RuntimeError(f"Archive {path} does not exist !")
data = file.read_bytes()

response = self.client.post(f"/volumes/{name}/import", data=data)
response.raise_for_status()
8 changes: 8 additions & 0 deletions podman/tests/integration/test_volumes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from podman import PodmanClient
from podman.errors import NotFound
from podman.tests.integration import base
from podman.tests.utils import PODMAN_VERSION


class VolumesIntegrationTest(base.IntegrationTest):
Expand Down Expand Up @@ -35,6 +36,13 @@ def test_volume_crud(self):
names = [i.name for i in report]
self.assertIn(volume_name, names)

if PODMAN_VERSION >= (5, 6, 0):
with self.subTest("Export"):
archive_bytes = self.client.volumes.export_archive(volume_name)

with self.subTest("Import"):
self.client.volumes.import_archive(volume_name, archive_bytes)

with self.subTest("Remove"):
self.client.volumes.remove(volume_name, force=True)
with self.assertRaises(NotFound):
Expand Down
22 changes: 22 additions & 0 deletions podman/tests/unit/test_volumesmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,28 @@ def test_prune(self, mock):
actual, {"VolumesDeleted": ["dbase", "source"], "SpaceReclaimed": 2048}
)

@requests_mock.Mocker()
def test_import(self, mock):
mock.post(
tests.LIBPOD_URL + "/volumes/dbase/import",
status_code=requests.codes.no_content,
)
actual = self.client.volumes.import_archive("dbase", data=b'mocked_archive')
self.assertIsInstance(actual, type(None))

with self.assertRaises(RuntimeError):
# The archive does not exist
self.client.volumes.import_archive("dbase", path="/path/to/archive.tar")

@requests_mock.Mocker()
def test_export(self, mock):
mock.get(
tests.LIBPOD_URL + "/volumes/dbase/export",
content=b'exported_archive',
)
actual = self.client.volumes.export_archive("dbase")
self.assertIsInstance(actual, bytes)
Copy link
Contributor Author

@Riushda Riushda Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I'm not sure about the value of these mock tests (for the import and export). I wouldn't have added them if you didn't ask for it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My motivation is primarily code coverage. In addition to this, tests help me a lot with future features working with the community, even when they are simple.

I'd like to hear your opinion: why you are not sure of the value for these specific tests. Do you mean they are too trivial?

thanks a lot for the patience and for updating the pr

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was mainly talking about the functions I added. There isn't much to test without an actual volume and archive file. But don't get me wrong, I think mock tests in general are great to add more test coverage in particular for complex scenario that are not easy to reproduce in actual integration test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be very nice to do something like this in python, but we are missing other options in the python bindings to do so

podman volume create testvol
podman volume export testvol --output testvol.tar
mkdir testvol
tar -xvf testvol.tar -C testvol/
touch  testvol/test
tar -rf testvol.tar testvol/test
cat testvol.tar | podman volume import testvol -
podman unshare
podman volume mount testvol
ls /home/nsella/.local/share/containers/storage/volumes/testvol/_data/testvol/test

So I think your tests are now good



if __name__ == '__main__':
unittest.main()