Skip to content

Commit 109fe9f

Browse files
committed
refactor(img): fold OciLoader.Params into Hyper.Img.OciLoader
1 parent f7eba1c commit 109fe9f

4 files changed

Lines changed: 103 additions & 117 deletions

File tree

lib/hyper/img/oci_loader.ex

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,17 @@ defmodule Hyper.Img.OciLoader do
3030

3131
alias Hyper.Config
3232
alias Hyper.Img.Db.{Blob, Image, ImageLayer, Repo}
33-
alias Hyper.Img.OciLoader.Params
3433

35-
@hash_chunk 2 * 1024 * 1024
34+
@mib 1024 * 1024
35+
@hash_chunk 2 * @mib
36+
37+
# ext4 metadata (inode tables, journal, reserved blocks) plus slack so the
38+
# rootfs always fits. Overhead scales with content -- a flat constant is far
39+
# too small for large images -- as 25% of content plus an 8 MiB base, never
40+
# below a 16 MiB floor. The base is a read-only dm-snapshot origin (guest
41+
# writes land in the COW layer, never here), so generous slack is cheap.
42+
@base_overhead_bytes 8 * @mib
43+
@floor_bytes 16 * @mib
3644

3745
@doc "Load `ref` into the store and DB. See the module doc. Label defaults to `ref`."
3846
@spec load(String.t()) :: {:ok, Hyper.Img.id()} | {:error, term()}
@@ -46,12 +54,12 @@ defmodule Hyper.Img.OciLoader do
4654
def load(ref, opts) when is_binary(ref) and is_list(opts) do
4755
label = Keyword.get(opts, :label, ref)
4856

49-
with {:ok, source} <- Params.source(ref),
57+
with {:ok, source} <- source(ref),
5058
{:ok, arch} <- Sys.Arch.current() do
5159
Sys.Tmp.with_tempdir("hyper-oci", fn tmp ->
52-
with {:ok, rootfs} <- pull_and_unpack(source, Params.goarch(arch), tmp),
60+
with {:ok, rootfs} <- pull_and_unpack(source, goarch(arch), tmp),
5361
{:ok, content} <- dir_bytes(rootfs),
54-
bytes = Params.ext4_bytes(content),
62+
bytes = ext4_bytes(content),
5563
{:ok, staged} <- build_ext4(rootfs, bytes),
5664
{:ok, id} <- finalize(staged, bytes, label) do
5765
{:ok, id}
@@ -74,6 +82,36 @@ defmodule Hyper.Img.OciLoader do
7482
if missing == [], do: :ok, else: {:error, {:missing_tools, missing}}
7583
end
7684

85+
# --- pure derivations (no I/O; the unit-tested core) ----------------------
86+
87+
# Validate `ref` and return the `skopeo` source `"docker://" <> ref`. A ref must
88+
# be non-empty and contain no whitespace (refs never do; rejecting whitespace
89+
# also closes the door on accidental arg-splitting surprises).
90+
@doc false
91+
@spec source(String.t()) :: {:ok, String.t()} | {:error, :invalid_ref}
92+
def source(ref) when is_binary(ref) do
93+
if ref != "" and not String.match?(ref, ~r/\s/),
94+
do: {:ok, "docker://" <> ref},
95+
else: {:error, :invalid_ref}
96+
end
97+
98+
# Map a Hyper architecture to the Go/OCI arch name `skopeo --override-arch` wants.
99+
@doc false
100+
@spec goarch(Sys.Arch.t()) :: String.t()
101+
def goarch(:x86_64), do: "amd64"
102+
def goarch(:aarch64), do: "arm64"
103+
104+
# ext4 image size (bytes) for a rootfs whose contents total `content_bytes`:
105+
# content + scaled overhead (25% of content + 8 MiB base), rounded up to a whole
106+
# MiB, never below 16 MiB.
107+
@doc false
108+
@spec ext4_bytes(non_neg_integer()) :: pos_integer()
109+
def ext4_bytes(content_bytes) when is_integer(content_bytes) and content_bytes >= 0 do
110+
raw = content_bytes + div(content_bytes, 4) + @base_overhead_bytes
111+
rounded = div(raw + @mib - 1, @mib) * @mib
112+
max(rounded, @floor_bytes)
113+
end
114+
77115
# --- pull + flatten -------------------------------------------------------
78116

79117
# `skopeo copy` into a local OCI layout, then `umoci unpack` into a bundle.

lib/hyper/img/oci_loader/params.ex

Lines changed: 0 additions & 47 deletions
This file was deleted.

test/hyper/img/oci_loader/params_test.exs

Lines changed: 0 additions & 61 deletions
This file was deleted.

test/hyper/img/oci_loader_test.exs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,70 @@
11
defmodule Hyper.Img.OciLoaderTest do
2-
# End-to-end: pulls a real (tiny) public image, builds the ext4 blob, and
3-
# records it in the DB. Opt-in -- needs skopeo, umoci, mke2fs, network, and a
4-
# running Postgres. Run with: mix test --include external
52
use ExUnit.Case, async: false
6-
@moduletag :external
3+
use ExUnitProperties
74

85
alias Hyper.Config
96
alias Hyper.Img.Db.{Blob, Repo}
107
alias Hyper.Img.OciLoader
118

9+
@mib 1024 * 1024
10+
11+
describe "source/1" do
12+
test "prefixes a valid ref with the docker transport" do
13+
assert OciLoader.source("docker.io/library/alpine:3.19") ==
14+
{:ok, "docker://docker.io/library/alpine:3.19"}
15+
16+
assert OciLoader.source("ghcr.io/foo/bar@sha256:abc") ==
17+
{:ok, "docker://ghcr.io/foo/bar@sha256:abc"}
18+
end
19+
20+
test "rejects empty, blank, or whitespace-bearing refs" do
21+
assert OciLoader.source("") == {:error, :invalid_ref}
22+
assert OciLoader.source(" ") == {:error, :invalid_ref}
23+
assert OciLoader.source("alpine 3.19") == {:error, :invalid_ref}
24+
assert OciLoader.source("alpine\n") == {:error, :invalid_ref}
25+
end
26+
end
27+
28+
describe "goarch/1" do
29+
test "maps Hyper arches to Go/OCI arch names" do
30+
assert OciLoader.goarch(:x86_64) == "amd64"
31+
assert OciLoader.goarch(:aarch64) == "arm64"
32+
end
33+
end
34+
35+
describe "ext4_bytes/1" do
36+
test "floors small inputs at 16 MiB" do
37+
assert OciLoader.ext4_bytes(0) == 16 * @mib
38+
assert OciLoader.ext4_bytes(1) == 16 * @mib
39+
end
40+
41+
test "leaves headroom above the content size, rounded up to a whole MiB" do
42+
size = OciLoader.ext4_bytes(100 * @mib)
43+
assert size > 100 * @mib
44+
assert rem(size, @mib) == 0
45+
end
46+
47+
test "scales overhead with content above the floor crossover" do
48+
# content small enough that 1.25x + 8 MiB stays under the 16 MiB floor
49+
assert OciLoader.ext4_bytes(4 * @mib) == 16 * @mib
50+
# content past the crossover gets proportional slack, not the floor
51+
assert OciLoader.ext4_bytes(64 * @mib) == 88 * @mib
52+
end
53+
54+
property "always a whole-MiB size that fits the content and clears the floor" do
55+
check all content <- integer(0..(8 * 1024 * @mib)) do
56+
size = OciLoader.ext4_bytes(content)
57+
assert rem(size, @mib) == 0
58+
assert size >= content
59+
assert size >= 16 * @mib
60+
end
61+
end
62+
end
63+
64+
# End-to-end: pulls a real (tiny) public image, builds the ext4 blob, and
65+
# records it in the DB. Opt-in -- needs skopeo, umoci, mke2fs, network, and a
66+
# running Postgres. Run with: mix test --include external
67+
@tag :external
1268
test "load/1 publishes a busybox base image to the store and DB" do
1369
assert OciLoader.test_system() == :ok
1470

0 commit comments

Comments
 (0)