Skip to content

Commit 0b7ae64

Browse files
committed
Add "Builder: oci-import" support
In the case of base images (`debian`, `alpine`, `ubuntu`, etc), using a `Dockerfile` as our method of ingestion doesn't really buy us very much. It made sense at the time it was implemented ("all `Dockerfile`, all the time"), but at this point they're all some variation on `FROM scratch \n ADD foo.tar.xz / \n CMD ["/bin/some-shell"]`, and cannot reasonably be "rebuilt" when their base image changes (which is one of the key functions of the official images) since they _are_ the base images in question. Functionally, consuming a tarball in this way isn't _that_ much different from consuming a raw tarball that's part of, say, an OCI image layout (https://github.com/opencontainers/image-spec/blob/v1.0.2/image-layout.md) -- it's some tarball plus some metadata about what to do with it. For less trivial images, there's a significant difference (and I'm not proposing to use this for anything beyond simple one-layer base images), but for a single layer this would be basically identical. As a more specific use case, the Debian `rootfs.tar.xz` files are currently [100% reproducible](https://github.com/debuerreotype/debuerreotype). Unfortunately, some of that gets lost when it gets imported into Docker, and thus it takes some additional effort to get from the Docker-generated rootfs back to the original debuerreotype-generated file. This adds the ability to consume an OCI image directly, to go even further and have a 100% fully reproducible image digest as well, which makes it easier to trace a given published image back to the reproducible source generated by the upstream tooling (especially if a given image is also pushed by the maintainer elsewhere). Here's an example `oci-debian` file I was using for testing this: Maintainers: Foo (@bar) GitRepo: https://github.com/tianon/docker-debian-artifacts.git GitFetch: refs/heads/oci-arm32v5 Architectures: arm32v5 GitCommit: d6ac440e7760b6b16e3d3da6f2b56736b9c10065 Builder: oci-import File: index.json Tags: bullseye, bullseye-20221114, 11.5, 11, latest Directory: bullseye/oci Tags: bullseye-slim, bullseye-20221114-slim, 11.5-slim, 11-slim Directory: bullseye/slim/oci
1 parent 18db6c5 commit 0b7ae64

File tree

15 files changed

+1753
-86
lines changed

15 files changed

+1753
-86
lines changed

cmd/bashbrew/cmd-build.go

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ func cmdBuild(c *cli.Context) error {
7676
if err != nil {
7777
return cli.NewMultiError(fmt.Errorf(`failed calculating "cache hash" for %q (tags %q)`, r.RepoName, entry.TagsString()), err)
7878
}
79+
imageTags := r.Tags(namespace, uniq, entry)
80+
tags := append([]string{cacheTag}, imageTags...)
7981

8082
// check whether we've already built this artifact
8183
_, err = dockerInspect("{{.Id}}", cacheTag)
@@ -87,51 +89,50 @@ func cmdBuild(c *cli.Context) error {
8789
return cli.NewMultiError(fmt.Errorf(`failed fetching git repo for %q (tags %q)`, r.RepoName, entry.TagsString()), err)
8890
}
8991

90-
archive, err := gitArchive(commit, entry.ArchDirectory(arch))
91-
if err != nil {
92-
return cli.NewMultiError(fmt.Errorf(`failed generating git archive for %q (tags %q)`, r.RepoName, entry.TagsString()), err)
93-
}
94-
defer archive.Close()
95-
96-
// TODO use "meta.StageNames" to do "docker build --target" so we can tag intermediate stages too for cache (streaming "git archive" directly to "docker build" makes that a little hard to accomplish without re-streaming)
97-
9892
switch builder := entry.ArchBuilder(arch); builder {
99-
case "classic", "":
93+
case "buildkit", "classic", "":
10094
var platform string
10195
if fromScratch {
10296
platform = ociArch.String()
10397
}
104-
err = dockerBuild(cacheTag, entry.ArchFile(arch), archive, platform)
98+
99+
archive, err := gitArchive(commit, entry.ArchDirectory(arch))
105100
if err != nil {
106-
return cli.NewMultiError(fmt.Errorf(`failed building %q (tags %q)`, r.RepoName, entry.TagsString()), err)
101+
return cli.NewMultiError(fmt.Errorf(`failed generating git archive for %q (tags %q)`, r.RepoName, entry.TagsString()), err)
107102
}
108-
case "buildkit":
109-
var platform string
110-
if fromScratch {
111-
platform = ociArch.String()
103+
defer archive.Close()
104+
105+
if builder == "buildkit" {
106+
err = dockerBuildxBuild(tags, entry.ArchFile(arch), archive, platform)
107+
} else {
108+
// TODO use "meta.StageNames" to do "docker build --target" so we can tag intermediate stages too for cache (streaming "git archive" directly to "docker build" makes that a little hard to accomplish without re-streaming)
109+
err = dockerBuild(tags, entry.ArchFile(arch), archive, platform)
112110
}
113-
err = dockerBuildxBuild(cacheTag, entry.ArchFile(arch), archive, platform)
114111
if err != nil {
115112
return cli.NewMultiError(fmt.Errorf(`failed building %q (tags %q)`, r.RepoName, entry.TagsString()), err)
116113
}
114+
115+
archive.Close() // be sure this happens sooner rather than later (defer might take a while, and we want to reap zombies more aggressively)
116+
117+
case "oci-import":
118+
err := ociImportBuild(tags, commit, entry.ArchDirectory(arch), entry.ArchFile(arch))
119+
if err != nil {
120+
return cli.NewMultiError(fmt.Errorf(`failed oci-import build of %q (tags %q)`, r.RepoName, entry.TagsString()), err)
121+
}
122+
123+
fmt.Printf("Importing %s into Docker\n", r.EntryIdentifier(entry))
124+
err = ociImportDockerLoad(imageTags)
125+
if err != nil {
126+
return cli.NewMultiError(fmt.Errorf(`failed oci-import into Docker of %q (tags %q)`, r.RepoName, entry.TagsString()), err)
127+
}
128+
117129
default:
118130
return cli.NewMultiError(fmt.Errorf(`unknown builder %q`, builder))
119131
}
120-
archive.Close() // be sure this happens sooner rather than later (defer might take a while, and we want to reap zombies more aggressively)
121132
}
122133
} else {
123134
fmt.Printf("Using %s (%s)\n", cacheTag, r.EntryIdentifier(entry))
124135
}
125-
126-
for _, tag := range r.Tags(namespace, uniq, entry) {
127-
fmt.Printf("Tagging %s\n", tag)
128-
if !dryRun {
129-
err := dockerTag(cacheTag, tag)
130-
if err != nil {
131-
return cli.NewMultiError(fmt.Errorf(`failed tagging %q as %q`, cacheTag, tag), err)
132-
}
133-
}
134-
}
135136
}
136137
}
137138

cmd/bashbrew/cmd-push.go

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path"
7+
"strings"
78

89
"github.com/urfave/cli"
910
)
@@ -38,29 +39,69 @@ func cmdPush(c *cli.Context) error {
3839
continue
3940
}
4041

42+
tags := []string{}
4143
// we can't use "r.Tags()" here because it will include SharedTags, which we never want to push directly (see "cmd-put-shared.go")
42-
TagsLoop:
4344
for i, tag := range entry.Tags {
4445
if uniq && i > 0 {
4546
break
4647
}
4748
tag = tagRepo + ":" + tag
49+
tags = append(tags, tag)
50+
}
4851

49-
if !force {
50-
localImageId, _ := dockerInspect("{{.Id}}", tag)
51-
registryImageIds := fetchRegistryImageIds(tag)
52-
for _, registryImageId := range registryImageIds {
53-
if localImageId == registryImageId {
54-
fmt.Fprintf(os.Stderr, "skipping %s (remote image matches local)\n", tag)
55-
continue TagsLoop
56-
}
57-
}
52+
switch builder := entry.ArchBuilder(arch); builder {
53+
case "oci-import":
54+
cacheTag, err := r.DockerCacheName(entry)
55+
if err != nil {
56+
return cli.NewMultiError(fmt.Errorf(`failed calculating "cache hash" for %q (tags %q)`, r.RepoName, entry.TagsString()), err)
57+
}
58+
desc, err := ociImportLookup(cacheTag)
59+
if err != nil {
60+
return cli.NewMultiError(fmt.Errorf(`failed looking up descriptor for %q (tags %q)`, r.RepoName, entry.TagsString()), err)
5861
}
59-
fmt.Printf("Pushing %s\n", tag)
62+
skip, update, err := ociImportPushFilter(*desc, tags)
63+
if err != nil {
64+
return cli.NewMultiError(fmt.Errorf(`failed looking up tags for %q (tags %q)`, r.RepoName, entry.TagsString()), err)
65+
}
66+
if len(skip) > 0 && len(update) == 0 {
67+
fmt.Fprintf(os.Stderr, "skipping %s (remote tags all up-to-date)\n", r.EntryIdentifier(entry))
68+
continue
69+
} else if len(skip) > 0 {
70+
fmt.Fprintf(os.Stderr, "partially skipping %s (remote tags up-to-date: %s)\n", r.EntryIdentifier(entry), strings.Join(skip, ", "))
71+
}
72+
fmt.Printf("Pushing %s to %s\n", desc.Digest, strings.Join(update, ", "))
6073
if !dryRun {
61-
err = dockerPush(tag)
74+
err := ociImportPush(*desc, update)
6275
if err != nil {
63-
return cli.NewMultiError(fmt.Errorf(`failed pushing %q`, tag), err)
76+
return cli.NewMultiError(fmt.Errorf(`failed pushing %q`, r.EntryIdentifier(entry)), err)
77+
}
78+
}
79+
80+
default:
81+
TagsLoop:
82+
for _, tag := range tags {
83+
if !force {
84+
localImageId, _ := dockerInspect("{{.Id}}", tag)
85+
if debugFlag {
86+
fmt.Printf("DEBUG: docker inspect %q -> %q\n", tag, localImageId)
87+
}
88+
registryImageIds := fetchRegistryImageIds(tag)
89+
if debugFlag {
90+
fmt.Printf("DEBUG: registry inspect %q -> %+v\n", tag, registryImageIds)
91+
}
92+
for _, registryImageId := range registryImageIds {
93+
if localImageId == registryImageId {
94+
fmt.Fprintf(os.Stderr, "skipping %s (remote image matches local)\n", tag)
95+
continue TagsLoop
96+
}
97+
}
98+
}
99+
fmt.Printf("Pushing %s\n", tag)
100+
if !dryRun {
101+
err = dockerPush(tag)
102+
if err != nil {
103+
return cli.NewMultiError(fmt.Errorf(`failed pushing %q`, tag), err)
104+
}
64105
}
65106
}
66107
}

cmd/bashbrew/containerd.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"time"
8+
9+
"github.com/containerd/containerd"
10+
"github.com/containerd/containerd/content/local"
11+
"github.com/containerd/containerd/metadata"
12+
"github.com/containerd/containerd/namespaces"
13+
14+
"go.etcd.io/bbolt"
15+
)
16+
17+
func newBuiltinContainerdServices(ctx context.Context) (containerd.ClientOpt, error) {
18+
// thanks to https://github.com/Azure/image-rootfs-scanner/blob/e7041e47d1a13e15d73d9c85644542e6758f9f3a/containerd.go#L42-L87 for inspiring this magic
19+
20+
root := filepath.Join(defaultCache, "containerd")
21+
dbPath := filepath.Join(root, "metadata.db")
22+
contentRoot := filepath.Join(root, "content")
23+
24+
cs, err := local.NewStore(contentRoot)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
db, err := bbolt.Open(dbPath, 0600, &bbolt.Options{
30+
Timeout: 1 * time.Minute,
31+
})
32+
33+
mdb := metadata.NewDB(db, cs, nil)
34+
return containerd.WithServices(
35+
containerd.WithContentStore(mdb.ContentStore()),
36+
containerd.WithImageStore(metadata.NewImageStore(mdb)),
37+
containerd.WithLeasesService(metadata.NewLeaseManager(mdb)),
38+
), nil
39+
}
40+
41+
var containerdClientCache *containerd.Client = nil
42+
43+
// the returned client is cached, don't Close() it!
44+
func newContainerdClient(ctx context.Context) (context.Context, *containerd.Client, error) {
45+
ns := "bashbrew"
46+
for _, envKey := range []string{
47+
`BASHBREW_CONTAINERD_NAMESPACE`,
48+
`CONTAINERD_NAMESPACE`,
49+
} {
50+
if env, ok := os.LookupEnv(envKey); ok {
51+
if env != "" {
52+
// set-but-empty environment variable means use default explicitly
53+
ns = env
54+
}
55+
break
56+
}
57+
}
58+
ctx = namespaces.WithNamespace(ctx, ns)
59+
60+
if containerdClientCache != nil {
61+
return ctx, containerdClientCache, nil
62+
}
63+
64+
for _, envKey := range []string{
65+
`BASHBREW_CONTAINERD_CONTENT_ADDRESS`, // TODO if we ever need to connnect to a containerd instance for something more interesting like running containers, we need to have *that* codepath not use _CONTENT_ variants
66+
`BASHBREW_CONTAINERD_ADDRESS`,
67+
`CONTAINERD_CONTENT_ADDRESS`,
68+
`CONTAINERD_ADDRESS`,
69+
} {
70+
if socket, ok := os.LookupEnv(envKey); ok {
71+
if socket == "" {
72+
// we'll use a set-but-empty variable as an explicit request to use our built-in implementation
73+
break
74+
}
75+
client, err := containerd.New(socket)
76+
containerdClientCache = client
77+
return ctx, client, err
78+
}
79+
}
80+
81+
// if we don't have an explicit variable asking us to connect to an existing containerd instance, we set up and use our own in-process content/image store
82+
services, err := newBuiltinContainerdServices(ctx)
83+
if err != nil {
84+
return ctx, nil, err
85+
}
86+
client, err := containerd.New("", services)
87+
containerdClientCache = client
88+
return ctx, client, err
89+
}

cmd/bashbrew/docker.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ func (r Repo) dockerfileMetadata(entry *manifest.Manifest2822Entry) (*dockerfile
5353
var dockerfileMetadataCache = map[string]*dockerfileMetadata{}
5454

5555
func (r Repo) archDockerfileMetadata(arch string, entry *manifest.Manifest2822Entry) (*dockerfileMetadata, error) {
56+
if builder := entry.ArchBuilder(arch); builder == "oci-import" {
57+
return &dockerfileMetadata{
58+
Froms: []string{
59+
"scratch",
60+
},
61+
}, nil
62+
}
63+
5664
commit, err := r.fetchGitRepo(arch, entry)
5765
if err != nil {
5866
return nil, cli.NewMultiError(fmt.Errorf("failed fetching Git repo for arch %q from entry %q", arch, entry.String()), err)
@@ -242,9 +250,13 @@ func (r Repo) dockerBuildUniqueBits(entry *manifest.Manifest2822Entry) ([]string
242250
return uniqueBits, nil
243251
}
244252

245-
func dockerBuild(tag string, file string, context io.Reader, platform string) error {
246-
args := []string{"build", "--tag", tag, "--file", file, "--rm", "--force-rm"}
247-
args = append(args, "-")
253+
func dockerBuild(tags []string, file string, context io.Reader, platform string) error {
254+
args := []string{"build"}
255+
for _, tag := range tags {
256+
args = append(args, "--tag", tag)
257+
}
258+
args = append(args, "--file", file, "--rm", "--force-rm", "-")
259+
248260
cmd := exec.Command("docker", args...)
249261
cmd.Env = append(os.Environ(), "DOCKER_BUILDKIT=0")
250262
if debugFlag {
@@ -278,7 +290,7 @@ func dockerBuild(tag string, file string, context io.Reader, platform string) er
278290

279291
const dockerfileSyntaxEnv = "BASHBREW_BUILDKIT_SYNTAX"
280292

281-
func dockerBuildxBuild(tag string, file string, context io.Reader, platform string) error {
293+
func dockerBuildxBuild(tags []string, file string, context io.Reader, platform string) error {
282294
dockerfileSyntax, ok := os.LookupEnv(dockerfileSyntaxEnv)
283295
if !ok {
284296
return fmt.Errorf("missing %q", dockerfileSyntaxEnv)
@@ -289,13 +301,14 @@ func dockerBuildxBuild(tag string, file string, context io.Reader, platform stri
289301
"build",
290302
"--progress", "plain",
291303
"--build-arg", "BUILDKIT_SYNTAX=" + dockerfileSyntax,
292-
"--tag", tag,
293-
"--file", file,
294304
}
295305
if platform != "" {
296306
args = append(args, "--platform", platform)
297307
}
298-
args = append(args, "-")
308+
for _, tag := range tags {
309+
args = append(args, "--tag", tag)
310+
}
311+
args = append(args, "--file", file, "-")
299312

300313
cmd := exec.Command("docker", args...)
301314
cmd.Stdin = context

cmd/bashbrew/git.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"fmt"
55
"io"
6+
"io/fs"
67
"io/ioutil"
78
"os"
89
"os/exec"
@@ -15,6 +16,7 @@ import (
1516

1617
"github.com/docker-library/bashbrew/manifest"
1718
"github.com/docker-library/bashbrew/pkg/execpipe"
19+
"github.com/docker-library/bashbrew/pkg/gitfs"
1820

1921
goGit "github.com/go-git/go-git/v5"
2022
goGitConfig "github.com/go-git/go-git/v5/config"
@@ -94,6 +96,13 @@ func getGitCommit(commit string) (string, error) {
9496
return h.String(), nil
9597
}
9698

99+
func gitCommitFS(commit string) (fs.FS, error) {
100+
if err := ensureGitInit(); err != nil {
101+
return nil, err
102+
}
103+
return gitfs.CommitHash(gitRepo, commit)
104+
}
105+
97106
func gitStream(args ...string) (io.ReadCloser, error) {
98107
return execpipe.Run(gitCommand(args...))
99108
}

cmd/bashbrew/main.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ func main() {
167167
if !debugFlag {
168168
// containerd uses logrus, but it defaults to "info" (which is a bit leaky where we use containerd)
169169
logrus.SetLevel(logrus.WarnLevel)
170+
} else {
171+
logrus.SetLevel(logrus.DebugLevel)
170172
}
171173

172174
arch = c.GlobalString("arch")
@@ -401,9 +403,9 @@ func main() {
401403
Category: "plumbing",
402404
},
403405
{
404-
Name: "remote",
405-
Usage: "query registries for bashbrew-related data",
406-
Before: subcommandBeforeFactory("remote"),
406+
Name: "remote",
407+
Usage: "query registries for bashbrew-related data",
408+
Before: subcommandBeforeFactory("remote"),
407409
Category: "plumbing",
408410
Subcommands: []cli.Command{
409411
{

0 commit comments

Comments
 (0)