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
11 changes: 10 additions & 1 deletion nihil/manager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,11 @@ def _pull_with_progress(self, image: str) -> None:
TextColumn,
)
from rich.console import Console
from nihil.utils.platform_info import get_image_platform
console = Console()
tasks: dict = {}
totals: dict = {}
image_platform = get_image_platform()
with Progress(
TextColumn("[bold cyan]{task.fields[layer]:<14}[/]"),
TextColumn("[bold white]{task.fields[status]:<20}[/]"),
Expand All @@ -235,7 +237,10 @@ def _pull_with_progress(self, image: str) -> None:
console=console,
transient=False,
) as progress:
for event in self.client.api.pull(image, stream=True, decode=True):
pull_options = {"stream": True, "decode": True}
if image_platform:
pull_options["platform"] = image_platform
for event in self.client.api.pull(image, **pull_options):
layer_id = event.get("id", "")
status = event.get("status", "")
detail = event.get("progressDetail") or {}
Expand Down Expand Up @@ -426,6 +431,10 @@ def create_container(
host_binding = ("127.0.0.1", browser_ui_port)
container_config["ports"] = container_config.get("ports") or {}
container_config["ports"][port_key] = host_binding
from nihil.utils.platform_info import get_image_platform
image_platform = get_image_platform()
if image_platform:
container_config["platform"] = image_platform
try:
container = self.client.containers.create(**container_config)
return container
Expand Down
4 changes: 2 additions & 2 deletions nihil/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Utilitaires : historique, doctor, platform
from nihil.utils.history import log_command, HISTORY_PATH
from nihil.utils.doctor import NihilDoctor, DoctorCheckResult
from nihil.utils.platform_info import get_host_os, get_docker_engine, host_network_supported, HostOS, DockerEngine
from nihil.utils.platform_info import get_host_os, get_docker_engine, get_image_platform, host_network_supported, HostOS, DockerEngine

__all__ = ["log_command", "HISTORY_PATH", "NihilDoctor", "DoctorCheckResult", "get_host_os", "get_docker_engine", "host_network_supported", "HostOS", "DockerEngine"]
__all__ = ["log_command", "HISTORY_PATH", "NihilDoctor", "DoctorCheckResult", "get_host_os", "get_docker_engine", "get_image_platform", "host_network_supported", "HostOS", "DockerEngine"]
12 changes: 12 additions & 0 deletions nihil/utils/platform_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import platform
from enum import Enum
from typing import Optional


class HostOS(Enum):
Expand Down Expand Up @@ -53,3 +54,14 @@ def host_network_supported(host_os: HostOS, engine: DockerEngine) -> bool:
if host_os == HostOS.WSL and engine == DockerEngine.NATIVE:
return True
return False


NIHIL_IMAGE_PLATFORM = "linux/amd64"


def get_image_platform() -> Optional[str]:
"""Return the Nihil image platform required by the current host."""
machine = platform.machine().lower()
if machine in ("arm64", "aarch64"):
return NIHIL_IMAGE_PLATFORM
return None
58 changes: 58 additions & 0 deletions tests/test_nihilManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,36 @@ def test_ensure_image_exists_pull_success(self, mock_docker_client, mock_formatt
assert result is True
mock_pull.assert_called_once_with("test-image:latest")

def test_pull_with_progress_sets_platform_on_arm(self, mock_docker_client):
"""Test pull force linux/amd64 sur un hôte ARM."""
mock_docker_client.api.pull.return_value = []

with patch('nihil.manager.manager.docker.from_env', return_value=mock_docker_client):
with patch('nihil.manager.manager.ensure_filesystem'):
with patch('nihil.utils.platform_info.get_image_platform', return_value='linux/amd64'):
with patch.object(NihilManager, 'snapshot_local_image_as_version'):
manager = NihilManager()
manager._pull_with_progress("test-image:latest")

mock_docker_client.api.pull.assert_called_once_with(
"test-image:latest", stream=True, decode=True, platform="linux/amd64"
)

def test_pull_with_progress_uses_native_platform_on_amd64(self, mock_docker_client):
"""Test pull laisse Docker choisir sur un hôte amd64."""
mock_docker_client.api.pull.return_value = []

with patch('nihil.manager.manager.docker.from_env', return_value=mock_docker_client):
with patch('nihil.manager.manager.ensure_filesystem'):
with patch('nihil.utils.platform_info.get_image_platform', return_value=None):
with patch.object(NihilManager, 'snapshot_local_image_as_version'):
manager = NihilManager()
manager._pull_with_progress("test-image:latest")

mock_docker_client.api.pull.assert_called_once_with(
"test-image:latest", stream=True, decode=True
)

def test_ensure_image_exists_pull_fails(self, mock_docker_client):
"""Test ensure_image_exists quand le pull échoue (manager utilise _pull_with_progress)."""
mock_docker_client.images.get.side_effect = docker.errors.ImageNotFound("Not found")
Expand Down Expand Up @@ -100,6 +130,34 @@ def test_create_container_config_defaults(self, mock_docker_client):
assert config["privileged"] is False
assert config["hostname"] == "test-container"

def test_create_container_sets_platform_on_arm(self, mock_docker_client):
"""Test création de container force linux/amd64 sur un hôte ARM."""
mock_container = MagicMock()
mock_docker_client.containers.create.return_value = mock_container
mock_docker_client.images.get.return_value = MagicMock()

with patch('nihil.manager.manager.docker.from_env', return_value=mock_docker_client):
with patch('nihil.manager.manager.ensure_filesystem'):
with patch('nihil.utils.platform_info.get_image_platform', return_value='linux/amd64'):
manager = NihilManager()
manager.create_container("test-container")

assert mock_docker_client.containers.create.call_args.kwargs["platform"] == "linux/amd64"

def test_create_container_uses_native_platform_on_amd64(self, mock_docker_client):
"""Test création de container laisse Docker choisir sur un hôte amd64."""
mock_container = MagicMock()
mock_docker_client.containers.create.return_value = mock_container
mock_docker_client.images.get.return_value = MagicMock()

with patch('nihil.manager.manager.docker.from_env', return_value=mock_docker_client):
with patch('nihil.manager.manager.ensure_filesystem'):
with patch('nihil.utils.platform_info.get_image_platform', return_value=None):
manager = NihilManager()
manager.create_container("test-container")

assert "platform" not in mock_docker_client.containers.create.call_args.kwargs

def test_create_container_with_privileged(self, mock_docker_client):
"""Test création de container avec --privileged"""
mock_container = MagicMock()
Expand Down
25 changes: 25 additions & 0 deletions tests/test_platform_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Tests unitaires pour platform_info.py"""

from unittest.mock import patch

from nihil.utils.platform_info import get_image_platform


def test_get_image_platform_native_amd64():
"""Ne force pas la plateforme sur un hôte amd64 natif."""
with patch("nihil.utils.platform_info.platform.machine", return_value="x86_64"):
assert get_image_platform() is None


def test_get_image_platform_arm64():
"""Force linux/amd64 sur un hôte ARM64 pour les images Nihil."""
with patch("nihil.utils.platform_info.platform.machine", return_value="arm64"):
assert get_image_platform() == "linux/amd64"


def test_get_image_platform_unknown_architecture():
"""Laisse Docker choisir sur une architecture non prise en charge."""
with patch("nihil.utils.platform_info.platform.machine", return_value="riscv64"):
assert get_image_platform() is None
Loading