Skip to content

Commit ceb0580

Browse files
thesayynimjasonh
andauthored
feat: implement gc command (#1811)
* feat: implement prune flag * address changes * revert * boilerplate * rename * boilerplate * Update .gitattributes --------- Co-authored-by: Jason Hall <[email protected]>
1 parent 5a53a12 commit ceb0580

16 files changed

+376
-2
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
**/zz_deepcopy_generated.go linguist-generated=true
66
cmd/crane/doc/crane*.md linguist-generated=true
77
go.sum linguist-generated=true
8+
**/testdata/** ignore-lint=true

cmd/crane/cmd/gc.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2018 Google LLC All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"fmt"
19+
"os"
20+
21+
"github.com/google/go-containerregistry/pkg/v1/layout"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
func NewCmdLayout() *cobra.Command {
26+
cmd := &cobra.Command{
27+
Use: "layout",
28+
}
29+
cmd.AddCommand(newCmdGc())
30+
return cmd
31+
}
32+
33+
// NewCmdGc creates a new cobra.Command for the pull subcommand.
34+
func newCmdGc() *cobra.Command {
35+
cmd := &cobra.Command{
36+
Use: "gc OCI-LAYOUT",
37+
Short: "Garbage collect unreferenced blobs in a local oci-layout",
38+
Args: cobra.ExactArgs(1),
39+
Hidden: true, // TODO: promote to public once theres some milage
40+
RunE: func(_ *cobra.Command, args []string) error {
41+
path := args[0]
42+
43+
p, err := layout.FromPath(path)
44+
45+
if err != nil {
46+
return err
47+
}
48+
49+
blobs, err := p.GarbageCollect()
50+
if err != nil {
51+
return err
52+
}
53+
54+
for _, blob := range blobs {
55+
if err := p.RemoveBlob(blob); err != nil {
56+
return err
57+
}
58+
fmt.Fprintf(os.Stderr, "garbage collecting: %s\n", blob.String())
59+
}
60+
61+
return nil
62+
},
63+
}
64+
65+
return cmd
66+
}

cmd/crane/cmd/root.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ func New(use, short string, options []crane.Option) *cobra.Command {
129129
NewCmdTag(&options),
130130
NewCmdValidate(&options),
131131
NewCmdVersion(),
132-
newCmdRegistry(),
132+
NewCmdRegistry(),
133+
NewCmdLayout(),
133134
)
134135

135136
root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logs")

cmd/crane/cmd/serve.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828
"github.com/google/go-containerregistry/pkg/registry"
2929
)
3030

31-
func newCmdRegistry() *cobra.Command {
31+
func NewCmdRegistry() *cobra.Command {
3232
cmd := &cobra.Command{
3333
Use: "registry",
3434
}

pkg/v1/layout/gc.go

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2018 Google LLC All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// This is an EXPERIMENTAL package, and may change in arbitrary ways without notice.
16+
package layout
17+
18+
import (
19+
"fmt"
20+
"io/fs"
21+
"path/filepath"
22+
"strings"
23+
24+
v1 "github.com/google/go-containerregistry/pkg/v1"
25+
)
26+
27+
// GarbageCollect removes unreferenced blobs from the oci-layout
28+
//
29+
// This is an experimental api, and not subject to any stability guarantees
30+
// We may abandon it at any time, without prior notice.
31+
// Deprecated: Use it at your own risk!
32+
func (l Path) GarbageCollect() ([]v1.Hash, error) {
33+
idx, err := l.ImageIndex()
34+
if err != nil {
35+
return nil, err
36+
}
37+
blobsToKeep := map[string]bool{}
38+
if err := l.garbageCollectImageIndex(idx, blobsToKeep); err != nil {
39+
return nil, err
40+
}
41+
blobsDir := l.path("blobs")
42+
removedBlobs := []v1.Hash{}
43+
44+
err = filepath.WalkDir(blobsDir, func(path string, d fs.DirEntry, err error) error {
45+
if err != nil {
46+
return err
47+
}
48+
49+
if d.IsDir() {
50+
return nil
51+
}
52+
53+
rel, err := filepath.Rel(blobsDir, path)
54+
if err != nil {
55+
return err
56+
}
57+
hashString := strings.Replace(rel, "/", ":", 1)
58+
if present := blobsToKeep[hashString]; !present {
59+
h, err := v1.NewHash(hashString)
60+
if err != nil {
61+
return err
62+
}
63+
removedBlobs = append(removedBlobs, h)
64+
}
65+
return nil
66+
})
67+
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
return removedBlobs, nil
73+
}
74+
75+
func (l Path) garbageCollectImageIndex(index v1.ImageIndex, blobsToKeep map[string]bool) error {
76+
idxm, err := index.IndexManifest()
77+
if err != nil {
78+
return err
79+
}
80+
81+
h, err := index.Digest()
82+
if err != nil {
83+
return err
84+
}
85+
86+
blobsToKeep[h.String()] = true
87+
88+
for _, descriptor := range idxm.Manifests {
89+
if descriptor.MediaType.IsImage() {
90+
img, err := index.Image(descriptor.Digest)
91+
if err != nil {
92+
return err
93+
}
94+
if err := l.garbageCollectImage(img, blobsToKeep); err != nil {
95+
return err
96+
}
97+
} else if descriptor.MediaType.IsIndex() {
98+
idx, err := index.ImageIndex(descriptor.Digest)
99+
if err != nil {
100+
return err
101+
}
102+
if err := l.garbageCollectImageIndex(idx, blobsToKeep); err != nil {
103+
return err
104+
}
105+
} else {
106+
return fmt.Errorf("gc: unknown media type: %s", descriptor.MediaType)
107+
}
108+
}
109+
return nil
110+
}
111+
112+
func (l Path) garbageCollectImage(image v1.Image, blobsToKeep map[string]bool) error {
113+
h, err := image.Digest()
114+
if err != nil {
115+
return err
116+
}
117+
blobsToKeep[h.String()] = true
118+
119+
h, err = image.ConfigName()
120+
if err != nil {
121+
return err
122+
}
123+
blobsToKeep[h.String()] = true
124+
125+
ls, err := image.Layers()
126+
if err != nil {
127+
return err
128+
}
129+
for _, l := range ls {
130+
h, err := l.Digest()
131+
if err != nil {
132+
return err
133+
}
134+
blobsToKeep[h.String()] = true
135+
}
136+
return nil
137+
}

pkg/v1/layout/gc_test.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2018 Google LLC All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package layout
16+
17+
import (
18+
"path/filepath"
19+
"testing"
20+
)
21+
22+
var (
23+
gcIndexPath = filepath.Join("testdata", "test_gc_index")
24+
gcIndexBlobHash = "sha256:492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0"
25+
gcUnknownMediaTypePath = filepath.Join("testdata", "test_gc_image_unknown_mediatype")
26+
gcUnknownMediaTypeErr = "gc: unknown media type: application/vnd.oci.descriptor.v1+json"
27+
gcTestOneImagePath = filepath.Join("testdata", "test_index_one_image")
28+
gcTestIndexMediaTypePath = filepath.Join("testdata", "test_index_media_type")
29+
)
30+
31+
func TestGcIndex(t *testing.T) {
32+
lp, err := FromPath(gcIndexPath)
33+
if err != nil {
34+
t.Fatalf("FromPath() = %v", err)
35+
}
36+
37+
removed, err := lp.GarbageCollect()
38+
if err != nil {
39+
t.Fatalf("GarbageCollect() = %v", err)
40+
}
41+
42+
if len(removed) != 1 {
43+
t.Fatalf("expected to have only one gc-able blob")
44+
}
45+
if removed[0].String() != gcIndexBlobHash {
46+
t.Fatalf("wrong blob is gc-ed: expected '%s', got '%s'", gcIndexBlobHash, removed[0].String())
47+
}
48+
}
49+
50+
func TestGcOneImage(t *testing.T) {
51+
lp, err := FromPath(gcTestOneImagePath)
52+
if err != nil {
53+
t.Fatalf("FromPath() = %v", err)
54+
}
55+
56+
removed, err := lp.GarbageCollect()
57+
if err != nil {
58+
t.Fatalf("GarbageCollect() = %v", err)
59+
}
60+
61+
if len(removed) != 0 {
62+
t.Fatalf("expected to have to gc-able blobs")
63+
}
64+
}
65+
66+
func TestGcIndexMediaType(t *testing.T) {
67+
lp, err := FromPath(gcTestIndexMediaTypePath)
68+
if err != nil {
69+
t.Fatalf("FromPath() = %v", err)
70+
}
71+
72+
removed, err := lp.GarbageCollect()
73+
if err != nil {
74+
t.Fatalf("GarbageCollect() = %v", err)
75+
}
76+
77+
if len(removed) != 0 {
78+
t.Fatalf("expected to have to gc-able blobs")
79+
}
80+
}
81+
82+
func TestGcUnknownMediaType(t *testing.T) {
83+
lp, err := FromPath(gcUnknownMediaTypePath)
84+
if err != nil {
85+
t.Fatalf("FromPath() = %v", err)
86+
}
87+
88+
_, err = lp.GarbageCollect()
89+
if err == nil {
90+
t.Fatalf("expected GarbageCollect to return err but did not")
91+
}
92+
93+
if err.Error() != gcUnknownMediaTypeErr {
94+
t.Fatalf("expected error '%s', got '%s'", gcUnknownMediaTypeErr, err.Error())
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"schemaVersion": 2,
3+
"manifests": [
4+
{
5+
"mediaType": "application/vnd.oci.descriptor.v1+json",
6+
"size": 423,
7+
"digest": "sha256:32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b003850720"
8+
}
9+
]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"imageLayoutVersion": "1.0.0"
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"schemaVersion": 2,
3+
"manifests": [
4+
{
5+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
6+
"size": 423,
7+
"digest": "sha256:eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650",
8+
"annotations": {
9+
"org.opencontainers.image.ref.name": "1"
10+
}
11+
}
12+
]
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"schemaVersion": 2,
3+
"manifests": [
4+
{
5+
"mediaType": "application/vnd.oci.image.manifest.v1+json",
6+
"size": 423,
7+
"digest": "sha256:eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650",
8+
"annotations": {
9+
"org.opencontainers.image.ref.name": "4"
10+
}
11+
}
12+
]
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"architecture": "amd64", "author": "Bazel", "config": {}, "created": "1970-01-01T00:00:00Z", "history": [{"author": "Bazel", "created": "1970-01-01T00:00:00Z", "created_by": "bazel build ..."}], "os": "linux", "rootfs": {"diff_ids": ["sha256:8897395fd26dc44ad0e2a834335b33198cb41ac4d98dfddf58eced3853fa7b17"], "type": "layers"}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":330,"digest":"sha256:6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":167,"digest":"sha256:dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b"}]}

0 commit comments

Comments
 (0)