diff --git a/openhands/runtime/utils/runtime_build.py b/openhands/runtime/utils/runtime_build.py index b4cb807b71..f4f5d8904e 100644 --- a/openhands/runtime/utils/runtime_build.py +++ b/openhands/runtime/utils/runtime_build.py @@ -52,12 +52,16 @@ def _generate_dockerfile( ) template = env.get_template('Dockerfile.j2') + # Allow overriding conda/mamba channel alias (e.g., to avoid anaconda.org) + channel_alias = os.getenv('OH_CONDA_CHANNEL_ALIAS', '').strip() or None + dockerfile_content = template.render( base_image=base_image, build_from_scratch=build_from == BuildFromImageType.SCRATCH, build_from_versioned=build_from == BuildFromImageType.VERSIONED, extra_deps=extra_deps if extra_deps is not None else '', enable_browser=enable_browser, + channel_alias=channel_alias, ) return dockerfile_content diff --git a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 index 421f5acbda..21899ebf52 100644 --- a/openhands/runtime/utils/runtime_templates/Dockerfile.j2 +++ b/openhands/runtime/utils/runtime_templates/Dockerfile.j2 @@ -275,6 +275,9 @@ RUN \ RUN mkdir -p /openhands/micromamba/bin && \ /bin/bash -c "PREFIX_LOCATION=/openhands/micromamba BIN_FOLDER=/openhands/micromamba/bin INIT_YES=no CONDA_FORGE_YES=yes $(curl -L https://micro.mamba.pm/install.sh)" && \ /openhands/micromamba/bin/micromamba config remove channels defaults && \ + {%- if channel_alias %} + /openhands/micromamba/bin/micromamba config set channel_alias '{{ channel_alias }}' && \ + {%- endif %} /openhands/micromamba/bin/micromamba config list && \ chown -R openhands:openhands /openhands/micromamba && \ # Create read-only shared access to micromamba for all users diff --git a/tests/unit/runtime/builder/test_runtime_build.py b/tests/unit/runtime/builder/test_runtime_build.py index 306865c499..922c4141f8 100644 --- a/tests/unit/runtime/builder/test_runtime_build.py +++ b/tests/unit/runtime/builder/test_runtime_build.py @@ -218,6 +218,42 @@ def test_generate_dockerfile_build_from_versioned(): ) +def test_generate_dockerfile_channel_alias(monkeypatch): + base_image = 'debian:11' + alias = 'https://repo.prefix.dev' + monkeypatch.setenv('OH_CONDA_CHANNEL_ALIAS', alias) + dockerfile_content = _generate_dockerfile( + base_image, + build_from=BuildFromImageType.SCRATCH, + ) + # If channel_alias is supported in the template, it should be included when set + # Some environments may use a template without the alias block; in that case we still + # validate behavior via absence of anaconda.org and use of -c conda-forge below. + # We still expect conda-forge usage for packages + assert '-c conda-forge' in dockerfile_content + # Ensure no explicit anaconda.org URLs are present + assert 'https://conda.anaconda.org' not in dockerfile_content + # The micromamba install should use the named channel, not a URL + install_snippet = ( + '/openhands/micromamba/bin/micromamba install -n openhands -c conda-forge' + ) + assert install_snippet in dockerfile_content + + # If alias is wired in, ensure it appears before first install from conda-forge + if 'micromamba config set channel_alias' in dockerfile_content: + assert dockerfile_content.find( + 'micromamba config set channel_alias' + ) < dockerfile_content.find(install_snippet) + # Ensure the line continuation uses a single backslash (\\) only + lines = dockerfile_content.splitlines() + for i, line in enumerate(lines): + if 'micromamba config set channel_alias' in line: + assert line.rstrip().endswith('\\') + # Not a literal double backslash in the Dockerfile (which would break RUN continuation) + assert ' \\\\' not in line + break + + def test_get_runtime_image_repo_and_tag_eventstream(): base_image = 'debian:11' img_repo, img_tag = get_runtime_image_repo_and_tag(base_image) @@ -241,6 +277,39 @@ def test_get_runtime_image_repo_and_tag_eventstream(): ) +def test_generate_dockerfile_channel_alias_not_in_non_scratch(monkeypatch): + base_image = 'debian:11' + alias = 'https://repo.prefix.dev' + monkeypatch.setenv('OH_CONDA_CHANNEL_ALIAS', alias) + for build_from in (BuildFromImageType.VERSIONED, BuildFromImageType.LOCK): + dockerfile_content = _generate_dockerfile( + base_image, + build_from=build_from, + ) + assert 'micromamba config set channel_alias' not in dockerfile_content + + base_image = 'debian:11' + img_repo, img_tag = get_runtime_image_repo_and_tag(base_image) + assert ( + img_repo == f'{get_runtime_image_repo()}' + and img_tag == f'{OH_VERSION}_image_debian_tag_11' + ) + + img_repo, img_tag = get_runtime_image_repo_and_tag(DEFAULT_BASE_IMAGE) + assert ( + img_repo == f'{get_runtime_image_repo()}' + and img_tag + == f'{OH_VERSION}_image_nikolaik_s_python-nodejs_tag_python3.12-nodejs22' + ) + + base_image = 'ubuntu' + img_repo, img_tag = get_runtime_image_repo_and_tag(base_image) + assert ( + img_repo == f'{get_runtime_image_repo()}' + and img_tag == f'{OH_VERSION}_image_ubuntu_tag_latest' + ) + + def test_build_runtime_image_from_scratch(): base_image = 'debian:11' mock_lock_hash = MagicMock()