Skip to content

Commit ec80e6c

Browse files
authored
Merge pull request linuxkit#3573 from deitch/dockerless
pull and cache images locally without docker
2 parents c1b02ee + 0b7502f commit ec80e6c

File tree

870 files changed

+19446
-189478
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

870 files changed

+19446
-189478
lines changed

docs/image-cache.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Image Caching
2+
3+
linuxkit builds each runtime OS image from a combination of Docker images.
4+
These images are pulled from a registry and cached locally.
5+
6+
linuxkit does not use the docker image cache to store these images. This is
7+
for two key reasons.
8+
9+
First, docker does not provide support for different architecture versions. For
10+
example, if you want to pull down `docker.io/library/alpine:3.11` by manifest,
11+
with its signature, but get the `arm64` version while you are on an `amd64` device,
12+
it is not supported.
13+
14+
Second, and more importantly, this requires a running docker daemon. Since the
15+
very essence of linuxkit is removing daemons and operating systems where unnecessary,
16+
just laying down bits in a file, removing docker from the image build process
17+
is valuable. It also simplifies many use cases, like CI, where a docker daemon
18+
may be unavailable.
19+
20+
## How LinuxKit Caches Images
21+
22+
LinuxKit pulls images down from a registry and stores them in a local cache.
23+
It stores the root manifest or index of the image, the manifest, and all of the layers
24+
for the requested architecture. It does not pull down layers, manifest or config
25+
for all available architectures, only the requested one. If none is requested, it
26+
defaults to the architecture on which you are running.
27+
28+
By default, LinuxKit caches images in `~/.linuxkit/cache/`. It can be changed
29+
via a command-line option. The structure of the cache directory matches the
30+
[OCI spec for image layout](http://github.com/opencontainers/image-spec/blob/master/image-layout.md).
31+
32+
Image names are kept in `index.json` in the [annotation](https://github.com/opencontainers/image-spec/blob/master/annotations.md) `org.opencontainers.image.ref.name`. For example"
33+
34+
```json
35+
{
36+
"schemaVersion": 2,
37+
"manifests": [
38+
{
39+
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
40+
"size": 1638,
41+
"digest": "sha256:9a839e63dad54c3a6d1834e29692c8492d93f90c59c978c1ed79109ea4fb9a54",
42+
"annotations": {
43+
"org.opencontainers.image.ref.name": "docker.io/library/alpine:3.11"
44+
}
45+
}
46+
]
47+
}
48+
```
49+
50+
## How LinuxKit Uses the Cache and Registry
51+
52+
For each image that linuxkit needs to read, it does the following. Note that if the `--pull` option
53+
is provided, it always will pull, independent of what is in the cache.
54+
55+
1. Check in the cache for the image name in the cache `index.json`. If it does not find it, pull it down and store it in cache, using content trust, if enabled.
56+
1. Read the root hash from `index.json`.
57+
1. Find the root blob in the `blobs/` directory via the hash and read it.
58+
1. Proceed to read the manifest, config and layers.
59+
60+
The read process is smart enough to check each blob in the local cache before downloading
61+
it from a registry.

docs/yaml.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ are downloaded at build time to create an image. The image is self-contained and
1111
so it can be tested reliably for continuous delivery.
1212

1313
Components are specified as Docker images which are pulled from a registry during build if they
14-
are not available locally. The Docker images are optionally verified with Docker Content Trust.
14+
are not available locally. See [image-cache](./image-cache.md) for more details on local caching.
15+
The Docker images are optionally verified with Docker Content Trust.
1516
For private registries or private repositories on a registry credentials provided via
1617
`docker login` are re-used.
1718

src/cmd/linuxkit/build.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"os"
1111
"path/filepath"
12+
"runtime"
1213
"strings"
1314

1415
"github.com/linuxkit/linuxkit/src/cmd/linuxkit/moby"
@@ -48,9 +49,12 @@ func build(args []string) {
4849
buildOutputFile := buildCmd.String("o", "", "File to use for a single output, or '-' for stdout")
4950
buildSize := buildCmd.String("size", "1024M", "Size for output image, if supported and fixed size")
5051
buildPull := buildCmd.Bool("pull", false, "Always pull images")
52+
buildDocker := buildCmd.Bool("docker", false, "Check for images in docker before linuxkit cache")
5153
buildDisableTrust := buildCmd.Bool("disable-content-trust", false, "Skip image trust verification specified in trust section of config (default false)")
5254
buildDecompressKernel := buildCmd.Bool("decompress-kernel", false, "Decompress the Linux kernel (default false)")
55+
buildCacheDir := buildCmd.String("cache", defaultLinuxkitCache(), "Directory for caching and finding cached image")
5356
buildCmd.Var(&buildFormats, "format", "Formats to create [ "+strings.Join(outputTypes, " ")+" ]")
57+
buildArch := buildCmd.String("arch", runtime.GOARCH, "target architecture for which to build")
5458

5559
if err := buildCmd.Parse(args); err != nil {
5660
log.Fatal("Unable to parse args")
@@ -95,6 +99,8 @@ func build(args []string) {
9599
}
96100
}
97101

102+
cacheDir := *buildCacheDir
103+
98104
if len(buildFormats) == 1 && moby.Streamable(buildFormats[0]) {
99105
if *buildOutputFile == "" {
100106
*buildOutputFile = filepath.Join(*buildDir, name+"."+buildFormats[0])
@@ -103,7 +109,7 @@ func build(args []string) {
103109
*buildDir = ""
104110
}
105111
} else {
106-
err := moby.ValidateFormats(buildFormats)
112+
err := moby.ValidateFormats(buildFormats, cacheDir)
107113
if err != nil {
108114
log.Errorf("Error parsing formats: %v", err)
109115
buildCmd.Usage()
@@ -175,6 +181,7 @@ func build(args []string) {
175181
if err != nil {
176182
log.Fatalf("Invalid config: %v", err)
177183
}
184+
c.Architecture = *buildArch
178185
m, err = moby.AppendConfig(m, c)
179186
if err != nil {
180187
log.Fatalf("Cannot append config files: %v", err)
@@ -204,7 +211,7 @@ func build(args []string) {
204211
if moby.Streamable(buildFormats[0]) {
205212
tp = buildFormats[0]
206213
}
207-
err = moby.Build(m, w, *buildPull, tp, *buildDecompressKernel)
214+
err = moby.Build(m, w, *buildPull, tp, *buildDecompressKernel, cacheDir, *buildDocker)
208215
if err != nil {
209216
log.Fatalf("%v", err)
210217
}
@@ -216,7 +223,7 @@ func build(args []string) {
216223
}
217224

218225
log.Infof("Create outputs:")
219-
err = moby.Formats(filepath.Join(*buildDir, name), image, buildFormats, size, !*buildDisableTrust)
226+
err = moby.Formats(filepath.Join(*buildDir, name), image, buildFormats, size, !*buildDisableTrust, cacheDir)
220227
if err != nil {
221228
log.Fatalf("Error writing outputs: %v", err)
222229
}

src/cmd/linuxkit/cache.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/linuxkit/linuxkit/src/cmd/linuxkit/util"
9+
log "github.com/sirupsen/logrus"
10+
)
11+
12+
func cacheUsage() {
13+
invoked := filepath.Base(os.Args[0])
14+
fmt.Printf("USAGE: %s cache command [options]\n\n", invoked)
15+
fmt.Printf("Supported commands are\n")
16+
// Please keep these in alphabetical order
17+
fmt.Printf(" clean\n")
18+
fmt.Printf(" ls\n")
19+
fmt.Printf("\n")
20+
fmt.Printf("'options' are the backend specific options.\n")
21+
fmt.Printf("See '%s cache [command] --help' for details.\n\n", invoked)
22+
}
23+
24+
// Process the cache
25+
func cache(args []string) {
26+
if len(args) < 1 {
27+
cacheUsage()
28+
os.Exit(1)
29+
}
30+
switch args[0] {
31+
// Please keep cases in alphabetical order
32+
case "clean":
33+
cacheClean(args[1:])
34+
case "ls":
35+
cacheList(args[1:])
36+
case "help", "-h", "-help", "--help":
37+
cacheUsage()
38+
os.Exit(0)
39+
default:
40+
log.Errorf("No 'cache' command specified.")
41+
}
42+
}
43+
44+
func defaultLinuxkitCache() string {
45+
lktDir := ".linuxkit"
46+
home := util.HomeDir()
47+
return filepath.Join(home, lktDir, "cache")
48+
}

src/cmd/linuxkit/cache/find.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package cache
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/google/go-containerregistry/pkg/v1"
7+
"github.com/google/go-containerregistry/pkg/v1/layout"
8+
"github.com/google/go-containerregistry/pkg/v1/match"
9+
"github.com/google/go-containerregistry/pkg/v1/partial"
10+
)
11+
12+
// matchPlatformsOSArch because match.Platforms rejects it if the provided
13+
// v1.Platform has a variant of "" but the actual index has a specific one.
14+
// This becomes an issue with arm64 vs arm64/v8. So this matches only on OS
15+
// and Architecture.
16+
func matchPlatformsOSArch(platforms ...v1.Platform) match.Matcher {
17+
return func(desc v1.Descriptor) bool {
18+
if desc.Platform == nil {
19+
return false
20+
}
21+
for _, platform := range platforms {
22+
if desc.Platform.OS == platform.OS && desc.Platform.Architecture == platform.Architecture {
23+
return true
24+
}
25+
}
26+
return false
27+
}
28+
}
29+
30+
func findImage(p layout.Path, imageName, architecture string) (v1.Image, error) {
31+
root, err := findRootFromLayout(p, imageName)
32+
if err != nil {
33+
return nil, err
34+
}
35+
img, err := root.Image()
36+
if err == nil {
37+
return img, nil
38+
}
39+
ii, err := root.ImageIndex()
40+
if err == nil {
41+
// we have the index, get the manifest that represents the manifest for the desired architecture
42+
platform := v1.Platform{OS: "linux", Architecture: architecture}
43+
images, err := partial.FindImages(ii, matchPlatformsOSArch(platform))
44+
if err != nil || len(images) < 1 {
45+
return nil, fmt.Errorf("error retrieving image %s for platform %v from cache: %v", imageName, platform, err)
46+
}
47+
return images[0], nil
48+
}
49+
return nil, fmt.Errorf("no image found for %s", imageName)
50+
}

src/cmd/linuxkit/cache/image.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package cache
2+
3+
import (
4+
"github.com/google/go-containerregistry/pkg/v1/layout"
5+
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
6+
)
7+
8+
// ListImages list the named images and their root digests from a layout.Path
9+
func ListImages(p layout.Path) (map[string]string, error) {
10+
ii, err := p.ImageIndex()
11+
if err != nil {
12+
return nil, err
13+
}
14+
index, err := ii.IndexManifest()
15+
if err != nil {
16+
return nil, err
17+
}
18+
names := map[string]string{}
19+
for _, i := range index.Manifests {
20+
if i.Annotations == nil {
21+
continue
22+
}
23+
if name, ok := i.Annotations[imagespec.AnnotationRefName]; ok {
24+
names[name] = i.Digest.String()
25+
}
26+
}
27+
return names, nil
28+
}

src/cmd/linuxkit/cache/open.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package cache
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/google/go-containerregistry/pkg/v1/empty"
7+
"github.com/google/go-containerregistry/pkg/v1/layout"
8+
)
9+
10+
// Get get or initialize the cache
11+
func Get(cache string) (layout.Path, error) {
12+
// initialize the cache path if needed
13+
p, err := layout.FromPath(cache)
14+
if err != nil {
15+
p, err = layout.Write(cache, empty.Index)
16+
if err != nil {
17+
return p, fmt.Errorf("could not initialize cache at path %s: %v", cache, err)
18+
}
19+
}
20+
return p, nil
21+
}

src/cmd/linuxkit/cache/pull.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package cache
2+
3+
import (
4+
"errors"
5+
6+
"github.com/containerd/containerd/reference"
7+
"github.com/google/go-containerregistry/pkg/v1"
8+
"github.com/google/go-containerregistry/pkg/v1/validate"
9+
)
10+
11+
// ValidateImage given a reference, validate that it is complete. If not, pull down missing
12+
// components as necessary.
13+
func ValidateImage(ref *reference.Spec, cacheDir, architecture string) (ImageSource, error) {
14+
var (
15+
imageIndex v1.ImageIndex
16+
image v1.Image
17+
imageName = ref.String()
18+
)
19+
// next try the local cache
20+
root, err := FindRoot(cacheDir, imageName)
21+
if err == nil {
22+
img, err := root.Image()
23+
if err == nil {
24+
image = img
25+
} else {
26+
ii, err := root.ImageIndex()
27+
if err == nil {
28+
imageIndex = ii
29+
}
30+
}
31+
}
32+
// three possibilities now:
33+
// - we did not find anything locally
34+
// - we found an index locally
35+
// - we found an image locally
36+
switch {
37+
case imageIndex == nil && image == nil:
38+
// we did not find it yet - either because we were told not to look locally,
39+
// or because it was not available - so get it from the remote
40+
return ImageSource{}, errors.New("no such image")
41+
case imageIndex != nil:
42+
// we found a local index, just make sure it is up to date and, if not, download it
43+
if err := validate.Index(imageIndex); err == nil {
44+
return NewSource(
45+
ref,
46+
cacheDir,
47+
architecture,
48+
), nil
49+
}
50+
return ImageSource{}, errors.New("invalid index")
51+
case image != nil:
52+
// we found a local image, just make sure it is up to date
53+
if err := validate.Image(image); err == nil {
54+
return NewSource(
55+
ref,
56+
cacheDir,
57+
architecture,
58+
), nil
59+
}
60+
return ImageSource{}, errors.New("invalid image")
61+
}
62+
// if we made it to here, we had some strange error
63+
return ImageSource{}, errors.New("should not have reached this point, image index and image were both empty and not-empty")
64+
}

0 commit comments

Comments
 (0)