Skip to content

Commit cc61e27

Browse files
author
Oleg Bulatov
committed
Add -prune option to dockerregistry
Signed-off-by: Oleg Bulatov <[email protected]>
1 parent 322171b commit cc61e27

File tree

3 files changed

+282
-4
lines changed

3 files changed

+282
-4
lines changed

pkg/cmd/dockerregistry/dockerregistry.go

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package dockerregistry
33
import (
44
"crypto/tls"
55
"crypto/x509"
6+
"flag"
67
"fmt"
78
"io"
89
"io/ioutil"
910
"net/http"
1011
"os"
12+
"strings"
1113
"time"
1214

1315
log "github.com/Sirupsen/logrus"
@@ -19,8 +21,10 @@ import (
1921
"github.com/docker/distribution/health"
2022
"github.com/docker/distribution/registry/auth"
2123
"github.com/docker/distribution/registry/handlers"
24+
"github.com/docker/distribution/registry/storage"
25+
"github.com/docker/distribution/registry/storage/driver/factory"
2226
"github.com/docker/distribution/uuid"
23-
"github.com/docker/distribution/version"
27+
distversion "github.com/docker/distribution/version"
2428

2529
_ "github.com/docker/distribution/registry/auth/htpasswd"
2630
_ "github.com/docker/distribution/registry/auth/token"
@@ -35,18 +39,90 @@ import (
3539
_ "github.com/docker/distribution/registry/storage/driver/s3-aws"
3640
_ "github.com/docker/distribution/registry/storage/driver/swift"
3741

38-
"strings"
42+
kubeversion "k8s.io/kubernetes/pkg/version"
3943

4044
"github.com/openshift/origin/pkg/cmd/server/crypto"
4145
"github.com/openshift/origin/pkg/cmd/util/clientcmd"
4246
"github.com/openshift/origin/pkg/dockerregistry/server"
4347
"github.com/openshift/origin/pkg/dockerregistry/server/api"
4448
"github.com/openshift/origin/pkg/dockerregistry/server/audit"
4549
registryconfig "github.com/openshift/origin/pkg/dockerregistry/server/configuration"
50+
"github.com/openshift/origin/pkg/version"
4651
)
4752

53+
var prune = flag.String("prune", "", "prune blobs from the storage and exit (check, delete)")
54+
55+
func versionFields() log.Fields {
56+
return log.Fields{
57+
"distribution_version": distversion.Version,
58+
"kubernetes_version": kubeversion.Get(),
59+
"openshift_version": version.Get(),
60+
}
61+
}
62+
63+
// ExecutePruner runs the pruner.
64+
func ExecutePruner(configFile io.Reader, dryRun bool) {
65+
config, _, err := registryconfig.Parse(configFile)
66+
if err != nil {
67+
log.Fatalf("error parsing configuration file: %s", err)
68+
}
69+
70+
// A lot of installations have the 'debug' log level in their config files,
71+
// but it's too verbose for pruning. Therefore we ignore it, but we still
72+
// respect overrides using environment variables.
73+
config.Loglevel = ""
74+
config.Log.Level = configuration.Loglevel(os.Getenv("REGISTRY_LOG_LEVEL"))
75+
if config.Log.Level == "" {
76+
config.Log.Level = "warning"
77+
}
78+
79+
ctx := context.Background()
80+
ctx, err = configureLogging(ctx, config)
81+
if err != nil {
82+
log.Fatalf("error configuring logging: %s", err)
83+
}
84+
85+
startPrune := "start prune"
86+
if dryRun {
87+
startPrune += " (dry-run mode)"
88+
}
89+
log.WithFields(versionFields()).Info(startPrune)
90+
91+
registryClient := server.NewRegistryClient(clientcmd.NewConfig().BindToFile())
92+
93+
storageDriver, err := factory.Create(config.Storage.Type(), config.Storage.Parameters())
94+
if err != nil {
95+
log.Fatalf("error creating storage driver: %s", err)
96+
}
97+
98+
var registryOptions []storage.RegistryOption
99+
if !dryRun {
100+
registryOptions = append(registryOptions, storage.EnableDelete)
101+
}
102+
registry, err := storage.NewRegistry(ctx, storageDriver, registryOptions...)
103+
if err != nil {
104+
log.Fatalf("error creating registry: %s", err)
105+
}
106+
107+
server.Prune(ctx, storageDriver, registry, registryClient, dryRun)
108+
}
109+
48110
// Execute runs the Docker registry.
49111
func Execute(configFile io.Reader) {
112+
if *prune != "" {
113+
var dryRun bool
114+
switch *prune {
115+
case "delete":
116+
dryRun = false
117+
case "check":
118+
dryRun = true
119+
default:
120+
log.Fatal("invalid value for the -prune option")
121+
}
122+
ExecutePruner(configFile, dryRun)
123+
return
124+
}
125+
50126
dockerConfig, extraConfig, err := registryconfig.Parse(configFile)
51127
if err != nil {
52128
log.Fatalf("error parsing configuration file: %s", err)
@@ -64,7 +140,7 @@ func Execute(configFile io.Reader) {
64140
registryClient := server.NewRegistryClient(clientcmd.NewConfig().BindToFile())
65141
ctx = server.WithRegistryClient(ctx, registryClient)
66142

67-
log.Infof("version=%s", version.Version)
143+
log.WithFields(versionFields()).Info("start registry")
68144
// inject a logger into the uuid library. warns us if there is a problem
69145
// with uuid generation under low entropy.
70146
uuid.Loggerf = context.GetLogger(ctx).Warnf

pkg/dockerregistry/server/errorblobstore.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ func (f statCrossMountCreateOptions) Apply(v interface{}) error {
151151
if err != nil {
152152
context.GetLogger(f.ctx).Infof("cannot mount blob %s from repository %s: %v - disabling cross-repo mount",
153153
opts.Mount.From.Digest().String(),
154-
opts.Mount.From.Name())
154+
opts.Mount.From.Name(),
155+
err)
155156
opts.Mount.ShouldMount = false
156157
return nil
157158
}

pkg/dockerregistry/server/prune.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package server
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/docker/go-units"
7+
8+
"github.com/docker/distribution"
9+
"github.com/docker/distribution/context"
10+
"github.com/docker/distribution/digest"
11+
"github.com/docker/distribution/manifest/schema2"
12+
"github.com/docker/distribution/reference"
13+
"github.com/docker/distribution/registry/storage"
14+
"github.com/docker/distribution/registry/storage/driver"
15+
16+
kerrors "k8s.io/apimachinery/pkg/api/errors"
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
19+
imageapi "github.com/openshift/origin/pkg/image/api"
20+
)
21+
22+
func imageStreamHasManifestDigest(is *imageapi.ImageStream, dgst digest.Digest) bool {
23+
for _, tagEventList := range is.Status.Tags {
24+
for _, tagEvent := range tagEventList.Items {
25+
if tagEvent.Image == string(dgst) {
26+
return true
27+
}
28+
}
29+
}
30+
return false
31+
}
32+
33+
// Prune removes blobs which are not used by Images in OpenShift.
34+
//
35+
// TODO(dmage): remove layer links to a blob if the blob is removed or it doesn't belong to the ImageStream.
36+
// TODO(dmage): keep young blobs (docker/distribution#2297).
37+
func Prune(ctx context.Context, storageDriver driver.StorageDriver, registry distribution.Namespace, registryClient RegistryClient, dryRun bool) {
38+
logger := context.GetLogger(ctx)
39+
40+
repositoryEnumerator, ok := registry.(distribution.RepositoryEnumerator)
41+
if !ok {
42+
logger.Fatal("unable to convert Namespace to RepositoryEnumerator")
43+
}
44+
45+
oc, _, err := registryClient.Clients()
46+
if err != nil {
47+
logger.Fatalf("error getting clients: %s", err)
48+
}
49+
50+
imageList, err := oc.Images().List(metav1.ListOptions{})
51+
if err != nil {
52+
logger.Fatalf("error listing images: %s", err)
53+
}
54+
55+
inuse := make(map[string]string)
56+
for _, image := range imageList.Items {
57+
// Keep the manifest.
58+
inuse[image.Name] = image.DockerImageReference
59+
60+
// Keep the config for a schema 2 manifest.
61+
if image.DockerImageManifestMediaType == schema2.MediaTypeManifest {
62+
inuse[image.DockerImageMetadata.ID] = image.DockerImageReference
63+
}
64+
65+
// Keep image layers.
66+
for _, layer := range image.DockerImageLayers {
67+
inuse[layer.Name] = image.DockerImageReference
68+
}
69+
}
70+
71+
var stats struct {
72+
Blobs int
73+
DiskSpace int64
74+
}
75+
76+
var reposToDelete []string
77+
err = repositoryEnumerator.Enumerate(ctx, func(repoName string) error {
78+
logger.Debugln("Processing repository", repoName)
79+
80+
named, err := reference.WithName(repoName)
81+
if err != nil {
82+
return fmt.Errorf("failed to parse the repo name %s: %v", repoName, err)
83+
}
84+
85+
ref, err := imageapi.ParseDockerImageReference(repoName)
86+
if err != nil {
87+
return fmt.Errorf("failed to parse the image reference %s: %v", repoName, err)
88+
}
89+
90+
is, err := oc.ImageStreams(ref.Namespace).Get(ref.Name, metav1.GetOptions{})
91+
if kerrors.IsNotFound(err) {
92+
logger.Printf("The image stream %s/%s is not found, will remove the whole repository", ref.Namespace, ref.Name)
93+
94+
// We cannot delete the repository at this point, because it would break Enumerate.
95+
reposToDelete = append(reposToDelete, repoName)
96+
97+
return nil
98+
} else if err != nil {
99+
return fmt.Errorf("failed to get the image stream %s: %v", repoName, err)
100+
}
101+
102+
repository, err := registry.Repository(ctx, named)
103+
if err != nil {
104+
return err
105+
}
106+
107+
manifestService, err := repository.Manifests(ctx)
108+
if err != nil {
109+
return err
110+
}
111+
112+
manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator)
113+
if !ok {
114+
return fmt.Errorf("unable to convert ManifestService into ManifestEnumerator")
115+
}
116+
117+
err = manifestEnumerator.Enumerate(ctx, func(dgst digest.Digest) error {
118+
if imageReference, ok := inuse[string(dgst)]; ok && imageStreamHasManifestDigest(is, dgst) {
119+
logger.Debugf("Keeping the manifest %s@%s (it belongs to the image %s)", repoName, dgst, imageReference)
120+
return nil
121+
}
122+
123+
if dryRun {
124+
logger.Printf("Would delete the manifest: %s@%s", repoName, dgst)
125+
} else {
126+
logger.Printf("Deleting the manifest: %s@%s", repoName, dgst)
127+
err = manifestService.Delete(ctx, dgst)
128+
if err != nil {
129+
return fmt.Errorf("failed to delete the manifest %s@%s: %s", repoName, dgst, err)
130+
}
131+
}
132+
133+
return nil
134+
})
135+
if e, ok := err.(driver.PathNotFoundError); ok {
136+
logger.Printf("Skipped manifest pruning for the repository %s: %s", repoName, e)
137+
} else if err != nil {
138+
return fmt.Errorf("failed to prune manifests in the repository %s: %s", repoName, err)
139+
}
140+
141+
return nil
142+
})
143+
if e, ok := err.(driver.PathNotFoundError); ok {
144+
logger.Warnf("No repositories found: %s", e)
145+
return
146+
} else if err != nil {
147+
logger.Fatal(err)
148+
}
149+
150+
vacuum := storage.NewVacuum(ctx, storageDriver)
151+
152+
logger.Debugln("Removing repositories")
153+
for _, repoName := range reposToDelete {
154+
if dryRun {
155+
logger.Printf("Would delete the repository: %s", repoName)
156+
} else {
157+
err = vacuum.RemoveRepository(repoName)
158+
if err != nil {
159+
logger.Fatal("unable to remove the repository %s: %v", repoName, err)
160+
}
161+
}
162+
}
163+
164+
logger.Debugln("Processing blobs")
165+
blobStatter := registry.BlobStatter()
166+
err = registry.Blobs().Enumerate(ctx, func(dgst digest.Digest) error {
167+
if imageReference, ok := inuse[string(dgst)]; ok {
168+
logger.Debugf("Keeping the blob %s (it belongs to the image %s)", dgst, imageReference)
169+
return nil
170+
}
171+
172+
desc, err := blobStatter.Stat(ctx, dgst)
173+
if err != nil {
174+
return err
175+
}
176+
177+
stats.Blobs++
178+
stats.DiskSpace += desc.Size
179+
180+
if dryRun {
181+
logger.Printf("Would delete the blob: %s", dgst)
182+
} else {
183+
err := vacuum.RemoveBlob(string(dgst))
184+
if err != nil {
185+
return fmt.Errorf("failed to delete the blob %s: %s", dgst, err)
186+
}
187+
}
188+
189+
return nil
190+
})
191+
if err != nil {
192+
logger.Fatal(err)
193+
}
194+
195+
fmt.Printf("Blobs deleted: %d\n", stats.Blobs)
196+
fmt.Printf("Disk space freed: %s\n", units.BytesSize(float64(stats.DiskSpace)))
197+
198+
if dryRun {
199+
fmt.Println("Use -prune=delete to actually delete the data.")
200+
}
201+
}

0 commit comments

Comments
 (0)