diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 56e10fa..5920988 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,6 +22,7 @@ jobs: # (docker-in-docker) which the autogenerated test image does not provide. # It is exercised in the test-scenarios job instead. - atlassian-jira-confluence-cli + - package-source-overrides baseImage: - debian:latest - ubuntu:latest @@ -48,6 +49,7 @@ jobs: - gitlab-ci-local - copilot-metrics-lgtm - atlassian-jira-confluence-cli + - package-source-overrides steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 44eebe5..e9fb371 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,41 @@ acli jira project list `acli auth login` opens an OAuth page via `xdg-open`, which doesn't honor `$BROWSER` inside a dev container. With `configureBrowser` enabled (the default), the feature installs a small `xdg-open` shim at `/usr/local/bin/xdg-open` that prefers `$BROWSER` (such as the VS Code browser helper) so the login page opens on your host machine. Set `configureBrowser` to `false` to skip it. +### 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. + +**Usage:** + +```json +{ + "features": { + "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" + }, + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/python:1": {} + }, + "overrideFeatureInstallOrder": [ + "ghcr.io/rosstaco/devcontainer-features/package-source-overrides" + ] +} +``` + +**Options:** +- `npmRegistry` - registry URL for npm and pnpm (default: "") +- `pipIndexUrl` - pip / PyPI index URL that replaces the default index (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`) + +**Lifecycle & Ordering:** + +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. + ## Publishing This repository uses a **GitHub Action** [workflow](.github/workflows/release.yaml) that publishes each Feature to GHCR (GitHub Container Registry). diff --git a/justfile b/justfile index 81208a3..ebcd0d3 100644 --- a/justfile +++ b/justfile @@ -25,12 +25,17 @@ build-gitlab-ci-local: build-atlassian-jira-confluence-cli: just build-feature atlassian-jira-confluence-cli +# Build Package Source Overrides feature +build-package-source-overrides: + just build-feature package-source-overrides + # Build all features build-all: just build-ohmyposh just build-microsoft-security-devops-cli just build-gitlab-ci-local just build-atlassian-jira-confluence-cli + just build-package-source-overrides # Generate feature README.md files from devcontainer-feature.json and NOTES.md generate-docs: @@ -41,5 +46,6 @@ clean: rm -rf .devcontainer/ohmyposh rm -rf .devcontainer/gitlab-ci-local rm -rf .devcontainer/atlassian-jira-confluence-cli + rm -rf .devcontainer/package-source-overrides rm -f .devcontainer/*.tgz echo "✓ Cleaned local features" diff --git a/src/package-source-overrides/NOTES.md b/src/package-source-overrides/NOTES.md new file mode 100644 index 0000000..1cdedce --- /dev/null +++ b/src/package-source-overrides/NOTES.md @@ -0,0 +1,78 @@ +## About + +In locked-down environments you often cannot reach public package feeds +(`registry.npmjs.org`, `pypi.org`, `api.nuget.org`, …) and must go through an +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. + +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 +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`: + +```jsonc +{ + "features": { + "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" + }, + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/python:1": {} + }, + // Ensure the override sources are configured before anything installs packages + "overrideFeatureInstallOrder": [ + "ghcr.io/rosstaco/devcontainer-features/package-source-overrides" + ] +} +``` + +## Notes + +- **No authentication** is configured — the override sources are expected to allow + anonymous access. Do not put credentials in the URL options (they would be baked + into image layers). +- **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. diff --git a/src/package-source-overrides/README.md b/src/package-source-overrides/README.md new file mode 100644 index 0000000..21e4264 --- /dev/null +++ b/src/package-source-overrides/README.md @@ -0,0 +1,107 @@ + +# 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 + +## Example Usage + +```json +"features": { + "ghcr.io/rosstaco/devcontainer-features/package-source-overrides:1": {} +} +``` + +## Options + +| 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 | - | +| 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 | + +## About + +In locked-down environments you often cannot reach public package feeds +(`registry.npmjs.org`, `pypi.org`, `api.nuget.org`, …) and must go through an +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. + +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 +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`: + +```jsonc +{ + "features": { + "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" + }, + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/python:1": {} + }, + // Ensure the override sources are configured before anything installs packages + "overrideFeatureInstallOrder": [ + "ghcr.io/rosstaco/devcontainer-features/package-source-overrides" + ] +} +``` + +## Notes + +- **No authentication** is configured — the override sources are expected to allow + anonymous access. Do not put credentials in the URL options (they would be baked + into image layers). +- **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. + + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/rosstaco/devcontainer-features/blob/main/src/package-source-overrides/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/package-source-overrides/devcontainer-feature.json b/src/package-source-overrides/devcontainer-feature.json new file mode 100644 index 0000000..5712fc8 --- /dev/null +++ b/src/package-source-overrides/devcontainer-feature.json @@ -0,0 +1,58 @@ +{ + "id": "package-source-overrides", + "version": "1.0.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", + "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/" + }, + "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" + }, + "nugetSource": { + "type": "string", + "default": "", + "description": "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" + }, + "nugetSourceName": { + "type": "string", + "default": "override", + "description": "Friendly key/name for the configured NuGet package source" + }, + "scope": { + "type": "string", + "enum": [ + "system", + "user", + "both" + ], + "default": "both", + "description": "Where configuration is written: 'system' (system-wide files), 'user' (root + remote user home), or 'both'" + }, + "strictSsl": { + "type": "boolean", + "default": true, + "description": "When false, allow self-signed/insecure internal proxies (npm strict-ssl=false, pip trusted-host, NuGet allowInsecureConnections)" + } + }, + "containerEnv": { + "NPM_CONFIG_GLOBALCONFIG": "/etc/npmrc" + }, + "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." + } + ] + } + } + } +} diff --git a/src/package-source-overrides/install.sh b/src/package-source-overrides/install.sh new file mode 100755 index 0000000..8bddde0 --- /dev/null +++ b/src/package-source-overrides/install.sh @@ -0,0 +1,223 @@ +#!/bin/bash +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. + +NPM_REGISTRY="${NPMREGISTRY:-}" +PIP_INDEX_URL="${PIPINDEXURL:-}" +NUGET_SOURCE="${NUGETSOURCE:-}" +NUGET_SOURCE_NAME="${NUGETSOURCENAME:-override}" +SCOPE="${SCOPE:-both}" +STRICT_SSL="${STRICTSSL:-true}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +log() { echo -e "${GREEN}[package-source-overrides]${NC} $*"; } +warn() { echo -e "${YELLOW}[package-source-overrides]${NC} $*" >&2; } +err() { echo -e "${RED}[package-source-overrides]${NC} $*" >&2; } + +if [ "$(id -u)" -ne 0 ]; then + err "Script must be run as root. Add \"USER root\" before running, or use sudo." + exit 1 +fi + +# --- Resolve scope ---------------------------------------------------------- +DO_SYSTEM=false +DO_USER=false +case "${SCOPE}" in + system) DO_SYSTEM=true ;; + user) DO_USER=true ;; + both) DO_SYSTEM=true; DO_USER=true ;; + *) err "Invalid scope '${SCOPE}' (expected system|user|both)"; exit 1 ;; +esac + +# --- Insecure/self-signed proxy handling ------------------------------------ +INSECURE=false +if [ "${STRICT_SSL}" = "false" ]; then + INSECURE=true +fi + +# --- Resolve remote (non-root) user + home ---------------------------------- +REMOTE_USER="${_REMOTE_USER:-"${USERNAME:-vscode}"}" +USER_HOME="" +USER_UID="" +USER_GID="" +if [ "${REMOTE_USER}" != "root" ] && [ "${REMOTE_USER}" != "none" ] && id -u "${REMOTE_USER}" >/dev/null 2>&1; then + USER_HOME="${_REMOTE_USER_HOME:-"$(getent passwd "${REMOTE_USER}" 2>/dev/null | cut -d: -f6)"}" + USER_UID="$(id -u "${REMOTE_USER}" 2>/dev/null || true)" + USER_GID="$(id -g "${REMOTE_USER}" 2>/dev/null || true)" +fi + +BEGIN_MARK="# >>> package-source-overrides >>>" +END_MARK="# <<< package-source-overrides <<<" + +# --- Helpers ---------------------------------------------------------------- + +# make_dir +# Creates the directory; when owner is 'user', chowns each created level up to +# (but not including) the user's home so the remote user can read the config. +make_dir() { + local dir="$1" owner="$2" + mkdir -p "${dir}" + if [ "${owner}" = "user" ] && [ -n "${USER_HOME}" ] && [ -n "${USER_UID}" ]; then + local cur="${dir}" + while [ "${cur}" != "${USER_HOME}" ] && [ "${cur}" != "/" ] && [ "${cur}" != "." ]; do + chown "${USER_UID}:${USER_GID}" "${cur}" 2>/dev/null || true + cur="$(dirname "${cur}")" + done + fi +} + +# apply_owner +apply_owner() { + local path="$1" owner="$2" + case "${owner}" in + user) + if [ -n "${USER_UID}" ]; then + chown "${USER_UID}:${USER_GID}" "${path}" 2>/dev/null || true + fi + ;; + "") : ;; + *) chown "${owner}" "${path}" 2>/dev/null || true ;; + esac +} + +# remove_block : strip a previously managed marker block (idempotency) +remove_block() { + local path="$1" + [ -f "${path}" ] || return 0 + awk -v b="${BEGIN_MARK}" -v e="${END_MARK}" ' + $0==b {skip=1; next} + skip==1 && $0==e {skip=0; next} + skip==1 {next} + {print} + ' "${path}" > "${path}.pso.tmp" && mv "${path}.pso.tmp" "${path}" +} + +# write_full_file : fully managed file (overwritten) +write_full_file() { + local path="$1" owner="$2" content="$3" + make_dir "$(dirname "${path}")" "${owner}" + printf '%s\n' "${content}" > "${path}" + chmod 0644 "${path}" + apply_owner "${path}" "${owner}" + log "wrote ${path}" +} + +# write_block_file : managed marker block, preserving +# any unrelated lines already in the file. Our block is appended last so its +# keys win for last-wins parsers such as npm. +write_block_file() { + local path="$1" owner="$2" content="$3" + make_dir "$(dirname "${path}")" "${owner}" + remove_block "${path}" + { + printf '%s\n' "${BEGIN_MARK}" + printf '%s\n' "${content}" + printf '%s\n' "${END_MARK}" + } >> "${path}" + chmod 0644 "${path}" + apply_owner "${path}" "${owner}" + log "updated ${path}" +} + +# url_host : extract host[:port] from a URL (for pip trusted-host) +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. +configure_npm() { + [ -z "${NPM_REGISTRY}" ] && return 0 + log "Configuring npm/pnpm 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 + fi +} + +# --- pip / PyPI ------------------------------------------------------------- +# `index-url` replaces the default PyPI index (public feed no longer consulted). +configure_pip() { + [ -z "${PIP_INDEX_URL}" ] && return 0 + log "Configuring pip index-url -> ${PIP_INDEX_URL}" + local content="# Managed by the package-source-overrides dev container Feature +[global] +index-url = ${PIP_INDEX_URL}" + if [ "${INSECURE}" = "true" ]; then + content="${content} +trusted-host = $(url_host "${PIP_INDEX_URL}")" + fi + if [ "${DO_SYSTEM}" = "true" ]; then + 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 + fi +} + +# --- NuGet / .NET ----------------------------------------------------------- +# drops nuget.org so only the override source remains (replace policy). +configure_nuget() { + [ -z "${NUGET_SOURCE}" ] && return 0 + log "Configuring NuGet source '${NUGET_SOURCE_NAME}' -> ${NUGET_SOURCE}" + local insecure_attr="" + if [ "${INSECURE}" = "true" ]; then + insecure_attr=" allowInsecureConnections=\"true\"" + fi + local 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 + fi +} + +# --- Main ------------------------------------------------------------------- +configure_npm +configure_pip +configure_nuget + +if [ -z "${NPM_REGISTRY}${PIP_INDEX_URL}${NUGET_SOURCE}" ]; then + warn "No override sources provided (npmRegistry, pipIndexUrl, nugetSource all empty); nothing configured." +else + log "Done. scope=${SCOPE} strictSsl=${STRICT_SSL} remoteUser=${REMOTE_USER}${USER_HOME:+ (${USER_HOME})}" +fi + +exit 0 diff --git a/test/package-source-overrides/all-insecure.sh b/test/package-source-overrides/all-insecure.sh new file mode 100755 index 0000000..d4af63e --- /dev/null +++ b/test/package-source-overrides/all-insecure.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Scenario 'all-insecure': all three ecosystems with strictSsl=false. Asserts the +# insecure/self-signed settings are emitted for each package manager. + +set -e + +source dev-container-features-test-lib + +check "npm strict-ssl disabled" grep -qF "strict-ssl=false" /etc/npmrc + +check "pip trusted-host set to proxy host" grep -qF "trusted-host = proxy.internal" /etc/pip.conf + +check "NuGet allowInsecureConnections enabled" grep -qF 'allowInsecureConnections="true"' /etc/opt/NuGet/Config/NuGet.Config + +reportResults diff --git a/test/package-source-overrides/npm.sh b/test/package-source-overrides/npm.sh new file mode 100755 index 0000000..21b3a24 --- /dev/null +++ b/test/package-source-overrides/npm.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Scenario 'npm': only npmRegistry set (scope defaults to 'both', strictSsl true). + +set -e + +source dev-container-features-test-lib + +REG="https://proxy.internal/artifactory/api/npm/npm-remote/" + +check "system /etc/npmrc has registry" grep -qF "registry=${REG}" /etc/npmrc + +check "root ~/.npmrc has registry" sudo grep -qF "registry=${REG}" /root/.npmrc + +check "remote user ~/.npmrc has registry" grep -qF "registry=${REG}" /home/vscode/.npmrc + +check "remote user ~/.npmrc owned by vscode" bash -c '[ "$(stat -c %U /home/vscode/.npmrc)" = "vscode" ]' + +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' + +reportResults diff --git a/test/package-source-overrides/nuget.sh b/test/package-source-overrides/nuget.sh new file mode 100755 index 0000000..a8372a1 --- /dev/null +++ b/test/package-source-overrides/nuget.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Scenario 'nuget': nugetSource + nugetSourceName=corp (scope 'both'). + +set -e + +source dev-container-features-test-lib + +MACHINE="/etc/opt/NuGet/Config/NuGet.Config" +USERCFG="/home/vscode/.nuget/NuGet/NuGet.Config" +URL="https://proxy.internal/artifactory/api/nuget/v3/index.json" + +check "machine-wide NuGet.Config exists" test -f "${MACHINE}" + +check "NuGet config clears public sources" grep -qF "" "${MACHINE}" + +check "NuGet config adds override source key" grep -qF 'key="corp"' "${MACHINE}" + +check "NuGet config has override source url" grep -qF "${URL}" "${MACHINE}" + +check "remote user NuGet.Config exists" test -f "${USERCFG}" + +check "remote user NuGet.Config owned by vscode" bash -c '[ "$(stat -c %U '"${USERCFG}"')" = "vscode" ]' + +reportResults diff --git a/test/package-source-overrides/pip.sh b/test/package-source-overrides/pip.sh new file mode 100755 index 0000000..3f9d7a6 --- /dev/null +++ b/test/package-source-overrides/pip.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Scenario 'pip': only pipIndexUrl set (scope defaults to 'both', strictSsl true). + +set -e + +source dev-container-features-test-lib + +URL="https://proxy.internal/artifactory/api/pypi/pypi-remote/simple" + +check "system /etc/pip.conf has index-url" grep -qF "index-url = ${URL}" /etc/pip.conf + +check "remote user pip.conf has index-url" grep -qF "index-url = ${URL}" /home/vscode/.config/pip/pip.conf + +check "remote user pip.conf owned by vscode" bash -c '[ "$(stat -c %U /home/vscode/.config/pip/pip.conf)" = "vscode" ]' + +check "no trusted-host when secure" bash -c '! grep -q "trusted-host" /etc/pip.conf' + +reportResults diff --git a/test/package-source-overrides/scenarios.json b/test/package-source-overrides/scenarios.json new file mode 100644 index 0000000..9a38ff7 --- /dev/null +++ b/test/package-source-overrides/scenarios.json @@ -0,0 +1,48 @@ +{ + "npm": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "package-source-overrides": { + "npmRegistry": "https://proxy.internal/artifactory/api/npm/npm-remote/" + } + } + }, + "pip": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "package-source-overrides": { + "pipIndexUrl": "https://proxy.internal/artifactory/api/pypi/pypi-remote/simple" + } + } + }, + "nuget": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "package-source-overrides": { + "nugetSource": "https://proxy.internal/artifactory/api/nuget/v3/index.json", + "nugetSourceName": "corp" + } + } + }, + "all-insecure": { + "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", + "strictSsl": false + } + } + }, + "scope-user": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "package-source-overrides": { + "npmRegistry": "https://proxy.internal/npm/", + "pipIndexUrl": "https://proxy.internal/pypi/simple", + "scope": "user" + } + } + } +} diff --git a/test/package-source-overrides/scope-user.sh b/test/package-source-overrides/scope-user.sh new file mode 100755 index 0000000..4aa1616 --- /dev/null +++ b/test/package-source-overrides/scope-user.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Scenario 'scope-user': scope=user should write per-user config (root + remote +# user) but NOT the system-wide files. + +set -e + +source dev-container-features-test-lib + +check "root ~/.npmrc present" sudo test -f /root/.npmrc + +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 "system /etc/npmrc absent" bash -c '[ ! -e /etc/npmrc ]' + +check "system /etc/pip.conf absent" bash -c '[ ! -e /etc/pip.conf ]' + +reportResults diff --git a/test/package-source-overrides/test.sh b/test/package-source-overrides/test.sh new file mode 100755 index 0000000..fdc9abe --- /dev/null +++ b/test/package-source-overrides/test.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Autogenerated test: the Feature runs with default options, so every source URL +# 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. + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +check "containerEnv NPM_CONFIG_GLOBALCONFIG applied" bash -c '[ "$NPM_CONFIG_GLOBALCONFIG" = "/etc/npmrc" ]' + +check "no npm override written by default" bash -c '! grep -rqs "package-source-overrides" /etc/npmrc /root/.npmrc 2>/dev/null' + +check "no pip override written by default" bash -c '[ ! -e /etc/pip.conf ] || ! grep -qs "package-source-overrides" /etc/pip.conf' + +check "no NuGet override written by default" bash -c '[ ! -e /etc/opt/NuGet/Config/NuGet.Config ]' + +# Report results +reportResults