diff --git a/README.md b/README.md index 3a40665..f123840 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Custom [dev container Features](https://containers.dev/implementors/features/) f | [Microsoft Security DevOps CLI](src/microsoft-security-devops-cli) | `guardian` CLI for running security analysis tools without .NET | | [Copilot CLI Persistence](src/copilot-persistence) | Persist GitHub Copilot CLI settings & chat history across rebuilds | | [Atlassian CLI for Jira & Confluence](src/atlassian-jira-confluence-cli) | `acli` for working with Jira and Confluence Cloud | -| [Package Source Overrides](src/package-source-overrides) | Point npm, pnpm, pip, and NuGet at internal override feeds | +| [Package Source Overrides](src/package-source-overrides) | Point npm, pnpm, Yarn, Corepack, pip, uv, and NuGet at internal override feeds | | [GitLab CI Local](src/gitlab-ci-local) | Run GitLab CI/CD pipelines locally with `gitlab-ci-local` | | [Prompty Dumpty](src/prompty-dumpty) | `dumpty` CLI for managing prompty files | @@ -158,7 +158,7 @@ acli jira project list ### Package Source Overrides -Points npm, pnpm, pip, and NuGet at internal **override package sources** (a pull-through proxy such as Artifactory, Nexus, or Azure Artifacts) instead of public feeds. It writes system- and user-scoped configuration as early as possible in the container lifecycle so that other Features also resolve packages through the proxy during their own installation. +Points npm, pnpm, Yarn, Corepack, pip, uv, and NuGet at internal **override package sources** (a pull-through proxy such as Artifactory, Nexus, or Azure Artifacts) instead of public feeds. It writes system- and user-scoped configuration so that Features installed after it, and your projects, resolve packages through the proxy. **Usage:** @@ -180,16 +180,18 @@ Points npm, pnpm, pip, and NuGet at internal **override package sources** (a pul ``` **Options:** -- `npmRegistry` - registry URL for npm and pnpm (default: "") -- `pipIndexUrl` - pip / PyPI index URL that replaces the default index (default: "") +- `npmRegistry` - registry URL for npm, pnpm, Yarn (Classic and Berry), and Corepack (default: "") +- `pipIndexUrl` - PyPI index URL for pip and uv that replaces the default index (default: "") +- `uvPythonInstallMirror` - mirror of the python-build-standalone releases for uv-managed Python downloads (default: "") - `nugetSource` - NuGet v3 source URL that replaces nuget.org (default: "") - `nugetSourceName` - key/name for the configured NuGet source (default: "override") - `scope` - where config is written: `system`, `user`, or `both` (default: "both") - `strictSsl` - set to `false` for self-signed / HTTP internal proxies (default: `true`) +- `bashEnvHook` - opt-in `BASH_ENV` hook so later Features' install scripts see env-var-only overrides such as `COREPACK_NPM_REGISTRY` (default: `false`) -**Lifecycle & Ordering:** +**Install Order:** -The feature declares no `installsAfter` dependencies so it installs as early as possible, and it writes root-readable config (`/etc/pip.conf`, `/etc/npmrc`, `/root/.npmrc`, root `NuGet.Config`) that other Features' build-time (root) package installs pick up automatically. Because a Feature can't force itself ahead of arbitrary third-party Features, list it first in `overrideFeatureInstallOrder` to guarantee it runs before anything that downloads packages. Providing no URLs makes it a safe no-op. +Features install in rounds, and within a round the spec sorts them alphabetically by registry path, so `ghcr.io/devcontainers/*` Features install *before* this one unless you change the order. Per repository, list it first in `overrideFeatureInstallOrder` (as above). Machine-wide via VS Code's `dev.containers.defaultFeatures` there is no global override order; add `ghcr.io/devcontainers/features/common-utils:2` to the defaults too, because the official Features declare `installsAfter: common-utils` and therefore move after this Feature. See the [Feature docs](src/package-source-overrides) for details and caveats. Providing no URLs makes it a safe no-op. ### GitLab CI Local diff --git a/src/package-source-overrides/NOTES.md b/src/package-source-overrides/NOTES.md index 1cdedce..c8d37bc 100644 --- a/src/package-source-overrides/NOTES.md +++ b/src/package-source-overrides/NOTES.md @@ -5,10 +5,10 @@ In locked-down environments you often cannot reach public package feeds internal pull-through proxy / mirror such as **JFrog Artifactory**, **Sonatype Nexus**, or **Azure Artifacts**. -This Feature points **npm**, **pnpm**, **pip**, and **NuGet** at those override -sources by writing their configuration files as early as possible in the -container lifecycle, so that **other Features installed afterward also resolve -their packages through the proxy** during their own install scripts. +This Feature points **npm**, **pnpm**, **Yarn**, **Corepack**, **pip**, **uv**, +and **NuGet** at those override sources by writing their configuration, so that +**Features installed after it, and your projects, resolve packages through the +proxy**. It only writes configuration — no network calls, no `apt`, and no assumption that the package managers are already installed. Providing no URLs makes the Feature a @@ -16,36 +16,54 @@ safe no-op. ## How It Works -For each ecosystem you give a URL, the Feature writes standard config files at -both **system** and **per-user** scope (`scope` option, default `both`). Per-user -config is written for **root** (so package installs run by other Features at build -time use the proxy) and for the **remote user** (so it also applies at runtime). +For each URL you provide, the Feature writes the standard config files for every +tool that consumes that feed. Per-user config is written for **root** (package +installs run by other Features at build time run as root) and for the **remote +user** (runtime). -| Ecosystem | System scope | User scope (root + remote user) | -|-----------|--------------|---------------------------------| -| npm / pnpm | `/etc/npmrc` (read via the `NPM_CONFIG_GLOBALCONFIG` env var this Feature sets) | `~/.npmrc` | -| pip / PyPI | `/etc/pip.conf` | `~/.config/pip/pip.conf` | -| NuGet | `/etc/opt/NuGet/Config/NuGet.Config` | `~/.nuget/NuGet/NuGet.Config` | +| Option | Tool | System scope | User scope (root + remote user) | +|--------|------|--------------|---------------------------------| +| `npmRegistry` | npm | `/etc/npmrc` (via the `NPM_CONFIG_GLOBALCONFIG` env var this Feature sets) | `~/.npmrc` | +| `npmRegistry` | pnpm, Yarn Classic | — (no system-wide config) | `~/.npmrc` (always written) | +| `npmRegistry` | Yarn Berry (v2+) | — (no system-wide config) | `~/.yarnrc.yml` `npmRegistryServer` (always written) | +| `npmRegistry` | Corepack | `COREPACK_NPM_REGISTRY` env var (see below) | — | +| `pipIndexUrl` | pip, pipx | `/etc/pip.conf` | `~/.config/pip/pip.conf` | +| `pipIndexUrl` | uv | `/etc/uv/uv.toml` | `~/.config/uv/uv.toml` | +| `uvPythonInstallMirror` | uv-managed Python downloads | `/etc/uv/uv.toml` `python-install-mirror` | `~/.config/uv/uv.toml` | +| `nugetSource` | NuGet / dotnet | `/etc/opt/NuGet/Config/NuGet.Config` | `~/.nuget/NuGet/NuGet.Config` | -- **npm / pnpm** share the same `npmRegistry` (pnpm reads `.npmrc`). The - `registry=` key replaces the default registry. -- **pip** sets `index-url`, which replaces the default PyPI index. -- **NuGet** writes `` before the override source, so `nuget.org` is - removed and only your source remains. +- `scope` (`system`, `user`, `both`; default `both`) selects the columns above. + pnpm and Yarn Berry have no system-wide config location, so their per-user + files are written for every scope. +- **npm / pnpm / Yarn Classic** — `registry=` replaces the default registry. +- **Yarn Berry** ignores `.npmrc`; it reads `npmRegistryServer` from `~/.yarnrc.yml`. +- **pip** — `index-url` replaces the default PyPI index. +- **uv** ignores `pip.conf` and `PIP_INDEX_URL`; it uses an `[[index]]` with + `default = true`. uv rejects unknown keys in `uv.toml`, so the file is kept + minimal. +- **uv-managed Python** (`uv python install`, automatic downloads) normally comes + from GitHub. Set `uvPythonInstallMirror` to a mirror of the + `python-build-standalone` release downloads. +- **NuGet** — `` removes inherited sources, and `nuget.org` is also + listed under `` so it stays off even when a + higher-priority config (such as the .NET SDK's default user config) re-adds it. -The `.npmrc` files are updated inside a managed marker block so any unrelated npm -settings you already have are preserved; `pip.conf` and `NuGet.Config` are fully -managed by this Feature. +`.npmrc` and `.yarnrc.yml` are updated inside a managed marker block so unrelated +settings are preserved (an existing `~/.yarnrc.yml` that already sets the +registry is left unchanged). `pip.conf`, `uv.toml`, and `NuGet.Config` are fully +managed by this Feature. Project-level configuration (a repository's own +`.npmrc`, `.yarnrc.yml`, `uv.toml`, `NuGet.Config`, …) still takes precedence. -## Getting It First in the Lifecycle +## Install order -A Feature cannot *force* itself to install before arbitrary third-party Features. -To maximize how early this runs, it declares **no `installsAfter` dependencies**, -so it sorts as early as the resolver allows, and it writes config to root-readable -default locations that later Features' package managers pick up automatically. +Dev container Features install in rounds; within a round the spec sorts Features +**alphabetically by their full registry path**. Features from +`ghcr.io/devcontainers/…` therefore install *before* +`ghcr.io/rosstaco/…`, and any of them that download packages (for example the +Python Feature installing its default tools with pip) would hit the public feeds +first. Make this Feature install first in one of these ways: -For a **hard guarantee** that it runs before Features that download packages, list -it first in `overrideFeatureInstallOrder`: +### Per repository: `overrideFeatureInstallOrder` ```jsonc { @@ -58,13 +76,73 @@ it first in `overrideFeatureInstallOrder`: "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/devcontainers/features/python:1": {} }, - // Ensure the override sources are configured before anything installs packages + // Configure the override sources before anything installs packages "overrideFeatureInstallOrder": [ "ghcr.io/rosstaco/devcontainer-features/package-source-overrides" ] } ``` +### Machine-wide: VS Code `dev.containers.defaultFeatures` + +Adding this Feature to the VS Code user setting `dev.containers.defaultFeatures` +applies it to every dev container you open locally, but there is no global +equivalent of `overrideFeatureInstallOrder`, so on its own it installs *after* +the official Features. Add `common-utils` to the default Features as well: the +official Features (node, python, dotnet, java, go, rust, ruby, conda, powershell, +docker-in-docker, azure-cli, github-cli, …) declare +`installsAfter: common-utils`, which moves them to a later round — after this +Feature, which deliberately declares no `installsAfter`. + +```jsonc +// VS Code user settings.json +"dev.containers.defaultFeatures": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": false, + "installOhMyZsh": false, + "upgradePackages": false, + "username": "none" + }, + "ghcr.io/rosstaco/devcontainer-features/package-source-overrides:1": { + "npmRegistry": "https://artifactory.example.com/artifactory/api/npm/npm-remote/", + "pipIndexUrl": "https://artifactory.example.com/artifactory/api/pypi/pypi-remote/simple", + "nugetSource": "https://artifactory.example.com/artifactory/api/nuget/v3/index.json" + } +} +``` + +Caveats: `common-utils` runs `apt-get`, so your apt sources must be reachable. +Features that don't declare `installsAfter: common-utils` can still sort ahead +of this one. `defaultFeatures` only applies to the local VS Code Dev Containers +extension — not to Codespaces, the `devcontainer` CLI in CI, or other tools. + +### Most robust: your base image + +Configuration baked into your organization's base image exists before any +Feature runs. + +If `npm`, `pip`, `uv`, `dotnet`, … already exist when this Feature runs, it prints +a warning: an earlier Feature (or the base image) installed them and may already +have fetched packages from public feeds. + +## Environment-variable-only tools + +**Corepack** (used via `corepack enable` and the `packageManager` field in +`package.json`) ignores `.npmrc` and can only be redirected with the +`COREPACK_NPM_REGISTRY` environment variable. Because `containerEnv` cannot +contain option values, the Feature writes such variables to +`/etc/package-source-overrides/env.sh`, which is sourced: + +- **at runtime** by login shells via `/etc/profile.d/package-source-overrides.sh` + (this includes VS Code's environment probe, terminals, and lifecycle commands); +- **at build time**, only when `bashEnvHook` is `true`: `BASH_ENV` makes every + non-interactive bash process (such as the install scripts of Features that run + after this one) source it. `sh`/`dash` scripts don't honor `BASH_ENV`. + +`BASH_ENV` is always set to `/etc/package-source-overrides/bash_env` (static +`containerEnv`); the file only exists when `bashEnvHook` is enabled or when the +base image already had a `BASH_ENV`, which is chained so it keeps working. + ## Notes - **No authentication** is configured — the override sources are expected to allow @@ -73,6 +151,10 @@ it first in `overrideFeatureInstallOrder`: - **Replace policy:** public feeds are replaced, not augmented. Only the override sources are consulted. - **Self-signed / HTTP proxies:** set `strictSsl` to `false` to emit - `strict-ssl=false` (npm), `trusted-host` (pip), and `allowInsecureConnections` - (NuGet). Leave it `true` whenever your proxy presents a trusted certificate. -- Leave an ecosystem's URL empty to skip configuring it. + `strict-ssl=false` (npm), `enableStrictSsl: false` (Yarn Berry), `trusted-host` + (pip), `allow-insecure-host` (uv), and `allowInsecureConnections` (NuGet). An + `http://` npm registry is also added to Yarn Berry's `unsafeHttpWhitelist`. + Leave `strictSsl` `true` whenever your proxy presents a trusted certificate. +- URL options must be `http(s)://` URLs without whitespace, quotes, backslashes, + backticks, `$`, `<`, or `>`. +- Leave a URL empty to skip configuring the tools that use it. diff --git a/src/package-source-overrides/README.md b/src/package-source-overrides/README.md index 21e4264..6291a5f 100644 --- a/src/package-source-overrides/README.md +++ b/src/package-source-overrides/README.md @@ -1,7 +1,7 @@ # Package Source Overrides (package-source-overrides) -Configure npm, pnpm, pip, and NuGet to use internal override package sources instead of public feeds, early in the container lifecycle so other Features resolve packages through them too +Configure npm, pnpm, Yarn, Corepack, pip, uv, and NuGet to use internal override package sources instead of public feeds, so other Features and your projects resolve packages through them ## Example Usage @@ -15,12 +15,14 @@ Configure npm, pnpm, pip, and NuGet to use internal override package sources ins | Options Id | Description | Type | Default Value | |-----|-----|-----|-----| -| npmRegistry | Registry URL for npm and pnpm (they share .npmrc). Leave empty to skip npm/pnpm. Example: https://artifactory.example.com/artifactory/api/npm/npm-remote/ | string | - | -| pipIndexUrl | pip / PyPI index URL that replaces the default index. Leave empty to skip pip. Example: https://artifactory.example.com/artifactory/api/pypi/pypi-remote/simple | string | - | +| npmRegistry | Registry URL for npm, pnpm, Yarn (Classic and Berry), and Corepack. Leave empty to skip. Example: https://artifactory.example.com/artifactory/api/npm/npm-remote/ | string | - | +| pipIndexUrl | PyPI index URL for pip and uv that replaces the default index. Leave empty to skip. Example: https://artifactory.example.com/artifactory/api/pypi/pypi-remote/simple | string | - | +| uvPythonInstallMirror | Mirror of the python-build-standalone GitHub releases used for uv-managed Python downloads (uv 'python-install-mirror'). Leave empty to skip. Example: https://artifactory.example.com/artifactory/github-remote/astral-sh/python-build-standalone/releases/download | string | - | | nugetSource | NuGet v3 package source URL that replaces nuget.org. Leave empty to skip NuGet. Example: https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json | string | - | | nugetSourceName | Friendly key/name for the configured NuGet package source | string | override | -| scope | Where configuration is written: 'system' (system-wide files), 'user' (root + remote user home), or 'both' | string | both | -| strictSsl | When false, allow self-signed/insecure internal proxies (npm strict-ssl=false, pip trusted-host, NuGet allowInsecureConnections) | boolean | true | +| scope | Where configuration is written: 'system' (system-wide files), 'user' (root + remote user home), or 'both'. pnpm and Yarn Berry have no system-wide config, so their per-user files are always written | string | both | +| strictSsl | When false, allow self-signed/insecure internal proxies (npm strict-ssl=false, Yarn enableStrictSsl=false, pip trusted-host, uv allow-insecure-host, NuGet allowInsecureConnections) | boolean | true | +| bashEnvHook | Opt-in: export environment-variable-only overrides (e.g. COREPACK_NPM_REGISTRY) to every non-interactive bash process via BASH_ENV, so install scripts of Features that run after this one see them at build time | boolean | false | ## About @@ -29,10 +31,10 @@ In locked-down environments you often cannot reach public package feeds internal pull-through proxy / mirror such as **JFrog Artifactory**, **Sonatype Nexus**, or **Azure Artifacts**. -This Feature points **npm**, **pnpm**, **pip**, and **NuGet** at those override -sources by writing their configuration files as early as possible in the -container lifecycle, so that **other Features installed afterward also resolve -their packages through the proxy** during their own install scripts. +This Feature points **npm**, **pnpm**, **Yarn**, **Corepack**, **pip**, **uv**, +and **NuGet** at those override sources by writing their configuration, so that +**Features installed after it, and your projects, resolve packages through the +proxy**. It only writes configuration — no network calls, no `apt`, and no assumption that the package managers are already installed. Providing no URLs makes the Feature a @@ -40,36 +42,54 @@ safe no-op. ## How It Works -For each ecosystem you give a URL, the Feature writes standard config files at -both **system** and **per-user** scope (`scope` option, default `both`). Per-user -config is written for **root** (so package installs run by other Features at build -time use the proxy) and for the **remote user** (so it also applies at runtime). - -| Ecosystem | System scope | User scope (root + remote user) | -|-----------|--------------|---------------------------------| -| npm / pnpm | `/etc/npmrc` (read via the `NPM_CONFIG_GLOBALCONFIG` env var this Feature sets) | `~/.npmrc` | -| pip / PyPI | `/etc/pip.conf` | `~/.config/pip/pip.conf` | -| NuGet | `/etc/opt/NuGet/Config/NuGet.Config` | `~/.nuget/NuGet/NuGet.Config` | - -- **npm / pnpm** share the same `npmRegistry` (pnpm reads `.npmrc`). The - `registry=` key replaces the default registry. -- **pip** sets `index-url`, which replaces the default PyPI index. -- **NuGet** writes `` before the override source, so `nuget.org` is - removed and only your source remains. - -The `.npmrc` files are updated inside a managed marker block so any unrelated npm -settings you already have are preserved; `pip.conf` and `NuGet.Config` are fully -managed by this Feature. - -## Getting It First in the Lifecycle - -A Feature cannot *force* itself to install before arbitrary third-party Features. -To maximize how early this runs, it declares **no `installsAfter` dependencies**, -so it sorts as early as the resolver allows, and it writes config to root-readable -default locations that later Features' package managers pick up automatically. - -For a **hard guarantee** that it runs before Features that download packages, list -it first in `overrideFeatureInstallOrder`: +For each URL you provide, the Feature writes the standard config files for every +tool that consumes that feed. Per-user config is written for **root** (package +installs run by other Features at build time run as root) and for the **remote +user** (runtime). + +| Option | Tool | System scope | User scope (root + remote user) | +|--------|------|--------------|---------------------------------| +| `npmRegistry` | npm | `/etc/npmrc` (via the `NPM_CONFIG_GLOBALCONFIG` env var this Feature sets) | `~/.npmrc` | +| `npmRegistry` | pnpm, Yarn Classic | — (no system-wide config) | `~/.npmrc` (always written) | +| `npmRegistry` | Yarn Berry (v2+) | — (no system-wide config) | `~/.yarnrc.yml` `npmRegistryServer` (always written) | +| `npmRegistry` | Corepack | `COREPACK_NPM_REGISTRY` env var (see below) | — | +| `pipIndexUrl` | pip, pipx | `/etc/pip.conf` | `~/.config/pip/pip.conf` | +| `pipIndexUrl` | uv | `/etc/uv/uv.toml` | `~/.config/uv/uv.toml` | +| `uvPythonInstallMirror` | uv-managed Python downloads | `/etc/uv/uv.toml` `python-install-mirror` | `~/.config/uv/uv.toml` | +| `nugetSource` | NuGet / dotnet | `/etc/opt/NuGet/Config/NuGet.Config` | `~/.nuget/NuGet/NuGet.Config` | + +- `scope` (`system`, `user`, `both`; default `both`) selects the columns above. + pnpm and Yarn Berry have no system-wide config location, so their per-user + files are written for every scope. +- **npm / pnpm / Yarn Classic** — `registry=` replaces the default registry. +- **Yarn Berry** ignores `.npmrc`; it reads `npmRegistryServer` from `~/.yarnrc.yml`. +- **pip** — `index-url` replaces the default PyPI index. +- **uv** ignores `pip.conf` and `PIP_INDEX_URL`; it uses an `[[index]]` with + `default = true`. uv rejects unknown keys in `uv.toml`, so the file is kept + minimal. +- **uv-managed Python** (`uv python install`, automatic downloads) normally comes + from GitHub. Set `uvPythonInstallMirror` to a mirror of the + `python-build-standalone` release downloads. +- **NuGet** — `` removes inherited sources, and `nuget.org` is also + listed under `` so it stays off even when a + higher-priority config (such as the .NET SDK's default user config) re-adds it. + +`.npmrc` and `.yarnrc.yml` are updated inside a managed marker block so unrelated +settings are preserved (an existing `~/.yarnrc.yml` that already sets the +registry is left unchanged). `pip.conf`, `uv.toml`, and `NuGet.Config` are fully +managed by this Feature. Project-level configuration (a repository's own +`.npmrc`, `.yarnrc.yml`, `uv.toml`, `NuGet.Config`, …) still takes precedence. + +## Install order + +Dev container Features install in rounds; within a round the spec sorts Features +**alphabetically by their full registry path**. Features from +`ghcr.io/devcontainers/…` therefore install *before* +`ghcr.io/rosstaco/…`, and any of them that download packages (for example the +Python Feature installing its default tools with pip) would hit the public feeds +first. Make this Feature install first in one of these ways: + +### Per repository: `overrideFeatureInstallOrder` ```jsonc { @@ -82,13 +102,73 @@ it first in `overrideFeatureInstallOrder`: "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/devcontainers/features/python:1": {} }, - // Ensure the override sources are configured before anything installs packages + // Configure the override sources before anything installs packages "overrideFeatureInstallOrder": [ "ghcr.io/rosstaco/devcontainer-features/package-source-overrides" ] } ``` +### Machine-wide: VS Code `dev.containers.defaultFeatures` + +Adding this Feature to the VS Code user setting `dev.containers.defaultFeatures` +applies it to every dev container you open locally, but there is no global +equivalent of `overrideFeatureInstallOrder`, so on its own it installs *after* +the official Features. Add `common-utils` to the default Features as well: the +official Features (node, python, dotnet, java, go, rust, ruby, conda, powershell, +docker-in-docker, azure-cli, github-cli, …) declare +`installsAfter: common-utils`, which moves them to a later round — after this +Feature, which deliberately declares no `installsAfter`. + +```jsonc +// VS Code user settings.json +"dev.containers.defaultFeatures": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": false, + "installOhMyZsh": false, + "upgradePackages": false, + "username": "none" + }, + "ghcr.io/rosstaco/devcontainer-features/package-source-overrides:1": { + "npmRegistry": "https://artifactory.example.com/artifactory/api/npm/npm-remote/", + "pipIndexUrl": "https://artifactory.example.com/artifactory/api/pypi/pypi-remote/simple", + "nugetSource": "https://artifactory.example.com/artifactory/api/nuget/v3/index.json" + } +} +``` + +Caveats: `common-utils` runs `apt-get`, so your apt sources must be reachable. +Features that don't declare `installsAfter: common-utils` can still sort ahead +of this one. `defaultFeatures` only applies to the local VS Code Dev Containers +extension — not to Codespaces, the `devcontainer` CLI in CI, or other tools. + +### Most robust: your base image + +Configuration baked into your organization's base image exists before any +Feature runs. + +If `npm`, `pip`, `uv`, `dotnet`, … already exist when this Feature runs, it prints +a warning: an earlier Feature (or the base image) installed them and may already +have fetched packages from public feeds. + +## Environment-variable-only tools + +**Corepack** (used via `corepack enable` and the `packageManager` field in +`package.json`) ignores `.npmrc` and can only be redirected with the +`COREPACK_NPM_REGISTRY` environment variable. Because `containerEnv` cannot +contain option values, the Feature writes such variables to +`/etc/package-source-overrides/env.sh`, which is sourced: + +- **at runtime** by login shells via `/etc/profile.d/package-source-overrides.sh` + (this includes VS Code's environment probe, terminals, and lifecycle commands); +- **at build time**, only when `bashEnvHook` is `true`: `BASH_ENV` makes every + non-interactive bash process (such as the install scripts of Features that run + after this one) source it. `sh`/`dash` scripts don't honor `BASH_ENV`. + +`BASH_ENV` is always set to `/etc/package-source-overrides/bash_env` (static +`containerEnv`); the file only exists when `bashEnvHook` is enabled or when the +base image already had a `BASH_ENV`, which is chained so it keeps working. + ## Notes - **No authentication** is configured — the override sources are expected to allow @@ -97,9 +177,39 @@ it first in `overrideFeatureInstallOrder`: - **Replace policy:** public feeds are replaced, not augmented. Only the override sources are consulted. - **Self-signed / HTTP proxies:** set `strictSsl` to `false` to emit - `strict-ssl=false` (npm), `trusted-host` (pip), and `allowInsecureConnections` - (NuGet). Leave it `true` whenever your proxy presents a trusted certificate. -- Leave an ecosystem's URL empty to skip configuring it. + `strict-ssl=false` (npm), `enableStrictSsl: false` (Yarn Berry), `trusted-host` + (pip), `allow-insecure-host` (uv), and `allowInsecureConnections` (NuGet). An + `http://` npm registry is also added to Yarn Berry's `unsafeHttpWhitelist`. + Leave `strictSsl` `true` whenever your proxy presents a trusted certificate. +- URL options must be `http(s)://` URLs without whitespace, quotes, backslashes, + backticks, ` +# Package Source Overrides (package-source-overrides) + +Configure npm, pnpm, Yarn, Corepack, pip, uv, and NuGet to use internal override package sources instead of public feeds, so other Features and your projects resolve packages through them + +## Example Usage + +```json +"features": { + "#{Registry}/#{Namespace}/package-source-overrides:#{Version}": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| npmRegistry | Registry URL for npm, pnpm, Yarn (Classic and Berry), and Corepack. Leave empty to skip. Example: https://artifactory.example.com/artifactory/api/npm/npm-remote/ | string | - | +| pipIndexUrl | PyPI index URL for pip and uv that replaces the default index. Leave empty to skip. Example: https://artifactory.example.com/artifactory/api/pypi/pypi-remote/simple | string | - | +| uvPythonInstallMirror | Mirror of the python-build-standalone GitHub releases used for uv-managed Python downloads (uv 'python-install-mirror'). Leave empty to skip. Example: https://artifactory.example.com/artifactory/github-remote/astral-sh/python-build-standalone/releases/download | string | - | +| nugetSource | NuGet v3 package source URL that replaces nuget.org. Leave empty to skip NuGet. Example: https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json | string | - | +| nugetSourceName | Friendly key/name for the configured NuGet package source | string | override | +| scope | Where configuration is written: 'system' (system-wide files), 'user' (root + remote user home), or 'both'. pnpm and Yarn Berry have no system-wide config, so their per-user files are always written | string | both | +| strictSsl | When false, allow self-signed/insecure internal proxies (npm strict-ssl=false, Yarn enableStrictSsl=false, pip trusted-host, uv allow-insecure-host, NuGet allowInsecureConnections) | boolean | true | +| bashEnvHook | Opt-in: export environment-variable-only overrides (e.g. COREPACK_NPM_REGISTRY) to every non-interactive bash process via BASH_ENV, so install scripts of Features that run after this one see them at build time | boolean | false | +#{Customizations} +, `<`, or `>`. +- Leave a URL empty to skip configuring the tools that use it. --- diff --git a/src/package-source-overrides/devcontainer-feature.json b/src/package-source-overrides/devcontainer-feature.json index 5712fc8..afe2603 100644 --- a/src/package-source-overrides/devcontainer-feature.json +++ b/src/package-source-overrides/devcontainer-feature.json @@ -1,19 +1,24 @@ { "id": "package-source-overrides", - "version": "1.0.0", + "version": "1.1.0", "name": "Package Source Overrides", - "description": "Configure npm, pnpm, pip, and NuGet to use internal override package sources instead of public feeds, early in the container lifecycle so other Features resolve packages through them too", + "description": "Configure npm, pnpm, Yarn, Corepack, pip, uv, and NuGet to use internal override package sources instead of public feeds, so other Features and your projects resolve packages through them", "documentationURL": "https://github.com/rosstaco/devcontainer-features/tree/main/src/package-source-overrides", "options": { "npmRegistry": { "type": "string", "default": "", - "description": "Registry URL for npm and pnpm (they share .npmrc). Leave empty to skip npm/pnpm. Example: https://artifactory.example.com/artifactory/api/npm/npm-remote/" + "description": "Registry URL for npm, pnpm, Yarn (Classic and Berry), and Corepack. Leave empty to skip. Example: https://artifactory.example.com/artifactory/api/npm/npm-remote/" }, "pipIndexUrl": { "type": "string", "default": "", - "description": "pip / PyPI index URL that replaces the default index. Leave empty to skip pip. Example: https://artifactory.example.com/artifactory/api/pypi/pypi-remote/simple" + "description": "PyPI index URL for pip and uv that replaces the default index. Leave empty to skip. Example: https://artifactory.example.com/artifactory/api/pypi/pypi-remote/simple" + }, + "uvPythonInstallMirror": { + "type": "string", + "default": "", + "description": "Mirror of the python-build-standalone GitHub releases used for uv-managed Python downloads (uv 'python-install-mirror'). Leave empty to skip. Example: https://artifactory.example.com/artifactory/github-remote/astral-sh/python-build-standalone/releases/download" }, "nugetSource": { "type": "string", @@ -33,23 +38,30 @@ "both" ], "default": "both", - "description": "Where configuration is written: 'system' (system-wide files), 'user' (root + remote user home), or 'both'" + "description": "Where configuration is written: 'system' (system-wide files), 'user' (root + remote user home), or 'both'. pnpm and Yarn Berry have no system-wide config, so their per-user files are always written" }, "strictSsl": { "type": "boolean", "default": true, - "description": "When false, allow self-signed/insecure internal proxies (npm strict-ssl=false, pip trusted-host, NuGet allowInsecureConnections)" + "description": "When false, allow self-signed/insecure internal proxies (npm strict-ssl=false, Yarn enableStrictSsl=false, pip trusted-host, uv allow-insecure-host, NuGet allowInsecureConnections)" + }, + "bashEnvHook": { + "type": "boolean", + "default": false, + "description": "Opt-in: export environment-variable-only overrides (e.g. COREPACK_NPM_REGISTRY) to every non-interactive bash process via BASH_ENV, so install scripts of Features that run after this one see them at build time" } }, "containerEnv": { - "NPM_CONFIG_GLOBALCONFIG": "/etc/npmrc" + "NPM_CONFIG_GLOBALCONFIG": "/etc/npmrc", + "PACKAGE_SOURCE_OVERRIDES_PREV_BASH_ENV": "${BASH_ENV}", + "BASH_ENV": "/etc/package-source-overrides/bash_env" }, "customizations": { "vscode": { "settings": { "github.copilot.chat.codeGeneration.instructions": [ { - "text": "This dev container is configured to resolve packages through internal override sources (npm, pnpm, pip, NuGet) instead of public feeds. Do not reconfigure package managers back to public registries such as registry.npmjs.org, pypi.org, or api.nuget.org." + "text": "This dev container is configured to resolve packages through internal override sources (npm, pnpm, Yarn, Corepack, pip, uv, NuGet) instead of public feeds. Do not reconfigure package managers back to public registries such as registry.npmjs.org, registry.yarnpkg.com, pypi.org, or api.nuget.org." } ] } diff --git a/src/package-source-overrides/install.sh b/src/package-source-overrides/install.sh index 8bddde0..f682100 100755 --- a/src/package-source-overrides/install.sh +++ b/src/package-source-overrides/install.sh @@ -3,18 +3,29 @@ set -e # package-source-overrides # ------------------------- -# Point npm/pnpm, pip, and NuGet at internal "override" package sources instead -# of public feeds. This Feature only writes configuration files (no network, no -# apt, no assumption that the package managers are installed yet) so it can run -# as early as possible and have other Features resolve packages through the same -# sources during their own install scripts. +# Point npm/pnpm/Yarn/Corepack, pip/uv, and NuGet at internal "override" package +# sources instead of public feeds. This Feature only writes configuration files +# (no network, no apt, no assumption that the package managers are installed +# yet) so it can run before other Features and have them resolve packages +# through the same sources during their own install scripts. NPM_REGISTRY="${NPMREGISTRY:-}" PIP_INDEX_URL="${PIPINDEXURL:-}" +PYTHON_MIRROR="${UVPYTHONINSTALLMIRROR:-}" NUGET_SOURCE="${NUGETSOURCE:-}" NUGET_SOURCE_NAME="${NUGETSOURCENAME:-override}" SCOPE="${SCOPE:-both}" STRICT_SSL="${STRICTSSL:-true}" +BASH_ENV_HOOK="${BASHENVHOOK:-false}" + +# Fixed paths; BASH_ENV_FILE is also referenced by containerEnv BASH_ENV. +STATE_DIR="/etc/package-source-overrides" +ENV_FILE="${STATE_DIR}/env.sh" +BASH_ENV_FILE="${STATE_DIR}/bash_env" +PROFILE_FILE="/etc/profile.d/package-source-overrides.sh" +# The BASH_ENV value from before this Feature's containerEnv replaced it. +PREV_BASH_ENV="${PACKAGE_SOURCE_OVERRIDES_PREV_BASH_ENV:-}" +DOC_URL="https://github.com/rosstaco/devcontainer-features/tree/main/src/package-source-overrides#install-order" RED='\033[0;31m' GREEN='\033[0;32m' @@ -60,6 +71,42 @@ fi BEGIN_MARK="# >>> package-source-overrides >>>" END_MARK="# <<< package-source-overrides <<<" +# --- Validation ------------------------------------------------------------- +# URLs end up in INI, TOML, YAML, XML and shell files, so reject characters that +# would need format-specific quoting. +validate_url() { + local name="$1" value="$2" + [ -z "${value}" ] && return 0 + case "${value}" in + http://?* | https://?*) ;; + *) err "Option '${name}' must be an http:// or https:// URL (got '${value}')."; exit 1 ;; + esac + case "${value}" in + *[[:space:]]* | *\"* | *\'* | *\\* | *\`* | *\$* | *\<* | *\>*) + err "Option '${name}' contains an unsupported character (whitespace, quote, backslash, backtick, \$, < or >)." + exit 1 + ;; + esac +} + +validate_url npmRegistry "${NPM_REGISTRY}" +validate_url pipIndexUrl "${PIP_INDEX_URL}" +validate_url uvPythonInstallMirror "${PYTHON_MIRROR}" +validate_url nugetSource "${NUGET_SOURCE}" + +if [ -n "${NUGET_SOURCE}" ]; then + case "${NUGET_SOURCE_NAME}" in + "" | *\"* | *\'* | *\<* | *\>* | *\&*) + err "Option 'nugetSourceName' must be non-empty and must not contain quotes, <, > or &." + exit 1 + ;; + esac + if [ "$(printf '%s' "${NUGET_SOURCE_NAME}" | tr '[:upper:]' '[:lower:]')" = "nuget.org" ]; then + err "Option 'nugetSourceName' must not be 'nuget.org' (that source is disabled to enforce the override)." + exit 1 + fi +fi + # --- Helpers ---------------------------------------------------------------- # make_dir @@ -130,31 +177,111 @@ write_block_file() { log "updated ${path}" } -# url_host : extract host[:port] from a URL (for pip trusted-host) +# per_user : run a writer for root +# (build-time installs run as root) and for the remote user (runtime). +per_user() { + local writer="$1" rel="$2" content="$3" + "${writer}" "/root/${rel}" "0:0" "${content}" + if [ -n "${USER_HOME}" ]; then + "${writer}" "${USER_HOME}/${rel}" "user" "${content}" + fi +} + +# url_host : extract host[:port] from a URL url_host() { printf '%s' "$1" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://##; s#/.*$##; s#^[^@]*@##' } -# --- npm / pnpm ------------------------------------------------------------- -# npm and pnpm both read `.npmrc`; `registry=` replaces the default registry. +# url_hostname : extract the host without any port +url_hostname() { + local host + host="$(url_host "$1")" + printf '%s' "${host%%:*}" +} + +# shell_quote : single-quote a value for a sourced shell file +shell_quote() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" +} + +# xml_escape : escape a value for an XML attribute +xml_escape() { + printf '%s' "$1" | sed -e 's/&/\&/g' -e 's//\>/g' -e 's/"/\"/g' +} + +# --- Late-install detection ------------------------------------------------- +# Package managers that already exist may have been installed (and used against +# public feeds) by Features that ran before this one. +PREINSTALLED="" +detect_preinstalled() { + local candidates="" tool + if [ -n "${NPM_REGISTRY}" ]; then + candidates="${candidates} npm pnpm yarn corepack" + fi + if [ -n "${PIP_INDEX_URL}" ]; then + candidates="${candidates} pip pip3 pipx uv" + elif [ -n "${PYTHON_MIRROR}" ]; then + candidates="${candidates} uv" + fi + if [ -n "${NUGET_SOURCE}" ]; then + candidates="${candidates} dotnet nuget" + fi + for tool in ${candidates}; do + if command -v "${tool}" >/dev/null 2>&1; then + PREINSTALLED="${PREINSTALLED:+${PREINSTALLED}, }${tool}" + fi + done +} + +# --- npm / pnpm / Yarn Classic ---------------------------------------------- +# All three read `.npmrc`; `registry=` replaces the default registry. npm also +# reads /etc/npmrc via containerEnv NPM_CONFIG_GLOBALCONFIG, but pnpm ignores +# that, so the per-user files are written regardless of scope. configure_npm() { [ -z "${NPM_REGISTRY}" ] && return 0 - log "Configuring npm/pnpm registry -> ${NPM_REGISTRY}" + log "Configuring npm/pnpm/Yarn Classic registry -> ${NPM_REGISTRY}" local content="registry=${NPM_REGISTRY}" if [ "${INSECURE}" = "true" ]; then content="${content} strict-ssl=false" fi - # System file is picked up via containerEnv NPM_CONFIG_GLOBALCONFIG=/etc/npmrc if [ "${DO_SYSTEM}" = "true" ]; then write_block_file "/etc/npmrc" "" "${content}" fi - if [ "${DO_USER}" = "true" ]; then - write_block_file "/root/.npmrc" "0:0" "${content}" - if [ -n "${USER_HOME}" ]; then - write_block_file "${USER_HOME}/.npmrc" "user" "${content}" - fi + per_user write_block_file ".npmrc" "${content}" +} + +# --- Yarn Berry (v2+) ------------------------------------------------------- +# Yarn Berry ignores .npmrc and has no system-wide config; it reads ~/.yarnrc.yml. +configure_yarn() { + [ -z "${NPM_REGISTRY}" ] && return 0 + log "Configuring Yarn Berry npmRegistryServer -> ${NPM_REGISTRY%/}" + local content="npmRegistryServer: \"${NPM_REGISTRY%/}\"" + if [ "${INSECURE}" = "true" ]; then + content="${content} +enableStrictSsl: false" + fi + case "${NPM_REGISTRY}" in + http://*) + content="${content} +unsafeHttpWhitelist: + - \"$(url_hostname "${NPM_REGISTRY}")\"" + ;; + esac + per_user write_yarnrc ".yarnrc.yml" "${content}" +} + +# write_yarnrc : managed block in ~/.yarnrc.yml that +# never duplicates keys the file already sets outside our block. Invoked via per_user. +# shellcheck disable=SC2317 +write_yarnrc() { + local path="$1" owner="$2" content="$3" + remove_block "${path}" + if [ -f "${path}" ] && grep -qE '^(npmRegistryServer|enableStrictSsl|unsafeHttpWhitelist):' "${path}"; then + warn "${path} already sets Yarn registry/SSL keys; leaving it unchanged" + return 0 fi + write_block_file "${path}" "${owner}" "${content}" } # --- pip / PyPI ------------------------------------------------------------- @@ -173,51 +300,153 @@ trusted-host = $(url_host "${PIP_INDEX_URL}")" write_full_file "/etc/pip.conf" "" "${content}" fi if [ "${DO_USER}" = "true" ]; then - write_full_file "/root/.config/pip/pip.conf" "0:0" "${content}" - if [ -n "${USER_HOME}" ]; then - write_full_file "${USER_HOME}/.config/pip/pip.conf" "user" "${content}" - fi + per_user write_full_file ".config/pip/pip.conf" "${content}" + fi +} + +# --- uv --------------------------------------------------------------------- +# uv ignores pip.conf and PIP_INDEX_URL; it reads /etc/uv/uv.toml (system) and +# ~/.config/uv/uv.toml (user). uv rejects unknown keys, so keep this minimal. +configure_uv() { + [ -z "${PIP_INDEX_URL}${PYTHON_MIRROR}" ] && return 0 + local content="# Managed by the package-source-overrides dev container Feature" + # Top-level keys must come before the [[index]] table. + if [ -n "${PYTHON_MIRROR}" ]; then + log "Configuring uv python-install-mirror -> ${PYTHON_MIRROR}" + content="${content} +python-install-mirror = \"${PYTHON_MIRROR}\"" + fi + if [ "${INSECURE}" = "true" ]; then + content="${content} +allow-insecure-host = [$(uv_insecure_hosts)]" + fi + if [ -n "${PIP_INDEX_URL}" ]; then + log "Configuring uv default index -> ${PIP_INDEX_URL}" + content="${content} + +[[index]] +url = \"${PIP_INDEX_URL}\" +default = true" + fi + if [ "${DO_SYSTEM}" = "true" ]; then + write_full_file "/etc/uv/uv.toml" "" "${content}" + fi + if [ "${DO_USER}" = "true" ]; then + per_user write_full_file ".config/uv/uv.toml" "${content}" fi } +# uv_insecure_hosts : de-duplicated, quoted TOML list items for allow-insecure-host +uv_insecure_hosts() { + local hosts="" item url + for url in "${PIP_INDEX_URL}" "${PYTHON_MIRROR}"; do + [ -z "${url}" ] && continue + item="\"$(url_host "${url}")\"" + case ", ${hosts}, " in + *", ${item}, "*) ;; + *) hosts="${hosts:+${hosts}, }${item}" ;; + esac + done + printf '%s' "${hosts}" +} + # --- NuGet / .NET ----------------------------------------------------------- -# drops nuget.org so only the override source remains (replace policy). +# drops inherited sources, and disabling nuget.org keeps it off even +# when a higher-priority NuGet.Config (e.g. the SDK's default user config) +# re-adds it. configure_nuget() { [ -z "${NUGET_SOURCE}" ] && return 0 log "Configuring NuGet source '${NUGET_SOURCE_NAME}' -> ${NUGET_SOURCE}" - local insecure_attr="" + local insecure_attr="" key value content if [ "${INSECURE}" = "true" ]; then insecure_attr=" allowInsecureConnections=\"true\"" fi - local content=" + key="$(xml_escape "${NUGET_SOURCE_NAME}")" + value="$(xml_escape "${NUGET_SOURCE}")" + content=" - + + + + " # Machine-wide NuGet config directory on Linux is /etc/opt/NuGet/Config if [ "${DO_SYSTEM}" = "true" ]; then write_full_file "/etc/opt/NuGet/Config/NuGet.Config" "" "${content}" fi if [ "${DO_USER}" = "true" ]; then - write_full_file "/root/.nuget/NuGet/NuGet.Config" "0:0" "${content}" - if [ -n "${USER_HOME}" ]; then - write_full_file "${USER_HOME}/.nuget/NuGet/NuGet.Config" "user" "${content}" - fi + per_user write_full_file ".nuget/NuGet/NuGet.Config" "${content}" + fi +} + +# --- Environment-variable-only overrides ------------------------------------ +# Some tools can only be redirected by an environment variable whose value is +# the (option-dependent) URL, which static containerEnv cannot express. They are +# written to ENV_FILE and exported for login shells via /etc/profile.d (and, +# opt-in, for non-interactive bash via BASH_ENV; see configure_bash_env). +configure_env_overrides() { + local exports=() + if [ -n "${NPM_REGISTRY}" ]; then + # Corepack ignores .npmrc and appends "/" to this base URL. + exports+=("export COREPACK_NPM_REGISTRY=$(shell_quote "${NPM_REGISTRY%/}")") + fi + [ "${#exports[@]}" -eq 0 ] && return 0 + write_full_file "${ENV_FILE}" "" "$(printf '%s\n' "# Managed by the package-source-overrides dev container Feature" "${exports[@]}")" + write_full_file "${PROFILE_FILE}" "" "# Managed by the package-source-overrides dev container Feature +if [ -r ${ENV_FILE} ]; then . ${ENV_FILE}; fi" +} + +# --- BASH_ENV hook ---------------------------------------------------------- +# containerEnv always points BASH_ENV at BASH_ENV_FILE (containerEnv is static). +# bash silently ignores a missing BASH_ENV file, so the file is only created when +# the hook is enabled or a pre-existing BASH_ENV has to keep working (chained). +configure_bash_env() { + local prev="" content + if [ -n "${PREV_BASH_ENV}" ] && [ "${PREV_BASH_ENV}" != "${BASH_ENV_FILE}" ]; then + prev="${PREV_BASH_ENV}" + fi + if [ "${BASH_ENV_HOOK}" != "true" ] && [ -z "${prev}" ]; then + rm -f "${BASH_ENV_FILE}" + return 0 + fi + content="# Managed by the package-source-overrides dev container Feature (sourced via BASH_ENV)" + if [ -n "${prev}" ]; then + content="${content} +# Chain to the BASH_ENV that was set before this Feature +if [ -r $(shell_quote "${prev}") ]; then . $(shell_quote "${prev}"); fi" fi + if [ "${BASH_ENV_HOOK}" = "true" ]; then + log "Enabling BASH_ENV hook -> ${BASH_ENV_FILE}" + content="${content} +if [ -r ${ENV_FILE} ]; then . ${ENV_FILE}; fi" + fi + write_full_file "${BASH_ENV_FILE}" "" "${content}" } # --- Main ------------------------------------------------------------------- +detect_preinstalled configure_npm +configure_yarn configure_pip +configure_uv configure_nuget +configure_env_overrides +configure_bash_env -if [ -z "${NPM_REGISTRY}${PIP_INDEX_URL}${NUGET_SOURCE}" ]; then - warn "No override sources provided (npmRegistry, pipIndexUrl, nugetSource all empty); nothing configured." +if [ -z "${NPM_REGISTRY}${PIP_INDEX_URL}${PYTHON_MIRROR}${NUGET_SOURCE}" ]; then + warn "No override sources provided (npmRegistry, pipIndexUrl, uvPythonInstallMirror, nugetSource all empty); nothing configured." else - log "Done. scope=${SCOPE} strictSsl=${STRICT_SSL} remoteUser=${REMOTE_USER}${USER_HOME:+ (${USER_HOME})}" + log "Done. scope=${SCOPE} strictSsl=${STRICT_SSL} bashEnvHook=${BASH_ENV_HOOK} remoteUser=${REMOTE_USER}${USER_HOME:+ (${USER_HOME})}" +fi + +if [ -n "${PREINSTALLED}" ]; then + warn "Already installed before this Feature ran: ${PREINSTALLED}." + warn "If an earlier Feature installed them, it may already have fetched packages from public feeds." + warn "To make this Feature install first, see: ${DOC_URL}" fi exit 0 diff --git a/test/package-source-overrides/all-insecure.sh b/test/package-source-overrides/all-insecure.sh index d4af63e..ad71b1b 100755 --- a/test/package-source-overrides/all-insecure.sh +++ b/test/package-source-overrides/all-insecure.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Scenario 'all-insecure': all three ecosystems with strictSsl=false. Asserts the -# insecure/self-signed settings are emitted for each package manager. +# Scenario 'all-insecure': every ecosystem with strictSsl=false and an http:// +# npm registry. Asserts the insecure/self-signed settings for each package manager. set -e @@ -13,4 +13,10 @@ check "pip trusted-host set to proxy host" grep -qF "trusted-host = proxy.intern check "NuGet allowInsecureConnections enabled" grep -qF 'allowInsecureConnections="true"' /etc/opt/NuGet/Config/NuGet.Config +check "Yarn strict SSL disabled" grep -qxF "enableStrictSsl: false" /home/vscode/.yarnrc.yml + +check "Yarn allows the http:// registry host (hostname only)" grep -qxF ' - "proxy.internal"' /home/vscode/.yarnrc.yml + +check "uv allow-insecure-host lists the proxy host once" grep -qxF 'allow-insecure-host = ["proxy.internal"]' /etc/uv/uv.toml + reportResults diff --git a/test/package-source-overrides/bash-env-hook.sh b/test/package-source-overrides/bash-env-hook.sh new file mode 100755 index 0000000..c7d673f --- /dev/null +++ b/test/package-source-overrides/bash-env-hook.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Scenario 'bash-env-hook': bashEnvHook=true exports env-only overrides to every +# non-interactive bash process (e.g. install scripts of later Features). + +set -e + +source dev-container-features-test-lib + +check "BASH_ENV hook file installed" test -f /etc/package-source-overrides/bash_env + +check "containerEnv BASH_ENV points at the hook" bash -c '[ "$BASH_ENV" = "/etc/package-source-overrides/bash_env" ]' + +check "clean non-interactive bash gets COREPACK_NPM_REGISTRY via BASH_ENV" bash -c 'out=$(env -i PATH="$PATH" BASH_ENV=/etc/package-source-overrides/bash_env bash -c "echo \$COREPACK_NPM_REGISTRY"); [ "$out" = "https://proxy.internal/npm" ]' + +check "without BASH_ENV the variable is not set" bash -c 'out=$(env -i PATH="$PATH" bash -c "echo \$COREPACK_NPM_REGISTRY"); [ -z "$out" ]' + +reportResults diff --git a/test/package-source-overrides/npm.sh b/test/package-source-overrides/npm.sh index 21b3a24..560d3b2 100755 --- a/test/package-source-overrides/npm.sh +++ b/test/package-source-overrides/npm.sh @@ -7,6 +7,7 @@ set -e source dev-container-features-test-lib REG="https://proxy.internal/artifactory/api/npm/npm-remote/" +REG_NOSLASH="${REG%/}" check "system /etc/npmrc has registry" grep -qF "registry=${REG}" /etc/npmrc @@ -20,4 +21,16 @@ check "managed marker present" grep -qF "package-source-overrides" /etc/npmrc check "strict-ssl not disabled when secure" bash -c '! grep -q "strict-ssl=false" /etc/npmrc' +check "remote user ~/.yarnrc.yml sets npmRegistryServer (no trailing slash)" grep -qxF "npmRegistryServer: \"${REG_NOSLASH}\"" /home/vscode/.yarnrc.yml + +check "root ~/.yarnrc.yml sets npmRegistryServer" sudo grep -qxF "npmRegistryServer: \"${REG_NOSLASH}\"" /root/.yarnrc.yml + +check "remote user ~/.yarnrc.yml owned by vscode" bash -c '[ "$(stat -c %U /home/vscode/.yarnrc.yml)" = "vscode" ]' + +check "Yarn strict SSL untouched when secure" bash -c '! grep -q "enableStrictSsl" /home/vscode/.yarnrc.yml' + +check "Corepack registry exported to login shells" bash -c "[ \"\$(env -i HOME=/home/vscode PATH=\"\$PATH\" bash -lc 'echo \$COREPACK_NPM_REGISTRY' | tail -1)\" = '${REG_NOSLASH}' ]" + +check "BASH_ENV hook not created when disabled" bash -c '[ ! -e /etc/package-source-overrides/bash_env ]' + reportResults diff --git a/test/package-source-overrides/nuget.sh b/test/package-source-overrides/nuget.sh index a8372a1..a734246 100755 --- a/test/package-source-overrides/nuget.sh +++ b/test/package-source-overrides/nuget.sh @@ -22,4 +22,10 @@ check "remote user NuGet.Config exists" test -f "${USERCFG}" check "remote user NuGet.Config owned by vscode" bash -c '[ "$(stat -c %U '"${USERCFG}"')" = "vscode" ]' +check "machine-wide config has disabledPackageSources" grep -qF "" "${MACHINE}" + +check "machine-wide config disables nuget.org" grep -qF '' "${MACHINE}" + +check "remote user config disables nuget.org" grep -qF '' "${USERCFG}" + reportResults diff --git a/test/package-source-overrides/pip.sh b/test/package-source-overrides/pip.sh index 3f9d7a6..352ad06 100755 --- a/test/package-source-overrides/pip.sh +++ b/test/package-source-overrides/pip.sh @@ -1,12 +1,13 @@ #!/bin/bash -# Scenario 'pip': only pipIndexUrl set (scope defaults to 'both', strictSsl true). +# Scenario 'pip': pipIndexUrl + uvPythonInstallMirror (scope 'both', strictSsl true). set -e source dev-container-features-test-lib URL="https://proxy.internal/artifactory/api/pypi/pypi-remote/simple" +MIRROR="https://proxy.internal/github/python-build-standalone" check "system /etc/pip.conf has index-url" grep -qF "index-url = ${URL}" /etc/pip.conf @@ -16,4 +17,18 @@ check "remote user pip.conf owned by vscode" bash -c '[ "$(stat -c %U /home/vsco check "no trusted-host when secure" bash -c '! grep -q "trusted-host" /etc/pip.conf' +check "system uv.toml sets the index url" grep -qxF "url = \"${URL}\"" /etc/uv/uv.toml + +check "system uv.toml marks the index as default" grep -qxF "default = true" /etc/uv/uv.toml + +check "uv python-install-mirror set" grep -qxF "python-install-mirror = \"${MIRROR}\"" /etc/uv/uv.toml + +check "python-install-mirror is a top-level key (before [[index]])" bash -c 'm=$(grep -n "^python-install-mirror" /etc/uv/uv.toml | cut -d: -f1); i=$(grep -n "^\[\[index\]\]" /etc/uv/uv.toml | cut -d: -f1); [ -n "$m" ] && [ -n "$i" ] && [ "$m" -lt "$i" ]' + +check "remote user uv.toml owned by vscode" bash -c '[ "$(stat -c %U /home/vscode/.config/uv/uv.toml)" = "vscode" ]' + +check "root uv.toml present" sudo test -f /root/.config/uv/uv.toml + +check "no allow-insecure-host when secure" bash -c '! grep -q "allow-insecure-host" /etc/uv/uv.toml' + reportResults diff --git a/test/package-source-overrides/scenarios.json b/test/package-source-overrides/scenarios.json index 9a38ff7..388f6c7 100644 --- a/test/package-source-overrides/scenarios.json +++ b/test/package-source-overrides/scenarios.json @@ -11,7 +11,8 @@ "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "package-source-overrides": { - "pipIndexUrl": "https://proxy.internal/artifactory/api/pypi/pypi-remote/simple" + "pipIndexUrl": "https://proxy.internal/artifactory/api/pypi/pypi-remote/simple", + "uvPythonInstallMirror": "https://proxy.internal/github/python-build-standalone" } } }, @@ -28,8 +29,9 @@ "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "package-source-overrides": { - "npmRegistry": "https://proxy.internal/npm/", + "npmRegistry": "http://proxy.internal:8081/npm/", "pipIndexUrl": "https://proxy.internal/pypi/simple", + "uvPythonInstallMirror": "https://proxy.internal/github/python-build-standalone", "nugetSource": "https://proxy.internal/nuget/v3/index.json", "strictSsl": false } @@ -44,5 +46,25 @@ "scope": "user" } } + }, + "scope-system": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "package-source-overrides": { + "npmRegistry": "https://proxy.internal/npm/", + "pipIndexUrl": "https://proxy.internal/pypi/simple", + "nugetSource": "https://proxy.internal/nuget/v3/index.json", + "scope": "system" + } + } + }, + "bash-env-hook": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "package-source-overrides": { + "npmRegistry": "https://proxy.internal/npm/", + "bashEnvHook": true + } + } } } diff --git a/test/package-source-overrides/scope-system.sh b/test/package-source-overrides/scope-system.sh new file mode 100755 index 0000000..eeeebfa --- /dev/null +++ b/test/package-source-overrides/scope-system.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Scenario 'scope-system': system-wide files only, except pnpm and Yarn Berry, +# which have no system-wide config and always get per-user files. + +set -e + +source dev-container-features-test-lib + +check "system /etc/npmrc present" test -f /etc/npmrc + +check "per-user ~/.npmrc still written for pnpm" test -f /home/vscode/.npmrc + +check "per-user ~/.yarnrc.yml still written for Yarn Berry" test -f /home/vscode/.yarnrc.yml + +check "system /etc/pip.conf present" test -f /etc/pip.conf + +check "no per-user pip.conf" bash -c '[ ! -e /home/vscode/.config/pip/pip.conf ]' + +check "system /etc/uv/uv.toml present" test -f /etc/uv/uv.toml + +check "no per-user uv.toml" bash -c '[ ! -e /home/vscode/.config/uv/uv.toml ]' + +check "machine-wide NuGet.Config disables nuget.org" grep -qF '' /etc/opt/NuGet/Config/NuGet.Config + +check "no per-user NuGet.Config" bash -c '[ ! -e /home/vscode/.nuget/NuGet/NuGet.Config ]' + +reportResults diff --git a/test/package-source-overrides/scope-user.sh b/test/package-source-overrides/scope-user.sh index 4aa1616..dfa373b 100755 --- a/test/package-source-overrides/scope-user.sh +++ b/test/package-source-overrides/scope-user.sh @@ -13,8 +13,14 @@ check "remote user ~/.npmrc present" test -f /home/vscode/.npmrc check "remote user pip.conf present" test -f /home/vscode/.config/pip/pip.conf +check "remote user uv.toml present" test -f /home/vscode/.config/uv/uv.toml + +check "remote user ~/.yarnrc.yml present" test -f /home/vscode/.yarnrc.yml + check "system /etc/npmrc absent" bash -c '[ ! -e /etc/npmrc ]' check "system /etc/pip.conf absent" bash -c '[ ! -e /etc/pip.conf ]' +check "system /etc/uv/uv.toml absent" bash -c '[ ! -e /etc/uv/uv.toml ]' + reportResults diff --git a/test/package-source-overrides/test.sh b/test/package-source-overrides/test.sh index fdc9abe..041e681 100755 --- a/test/package-source-overrides/test.sh +++ b/test/package-source-overrides/test.sh @@ -4,7 +4,7 @@ # is empty and the Feature is a no-op. We assert the Feature applied cleanly # (its static containerEnv is present) and that it wrote no override config. # -# These scripts are run as 'root' by default. +# These scripts run as the image's remote user (root on plain base images). set -e @@ -13,11 +13,23 @@ source dev-container-features-test-lib check "containerEnv NPM_CONFIG_GLOBALCONFIG applied" bash -c '[ "$NPM_CONFIG_GLOBALCONFIG" = "/etc/npmrc" ]' +check "containerEnv BASH_ENV points at the hook path" bash -c '[ "$BASH_ENV" = "/etc/package-source-overrides/bash_env" ]' + +check "BASH_ENV hook file not created by default" bash -c '[ ! -e /etc/package-source-overrides/bash_env ]' + +check "bash runs cleanly with the (absent) BASH_ENV file" bash -c '[ -z "$(bash -c true 2>&1)" ]' + check "no npm override written by default" bash -c '! grep -rqs "package-source-overrides" /etc/npmrc /root/.npmrc 2>/dev/null' +check "no Yarn override written by default" bash -c '! grep -qs "package-source-overrides" "$HOME/.yarnrc.yml"' + check "no pip override written by default" bash -c '[ ! -e /etc/pip.conf ] || ! grep -qs "package-source-overrides" /etc/pip.conf' +check "no uv override written by default" bash -c '[ ! -e /etc/uv/uv.toml ]' + check "no NuGet override written by default" bash -c '[ ! -e /etc/opt/NuGet/Config/NuGet.Config ]' +check "no env-only overrides written by default" bash -c '[ ! -e /etc/profile.d/package-source-overrides.sh ] && [ ! -e /etc/package-source-overrides/env.sh ]' + # Report results reportResults