code-sandboxes 1.9.36__tar.gz → 1.9.37__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.
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/PKG-INFO +1 -1
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/__version__.py +1 -1
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/datalayer.py +21 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/resolve.py +82 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/resolve_conda.py +15 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/specification.mdx +1 -1
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/modal.mdx +24 -8
- code_sandboxes-1.9.37/tests/egress_logs/buildctl-base.log +16 -0
- code_sandboxes-1.9.37/tests/egress_logs/buildctl-curl.log +41 -0
- code_sandboxes-1.9.37/tests/egress_logs/buildctl-pip.log +43 -0
- code_sandboxes-1.9.37/tests/egress_logs/buildctl-uv.log +54 -0
- code_sandboxes-1.9.37/tests/egress_logs/micromamba.log +30 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_datalayer_builder.py +5 -2
- code_sandboxes-1.9.37/tests/test_environment_egress_refusals.py +146 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/build.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/environments-live.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/py-code-style.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/py-tests.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/py-typing.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.github/workflows/reusable-python.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.gitignore +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.licenserc.yaml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.pre-commit-config.yaml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/.vscode/settings.json +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/CHANGELOG.md +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/Dockerfile +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/LICENSE +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/Makefile +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/README.md +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/__init__.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/base.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/bridge_mount.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/builds.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/cli.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/client.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/cloudflare_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/commands.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/console.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/contents.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/coreweave_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/daytona_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/docker_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/e2b_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/__init__.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/__main__.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/accounts.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/__init__.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/daytona.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/e2b.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/managed.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/modal.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/attest.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/bases.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/breaker.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/build_secrets.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/builders.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/canonical.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/conformance.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/constraints/sandbox-contract-v1.txt +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/constraints/wheelhouse/README.md +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/constraints/wheelhouse/jupyter_server-2.21.0+datalayer.1-py3-none-any.whl +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/contract.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/doctor/__init__.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/doctor/build.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/doctor/datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/doctor/report.schema.json +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/errors.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/files.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/image_import.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/lifecycle.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/policy.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/redact.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/schema.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/spec.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/eval_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/exceptions.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/filesystem.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/google_colab.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/interfaces.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/jupyter_server_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/kaggle.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/kaggle_execute.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/kaggle_live.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/kaggle_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/lifecycle.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/manage.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/modal_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/models.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/monty_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/provider_ingress.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/providers.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docker/Dockerfile +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/.gitignore +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/LICENSE +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/Makefile +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/README.md +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/babel.config.js +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/api-reference/_category_.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/api-reference/index.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/cli/_category_.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/cli/index.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/contribute/_category_.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/contribute/index.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/_category_.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/contract.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/index.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/environments/spike-2026-09.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/examples/_category_.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/examples/index.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/guide/_category_.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/guide/index.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/index.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/install/_category_.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/install/index.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/provider-ingress.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/_category_.yml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/cloudflare.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/coreweave.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/datalayer.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/daytona.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/docker.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/e2b.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/eval.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/google-colab.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/index.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/jupyter-server.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/kaggle.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docs/providers/monty.mdx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/docusaurus.config.js +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/package.json +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/sidebars.js +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow1.js +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow1.module.css +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow2.js +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow2.module.css +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow3.js +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/components/HomepageRow3.module.css +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/css/custom.css +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/pages/index.module.css +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/pages/markdown-page.md +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/theme/CustomDocItem.tsx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/src/theme/IconsTitle.tsx +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/bricks.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/building-construction.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/cloud-sun.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/cloud.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/collaboration.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/copy-left.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/logo.png +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/logo.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/personas/business.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/favicon.ico +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/jupyter.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/open-source.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/react-js.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/react-js2.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/ringed-planet.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/robot.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/rocket.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/shield.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/sparkles.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/streamlit.gif +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/super-hero.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/target.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/docs/static/img/thumbs-up.svg +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/README.md +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/Makefile +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/exec/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/Makefile +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/cloudflare_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/coreweave_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/datalayer_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/daytona_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/docker_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/e2b_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/eval_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/google_colab_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/jupyter_server_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/kaggle_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/modal_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/examples/repl/monty_sandbox_example.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/pyproject.toml +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/schemas/environment-v1alpha1.json +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/__init__.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/conftest.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_a_datalayer_sandbox_can_be_interrupted.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_bridge_mount.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_builds.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_cli_exec.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_cli_repl.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_client.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_client_contents.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_client_files.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_client_outcome_error_output.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_cloudflare.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_commands_envelope.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_console.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_contents_contract.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_contents_live_matrix.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_coreweave.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_datalayer_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_daytona.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_e2b.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_accounts.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_attest.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_bases.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_breaker.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_build_secrets.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_builders.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_canonical.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_conformance.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_constraints.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_contract.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_cross_variant_report.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_daytona_builder.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_dockerfile_source.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_doctor.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_e2b_builder.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_errors.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_image_import.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_lifecycle.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_live_matrix.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_managed_builders.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_modal_builder.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_organization_policy.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_redact.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_resolve.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_resolve_conda.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_spec.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_environment_spec_organization_policy.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_eval.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_every_variant_has_a_real_interrupt.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_factory.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_google_colab.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_integration.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_jupyter_ingress.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_jupyter_server.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_jupyter_server_streaming.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kaggle.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kaggle_execute.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kaggle_live.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kaggle_session.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_kernel_client_compatibility.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_lifecycle_vocabulary.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_manage.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_manage_datalayer.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_modal_google_colab_sandbox.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_modal_session.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_models.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_output_line_fidelity.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_provider_catalog_names.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_provider_ingress.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_providers.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_sandboxes.py +0 -0
- {code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/tests/test_the_kill_relaunch_drill.py +0 -0
{code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/adapters/datalayer.py
RENAMED
|
@@ -78,6 +78,9 @@ from ..resolve import (
|
|
|
78
78
|
apt_snapshot_in,
|
|
79
79
|
buildkit_proxy,
|
|
80
80
|
buildkit_proxy_options,
|
|
81
|
+
egress_findings,
|
|
82
|
+
egress_hosts_text,
|
|
83
|
+
egress_refused_hosts,
|
|
81
84
|
locked_versions,
|
|
82
85
|
)
|
|
83
86
|
from ..resolve_conda import (
|
|
@@ -546,6 +549,24 @@ class Builder:
|
|
|
546
549
|
self._log(f"Building {reference} from {request.resolved_base}")
|
|
547
550
|
finished = self._invoke(command, timeout=self._max_build_seconds)
|
|
548
551
|
if finished.returncode != 0:
|
|
552
|
+
refused = egress_refused_hosts(finished.stderr or "", proxy=self._proxy)
|
|
553
|
+
if refused:
|
|
554
|
+
# The one failure a log line hid best: a host the build
|
|
555
|
+
# pool may not reach, named as a finding (E2-19).
|
|
556
|
+
raise EnvironmentsError(
|
|
557
|
+
BUILD_FAILED,
|
|
558
|
+
f"The build pool's egress proxy refused {egress_hosts_text(refused)}: "
|
|
559
|
+
"a build reaches only the package indexes, mirrors and registries "
|
|
560
|
+
"the build pool allows. Put what it fetches in the build, or ask "
|
|
561
|
+
"for the host to be allowed",
|
|
562
|
+
detail={
|
|
563
|
+
"variant": self.variant,
|
|
564
|
+
"reference": reference,
|
|
565
|
+
"exit": finished.returncode,
|
|
566
|
+
"refused_hosts": refused,
|
|
567
|
+
"findings": egress_findings(refused),
|
|
568
|
+
},
|
|
569
|
+
)
|
|
549
570
|
raise EnvironmentsError(
|
|
550
571
|
BUILD_FAILED,
|
|
551
572
|
"The build failed; its log says where",
|
|
@@ -101,6 +101,7 @@ __all__ = [
|
|
|
101
101
|
"apt_snapshot_in",
|
|
102
102
|
"buildkit_proxy",
|
|
103
103
|
"buildkit_proxy_options",
|
|
104
|
+
"egress_refused_hosts",
|
|
104
105
|
"lock_document",
|
|
105
106
|
"locked_versions",
|
|
106
107
|
"merge_requirements",
|
|
@@ -188,6 +189,74 @@ def buildkit_proxy_options(proxy: str) -> list[str]:
|
|
|
188
189
|
return options
|
|
189
190
|
|
|
190
191
|
|
|
192
|
+
#: What each tool writes when the build pool's egress proxy refuses it a
|
|
193
|
+
#: tunnel (E2-19). Read from the tools themselves, through the chart's own
|
|
194
|
+
#: Squid refusing every host, on 2026-09-19: pip, uv 0.12, micromamba 2.3,
|
|
195
|
+
#: git, curl 8 without `-f`, wget, and a Go client — `buildkitd`'s own pulls.
|
|
196
|
+
#: Each is a refused CONNECT and nothing else: a host that answered 403 over
|
|
197
|
+
#: its own TLS says so differently. `curl -f` ("The requested URL returned
|
|
198
|
+
#: error: 403"), `wget -q` (nothing) and `apt` over plain HTTP ("403
|
|
199
|
+
#: Forbidden [IP: proxy]") cannot be told from the host's own 403, and are
|
|
200
|
+
#: left to the log.
|
|
201
|
+
_EGRESS_REFUSED = re.compile(
|
|
202
|
+
r"Tunnel connection failed: 403" # pip, requests, urllib3
|
|
203
|
+
r"|tunnel error: unsuccessful" # uv
|
|
204
|
+
r"|CONNECT tunnel failed, response 403" # curl, git, micromamba
|
|
205
|
+
r"|Proxy tunneling failed: Forbidden" # wget
|
|
206
|
+
r'|"https?://[^"\s]+": Forbidden' # Go: buildkitd, containerd
|
|
207
|
+
)
|
|
208
|
+
#: A host a line names: urllib3's `host='…'`, or the host of a URL.
|
|
209
|
+
_NAMED_HOST = re.compile(r"host='(?P<pool>[A-Za-z0-9.\-]+)'|https?://(?P<url>[A-Za-z0-9.\-]+)")
|
|
210
|
+
#: `buildctl`'s plain progress: `#12 0.874 <what the step wrote>`. Steps run
|
|
211
|
+
#: side by side, so a line is read against the lines of its own step.
|
|
212
|
+
_BUILDKIT_LINE = re.compile(r"^#(?P<step>\d+) (?:\d+\.\d+ )?(?P<text>.*)$")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def egress_refused_hosts(log: str, *, proxy: str = "", limit: int = 10) -> list[str]:
|
|
216
|
+
"""The hosts the build pool's egress proxy refused, as a build's log shows them (E2-19).
|
|
217
|
+
|
|
218
|
+
A refusal is found by what the tool writes (`_EGRESS_REFUSED`), and its
|
|
219
|
+
host is the one the same line names — pip's `host='files.pythonhosted.org'`,
|
|
220
|
+
git's URL — or else the last one its step named before it: uv and
|
|
221
|
+
micromamba write the URL a line or four above the refusal, wget and a
|
|
222
|
+
silent `curl` only in the `RUN` line. The proxy's own address is never a
|
|
223
|
+
host it refused. In the order they were first refused, at most ``limit``.
|
|
224
|
+
"""
|
|
225
|
+
own = {"127.0.0.1", "localhost"}
|
|
226
|
+
if proxy:
|
|
227
|
+
own.add(proxy.split("://", 1)[-1].rsplit(":", 1)[0])
|
|
228
|
+
hosts: list[str] = []
|
|
229
|
+
last: dict[str, str] = {}
|
|
230
|
+
for raw in log.splitlines():
|
|
231
|
+
line = _BUILDKIT_LINE.match(raw)
|
|
232
|
+
step, text = (line.group("step"), line.group("text")) if line else ("", raw)
|
|
233
|
+
named = [
|
|
234
|
+
host.lower().rstrip(".")
|
|
235
|
+
for match in _NAMED_HOST.finditer(text)
|
|
236
|
+
for host in (match.group("pool") or match.group("url"),)
|
|
237
|
+
if host and host.lower().rstrip(".") not in own
|
|
238
|
+
]
|
|
239
|
+
if _EGRESS_REFUSED.search(text):
|
|
240
|
+
host = named[0] if named else last.get(step, "")
|
|
241
|
+
if host and host not in hosts:
|
|
242
|
+
hosts.append(host)
|
|
243
|
+
if len(hosts) >= limit:
|
|
244
|
+
break
|
|
245
|
+
if named:
|
|
246
|
+
last[step] = named[-1]
|
|
247
|
+
return hosts
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def egress_findings(hosts: Sequence[str]) -> list[dict[str, str]]:
|
|
251
|
+
"""The refused hosts as a failure's findings: what a page lists, not the log."""
|
|
252
|
+
return [{"kind": "egress_refused", "subject": host} for host in hosts]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def egress_hosts_text(hosts: Sequence[str]) -> str:
|
|
256
|
+
quoted = [f"`{host}`" for host in hosts]
|
|
257
|
+
return quoted[0] if len(quoted) == 1 else ", ".join(quoted[:-1]) + " and " + quoted[-1]
|
|
258
|
+
|
|
259
|
+
|
|
191
260
|
#: How a protected pin is recorded in the lock.
|
|
192
261
|
PROTECTED_PIN_PREFIX = "# datalayer-protected: "
|
|
193
262
|
|
|
@@ -521,6 +590,19 @@ def parse_resolver_failure(
|
|
|
521
590
|
},
|
|
522
591
|
)
|
|
523
592
|
|
|
593
|
+
# An index the build pool may not reach is not an outage: resolving again
|
|
594
|
+
# is refused again, so it is said with the host rather than retried.
|
|
595
|
+
refused = egress_refused_hosts(output)
|
|
596
|
+
if refused and "No solution found" not in text:
|
|
597
|
+
return EnvironmentsError(
|
|
598
|
+
PACKAGE_NOT_FOUND,
|
|
599
|
+
f"The build pool's egress proxy refused {egress_hosts_text(refused)}: a version "
|
|
600
|
+
"resolves only against the indexes the build pool allows. Use one of them, or "
|
|
601
|
+
"ask for the host to be allowed",
|
|
602
|
+
detail={**detail, "refused_hosts": refused, "findings": egress_findings(refused)},
|
|
603
|
+
retryable=False,
|
|
604
|
+
)
|
|
605
|
+
|
|
524
606
|
pair = _conflicting_pair(mentions) if "No solution found" in text else None
|
|
525
607
|
if pair:
|
|
526
608
|
return EnvironmentsError(
|
{code_sandboxes-1.9.36 → code_sandboxes-1.9.37}/code_sandboxes/environments/resolve_conda.py
RENAMED
|
@@ -70,6 +70,9 @@ from .resolve import (
|
|
|
70
70
|
ProtectedPin,
|
|
71
71
|
buildkit_proxy,
|
|
72
72
|
buildkit_proxy_options,
|
|
73
|
+
egress_findings,
|
|
74
|
+
egress_hosts_text,
|
|
75
|
+
egress_refused_hosts,
|
|
73
76
|
merge_requirements,
|
|
74
77
|
)
|
|
75
78
|
|
|
@@ -529,6 +532,18 @@ def parse_conda_failure(output: str) -> EnvironmentsError:
|
|
|
529
532
|
f"`{name}` is not in any channel this environment may read",
|
|
530
533
|
detail={**detail, "package": name},
|
|
531
534
|
)
|
|
535
|
+
# A channel the build pool may not reach: solving again is refused again,
|
|
536
|
+
# so it is said with the host rather than retried (E2-19).
|
|
537
|
+
refused = egress_refused_hosts(output)
|
|
538
|
+
if refused:
|
|
539
|
+
return EnvironmentsError(
|
|
540
|
+
PACKAGE_NOT_FOUND,
|
|
541
|
+
f"The build pool's egress proxy refused {egress_hosts_text(refused)}: a version "
|
|
542
|
+
"solves only against the channels the build pool allows. Use one of them, or "
|
|
543
|
+
"ask for the host to be allowed",
|
|
544
|
+
detail={**detail, "refused_hosts": refused, "findings": egress_findings(refused)},
|
|
545
|
+
retryable=False,
|
|
546
|
+
)
|
|
532
547
|
lowered = text.lower()
|
|
533
548
|
if (
|
|
534
549
|
"could not solve" in lowered
|
|
@@ -37,7 +37,7 @@ A document is refused with `DL_ENV_SPEC_INVALID` when a field is wrong, and with
|
|
|
37
37
|
| `commands.postInstall` | none | At most 32 non-empty commands, run after the packages are installed. |
|
|
38
38
|
| `buildSecrets` | none | `id` is a Datalayer secret id, `dlsec_...`; `mountAs` is `env` or `file`; `name` is the variable or file name. Ids and names are unique. Secrets are injected into the build step that needs them and never written into the artifact. |
|
|
39
39
|
| `resources.sizeClass` | `small` | `small`, `medium`, `large`, `gpu-small` or `gpu-large`. A GPU class needs an accelerator and a CUDA base; an accelerator needs a GPU class. Datalayer runs no GPU nodes yet, so a GPU class is built for Modal and Daytona, which bring their own GPUs; E2B has none. |
|
|
40
|
-
| `resources.accelerator` | `none` | `none`, or `type`, `count` and `cuda`. `type` is the provider's own name for the GPU (Daytona: `H100`, `H200`, `RTX-PRO-6000`, `RTX-4090`, `RTX-5090`), and a GPU a variant does not offer is refused for it at `validate`. `cuda` is the base's toolkit: `datalayer/python-cuda:2026.09` carries 12.8, and another version is refused. |
|
|
40
|
+
| `resources.accelerator` | `none` | `none`, or `type`, `count` and `cuda`. `type` is the provider's own name for the GPU (Modal: `T4`, `L4`, `A10G`, `L40S`, `A100`, `A100-40GB`, `A100-80GB`, `H100`, `H200`, `B200`; Daytona: `H100`, `H200`, `RTX-PRO-6000`, `RTX-4090`, `RTX-5090`), and a GPU a variant does not offer is refused for it at `validate`. `cuda` is the base's toolkit: `datalayer/python-cuda:2026.09` carries 12.8, and another version is refused. |
|
|
41
41
|
| `resources.hints` | none | `cpu`, `memoryGi`, `diskGi`: positive numbers, within the size class. |
|
|
42
42
|
| `compatibility.variants.required` | `[datalayer]` | At least one of `datalayer`, `e2b`, `daytona`, `modal`. |
|
|
43
43
|
| `compatibility.variants.optional` | none | The same variants, none of them also required. |
|
|
@@ -77,9 +77,11 @@ with Sandbox.create(
|
|
|
77
77
|
An [Environment](/environments) built for Modal is an **image id**,
|
|
78
78
|
`im-…` — pulled `from_aws_ecr` off the approved Datalayer base, with your
|
|
79
79
|
lock installed by `uv pip sync --require-hashes` on top. A published name
|
|
80
|
-
is worth having for operability, but a launch always pins the id:
|
|
81
|
-
build
|
|
82
|
-
|
|
80
|
+
is worth having for operability, but a launch always pins the id: a name
|
|
81
|
+
moves when the same build is published again, and the id is the only
|
|
82
|
+
reference that names one build's image. Modal answers an identical image
|
|
83
|
+
definition with the image it already has, so two builds of the same lock
|
|
84
|
+
can share one id.
|
|
83
85
|
|
|
84
86
|
**Bring your own account:** a build runs in the environment owner's own
|
|
85
87
|
Modal workspace, from the same `MODAL_TOKEN_ID`/`MODAL_TOKEN_SECRET` pair
|
|
@@ -96,8 +98,22 @@ with no image id — general Modal sandbox usage here is unaffected).
|
|
|
96
98
|
Confirmed live: a launched sandbox's identity check now passes, where it
|
|
97
99
|
used to report `uid: 0`.
|
|
98
100
|
|
|
99
|
-
**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
**The sandbox is held by `sleep infinity`, not the base's Jupyter.**
|
|
102
|
+
Modal keeps the base image's CMD, `start-jupyter.sh`, under the builder's
|
|
103
|
+
`ENTRYPOINT`, where Docker would reset it, so a sandbox started with no
|
|
104
|
+
command ran a Jupyter server as its main process. That server exits
|
|
105
|
+
within a minute and the sandbox ends with it: "Modal Sandbox is shutting
|
|
106
|
+
down", a few commands in. Since 1.9.36 the artifact's CMD is
|
|
107
|
+
`sleep infinity`, and `ModalSandbox` names `sleep infinity` when it
|
|
108
|
+
launches an artifact, which covers images built by earlier releases too.
|
|
109
|
+
Jupyter is started by exec when it is asked for
|
|
110
|
+
(`prepare_jupyter_server`).
|
|
111
|
+
|
|
112
|
+
**GPU versions.** On Modal a GPU is a launch option, not part of the
|
|
113
|
+
image: a GPU version's image builds like any other on the CUDA base, and
|
|
114
|
+
its sandboxes start on the GPU the version names. The build's smoke test
|
|
115
|
+
does that (`gpu="L4"`, or `"H100:2"` for two) and adds the contract's
|
|
116
|
+
check 11: the GPUs asked for are visible, and the image's CUDA is the
|
|
117
|
+
version's. A GPU Modal does not offer — it offers `T4`, `L4`, `A10G`,
|
|
118
|
+
`L40S`, `A100`, `A100-40GB`, `A100-80GB`, `H100`, `H200`, `B200` — is
|
|
119
|
+
refused at `validate`, before anything is built.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#1 [internal] load build definition from Dockerfile
|
|
2
|
+
#1 transferring dockerfile: 80B done
|
|
3
|
+
#1 DONE 0.0s
|
|
4
|
+
|
|
5
|
+
#2 [internal] load metadata for ghcr.io/astral-sh/uv:0.12.11
|
|
6
|
+
#2 ERROR: failed to do request: Head "https://ghcr.io/v2/astral-sh/uv/manifests/0.12.11": Forbidden
|
|
7
|
+
------
|
|
8
|
+
> [internal] load metadata for ghcr.io/astral-sh/uv:0.12.11:
|
|
9
|
+
------
|
|
10
|
+
Dockerfile:1
|
|
11
|
+
--------------------
|
|
12
|
+
1 | >>> FROM ghcr.io/astral-sh/uv:0.12.11
|
|
13
|
+
2 | RUN true
|
|
14
|
+
3 |
|
|
15
|
+
--------------------
|
|
16
|
+
error: failed to solve: ghcr.io/astral-sh/uv:0.12.11: failed to resolve source metadata for ghcr.io/astral-sh/uv:0.12.11: failed to do request: Head "https://ghcr.io/v2/astral-sh/uv/manifests/0.12.11": Forbidden
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#1 [internal] load build definition from Dockerfile
|
|
2
|
+
#1 transferring dockerfile: 325B done
|
|
3
|
+
#1 DONE 0.0s
|
|
4
|
+
|
|
5
|
+
#2 [internal] load metadata for docker.io/library/buildpack-deps:bookworm-scm
|
|
6
|
+
#2 DONE 0.2s
|
|
7
|
+
|
|
8
|
+
#3 [internal] load .dockerignore
|
|
9
|
+
#3 transferring context: 2B done
|
|
10
|
+
#3 DONE 0.0s
|
|
11
|
+
|
|
12
|
+
#4 [1/4] FROM docker.io/library/buildpack-deps:bookworm-scm@sha256:bb8654d03bd8341e702f1ae30adca91a11f4164da2d37dbe7a0bf49e4901bd03
|
|
13
|
+
#4 CACHED
|
|
14
|
+
|
|
15
|
+
#5 [2/4] RUN git clone -q https://github.com/psf/requests.git /tmp/r || true
|
|
16
|
+
#5 0.063 fatal: unable to access 'https://github.com/psf/requests.git/': CONNECT tunnel failed, response 403
|
|
17
|
+
#5 DONE 0.1s
|
|
18
|
+
|
|
19
|
+
#6 [3/4] RUN curl -fsSL https://raw.githubusercontent.com/psf/requests/main/README.md -o /dev/null || true
|
|
20
|
+
#6 0.065 curl: (22) The requested URL returned error: 403
|
|
21
|
+
#6 DONE 0.1s
|
|
22
|
+
|
|
23
|
+
#7 [4/4] RUN wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null
|
|
24
|
+
#7 0.060 --2026-09-19 08:44:09-- https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz
|
|
25
|
+
#7 0.066 Connecting to 172.18.0.2:3128... connected.
|
|
26
|
+
#7 0.067 Proxy tunneling failed: ForbiddenUnable to establish SSL connection.
|
|
27
|
+
#7 ERROR: process "/bin/sh -c wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null" did not complete successfully: exit code: 4
|
|
28
|
+
------
|
|
29
|
+
> [4/4] RUN wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null:
|
|
30
|
+
0.060 --2026-09-19 08:44:09-- https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz
|
|
31
|
+
0.066 Connecting to 172.18.0.2:3128... connected.
|
|
32
|
+
0.067 Proxy tunneling failed: ForbiddenUnable to establish SSL connection.
|
|
33
|
+
------
|
|
34
|
+
Dockerfile:4
|
|
35
|
+
--------------------
|
|
36
|
+
2 | RUN git clone -q https://github.com/psf/requests.git /tmp/r || true
|
|
37
|
+
3 | RUN curl -fsSL https://raw.githubusercontent.com/psf/requests/main/README.md -o /dev/null || true
|
|
38
|
+
4 | >>> RUN wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null
|
|
39
|
+
5 |
|
|
40
|
+
--------------------
|
|
41
|
+
error: failed to solve: process "/bin/sh -c wget https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz -O /dev/null" did not complete successfully: exit code: 4
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#1 [internal] load build definition from Dockerfile
|
|
2
|
+
#1 transferring dockerfile: 192B done
|
|
3
|
+
#1 DONE 0.0s
|
|
4
|
+
|
|
5
|
+
#2 [internal] load metadata for docker.io/library/python:3.12-slim
|
|
6
|
+
#2 DONE 0.2s
|
|
7
|
+
|
|
8
|
+
#3 [internal] load .dockerignore
|
|
9
|
+
#3 transferring context: 2B done
|
|
10
|
+
#3 DONE 0.0s
|
|
11
|
+
|
|
12
|
+
#4 [1/3] FROM docker.io/library/python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9
|
|
13
|
+
#4 CACHED
|
|
14
|
+
|
|
15
|
+
#5 [2/3] RUN pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests
|
|
16
|
+
#5 0.887 Looking in indexes: https://pypi.example-private.io/simple
|
|
17
|
+
#5 0.890 WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
18
|
+
#5 1.391 WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
19
|
+
#5 2.393 WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
20
|
+
#5 4.395 WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
21
|
+
#5 8.397 WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
22
|
+
#5 8.401 ERROR: Could not find a version that satisfies the requirement requests (from versions: none)
|
|
23
|
+
#5 8.413 ERROR: No matching distribution found for requests
|
|
24
|
+
#5 ERROR: process "/bin/sh -c pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests" did not complete successfully: exit code: 1
|
|
25
|
+
------
|
|
26
|
+
> [2/3] RUN pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests:
|
|
27
|
+
0.887 Looking in indexes: https://pypi.example-private.io/simple
|
|
28
|
+
0.890 WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
29
|
+
1.391 WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
30
|
+
2.393 WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
31
|
+
4.395 WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
32
|
+
8.397 WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', OSError('Tunnel connection failed: 403 Forbidden'))': /simple/requests/
|
|
33
|
+
8.401 ERROR: Could not find a version that satisfies the requirement requests (from versions: none)
|
|
34
|
+
8.413 ERROR: No matching distribution found for requests
|
|
35
|
+
------
|
|
36
|
+
Dockerfile:2
|
|
37
|
+
--------------------
|
|
38
|
+
1 | FROM python:3.12-slim
|
|
39
|
+
2 | >>> RUN pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests
|
|
40
|
+
3 | RUN pip install --no-cache-dir requests
|
|
41
|
+
4 |
|
|
42
|
+
--------------------
|
|
43
|
+
error: failed to solve: process "/bin/sh -c pip install --no-cache-dir --index-url https://pypi.example-private.io/simple requests" did not complete successfully: exit code: 1
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#1 [internal] load build definition from Dockerfile
|
|
2
|
+
#1 transferring dockerfile: 193B done
|
|
3
|
+
#1 DONE 0.0s
|
|
4
|
+
|
|
5
|
+
#2 [internal] load metadata for docker.io/library/python:3.12-slim
|
|
6
|
+
#2 DONE 0.2s
|
|
7
|
+
|
|
8
|
+
#3 [internal] load .dockerignore
|
|
9
|
+
#3 transferring context: 2B done
|
|
10
|
+
#3 DONE 0.0s
|
|
11
|
+
|
|
12
|
+
#4 [1/3] FROM docker.io/library/python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9
|
|
13
|
+
#4 DONE 0.0s
|
|
14
|
+
|
|
15
|
+
#5 [internal] load build context
|
|
16
|
+
#5 transferring context: 26B done
|
|
17
|
+
#5 DONE 0.0s
|
|
18
|
+
|
|
19
|
+
#6 [2/3] COPY uv /usr/local/bin/uv
|
|
20
|
+
#6 CACHED
|
|
21
|
+
|
|
22
|
+
#7 [3/3] RUN uv pip install --system "requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz"
|
|
23
|
+
#7 0.175 Using Python 3.12.14 environment at: /usr/local
|
|
24
|
+
#7 9.090 × Failed to download and build `requests @
|
|
25
|
+
#7 9.090 │ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz`
|
|
26
|
+
#7 9.090 ├─▶ Request failed after 3 retries in 8.9s
|
|
27
|
+
#7 9.090 ├─▶ Failed to fetch:
|
|
28
|
+
#7 9.090 │ `https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz`
|
|
29
|
+
#7 9.090 ├─▶ error sending request for url
|
|
30
|
+
#7 9.090 │ (https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz)
|
|
31
|
+
#7 9.090 ├─▶ client error (Connect)
|
|
32
|
+
#7 9.090 ╰─▶ tunnel error: unsuccessful
|
|
33
|
+
#7 ERROR: process "/bin/sh -c uv pip install --system \"requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz\"" did not complete successfully: exit code: 1
|
|
34
|
+
------
|
|
35
|
+
> [3/3] RUN uv pip install --system "requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz":
|
|
36
|
+
0.175 Using Python 3.12.14 environment at: /usr/local
|
|
37
|
+
9.090 × Failed to download and build `requests @
|
|
38
|
+
9.090 │ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz`
|
|
39
|
+
9.090 ├─▶ Request failed after 3 retries in 8.9s
|
|
40
|
+
9.090 ├─▶ Failed to fetch:
|
|
41
|
+
9.090 │ `https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz`
|
|
42
|
+
9.090 ├─▶ error sending request for url
|
|
43
|
+
9.090 │ (https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz)
|
|
44
|
+
9.090 ├─▶ client error (Connect)
|
|
45
|
+
9.090 ╰─▶ tunnel error: unsuccessful
|
|
46
|
+
------
|
|
47
|
+
Dockerfile:3
|
|
48
|
+
--------------------
|
|
49
|
+
1 | FROM python:3.12-slim
|
|
50
|
+
2 | COPY uv /usr/local/bin/uv
|
|
51
|
+
3 | >>> RUN uv pip install --system "requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz"
|
|
52
|
+
4 |
|
|
53
|
+
--------------------
|
|
54
|
+
error: failed to solve: process "/bin/sh -c uv pip install --system \"requests @ https://github.com/psf/requests/archive/refs/tags/v2.32.3.tar.gz\"" did not complete successfully: exit code: 1
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
|
|
2
|
+
CONNECT tunnel failed, response 403
|
|
3
|
+
warning libmamba Retrying in 2 seconds
|
|
4
|
+
warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/linux-64/repodata.json.zst]
|
|
5
|
+
CONNECT tunnel failed, response 403
|
|
6
|
+
warning libmamba Retrying in 2 seconds
|
|
7
|
+
warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
|
|
8
|
+
CONNECT tunnel failed, response 403
|
|
9
|
+
warning libmamba Retrying in 2 seconds
|
|
10
|
+
warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/linux-64/repodata.json.zst]
|
|
11
|
+
CONNECT tunnel failed, response 403
|
|
12
|
+
warning libmamba Retrying in 2 seconds
|
|
13
|
+
warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
|
|
14
|
+
CONNECT tunnel failed, response 403
|
|
15
|
+
warning libmamba Retrying in 2 seconds
|
|
16
|
+
warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/linux-64/repodata.json.zst]
|
|
17
|
+
CONNECT tunnel failed, response 403
|
|
18
|
+
warning libmamba Retrying in 2 seconds
|
|
19
|
+
warning libmamba Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
|
|
20
|
+
CONNECT tunnel failed, response 403
|
|
21
|
+
warning libmamba Retrying in 2 seconds
|
|
22
|
+
critical libmamba Multiple errors occurred:
|
|
23
|
+
Download error (56) Failure when receiving data from the peer [https://conda.anaconda.org/conda-forge/noarch/repodata.json.zst]
|
|
24
|
+
CONNECT tunnel failed, response 403
|
|
25
|
+
Subdir conda-forge/noarch not loaded!
|
|
26
|
+
If you run into this error repeatedly, your package cache may be corrupted.
|
|
27
|
+
Please try running `mamba clean -a` to remove this cache before retrying the operation.
|
|
28
|
+
|
|
29
|
+
If you still are having issues, please report the error on `mamba-org/mamba`'s issue tracker:
|
|
30
|
+
https://github.com/mamba-org/mamba/issues/new?assignees=&labels=&projects=&template=bug.yml
|
|
@@ -184,9 +184,12 @@ def a_builder(**changes) -> Builder:
|
|
|
184
184
|
class Buildctl:
|
|
185
185
|
"""A `buildctl` that writes the metadata a successful build writes."""
|
|
186
186
|
|
|
187
|
-
def __init__(
|
|
187
|
+
def __init__(
|
|
188
|
+
self, *, digest: str | None = DIGEST, returncode: int = 0, stderr: str = "solved\n"
|
|
189
|
+
) -> None:
|
|
188
190
|
self.digest = digest
|
|
189
191
|
self.returncode = returncode
|
|
192
|
+
self.stderr = stderr
|
|
190
193
|
self.argv: list[str] = []
|
|
191
194
|
self.context: Path | None = None
|
|
192
195
|
self.env: dict[str, str] | None = None
|
|
@@ -201,7 +204,7 @@ class Buildctl:
|
|
|
201
204
|
Path(self.argv[position + 1]).write_text(
|
|
202
205
|
json.dumps({"containerimage.digest": self.digest}), encoding="utf-8"
|
|
203
206
|
)
|
|
204
|
-
return subprocess.CompletedProcess(self.argv, self.returncode, "",
|
|
207
|
+
return subprocess.CompletedProcess(self.argv, self.returncode, "", self.stderr)
|
|
205
208
|
|
|
206
209
|
|
|
207
210
|
# -- The Dockerfile ------------------------------------------------------------
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
2
|
+
# Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
"""A host the build pool's egress proxy refused, named as a finding (E2-19).
|
|
5
|
+
|
|
6
|
+
The logs under `egress_logs/` are real: `buildctl --progress=plain` against
|
|
7
|
+
`moby/buildkit:v0.33.0`, its steps sent through the build pool chart's own
|
|
8
|
+
Squid (`ubuntu/squid:6.6`, the chart's `squid.conf`, allowing `pypi.org`
|
|
9
|
+
alone), on 2026-09-19. Only the image layers' progress lines are left out.
|
|
10
|
+
`micromamba.log` is `micromamba create` 2.3.3 through the same proxy.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from code_sandboxes.environments.errors import EnvironmentsError
|
|
20
|
+
from code_sandboxes.environments.resolve import (
|
|
21
|
+
egress_hosts_text,
|
|
22
|
+
egress_refused_hosts,
|
|
23
|
+
parse_resolver_failure,
|
|
24
|
+
)
|
|
25
|
+
from code_sandboxes.environments.resolve_conda import parse_conda_failure
|
|
26
|
+
|
|
27
|
+
from .test_environment_datalayer_builder import Buildctl, a_builder, a_request
|
|
28
|
+
|
|
29
|
+
LOGS = Path(__file__).parent / "egress_logs"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def a_log(name: str) -> str:
|
|
33
|
+
return (LOGS / name).read_text(encoding="utf-8")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestTheRefusedHostIsReadFromWhatEachToolWrites:
|
|
37
|
+
def test_pip_names_its_index_a_line_before_it_is_refused(self) -> None:
|
|
38
|
+
# pip's retries say `Tunnel connection failed: 403` with a path only;
|
|
39
|
+
# the host is the index it said it was looking in.
|
|
40
|
+
assert egress_refused_hosts(a_log("buildctl-pip.log")) == ["pypi.example-private.io"]
|
|
41
|
+
|
|
42
|
+
def test_uv_names_the_url_four_lines_above_its_tunnel_error(self) -> None:
|
|
43
|
+
assert egress_refused_hosts(a_log("buildctl-uv.log")) == ["github.com"]
|
|
44
|
+
|
|
45
|
+
def test_git_and_wget_are_named_and_curl_f_is_left_to_the_log(self) -> None:
|
|
46
|
+
# `curl -f` says only "returned error: 403", which a host answering
|
|
47
|
+
# 403 itself says too: not a finding.
|
|
48
|
+
assert egress_refused_hosts(a_log("buildctl-curl.log"), proxy="http://172.18.0.2:3128") == [
|
|
49
|
+
"github.com"
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
def test_buildkitd_s_own_pull_of_a_base_is_named(self) -> None:
|
|
53
|
+
assert egress_refused_hosts(a_log("buildctl-base.log")) == ["ghcr.io"]
|
|
54
|
+
|
|
55
|
+
def test_micromamba_names_the_channel_a_line_above(self) -> None:
|
|
56
|
+
# The issue tracker's URL after the refusal is not what was refused.
|
|
57
|
+
assert egress_refused_hosts(a_log("micromamba.log")) == ["conda.anaconda.org"]
|
|
58
|
+
|
|
59
|
+
def test_a_log_with_no_refusal_names_nothing(self) -> None:
|
|
60
|
+
assert (
|
|
61
|
+
egress_refused_hosts(
|
|
62
|
+
"#5 [2/2] RUN pip install requests\n#5 1.2 Successfully installed\n"
|
|
63
|
+
)
|
|
64
|
+
== []
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def test_a_host_s_own_403_over_its_own_tls_is_not_a_refusal(self) -> None:
|
|
68
|
+
log = (
|
|
69
|
+
"#6 [3/4] RUN curl -fsSL https://example.org/private -o /dev/null\n"
|
|
70
|
+
"#6 0.065 curl: (22) The requested URL returned error: 403\n"
|
|
71
|
+
)
|
|
72
|
+
assert egress_refused_hosts(log) == []
|
|
73
|
+
|
|
74
|
+
def test_steps_side_by_side_are_read_apart(self) -> None:
|
|
75
|
+
# Step 8's URL is not step 7's refusal.
|
|
76
|
+
log = (
|
|
77
|
+
"#7 [3/4] RUN wget https://blocked.example/a.tar.gz\n"
|
|
78
|
+
"#8 [4/4] RUN curl https://allowed.example/b\n"
|
|
79
|
+
"#8 0.1 ok\n"
|
|
80
|
+
"#7 0.067 Proxy tunneling failed: ForbiddenUnable to establish SSL connection.\n"
|
|
81
|
+
)
|
|
82
|
+
assert egress_refused_hosts(log) == ["blocked.example"]
|
|
83
|
+
|
|
84
|
+
def test_the_proxy_itself_is_never_a_refused_host(self) -> None:
|
|
85
|
+
log = (
|
|
86
|
+
"#7 0.066 Connecting to proxy http://10.0.0.5:3128\n"
|
|
87
|
+
"#7 0.067 Proxy tunneling failed: Forbidden\n"
|
|
88
|
+
)
|
|
89
|
+
assert egress_refused_hosts(log, proxy="http://10.0.0.5:3128") == []
|
|
90
|
+
|
|
91
|
+
def test_each_host_once_in_the_order_refused(self) -> None:
|
|
92
|
+
log = a_log("buildctl-uv.log") + a_log("buildctl-base.log") + a_log("buildctl-uv.log")
|
|
93
|
+
assert egress_refused_hosts(log) == ["github.com", "ghcr.io"]
|
|
94
|
+
|
|
95
|
+
def test_the_hosts_read_as_a_sentence(self) -> None:
|
|
96
|
+
assert egress_hosts_text(["a.io"]) == "`a.io`"
|
|
97
|
+
assert egress_hosts_text(["a.io", "b.io", "c.io"]) == "`a.io`, `b.io` and `c.io`"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestARefusalIsAFindingOfTheFailure:
|
|
101
|
+
def test_a_build_refused_a_host_names_it_rather_than_the_log(self) -> None:
|
|
102
|
+
run = Buildctl(returncode=1, digest=None, stderr=a_log("buildctl-uv.log"))
|
|
103
|
+
with pytest.raises(EnvironmentsError) as raised:
|
|
104
|
+
a_builder(run=run).build(a_request())
|
|
105
|
+
error = raised.value
|
|
106
|
+
assert error.code.code == "DL_ENV_BUILD_FAILED"
|
|
107
|
+
assert error.retryable is False
|
|
108
|
+
assert "`github.com`" in error.message
|
|
109
|
+
assert error.detail["refused_hosts"] == ["github.com"]
|
|
110
|
+
assert error.detail["findings"] == [{"kind": "egress_refused", "subject": "github.com"}]
|
|
111
|
+
|
|
112
|
+
def test_a_build_that_failed_otherwise_still_says_to_read_the_log(self) -> None:
|
|
113
|
+
run = Buildctl(
|
|
114
|
+
returncode=1, digest=None, stderr=a_log("buildctl-curl.log").split("#7 [4/4]")[0]
|
|
115
|
+
)
|
|
116
|
+
with pytest.raises(EnvironmentsError) as raised:
|
|
117
|
+
a_builder(run=run).build(a_request())
|
|
118
|
+
# git's refusal in step 5 is in this log too: it is named.
|
|
119
|
+
assert raised.value.detail["refused_hosts"] == ["github.com"]
|
|
120
|
+
with pytest.raises(EnvironmentsError) as plain:
|
|
121
|
+
a_builder(run=Buildctl(returncode=1, digest=None, stderr="#5 0.1 exit 1\n")).build(
|
|
122
|
+
a_request()
|
|
123
|
+
)
|
|
124
|
+
assert "findings" not in plain.value.detail
|
|
125
|
+
assert plain.value.message == "The build failed; its log says where"
|
|
126
|
+
|
|
127
|
+
def test_a_resolve_refused_an_index_is_not_retried_as_an_outage(self) -> None:
|
|
128
|
+
error = parse_resolver_failure(a_log("buildctl-pip.log"))
|
|
129
|
+
assert error.code.code == "DL_ENV_PACKAGE_NOT_FOUND"
|
|
130
|
+
assert error.retryable is False
|
|
131
|
+
assert "`pypi.example-private.io`" in error.message
|
|
132
|
+
assert error.detail["findings"] == [
|
|
133
|
+
{"kind": "egress_refused", "subject": "pypi.example-private.io"}
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
def test_a_conda_solve_refused_a_channel_names_it(self) -> None:
|
|
137
|
+
error = parse_conda_failure(a_log("micromamba.log"))
|
|
138
|
+
assert error.code.code == "DL_ENV_PACKAGE_NOT_FOUND"
|
|
139
|
+
assert error.retryable is False
|
|
140
|
+
assert error.detail["refused_hosts"] == ["conda.anaconda.org"]
|
|
141
|
+
|
|
142
|
+
def test_an_unreachable_index_that_the_proxy_did_not_refuse_is_still_an_outage(self) -> None:
|
|
143
|
+
error = parse_resolver_failure(
|
|
144
|
+
"error: Failed to fetch: `https://pypi.org/simple/x/`\n Caused by: dns error\n"
|
|
145
|
+
)
|
|
146
|
+
assert error.code.code == "DL_ENV_PROVIDER_ERROR"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|