Skip to content

feat(provider): daemonless oci:// + ::<path> for custom binary location - #143

Merged
fentas merged 7 commits into
mainfrom
feat/oci-provider-and-custom-path
Apr 16, 2026
Merged

feat(provider): daemonless oci:// + ::<path> for custom binary location#143
fentas merged 7 commits into
mainfrom
feat/oci-provider-and-custom-path

Conversation

@fentas

@fentas fentas commented Apr 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • New oci:// provider: pulls binaries directly from any OCI registry (Docker Hub, ghcr.io, quay.io, private) without a container runtime. Uses go-containerregistry for native auth (reads ~/.docker/config.json) and platform-aware manifest resolution.
  • New :/<path> suffix on both docker:// and oci:// refs to pin an explicit in-container binary location. The leading / disambiguates the path from docker's native image:tag syntax (which b never uses — tags always go after @, consistent with every other provider).
    • docker://docker@cli:/usr/local/bin/docker
    • oci://ghcr.io/org/img@v1:/bin/tool
  • Binary size impact (stripped): +1.03 MB (~10.7 MB → ~11.8 MB).

Why

  • docker:// forces a running container runtime and only searches hardcoded paths, which doesn't cover custom image layouts.
  • oci:// works in CI/minimal environments without docker, and makes the "any registry" intent explicit.
  • @tag works for standard images, but there was no way to install a binary from a non-standard in-container path.

Design notes

  • Tag separator: @ (consistent with every other b provider). Docker-style image:tag is not used.
  • Path separator: :/ (single : followed by absolute path). Registry ports like localhost:5000/org/img are preserved because SplitImagePath uses LastIndex(":/") after skipping the :// scheme prefix.
  • docker:// / oci:// refs are never interpreted as SCP-style env installs.

Test plan

  • go test ./... — all pass
  • Unit tests for ParseImageRef, SplitImagePath, BinaryName covering registry ports, empty paths, and docker-style refs.
  • In-memory tar extraction tests (oci_extract_test.go): priority, no-match, non-regular files, empty search paths — no network needed.
  • End-to-end: b i oci://docker@cli:/usr/local/bin/docker → extracts working docker CLI (41M) without docker daemon.
  • End-to-end: b i oci://ghcr.io/linuxcontainers/alpine@latest:/bin/busybox → daemonless pull from ghcr.io.
  • Regression: existing docker:// refs continue to work unchanged.

Docs

  • README.md — examples and config snippet updated for :/path syntax.
  • docs/b/subcommands/install.mdx — new "Install from container images" section covering both providers.
  • docs/authentication.mdx — OCI auth via ~/.docker/config.json.
  • docs/glossary.mdx — provider reference entry lists all supported prefixes.

🤖 Generated with Claude Code

…cation

Adds a new oci:// provider that pulls and extracts binaries from any OCI
registry (Docker Hub, ghcr.io, quay.io, private) without requiring a
container runtime. Uses go-containerregistry for native auth (reads
~/.docker/config.json) and multi-arch manifest resolution.

Also adds ::<path> syntax to both docker:// and oci:// refs to pin an
explicit in-container binary path, so consumers no longer depend on the
provider guessing the right search path:

  docker://docker@cli::/usr/local/bin/docker
  oci://ghcr.io/org/img@v1::/bin/tool

Binary size impact (stripped): +1.03 MB (~10.7 MB → ~11.8 MB).

- pkg/provider/oci.go          — daemonless pull + layer tar extract
- pkg/provider/docker.go       — supports ::<path> override
- pkg/provider/provider.go     — ParseImageRef, ParseRef/BinaryName aware of ::
- pkg/binary/download.go       — dispatches OCI case
- pkg/cli/install.go           — parseBinaryArg and parseSCPArg skip ::

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- List oci:// in the providers example in b.yaml config
- Note that oci:// uses ~/.docker/config.json for registry auth

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new daemonless oci:// provider (OCI registry pull + in-image binary extraction) and extends both docker:// and oci:// refs with a ::<path> suffix to pin an explicit in-image binary location.

Changes:

  • Introduce oci:// provider using go-containerregistry to pull images without a container runtime and extract a single binary from layers.
  • Add ::<in-container-path> parsing/behavior for docker:// and oci:// refs, including correct @tag parsing and binary name derivation.
  • Update CLI parsing/tests/docs plus Go module dependencies to support the new provider.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pkg/provider/oci.go New OCI provider implementation: remote pull + platform selection + tar-layer extraction.
pkg/provider/oci_test.go Basic unit tests for OCI provider metadata and ParseImageRef.
pkg/provider/docker.go Teach Docker provider to accept ::<path> and reuse ParseImageRef.
pkg/provider/provider.go Extend ref parsing (ParseRef, BinaryName) and add shared ParseImageRef.
pkg/provider/provider_test.go Add tests for ParseRef and BinaryName covering ::<path>.
pkg/binary/download.go Route downloads to the new provider.OCI install path.
pkg/cli/install.go Update CLI arg parsing to preserve ::<path> while parsing @version.
pkg/cli/install_test.go Ensure ::<path> isn’t misinterpreted as SCP syntax.
pkg/cli/cli_extra_test.go Add CLI parse tests for docker:///oci:// with ::<path>.
README.md Document new oci:// provider and ::<path> examples.
go.mod / go.sum Add go-containerregistry and update dependency graph accordingly.
Comments suppressed due to low confidence (1)

pkg/provider/docker.go:53

  • Docker.Install(): ParseImageRef currently does not strip or parse docker-style ":tag" in the image portion, but Install always appends ":" + tag. As a result, refs like "docker://org/image:1.0" will produce an invalid imageRef ("org/image:1.0:latest" when version is empty). Consider teaching ParseImageRef (or Install) to recognize and handle ":tag" (including the registry-port case) so existing docker-style refs don’t break.
	rest := strings.TrimPrefix(ref, "docker://")
	image, refTag, inContainerPath := ParseImageRef(rest)

	tag := version
	if tag == "" {
		tag = refTag
	}
	if tag == "" {
		tag = "latest"
	}
	imageRef := image + ":" + tag
	name := BinaryName(ref)

Comment thread pkg/provider/provider.go Outdated
Comment thread pkg/provider/provider.go Outdated
Comment thread pkg/provider/docker.go Outdated
Comment thread pkg/provider/oci.go Outdated
Comment thread pkg/provider/oci.go Outdated
Comment thread pkg/provider/oci.go Outdated
- Syntax changes from '::/path' to ':/path', consistent with b's other
  providers where '@' is the tag separator. The leading '/' on the path
  disambiguates it from docker's native 'image:tag' (which b never uses).
- SplitImagePath skips the "://" scheme prefix and uses the last ":/"
  so registry ports like "localhost:5000/org/img" parse correctly.
- Docker/OCI refs are no longer eligible for SCP-style env install; the
  parser short-circuits on their prefixes.

Copilot review follow-ups:
- BinaryName falls back to default derivation when path is empty / trailing
  slash so we never return "".
- dockerImage only strips "image:tag" when ':' is after the last '/',
  preserving registry ports.
- OCI.Install scans each layer once against a set of candidate paths
  (instead of O(layers x paths) re-decompressions), writing to a temp file
  and renaming once the highest-priority match is known.
- resolveImage replaced with remote.Image + WithPlatform, which handles
  variant matching and fallback correctly via go-containerregistry.
- New oci_extract_test.go covers priority, no-match, non-regular files,
  and empty search paths against in-memory tar layers.

Docs:
- README binary+config examples updated to the ':' path syntax.
- docs/b/subcommands/install.mdx gets a "container images" section.
- docs/authentication.mdx documents OCI auth via ~/.docker/config.json.
- docs/glossary.mdx lists the new prefixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 9 comments.

Comment thread docs/glossary.mdx Outdated
Comment thread docs/b/subcommands/install.mdx
Comment thread pkg/provider/docker.go Outdated
Comment thread pkg/provider/oci.go Outdated
Comment thread pkg/provider/oci.go
Comment thread pkg/provider/provider.go Outdated
Comment thread pkg/provider/provider.go Outdated
Comment thread pkg/provider/docker.go
Comment thread docs/b/subcommands/install.mdx Outdated
Addresses the second round of Copilot review:

- OCI.Install now tracks whiteouts (.wh.<name> and .wh..wh..opq) while
  walking layers newest-first, so a deleted-in-newer-layer path can't be
  resurrected from an older layer.
- Dropped the stale '::<path>'/'docker://oci://' wording from docstrings
  and examples — everything now uses ':/<path>' and 'docker:// or oci://'.
- docs/b/subcommands/install.mdx uses an absolute '/authentication' link
  so it resolves correctly in the Docusaurus build.
- Provider Reference glossary entry is no longer git-centric.

Added whiteout-specific tests (file whiteout, opaque-dir whiteout,
whiteout map recording) against in-memory tar layers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 'if whiteouts["/"] { return true }; return false' with
direct 'return whiteouts["/"]'. Functionally identical.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

pkg/provider/docker.go:54

  • Docker.Install now uses ParseImageRef() and then builds imageRef as image+":"+tag. If the user passes a docker-style image ref containing a ':tag' (common copy/paste), image will already include the tag and this will produce an invalid reference like "alpine:3.19:latest" (and a confusing runtime error). Consider explicitly detecting docker-style tags (only ':' after last '/') and either parsing them into refTag when version is empty, stripping them before appending tag, or returning a clear error instructing users to use '@tag'.
	rest := strings.TrimPrefix(ref, "docker://")
	image, refTag, inContainerPath := ParseImageRef(rest)

	tag := version
	if tag == "" {
		tag = refTag
	}
	if tag == "" {
		tag = "latest"
	}
	imageRef := image + ":" + tag
	name := BinaryName(ref)

Comment thread pkg/cli/install.go Outdated
Comment thread pkg/provider/oci.go
Comment thread pkg/provider/oci.go Outdated
Comment thread pkg/provider/oci.go
- ParseImageRef also accepts docker-style 'image:tag' as a copy-paste
  convenience. @tag remains the preferred syntax, but users can paste
  'oci://alpine:3.19' from docker docs without a confusing error.
  Registry ports are still preserved (the scan only treats a ':' as a tag
  when it's after the last '/').
- parseBinaryArg now delegates to provider.ParseRef, eliminating the
  duplicated docker://oci://-aware '@' split logic.
- extractBinaryFromLayer skips whiteout-blocked candidates during the
  scan rather than only after, so a lower-priority fallback in the same
  layer is correctly used when the preferred path is whited out.
- Regular-file check uses FileInfo().Mode().IsRegular() so both TypeReg
  and the deprecated NUL-byte TypeRegA (still seen in some tar encodings)
  are accepted, without referencing the deprecated constant.

New tests cover: fallback after whiteout in same layer, legacy NUL
typeflag, and docker-style 'image:tag' parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.

Comment thread pkg/provider/provider.go Outdated
Comment thread pkg/provider/oci.go Outdated
Comment thread pkg/cli/install.go Outdated
- ParseRef now also tolerates docker-style 'image:tag' (with ':' only
  after the last '/' so registry ports survive). Previously the tag was
  silently kept on base, producing inconsistent lock/config representations
  vs. the preferred '@tag' form.
- Root opaque whiteout ('/.wh..wh..opq') is now stored under the '/'
  sentinel so isWhiteoutBlocked actually hides older layers. Previously
  path.Dir("/")+"/" produced '//' and the root sentinel never matched.
- Minor wording fix in parseBinaryArg comment.

New test: TestExtractBinaryFromLayer_RootOpaqueBlocksEverything.
New ParseRef cases cover 'image:tag', 'image:tag:/path', registry ports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.

@fentas
fentas merged commit 6fc80e6 into main Apr 16, 2026
14 checks passed
@fentas
fentas deleted the feat/oci-provider-and-custom-path branch April 16, 2026 10:10
fentas pushed a commit that referenced this pull request Apr 16, 2026
🤖 I have created a release *beep* *boop*
---


## [4.15.0](v4.14.1...v4.15.0)
(2026-04-16)


### Features

* **provider:** daemonless oci:// + ::&lt;path&gt; for custom binary
location ([#143](#143))
([6fc80e6](6fc80e6))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants