Skip to content

Commit d6fd0ee

Browse files
feat(distribution): add enterprise bootstrap mirrors (#1733)
* feat(distribution): add enterprise bootstrap mirrors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Redact bootstrap mirror output Folds copilot-pull-request-reviewer follow-ups (credential redaction in mirror output, copy-pasteable self-update fallback, changelog placement). Refs #1680 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(install): do not send GitHub token to mirror hosts install.sh attached the resolved GitHub token to APM_RELEASE_METADATA_URL and APM_RELEASE_BASE_URL mirror requests, and install.ps1 leaked it on the asset final-fallback and checksum-retry paths. Gate every auth attach on mirror-env absence so the token rides only to the canonical GitHub / configured GHES host, never to an operator mirror. The two scripts are now symmetric. Adds static symmetry traps plus executable fail-closed exit-code tests (no network) for the installer guards. addresses CEO follow-up #1 (Auth Expert / Supply Chain) and Test Coverage follow-up #3 on PR #1733. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(install): clarify fail-closed GHES boundary and pip egress Document that APM_NO_DIRECT_FALLBACK keys off the public github.com default: a custom GHES GITHUB_URL still egresses to that host, so 'no direct fallback' means no public fallback, not zero egress. Extend the no-egress smoke recipe to wrap pip alongside curl so the PyPI fallback path is actually proven, and note the token-to-canonical-host-only rule across installer and self-update docs and the apm-guide usage doc. addresses CEO follow-up #2 (Supply Chain), #4 (smoke pip path), and Doc Writer nit on PR #1733. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(version-checker): use relative bootstrap_mirror import Align version_checker.py with self_update.py: import the shared bootstrap_mirror module via a package-relative path (from ..bootstrap_mirror) for one consistent convention across both consumers. addresses Python Architect nit on PR #1733. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: avoid auth header on release metadata mirrors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1f6d522 commit d6fd0ee

12 files changed

Lines changed: 1065 additions & 86 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
of `~/.hermes/config.yaml` (written atomically with `0o600` perms, preserving
1818
unrelated config keys and refusing to overwrite a malformed file). `HERMES_HOME`
1919
overrides the Hermes home directory. See the [Hermes integration guide](https://microsoft.github.io/apm/integrations/hermes/).
20+
- Enterprise bootstrap mirror mode lets `install.sh`, `install.ps1`, and `apm self-update` use internal release, installer, and PyPI mirrors with fail-closed public fallback, and closes #1680. (#1733)
2021

2122
### Fixed
2223

docs/src/content/docs/getting-started/installation.md

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,113 @@ jobs:
9090
|----------|---------|-------------|
9191
| `APM_INSTALL_DIR` | `/usr/local/bin` (Unix) / `%LOCALAPPDATA%\Programs\apm\bin` (Windows) | Directory for the `apm` symlink / `apm.cmd` shim |
9292
| `APM_LIB_DIR` | `$(dirname APM_INSTALL_DIR)/lib/apm` | *(Unix only)* Directory for the full binary bundle. Must end with `/apm` (for example, `/lib/apm`). The installer rejects shared directories (e.g. `$HOME/.local/share`) to prevent accidental data loss. |
93-
| `GITHUB_URL` | `https://github.com` | Base GitHub URL (asset downloads **and** API host: `api.github.com` on github.com, `{GITHUB_URL}/api/v3` on GHES). Must be `https://`. |
93+
| `GITHUB_URL` | `https://github.com` | Base GitHub URL (asset downloads **and** API host: `api.github.com` on github.com, `{GITHUB_URL}/api/v3` on GHES). Must be `https://` on Windows. |
9494
| `APM_REPO` | `microsoft/apm` | Repository as `owner/name` |
9595
| `VERSION` | *(latest)* | Pin a release tag (skips the **releases/latest** HTTP API). Must look like `v1.2.3` or `1.2.3`. |
96+
| `APM_RELEASE_METADATA_URL` | *(unset)* | Exact mirror URL for release metadata, usually `latest.json` with at least `{"tag_name":"vX.Y.Z"}`. |
97+
| `APM_RELEASE_BASE_URL` | *(unset)* | Base URL for release assets laid out as `{base}/{tag}/{asset}` and `{base}/{tag}/{asset}.sha256`. |
98+
| `APM_INSTALLER_BASE_URL` | *(unset)* | Base URL containing `install.sh` and `install.ps1`; used by `apm self-update` and by your bootstrap one-liner. |
99+
| `APM_PYPI_INDEX_URL` | *(unset)* | PyPI-compatible mirror used when the installer falls back to pip. |
100+
| `APM_NO_DIRECT_FALLBACK` | *(unset)* | Set to `1` to fail closed when a mirror is missing or unreachable instead of using public GitHub, `aka.ms`, or PyPI. |
96101
| `APM_SKIP_CHECKSUM` | *(unset)* | Windows only: set to `1` to skip `.sha256` verification on **pinned** installs (emergency only). |
97102

98-
> **Note - Unix (`install.sh`):** Latest-release discovery still calls `https://api.github.com/repos/.../releases/latest` unless `VERSION` is set. For GHES or mirrors with no access to `api.github.com`, pin `VERSION` so the script never hits that endpoint.
99-
>
100-
> **Note - Windows (`install.ps1`):** The **releases/latest** URL is derived from `GITHUB_URL`: `https://api.github.com` for GitHub.com, or `{GITHUB_URL}/api/v3` for GitHub Enterprise Server. Air-gapped runners should still set `VERSION` so the installer does not need the API at all. When `VERSION` is pinned, the release **`.sha256`** file is required unless you set **`APM_SKIP_CHECKSUM=1`** (emergency only).
103+
### Enterprise bootstrap mirror mode
104+
105+
Mirror mode lets locked-down workstations install and update APM without direct public egress:
106+
107+
```bash
108+
export APM_INSTALLER_BASE_URL="https://artifactory.mycorp.example/generic/apm-install"
109+
export APM_RELEASE_METADATA_URL="https://artifactory.mycorp.example/generic/apm-releases/latest.json"
110+
export APM_RELEASE_BASE_URL="https://artifactory.mycorp.example/generic/apm-releases"
111+
export APM_PYPI_INDEX_URL="https://artifactory.mycorp.example/api/pypi/python-proxy/simple"
112+
export APM_NO_DIRECT_FALLBACK=1
113+
114+
curl -sSL "$APM_INSTALLER_BASE_URL/install.sh" | sh
115+
apm self-update --check
116+
```
117+
118+
For Windows:
119+
120+
```powershell
121+
$env:APM_INSTALLER_BASE_URL = "https://artifactory.mycorp.example/generic/apm-install"
122+
$env:APM_RELEASE_METADATA_URL = "https://artifactory.mycorp.example/generic/apm-releases/latest.json"
123+
$env:APM_RELEASE_BASE_URL = "https://artifactory.mycorp.example/generic/apm-releases"
124+
$env:APM_PYPI_INDEX_URL = "https://artifactory.mycorp.example/api/pypi/python-proxy/simple"
125+
$env:APM_NO_DIRECT_FALLBACK = "1"
126+
127+
irm "$env:APM_INSTALLER_BASE_URL/install.ps1" | iex
128+
apm self-update --check
129+
```
130+
131+
Mirror layout for binary releases:
132+
133+
```text
134+
apm-releases/
135+
latest.json
136+
v0.19.0/
137+
apm-linux-x86_64.tar.gz
138+
apm-darwin-arm64.tar.gz
139+
apm-windows-x86_64.zip
140+
apm-windows-x86_64.zip.sha256
141+
```
142+
143+
`APM_NO_DIRECT_FALLBACK=1` makes missing mirror settings and unreachable mirrors hard failures. It does not replace package-install proxying; keep using `PROXY_REGISTRY_URL` and `PROXY_REGISTRY_ONLY=1` for `apm install` dependencies.
144+
145+
#### What fail-closed does and does not cover
146+
147+
Fail-closed scoping keys off the public `github.com` default. The guard only blocks egress when the resolved host would be public GitHub (`github.com` / `api.github.com`), `aka.ms`, or public PyPI. It does **not** suppress egress to a custom `GITHUB_URL`: if you set a GHES host (for example `GITHUB_URL=https://github.corp.com`) together with `APM_NO_DIRECT_FALLBACK=1` and no release mirror, the installer still reaches that GHES host. This is intentional coexistence with GHES, but "no direct fallback" should not be read as "zero egress" -- it means "no fallback to public hosts". For true zero-egress, set the `APM_RELEASE_METADATA_URL` / `APM_RELEASE_BASE_URL` / `APM_INSTALLER_BASE_URL` / `APM_PYPI_INDEX_URL` mirrors so every request resolves to your internal hosts. The GitHub token is attached only when the request targets the canonical GitHub / configured GHES host; it is never sent to a mirror host.
148+
149+
Homebrew and Scoop mirror support is docs-only in this v0: mirror the tap or bucket with your package manager's normal enterprise controls, but the APM env vars above do not rewrite Homebrew or Scoop internals.
150+
151+
### No-egress smoke test
152+
153+
Run this on a disposable Linux or macOS runner. It starts a local mirror, wraps both `curl` and `pip` with deny-lists for public hosts, and expects the installer to fail only after downloading the fake archive. Any request to GitHub, `aka.ms`, PyPI, Homebrew, or Scoop fails the smoke test immediately. The `pip` wrapper makes the PyPI egress path explicit: pip fallback is gated by `APM_NO_DIRECT_FALLBACK` + `APM_PYPI_INDEX_URL`, so the wrapper proves pip cannot reach public PyPI even if the binary path falls back to it.
154+
155+
```bash
156+
set -eu
157+
rm -rf .apm-mirror-smoke
158+
mkdir -p .apm-mirror-smoke/mirror/apm-install \
159+
.apm-mirror-smoke/mirror/apm-releases/v9.9.9 \
160+
.apm-mirror-smoke/bin
161+
cp install.sh .apm-mirror-smoke/mirror/apm-install/install.sh
162+
printf '{"tag_name":"v9.9.9"}\n' > .apm-mirror-smoke/mirror/apm-releases/latest.json
163+
printf 'not-a-real-archive\n' > .apm-mirror-smoke/mirror/apm-releases/v9.9.9/apm-linux-x86_64.tar.gz
164+
cat > .apm-mirror-smoke/bin/curl <<'SH'
165+
#!/bin/sh
166+
case " $* " in
167+
*github.com*|*api.github.com*|*aka.ms*|*pypi.org*|*pythonhosted.org*|*brew.sh*|*scoop*)
168+
echo "public egress blocked: $*" >&2
169+
exit 70
170+
;;
171+
esac
172+
exec /usr/bin/curl "$@"
173+
SH
174+
cat > .apm-mirror-smoke/bin/pip <<'SH'
175+
#!/bin/sh
176+
# Deny public PyPI; allow only the configured mirror index.
177+
case " $* " in
178+
*pypi.org*|*pythonhosted.org*)
179+
echo "public pip egress blocked: $*" >&2
180+
exit 70
181+
;;
182+
esac
183+
exec /usr/bin/pip "$@"
184+
SH
185+
cp .apm-mirror-smoke/bin/pip .apm-mirror-smoke/bin/pip3
186+
chmod +x .apm-mirror-smoke/bin/curl .apm-mirror-smoke/bin/pip .apm-mirror-smoke/bin/pip3
187+
python3 -m http.server 8765 --directory .apm-mirror-smoke/mirror > .apm-mirror-smoke/server.log 2>&1 &
188+
server_pid=$!
189+
trap 'kill "$server_pid" 2>/dev/null || true' EXIT
190+
PATH="$PWD/.apm-mirror-smoke/bin:$PATH" \
191+
APM_INSTALLER_BASE_URL="http://127.0.0.1:8765/apm-install" \
192+
APM_RELEASE_METADATA_URL="http://127.0.0.1:8765/apm-releases/latest.json" \
193+
APM_RELEASE_BASE_URL="http://127.0.0.1:8765/apm-releases" \
194+
APM_PYPI_INDEX_URL="http://127.0.0.1:8765/pypi/simple" \
195+
APM_NO_DIRECT_FALLBACK=1 \
196+
sh .apm-mirror-smoke/mirror/apm-install/install.sh || test $? -eq 1
197+
```
198+
199+
For `apm self-update`, run `apm self-update --check` with the same env vars and verify your proxy, firewall, or CI egress logs show only the mirror host. Use a disposable runner for a full `apm self-update` because it executes the mirrored installer.
101200

102201
## Package managers
103202

docs/src/content/docs/reference/cli/self-update.md

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,38 @@ The command compares the installed version against the latest GitHub release and
3333
Some package-manager distributions (for example, Homebrew) disable self-update at build time. In those builds, `apm self-update` prints a distributor-defined message (such as `brew upgrade apm`) and exits without running the installer. The startup update notification is also suppressed in those builds.
3434
:::
3535

36-
## Air-gapped and GitHub Enterprise environments
36+
## Enterprise bootstrap mirrors
3737

38-
`apm self-update` respects the same environment variables that `install.sh` honours
39-
for air-gapped networks and GitHub Enterprise Server (GHE):
38+
`apm self-update` uses the same mirror contract as the installer scripts. These variables are additive to the older `GITHUB_URL` / `APM_REPO` / `VERSION` flow:
4039

4140
| Variable | Default | Effect |
4241
|----------|---------|--------|
43-
| `GITHUB_URL` | `https://github.com` | Base URL of the GitHub host. When set to a GHE host (e.g. `https://gh.corp.com`), the version-check API uses `{GITHUB_URL}/api/v3` and the installer script is fetched from `{GITHUB_URL}/{APM_REPO}/raw/main/install.sh` (Unix) or `install.ps1` (Windows). |
44-
| `APM_REPO` | `microsoft/apm` | Repository in `owner/repo` form. Overrides the default when using a fork or an internal mirror. |
45-
| `VERSION` | _(unset)_ | Pin a specific release (e.g. `v1.2.3`). Skips the GitHub API call entirely -- required for fully offline setups. The pinned version is passed through to the installer subprocess. |
42+
| `APM_RELEASE_METADATA_URL` | _(unset)_ | Exact URL for mirrored release metadata, usually a static `latest.json` with at least `{"tag_name":"vX.Y.Z"}`. Overrides GitHub release metadata lookup. |
43+
| `APM_INSTALLER_BASE_URL` | _(unset)_ | Base URL containing `install.sh` and `install.ps1`. `apm self-update` downloads the platform script from this base. |
44+
| `APM_RELEASE_BASE_URL` | _(unset)_ | Base URL containing release assets at `{base}/{tag}/{asset}`. The downloaded installer subprocess inherits this value. |
45+
| `APM_PYPI_INDEX_URL` | _(unset)_ | PyPI-compatible index used by installer pip fallback. |
46+
| `APM_NO_DIRECT_FALLBACK` | _(unset)_ | Set to `1` to fail closed instead of using public GitHub, `aka.ms`, or PyPI fallback. |
47+
| `GITHUB_URL` | `https://github.com` | Legacy GitHub/GHES base URL. Still supported when mirror env vars are not set. |
48+
| `APM_REPO` | `microsoft/apm` | Repository in `owner/repo` form for GitHub/GHES paths. |
49+
| `VERSION` | _(unset)_ | Pin a release tag and skip release metadata lookup. |
4650

47-
Examples:
51+
Example:
4852

4953
```bash
50-
# Air-gapped update: skip API, fetch installer from internal mirror
51-
GITHUB_URL=https://gh.corp.com APM_REPO=corp/apm VERSION=v1.2.3 apm self-update
54+
export APM_RELEASE_METADATA_URL="https://artifactory.mycorp.example/generic/apm-releases/latest.json"
55+
export APM_INSTALLER_BASE_URL="https://artifactory.mycorp.example/generic/apm-install"
56+
export APM_RELEASE_BASE_URL="https://artifactory.mycorp.example/generic/apm-releases"
57+
export APM_PYPI_INDEX_URL="https://artifactory.mycorp.example/api/pypi/python-proxy/simple"
58+
export APM_NO_DIRECT_FALLBACK=1
5259

53-
# GHE host with version check (no VERSION pin -- API is queried via /api/v3)
54-
GITHUB_URL=https://gh.corp.com APM_REPO=corp/apm apm self-update --check
60+
apm self-update --check
61+
apm self-update
5562
```
5663

64+
With `APM_NO_DIRECT_FALLBACK=1`, missing or unreachable mirror settings are hard failures with a non-zero exit. APM does not fall back to public GitHub, `aka.ms`, or PyPI in that mode.
65+
66+
Fail-closed scoping keys off the public `github.com` default: it blocks fallback to public hosts, not all egress. A custom `GITHUB_URL` (a GHES host) combined with `APM_NO_DIRECT_FALLBACK=1` and no release mirror still egresses to that GHES host -- this is intentional GHES coexistence. For zero public egress set the `APM_RELEASE_METADATA_URL` / `APM_RELEASE_BASE_URL` / `APM_INSTALLER_BASE_URL` / `APM_PYPI_INDEX_URL` mirrors. The GitHub token is sent only to the canonical GitHub / configured GHES host, never to a mirror host.
67+
5768
## Options
5869

5970
| Flag | Description |
@@ -104,7 +115,7 @@ The installer scripts accept a version pin via environment variable -- see [Quic
104115

105116
## Failure modes
106117

107-
If GitHub is unreachable, the download fails, or the installer exits non-zero, `apm self-update` exits with code `1` and prints the manual update command. Your existing binary is unaffected.
118+
If GitHub or a configured mirror is unreachable, the download fails, or the installer exits non-zero, `apm self-update` exits with code `1` and prints the next mirror or manual update action. Your existing binary is unaffected.
108119

109120
## Startup update notification
110121

0 commit comments

Comments
 (0)