From a867802c9507e02731c5e770e9d5f501eb288ace Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Mon, 27 Apr 2015 14:23:06 -0400 Subject: [PATCH 01/21] UPSTREAM(docker/distribution): add layer unlinking Add ability to unlink a layer from a repository. --- .../github.com/docker/distribution/registry.go | 3 +++ .../registry/storage/cache/cache.go | 14 ++++++++++++++ .../registry/storage/cache/memory.go | 9 +++++++++ .../registry/storage/cache/redis.go | 5 +++++ .../registry/storage/layercache.go | 8 ++++++++ .../registry/storage/layerstore.go | 18 +++++++++++++++++- 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry.go b/Godeps/_workspace/src/github.com/docker/distribution/registry.go index c5d84a0faca2..7bc19c891133 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry.go @@ -108,6 +108,9 @@ type LayerService interface { // Fetch the layer identifed by TarSum. Fetch(digest digest.Digest) (Layer, error) + // Delete unlinks the layer from a Repository. + Delete(dgst digest.Digest) error + // Upload begins a layer upload to repository identified by name, // returning a handle. Upload() (LayerUpload, error) diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/cache.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/cache.go index a21cefd5745e..dcc79d8d53fd 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/cache.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/cache.go @@ -38,6 +38,8 @@ type LayerInfoCache interface { // Add includes the layer in the given repository cache. Add(ctx context.Context, repo string, dgst digest.Digest) error + Delete(ctx context.Context, repo string, dgst digest.Digest) error + // Meta provides the location of the layer on the backend and its size. Membership of a // repository should be tested before using the result, if required. Meta(ctx context.Context, dgst digest.Digest) (LayerMeta, error) @@ -77,6 +79,18 @@ func (b *base) Add(ctx context.Context, repo string, dgst digest.Digest) error { return b.LayerInfoCache.Add(ctx, repo, dgst) } +func (b *base) Delete(ctx context.Context, repo string, dgst digest.Digest) error { + if repo == "" { + return fmt.Errorf("cache: cannot delete empty repository name") + } + + if dgst == "" { + return fmt.Errorf("cache: cannot delete empty digest") + } + + return b.LayerInfoCache.Delete(ctx, repo, dgst) +} + func (b *base) Meta(ctx context.Context, dgst digest.Digest) (LayerMeta, error) { if dgst == "" { return LayerMeta{}, fmt.Errorf("cache: cannot get meta for empty digest") diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/memory.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/memory.go index 6d949792502c..63b0dcc0d789 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/memory.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/memory.go @@ -43,6 +43,15 @@ func (ilic *inmemoryLayerInfoCache) Add(ctx context.Context, repo string, dgst d return nil } +func (ilic *inmemoryLayerInfoCache) Delete(ctx context.Context, repo string, dgst digest.Digest) error { + members, ok := ilic.membership[repo] + if !ok { + return nil + } + delete(members, dgst) + return nil +} + // Meta retrieves the layer meta data from the redis hash, returning // ErrUnknownLayer if not found. func (ilic *inmemoryLayerInfoCache) Meta(ctx context.Context, dgst digest.Digest) (LayerMeta, error) { diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/redis.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/redis.go index 6b8f7679abe7..eba0a8af2849 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/redis.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/cache/redis.go @@ -53,6 +53,11 @@ func (rlic *redisLayerInfoCache) Add(ctx context.Context, repo string, dgst dige return err } +func (rlic *redisLayerInfoCache) Delete(ctx context.Context, repo string, dgst digest.Digest) error { + //TODO + return nil +} + // Meta retrieves the layer meta data from the redis hash, returning // ErrUnknownLayer if not found. func (rlic *redisLayerInfoCache) Meta(ctx context.Context, dgst digest.Digest) (LayerMeta, error) { diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/layercache.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/layercache.go index b9732f203eb5..3d7949f4c1ca 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/layercache.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/layercache.go @@ -128,6 +128,14 @@ fallback: return layer, err } +func (lc *cachedLayerService) Delete(dgst digest.Digest) error { + ctxu.GetLogger(lc.ctx).Debugf("(*layerInfoCache).Delete(%q)", dgst) + if err := lc.cache.Delete(lc.ctx, lc.repository.Name(), dgst); err != nil { + ctxu.GetLogger(lc.ctx).Errorf("error deleting layer link from cache; repo=%s, layer=%s: %v", lc.repository.Name(), dgst, err) + } + return lc.LayerService.Delete(dgst) +} + // extractLayerInfo pulls the layerInfo from the layer, attempting to get the // path information from either the concrete object or by resolving the // primary blob store path. diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/layerstore.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/layerstore.go index 1c7428a9f37a..802753633fa2 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/layerstore.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/layerstore.go @@ -1,6 +1,7 @@ package storage import ( + "strings" "time" "code.google.com/p/go-uuid/uuid" @@ -52,6 +53,17 @@ func (ls *layerStore) Fetch(dgst digest.Digest) (distribution.Layer, error) { }, nil } +func (ls *layerStore) Delete(dgst digest.Digest) error { + lp, err := ls.linkPath(dgst) + if err != nil { + return err + } + + lp = strings.TrimSuffix(lp, "/link") + + return ls.repository.driver.Delete(lp) +} + // Upload begins a layer upload, returning a handle. If the layer upload // is already in progress or the layer has already been uploaded, this // will return an error. @@ -150,9 +162,13 @@ func (ls *layerStore) newLayerUpload(uuid, path string, startedAt time.Time) (di return lw, nil } +func (ls *layerStore) linkPath(dgst digest.Digest) (string, error) { + return ls.repository.registry.pm.path(layerLinkPathSpec{name: ls.repository.Name(), digest: dgst}) +} + func (ls *layerStore) path(dgst digest.Digest) (string, error) { // We must traverse this path through the link to enforce ownership. - layerLinkPath, err := ls.repository.registry.pm.path(layerLinkPathSpec{name: ls.repository.Name(), digest: dgst}) + layerLinkPath, err := ls.linkPath(dgst) if err != nil { return "", err } From d11c63ca5e55773b8e7796ed5bbe420f37e9e714 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Wed, 20 May 2015 12:17:02 -0400 Subject: [PATCH 02/21] UPSTREAM(docker/distribution): add BlobService Add Blobs() to Registry. Add BlobService with the ability to Delete() a blob. --- .../docker/distribution/registry.go | 6 +++++ .../distribution/registry/handlers/app.go | 4 +++ .../registry/storage/blobstore.go | 26 +++++++++++++++++++ .../distribution/registry/storage/registry.go | 4 +++ 4 files changed, 40 insertions(+) diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry.go b/Godeps/_workspace/src/github.com/docker/distribution/registry.go index 7bc19c891133..dcff2a946eca 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry.go @@ -39,6 +39,8 @@ type Namespace interface { // registry may or may not have the repository but should always return a // reference. Repository(ctx context.Context, name string) (Repository, error) + + Blobs() BlobService } // Repository is a named collection of manifests and layers. @@ -176,6 +178,10 @@ type SignatureService interface { Put(dgst digest.Digest, signatures ...[]byte) error } +type BlobService interface { + Delete(dgst digest.Digest) error +} + // Descriptor describes targeted content. Used in conjunction with a blob // store, a descriptor can be used to fetch, store and target any kind of // blob. The struct also describes the wire protocol format. Fields should diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/handlers/app.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/handlers/app.go index 28940c8e1d40..ee47334824f7 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/handlers/app.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/handlers/app.go @@ -133,6 +133,10 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App return app } +func (app *App) Registry() distribution.Namespace { + return app.registry +} + // register a handler with the application, by route name. The handler will be // passed through the application filters and context will be constructed at // request time. diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/blobstore.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/blobstore.go index 8bab2f5e1d76..2a0d449accaf 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/blobstore.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/blobstore.go @@ -2,7 +2,9 @@ package storage import ( "fmt" + "strings" + "github.com/docker/distribution" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/digest" storagedriver "github.com/docker/distribution/registry/storage/driver" @@ -23,6 +25,30 @@ type blobStore struct { ctx context.Context } +var _ distribution.BlobService = &blobStore{} + +func (bs *blobStore) Delete(dgst digest.Digest) error { + found, err := bs.exists(dgst) + if err != nil { + return err + } + + if !found { + // TODO if the blob doesn't exist, should this be an error? + return nil + } + + path, err := bs.path(dgst) + + if err != nil { + return err + } + + path = strings.TrimSuffix(path, "/data") + + return bs.driver.Delete(path) +} + // exists reports whether or not the path exists. If the driver returns error // other than storagedriver.PathNotFound, an error may be returned. func (bs *blobStore) exists(dgst digest.Digest) (bool, error) { diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/registry.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/registry.go index 1126db457200..919fd7b70525 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/registry.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/registry.go @@ -121,3 +121,7 @@ func (repo *repository) Signatures() distribution.SignatureService { repository: repo, } } + +func (reg *registry) Blobs() distribution.BlobService { + return reg.blobStore +} From aaad20efb595805bc1e6176da86f6bd4a2a23587 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Fri, 8 May 2015 12:46:28 -0400 Subject: [PATCH 03/21] UPSTREAM(docker/distribution): custom routes/auth Add support for custom routes and custom auth records per route. --- .../distribution/registry/handlers/app.go | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/handlers/app.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/handlers/app.go index ee47334824f7..2664a43eb24a 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/handlers/app.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/handlers/app.go @@ -137,18 +137,38 @@ func (app *App) Registry() distribution.Namespace { return app.registry } +type customAccessRecordsFunc func(*http.Request) []auth.Access + +func NoCustomAccessRecords(*http.Request) []auth.Access { + return []auth.Access{} +} + +func NameNotRequired(*http.Request) bool { + return false +} + +func NameRequired(*http.Request) bool { + return true +} + // register a handler with the application, by route name. The handler will be // passed through the application filters and context will be constructed at // request time. func (app *App) register(routeName string, dispatch dispatchFunc) { + app.RegisterRoute(app.router.GetRoute(routeName), dispatch, app.nameRequired, NoCustomAccessRecords) +} +func (app *App) RegisterRoute(route *mux.Route, dispatch dispatchFunc, nameRequired nameRequiredFunc, accessRecords customAccessRecordsFunc) { // TODO(stevvooe): This odd dispatcher/route registration is by-product of // some limitations in the gorilla/mux router. We are using it to keep // routing consistent between the client and server, but we may want to // replace it with manual routing and structure-based dispatch for better // control over the request execution. + route.Handler(app.dispatcher(dispatch, nameRequired, accessRecords)) +} - app.router.GetRoute(routeName).Handler(app.dispatcher(dispatch)) +func (app *App) NewRoute() *mux.Route { + return app.router.NewRoute() } // configureEvents prepares the event sink for action. @@ -312,11 +332,11 @@ type dispatchFunc func(ctx *Context, r *http.Request) http.Handler // dispatcher returns a handler that constructs a request specific context and // handler, using the dispatch factory function. -func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { +func (app *App) dispatcher(dispatch dispatchFunc, nameRequired nameRequiredFunc, accessRecords customAccessRecordsFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { context := app.context(w, r) - if err := app.authorized(w, r, context); err != nil { + if err := app.authorized(w, r, context, nameRequired, accessRecords(r)); err != nil { ctxu.GetLogger(context).Errorf("error authorizing context: %v", err) return } @@ -324,7 +344,7 @@ func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { // Add username to request logging context.Context = ctxu.WithLogger(context.Context, ctxu.GetLogger(context.Context, "auth.user.name")) - if app.nameRequired(r) { + if nameRequired(r) { repository, err := app.registry.Repository(context, getName(context)) if err != nil { @@ -397,7 +417,7 @@ func (app *App) context(w http.ResponseWriter, r *http.Request) *Context { // authorized checks if the request can proceed with access to the requested // repository. If it succeeds, the context may access the requested // repository. An error will be returned if access is not available. -func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Context) error { +func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Context, nameRequired nameRequiredFunc, customAccessRecords []auth.Access) error { ctxu.GetLogger(context).Debug("authorizing request") repo := getName(context) @@ -406,12 +426,15 @@ func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Cont } var accessRecords []auth.Access + accessRecords = append(accessRecords, customAccessRecords...) if repo != "" { accessRecords = appendAccessRecords(accessRecords, r.Method, repo) - } else { + } + + if len(accessRecords) == 0 { // Only allow the name not to be set on the base route. - if app.nameRequired(r) { + if nameRequired(r) { // For this to be properly secured, repo must always be set for a // resource that may make a modification. The only condition under // which name is not set and we still allow access is when the @@ -468,6 +491,8 @@ func (app *App) eventBridge(ctx *Context, r *http.Request) notifications.Listene return notifications.NewBridge(ctx.urlBuilder, app.events.source, actor, request, app.events.sink) } +type nameRequiredFunc func(*http.Request) bool + // nameRequired returns true if the route requires a name. func (app *App) nameRequired(r *http.Request) bool { route := mux.CurrentRoute(r) From 902855ed3aa5587ec1d2ed737a885b079cd7131d Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Wed, 20 May 2015 12:22:56 -0400 Subject: [PATCH 04/21] UPSTREAM(docker/distribution): manifest deletions Implement Delete in the manifestStore. --- .../docker/distribution/registry/storage/manifeststore.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/manifeststore.go b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/manifeststore.go index d83c48b6e551..0a554ad3ad47 100644 --- a/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/manifeststore.go +++ b/Godeps/_workspace/src/github.com/docker/distribution/registry/storage/manifeststore.go @@ -55,8 +55,8 @@ func (ms *manifestStore) Put(ctx context.Context, manifest *manifest.SignedManif // Delete removes the revision of the specified manfiest. func (ms *manifestStore) Delete(ctx context.Context, dgst digest.Digest) error { - ctxu.GetLogger(ms.repository.ctx).Debug("(*manifestStore).Delete - unsupported") - return fmt.Errorf("deletion of manifests not supported") + ctxu.GetLogger(ms.repository.ctx).Debug("(*manifestStore).Delete") + return ms.revisionStore.delete(dgst) } func (ms *manifestStore) Tags(ctx context.Context) ([]string, error) { From 9f5fcd7b046ceb953f280641f3086d4c5ca9604d Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Mon, 27 Apr 2015 14:24:39 -0400 Subject: [PATCH 05/21] Add image pruning support --- pkg/api/graph/graph.go | 14 +- pkg/api/graph/graph_test.go | 3 +- pkg/api/graph/types.go | 171 ++++- .../controller/image_change_controller.go | 2 +- pkg/build/util/util.go | 12 +- pkg/client/fake_imagestreams.go | 11 +- pkg/client/imagestreams.go | 8 + pkg/cmd/admin/admin.go | 2 + pkg/cmd/dockerregistry/dockerregistry.go | 133 +++- pkg/cmd/experimental/buildchain/buildchain.go | 4 +- pkg/cmd/experimental/imageprune/imageprune.go | 111 +++ pkg/image/prune/imagepruner.go | 722 ++++++++++++++++++ pkg/image/prune/imagepruner_test.go | 582 ++++++++++++++ pkg/image/prune/summary.go | 66 ++ pkg/image/registry/imagestreamimage/rest.go | 6 +- .../registry/imagestreamimage/rest_test.go | 2 +- 16 files changed, 1816 insertions(+), 33 deletions(-) create mode 100644 pkg/cmd/experimental/imageprune/imageprune.go create mode 100644 pkg/image/prune/imagepruner.go create mode 100644 pkg/image/prune/imagepruner_test.go create mode 100644 pkg/image/prune/summary.go diff --git a/pkg/api/graph/graph.go b/pkg/api/graph/graph.go index 6a74172e92b8..8d31fbdd1386 100644 --- a/pkg/api/graph/graph.go +++ b/pkg/api/graph/graph.go @@ -9,7 +9,7 @@ import ( ) type Node struct { - concrete.Node + graph.Node UniqueName } @@ -23,6 +23,10 @@ type uniqueNamer interface { UniqueName() string } +type NodeFinder interface { + Find(name UniqueName) graph.Node +} + // UniqueNodeInitializer is a graph that allows nodes with a unique name to be added without duplication. // If the node is newly added, true will be returned. type UniqueNodeInitializer interface { @@ -44,6 +48,7 @@ type MutableUniqueGraph interface { graph.Mutable MutableDirectedEdge UniqueNodeInitializer + NodeFinder } type Edge struct { @@ -294,6 +299,13 @@ func (g uniqueNamedGraph) FindOrCreate(name UniqueName, fn NodeInitializerFunc) return node, false } +func (g uniqueNamedGraph) Find(name UniqueName) graph.Node { + if node, ok := g.names[name]; ok { + return node + } + return nil +} + type typedGraph struct{} type stringer interface { diff --git a/pkg/api/graph/graph_test.go b/pkg/api/graph/graph_test.go index 4e49f36d78f9..a9caf2ca3bb8 100644 --- a/pkg/api/graph/graph_test.go +++ b/pkg/api/graph/graph_test.go @@ -188,7 +188,8 @@ func TestGraph(t *testing.T) { } bc++ case *image.ImageStream: - if g.Kind(node) != ImageStreamGraphKind { + // TODO resolve this check for 2 kinds, since both have the same object type + if g.Kind(node) != ImageStreamGraphKind && g.Kind(node) != ImageStreamTagGraphKind { t.Fatalf("unexpected kind: %v", g.Kind(node)) } ir++ diff --git a/pkg/api/graph/types.go b/pkg/api/graph/types.go index e0f1336f873a..2ac9d6662fb4 100644 --- a/pkg/api/graph/types.go +++ b/pkg/api/graph/types.go @@ -20,12 +20,18 @@ import ( const ( UnknownGraphKind = iota - ImageStreamGraphKind + ImageStreamTagGraphKind DockerRepositoryGraphKind BuildConfigGraphKind DeploymentConfigGraphKind SourceRepositoryGraphKind ServiceGraphKind + ImageGraphKind + PodGraphKind + ImageStreamGraphKind + ReplicationControllerGraphKind + ImageLayerGraphKind + BuildGraphKind ) const ( UnknownGraphEdgeKind = iota @@ -36,6 +42,9 @@ const ( BuildOutputGraphEdgeKind UsedInDeploymentGraphEdgeKind ExposedThroughServiceGraphEdgeKind + ReferencedImageGraphEdgeKind + WeakReferencedImageGraphEdgeKind + ReferencedImageLayerGraphEdgeKind ) type ServiceNode struct { @@ -119,7 +128,7 @@ func (n ImageStreamTagNode) String() string { } func (*ImageStreamTagNode) Kind() int { - return ImageStreamGraphKind + return ImageStreamTagGraphKind } type DockerImageRepositoryNode struct { @@ -159,6 +168,50 @@ func (SourceRepositoryNode) Kind() int { return SourceRepositoryGraphKind } +type ImageNode struct { + Node + Image *image.Image +} + +func (n ImageNode) Object() interface{} { + return n.Image +} + +func (n ImageNode) String() string { + return fmt.Sprintf("", n.Image.Name) +} + +func (*ImageNode) Kind() int { + return ImageGraphKind +} + +func Image(g MutableUniqueGraph, img *image.Image) graph.Node { + return EnsureUnique(g, + UniqueName(fmt.Sprintf("%d|%s", ImageGraphKind, img.Name)), + func(node Node) graph.Node { + return &ImageNode{node, img} + }, + ) +} + +func FindImage(g MutableUniqueGraph, imageName string) graph.Node { + return g.Find(UniqueName(fmt.Sprintf("%d|%s", ImageGraphKind, imageName))) +} + +type PodNode struct { + Node + Pod *kapi.Pod +} + +func Pod(g MutableUniqueGraph, pod *kapi.Pod) graph.Node { + return EnsureUnique(g, + UniqueName(fmt.Sprintf("%d|%s/%s", PodGraphKind, pod.Namespace, pod.Name)), + func(node Node) graph.Node { + return &PodNode{node, pod} + }, + ) +} + // Service adds the provided service to the graph if it does not already exist. It does not // link the service to covered nodes (that is a separate method). func Service(g MutableUniqueGraph, svc *kapi.Service) graph.Node { @@ -218,7 +271,7 @@ func SourceRepository(g MutableUniqueGraph, source build.BuildSource) (graph.Nod ), true } -// ImageStreamTag adds a graph node for the specific tag in an Image Repository if it +// ImageStreamTag adds a graph node for the specific tag in an Image Stream if it // does not already exist. func ImageStreamTag(g MutableUniqueGraph, namespace, name, tag string) graph.Node { if len(tag) == 0 { @@ -227,7 +280,7 @@ func ImageStreamTag(g MutableUniqueGraph, namespace, name, tag string) graph.Nod if strings.Contains(name, ":") { panic(name) } - uname := UniqueName(fmt.Sprintf("%d|%s/%s:%s", ImageStreamGraphKind, namespace, name, tag)) + uname := UniqueName(fmt.Sprintf("%d|%s/%s:%s", ImageStreamTagGraphKind, namespace, name, tag)) return EnsureUnique(g, uname, func(node Node) graph.Node { @@ -273,7 +326,7 @@ func BuildConfig(g MutableUniqueGraph, config *build.BuildConfig) graph.Node { g.AddEdge(in, node, BuildInputGraphEdgeKind) } - from := buildutil.GetImageStreamForStrategy(config) + from := buildutil.GetImageStreamForStrategy(config.Parameters.Strategy) if from != nil { switch from.Kind { case "DockerImage": @@ -433,3 +486,111 @@ func defaultNamespace(value, defaultValue string) string { } return value } + +type ImageStreamNode struct { + Node + *image.ImageStream +} + +func (n ImageStreamNode) Object() interface{} { + return n.ImageStream +} + +func (n ImageStreamNode) String() string { + return fmt.Sprintf("", n.Namespace, n.Name) +} + +func (*ImageStreamNode) Kind() int { + return ImageStreamGraphKind +} + +// ImageStream adds a graph node for the Image Stream if it does not already exist. +func ImageStream(g MutableUniqueGraph, stream *image.ImageStream) graph.Node { + return EnsureUnique(g, + UniqueName(fmt.Sprintf("%d|%s/%s", ImageStreamGraphKind, stream.Namespace, stream.Name)), + func(node Node) graph.Node { + return &ImageStreamNode{node, stream} + }, + ) +} + +type ReplicationControllerNode struct { + Node + *kapi.ReplicationController +} + +func (n ReplicationControllerNode) Object() interface{} { + return n.ReplicationController +} + +func (n ReplicationControllerNode) String() string { + return fmt.Sprintf("", n.Namespace, n.Name) +} + +func (*ReplicationControllerNode) Kind() int { + return ReplicationControllerGraphKind +} + +// ReplicationController adds a graph node for the ReplicationController if it does not already exist. +func ReplicationController(g MutableUniqueGraph, rc *kapi.ReplicationController) graph.Node { + return EnsureUnique(g, + UniqueName(fmt.Sprintf("%d|%s/%s", ReplicationControllerGraphKind, rc.Namespace, rc.Name)), + func(node Node) graph.Node { + return &ReplicationControllerNode{node, rc} + }, + ) +} + +type ImageLayerNode struct { + Node + Layer string +} + +func (n ImageLayerNode) Object() interface{} { + return n.Layer +} + +func (n ImageLayerNode) String() string { + return fmt.Sprintf("", n.Layer) +} + +func (*ImageLayerNode) Kind() int { + return ImageLayerGraphKind +} + +// ImageLayer adds a graph node for the layer if it does not already exist. +func ImageLayer(g MutableUniqueGraph, layer string) graph.Node { + return EnsureUnique(g, + UniqueName(fmt.Sprintf("%d|%s", ImageLayerGraphKind, layer)), + func(node Node) graph.Node { + return &ImageLayerNode{node, layer} + }, + ) +} + +type BuildNode struct { + Node + Build *build.Build +} + +func (n BuildNode) Object() interface{} { + return n.Build +} + +func (n BuildNode) String() string { + return fmt.Sprintf("", n.Build.Namespace, n.Build.Name) +} + +func (*BuildNode) Kind() int { + return BuildGraphKind +} + +// Build adds a graph node for the build if it does not already exist. +func Build(g MutableUniqueGraph, build *build.Build) graph.Node { + return EnsureUnique(g, + UniqueName(fmt.Sprintf("%d|%s/%s", BuildGraphKind, build.Namespace, build.Name)), + func(node Node) graph.Node { + return &BuildNode{node, build} + }, + ) +} diff --git a/pkg/build/controller/image_change_controller.go b/pkg/build/controller/image_change_controller.go index 90f292e8e5bc..e106b04db2db 100644 --- a/pkg/build/controller/image_change_controller.go +++ b/pkg/build/controller/image_change_controller.go @@ -57,7 +57,7 @@ func (c *ImageChangeController) HandleImageRepo(repo *imageapi.ImageStream) erro } originalConfig := obj.(*buildapi.BuildConfig) - from := buildutil.GetImageStreamForStrategy(config) + from := buildutil.GetImageStreamForStrategy(config.Parameters.Strategy) if from == nil || from.Kind != "ImageStreamTag" { continue } diff --git a/pkg/build/util/util.go b/pkg/build/util/util.go index 4584040490fd..f91947c7f3ea 100644 --- a/pkg/build/util/util.go +++ b/pkg/build/util/util.go @@ -13,15 +13,15 @@ func GetBuildPodName(build *buildapi.Build) string { } // GetImageStreamForStrategy returns the ImageStream[Tag/Image] ObjectReference associated -// with the BuildStrategy of a BuildConfig. -func GetImageStreamForStrategy(config *buildapi.BuildConfig) *kapi.ObjectReference { - switch config.Parameters.Strategy.Type { +// with the BuildStrategy. +func GetImageStreamForStrategy(strategy buildapi.BuildStrategy) *kapi.ObjectReference { + switch strategy.Type { case buildapi.SourceBuildStrategyType: - return config.Parameters.Strategy.SourceStrategy.From + return strategy.SourceStrategy.From case buildapi.DockerBuildStrategyType: - return config.Parameters.Strategy.DockerStrategy.From + return strategy.DockerStrategy.From case buildapi.CustomBuildStrategyType: - return config.Parameters.Strategy.CustomStrategy.From + return strategy.CustomStrategy.From default: return nil } diff --git a/pkg/client/fake_imagestreams.go b/pkg/client/fake_imagestreams.go index cf4fd9c0ddcb..879bf2c6a49e 100644 --- a/pkg/client/fake_imagestreams.go +++ b/pkg/client/fake_imagestreams.go @@ -28,13 +28,13 @@ func (c *FakeImageStreams) Get(name string) (*imageapi.ImageStream, error) { return obj.(*imageapi.ImageStream), err } -func (c *FakeImageStreams) Create(repo *imageapi.ImageStream) (*imageapi.ImageStream, error) { +func (c *FakeImageStreams) Create(stream *imageapi.ImageStream) (*imageapi.ImageStream, error) { obj, err := c.Fake.Invokes(FakeAction{Action: "create-imagestream"}, &imageapi.ImageStream{}) return obj.(*imageapi.ImageStream), err } -func (c *FakeImageStreams) Update(repo *imageapi.ImageStream) (*imageapi.ImageStream, error) { - obj, err := c.Fake.Invokes(FakeAction{Action: "update-imagestream"}, &imageapi.ImageStream{}) +func (c *FakeImageStreams) Update(stream *imageapi.ImageStream) (*imageapi.ImageStream, error) { + obj, err := c.Fake.Invokes(FakeAction{Action: "update-imagestream", Value: stream}, stream) return obj.(*imageapi.ImageStream), err } @@ -47,3 +47,8 @@ func (c *FakeImageStreams) Watch(label labels.Selector, field fields.Selector, r c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "watch-imagestreams"}) return nil, nil } + +func (c *FakeImageStreams) UpdateStatus(stream *imageapi.ImageStream) (result *imageapi.ImageStream, err error) { + obj, err := c.Fake.Invokes(FakeAction{Action: "update-status-imagestream", Value: stream}, stream) + return obj.(*imageapi.ImageStream), err +} diff --git a/pkg/client/imagestreams.go b/pkg/client/imagestreams.go index 43ca3b04ab31..917af5451a14 100644 --- a/pkg/client/imagestreams.go +++ b/pkg/client/imagestreams.go @@ -21,6 +21,7 @@ type ImageStreamInterface interface { Update(stream *imageapi.ImageStream) (*imageapi.ImageStream, error) Delete(name string) error Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) + UpdateStatus(stream *imageapi.ImageStream) (*imageapi.ImageStream, error) } // ImageStreamNamespaceGetter exposes methods to get ImageStreams by Namespace @@ -100,3 +101,10 @@ func (c *imageStreams) Watch(label labels.Selector, field fields.Selector, resou FieldsSelectorParam(field). Watch() } + +// UpdateStatus updates the image stream's status. Returns the server's representation of the image stream, and an error, if it occurs. +func (c *imageStreams) UpdateStatus(stream *imageapi.ImageStream) (result *imageapi.ImageStream, err error) { + result = &imageapi.ImageStream{} + err = c.r.Put().Namespace(c.ns).Resource("imageStreams").Name(stream.Name).SubResource("status").Body(stream).Do().Into(result) + return +} diff --git a/pkg/cmd/admin/admin.go b/pkg/cmd/admin/admin.go index 839b40de2ce0..743f11578aba 100644 --- a/pkg/cmd/admin/admin.go +++ b/pkg/cmd/admin/admin.go @@ -4,6 +4,7 @@ import ( "fmt" "io" + eximageprune "github.com/openshift/origin/pkg/cmd/experimental/imageprune" "github.com/spf13/cobra" "github.com/openshift/origin/pkg/cmd/admin/node" @@ -47,6 +48,7 @@ func NewCommandAdmin(name, fullName string, out io.Writer) *cobra.Command { cmds.AddCommand(exipfailover.NewCmdIPFailoverConfig(f, fullName, "ipfailover", out)) cmds.AddCommand(exrouter.NewCmdRouter(f, fullName, "router", out)) cmds.AddCommand(exregistry.NewCmdRegistry(f, fullName, "registry", out)) + cmds.AddCommand(eximageprune.NewCmdPruneImages(f, fullName, "prune-images", out)) cmds.AddCommand(buildchain.NewCmdBuildChain(f, fullName, "build-chain")) cmds.AddCommand(node.NewCommandManageNode(f, node.ManageNodeCommandName, fullName+" "+node.ManageNodeCommandName, out)) cmds.AddCommand(cmd.NewCmdConfig(fullName, "config")) diff --git a/pkg/cmd/dockerregistry/dockerregistry.go b/pkg/cmd/dockerregistry/dockerregistry.go index 08128a0ce0d6..5d11c2659e88 100644 --- a/pkg/cmd/dockerregistry/dockerregistry.go +++ b/pkg/cmd/dockerregistry/dockerregistry.go @@ -3,6 +3,7 @@ package dockerregistry import ( "crypto/tls" "crypto/x509" + "encoding/json" "fmt" "io" "io/ioutil" @@ -18,24 +19,136 @@ import ( _ "github.com/docker/distribution/registry/storage/driver/s3" "github.com/docker/distribution/version" gorillahandlers "github.com/gorilla/handlers" + "github.com/gorilla/mux" _ "github.com/openshift/origin/pkg/dockerregistry/server" ) -type healthHandler struct { - delegate http.Handler +func newOpenShiftHandler(app *handlers.App) http.Handler { + router := mux.NewRouter() + router.HandleFunc("/healthz", health.StatusHandler) + // TODO add https scheme + router.HandleFunc("/admin/layers", deleteLayerFunc(app)).Methods("DELETE") + //router.HandleFunc("/admin/manifests", deleteManifestFunc(app)).Methods("DELETE") + // delegate to the registry if it's not 1 of the OpenShift routes + router.NotFoundHandler = app + + return router +} + +// DeleteLayersRequest is a mapping from layers to the image repositories that +// reference them. Below is a sample request: +// +// { +// "layer1": ["repo1", "repo2"], +// "layer2": ["repo1", "repo3"], +// ... +// } +type DeleteLayersRequest map[string][]string + +// AddLayer adds a layer to the request if it doesn't already exist. +func (r DeleteLayersRequest) AddLayer(layer string) { + if _, ok := r[layer]; !ok { + r[layer] = []string{} + } } -func newHealthHandler(delegate http.Handler) http.Handler { - return &healthHandler{delegate} +// AddStream adds an image stream reference to the layer. +func (r DeleteLayersRequest) AddStream(layer, stream string) { + r[layer] = append(r[layer], stream) } -func (h *healthHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if req.URL.Path == "/healthz" { - health.StatusHandler(w, req) - return +// deleteLayerFunc returns an http.HandlerFunc that is able to fully delete a +// layer from storage. +func deleteLayerFunc(app *handlers.App) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + log.Infof("deleteLayerFunc invoked") + + //TODO verify auth + + body, err := ioutil.ReadAll(req.Body) + if err != nil { + //TODO + log.Errorf("Error reading body: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + deletions := DeleteLayersRequest{} + err = json.Unmarshal(body, &deletions) + if err != nil { + //TODO + log.Errorf("Error unmarshaling body: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + adminService := app.Registry().AdminService() + errs := []error{} + for layer, repos := range deletions { + log.Infof("Deleting layer=%q, repos=%v", layer, repos) + layerErrs := adminService.DeleteLayer(layer, repos) + errs = append(errs, layerErrs...) + } + + log.Infof("errs=%v", errs) + + //TODO write response + w.WriteHeader(http.StatusOK) + } +} + +/* +type DeleteManifestsRequest map[string][]string + +func (r *DeleteManifestsRequest) AddManifest(revision string) { + if _, ok := r[revision]; !ok { + r[revision] = []string{} + } +} + +func (r *DeleteManifestsRequest) AddStream(revision, stream string) { + r[revision] = append(r[revision], stream) +} + +func deleteManifestsFunc(app *handlers.App) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + //TODO verify auth + + body, err := ioutil.ReadAll(req.Body) + if err != nil { + //TODO + log.Errorf("Error reading body: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + deletions := DeleteManifestsRequest{} + err = json.Unmarshal(body, &deletions) + if err != nil { + //TODO + log.Errorf("Error unmarshaling body: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + adminService := app.Registry().AdminService() + errs := []error{} + for revision, repos := range deletions { + log.Infof("Deleting manifest revision=%q, repos=%v", revision, repos) + manifestErrs := adminService.DeleteManifest(revision, repos) + errs = append(errs, manifestErrs...) + } + + log.Infof("errs=%v", errs) + + //TODO write response + w.WriteHeader(http.StatusOK) } - h.delegate.ServeHTTP(w, req) } +*/ // Execute runs the Docker registry. func Execute(configFile io.Reader) { @@ -55,7 +168,7 @@ func Execute(configFile io.Reader) { ctx := context.Background() app := handlers.NewApp(ctx, *config) - handler := newHealthHandler(app) + handler := newOpenShiftHandler(app) handler = gorillahandlers.CombinedLoggingHandler(os.Stdout, handler) if config.HTTP.TLS.Certificate == "" { diff --git a/pkg/cmd/experimental/buildchain/buildchain.go b/pkg/cmd/experimental/buildchain/buildchain.go index bb64ec27b265..f09f1c9b4c93 100644 --- a/pkg/cmd/experimental/buildchain/buildchain.go +++ b/pkg/cmd/experimental/buildchain/buildchain.go @@ -256,7 +256,7 @@ func getStreams(configs []buildapi.BuildConfig) map[string][]string { glog.V(1).Infof("Scanning buildconfig %v", cfg) for _, tr := range cfg.Triggers { glog.V(1).Infof("Scanning trigger %v", tr) - from := buildutil.GetImageStreamForStrategy(&cfg) + from := buildutil.GetImageStreamForStrategy(cfg.Parameters.Strategy) glog.V(1).Infof("Strategy from= %v", from) if tr.ImageChange != nil && from != nil && from.Name != "" { glog.V(1).Infof("found ICT with from %s kind %s", from.Name, from.Kind) @@ -316,7 +316,7 @@ func findStreamDeps(stream, tag string, buildConfigList []buildapi.BuildConfig) var childNamespace, childName, childTag string for _, cfg := range buildConfigList { for _, tr := range cfg.Triggers { - from := buildutil.GetImageStreamForStrategy(&cfg) + from := buildutil.GetImageStreamForStrategy(cfg.Parameters.Strategy) if from == nil || from.Kind != "ImageStreamTag" || tr.ImageChange == nil { continue } diff --git a/pkg/cmd/experimental/imageprune/imageprune.go b/pkg/cmd/experimental/imageprune/imageprune.go new file mode 100644 index 000000000000..ddce28cad2ce --- /dev/null +++ b/pkg/cmd/experimental/imageprune/imageprune.go @@ -0,0 +1,111 @@ +package imageprune + +import ( + "fmt" + "io" + "net/http" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/golang/glog" + imageapi "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/prune" + "github.com/spf13/cobra" + + "github.com/openshift/origin/pkg/cmd/dockerregistry" + "github.com/openshift/origin/pkg/cmd/util/clientcmd" +) + +const longDesc = ` +` + +type registryURLs []string + +func (u *registryURLs) Type() string { + return "string" +} + +func (u *registryURLs) String() string { + return fmt.Sprintf("%v", *u) +} + +func (u *registryURLs) Set(value string) error { + *u = append(*u, value) + return nil +} + +type config struct { + DryRun bool + RegistryURLs registryURLs + MinimumResourcePruningAge int + TagRevisionsToKeep int +} + +func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command { + cfg := &config{ + DryRun: true, + RegistryURLs: registryURLs{"docker-registry.default.local"}, + MinimumResourcePruningAge: 60, + TagRevisionsToKeep: 3, + } + + cmd := &cobra.Command{ + Use: name, + Short: "Prune images", + Long: fmt.Sprintf(longDesc, parentName, name), + + Run: func(cmd *cobra.Command, args []string) { + if len(args) > 0 { + glog.Fatalf("No arguments are allowed to this command") + } + + osClient, kClient, err := f.Clients() + if err != nil { + glog.Fatalf("Error getting client: %v", err) + } + + if registryService, err := kClient.Services(kapi.NamespaceDefault).Get("docker-registry"); err != nil { + glog.Errorf("Error getting docker-registry service: %v", err) + } else { + cfg.RegistryURLs = append(cfg.RegistryURLs, fmt.Sprintf("%s:%d", registryService.Spec.PortalIP, registryService.Spec.Ports[0].Port)) + } + + pruner, err := prune.NewImagePruner(cfg.RegistryURLs, cfg.MinimumResourcePruningAge, cfg.TagRevisionsToKeep, osClient, osClient, kClient, kClient, osClient, osClient, osClient) + if err != nil { + glog.Fatalf("Error creating image pruner: %v", err) + } + + pruner = prune.NewSummarizingImagePruner(pruner, out) + + var ( + imagePruneFunc prune.ImagePruneFunc + layerPruneFunc prune.LayerPruneFunc + ) + + switch cfg.DryRun { + case false: + fmt.Fprintln(out, "Dry run *disabled* - images will be pruned and data will be deleted!") + imagePruneFunc = func(image *imageapi.Image, referencedStreams []*imageapi.ImageStream) []error { + prune.DescribingImagePruneFunc(out)(image, referencedStreams) + return prune.DeletingImagePruneFunc(osClient.Images(), osClient)(image, referencedStreams) + } + layerPruneFunc = func(registryURL string, req dockerregistry.DeleteLayersRequest) []error { + prune.DescribingLayerPruneFunc(out)(registryURL, req) + return prune.DeletingLayerPruneFunc(http.DefaultClient)(registryURL, req) + } + default: + fmt.Fprintln(out, "Dry run enabled - no modifications will be made.") + imagePruneFunc = prune.DescribingImagePruneFunc(out) + layerPruneFunc = prune.DescribingLayerPruneFunc(out) + } + + pruner.Run(imagePruneFunc, layerPruneFunc) + }, + } + + cmd.Flags().BoolVar(&cfg.DryRun, "dry-run", cfg.DryRun, "Perform an image pruning dry-run, displaying what would be deleted but not actually deleting anything (default=true).") + cmd.Flags().Var(&cfg.RegistryURLs, "registry-urls", "TODO") + cmd.Flags().IntVar(&cfg.MinimumResourcePruningAge, "older-than", cfg.MinimumResourcePruningAge, "TODO") + cmd.Flags().IntVar(&cfg.TagRevisionsToKeep, "keep-tag-revisions", cfg.TagRevisionsToKeep, "TODO") + + return cmd +} diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go new file mode 100644 index 000000000000..a7575edb2a33 --- /dev/null +++ b/pkg/image/prune/imagepruner.go @@ -0,0 +1,722 @@ +package prune + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" + gonum "github.com/gonum/graph" + "github.com/openshift/origin/pkg/api/graph" + buildapi "github.com/openshift/origin/pkg/build/api" + buildutil "github.com/openshift/origin/pkg/build/util" + "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/cmd/dockerregistry" + deployapi "github.com/openshift/origin/pkg/deploy/api" + imageapi "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/registry/imagestreamimage" +) + +// pruneAlgorithm contains the various settings to use when evaluating images +// and layers for pruning. +type pruneAlgorithm struct { + registryURLs []string + minimumAgeInMinutesToPrune int + tagRevisionsToKeep int +} + +// externalImage returns true if the image belongs to an external Docker +// registry; i.e., a registry not controlled by OpenShift. +func (pa pruneAlgorithm) externalImage(image string) bool { + for _, url := range pa.registryURLs { + if strings.HasPrefix(image, url) { + return false + } + } + + return true +} + +// ImagePruneFunc is a function that is invoked for each image that is +// prunable, along with the list of image streams that reference it. +type ImagePruneFunc func(image *imageapi.Image, streams []*imageapi.ImageStream) []error + +// LayerPruneFunc is a function that is invoked for each registry, along with +// a DeleteLayersRequest that contains the layers that can be pruned and the +// image stream names that reference each layer. +type LayerPruneFunc func(registryURL string, req dockerregistry.DeleteLayersRequest) []error + +// ImagePruner knows how to prune images and layers. +type ImagePruner interface { + // Run prunes images and layers. + Run(imagePruneFunc ImagePruneFunc, layerPruneFunc LayerPruneFunc) +} + +// imagePruner implements ImagePruner. +type imagePruner struct { + g graph.Graph + algorithm pruneAlgorithm +} + +var _ ImagePruner = &imagePruner{} + +/* +NewImagePruner creates a new ImagePruner. + +registryURLs is a list of OpenShift registries. Only images with URLs +belonging to this list are candidates for pruning. + +minimumAgeInMinutesToPrune is the minimum age, in minutes, that a resource +must be in order for the image it references (or an image itself) to be a +candidate for pruning. For example, if minimumAgeInMinutesToPrune is 60, and +an ImageStream is only 59 minutes old, none of the images it references are +eligible for pruning. + +tagRevisionsToKeep is the number of revisions per tag in an image stream's +status.tags that are preserved and ineligible for pruning. Any revision older +than tagRevisionsToKeep is eligible for pruning. + +images, streams, pods, rcs, bcs, builds, and dcs are client interfaces for +retrieving each respective resource type. + +The ImagePruner performs the following logic: remove any image belonging to the +specified registry URL(s) that was created at least *n* minutes ago and is +*not* currently referenced by: + +- any pod created less than *n* minutes ago +- any image stream created less than *n* minutes ago +- any running pods +- any pending pods +- any replication controllers +- any deployment configs +- any build configs +- any builds +- the n most recent tag revisions in an image stream's status.tags + +When removing an image, remove all references to the image from all +ImageStreams having a reference to the image in `status.tags`. + +Also automatically remove any image layer that is no longer referenced by any +images. +*/ +func NewImagePruner(registryURLs []string, minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, images client.ImagesInterfacer, streams client.ImageStreamsNamespacer, pods kclient.PodsNamespacer, rcs kclient.ReplicationControllersNamespacer, bcs client.BuildConfigsNamespacer, builds client.BuildsNamespacer, dcs client.DeploymentConfigsNamespacer) (ImagePruner, error) { + allImages, err := images.Images().List(labels.Everything(), fields.Everything()) + if err != nil { + return nil, fmt.Errorf("Error listing images: %v", err) + } + + allStreams, err := streams.ImageStreams(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) + if err != nil { + return nil, fmt.Errorf("Error listing image streams: %v", err) + } + + allPods, err := pods.Pods(kapi.NamespaceAll).List(labels.Everything()) + if err != nil { + return nil, fmt.Errorf("Error listing pods: %v", err) + } + + allRCs, err := rcs.ReplicationControllers(kapi.NamespaceAll).List(labels.Everything()) + if err != nil { + return nil, fmt.Errorf("Error listing replication controllers: %v", err) + } + + allBCs, err := bcs.BuildConfigs(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) + if err != nil { + return nil, fmt.Errorf("Error listing build configs: %v", err) + } + + allBuilds, err := builds.Builds(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) + if err != nil { + return nil, fmt.Errorf("Error listing builds: %v", err) + } + + allDCs, err := dcs.DeploymentConfigs(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) + if err != nil { + return nil, fmt.Errorf("Error listing deployment configs: %v", err) + } + + return newImagePruner(registryURLs, minimumAgeInMinutesToPrune, tagRevisionsToKeep, allImages, allStreams, allPods, allRCs, allBCs, allBuilds, allDCs), nil +} + +// newImagePruner creates a new ImagePruner. +func newImagePruner(registryURLs []string, minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, images *imageapi.ImageList, streams *imageapi.ImageStreamList, pods *kapi.PodList, rcs *kapi.ReplicationControllerList, bcs *buildapi.BuildConfigList, builds *buildapi.BuildList, dcs *deployapi.DeploymentConfigList) ImagePruner { + g := graph.New() + + glog.V(1).Infof("Creating image pruner with registryURLs=%v, minimumAgeInMinutesToPrune=%d, tagRevisionsToKeep=%d", registryURLs, minimumAgeInMinutesToPrune, tagRevisionsToKeep) + + algorithm := pruneAlgorithm{ + registryURLs: registryURLs, + minimumAgeInMinutesToPrune: minimumAgeInMinutesToPrune, + tagRevisionsToKeep: tagRevisionsToKeep, + } + + addImagesToGraph(g, images, algorithm) + addImageStreamsToGraph(g, streams, algorithm) + addPodsToGraph(g, pods, algorithm) + addReplicationControllersToGraph(g, rcs, algorithm) + addBuildConfigsToGraph(g, bcs, algorithm) + addBuildsToGraph(g, builds, algorithm) + addDeploymentConfigsToGraph(g, dcs, algorithm) + + return &imagePruner{ + g: g, + algorithm: algorithm, + } +} + +// addImagesToGraph adds all images to the graph that belong to one of the +// registries in the algorithm and are at least as old as the minimum age +// threshold as specified by the algorithm. It also adds all the images' layers +// to the graph. +func addImagesToGraph(g graph.Graph, images *imageapi.ImageList, algorithm pruneAlgorithm) { + for i := range images.Items { + image := &images.Items[i] + + if algorithm.externalImage(image.DockerImageReference) { + glog.V(4).Infof("Image %q belongs to an external registry - skipping", image.DockerImageReference) + continue + } + + age := util.Now().Sub(image.CreationTimestamp.Time) + ageInMinutes := int(age.Minutes()) + if ageInMinutes < algorithm.minimumAgeInMinutesToPrune { + glog.V(4).Infof("Image %q is younger than minimum pruning age, skipping (age=%d)", image.Name, ageInMinutes) + continue + } + + glog.V(4).Infof("Adding image %q to graph", image.Name) + imageNode := graph.Image(g, image) + + manifest := imageapi.DockerImageManifest{} + if err := json.Unmarshal([]byte(image.DockerImageManifest), &manifest); err != nil { + glog.Errorf("Unable to extract manifest from image: %v. This image's layers won't be pruned if the image is pruned now.", err) + } + + for _, layer := range manifest.FSLayers { + glog.V(4).Infof("Adding image layer %q to graph", layer.DockerBlobSum) + layerNode := graph.ImageLayer(g, layer.DockerBlobSum) + g.AddEdge(imageNode, layerNode, graph.ReferencedImageLayerGraphEdgeKind) + } + } +} + +// addImageStreamsToGraph adds all the streams to the graph. The most recent n +// image revisions for a tag will be preserved, where n is specified by the +// algorithm's tagRevisionsToKeep. Image revisions older than n are candidates +// for pruning. if the image stream's age is at least as old as the minimum +// threshold in algorithm. Otherwise, if the image stream is younger than the +// threshold, all image revisions for that stream are ineligible for pruning. +// +// addImageStreamsToGraph also adds references from each stream to all the +// layers it references (via each image a stream references). +func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, algorithm pruneAlgorithm) { + for i := range streams.Items { + stream := &streams.Items[i] + + // use a weak reference for old image revisions by default + oldImageRevisionReferenceKind := graph.WeakReferencedImageGraphEdgeKind + + age := util.Now().Sub(stream.CreationTimestamp.Time) + if int(age.Minutes()) < algorithm.minimumAgeInMinutesToPrune { + // stream's age is below threshold - use a strong reference for old image revisions instead + glog.V(4).Infof("Stream %s/%s is below age threshold - none of its images are eligible for pruning", stream.Namespace, stream.Name) + oldImageRevisionReferenceKind = graph.ReferencedImageGraphEdgeKind + } + + glog.V(4).Infof("Adding image stream %s/%s to graph", stream.Namespace, stream.Name) + isNode := graph.ImageStream(g, stream) + imageStreamNode := isNode.(*graph.ImageStreamNode) + + for tag, history := range stream.Status.Tags { + for i := range history.Items { + if algorithm.externalImage(history.Items[i].DockerImageReference) { + glog.V(4).Infof("Tag %q revision %d points to %s which is part of an external registry; skipping", tag, i, history.Items[i].DockerImageReference) + continue + } + + n := graph.FindImage(g, history.Items[i].Image) + if n == nil { + glog.V(1).Infof("Unable to find image %q in graph", history.Items[i].Image) + continue + } + imageNode := n.(*graph.ImageNode) + + var kind int + switch { + case i < algorithm.tagRevisionsToKeep: + kind = graph.ReferencedImageGraphEdgeKind + default: + kind = oldImageRevisionReferenceKind + } + glog.V(4).Infof("Adding edge (kind=%d) from %q to %q", kind, imageStreamNode.UniqueName.UniqueName(), imageNode.UniqueName.UniqueName()) + g.AddEdge(imageStreamNode, imageNode, kind) + + glog.V(4).Infof("Adding stream->layer references") + // add stream -> layer references so we can prune them later + for _, s := range g.Successors(imageNode) { + if g.Kind(s) != graph.ImageLayerGraphKind { + continue + } + glog.V(4).Infof("Adding reference from stream %q to layer %q", stream.Name, s.(*graph.ImageLayerNode).Layer) + g.AddEdge(imageStreamNode, s, graph.ReferencedImageLayerGraphEdgeKind) + } + } + } + } +} + +// addPodsToGraph adds pods to the graph. +// +// A pod is only *excluded* from being added to the graph if its phase is not +// pending or running and it is at least as old as the minimum age threshold +// defined by algorithm. +// +// Edges are added to the graph from each pod to the images specified by that +// pod's list of containers, as long as the image belongs to one of the +// registries specified in algorithm. +func addPodsToGraph(g graph.Graph, pods *kapi.PodList, algorithm pruneAlgorithm) { + for i := range pods.Items { + pod := &pods.Items[i] + + if pod.Status.Phase != kapi.PodRunning && pod.Status.Phase != kapi.PodPending { + age := util.Now().Sub(pod.CreationTimestamp.Time) + if int(age.Minutes()) >= algorithm.minimumAgeInMinutesToPrune { + glog.V(4).Infof("Pod %s/%s is not running or pending and age is at least minimum pruning age - skipping", pod.Namespace, pod.Name) + // not pending or running, age is at least minimum pruning age, skip + continue + } + } + + glog.V(4).Infof("Adding pod %s/%s to graph", pod.Namespace, pod.Name) + podNode := graph.Pod(g, pod) + + addPodSpecToGraph(g, &pod.Spec, podNode, algorithm) + } +} + +// Edges are added to the graph from each predecessor (pod or replication +// controller) to the images specified by the pod spec's list of containers, as +// long as the image belongs to one of the registries specified in algorithm. +func addPodSpecToGraph(g graph.Graph, spec *kapi.PodSpec, predecessor gonum.Node, algorithm pruneAlgorithm) { + for j := range spec.Containers { + container := spec.Containers[j] + + glog.V(4).Infof("Examining container image %q", container.Image) + if algorithm.externalImage(container.Image) { + glog.V(4).Infof("Image belongs to an external registry - skipping") + continue + } + + ref, err := imageapi.ParseDockerImageReference(container.Image) + if err != nil { + glog.Errorf("Unable to parse docker image reference %q: %v", container.Image, err) + continue + } + + if len(ref.ID) == 0 { + glog.Errorf("Missing image ID") + continue + } + + imageNode := graph.FindImage(g, ref.ID) + if imageNode == nil { + glog.Errorf("Expected to find image %q in the graph, but it was missing", ref.ID) + continue + } + + glog.V(4).Infof("Adding edge from pod to image") + g.AddEdge(predecessor, imageNode, graph.ReferencedImageGraphEdgeKind) + } +} + +// addReplicationControllersToGraph adds replication controllers to the graph. +// +// Edges are added to the graph from each replication controller to the images +// specified by its pod spec's list of containers, as long as the image belongs +// to one of the registries specified in algorithm. +func addReplicationControllersToGraph(g graph.Graph, rcs *kapi.ReplicationControllerList, algorithm pruneAlgorithm) { + for i := range rcs.Items { + rc := &rcs.Items[i] + rcNode := graph.ReplicationController(g, rc) + addPodSpecToGraph(g, &rc.Spec.Template.Spec, rcNode, algorithm) + } +} + +// addDeploymentConfigsToGraph adds deployment configs to the graph. +// +// Edges are added to the graph from each deployment config to the images +// specified by its pod spec's list of containers, as long as the image belongs +// to one of the registries specified in algorithm. +func addDeploymentConfigsToGraph(g graph.Graph, dcs *deployapi.DeploymentConfigList, algorithm pruneAlgorithm) { + for i := range dcs.Items { + dc := &dcs.Items[i] + dcNode := graph.DeploymentConfig(g, dc) + addPodSpecToGraph(g, &dc.Template.ControllerTemplate.Template.Spec, dcNode, algorithm) + } +} + +// addBuildConfigsToGraph adds build configs to the graph. +// +// Edges are added to the graph from each build config to the image specified by its strategy.from. +func addBuildConfigsToGraph(g graph.Graph, bcs *buildapi.BuildConfigList, algorithm pruneAlgorithm) { + for i := range bcs.Items { + bc := &bcs.Items[i] + bcNode := graph.BuildConfig(g, bc) + addBuildStrategyImageReferencesToGraph(g, bc.Parameters.Strategy, bcNode, algorithm) + } +} + +// addBuildsToGraph adds builds to the graph. +// +// Edges are added to the graph from each build to the image specified by its strategy.from. +func addBuildsToGraph(g graph.Graph, builds *buildapi.BuildList, algorithm pruneAlgorithm) { + for i := range builds.Items { + build := &builds.Items[i] + buildNode := graph.Build(g, build) + addBuildStrategyImageReferencesToGraph(g, build.Parameters.Strategy, buildNode, algorithm) + } +} + +// Edges are added to the graph from each predecessor (build or build config) +// to the image specified by strategy.from, as long as the image belongs to one +// of the registries specified in algorithm. +func addBuildStrategyImageReferencesToGraph(g graph.Graph, strategy buildapi.BuildStrategy, predecessor gonum.Node, algorithm pruneAlgorithm) { + from := buildutil.GetImageStreamForStrategy(strategy) + if from == nil { + return + } + + var imageID string + + switch from.Kind { + case "ImageStreamImage": + _, id, err := imagestreamimage.ParseNameAndID(from.Name) + if err != nil { + return + } + imageID = id + case "DockerImage": + if algorithm.externalImage(from.Name) { + return + } + ref, err := imageapi.ParseDockerImageReference(from.Name) + if err != nil { + return + } + imageID = ref.ID + default: + return + } + + imageNode := graph.FindImage(g, imageID) + if imageNode == nil { + return + } + + g.AddEdge(predecessor, imageNode, graph.ReferencedImageGraphEdgeKind) +} + +// imageNodeSubgraph returns only nodes of type ImageNode. +func imageNodeSubgraph(nodes []gonum.Node) []*graph.ImageNode { + ret := []*graph.ImageNode{} + for i := range nodes { + if node, ok := nodes[i].(*graph.ImageNode); ok { + ret = append(ret, node) + } + } + return ret +} + +// edgeKind returns true if the edge from "from" to "to" is of the desired kind. +func edgeKind(g graph.Graph, from, to gonum.Node, desiredKind int) bool { + edge := g.EdgeBetween(from, to) + kind := g.EdgeKind(edge) + return kind == desiredKind +} + +// imageIsPrunable returns true iff the image node only has weak references +// from its predecessors to it. A weak reference to an image is a reference +// from an image stream to an image where the image is not the current image +// for a tag and the image stream is at least as old as the minimum pruning +// age. +func imageIsPrunable(g graph.Graph, imageNode *graph.ImageNode) bool { + onlyWeakReferences := true + + for _, n := range g.Predecessors(imageNode) { + glog.V(4).Infof("Examining predecessor %#v", n) + if !edgeKind(g, n, imageNode, graph.WeakReferencedImageGraphEdgeKind) { + glog.V(4).Infof("Strong reference detected") + onlyWeakReferences = false + break + } + } + + return onlyWeakReferences + +} + +// pruneImages invokes imagePruneFunc with each image that is prunable, along +// with the image streams that reference the image. After imagePruneFunc is +// invoked, the image node is removed from the graph, so that layers eligible +// for pruning may later be identified. +func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, imagePruneFunc ImagePruneFunc) { + for _, imageNode := range imageNodes { + glog.V(4).Infof("Examining image %q", imageNode.Image.Name) + + if !imageIsPrunable(g, imageNode) { + glog.V(4).Infof("Image has strong references - not pruning") + continue + } + + glog.V(4).Infof("Image has only weak references - pruning") + + streams := imageStreamPredecessors(g, imageNode) + if errs := imagePruneFunc(imageNode.Image, streams); len(errs) > 0 { + //TODO + glog.Errorf("Error pruning image %q: %v", imageNode.Image.Name, errs) + } + + // remove pruned image node from graph, for layer pruning later + g.RemoveNode(imageNode) + } +} + +// Run identifies images eligible for pruning, invoking imagePruneFunc for each +// image, and then it identifies layers eligible for pruning, invoking +// layerPruneFunc for each registry URL that has layers that can be pruned. +func (p *imagePruner) Run(imagePruneFunc ImagePruneFunc, layerPruneFunc LayerPruneFunc) { + allNodes := p.g.NodeList() + + imageNodes := imageNodeSubgraph(allNodes) + pruneImages(p.g, imageNodes, imagePruneFunc) + + layerNodes := layerNodeSubgraph(allNodes) + pruneLayers(p.g, layerNodes, layerPruneFunc) +} + +// layerNodeSubgraph returns the subset of nodes that are ImageLayerNodes. +func layerNodeSubgraph(nodes []gonum.Node) []*graph.ImageLayerNode { + ret := []*graph.ImageLayerNode{} + for i := range nodes { + if node, ok := nodes[i].(*graph.ImageLayerNode); ok { + ret = append(ret, node) + } + } + return ret +} + +// layerIsPrunable returns true if the layer is not referenced by any images. +func layerIsPrunable(g graph.Graph, layerNode *graph.ImageLayerNode) bool { + for _, predecessor := range g.Predecessors(layerNode) { + glog.V(4).Infof("Examining layer predecessor %#v", predecessor) + if g.Kind(predecessor) == graph.ImageGraphKind { + glog.V(4).Infof("Layer has an image predecessor") + return false + } + } + + return true +} + +// streamLayerReferences returns a list of ImageStreamNodes that reference a +// given ImageLayeNode. +func streamLayerReferences(g graph.Graph, layerNode *graph.ImageLayerNode) []*graph.ImageStreamNode { + ret := []*graph.ImageStreamNode{} + + for _, predecessor := range g.Predecessors(layerNode) { + if g.Kind(predecessor) != graph.ImageStreamGraphKind { + continue + } + + ret = append(ret, predecessor.(*graph.ImageStreamNode)) + } + + return ret +} + +// pruneLayers creates a mapping of registryURLs to +// dockerregistry.DeleteLayersRequest objects, invoking layerPruneFunc for each +// registryURL and request. +func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFunc LayerPruneFunc) { + registryDeletionRequests := map[string]dockerregistry.DeleteLayersRequest{} + + for _, layerNode := range layerNodes { + glog.V(4).Infof("Examining layer %q", layerNode.Layer) + + if !layerIsPrunable(g, layerNode) { + glog.V(4).Infof("Layer %q has image references - not pruning", layerNode.Layer) + continue + } + + // get streams that reference layer + streamNodes := streamLayerReferences(g, layerNode) + + // for each stream, get its registry + for _, streamNode := range streamNodes { + stream := streamNode.ImageStream + streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) + glog.V(4).Infof("Layer has an image stream predecessor: %s", streamName) + + ref, err := imageapi.DockerImageReferenceForStream(stream) + if err != nil { + //TODO + glog.Errorf("Error constructing DockerImageReference for %q", streamName) + continue + } + + // update registry layer deletion request + glog.V(4).Infof("Looking for existing deletion request for registry %q", ref.Registry) + deletionRequest, ok := registryDeletionRequests[ref.Registry] + if !ok { + glog.V(4).Infof("Request not found - creating new one") + deletionRequest = dockerregistry.DeleteLayersRequest{} + registryDeletionRequests[ref.Registry] = deletionRequest + } + + glog.V(4).Infof("Adding or updating layer %q in deletion request", layerNode.Layer) + deletionRequest.AddLayer(layerNode.Layer) + + glog.V(4).Infof("Adding stream %q", streamName) + deletionRequest.AddStream(layerNode.Layer, streamName) + } + } + + for registryURL, req := range registryDeletionRequests { + glog.V(4).Infof("Invoking layerPruneFunc with registry=%q, req=%#v", registryURL, req) + layerPruneFunc(registryURL, req) + } +} + +// DescribingImagePruneFunc returns an ImagePruneFunc that writes information +// about the images that are eligible for pruning to out. +func DescribingImagePruneFunc(out io.Writer) ImagePruneFunc { + return func(image *imageapi.Image, referencedStreams []*imageapi.ImageStream) []error { + streamNames := []string{} + for _, stream := range referencedStreams { + streamNames = append(streamNames, fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)) + } + fmt.Fprintf(out, "Pruning image %q and updating image streams %v\n", image.Name, streamNames) + return []error{} + } +} + +// DeletingImagePruneFunc returns an ImagePruneFunc that deletes the image and +// removes it from each referencing ImageStream's status.tags. +func DeletingImagePruneFunc(images client.ImageInterface, streams client.ImageStreamsNamespacer) ImagePruneFunc { + return func(image *imageapi.Image, referencedStreams []*imageapi.ImageStream) []error { + result := []error{} + + glog.V(4).Infof("Deleting image %q", image.Name) + if err := images.Delete(image.Name); err != nil { + e := fmt.Errorf("Error deleting image: %v", err) + glog.Error(e) + result = append(result, e) + return result + } + + for _, stream := range referencedStreams { + glog.V(4).Infof("Checking if stream %s/%s has references to image in status.tags", stream.Namespace, stream.Name) + for tag, history := range stream.Status.Tags { + glog.V(4).Infof("Checking tag %q", tag) + newHistory := imageapi.TagEventList{Items: []imageapi.TagEvent{history.Items[0]}} + for i, tagEvent := range history.Items[1:] { + glog.V(4).Infof("Checking tag event %d with image %q", i+1, tagEvent.Image) + if tagEvent.Image != image.Name { + glog.V(4).Infof("Tag event doesn't match deleting image - keeping") + newHistory.Items = append(newHistory.Items, tagEvent) + } + } + stream.Status.Tags[tag] = newHistory + } + + glog.V(4).Infof("Updating image stream %s/%s", stream.Namespace, stream.Name) + glog.V(5).Infof("Updated stream: %#v", stream) + if _, err := streams.ImageStreams(stream.Namespace).UpdateStatus(stream); err != nil { + e := fmt.Errorf("Unable to update image stream status %s/%s: %v", stream.Namespace, stream.Name, err) + glog.Error(e) + result = append(result, e) + } + } + + return result + } +} + +// DescribingLayerPruneFunc returns a LayerPruneFunc that writes information +// about the layers that are eligible for pruning to out. +func DescribingLayerPruneFunc(out io.Writer) LayerPruneFunc { + return func(registryURL string, deletions dockerregistry.DeleteLayersRequest) []error { + fmt.Fprintf(out, "Pruning from registry %q\n", registryURL) + for layer, repos := range deletions { + fmt.Fprintf(out, "\tLayer %q\n", layer) + if len(repos) > 0 { + fmt.Fprint(out, "\tReferenced streams:\n") + } + for _, repo := range repos { + fmt.Fprintf(out, "\t\t%q\n", repo) + } + } + return []error{} + } +} + +// DeletingLayerPruneFunc returns a LayerPruneFunc that sends the +// DeleteLayersRequest to the Docker registry. +// +// The request URL is http://registryURL/admin/layers and it is a DELETE +// request. +// +// The body of the request is JSON, and it is a map[string][]string, with each +// key being a layer, and each value being a list of Docker image repository +// names referenced by the layer. +func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { + return func(registryURL string, deletions dockerregistry.DeleteLayersRequest) []error { + glog.V(4).Infof("Starting pruning of layers from %q, req %#v", registryURL, deletions) + body, err := json.Marshal(&deletions) + if err != nil { + glog.Errorf("Error marshaling request body: %v", err) + return []error{fmt.Errorf("Error creating request body: %v", err)} + } + + //TODO https + req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/admin/layers", registryURL), bytes.NewReader(body)) + if err != nil { + glog.Errorf("Error creating request: %v", err) + return []error{fmt.Errorf("Error creating request: %v", err)} + } + + glog.V(4).Infof("Sending request to registry") + resp, err := registryClient.Do(req) + if err != nil { + glog.Errorf("Error sending request: %v", err) + return []error{fmt.Errorf("Error sending request: %v", err)} + } + defer resp.Body.Close() + + //TODO read response + + return []error{} + } +} + +// imageStreamPredecessors returns a list of ImageStreams that are predecessors +// of imageNode. +func imageStreamPredecessors(g graph.Graph, imageNode *graph.ImageNode) []*imageapi.ImageStream { + streams := []*imageapi.ImageStream{} + + for _, n := range g.Predecessors(imageNode) { + if streamNode, ok := n.(*graph.ImageStreamNode); ok { + streams = append(streams, streamNode.ImageStream) + } + } + + return streams +} diff --git a/pkg/image/prune/imagepruner_test.go b/pkg/image/prune/imagepruner_test.go new file mode 100644 index 000000000000..21337538eebf --- /dev/null +++ b/pkg/image/prune/imagepruner_test.go @@ -0,0 +1,582 @@ +package prune + +import ( + "flag" + "fmt" + "reflect" + "testing" + "time" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + buildapi "github.com/openshift/origin/pkg/build/api" + "github.com/openshift/origin/pkg/client" + "github.com/openshift/origin/pkg/cmd/dockerregistry" + deployapi "github.com/openshift/origin/pkg/deploy/api" + imageapi "github.com/openshift/origin/pkg/image/api" +) + +func imageList(images ...imageapi.Image) imageapi.ImageList { + return imageapi.ImageList{ + Items: images, + } +} + +func agedImage(id, ref string, ageInMinutes int64) imageapi.Image { + image := imageapi.Image{ + ObjectMeta: kapi.ObjectMeta{ + Name: id, + }, + DockerImageReference: ref, + DockerImageManifest: `{ + "fsLayers": [ + { + "blobSum": "tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "blobSum": "tarsum.dev+sha256:b194de3772ebbcdc8f244f663669799ac1cb141834b7cb8b69100285d357a2b0" + }, + { + "blobSum": "tarsum.dev+sha256:c937c4bb1c1a21cc6d94340812262c6472092028972ae69b551b1a70d4276171" + }, + { + "blobSum": "tarsum.dev+sha256:2aaacc362ac6be2b9e9ae8c6029f6f616bb50aec63746521858e47841b90fabd" + }, + { + "blobSum": "tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + ] + }`, + } + + if ageInMinutes >= 0 { + image.CreationTimestamp = util.NewTime(util.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute)) + } + + return image +} + +func image(id, ref string) imageapi.Image { + return agedImage(id, ref, -1) +} + +func podList(pods ...kapi.Pod) kapi.PodList { + return kapi.PodList{ + Items: pods, + } +} + +func pod(namespace, name string, phase kapi.PodPhase, containerImages ...string) kapi.Pod { + return agedPod(namespace, name, phase, -1, containerImages...) +} + +func agedPod(namespace, name string, phase kapi.PodPhase, ageInMinutes int64, containerImages ...string) kapi.Pod { + pod := kapi.Pod{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: podSpec(containerImages...), + Status: kapi.PodStatus{ + Phase: phase, + }, + } + + if ageInMinutes >= 0 { + pod.CreationTimestamp = util.NewTime(util.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute)) + } + + return pod +} + +func podSpec(containerImages ...string) kapi.PodSpec { + spec := kapi.PodSpec{ + Containers: []kapi.Container{}, + } + for _, image := range containerImages { + container := kapi.Container{ + Image: image, + } + spec.Containers = append(spec.Containers, container) + } + return spec +} + +func streamList(streams ...imageapi.ImageStream) imageapi.ImageStreamList { + return imageapi.ImageStreamList{ + Items: streams, + } +} + +func stream(namespace, name string, tags map[string]imageapi.TagEventList) imageapi.ImageStream { + return agedStream(namespace, name, -1, tags) +} + +func agedStream(namespace, name string, ageInMinutes int64, tags map[string]imageapi.TagEventList) imageapi.ImageStream { + stream := imageapi.ImageStream{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Status: imageapi.ImageStreamStatus{ + Tags: tags, + }, + } + + if ageInMinutes >= 0 { + stream.CreationTimestamp = util.NewTime(util.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute)) + } + + return stream +} + +func streamPtr(namespace, name string, tags map[string]imageapi.TagEventList) *imageapi.ImageStream { + s := stream(namespace, name, tags) + return &s +} + +func tags(list ...namedTagEventList) map[string]imageapi.TagEventList { + m := make(map[string]imageapi.TagEventList, len(list)) + for _, tag := range list { + m[tag.name] = tag.events + } + return m +} + +type namedTagEventList struct { + name string + events imageapi.TagEventList +} + +func tag(name string, events ...imageapi.TagEvent) namedTagEventList { + return namedTagEventList{ + name: name, + events: imageapi.TagEventList{ + Items: events, + }, + } +} + +func tagEvent(id, ref string) imageapi.TagEvent { + return imageapi.TagEvent{ + Image: id, + DockerImageReference: ref, + } +} + +func rcList(rcs ...kapi.ReplicationController) kapi.ReplicationControllerList { + return kapi.ReplicationControllerList{ + Items: rcs, + } +} + +func rc(namespace, name string, containerImages ...string) kapi.ReplicationController { + return kapi.ReplicationController{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Spec: kapi.ReplicationControllerSpec{ + Template: &kapi.PodTemplateSpec{ + Spec: podSpec(containerImages...), + }, + }, + } +} + +func dcList(dcs ...deployapi.DeploymentConfig) deployapi.DeploymentConfigList { + return deployapi.DeploymentConfigList{ + Items: dcs, + } +} + +func dc(namespace, name string, containerImages ...string) deployapi.DeploymentConfig { + return deployapi.DeploymentConfig{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Template: deployapi.DeploymentTemplate{ + ControllerTemplate: kapi.ReplicationControllerSpec{ + Template: &kapi.PodTemplateSpec{ + Spec: podSpec(containerImages...), + }, + }, + }, + } +} + +func bcList(bcs ...buildapi.BuildConfig) buildapi.BuildConfigList { + return buildapi.BuildConfigList{ + Items: bcs, + } +} + +func bc(namespace, name string, strategyType buildapi.BuildStrategyType, fromKind, fromNamespace, fromName string) buildapi.BuildConfig { + return buildapi.BuildConfig{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Parameters: buildParameters(strategyType, fromKind, fromNamespace, fromName), + } +} + +func buildList(builds ...buildapi.Build) buildapi.BuildList { + return buildapi.BuildList{ + Items: builds, + } +} + +func build(namespace, name string, strategyType buildapi.BuildStrategyType, fromKind, fromNamespace, fromName string) buildapi.Build { + return buildapi.Build{ + ObjectMeta: kapi.ObjectMeta{ + Namespace: namespace, + Name: name, + }, + Parameters: buildParameters(strategyType, fromKind, fromNamespace, fromName), + } +} + +func buildParameters(strategyType buildapi.BuildStrategyType, fromKind, fromNamespace, fromName string) buildapi.BuildParameters { + params := buildapi.BuildParameters{ + Strategy: buildapi.BuildStrategy{ + Type: strategyType, + }, + } + switch strategyType { + case buildapi.STIBuildStrategyType: + params.Strategy.STIStrategy = &buildapi.STIBuildStrategy{ + From: &kapi.ObjectReference{ + Kind: fromKind, + Namespace: fromNamespace, + Name: fromName, + }, + } + case buildapi.DockerBuildStrategyType: + params.Strategy.DockerStrategy = &buildapi.DockerBuildStrategy{ + From: &kapi.ObjectReference{ + Kind: fromKind, + Namespace: fromNamespace, + Name: fromName, + }, + } + case buildapi.CustomBuildStrategyType: + params.Strategy.CustomStrategy = &buildapi.CustomBuildStrategy{ + From: &kapi.ObjectReference{ + Kind: fromKind, + Namespace: fromNamespace, + Name: fromName, + }, + } + } + + return params +} + +var logLevel = flag.Int("loglevel", 0, "") + +func TestRun(t *testing.T) { + flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel)) + registryURL := "registry" + + tests := map[string]struct { + registryURLs []string + images imageapi.ImageList + pods kapi.PodList + streams imageapi.ImageStreamList + rcs kapi.ReplicationControllerList + bcs buildapi.BuildConfigList + builds buildapi.BuildList + dcs deployapi.DeploymentConfigList + expectedDeletions []string + expectedUpdatedStreams []string + }{ + "1 pod - phase pending - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList(pod("foo", "pod1", kapi.PodPending, registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "3 pods - last phase pending - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList( + pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id"), + pod("foo", "pod2", kapi.PodSucceeded, registryURL+"/foo/bar@id"), + pod("foo", "pod3", kapi.PodPending, registryURL+"/foo/bar@id"), + ), + expectedDeletions: []string{}, + }, + "1 pod - phase running - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList(pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "3 pods - last phase running - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList( + pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id"), + pod("foo", "pod2", kapi.PodSucceeded, registryURL+"/foo/bar@id"), + pod("foo", "pod3", kapi.PodRunning, registryURL+"/foo/bar@id"), + ), + expectedDeletions: []string{}, + }, + "pod phase succeeded - prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList(pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id")), + expectedDeletions: []string{"id"}, + }, + "pod phase succeeded, pod less than min pruning age - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList(agedPod("foo", "pod1", kapi.PodSucceeded, 5, registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "pod phase succeeded, image less than min pruning age - don't prune": { + images: imageList(agedImage("id", registryURL+"/foo/bar@id", 5)), + pods: podList(pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "pod phase failed - prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList( + pod("foo", "pod1", kapi.PodFailed, registryURL+"/foo/bar@id"), + pod("foo", "pod2", kapi.PodFailed, registryURL+"/foo/bar@id"), + pod("foo", "pod3", kapi.PodFailed, registryURL+"/foo/bar@id"), + ), + expectedDeletions: []string{"id"}, + }, + "pod phase unknown - prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList( + pod("foo", "pod1", kapi.PodUnknown, registryURL+"/foo/bar@id"), + pod("foo", "pod2", kapi.PodUnknown, registryURL+"/foo/bar@id"), + pod("foo", "pod3", kapi.PodUnknown, registryURL+"/foo/bar@id"), + ), + expectedDeletions: []string{"id"}, + }, + "referenced by rc - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + rcs: rcList(rc("foo", "rc1", registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "referenced by dc - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + dcs: dcList(dc("foo", "rc1", registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "referenced by bc - sti - ImageStreamImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.STIBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), + expectedDeletions: []string{}, + }, + "referenced by bc - docker - ImageStreamImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.DockerBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), + expectedDeletions: []string{}, + }, + "referenced by bc - custom - ImageStreamImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.CustomBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), + expectedDeletions: []string{}, + }, + "referenced by bc - sti - DockerImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.STIBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "referenced by bc - docker - DockerImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.DockerBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "referenced by bc - custom - DockerImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.CustomBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "referenced by build - sti - ImageStreamImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + builds: buildList(build("foo", "build1", buildapi.STIBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), + expectedDeletions: []string{}, + }, + "referenced by build - docker - ImageStreamImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + builds: buildList(build("foo", "build1", buildapi.DockerBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), + expectedDeletions: []string{}, + }, + "referenced by build - custom - ImageStreamImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + builds: buildList(build("foo", "build1", buildapi.CustomBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), + expectedDeletions: []string{}, + }, + "referenced by build - sti - DockerImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + builds: buildList(build("foo", "build1", buildapi.STIBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "referenced by build - docker - DockerImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + builds: buildList(build("foo", "build1", buildapi.DockerBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "referenced by build - custom - DockerImage - don't prune": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + builds: buildList(build("foo", "build1", buildapi.CustomBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + expectedDeletions: []string{}, + }, + "image stream - keep most recent n images": { + images: imageList( + image("id", registryURL+"/foo/bar@id"), + image("id2", registryURL+"/foo/bar@id2"), + image("id3", registryURL+"/foo/bar@id3"), + image("id4", registryURL+"/foo/bar@id4"), + ), + streams: streamList( + stream("foo", "bar", tags( + tag("latest", + tagEvent("id", registryURL+"/foo/bar@id"), + tagEvent("id2", registryURL+"/foo/bar@id2"), + tagEvent("id3", registryURL+"/foo/bar@id3"), + tagEvent("id4", registryURL+"/foo/bar@id4"), + ), + )), + ), + expectedDeletions: []string{"id4"}, + expectedUpdatedStreams: []string{"foo/bar"}, + }, + "image stream age less than min pruning age - don't prune": { + images: imageList( + image("id", registryURL+"/foo/bar@id"), + image("id2", registryURL+"/foo/bar@id2"), + ), + streams: streamList( + agedStream("foo", "bar", 5, tags( + tag("latest", + tagEvent("id", registryURL+"/foo/bar@id"), + tagEvent("id2", registryURL+"/foo/bar@id2"), + ), + )), + ), + expectedDeletions: []string{}, + expectedUpdatedStreams: []string{}, + }, + "multiple resources pointing to image - don't prune": { + images: imageList( + image("id", registryURL+"/foo/bar@id"), + image("id2", registryURL+"/foo/bar@id2"), + ), + streams: streamList( + stream("foo", "bar", tags( + tag("latest", + tagEvent("id", registryURL+"/foo/bar@id"), + tagEvent("id2", registryURL+"/foo/bar@id2"), + ), + )), + ), + rcs: rcList(rc("foo", "rc1", registryURL+"/foo/bar@id2")), + pods: podList(pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@id2")), + dcs: dcList(dc("foo", "rc1", registryURL+"/foo/bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.STIBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + builds: buildList(build("foo", "build1", buildapi.CustomBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), + expectedDeletions: []string{}, + expectedUpdatedStreams: []string{}, + }, + } + + for name, test := range tests { + registryURLs := []string{registryURL} + if len(test.registryURLs) > 0 { + registryURLs = test.registryURLs + } + p := newImagePruner(registryURLs, 60, 3, &test.images, &test.streams, &test.pods, &test.rcs, &test.bcs, &test.builds, &test.dcs) + actualDeletions := util.NewStringSet() + actualUpdatedStreams := util.NewStringSet() + + imagePruneFunc := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { + actualDeletions.Insert(image.Name) + for _, stream := range streams { + actualUpdatedStreams.Insert(fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)) + } + return []error{} + } + + layerPruneFunc := func(registryURL string, req dockerregistry.DeleteLayersRequest) []error { + return []error{} + } + + p.Run(imagePruneFunc, layerPruneFunc) + + expectedDeletions := util.NewStringSet(test.expectedDeletions...) + if !reflect.DeepEqual(expectedDeletions, actualDeletions) { + t.Errorf("%s: expected image deletions %q, got %q", name, expectedDeletions.List(), actualDeletions.List()) + } + + expectedUpdatedStreams := util.NewStringSet(test.expectedUpdatedStreams...) + if !reflect.DeepEqual(expectedUpdatedStreams, actualUpdatedStreams) { + t.Errorf("%s: expected stream updates %q, got %q", name, expectedUpdatedStreams.List(), actualUpdatedStreams.List()) + } + } +} + +func TestDefaultImagePruneFunc(t *testing.T) { + tests := map[string]struct { + referencedStreams []*imageapi.ImageStream + expectedUpdates []*imageapi.ImageStream + }{ + "no referenced streams": { + referencedStreams: []*imageapi.ImageStream{}, + expectedUpdates: []*imageapi.ImageStream{}, + }, + "1 tag, 1 image revision": { + referencedStreams: []*imageapi.ImageStream{ + streamPtr("foo", "bar", tags( + tag("latest", + tagEvent("id", "registry/foo/bar@id"), + ), + )), + }, + expectedUpdates: []*imageapi.ImageStream{}, + }, + "1 tag, multiple image revisions": { + referencedStreams: []*imageapi.ImageStream{ + streamPtr("foo", "bar", tags( + tag("latest", + tagEvent("id", "registry/foo/bar@id"), + tagEvent("id2", "registry/foo/bar@id2"), + ), + )), + }, + expectedUpdates: []*imageapi.ImageStream{ + streamPtr("foo", "bar", tags( + tag("latest", + tagEvent("id", "registry/foo/bar@id"), + ), + )), + }, + }, + } + + for name, test := range tests { + fakeClient := client.Fake{} + pruneFunc := DeletingImagePruneFunc(fakeClient.Images(), &fakeClient) + err := pruneFunc(&imageapi.Image{ObjectMeta: kapi.ObjectMeta{Name: "id2"}}, test.referencedStreams) + _ = err + + if len(fakeClient.Actions) < 1 { + t.Fatalf("%s: expected image deletion", name) + } + + if e, a := len(test.referencedStreams), len(fakeClient.Actions)-1; e != a { + t.Errorf("%s: expected %d stream updates, got %d", name, e, a) + } + + for i := range test.expectedUpdates { + if e, a := "update-status-imagestream", fakeClient.Actions[i+1].Action; e != a { + t.Errorf("%s: unexpected action %q", name, a) + } + updatedStream := fakeClient.Actions[i+1].Value.(*imageapi.ImageStream) + if e, a := test.expectedUpdates[i], updatedStream; !reflect.DeepEqual(e, a) { + t.Errorf("%s: unexpected updated stream: %s", name, util.ObjectDiff(e, a)) + } + } + } +} diff --git a/pkg/image/prune/summary.go b/pkg/image/prune/summary.go new file mode 100644 index 000000000000..2902d3cda761 --- /dev/null +++ b/pkg/image/prune/summary.go @@ -0,0 +1,66 @@ +package prune + +import ( + "fmt" + "io" + + "github.com/openshift/origin/pkg/cmd/dockerregistry" + imageapi "github.com/openshift/origin/pkg/image/api" +) + +type summarizingPruner struct { + delegate ImagePruner + out io.Writer + + imageSuccesses []string + imageFailures []string + imageErrors []error + + layerSuccesses []string + layerFailures []string + layerErrors []error +} + +var _ ImagePruner = &summarizingPruner{} + +func NewSummarizingImagePruner(pruner ImagePruner, out io.Writer) ImagePruner { + return &summarizingPruner{ + delegate: pruner, + out: out, + } +} + +func (p *summarizingPruner) Run(baseImagePruneFunc ImagePruneFunc, baseLayerPruneFunc LayerPruneFunc) { + p.delegate.Run(p.imagePruneFunc(baseImagePruneFunc), p.layerPruneFunc(baseLayerPruneFunc)) + p.summarize() +} + +func (p *summarizingPruner) summarize() { + fmt.Fprintln(p.out, "IMAGE PRUNING SUMMARY:") + fmt.Fprintf(p.out, "# Image prune successes: %d\n", len(p.imageSuccesses)) + fmt.Fprintf(p.out, "# Image prune errors: %d\n", len(p.imageFailures)) + fmt.Fprintln(p.out, "LAYER PRUNING SUMMARY:") + fmt.Fprintf(p.out, "# Layer prune successes: %d\n", len(p.layerSuccesses)) + fmt.Fprintf(p.out, "# Layer prune errors: %d\n", len(p.layerFailures)) +} + +func (p *summarizingPruner) imagePruneFunc(base ImagePruneFunc) ImagePruneFunc { + return func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { + errs := base(image, streams) + switch len(errs) { + case 0: + p.imageSuccesses = append(p.imageSuccesses, image.Name) + default: + p.imageFailures = append(p.imageFailures, image.Name) + p.imageErrors = append(p.imageErrors, errs...) + } + return errs + } +} + +func (p *summarizingPruner) layerPruneFunc(base LayerPruneFunc) LayerPruneFunc { + return func(registryURL string, req dockerregistry.DeleteLayersRequest) []error { + errs := base(registryURL, req) + return errs + } +} diff --git a/pkg/image/registry/imagestreamimage/rest.go b/pkg/image/registry/imagestreamimage/rest.go index 00ed888ffb0a..ca058ad0c42f 100644 --- a/pkg/image/registry/imagestreamimage/rest.go +++ b/pkg/image/registry/imagestreamimage/rest.go @@ -35,9 +35,9 @@ func (r *REST) New() runtime.Object { return &api.ImageStreamImage{} } -// nameAndID splits a string into its name component and ID component, and returns an error +// ParseNameAndID splits a string into its name component and ID component, and returns an error // if the string is not in the right form. -func nameAndID(input string) (name string, id string, err error) { +func ParseNameAndID(input string) (name string, id string, err error) { segments := strings.Split(input, "@") switch len(segments) { case 2: @@ -55,7 +55,7 @@ func nameAndID(input string) (name string, id string, err error) { // Get retrieves an image by ID that has previously been tagged into an image stream. // `id` is of the form @. func (r *REST) Get(ctx kapi.Context, id string) (runtime.Object, error) { - name, imageID, err := nameAndID(id) + name, imageID, err := ParseNameAndID(id) if err != nil { return nil, err } diff --git a/pkg/image/registry/imagestreamimage/rest_test.go b/pkg/image/registry/imagestreamimage/rest_test.go index 7b5b7fd38aad..e351100ff2b8 100644 --- a/pkg/image/registry/imagestreamimage/rest_test.go +++ b/pkg/image/registry/imagestreamimage/rest_test.go @@ -76,7 +76,7 @@ func TestNameAndID(t *testing.T) { } for name, test := range tests { - repo, id, err := nameAndID(test.input) + repo, id, err := ParseNameAndID(test.input) didError := err != nil if e, a := test.expectError, didError; e != a { t.Fatalf("%s: expected error=%t, got=%t: %s", name, e, a, err) From c0fc084b3bae50fa3c937c79293608b56549ec68 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Fri, 1 May 2015 15:33:08 -0400 Subject: [PATCH 06/21] Image pruning Add annotation to images indicating they're from a registry managed by OpenShift. This makes determining if an image is prunable significantly easier. Remove registry URLs pruning configuration, as the above annotation makes it no longer needed. --- pkg/cmd/experimental/imageprune/imageprune.go | 33 +--- .../server/repositorymiddleware.go | 3 + pkg/image/api/types.go | 2 + pkg/image/prune/imagepruner.go | 127 ++++++------- pkg/image/prune/imagepruner_test.go | 171 +++++++++++++----- 5 files changed, 198 insertions(+), 138 deletions(-) diff --git a/pkg/cmd/experimental/imageprune/imageprune.go b/pkg/cmd/experimental/imageprune/imageprune.go index ddce28cad2ce..91a8ab750c2d 100644 --- a/pkg/cmd/experimental/imageprune/imageprune.go +++ b/pkg/cmd/experimental/imageprune/imageprune.go @@ -5,7 +5,6 @@ import ( "io" "net/http" - kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/golang/glog" imageapi "github.com/openshift/origin/pkg/image/api" "github.com/openshift/origin/pkg/image/prune" @@ -18,32 +17,15 @@ import ( const longDesc = ` ` -type registryURLs []string - -func (u *registryURLs) Type() string { - return "string" -} - -func (u *registryURLs) String() string { - return fmt.Sprintf("%v", *u) -} - -func (u *registryURLs) Set(value string) error { - *u = append(*u, value) - return nil -} - type config struct { DryRun bool - RegistryURLs registryURLs MinimumResourcePruningAge int TagRevisionsToKeep int } func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command { cfg := &config{ - DryRun: true, - RegistryURLs: registryURLs{"docker-registry.default.local"}, + DryRun: true, MinimumResourcePruningAge: 60, TagRevisionsToKeep: 3, } @@ -63,13 +45,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri glog.Fatalf("Error getting client: %v", err) } - if registryService, err := kClient.Services(kapi.NamespaceDefault).Get("docker-registry"); err != nil { - glog.Errorf("Error getting docker-registry service: %v", err) - } else { - cfg.RegistryURLs = append(cfg.RegistryURLs, fmt.Sprintf("%s:%d", registryService.Spec.PortalIP, registryService.Spec.Ports[0].Port)) - } - - pruner, err := prune.NewImagePruner(cfg.RegistryURLs, cfg.MinimumResourcePruningAge, cfg.TagRevisionsToKeep, osClient, osClient, kClient, kClient, osClient, osClient, osClient) + pruner, err := prune.NewImagePruner(cfg.MinimumResourcePruningAge, cfg.TagRevisionsToKeep, osClient, osClient, kClient, kClient, osClient, osClient, osClient) if err != nil { glog.Fatalf("Error creating image pruner: %v", err) } @@ -103,9 +79,8 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri } cmd.Flags().BoolVar(&cfg.DryRun, "dry-run", cfg.DryRun, "Perform an image pruning dry-run, displaying what would be deleted but not actually deleting anything (default=true).") - cmd.Flags().Var(&cfg.RegistryURLs, "registry-urls", "TODO") - cmd.Flags().IntVar(&cfg.MinimumResourcePruningAge, "older-than", cfg.MinimumResourcePruningAge, "TODO") - cmd.Flags().IntVar(&cfg.TagRevisionsToKeep, "keep-tag-revisions", cfg.TagRevisionsToKeep, "TODO") + cmd.Flags().IntVar(&cfg.MinimumResourcePruningAge, "older-than", cfg.MinimumResourcePruningAge, "Specify the minimum age for an image to be prunable, as well as the minimum age for an image stream or pod that references an image to be prunable.") + cmd.Flags().IntVar(&cfg.TagRevisionsToKeep, "keep-tag-revisions", cfg.TagRevisionsToKeep, "Specify the number of image revisions for a tag in an image stream that will be preserved.") return cmd } diff --git a/pkg/dockerregistry/server/repositorymiddleware.go b/pkg/dockerregistry/server/repositorymiddleware.go index 34b4f4a0357f..9ef1b6023cdd 100644 --- a/pkg/dockerregistry/server/repositorymiddleware.go +++ b/pkg/dockerregistry/server/repositorymiddleware.go @@ -161,6 +161,9 @@ func (r *repository) Put(ctx context.Context, manifest *manifest.SignedManifest) Image: imageapi.Image{ ObjectMeta: kapi.ObjectMeta{ Name: dgst.String(), + Annotations: map[string]string{ + imageapi.ManagedByOpenShiftAnnotation: "true", + }, }, DockerImageReference: fmt.Sprintf("%s/%s/%s@%s", r.registryAddr, r.namespace, r.name, dgst.String()), DockerImageManifest: string(payload), diff --git a/pkg/image/api/types.go b/pkg/image/api/types.go index 816d57c0e3cb..48818acb6f0b 100644 --- a/pkg/image/api/types.go +++ b/pkg/image/api/types.go @@ -13,6 +13,8 @@ type ImageList struct { Items []Image `json:"items"` } +const ManagedByOpenShiftAnnotation = "openshift.io/image.managed" + // Image is an immutable representation of a Docker image and metadata at a point in time. type Image struct { kapi.TypeMeta `json:",inline"` diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index a7575edb2a33..9202b44603a7 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "net/http" - "strings" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" @@ -28,23 +27,10 @@ import ( // pruneAlgorithm contains the various settings to use when evaluating images // and layers for pruning. type pruneAlgorithm struct { - registryURLs []string minimumAgeInMinutesToPrune int tagRevisionsToKeep int } -// externalImage returns true if the image belongs to an external Docker -// registry; i.e., a registry not controlled by OpenShift. -func (pa pruneAlgorithm) externalImage(image string) bool { - for _, url := range pa.registryURLs { - if strings.HasPrefix(image, url) { - return false - } - } - - return true -} - // ImagePruneFunc is a function that is invoked for each image that is // prunable, along with the list of image streams that reference it. type ImagePruneFunc func(image *imageapi.Image, streams []*imageapi.ImageStream) []error @@ -71,9 +57,6 @@ var _ ImagePruner = &imagePruner{} /* NewImagePruner creates a new ImagePruner. -registryURLs is a list of OpenShift registries. Only images with URLs -belonging to this list are candidates for pruning. - minimumAgeInMinutesToPrune is the minimum age, in minutes, that a resource must be in order for the image it references (or an image itself) to be a candidate for pruning. For example, if minimumAgeInMinutesToPrune is 60, and @@ -87,9 +70,9 @@ than tagRevisionsToKeep is eligible for pruning. images, streams, pods, rcs, bcs, builds, and dcs are client interfaces for retrieving each respective resource type. -The ImagePruner performs the following logic: remove any image belonging to the -specified registry URL(s) that was created at least *n* minutes ago and is -*not* currently referenced by: +The ImagePruner performs the following logic: remove any image contaning the +annotation openshift.io/image.managed=true that was created at least *n* +minutes ago and is *not* currently referenced by: - any pod created less than *n* minutes ago - any image stream created less than *n* minutes ago @@ -107,7 +90,7 @@ ImageStreams having a reference to the image in `status.tags`. Also automatically remove any image layer that is no longer referenced by any images. */ -func NewImagePruner(registryURLs []string, minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, images client.ImagesInterfacer, streams client.ImageStreamsNamespacer, pods kclient.PodsNamespacer, rcs kclient.ReplicationControllersNamespacer, bcs client.BuildConfigsNamespacer, builds client.BuildsNamespacer, dcs client.DeploymentConfigsNamespacer) (ImagePruner, error) { +func NewImagePruner(minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, images client.ImagesInterfacer, streams client.ImageStreamsNamespacer, pods kclient.PodsNamespacer, rcs kclient.ReplicationControllersNamespacer, bcs client.BuildConfigsNamespacer, builds client.BuildsNamespacer, dcs client.DeploymentConfigsNamespacer) (ImagePruner, error) { allImages, err := images.Images().List(labels.Everything(), fields.Everything()) if err != nil { return nil, fmt.Errorf("Error listing images: %v", err) @@ -143,17 +126,16 @@ func NewImagePruner(registryURLs []string, minimumAgeInMinutesToPrune int, tagRe return nil, fmt.Errorf("Error listing deployment configs: %v", err) } - return newImagePruner(registryURLs, minimumAgeInMinutesToPrune, tagRevisionsToKeep, allImages, allStreams, allPods, allRCs, allBCs, allBuilds, allDCs), nil + return newImagePruner(minimumAgeInMinutesToPrune, tagRevisionsToKeep, allImages, allStreams, allPods, allRCs, allBCs, allBuilds, allDCs), nil } // newImagePruner creates a new ImagePruner. -func newImagePruner(registryURLs []string, minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, images *imageapi.ImageList, streams *imageapi.ImageStreamList, pods *kapi.PodList, rcs *kapi.ReplicationControllerList, bcs *buildapi.BuildConfigList, builds *buildapi.BuildList, dcs *deployapi.DeploymentConfigList) ImagePruner { +func newImagePruner(minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, images *imageapi.ImageList, streams *imageapi.ImageStreamList, pods *kapi.PodList, rcs *kapi.ReplicationControllerList, bcs *buildapi.BuildConfigList, builds *buildapi.BuildList, dcs *deployapi.DeploymentConfigList) ImagePruner { g := graph.New() - glog.V(1).Infof("Creating image pruner with registryURLs=%v, minimumAgeInMinutesToPrune=%d, tagRevisionsToKeep=%d", registryURLs, minimumAgeInMinutesToPrune, tagRevisionsToKeep) + glog.V(1).Infof("Creating image pruner with minimumAgeInMinutesToPrune=%d, tagRevisionsToKeep=%d", minimumAgeInMinutesToPrune, tagRevisionsToKeep) algorithm := pruneAlgorithm{ - registryURLs: registryURLs, minimumAgeInMinutesToPrune: minimumAgeInMinutesToPrune, tagRevisionsToKeep: tagRevisionsToKeep, } @@ -161,10 +143,10 @@ func newImagePruner(registryURLs []string, minimumAgeInMinutesToPrune int, tagRe addImagesToGraph(g, images, algorithm) addImageStreamsToGraph(g, streams, algorithm) addPodsToGraph(g, pods, algorithm) - addReplicationControllersToGraph(g, rcs, algorithm) - addBuildConfigsToGraph(g, bcs, algorithm) - addBuildsToGraph(g, builds, algorithm) - addDeploymentConfigsToGraph(g, dcs, algorithm) + addReplicationControllersToGraph(g, rcs) + addBuildConfigsToGraph(g, bcs) + addBuildsToGraph(g, builds) + addDeploymentConfigsToGraph(g, dcs) return &imagePruner{ g: g, @@ -180,8 +162,14 @@ func addImagesToGraph(g graph.Graph, images *imageapi.ImageList, algorithm prune for i := range images.Items { image := &images.Items[i] - if algorithm.externalImage(image.DockerImageReference) { - glog.V(4).Infof("Image %q belongs to an external registry - skipping", image.DockerImageReference) + glog.V(4).Infof("Examining image %q", image.Name) + + if image.Annotations == nil { + glog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", image.Name, image.DockerImageReference) + continue + } + if value, ok := image.Annotations[imageapi.ManagedByOpenShiftAnnotation]; !ok || value != "true" { + glog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", image.Name, image.DockerImageReference) continue } @@ -221,6 +209,8 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, al for i := range streams.Items { stream := &streams.Items[i] + glog.V(4).Infof("Examining image stream %s/%s", stream.Namespace, stream.Name) + // use a weak reference for old image revisions by default oldImageRevisionReferenceKind := graph.WeakReferencedImageGraphEdgeKind @@ -237,14 +227,9 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, al for tag, history := range stream.Status.Tags { for i := range history.Items { - if algorithm.externalImage(history.Items[i].DockerImageReference) { - glog.V(4).Infof("Tag %q revision %d points to %s which is part of an external registry; skipping", tag, i, history.Items[i].DockerImageReference) - continue - } - n := graph.FindImage(g, history.Items[i].Image) if n == nil { - glog.V(1).Infof("Unable to find image %q in graph", history.Items[i].Image) + glog.V(1).Infof("Unable to find image %q in graph (from tag %q, revision %d, dockerImageReference %s)", history.Items[i].Image, tag, i, history.Items[i].DockerImageReference) continue } imageNode := n.(*graph.ImageNode) @@ -280,12 +265,13 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, al // defined by algorithm. // // Edges are added to the graph from each pod to the images specified by that -// pod's list of containers, as long as the image belongs to one of the -// registries specified in algorithm. +// pod's list of containers, as long as the image is managed by OpenShift. func addPodsToGraph(g graph.Graph, pods *kapi.PodList, algorithm pruneAlgorithm) { for i := range pods.Items { pod := &pods.Items[i] + glog.V(4).Infof("Examining pod %s/%s", pod.Namespace, pod.Name) + if pod.Status.Phase != kapi.PodRunning && pod.Status.Phase != kapi.PodPending { age := util.Now().Sub(pod.CreationTimestamp.Time) if int(age.Minutes()) >= algorithm.minimumAgeInMinutesToPrune { @@ -298,22 +284,18 @@ func addPodsToGraph(g graph.Graph, pods *kapi.PodList, algorithm pruneAlgorithm) glog.V(4).Infof("Adding pod %s/%s to graph", pod.Namespace, pod.Name) podNode := graph.Pod(g, pod) - addPodSpecToGraph(g, &pod.Spec, podNode, algorithm) + addPodSpecToGraph(g, &pod.Spec, podNode) } } // Edges are added to the graph from each predecessor (pod or replication // controller) to the images specified by the pod spec's list of containers, as -// long as the image belongs to one of the registries specified in algorithm. -func addPodSpecToGraph(g graph.Graph, spec *kapi.PodSpec, predecessor gonum.Node, algorithm pruneAlgorithm) { +// long as the image is managed by OpenShift. +func addPodSpecToGraph(g graph.Graph, spec *kapi.PodSpec, predecessor gonum.Node) { for j := range spec.Containers { container := spec.Containers[j] glog.V(4).Infof("Examining container image %q", container.Image) - if algorithm.externalImage(container.Image) { - glog.V(4).Infof("Image belongs to an external registry - skipping") - continue - } ref, err := imageapi.ParseDockerImageReference(container.Image) if err != nil { @@ -322,13 +304,13 @@ func addPodSpecToGraph(g graph.Graph, spec *kapi.PodSpec, predecessor gonum.Node } if len(ref.ID) == 0 { - glog.Errorf("Missing image ID") + glog.V(4).Infof("%q has no image ID", container.Image) continue } imageNode := graph.FindImage(g, ref.ID) if imageNode == nil { - glog.Errorf("Expected to find image %q in the graph, but it was missing", ref.ID) + glog.Infof("Unable to find image %q in the graph", ref.ID) continue } @@ -340,75 +322,83 @@ func addPodSpecToGraph(g graph.Graph, spec *kapi.PodSpec, predecessor gonum.Node // addReplicationControllersToGraph adds replication controllers to the graph. // // Edges are added to the graph from each replication controller to the images -// specified by its pod spec's list of containers, as long as the image belongs -// to one of the registries specified in algorithm. -func addReplicationControllersToGraph(g graph.Graph, rcs *kapi.ReplicationControllerList, algorithm pruneAlgorithm) { +// specified by its pod spec's list of containers, as long as the image is +// managed by OpenShift. +func addReplicationControllersToGraph(g graph.Graph, rcs *kapi.ReplicationControllerList) { for i := range rcs.Items { rc := &rcs.Items[i] + glog.V(4).Infof("Examining replication controller %s/%s", rc.Namespace, rc.Name) rcNode := graph.ReplicationController(g, rc) - addPodSpecToGraph(g, &rc.Spec.Template.Spec, rcNode, algorithm) + addPodSpecToGraph(g, &rc.Spec.Template.Spec, rcNode) } } // addDeploymentConfigsToGraph adds deployment configs to the graph. // // Edges are added to the graph from each deployment config to the images -// specified by its pod spec's list of containers, as long as the image belongs -// to one of the registries specified in algorithm. -func addDeploymentConfigsToGraph(g graph.Graph, dcs *deployapi.DeploymentConfigList, algorithm pruneAlgorithm) { +// specified by its pod spec's list of containers, as long as the image is +// managed by OpenShift. +func addDeploymentConfigsToGraph(g graph.Graph, dcs *deployapi.DeploymentConfigList) { for i := range dcs.Items { dc := &dcs.Items[i] + glog.V(4).Infof("Examining deployment config %s/%s", dc.Namespace, dc.Name) dcNode := graph.DeploymentConfig(g, dc) - addPodSpecToGraph(g, &dc.Template.ControllerTemplate.Template.Spec, dcNode, algorithm) + addPodSpecToGraph(g, &dc.Template.ControllerTemplate.Template.Spec, dcNode) } } // addBuildConfigsToGraph adds build configs to the graph. // // Edges are added to the graph from each build config to the image specified by its strategy.from. -func addBuildConfigsToGraph(g graph.Graph, bcs *buildapi.BuildConfigList, algorithm pruneAlgorithm) { +func addBuildConfigsToGraph(g graph.Graph, bcs *buildapi.BuildConfigList) { for i := range bcs.Items { bc := &bcs.Items[i] + glog.V(4).Infof("Examining build config %s/%s", bc.Namespace, bc.Name) bcNode := graph.BuildConfig(g, bc) - addBuildStrategyImageReferencesToGraph(g, bc.Parameters.Strategy, bcNode, algorithm) + addBuildStrategyImageReferencesToGraph(g, bc.Parameters.Strategy, bcNode) } } // addBuildsToGraph adds builds to the graph. // // Edges are added to the graph from each build to the image specified by its strategy.from. -func addBuildsToGraph(g graph.Graph, builds *buildapi.BuildList, algorithm pruneAlgorithm) { +func addBuildsToGraph(g graph.Graph, builds *buildapi.BuildList) { for i := range builds.Items { build := &builds.Items[i] + glog.V(4).Infof("Examining build %s/%s", build.Namespace, build.Name) buildNode := graph.Build(g, build) - addBuildStrategyImageReferencesToGraph(g, build.Parameters.Strategy, buildNode, algorithm) + addBuildStrategyImageReferencesToGraph(g, build.Parameters.Strategy, buildNode) } } // Edges are added to the graph from each predecessor (build or build config) -// to the image specified by strategy.from, as long as the image belongs to one -// of the registries specified in algorithm. -func addBuildStrategyImageReferencesToGraph(g graph.Graph, strategy buildapi.BuildStrategy, predecessor gonum.Node, algorithm pruneAlgorithm) { +// to the image specified by strategy.from, as long as the image is managed by +// OpenShift. +func addBuildStrategyImageReferencesToGraph(g graph.Graph, strategy buildapi.BuildStrategy, predecessor gonum.Node) { + glog.V(4).Infof("Examining build strategy with type %q", strategy.Type) + from := buildutil.GetImageStreamForStrategy(strategy) if from == nil { + glog.V(4).Infof("Unable to determine 'from' reference - skipping") return } + glog.V(4).Infof("Examining build strategy with from: %#v", from) + var imageID string switch from.Kind { case "ImageStreamImage": _, id, err := imagestreamimage.ParseNameAndID(from.Name) if err != nil { + glog.V(4).Infof("Error parsing ImageStreamImage name %q: %v - skipping", from.Name, err) return } imageID = id case "DockerImage": - if algorithm.externalImage(from.Name) { - return - } ref, err := imageapi.ParseDockerImageReference(from.Name) if err != nil { + glog.V(4).Infof("Error parsing DockerImage name %q: %v - skipping", from.Name, err) return } imageID = ref.ID @@ -416,11 +406,14 @@ func addBuildStrategyImageReferencesToGraph(g graph.Graph, strategy buildapi.Bui return } + glog.V(4).Infof("Looking for image %q in graph", imageID) imageNode := graph.FindImage(g, imageID) if imageNode == nil { + glog.V(4).Infof("Unable to find image %q in graph - skipping", imageID) return } + glog.V(4).Infof("Adding edge from %v to %v", predecessor, imageNode) g.AddEdge(predecessor, imageNode, graph.ReferencedImageGraphEdgeKind) } @@ -568,7 +561,7 @@ func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFu ref, err := imageapi.DockerImageReferenceForStream(stream) if err != nil { //TODO - glog.Errorf("Error constructing DockerImageReference for %q", streamName) + glog.Errorf("Error constructing DockerImageReference for %q: %v", streamName, err) continue } diff --git a/pkg/image/prune/imagepruner_test.go b/pkg/image/prune/imagepruner_test.go index 21337538eebf..c434f88a73c4 100644 --- a/pkg/image/prune/imagepruner_test.go +++ b/pkg/image/prune/imagepruner_test.go @@ -1,6 +1,7 @@ package prune import ( + "encoding/json" "flag" "fmt" "reflect" @@ -23,31 +24,13 @@ func imageList(images ...imageapi.Image) imageapi.ImageList { } func agedImage(id, ref string, ageInMinutes int64) imageapi.Image { - image := imageapi.Image{ - ObjectMeta: kapi.ObjectMeta{ - Name: id, - }, - DockerImageReference: ref, - DockerImageManifest: `{ - "fsLayers": [ - { - "blobSum": "tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - }, - { - "blobSum": "tarsum.dev+sha256:b194de3772ebbcdc8f244f663669799ac1cb141834b7cb8b69100285d357a2b0" - }, - { - "blobSum": "tarsum.dev+sha256:c937c4bb1c1a21cc6d94340812262c6472092028972ae69b551b1a70d4276171" - }, - { - "blobSum": "tarsum.dev+sha256:2aaacc362ac6be2b9e9ae8c6029f6f616bb50aec63746521858e47841b90fabd" - }, - { - "blobSum": "tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - } - ] - }`, - } + image := imageWithLayers(id, ref, + "tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "tarsum.dev+sha256:b194de3772ebbcdc8f244f663669799ac1cb141834b7cb8b69100285d357a2b0", + "tarsum.dev+sha256:c937c4bb1c1a21cc6d94340812262c6472092028972ae69b551b1a70d4276171", + "tarsum.dev+sha256:2aaacc362ac6be2b9e9ae8c6029f6f616bb50aec63746521858e47841b90fabd", + "tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ) if ageInMinutes >= 0 { image.CreationTimestamp = util.NewTime(util.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute)) @@ -60,6 +43,35 @@ func image(id, ref string) imageapi.Image { return agedImage(id, ref, -1) } +func imageWithLayers(id, ref string, layers ...string) imageapi.Image { + image := imageapi.Image{ + ObjectMeta: kapi.ObjectMeta{ + Name: id, + Annotations: map[string]string{ + imageapi.ManagedByOpenShiftAnnotation: "true", + }, + }, + DockerImageReference: ref, + } + + manifest := imageapi.DockerImageManifest{ + FSLayers: []imageapi.DockerFSLayer{}, + } + + for _, layer := range layers { + manifest.FSLayers = append(manifest.FSLayers, imageapi.DockerFSLayer{DockerBlobSum: layer}) + } + + manifestBytes, err := json.Marshal(&manifest) + if err != nil { + panic(err) + } + + image.DockerImageManifest = string(manifestBytes) + + return image +} + func podList(pods ...kapi.Pod) kapi.PodList { return kapi.PodList{ Items: pods, @@ -108,17 +120,18 @@ func streamList(streams ...imageapi.ImageStream) imageapi.ImageStreamList { } } -func stream(namespace, name string, tags map[string]imageapi.TagEventList) imageapi.ImageStream { - return agedStream(namespace, name, -1, tags) +func stream(registry, namespace, name string, tags map[string]imageapi.TagEventList) imageapi.ImageStream { + return agedStream(registry, namespace, name, -1, tags) } -func agedStream(namespace, name string, ageInMinutes int64, tags map[string]imageapi.TagEventList) imageapi.ImageStream { +func agedStream(registry, namespace, name string, ageInMinutes int64, tags map[string]imageapi.TagEventList) imageapi.ImageStream { stream := imageapi.ImageStream{ ObjectMeta: kapi.ObjectMeta{ Namespace: namespace, Name: name, }, Status: imageapi.ImageStreamStatus{ + DockerImageRepository: fmt.Sprintf("%s/%s/%s", registry, namespace, name), Tags: tags, }, } @@ -130,8 +143,8 @@ func agedStream(namespace, name string, ageInMinutes int64, tags map[string]imag return stream } -func streamPtr(namespace, name string, tags map[string]imageapi.TagEventList) *imageapi.ImageStream { - s := stream(namespace, name, tags) +func streamPtr(registry, namespace, name string, tags map[string]imageapi.TagEventList) *imageapi.ImageStream { + s := stream(registry, namespace, name, tags) return &s } @@ -276,7 +289,7 @@ func buildParameters(strategyType buildapi.BuildStrategyType, fromKind, fromName var logLevel = flag.Int("loglevel", 0, "") -func TestRun(t *testing.T) { +func TestImagePruning(t *testing.T) { flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel)) registryURL := "registry" @@ -431,7 +444,7 @@ func TestRun(t *testing.T) { image("id4", registryURL+"/foo/bar@id4"), ), streams: streamList( - stream("foo", "bar", tags( + stream(registryURL, "foo", "bar", tags( tag("latest", tagEvent("id", registryURL+"/foo/bar@id"), tagEvent("id2", registryURL+"/foo/bar@id2"), @@ -449,7 +462,7 @@ func TestRun(t *testing.T) { image("id2", registryURL+"/foo/bar@id2"), ), streams: streamList( - agedStream("foo", "bar", 5, tags( + agedStream(registryURL, "foo", "bar", 5, tags( tag("latest", tagEvent("id", registryURL+"/foo/bar@id"), tagEvent("id2", registryURL+"/foo/bar@id2"), @@ -465,7 +478,7 @@ func TestRun(t *testing.T) { image("id2", registryURL+"/foo/bar@id2"), ), streams: streamList( - stream("foo", "bar", tags( + stream(registryURL, "foo", "bar", tags( tag("latest", tagEvent("id", registryURL+"/foo/bar@id"), tagEvent("id2", registryURL+"/foo/bar@id2"), @@ -483,11 +496,7 @@ func TestRun(t *testing.T) { } for name, test := range tests { - registryURLs := []string{registryURL} - if len(test.registryURLs) > 0 { - registryURLs = test.registryURLs - } - p := newImagePruner(registryURLs, 60, 3, &test.images, &test.streams, &test.pods, &test.rcs, &test.bcs, &test.builds, &test.dcs) + p := newImagePruner(60, 3, &test.images, &test.streams, &test.pods, &test.rcs, &test.bcs, &test.builds, &test.dcs) actualDeletions := util.NewStringSet() actualUpdatedStreams := util.NewStringSet() @@ -518,6 +527,8 @@ func TestRun(t *testing.T) { } func TestDefaultImagePruneFunc(t *testing.T) { + registryURL := "registry" + tests := map[string]struct { referencedStreams []*imageapi.ImageStream expectedUpdates []*imageapi.ImageStream @@ -528,7 +539,7 @@ func TestDefaultImagePruneFunc(t *testing.T) { }, "1 tag, 1 image revision": { referencedStreams: []*imageapi.ImageStream{ - streamPtr("foo", "bar", tags( + streamPtr(registryURL, "foo", "bar", tags( tag("latest", tagEvent("id", "registry/foo/bar@id"), ), @@ -538,7 +549,7 @@ func TestDefaultImagePruneFunc(t *testing.T) { }, "1 tag, multiple image revisions": { referencedStreams: []*imageapi.ImageStream{ - streamPtr("foo", "bar", tags( + streamPtr(registryURL, "foo", "bar", tags( tag("latest", tagEvent("id", "registry/foo/bar@id"), tagEvent("id2", "registry/foo/bar@id2"), @@ -546,7 +557,7 @@ func TestDefaultImagePruneFunc(t *testing.T) { )), }, expectedUpdates: []*imageapi.ImageStream{ - streamPtr("foo", "bar", tags( + streamPtr(registryURL, "foo", "bar", tags( tag("latest", tagEvent("id", "registry/foo/bar@id"), ), @@ -580,3 +591,79 @@ func TestDefaultImagePruneFunc(t *testing.T) { } } } + +func TestLayerPruning(t *testing.T) { + flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel)) + registryURL := "registry1" + + tests := map[string]struct { + images imageapi.ImageList + streams imageapi.ImageStreamList + expectedDeletions map[string]util.StringSet + expectedStreamUpdates map[string]util.StringSet + }{ + "layers unique to id1 pruned": { + images: imageList( + imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"), + imageWithLayers("id2", "registry1/foo/bar@id2", "layer3", "layer4", "layer5", "layer6"), + ), + streams: streamList( + stream(registryURL, "foo", "bar", tags( + tag("latest", + tagEvent("id2", registryURL+"/foo/bar@id2"), + tagEvent("id1", registryURL+"/foo/bar@id1"), + ), + )), + ), + expectedDeletions: map[string]util.StringSet{ + "registry1": util.NewStringSet("layer1", "layer2"), + }, + }, + } + + for name, test := range tests { + actualDeletions := map[string]util.StringSet{} + actualUpdatedStreams := map[string]util.StringSet{} + + imagePruneFunc := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { + return []error{} + } + + layerPruneFunc := func(registryURL string, req dockerregistry.DeleteLayersRequest) []error { + registryDeletions, ok := actualDeletions[registryURL] + if !ok { + registryDeletions = util.NewStringSet() + } + streamUpdates, ok := actualUpdatedStreams[registryURL] + if !ok { + streamUpdates = util.NewStringSet() + } + + for layer, streams := range req { + registryDeletions.Insert(layer) + streamUpdates.Insert(streams...) + } + + actualDeletions[registryURL] = registryDeletions + actualUpdatedStreams[registryURL] = streamUpdates + + return []error{} + } + + p := newImagePruner(60, 1, &test.images, &test.streams, &kapi.PodList{}, &kapi.ReplicationControllerList{}, &buildapi.BuildConfigList{}, &buildapi.BuildList{}, &deployapi.DeploymentConfigList{}) + + p.Run(imagePruneFunc, layerPruneFunc) + + if !reflect.DeepEqual(test.expectedDeletions, actualDeletions) { + t.Errorf("%s: expected layer deletions %#v, got %#v", name, test.expectedDeletions, actualDeletions) + } + + /* + expectedUpdatedStreams := util.NewStringSet(test.expectedUpdatedStreams...) + if !reflect.DeepEqual(expectedUpdatedStreams, actualUpdatedStreams) { + t.Errorf("%s: expected stream updates %q, got %q", name, expectedUpdatedStreams.List(), actualUpdatedStreams.List()) + } + */ + + } +} From 4203e1c356d2dfab77b061c213a5a0642ec2dd2b Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Mon, 4 May 2015 14:20:38 -0400 Subject: [PATCH 07/21] Image pruning Have the registry send a response after attempting to delete layers. Have the pruner process the registry's delete layers response. Add more test coverage. --- pkg/client/fake_images.go | 3 +- pkg/cmd/dockerregistry/dockerregistry.go | 30 ++- pkg/image/prune/imagepruner.go | 38 ++- pkg/image/prune/imagepruner_test.go | 299 +++++++++++++++++++++-- 4 files changed, 341 insertions(+), 29 deletions(-) diff --git a/pkg/client/fake_images.go b/pkg/client/fake_images.go index 4cac86da49fd..978c3616f4c1 100644 --- a/pkg/client/fake_images.go +++ b/pkg/client/fake_images.go @@ -33,5 +33,6 @@ func (c *FakeImages) Create(image *imageapi.Image) (*imageapi.Image, error) { func (c *FakeImages) Delete(name string) error { c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-image", Value: name}) - return nil + _, err := c.Fake.Invokes(FakeAction{Action: "delete-image", Value: name}, nil) + return err } diff --git a/pkg/cmd/dockerregistry/dockerregistry.go b/pkg/cmd/dockerregistry/dockerregistry.go index 5d11c2659e88..80d0027601ec 100644 --- a/pkg/cmd/dockerregistry/dockerregistry.go +++ b/pkg/cmd/dockerregistry/dockerregistry.go @@ -57,6 +57,11 @@ func (r DeleteLayersRequest) AddStream(layer, stream string) { r[layer] = append(r[layer], stream) } +type DeleteLayersResponse struct { + Result string + Errors []string +} + // deleteLayerFunc returns an http.HandlerFunc that is able to fully delete a // layer from storage. func deleteLayerFunc(app *handlers.App) http.HandlerFunc { @@ -93,7 +98,30 @@ func deleteLayerFunc(app *handlers.App) http.HandlerFunc { log.Infof("errs=%v", errs) - //TODO write response + var result string + switch len(errs) { + case 0: + result = "success" + default: + result = "failure" + } + + response := DeleteLayersResponse{ + Result: result, + } + + for _, err := range errs { + response.Errors = append(response.Errors, err.Error()) + } + + buf, err := json.Marshal(&response) + if err != nil { + w.Write([]byte(fmt.Sprintf("Error marshaling response: %v", err))) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Write(buf) w.WriteHeader(http.StatusOK) } } diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index 9202b44603a7..f1d9dadff349 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -3,8 +3,10 @@ package prune import ( "bytes" "encoding/json" + "errors" "fmt" "io" + "io/ioutil" "net/http" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -186,6 +188,7 @@ func addImagesToGraph(g graph.Graph, images *imageapi.ImageList, algorithm prune manifest := imageapi.DockerImageManifest{} if err := json.Unmarshal([]byte(image.DockerImageManifest), &manifest); err != nil { glog.Errorf("Unable to extract manifest from image: %v. This image's layers won't be pruned if the image is pruned now.", err) + continue } for _, layer := range manifest.FSLayers { @@ -229,7 +232,7 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, al for i := range history.Items { n := graph.FindImage(g, history.Items[i].Image) if n == nil { - glog.V(1).Infof("Unable to find image %q in graph (from tag %q, revision %d, dockerImageReference %s)", history.Items[i].Image, tag, i, history.Items[i].DockerImageReference) + glog.V(1).Infof("Unable to find image %q in graph (from tag=%q, revision=%d, dockerImageReference=%s)", history.Items[i].Image, tag, i, history.Items[i].DockerImageReference) continue } imageNode := n.(*graph.ImageNode) @@ -473,7 +476,6 @@ func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, imagePruneFunc Im streams := imageStreamPredecessors(g, imageNode) if errs := imagePruneFunc(imageNode.Image, streams); len(errs) > 0 { - //TODO glog.Errorf("Error pruning image %q: %v", imageNode.Image.Name, errs) } @@ -560,7 +562,6 @@ func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFu ref, err := imageapi.DockerImageReferenceForStream(stream) if err != nil { - //TODO glog.Errorf("Error constructing DockerImageReference for %q: %v", streamName, err) continue } @@ -619,9 +620,9 @@ func DeletingImagePruneFunc(images client.ImageInterface, streams client.ImageSt glog.V(4).Infof("Checking if stream %s/%s has references to image in status.tags", stream.Namespace, stream.Name) for tag, history := range stream.Status.Tags { glog.V(4).Infof("Checking tag %q", tag) - newHistory := imageapi.TagEventList{Items: []imageapi.TagEvent{history.Items[0]}} - for i, tagEvent := range history.Items[1:] { - glog.V(4).Infof("Checking tag event %d with image %q", i+1, tagEvent.Image) + newHistory := imageapi.TagEventList{} + for i, tagEvent := range history.Items { + glog.V(4).Infof("Checking tag event %d with image %q", i, tagEvent.Image) if tagEvent.Image != image.Name { glog.V(4).Infof("Tag event doesn't match deleting image - keeping") newHistory.Items = append(newHistory.Items, tagEvent) @@ -672,6 +673,8 @@ func DescribingLayerPruneFunc(out io.Writer) LayerPruneFunc { // names referenced by the layer. func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { return func(registryURL string, deletions dockerregistry.DeleteLayersRequest) []error { + errs := []error{} + glog.V(4).Infof("Starting pruning of layers from %q, req %#v", registryURL, deletions) body, err := json.Marshal(&deletions) if err != nil { @@ -694,9 +697,28 @@ func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { } defer resp.Body.Close() - //TODO read response + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + glog.Errorf("Error reading response body: %v", err) + return []error{fmt.Errorf("Error reading response body: %v", err)} + } - return []error{} + if resp.StatusCode != http.StatusOK { + glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) + return []error{fmt.Errorf("Unexpected status code %d in response %s", resp.StatusCode, buf)} + } + + var deleteResponse dockerregistry.DeleteLayersResponse + if err := json.Unmarshal(buf, &deleteResponse); err != nil { + glog.Errorf("Error unmarshaling response: %v", err) + return []error{fmt.Errorf("Error unmarshaling response: %v", err)} + } + + for _, e := range deleteResponse.Errors { + errs = append(errs, errors.New(e)) + } + + return errs } } diff --git a/pkg/image/prune/imagepruner_test.go b/pkg/image/prune/imagepruner_test.go index c434f88a73c4..46453a69bd2f 100644 --- a/pkg/image/prune/imagepruner_test.go +++ b/pkg/image/prune/imagepruner_test.go @@ -4,11 +4,15 @@ import ( "encoding/json" "flag" "fmt" + "net/http" + "net/http/httptest" "reflect" + "strings" "testing" "time" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" buildapi "github.com/openshift/origin/pkg/build/api" "github.com/openshift/origin/pkg/client" @@ -72,6 +76,23 @@ func imageWithLayers(id, ref string, layers ...string) imageapi.Image { return image } +func unmanagedImage(id, ref string, hasAnnotations bool, annotation, value string) imageapi.Image { + image := imageWithLayers(id, ref) + if !hasAnnotations { + image.Annotations = nil + } else { + delete(image.Annotations, imageapi.ManagedByOpenShiftAnnotation) + image.Annotations[annotation] = value + } + return image +} + +func imageWithBadManifest(id, ref string) imageapi.Image { + image := image(id, ref) + image.DockerImageManifest = "asdf" + return image +} + func podList(pods ...kapi.Pod) kapi.PodList { return kapi.PodList{ Items: pods, @@ -288,6 +309,7 @@ func buildParameters(strategyType buildapi.BuildStrategyType, fromKind, fromName } var logLevel = flag.Int("loglevel", 0, "") +var testCase = flag.String("testcase", "", "") func TestImagePruning(t *testing.T) { flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel)) @@ -366,6 +388,27 @@ func TestImagePruning(t *testing.T) { ), expectedDeletions: []string{"id"}, }, + "pod container image not parsable": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList( + pod("foo", "pod1", kapi.PodRunning, "a/b/c/d/e"), + ), + expectedDeletions: []string{"id"}, + }, + "pod container image doesn't have an id": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList( + pod("foo", "pod1", kapi.PodRunning, "foo/bar:latest"), + ), + expectedDeletions: []string{"id"}, + }, + "pod refers to image not in graph": { + images: imageList(image("id", registryURL+"/foo/bar@id")), + pods: podList( + pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@otherid"), + ), + expectedDeletions: []string{"id"}, + }, "referenced by rc - don't prune": { images: imageList(image("id", registryURL+"/foo/bar@id")), rcs: rcList(rc("foo", "rc1", registryURL+"/foo/bar@id")), @@ -438,7 +481,7 @@ func TestImagePruning(t *testing.T) { }, "image stream - keep most recent n images": { images: imageList( - image("id", registryURL+"/foo/bar@id"), + unmanagedImage("id", "otherregistry/foo/bar@id", false, "", ""), image("id2", registryURL+"/foo/bar@id2"), image("id3", registryURL+"/foo/bar@id3"), image("id4", registryURL+"/foo/bar@id4"), @@ -446,7 +489,7 @@ func TestImagePruning(t *testing.T) { streams: streamList( stream(registryURL, "foo", "bar", tags( tag("latest", - tagEvent("id", registryURL+"/foo/bar@id"), + tagEvent("id", "otherregistry/foo/bar@id"), tagEvent("id2", registryURL+"/foo/bar@id2"), tagEvent("id3", registryURL+"/foo/bar@id3"), tagEvent("id4", registryURL+"/foo/bar@id4"), @@ -460,12 +503,16 @@ func TestImagePruning(t *testing.T) { images: imageList( image("id", registryURL+"/foo/bar@id"), image("id2", registryURL+"/foo/bar@id2"), + image("id3", registryURL+"/foo/bar@id3"), + image("id4", registryURL+"/foo/bar@id4"), ), streams: streamList( agedStream(registryURL, "foo", "bar", 5, tags( tag("latest", tagEvent("id", registryURL+"/foo/bar@id"), tagEvent("id2", registryURL+"/foo/bar@id2"), + tagEvent("id3", registryURL+"/foo/bar@id3"), + tagEvent("id4", registryURL+"/foo/bar@id4"), ), )), ), @@ -493,9 +540,46 @@ func TestImagePruning(t *testing.T) { expectedDeletions: []string{}, expectedUpdatedStreams: []string{}, }, + "image with nil annotations": { + images: imageList( + unmanagedImage("id", "someregistry/foo/bar@id", false, "", ""), + ), + expectedDeletions: []string{}, + expectedUpdatedStreams: []string{}, + }, + "image missing managed annotation": { + images: imageList( + unmanagedImage("id", "someregistry/foo/bar@id", true, "foo", "bar"), + ), + expectedDeletions: []string{}, + expectedUpdatedStreams: []string{}, + }, + "image with managed annotation != true": { + images: imageList( + unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "false"), + unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "0"), + unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "1"), + unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "True"), + unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "yes"), + unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "Yes"), + ), + expectedDeletions: []string{}, + expectedUpdatedStreams: []string{}, + }, + "image with bad manifest is pruned ok": { + images: imageList( + imageWithBadManifest("id", "someregistry/foo/bar@id"), + ), + expectedDeletions: []string{"id"}, + expectedUpdatedStreams: []string{}, + }, } for name, test := range tests { + tcFilter := flag.Lookup("testcase").Value.String() + if len(tcFilter) > 0 && name != tcFilter { + continue + } p := newImagePruner(60, 3, &test.images, &test.streams, &test.pods, &test.rcs, &test.bcs, &test.builds, &test.dcs) actualDeletions := util.NewStringSet() actualUpdatedStreams := util.NewStringSet() @@ -526,12 +610,14 @@ func TestImagePruning(t *testing.T) { } } -func TestDefaultImagePruneFunc(t *testing.T) { +func TestDeletingImagePruneFunc(t *testing.T) { registryURL := "registry" tests := map[string]struct { - referencedStreams []*imageapi.ImageStream - expectedUpdates []*imageapi.ImageStream + referencedStreams []*imageapi.ImageStream + expectedUpdates []*imageapi.ImageStream + imageDeletionError error + streamUpdateError error }{ "no referenced streams": { referencedStreams: []*imageapi.ImageStream{}, @@ -564,27 +650,79 @@ func TestDefaultImagePruneFunc(t *testing.T) { )), }, }, + "image deletion error": { + referencedStreams: []*imageapi.ImageStream{ + streamPtr(registryURL, "foo", "bar", tags( + tag("latest", + tagEvent("id", "registry/foo/bar@id"), + ), + )), + }, + imageDeletionError: fmt.Errorf("foo"), + }, + "stream update error": { + referencedStreams: []*imageapi.ImageStream{ + streamPtr(registryURL, "foo", "bar", tags( + tag("latest", + tagEvent("id", "registry/foo/bar@id"), + ), + )), + streamPtr(registryURL, "bar", "baz", tags( + tag("latest", + tagEvent("id", "registry/foo/bar@id"), + ), + )), + }, + streamUpdateError: fmt.Errorf("foo"), + }, } for name, test := range tests { - fakeClient := client.Fake{} - pruneFunc := DeletingImagePruneFunc(fakeClient.Images(), &fakeClient) - err := pruneFunc(&imageapi.Image{ObjectMeta: kapi.ObjectMeta{Name: "id2"}}, test.referencedStreams) - _ = err + imageClient := client.Fake{ + Err: test.imageDeletionError, + } + streamClient := client.Fake{ + Err: test.streamUpdateError, + } + pruneFunc := DeletingImagePruneFunc(imageClient.Images(), &streamClient) + errs := pruneFunc(&imageapi.Image{ObjectMeta: kapi.ObjectMeta{Name: "id2"}}, test.referencedStreams) + if test.imageDeletionError != nil { + if e, a := 1, len(errs); e != a { + t.Errorf("%s: # of errors: expected %v, got %v", name, e, a) + continue + } + if e, a := fmt.Sprintf("Error deleting image: %v", test.imageDeletionError), errs[0].Error(); e != a { + t.Errorf("%s: errs: expected %v, got %v", name, e, a) + } + continue + } + + if test.streamUpdateError != nil { + if e, a := len(test.referencedStreams), len(errs); e != a { + t.Errorf("%s: # of errors: expected %v, got %v", name, e, a) + continue + } + for i, stream := range test.referencedStreams { + if e, a := fmt.Sprintf("Unable to update image stream status %s/%s: %v", stream.Namespace, stream.Name, test.streamUpdateError), errs[i].Error(); e != a { + t.Errorf("%s: errs: expected %v, got %v", name, e, a) + } + } + continue + } - if len(fakeClient.Actions) < 1 { + if len(imageClient.Actions) < 1 { t.Fatalf("%s: expected image deletion", name) } - if e, a := len(test.referencedStreams), len(fakeClient.Actions)-1; e != a { + if e, a := len(test.referencedStreams), len(streamClient.Actions); e != a { t.Errorf("%s: expected %d stream updates, got %d", name, e, a) } for i := range test.expectedUpdates { - if e, a := "update-status-imagestream", fakeClient.Actions[i+1].Action; e != a { + if e, a := "update-status-imagestream", streamClient.Actions[i].Action; e != a { t.Errorf("%s: unexpected action %q", name, a) } - updatedStream := fakeClient.Actions[i+1].Value.(*imageapi.ImageStream) + updatedStream := streamClient.Actions[i].Value.(*imageapi.ImageStream) if e, a := test.expectedUpdates[i], updatedStream; !reflect.DeepEqual(e, a) { t.Errorf("%s: unexpected updated stream: %s", name, util.ObjectDiff(e, a)) } @@ -614,10 +752,32 @@ func TestLayerPruning(t *testing.T) { tagEvent("id1", registryURL+"/foo/bar@id1"), ), )), + stream(registryURL, "foo", "other", tags( + tag("latest", + tagEvent("id2", registryURL+"/foo/other@id2"), + ), + )), ), expectedDeletions: map[string]util.StringSet{ "registry1": util.NewStringSet("layer1", "layer2"), }, + expectedStreamUpdates: map[string]util.StringSet{ + "registry1": util.NewStringSet("foo/bar"), + }, + }, + "no pruning when no images are pruned": { + images: imageList( + imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"), + ), + streams: streamList( + stream(registryURL, "foo", "bar", tags( + tag("latest", + tagEvent("id1", registryURL+"/foo/bar@id1"), + ), + )), + ), + expectedDeletions: map[string]util.StringSet{}, + expectedStreamUpdates: map[string]util.StringSet{}, }, } @@ -658,12 +818,113 @@ func TestLayerPruning(t *testing.T) { t.Errorf("%s: expected layer deletions %#v, got %#v", name, test.expectedDeletions, actualDeletions) } - /* - expectedUpdatedStreams := util.NewStringSet(test.expectedUpdatedStreams...) - if !reflect.DeepEqual(expectedUpdatedStreams, actualUpdatedStreams) { - t.Errorf("%s: expected stream updates %q, got %q", name, expectedUpdatedStreams.List(), actualUpdatedStreams.List()) - } - */ + if !reflect.DeepEqual(test.expectedStreamUpdates, actualUpdatedStreams) { + t.Errorf("%s: expected stream updates %q, got %q", name, test.expectedStreamUpdates, actualUpdatedStreams) + } + } +} +func TestNewImagePruner(t *testing.T) { + osFake := &client.Fake{} + + kFake := &testclient.Fake{} + p, err := NewImagePruner(60, 3, osFake, osFake, kFake, kFake, osFake, osFake, osFake) + if err != nil { + t.Fatalf("unexpected error creating image pruner: %v", err) + } + if p == nil { + t.Fatalf("unexpected nil pruner") + } + + seen := util.NewStringSet() + for _, action := range osFake.Actions { + seen.Insert(action.Action) + } + for _, action := range kFake.Actions { + seen.Insert(action.Action) + } + + expected := util.NewStringSet( + "list-images", + "list-imagestreams", + "list-pods", + "list-replicationControllers", + "list-buildconfig", + "list-builds", + "list-deploymentconfig", + ) + + if e, a := expected, seen; !reflect.DeepEqual(e, a) { + t.Errorf("Expected actions=%v, got: %v", e.List(), a.List()) + } +} + +func TestDeletingLayerPruneFunc(t *testing.T) { + tests := map[string]struct { + simulateClientError bool + registryResponseStatusCode int + registryResponse string + expectedErrors []string + }{ + "client error": { + simulateClientError: true, + expectedErrors: []string{"Error sending request:"}, + }, + "non-200 response": { + registryResponseStatusCode: http.StatusInternalServerError, + expectedErrors: []string{fmt.Sprintf("Unexpected status code %d in response", http.StatusInternalServerError)}, + registryResponse: "{}", + }, + "error unmarshaling response body": { + registryResponseStatusCode: http.StatusOK, + registryResponse: "foo", + expectedErrors: []string{"Error unmarshaling response:"}, + }, + "happy path - no response errors": { + registryResponseStatusCode: http.StatusOK, + registryResponse: `{"result":"success"}`, + expectedErrors: []string{}, + }, + "happy path - with response errors": { + registryResponseStatusCode: http.StatusOK, + registryResponse: `{"result":"failure","errors":["error1","error2","error3"]}`, + expectedErrors: []string{"error1", "error2", "error3"}, + }, + } + + for name, test := range tests { + client := http.DefaultClient + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(test.registryResponseStatusCode) + w.Write([]byte(test.registryResponse)) + })) + registry := server.Listener.Addr().String() + + if !test.simulateClientError { + server.Start() + defer server.Close() + } else { + registry = "noregistryhere!" + } + + pruneFunc := DeletingLayerPruneFunc(client) + + deletions := dockerregistry.DeleteLayersRequest{ + "layer1": {"aaa/stream1", "bbb/stream2"}, + } + + errs := pruneFunc(registry, deletions) + + if e, a := len(test.expectedErrors), len(errs); e != a { + t.Errorf("%s: expected %d errors (%v), got %d (%v)", name, e, test.expectedErrors, a, errs) + continue + } + for i, e := range test.expectedErrors { + a := errs[i].Error() + if !strings.HasPrefix(a, e) { + t.Errorf("%s: expected error starting with %q, got %q", name, e, a) + } + } } } From bd9acb8bae0fd18f103f61a14d1695c93e9dba52 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Wed, 6 May 2015 15:29:51 -0400 Subject: [PATCH 08/21] Image Pruning Update layer deletion responses to include per-layer errors. --- pkg/cmd/dockerregistry/dockerregistry.go | 15 +++-- pkg/cmd/experimental/imageprune/imageprune.go | 2 +- pkg/image/prune/imagepruner.go | 41 +++++++----- pkg/image/prune/imagepruner_test.go | 34 +++++++--- pkg/image/prune/summary.go | 62 +++++++++++++++---- 5 files changed, 108 insertions(+), 46 deletions(-) diff --git a/pkg/cmd/dockerregistry/dockerregistry.go b/pkg/cmd/dockerregistry/dockerregistry.go index 80d0027601ec..54ceeda01d3e 100644 --- a/pkg/cmd/dockerregistry/dockerregistry.go +++ b/pkg/cmd/dockerregistry/dockerregistry.go @@ -59,7 +59,7 @@ func (r DeleteLayersRequest) AddStream(layer, stream string) { type DeleteLayersResponse struct { Result string - Errors []string + Errors map[string][]string } // deleteLayerFunc returns an http.HandlerFunc that is able to fully delete a @@ -89,11 +89,10 @@ func deleteLayerFunc(app *handlers.App) http.HandlerFunc { } adminService := app.Registry().AdminService() - errs := []error{} + errs := map[string][]error{} for layer, repos := range deletions { log.Infof("Deleting layer=%q, repos=%v", layer, repos) - layerErrs := adminService.DeleteLayer(layer, repos) - errs = append(errs, layerErrs...) + errs[layer] = adminService.DeleteLayer(layer, repos) } log.Infof("errs=%v", errs) @@ -108,10 +107,14 @@ func deleteLayerFunc(app *handlers.App) http.HandlerFunc { response := DeleteLayersResponse{ Result: result, + Errors: map[string][]string{}, } - for _, err := range errs { - response.Errors = append(response.Errors, err.Error()) + for layer, layerErrors := range errs { + response.Errors[layer] = []string{} + for _, err := range layerErrors { + response.Errors[layer] = append(response.Errors[layer], err.Error()) + } } buf, err := json.Marshal(&response) diff --git a/pkg/cmd/experimental/imageprune/imageprune.go b/pkg/cmd/experimental/imageprune/imageprune.go index 91a8ab750c2d..921a792579a3 100644 --- a/pkg/cmd/experimental/imageprune/imageprune.go +++ b/pkg/cmd/experimental/imageprune/imageprune.go @@ -64,7 +64,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri prune.DescribingImagePruneFunc(out)(image, referencedStreams) return prune.DeletingImagePruneFunc(osClient.Images(), osClient)(image, referencedStreams) } - layerPruneFunc = func(registryURL string, req dockerregistry.DeleteLayersRequest) []error { + layerPruneFunc = func(registryURL string, req dockerregistry.DeleteLayersRequest) (error, map[string][]error) { prune.DescribingLayerPruneFunc(out)(registryURL, req) return prune.DeletingLayerPruneFunc(http.DefaultClient)(registryURL, req) } diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index f1d9dadff349..f7fecd0fb1bc 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -40,7 +40,7 @@ type ImagePruneFunc func(image *imageapi.Image, streams []*imageapi.ImageStream) // LayerPruneFunc is a function that is invoked for each registry, along with // a DeleteLayersRequest that contains the layers that can be pruned and the // image stream names that reference each layer. -type LayerPruneFunc func(registryURL string, req dockerregistry.DeleteLayersRequest) []error +type LayerPruneFunc func(registryURL string, req dockerregistry.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) // ImagePruner knows how to prune images and layers. type ImagePruner interface { @@ -103,7 +103,7 @@ func NewImagePruner(minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, imag return nil, fmt.Errorf("Error listing image streams: %v", err) } - allPods, err := pods.Pods(kapi.NamespaceAll).List(labels.Everything()) + allPods, err := pods.Pods(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) if err != nil { return nil, fmt.Errorf("Error listing pods: %v", err) } @@ -585,7 +585,8 @@ func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFu for registryURL, req := range registryDeletionRequests { glog.V(4).Infof("Invoking layerPruneFunc with registry=%q, req=%#v", registryURL, req) - layerPruneFunc(registryURL, req) + requestError, layerErrors := layerPruneFunc(registryURL, req) + glog.V(4).Infof("layerPruneFunc requestError=%v, layerErrors=%#v", requestError, layerErrors) } } @@ -647,9 +648,12 @@ func DeletingImagePruneFunc(images client.ImageInterface, streams client.ImageSt // DescribingLayerPruneFunc returns a LayerPruneFunc that writes information // about the layers that are eligible for pruning to out. func DescribingLayerPruneFunc(out io.Writer) LayerPruneFunc { - return func(registryURL string, deletions dockerregistry.DeleteLayersRequest) []error { + return func(registryURL string, deletions dockerregistry.DeleteLayersRequest) (error, map[string][]error) { + result := map[string][]error{} + fmt.Fprintf(out, "Pruning from registry %q\n", registryURL) for layer, repos := range deletions { + result[layer] = []error{} fmt.Fprintf(out, "\tLayer %q\n", layer) if len(repos) > 0 { fmt.Fprint(out, "\tReferenced streams:\n") @@ -658,7 +662,7 @@ func DescribingLayerPruneFunc(out io.Writer) LayerPruneFunc { fmt.Fprintf(out, "\t\t%q\n", repo) } } - return []error{} + return nil, result } } @@ -672,53 +676,56 @@ func DescribingLayerPruneFunc(out io.Writer) LayerPruneFunc { // key being a layer, and each value being a list of Docker image repository // names referenced by the layer. func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { - return func(registryURL string, deletions dockerregistry.DeleteLayersRequest) []error { - errs := []error{} - + return func(registryURL string, deletions dockerregistry.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) { glog.V(4).Infof("Starting pruning of layers from %q, req %#v", registryURL, deletions) body, err := json.Marshal(&deletions) if err != nil { glog.Errorf("Error marshaling request body: %v", err) - return []error{fmt.Errorf("Error creating request body: %v", err)} + return fmt.Errorf("Error creating request body: %v", err), nil } //TODO https req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/admin/layers", registryURL), bytes.NewReader(body)) if err != nil { glog.Errorf("Error creating request: %v", err) - return []error{fmt.Errorf("Error creating request: %v", err)} + return fmt.Errorf("Error creating request: %v", err), nil } glog.V(4).Infof("Sending request to registry") resp, err := registryClient.Do(req) if err != nil { glog.Errorf("Error sending request: %v", err) - return []error{fmt.Errorf("Error sending request: %v", err)} + return fmt.Errorf("Error sending request: %v", err), nil } defer resp.Body.Close() buf, err := ioutil.ReadAll(resp.Body) if err != nil { glog.Errorf("Error reading response body: %v", err) - return []error{fmt.Errorf("Error reading response body: %v", err)} + return fmt.Errorf("Error reading response body: %v", err), nil } if resp.StatusCode != http.StatusOK { glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) - return []error{fmt.Errorf("Unexpected status code %d in response %s", resp.StatusCode, buf)} + return fmt.Errorf("Unexpected status code %d in response %s", resp.StatusCode, buf), nil } var deleteResponse dockerregistry.DeleteLayersResponse if err := json.Unmarshal(buf, &deleteResponse); err != nil { glog.Errorf("Error unmarshaling response: %v", err) - return []error{fmt.Errorf("Error unmarshaling response: %v", err)} + return fmt.Errorf("Error unmarshaling response: %v", err), nil } - for _, e := range deleteResponse.Errors { - errs = append(errs, errors.New(e)) + errs := map[string][]error{} + + for layer, layerErrors := range deleteResponse.Errors { + errs[layer] = []error{} + for _, err := range layerErrors { + errs[layer] = append(errs[layer], errors.New(err)) + } } - return errs + return nil, errs } } diff --git a/pkg/image/prune/imagepruner_test.go b/pkg/image/prune/imagepruner_test.go index 46453a69bd2f..a8d1e910663d 100644 --- a/pkg/image/prune/imagepruner_test.go +++ b/pkg/image/prune/imagepruner_test.go @@ -592,8 +592,8 @@ func TestImagePruning(t *testing.T) { return []error{} } - layerPruneFunc := func(registryURL string, req dockerregistry.DeleteLayersRequest) []error { - return []error{} + layerPruneFunc := func(registryURL string, req dockerregistry.DeleteLayersRequest) (error, map[string][]error) { + return nil, map[string][]error{} } p.Run(imagePruneFunc, layerPruneFunc) @@ -789,7 +789,7 @@ func TestLayerPruning(t *testing.T) { return []error{} } - layerPruneFunc := func(registryURL string, req dockerregistry.DeleteLayersRequest) []error { + layerPruneFunc := func(registryURL string, req dockerregistry.DeleteLayersRequest) (error, map[string][]error) { registryDeletions, ok := actualDeletions[registryURL] if !ok { registryDeletions = util.NewStringSet() @@ -807,7 +807,7 @@ func TestLayerPruning(t *testing.T) { actualDeletions[registryURL] = registryDeletions actualUpdatedStreams[registryURL] = streamUpdates - return []error{} + return nil, map[string][]error{} } p := newImagePruner(60, 1, &test.images, &test.streams, &kapi.PodList{}, &kapi.ReplicationControllerList{}, &buildapi.BuildConfigList{}, &buildapi.BuildList{}, &deployapi.DeploymentConfigList{}) @@ -864,21 +864,22 @@ func TestDeletingLayerPruneFunc(t *testing.T) { simulateClientError bool registryResponseStatusCode int registryResponse string + expectedRequestError string expectedErrors []string }{ "client error": { - simulateClientError: true, - expectedErrors: []string{"Error sending request:"}, + simulateClientError: true, + expectedRequestError: "Error sending request:", }, "non-200 response": { registryResponseStatusCode: http.StatusInternalServerError, - expectedErrors: []string{fmt.Sprintf("Unexpected status code %d in response", http.StatusInternalServerError)}, + expectedRequestError: fmt.Sprintf("Unexpected status code %d in response", http.StatusInternalServerError), registryResponse: "{}", }, "error unmarshaling response body": { registryResponseStatusCode: http.StatusOK, registryResponse: "foo", - expectedErrors: []string{"Error unmarshaling response:"}, + expectedRequestError: "Error unmarshaling response:", }, "happy path - no response errors": { registryResponseStatusCode: http.StatusOK, @@ -887,7 +888,7 @@ func TestDeletingLayerPruneFunc(t *testing.T) { }, "happy path - with response errors": { registryResponseStatusCode: http.StatusOK, - registryResponse: `{"result":"failure","errors":["error1","error2","error3"]}`, + registryResponse: `{"result":"failure","errors":{"layer1":["error1","error2","error3"]}}`, expectedErrors: []string{"error1", "error2", "error3"}, }, } @@ -914,8 +915,21 @@ func TestDeletingLayerPruneFunc(t *testing.T) { "layer1": {"aaa/stream1", "bbb/stream2"}, } - errs := pruneFunc(registry, deletions) + requestError, layerErrors := pruneFunc(registry, deletions) + + gotError := requestError != nil + expectError := len(test.expectedRequestError) != 0 + if e, a := expectError, gotError; e != a { + t.Errorf("%s: requestError: expected %t, got %t: %v", name, e, a, requestError) + continue + } + if gotError { + if e, a := test.expectedRequestError, requestError; !strings.HasPrefix(a.Error(), e) { + t.Errorf("%s: expected request error %q, got %q", name, e, a) + } + } + errs := layerErrors["layer1"] if e, a := len(test.expectedErrors), len(errs); e != a { t.Errorf("%s: expected %d errors (%v), got %d (%v)", name, e, test.expectedErrors, a, errs) continue diff --git a/pkg/image/prune/summary.go b/pkg/image/prune/summary.go index 2902d3cda761..d9db74578c0a 100644 --- a/pkg/image/prune/summary.go +++ b/pkg/image/prune/summary.go @@ -16,17 +16,33 @@ type summarizingPruner struct { imageFailures []string imageErrors []error - layerSuccesses []string - layerFailures []string - layerErrors []error + /* + { + registry1: { + layer1: { + requestError: nil, + layerErrors: [err1, err2], + }, + ..., + }, + registry2: ... + } + */ + registryResults map[string]registryResult +} + +type registryResult struct { + requestError error + layerErrors map[string][]error } var _ ImagePruner = &summarizingPruner{} func NewSummarizingImagePruner(pruner ImagePruner, out io.Writer) ImagePruner { return &summarizingPruner{ - delegate: pruner, - out: out, + delegate: pruner, + out: out, + registryResults: map[string]registryResult{}, } } @@ -36,12 +52,30 @@ func (p *summarizingPruner) Run(baseImagePruneFunc ImagePruneFunc, baseLayerPrun } func (p *summarizingPruner) summarize() { - fmt.Fprintln(p.out, "IMAGE PRUNING SUMMARY:") + fmt.Fprintln(p.out, "\nIMAGE PRUNING SUMMARY:") fmt.Fprintf(p.out, "# Image prune successes: %d\n", len(p.imageSuccesses)) fmt.Fprintf(p.out, "# Image prune errors: %d\n", len(p.imageFailures)) - fmt.Fprintln(p.out, "LAYER PRUNING SUMMARY:") - fmt.Fprintf(p.out, "# Layer prune successes: %d\n", len(p.layerSuccesses)) - fmt.Fprintf(p.out, "# Layer prune errors: %d\n", len(p.layerFailures)) + + fmt.Fprintln(p.out, "\nLAYER PRUNING SUMMARY:") + for registry, result := range p.registryResults { + p.summarizeRegistry(registry, result) + } +} + +func (p *summarizingPruner) summarizeRegistry(registry string, result registryResult) { + fmt.Fprintf(p.out, "\tRegistry: %s\n", registry) + fmt.Fprintf(p.out, "\t\tRequest sent successfully: %t\n", result.requestError == nil) + successes, failures := 0, 0 + for _, errs := range result.layerErrors { + switch len(errs) { + case 0: + successes++ + default: + failures++ + } + } + fmt.Fprintf(p.out, "\t\t# Layer prune successes: %d\n", successes) + fmt.Fprintf(p.out, "\t\t# Layer prune errors: %d\n", failures) } func (p *summarizingPruner) imagePruneFunc(base ImagePruneFunc) ImagePruneFunc { @@ -59,8 +93,12 @@ func (p *summarizingPruner) imagePruneFunc(base ImagePruneFunc) ImagePruneFunc { } func (p *summarizingPruner) layerPruneFunc(base LayerPruneFunc) LayerPruneFunc { - return func(registryURL string, req dockerregistry.DeleteLayersRequest) []error { - errs := base(registryURL, req) - return errs + return func(registryURL string, req dockerregistry.DeleteLayersRequest) (error, map[string][]error) { + requestError, layerErrors := base(registryURL, req) + p.registryResults[registryURL] = registryResult{ + requestError: requestError, + layerErrors: layerErrors, + } + return requestError, layerErrors } } From 8daa4a7dbf0b0f10008bdcbab2c05f0e3c9b7a4d Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Fri, 8 May 2015 12:51:36 -0400 Subject: [PATCH 09/21] Image pruning Remove fronting mux router and use route and auth extensions added upstream in registry. Move admin registry handlers into pkg/dockerregistry/server. Rename --older-than to --keep-younger-than and make it a time.Duration. --- pkg/cmd/dockerregistry/dockerregistry.go | 209 ++++-------------- pkg/cmd/experimental/imageprune/imageprune.go | 21 +- pkg/dockerregistry/server/admin.go | 188 ++++++++++++++++ pkg/dockerregistry/server/auth.go | 78 ++++--- pkg/dockerregistry/server/healthz.go | 12 + pkg/image/prune/imagepruner.go | 50 ++--- pkg/image/prune/summary.go | 4 +- 7 files changed, 335 insertions(+), 227 deletions(-) create mode 100644 pkg/dockerregistry/server/admin.go create mode 100644 pkg/dockerregistry/server/healthz.go diff --git a/pkg/cmd/dockerregistry/dockerregistry.go b/pkg/cmd/dockerregistry/dockerregistry.go index 54ceeda01d3e..65d1a9403237 100644 --- a/pkg/cmd/dockerregistry/dockerregistry.go +++ b/pkg/cmd/dockerregistry/dockerregistry.go @@ -3,7 +3,6 @@ package dockerregistry import ( "crypto/tls" "crypto/x509" - "encoding/json" "fmt" "io" "io/ioutil" @@ -13,174 +12,15 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/distribution/configuration" "github.com/docker/distribution/context" - "github.com/docker/distribution/health" + "github.com/docker/distribution/registry/auth" "github.com/docker/distribution/registry/handlers" _ "github.com/docker/distribution/registry/storage/driver/filesystem" _ "github.com/docker/distribution/registry/storage/driver/s3" "github.com/docker/distribution/version" gorillahandlers "github.com/gorilla/handlers" - "github.com/gorilla/mux" - _ "github.com/openshift/origin/pkg/dockerregistry/server" + "github.com/openshift/origin/pkg/dockerregistry/server" ) -func newOpenShiftHandler(app *handlers.App) http.Handler { - router := mux.NewRouter() - router.HandleFunc("/healthz", health.StatusHandler) - // TODO add https scheme - router.HandleFunc("/admin/layers", deleteLayerFunc(app)).Methods("DELETE") - //router.HandleFunc("/admin/manifests", deleteManifestFunc(app)).Methods("DELETE") - // delegate to the registry if it's not 1 of the OpenShift routes - router.NotFoundHandler = app - - return router -} - -// DeleteLayersRequest is a mapping from layers to the image repositories that -// reference them. Below is a sample request: -// -// { -// "layer1": ["repo1", "repo2"], -// "layer2": ["repo1", "repo3"], -// ... -// } -type DeleteLayersRequest map[string][]string - -// AddLayer adds a layer to the request if it doesn't already exist. -func (r DeleteLayersRequest) AddLayer(layer string) { - if _, ok := r[layer]; !ok { - r[layer] = []string{} - } -} - -// AddStream adds an image stream reference to the layer. -func (r DeleteLayersRequest) AddStream(layer, stream string) { - r[layer] = append(r[layer], stream) -} - -type DeleteLayersResponse struct { - Result string - Errors map[string][]string -} - -// deleteLayerFunc returns an http.HandlerFunc that is able to fully delete a -// layer from storage. -func deleteLayerFunc(app *handlers.App) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - defer req.Body.Close() - log.Infof("deleteLayerFunc invoked") - - //TODO verify auth - - body, err := ioutil.ReadAll(req.Body) - if err != nil { - //TODO - log.Errorf("Error reading body: %v", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - deletions := DeleteLayersRequest{} - err = json.Unmarshal(body, &deletions) - if err != nil { - //TODO - log.Errorf("Error unmarshaling body: %v", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - adminService := app.Registry().AdminService() - errs := map[string][]error{} - for layer, repos := range deletions { - log.Infof("Deleting layer=%q, repos=%v", layer, repos) - errs[layer] = adminService.DeleteLayer(layer, repos) - } - - log.Infof("errs=%v", errs) - - var result string - switch len(errs) { - case 0: - result = "success" - default: - result = "failure" - } - - response := DeleteLayersResponse{ - Result: result, - Errors: map[string][]string{}, - } - - for layer, layerErrors := range errs { - response.Errors[layer] = []string{} - for _, err := range layerErrors { - response.Errors[layer] = append(response.Errors[layer], err.Error()) - } - } - - buf, err := json.Marshal(&response) - if err != nil { - w.Write([]byte(fmt.Sprintf("Error marshaling response: %v", err))) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Write(buf) - w.WriteHeader(http.StatusOK) - } -} - -/* -type DeleteManifestsRequest map[string][]string - -func (r *DeleteManifestsRequest) AddManifest(revision string) { - if _, ok := r[revision]; !ok { - r[revision] = []string{} - } -} - -func (r *DeleteManifestsRequest) AddStream(revision, stream string) { - r[revision] = append(r[revision], stream) -} - -func deleteManifestsFunc(app *handlers.App) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - defer req.Body.Close() - - //TODO verify auth - - body, err := ioutil.ReadAll(req.Body) - if err != nil { - //TODO - log.Errorf("Error reading body: %v", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - deletions := DeleteManifestsRequest{} - err = json.Unmarshal(body, &deletions) - if err != nil { - //TODO - log.Errorf("Error unmarshaling body: %v", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - adminService := app.Registry().AdminService() - errs := []error{} - for revision, repos := range deletions { - log.Infof("Deleting manifest revision=%q, repos=%v", revision, repos) - manifestErrs := adminService.DeleteManifest(revision, repos) - errs = append(errs, manifestErrs...) - } - - log.Infof("errs=%v", errs) - - //TODO write response - w.WriteHeader(http.StatusOK) - } -} -*/ - // Execute runs the Docker registry. func Execute(configFile io.Reader) { config, err := configuration.Parse(configFile) @@ -199,8 +39,49 @@ func Execute(configFile io.Reader) { ctx := context.Background() app := handlers.NewApp(ctx, *config) - handler := newOpenShiftHandler(app) - handler = gorillahandlers.CombinedLoggingHandler(os.Stdout, handler) + + // register OpenShift routes + app.RegisterRoute(app.NewRoute().Path("/healthz"), server.HealthzHandler, handlers.NameNotRequired, handlers.NoCustomAccessRecords) + + // TODO add https scheme + adminRouter := app.NewRoute().PathPrefix("/admin/").Subrouter() + + pruneAccessRecords := func(*http.Request) []auth.Access { + return []auth.Access{ + { + Resource: auth.Resource{ + Type: "admin", + }, + Action: "prune", + }, + } + } + + app.RegisterRoute( + // DELETE /admin/layers + adminRouter.Path("/layers").Methods("DELETE"), + // handler + server.DeleteLayersHandler(app.Registry().AdminService()), + // repo name not required in url + handlers.NameNotRequired, + // custom access records + pruneAccessRecords, + ) + + app.RegisterRoute( + // DELETE /admin/manifests + adminRouter.Path("/manifests").Methods("DELETE"), + // handler + server.DeleteManifestsHandler(app.Registry().AdminService()), + // repo name not required in url + handlers.NameNotRequired, + // custom access records + pruneAccessRecords, + ) + + //app.RegisterRoute(app.NewRoute().Path("/admin/repositories/{repository}/").Methods("DELETE"), server.DeleteRepositoryHandler(app.Registry().AdminService()), func(*http.Request) bool { return true }) + + handler := gorillahandlers.CombinedLoggingHandler(os.Stdout, app) if config.HTTP.TLS.Certificate == "" { context.GetLogger(app).Infof("listening on %v", config.HTTP.Addr) diff --git a/pkg/cmd/experimental/imageprune/imageprune.go b/pkg/cmd/experimental/imageprune/imageprune.go index 921a792579a3..7130728543dc 100644 --- a/pkg/cmd/experimental/imageprune/imageprune.go +++ b/pkg/cmd/experimental/imageprune/imageprune.go @@ -4,13 +4,14 @@ import ( "fmt" "io" "net/http" + "time" "github.com/golang/glog" + "github.com/openshift/origin/pkg/dockerregistry/server" imageapi "github.com/openshift/origin/pkg/image/api" "github.com/openshift/origin/pkg/image/prune" "github.com/spf13/cobra" - "github.com/openshift/origin/pkg/cmd/dockerregistry" "github.com/openshift/origin/pkg/cmd/util/clientcmd" ) @@ -18,16 +19,16 @@ const longDesc = ` ` type config struct { - DryRun bool - MinimumResourcePruningAge int - TagRevisionsToKeep int + DryRun bool + KeepYoungerThan time.Duration + TagRevisionsToKeep int } func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command { cfg := &config{ - DryRun: true, - MinimumResourcePruningAge: 60, - TagRevisionsToKeep: 3, + DryRun: true, + KeepYoungerThan: 60 * time.Minute, + TagRevisionsToKeep: 3, } cmd := &cobra.Command{ @@ -45,7 +46,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri glog.Fatalf("Error getting client: %v", err) } - pruner, err := prune.NewImagePruner(cfg.MinimumResourcePruningAge, cfg.TagRevisionsToKeep, osClient, osClient, kClient, kClient, osClient, osClient, osClient) + pruner, err := prune.NewImagePruner(cfg.KeepYoungerThan, cfg.TagRevisionsToKeep, osClient, osClient, kClient, kClient, osClient, osClient, osClient) if err != nil { glog.Fatalf("Error creating image pruner: %v", err) } @@ -64,7 +65,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri prune.DescribingImagePruneFunc(out)(image, referencedStreams) return prune.DeletingImagePruneFunc(osClient.Images(), osClient)(image, referencedStreams) } - layerPruneFunc = func(registryURL string, req dockerregistry.DeleteLayersRequest) (error, map[string][]error) { + layerPruneFunc = func(registryURL string, req server.DeleteLayersRequest) (error, map[string][]error) { prune.DescribingLayerPruneFunc(out)(registryURL, req) return prune.DeletingLayerPruneFunc(http.DefaultClient)(registryURL, req) } @@ -79,7 +80,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri } cmd.Flags().BoolVar(&cfg.DryRun, "dry-run", cfg.DryRun, "Perform an image pruning dry-run, displaying what would be deleted but not actually deleting anything (default=true).") - cmd.Flags().IntVar(&cfg.MinimumResourcePruningAge, "older-than", cfg.MinimumResourcePruningAge, "Specify the minimum age for an image to be prunable, as well as the minimum age for an image stream or pod that references an image to be prunable.") + cmd.Flags().DurationVar(&cfg.KeepYoungerThan, "keep-younger-than", cfg.KeepYoungerThan, "Specify the minimum age for an image to be prunable, as well as the minimum age for an image stream or pod that references an image to be prunable.") cmd.Flags().IntVar(&cfg.TagRevisionsToKeep, "keep-tag-revisions", cfg.TagRevisionsToKeep, "Specify the number of image revisions for a tag in an image stream that will be preserved.") return cmd diff --git a/pkg/dockerregistry/server/admin.go b/pkg/dockerregistry/server/admin.go new file mode 100644 index 000000000000..7ec526c5f1a3 --- /dev/null +++ b/pkg/dockerregistry/server/admin.go @@ -0,0 +1,188 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + + log "github.com/Sirupsen/logrus" + "github.com/docker/distribution" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/registry/handlers" +) + +// DeleteLayersRequest is a mapping from layers to the image repositories that +// reference them. Below is a sample request: +// +// { +// "layer1": ["repo1", "repo2"], +// "layer2": ["repo1", "repo3"], +// ... +// } +type DeleteLayersRequest map[string][]string + +// AddLayer adds a layer to the request if it doesn't already exist. +func (r DeleteLayersRequest) AddLayer(layer string) { + if _, ok := r[layer]; !ok { + r[layer] = []string{} + } +} + +// AddStream adds an image stream reference to the layer. +func (r DeleteLayersRequest) AddStream(layer, stream string) { + r[layer] = append(r[layer], stream) +} + +type DeleteLayersResponse struct { + Result string + Errors map[string][]string +} + +// deleteLayerFunc returns an http.HandlerFunc that is able to fully delete a +// layer from storage. +func DeleteLayersHandler(adminService distribution.AdminService) func(ctx *handlers.Context, r *http.Request) http.Handler { + return func(ctx *handlers.Context, r *http.Request) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + log.Infof("deleteLayerFunc invoked") + + decoder := json.NewDecoder(req.Body) + deletions := DeleteLayersRequest{} + if err := decoder.Decode(&deletions); err != nil { + //TODO + log.Errorf("Error unmarshaling body: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + errs := map[string][]error{} + for layer, repos := range deletions { + log.Infof("Deleting layer=%q, repos=%v", layer, repos) + errs[layer] = adminService.DeleteLayer(layer, repos) + } + + log.Infof("errs=%v", errs) + + var result string + switch len(errs) { + case 0: + result = "success" + default: + result = "failure" + } + + response := DeleteLayersResponse{ + Result: result, + Errors: map[string][]string{}, + } + + for layer, layerErrors := range errs { + response.Errors[layer] = []string{} + for _, err := range layerErrors { + response.Errors[layer] = append(response.Errors[layer], err.Error()) + } + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + encoder := json.NewEncoder(w) + if err := encoder.Encode(&response); err != nil { + w.Write([]byte(fmt.Sprintf("Error marshaling response: %v", err))) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + }) + } +} + +type DeleteManifestsRequest map[string][]string + +func (r DeleteManifestsRequest) AddManifest(revision string) { + if _, ok := r[revision]; !ok { + r[revision] = []string{} + } +} + +func (r DeleteManifestsRequest) AddStream(revision, stream string) { + r[revision] = append(r[revision], stream) +} + +type DeleteManifestsResponse struct { + Result string + Errors map[string][]string +} + +func DeleteManifestsHandler(adminService distribution.AdminService) func(ctx *handlers.Context, r *http.Request) http.Handler { + return func(ctx *handlers.Context, r *http.Request) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + decoder := json.NewDecoder(req.Body) + deletions := DeleteManifestsRequest{} + if err := decoder.Decode(&deletions); err != nil { + //TODO + log.Errorf("Error unmarshaling body: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + errs := map[string][]error{} + for revision, repos := range deletions { + log.Infof("Deleting manifest revision=%q, repos=%v", revision, repos) + dgst, err := digest.ParseDigest(revision) + if err != nil { + errs[revision] = []error{fmt.Errorf("Error parsing revision %q: %v", revision, err)} + continue + } + errs[revision] = adminService.DeleteManifest(dgst, repos) + } + + log.Infof("errs=%v", errs) + + var result string + switch len(errs) { + case 0: + result = "success" + default: + result = "failure" + } + + response := DeleteManifestsResponse{ + Result: result, + Errors: map[string][]string{}, + } + + for revision, revisionErrors := range errs { + response.Errors[revision] = []string{} + for _, err := range revisionErrors { + response.Errors[revision] = append(response.Errors[revision], err.Error()) + } + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + encoder := json.NewEncoder(w) + if err := encoder.Encode(&response); err != nil { + w.Write([]byte(fmt.Sprintf("Error marshaling response: %v", err))) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + }) + } +} + +func DeleteRepositoryHandler(adminService distribution.AdminService) func(ctx *handlers.Context, r *http.Request) http.Handler { + return func(ctx *handlers.Context, r *http.Request) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + if err := adminService.DeleteRepository(ctx.Repository.Name()); err != nil { + w.Write([]byte(fmt.Sprintf("Error deleting repository %q: %v", ctx.Repository.Name(), err))) + } + + w.WriteHeader(http.StatusNoContent) + }) + } +} diff --git a/pkg/dockerregistry/server/auth.go b/pkg/dockerregistry/server/auth.go index 1c9a73981c58..08c7535af720 100644 --- a/pkg/dockerregistry/server/auth.go +++ b/pkg/dockerregistry/server/auth.go @@ -132,7 +132,7 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg // In case of docker login, hits endpoint /v2 if len(accessRecords) == 0 { - err = VerifyOpenShiftUser(user, client) + err = verifyOpenShiftUser(user, client) if err != nil { challenge.err = err return nil, challenge @@ -143,37 +143,46 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg for _, access := range accessRecords { log.Debugf("%s:%s:%s", access.Resource.Type, access.Resource.Name, access.Action) - if access.Resource.Type != "repository" { - continue - } - - repoParts := strings.SplitN(access.Resource.Name, "/", 2) - if len(repoParts) != 2 { - challenge.err = ErrNamespaceRequired - return nil, challenge - } + switch access.Resource.Type { + case "repository": + repoParts := strings.SplitN(access.Resource.Name, "/", 2) + if len(repoParts) != 2 { + challenge.err = ErrNamespaceRequired + return nil, challenge + } - verb := "" - switch access.Action { - case "push": - verb = "update" - case "pull": - verb = "get" - default: - challenge.err = fmt.Errorf("Unknown action: %s", access.Action) - return nil, challenge - } + verb := "" + switch access.Action { + case "push": + verb = "update" + case "pull": + verb = "get" + default: + challenge.err = fmt.Errorf("Unknown action: %s", access.Action) + return nil, challenge + } - err = VerifyOpenShiftAccess(repoParts[0], repoParts[1], verb, client) - if err != nil { - challenge.err = err - return nil, challenge + if err := verifyImageStreamAccess(repoParts[0], repoParts[1], verb, client); err != nil { + challenge.err = err + return nil, challenge + } + case "admin": + switch access.Action { + case "prune": + if err := verifyPruneAccess(client); err != nil { + challenge.err = err + return nil, challenge + } + default: + challenge.err = fmt.Errorf("Unknown action: %s", access.Action) + return nil, challenge + } } } return WithUserClient(ctx, client), nil } -func VerifyOpenShiftUser(user string, client *client.Client) error { +func verifyOpenShiftUser(user string, client *client.Client) error { userObj, err := client.Users().Get("~") if err != nil { log.Errorf("Get user failed with error: %s", err) @@ -186,7 +195,7 @@ func VerifyOpenShiftUser(user string, client *client.Client) error { return nil } -func VerifyOpenShiftAccess(namespace, imageRepo, verb string, client *client.Client) error { +func verifyImageStreamAccess(namespace, imageRepo, verb string, client *client.Client) error { sar := authorizationapi.SubjectAccessReview{ Verb: verb, Resource: "imageStreams", @@ -203,3 +212,20 @@ func VerifyOpenShiftAccess(namespace, imageRepo, verb string, client *client.Cli } return nil } + +func verifyPruneAccess(client *client.Client) error { + sar := authorizationapi.SubjectAccessReview{ + Verb: "delete", + Resource: "images", + } + response, err := client.ClusterSubjectAccessReviews().Create(&sar) + if err != nil { + log.Errorf("OpenShift client error: %s", err) + return ErrOpenShiftAccessDenied + } + if !response.Allowed { + log.Errorf("OpenShift access denied: %s", response.Reason) + return ErrOpenShiftAccessDenied + } + return nil +} diff --git a/pkg/dockerregistry/server/healthz.go b/pkg/dockerregistry/server/healthz.go new file mode 100644 index 000000000000..cb8eaac7032b --- /dev/null +++ b/pkg/dockerregistry/server/healthz.go @@ -0,0 +1,12 @@ +package server + +import ( + "net/http" + + "github.com/docker/distribution/health" + "github.com/docker/distribution/registry/handlers" +) + +func HealthzHandler(ctx *handlers.Context, r *http.Request) http.Handler { + return http.HandlerFunc(health.StatusHandler) +} diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index f7fecd0fb1bc..305091bd74fa 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -8,6 +8,7 @@ import ( "io" "io/ioutil" "net/http" + "time" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" @@ -20,8 +21,8 @@ import ( buildapi "github.com/openshift/origin/pkg/build/api" buildutil "github.com/openshift/origin/pkg/build/util" "github.com/openshift/origin/pkg/client" - "github.com/openshift/origin/pkg/cmd/dockerregistry" deployapi "github.com/openshift/origin/pkg/deploy/api" + "github.com/openshift/origin/pkg/dockerregistry/server" imageapi "github.com/openshift/origin/pkg/image/api" "github.com/openshift/origin/pkg/image/registry/imagestreamimage" ) @@ -29,8 +30,8 @@ import ( // pruneAlgorithm contains the various settings to use when evaluating images // and layers for pruning. type pruneAlgorithm struct { - minimumAgeInMinutesToPrune int - tagRevisionsToKeep int + keepYoungerThan time.Duration + tagRevisionsToKeep int } // ImagePruneFunc is a function that is invoked for each image that is @@ -40,7 +41,7 @@ type ImagePruneFunc func(image *imageapi.Image, streams []*imageapi.ImageStream) // LayerPruneFunc is a function that is invoked for each registry, along with // a DeleteLayersRequest that contains the layers that can be pruned and the // image stream names that reference each layer. -type LayerPruneFunc func(registryURL string, req dockerregistry.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) +type LayerPruneFunc func(registryURL string, req server.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) // ImagePruner knows how to prune images and layers. type ImagePruner interface { @@ -59,10 +60,10 @@ var _ ImagePruner = &imagePruner{} /* NewImagePruner creates a new ImagePruner. -minimumAgeInMinutesToPrune is the minimum age, in minutes, that a resource -must be in order for the image it references (or an image itself) to be a -candidate for pruning. For example, if minimumAgeInMinutesToPrune is 60, and -an ImageStream is only 59 minutes old, none of the images it references are +Images younger than keepYoungerThan and images referenced by image streams +and/or pods younger than keepYoungerThan are preserved. All other images are +candidates for pruning. For example, if keepYoungerThan is 60m, and an +ImageStream is only 59 minutes old, none of the images it references are eligible for pruning. tagRevisionsToKeep is the number of revisions per tag in an image stream's @@ -92,7 +93,7 @@ ImageStreams having a reference to the image in `status.tags`. Also automatically remove any image layer that is no longer referenced by any images. */ -func NewImagePruner(minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, images client.ImagesInterfacer, streams client.ImageStreamsNamespacer, pods kclient.PodsNamespacer, rcs kclient.ReplicationControllersNamespacer, bcs client.BuildConfigsNamespacer, builds client.BuildsNamespacer, dcs client.DeploymentConfigsNamespacer) (ImagePruner, error) { +func NewImagePruner(keepYoungerThan time.Duration, tagRevisionsToKeep int, images client.ImagesInterfacer, streams client.ImageStreamsNamespacer, pods kclient.PodsNamespacer, rcs kclient.ReplicationControllersNamespacer, bcs client.BuildConfigsNamespacer, builds client.BuildsNamespacer, dcs client.DeploymentConfigsNamespacer) (ImagePruner, error) { allImages, err := images.Images().List(labels.Everything(), fields.Everything()) if err != nil { return nil, fmt.Errorf("Error listing images: %v", err) @@ -128,18 +129,18 @@ func NewImagePruner(minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, imag return nil, fmt.Errorf("Error listing deployment configs: %v", err) } - return newImagePruner(minimumAgeInMinutesToPrune, tagRevisionsToKeep, allImages, allStreams, allPods, allRCs, allBCs, allBuilds, allDCs), nil + return newImagePruner(keepYoungerThan, tagRevisionsToKeep, allImages, allStreams, allPods, allRCs, allBCs, allBuilds, allDCs), nil } // newImagePruner creates a new ImagePruner. -func newImagePruner(minimumAgeInMinutesToPrune int, tagRevisionsToKeep int, images *imageapi.ImageList, streams *imageapi.ImageStreamList, pods *kapi.PodList, rcs *kapi.ReplicationControllerList, bcs *buildapi.BuildConfigList, builds *buildapi.BuildList, dcs *deployapi.DeploymentConfigList) ImagePruner { +func newImagePruner(keepYoungerThan time.Duration, tagRevisionsToKeep int, images *imageapi.ImageList, streams *imageapi.ImageStreamList, pods *kapi.PodList, rcs *kapi.ReplicationControllerList, bcs *buildapi.BuildConfigList, builds *buildapi.BuildList, dcs *deployapi.DeploymentConfigList) ImagePruner { g := graph.New() - glog.V(1).Infof("Creating image pruner with minimumAgeInMinutesToPrune=%d, tagRevisionsToKeep=%d", minimumAgeInMinutesToPrune, tagRevisionsToKeep) + glog.V(1).Infof("Creating image pruner with keepYoungerThan=%v, tagRevisionsToKeep=%d", keepYoungerThan, tagRevisionsToKeep) algorithm := pruneAlgorithm{ - minimumAgeInMinutesToPrune: minimumAgeInMinutesToPrune, - tagRevisionsToKeep: tagRevisionsToKeep, + keepYoungerThan: keepYoungerThan, + tagRevisionsToKeep: tagRevisionsToKeep, } addImagesToGraph(g, images, algorithm) @@ -176,9 +177,8 @@ func addImagesToGraph(g graph.Graph, images *imageapi.ImageList, algorithm prune } age := util.Now().Sub(image.CreationTimestamp.Time) - ageInMinutes := int(age.Minutes()) - if ageInMinutes < algorithm.minimumAgeInMinutesToPrune { - glog.V(4).Infof("Image %q is younger than minimum pruning age, skipping (age=%d)", image.Name, ageInMinutes) + if age < algorithm.keepYoungerThan { + glog.V(4).Infof("Image %q is younger than minimum pruning age, skipping (age=%v)", image.Name, age) continue } @@ -218,7 +218,7 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, al oldImageRevisionReferenceKind := graph.WeakReferencedImageGraphEdgeKind age := util.Now().Sub(stream.CreationTimestamp.Time) - if int(age.Minutes()) < algorithm.minimumAgeInMinutesToPrune { + if age < algorithm.keepYoungerThan { // stream's age is below threshold - use a strong reference for old image revisions instead glog.V(4).Infof("Stream %s/%s is below age threshold - none of its images are eligible for pruning", stream.Namespace, stream.Name) oldImageRevisionReferenceKind = graph.ReferencedImageGraphEdgeKind @@ -277,7 +277,7 @@ func addPodsToGraph(g graph.Graph, pods *kapi.PodList, algorithm pruneAlgorithm) if pod.Status.Phase != kapi.PodRunning && pod.Status.Phase != kapi.PodPending { age := util.Now().Sub(pod.CreationTimestamp.Time) - if int(age.Minutes()) >= algorithm.minimumAgeInMinutesToPrune { + if age >= algorithm.keepYoungerThan { glog.V(4).Infof("Pod %s/%s is not running or pending and age is at least minimum pruning age - skipping", pod.Namespace, pod.Name) // not pending or running, age is at least minimum pruning age, skip continue @@ -538,10 +538,10 @@ func streamLayerReferences(g graph.Graph, layerNode *graph.ImageLayerNode) []*gr } // pruneLayers creates a mapping of registryURLs to -// dockerregistry.DeleteLayersRequest objects, invoking layerPruneFunc for each +// server.DeleteLayersRequest objects, invoking layerPruneFunc for each // registryURL and request. func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFunc LayerPruneFunc) { - registryDeletionRequests := map[string]dockerregistry.DeleteLayersRequest{} + registryDeletionRequests := map[string]server.DeleteLayersRequest{} for _, layerNode := range layerNodes { glog.V(4).Infof("Examining layer %q", layerNode.Layer) @@ -571,7 +571,7 @@ func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFu deletionRequest, ok := registryDeletionRequests[ref.Registry] if !ok { glog.V(4).Infof("Request not found - creating new one") - deletionRequest = dockerregistry.DeleteLayersRequest{} + deletionRequest = server.DeleteLayersRequest{} registryDeletionRequests[ref.Registry] = deletionRequest } @@ -648,7 +648,7 @@ func DeletingImagePruneFunc(images client.ImageInterface, streams client.ImageSt // DescribingLayerPruneFunc returns a LayerPruneFunc that writes information // about the layers that are eligible for pruning to out. func DescribingLayerPruneFunc(out io.Writer) LayerPruneFunc { - return func(registryURL string, deletions dockerregistry.DeleteLayersRequest) (error, map[string][]error) { + return func(registryURL string, deletions server.DeleteLayersRequest) (error, map[string][]error) { result := map[string][]error{} fmt.Fprintf(out, "Pruning from registry %q\n", registryURL) @@ -676,7 +676,7 @@ func DescribingLayerPruneFunc(out io.Writer) LayerPruneFunc { // key being a layer, and each value being a list of Docker image repository // names referenced by the layer. func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { - return func(registryURL string, deletions dockerregistry.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) { + return func(registryURL string, deletions server.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) { glog.V(4).Infof("Starting pruning of layers from %q, req %#v", registryURL, deletions) body, err := json.Marshal(&deletions) if err != nil { @@ -710,7 +710,7 @@ func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { return fmt.Errorf("Unexpected status code %d in response %s", resp.StatusCode, buf), nil } - var deleteResponse dockerregistry.DeleteLayersResponse + var deleteResponse server.DeleteLayersResponse if err := json.Unmarshal(buf, &deleteResponse); err != nil { glog.Errorf("Error unmarshaling response: %v", err) return fmt.Errorf("Error unmarshaling response: %v", err), nil diff --git a/pkg/image/prune/summary.go b/pkg/image/prune/summary.go index d9db74578c0a..1e80f492f898 100644 --- a/pkg/image/prune/summary.go +++ b/pkg/image/prune/summary.go @@ -4,7 +4,7 @@ import ( "fmt" "io" - "github.com/openshift/origin/pkg/cmd/dockerregistry" + "github.com/openshift/origin/pkg/dockerregistry/server" imageapi "github.com/openshift/origin/pkg/image/api" ) @@ -93,7 +93,7 @@ func (p *summarizingPruner) imagePruneFunc(base ImagePruneFunc) ImagePruneFunc { } func (p *summarizingPruner) layerPruneFunc(base LayerPruneFunc) LayerPruneFunc { - return func(registryURL string, req dockerregistry.DeleteLayersRequest) (error, map[string][]error) { + return func(registryURL string, req server.DeleteLayersRequest) (error, map[string][]error) { requestError, layerErrors := base(registryURL, req) p.registryResults[registryURL] = registryResult{ requestError: requestError, From 78c7c08494f66a20ae24a9d83f7b79731bdea3cb Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Fri, 8 May 2015 13:29:46 -0400 Subject: [PATCH 10/21] Image pruning Fix tests from prior refactoring --- pkg/dockerregistry/server/auth_test.go | 10 +++++----- pkg/image/prune/imagepruner_test.go | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/dockerregistry/server/auth_test.go b/pkg/dockerregistry/server/auth_test.go index 758fea3eedc2..bb83f162f043 100644 --- a/pkg/dockerregistry/server/auth_test.go +++ b/pkg/dockerregistry/server/auth_test.go @@ -11,9 +11,9 @@ import ( "golang.org/x/net/context" ) -// TestVerifyOpenShiftAccess mocks openshift http request/response and +// TestVerifyImageStreamAccess mocks openshift http request/response and // tests invalid/valid/scoped openshift tokens. -func TestVerifyOpenShiftAccess(t *testing.T) { +func TestVerifyImageStreamAccess(t *testing.T) { tests := []struct { openshiftStatusCode int openshiftResponse string @@ -44,13 +44,13 @@ func TestVerifyOpenShiftAccess(t *testing.T) { if err != nil { t.Fatal(err) } - err = VerifyOpenShiftAccess("foo", "bar", "create", client) + err = verifyImageStreamAccess("foo", "bar", "create", client) if err == nil || test.expectedError == nil { if err != test.expectedError { - t.Fatal("VerifyOpenShiftAccess did not get expected error - got %s - expected %s", err, test.expectedError) + t.Fatal("verifyImageStreamAccess did not get expected error - got %s - expected %s", err, test.expectedError) } } else if err.Error() != test.expectedError.Error() { - t.Fatal("VerifyOpenShiftAccess did not get expected error - got %s - expected %s", err, test.expectedError) + t.Fatal("verifyImageStreamAccess did not get expected error - got %s - expected %s", err, test.expectedError) } server.Close() } diff --git a/pkg/image/prune/imagepruner_test.go b/pkg/image/prune/imagepruner_test.go index a8d1e910663d..4194955155ab 100644 --- a/pkg/image/prune/imagepruner_test.go +++ b/pkg/image/prune/imagepruner_test.go @@ -16,8 +16,8 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/util" buildapi "github.com/openshift/origin/pkg/build/api" "github.com/openshift/origin/pkg/client" - "github.com/openshift/origin/pkg/cmd/dockerregistry" deployapi "github.com/openshift/origin/pkg/deploy/api" + "github.com/openshift/origin/pkg/dockerregistry/server" imageapi "github.com/openshift/origin/pkg/image/api" ) @@ -580,7 +580,7 @@ func TestImagePruning(t *testing.T) { if len(tcFilter) > 0 && name != tcFilter { continue } - p := newImagePruner(60, 3, &test.images, &test.streams, &test.pods, &test.rcs, &test.bcs, &test.builds, &test.dcs) + p := newImagePruner(60*time.Minute, 3, &test.images, &test.streams, &test.pods, &test.rcs, &test.bcs, &test.builds, &test.dcs) actualDeletions := util.NewStringSet() actualUpdatedStreams := util.NewStringSet() @@ -592,7 +592,7 @@ func TestImagePruning(t *testing.T) { return []error{} } - layerPruneFunc := func(registryURL string, req dockerregistry.DeleteLayersRequest) (error, map[string][]error) { + layerPruneFunc := func(registryURL string, req server.DeleteLayersRequest) (error, map[string][]error) { return nil, map[string][]error{} } @@ -789,7 +789,7 @@ func TestLayerPruning(t *testing.T) { return []error{} } - layerPruneFunc := func(registryURL string, req dockerregistry.DeleteLayersRequest) (error, map[string][]error) { + layerPruneFunc := func(registryURL string, req server.DeleteLayersRequest) (error, map[string][]error) { registryDeletions, ok := actualDeletions[registryURL] if !ok { registryDeletions = util.NewStringSet() @@ -896,22 +896,22 @@ func TestDeletingLayerPruneFunc(t *testing.T) { for name, test := range tests { client := http.DefaultClient - server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(test.registryResponseStatusCode) w.Write([]byte(test.registryResponse)) })) - registry := server.Listener.Addr().String() + registry := testServer.Listener.Addr().String() if !test.simulateClientError { - server.Start() - defer server.Close() + testServer.Start() + defer testServer.Close() } else { registry = "noregistryhere!" } pruneFunc := DeletingLayerPruneFunc(client) - deletions := dockerregistry.DeleteLayersRequest{ + deletions := server.DeleteLayersRequest{ "layer1": {"aaa/stream1", "bbb/stream2"}, } From bbeaa4a4e437f975782227d12122e7d8e1dfce24 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Fri, 8 May 2015 15:19:26 -0400 Subject: [PATCH 11/21] Image pruning Add TODO about pruning manifests (signatures) from the registry. --- pkg/image/prune/imagepruner.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index 305091bd74fa..b0fe8491fb8f 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -583,6 +583,12 @@ func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFu } } + // TODO this really should be a registryPruneFunc instead of a layerPruneFunc, + // sending a map of manifest->streams and a map of layer->streams to the registry. + // The registry should delete each manifest directory from each stream, delete + // each layer dir from each stream, and finally delete each layer from blobs. + // + // Right now this only handles the layer portion. for registryURL, req := range registryDeletionRequests { glog.V(4).Infof("Invoking layerPruneFunc with registry=%q, req=%#v", registryURL, req) requestError, layerErrors := layerPruneFunc(registryURL, req) From 931dd4036dff8b9fb3f1bdfa842b736b5617a871 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Mon, 18 May 2015 13:14:07 -0400 Subject: [PATCH 12/21] Image Pruning Update STI->Source for build strategy. --- pkg/image/prune/imagepruner_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/image/prune/imagepruner_test.go b/pkg/image/prune/imagepruner_test.go index 4194955155ab..606e1000d085 100644 --- a/pkg/image/prune/imagepruner_test.go +++ b/pkg/image/prune/imagepruner_test.go @@ -279,8 +279,8 @@ func buildParameters(strategyType buildapi.BuildStrategyType, fromKind, fromName }, } switch strategyType { - case buildapi.STIBuildStrategyType: - params.Strategy.STIStrategy = &buildapi.STIBuildStrategy{ + case buildapi.SourceBuildStrategyType: + params.Strategy.SourceStrategy = &buildapi.SourceBuildStrategy{ From: &kapi.ObjectReference{ Kind: fromKind, Namespace: fromNamespace, @@ -421,7 +421,7 @@ func TestImagePruning(t *testing.T) { }, "referenced by bc - sti - ImageStreamImage - don't prune": { images: imageList(image("id", registryURL+"/foo/bar@id")), - bcs: bcList(bc("foo", "bc1", buildapi.STIBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.SourceBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), expectedDeletions: []string{}, }, "referenced by bc - docker - ImageStreamImage - don't prune": { @@ -436,7 +436,7 @@ func TestImagePruning(t *testing.T) { }, "referenced by bc - sti - DockerImage - don't prune": { images: imageList(image("id", registryURL+"/foo/bar@id")), - bcs: bcList(bc("foo", "bc1", buildapi.STIBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.SourceBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), expectedDeletions: []string{}, }, "referenced by bc - docker - DockerImage - don't prune": { @@ -451,7 +451,7 @@ func TestImagePruning(t *testing.T) { }, "referenced by build - sti - ImageStreamImage - don't prune": { images: imageList(image("id", registryURL+"/foo/bar@id")), - builds: buildList(build("foo", "build1", buildapi.STIBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), + builds: buildList(build("foo", "build1", buildapi.SourceBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), expectedDeletions: []string{}, }, "referenced by build - docker - ImageStreamImage - don't prune": { @@ -466,7 +466,7 @@ func TestImagePruning(t *testing.T) { }, "referenced by build - sti - DockerImage - don't prune": { images: imageList(image("id", registryURL+"/foo/bar@id")), - builds: buildList(build("foo", "build1", buildapi.STIBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + builds: buildList(build("foo", "build1", buildapi.SourceBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), expectedDeletions: []string{}, }, "referenced by build - docker - DockerImage - don't prune": { @@ -535,7 +535,7 @@ func TestImagePruning(t *testing.T) { rcs: rcList(rc("foo", "rc1", registryURL+"/foo/bar@id2")), pods: podList(pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@id2")), dcs: dcList(dc("foo", "rc1", registryURL+"/foo/bar@id")), - bcs: bcList(bc("foo", "bc1", buildapi.STIBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), + bcs: bcList(bc("foo", "bc1", buildapi.SourceBuildStrategyType, "DockerImage", "foo", registryURL+"/foo/bar@id")), builds: buildList(build("foo", "build1", buildapi.CustomBuildStrategyType, "ImageStreamImage", "foo", "bar@id")), expectedDeletions: []string{}, expectedUpdatedStreams: []string{}, From 9760a723eeb5e3624af3104f34e0e485baacaafe Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Wed, 20 May 2015 12:24:33 -0400 Subject: [PATCH 13/21] Delegate manifest deletion to original Repository Because images are deleted from OpenShift via pruning, when we ask the registry to delete a manifest (really just its signatures in storage), our repository middleware simply needs to delegate the deletion to the original distribution.Repository, as the image will have already been removed from etcd by this point in time. --- pkg/dockerregistry/server/repositorymiddleware.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/dockerregistry/server/repositorymiddleware.go b/pkg/dockerregistry/server/repositorymiddleware.go index 9ef1b6023cdd..524c76f5d541 100644 --- a/pkg/dockerregistry/server/repositorymiddleware.go +++ b/pkg/dockerregistry/server/repositorymiddleware.go @@ -224,9 +224,11 @@ func (r *repository) Put(ctx context.Context, manifest *manifest.SignedManifest) return nil } -// Delete deletes the manifest with digest `dgst`. +// Delete deletes the manifest with digest `dgst`. Note: Image resources +// in OpenShift are deleted via 'osadm prune images'. This function deletes +// the content related to the manifest in the registry's storage (signatures). func (r *repository) Delete(ctx context.Context, dgst digest.Digest) error { - return r.registryClient.Images().Delete(dgst.String()) + return r.Repository.Manifests().Delete(ctx, dgst) } // getImageStream retrieves the ImageStream for r. From e3e1e0e38233913df15416e11989655b546d01b9 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Wed, 20 May 2015 12:28:44 -0400 Subject: [PATCH 14/21] wip --- pkg/cmd/admin/admin.go | 2 - pkg/cmd/admin/prune/prune.go | 1 + pkg/cmd/dockerregistry/dockerregistry.go | 29 +- pkg/cmd/experimental/imageprune/imageprune.go | 87 ------ pkg/dockerregistry/server/admin.go | 262 +++++++---------- pkg/dockerregistry/server/auth.go | 7 +- pkg/image/prune/imagepruner.go | 276 ++++++++---------- pkg/image/prune/imagepruner_test.go | 78 +++-- pkg/image/prune/summary.go | 26 +- 9 files changed, 304 insertions(+), 464 deletions(-) delete mode 100644 pkg/cmd/experimental/imageprune/imageprune.go diff --git a/pkg/cmd/admin/admin.go b/pkg/cmd/admin/admin.go index 743f11578aba..839b40de2ce0 100644 --- a/pkg/cmd/admin/admin.go +++ b/pkg/cmd/admin/admin.go @@ -4,7 +4,6 @@ import ( "fmt" "io" - eximageprune "github.com/openshift/origin/pkg/cmd/experimental/imageprune" "github.com/spf13/cobra" "github.com/openshift/origin/pkg/cmd/admin/node" @@ -48,7 +47,6 @@ func NewCommandAdmin(name, fullName string, out io.Writer) *cobra.Command { cmds.AddCommand(exipfailover.NewCmdIPFailoverConfig(f, fullName, "ipfailover", out)) cmds.AddCommand(exrouter.NewCmdRouter(f, fullName, "router", out)) cmds.AddCommand(exregistry.NewCmdRegistry(f, fullName, "registry", out)) - cmds.AddCommand(eximageprune.NewCmdPruneImages(f, fullName, "prune-images", out)) cmds.AddCommand(buildchain.NewCmdBuildChain(f, fullName, "build-chain")) cmds.AddCommand(node.NewCommandManageNode(f, node.ManageNodeCommandName, fullName+" "+node.ManageNodeCommandName, out)) cmds.AddCommand(cmd.NewCmdConfig(fullName, "config")) diff --git a/pkg/cmd/admin/prune/prune.go b/pkg/cmd/admin/prune/prune.go index 12b40f9e10c8..4c96e9103fdb 100644 --- a/pkg/cmd/admin/prune/prune.go +++ b/pkg/cmd/admin/prune/prune.go @@ -26,6 +26,7 @@ func NewCommandPrune(name, fullName string, f *clientcmd.Factory, out io.Writer) cmds.AddCommand(NewCmdPruneBuilds(f, fullName, PruneBuildsRecommendedName, out)) cmds.AddCommand(NewCmdPruneDeployments(f, fullName, PruneDeploymentsRecommendedName, out)) + cmds.AddCommand(NewCmdPruneImages(f, fullName, PruneImagesRecommendedName, out)) return cmds } diff --git a/pkg/cmd/dockerregistry/dockerregistry.go b/pkg/cmd/dockerregistry/dockerregistry.go index 65d1a9403237..a9aa816b84c2 100644 --- a/pkg/cmd/dockerregistry/dockerregistry.go +++ b/pkg/cmd/dockerregistry/dockerregistry.go @@ -12,6 +12,8 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/distribution/configuration" "github.com/docker/distribution/context" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/auth" "github.com/docker/distribution/registry/handlers" _ "github.com/docker/distribution/registry/storage/driver/filesystem" @@ -58,10 +60,10 @@ func Execute(configFile io.Reader) { } app.RegisterRoute( - // DELETE /admin/layers - adminRouter.Path("/layers").Methods("DELETE"), + // DELETE /admin/blobs/ + adminRouter.Path("/blobs/{digest:"+digest.DigestRegexp.String()+"}").Methods("DELETE"), // handler - server.DeleteLayersHandler(app.Registry().AdminService()), + server.BlobDispatcher, // repo name not required in url handlers.NameNotRequired, // custom access records @@ -69,17 +71,26 @@ func Execute(configFile io.Reader) { ) app.RegisterRoute( - // DELETE /admin/manifests - adminRouter.Path("/manifests").Methods("DELETE"), + // DELETE /admin//manifests/ + adminRouter.Path("/{name:"+v2.RepositoryNameRegexp.String()+"}/manifests/{digest:"+digest.DigestRegexp.String()+"}").Methods("DELETE"), // handler - server.DeleteManifestsHandler(app.Registry().AdminService()), - // repo name not required in url - handlers.NameNotRequired, + server.ManifestDispatcher, + // repo name required in url + handlers.NameRequired, // custom access records pruneAccessRecords, ) - //app.RegisterRoute(app.NewRoute().Path("/admin/repositories/{repository}/").Methods("DELETE"), server.DeleteRepositoryHandler(app.Registry().AdminService()), func(*http.Request) bool { return true }) + app.RegisterRoute( + // DELETE /admin//layers/ + adminRouter.Path("/{name:"+v2.RepositoryNameRegexp.String()+"}/layers/{digest:"+digest.DigestRegexp.String()+"}").Methods("DELETE"), + // handler + server.LayerDispatcher, + // repo name required in url + handlers.NameRequired, + // custom access records + pruneAccessRecords, + ) handler := gorillahandlers.CombinedLoggingHandler(os.Stdout, app) diff --git a/pkg/cmd/experimental/imageprune/imageprune.go b/pkg/cmd/experimental/imageprune/imageprune.go deleted file mode 100644 index 7130728543dc..000000000000 --- a/pkg/cmd/experimental/imageprune/imageprune.go +++ /dev/null @@ -1,87 +0,0 @@ -package imageprune - -import ( - "fmt" - "io" - "net/http" - "time" - - "github.com/golang/glog" - "github.com/openshift/origin/pkg/dockerregistry/server" - imageapi "github.com/openshift/origin/pkg/image/api" - "github.com/openshift/origin/pkg/image/prune" - "github.com/spf13/cobra" - - "github.com/openshift/origin/pkg/cmd/util/clientcmd" -) - -const longDesc = ` -` - -type config struct { - DryRun bool - KeepYoungerThan time.Duration - TagRevisionsToKeep int -} - -func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command { - cfg := &config{ - DryRun: true, - KeepYoungerThan: 60 * time.Minute, - TagRevisionsToKeep: 3, - } - - cmd := &cobra.Command{ - Use: name, - Short: "Prune images", - Long: fmt.Sprintf(longDesc, parentName, name), - - Run: func(cmd *cobra.Command, args []string) { - if len(args) > 0 { - glog.Fatalf("No arguments are allowed to this command") - } - - osClient, kClient, err := f.Clients() - if err != nil { - glog.Fatalf("Error getting client: %v", err) - } - - pruner, err := prune.NewImagePruner(cfg.KeepYoungerThan, cfg.TagRevisionsToKeep, osClient, osClient, kClient, kClient, osClient, osClient, osClient) - if err != nil { - glog.Fatalf("Error creating image pruner: %v", err) - } - - pruner = prune.NewSummarizingImagePruner(pruner, out) - - var ( - imagePruneFunc prune.ImagePruneFunc - layerPruneFunc prune.LayerPruneFunc - ) - - switch cfg.DryRun { - case false: - fmt.Fprintln(out, "Dry run *disabled* - images will be pruned and data will be deleted!") - imagePruneFunc = func(image *imageapi.Image, referencedStreams []*imageapi.ImageStream) []error { - prune.DescribingImagePruneFunc(out)(image, referencedStreams) - return prune.DeletingImagePruneFunc(osClient.Images(), osClient)(image, referencedStreams) - } - layerPruneFunc = func(registryURL string, req server.DeleteLayersRequest) (error, map[string][]error) { - prune.DescribingLayerPruneFunc(out)(registryURL, req) - return prune.DeletingLayerPruneFunc(http.DefaultClient)(registryURL, req) - } - default: - fmt.Fprintln(out, "Dry run enabled - no modifications will be made.") - imagePruneFunc = prune.DescribingImagePruneFunc(out) - layerPruneFunc = prune.DescribingLayerPruneFunc(out) - } - - pruner.Run(imagePruneFunc, layerPruneFunc) - }, - } - - cmd.Flags().BoolVar(&cfg.DryRun, "dry-run", cfg.DryRun, "Perform an image pruning dry-run, displaying what would be deleted but not actually deleting anything (default=true).") - cmd.Flags().DurationVar(&cfg.KeepYoungerThan, "keep-younger-than", cfg.KeepYoungerThan, "Specify the minimum age for an image to be prunable, as well as the minimum age for an image stream or pod that references an image to be prunable.") - cmd.Flags().IntVar(&cfg.TagRevisionsToKeep, "keep-tag-revisions", cfg.TagRevisionsToKeep, "Specify the number of image revisions for a tag in an image stream that will be preserved.") - - return cmd -} diff --git a/pkg/dockerregistry/server/admin.go b/pkg/dockerregistry/server/admin.go index 7ec526c5f1a3..97cbb77f8af1 100644 --- a/pkg/dockerregistry/server/admin.go +++ b/pkg/dockerregistry/server/admin.go @@ -1,188 +1,142 @@ package server import ( - "encoding/json" "fmt" "net/http" - log "github.com/Sirupsen/logrus" - "github.com/docker/distribution" + ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/digest" + "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/handlers" + gorillahandlers "github.com/gorilla/handlers" ) -// DeleteLayersRequest is a mapping from layers to the image repositories that -// reference them. Below is a sample request: -// -// { -// "layer1": ["repo1", "repo2"], -// "layer2": ["repo1", "repo3"], -// ... -// } -type DeleteLayersRequest map[string][]string - -// AddLayer adds a layer to the request if it doesn't already exist. -func (r DeleteLayersRequest) AddLayer(layer string) { - if _, ok := r[layer]; !ok { - r[layer] = []string{} +// BlobDispatcher takes the request context and builds the appropriate handler +// for handling blob requests. +func BlobDispatcher(ctx *handlers.Context, r *http.Request) http.Handler { + reference := ctxu.GetStringValue(ctx, "vars.digest") + dgst, _ := digest.ParseDigest(reference) + + blobHandler := &blobHandler{ + Context: ctx, + Digest: dgst, } -} -// AddStream adds an image stream reference to the layer. -func (r DeleteLayersRequest) AddStream(layer, stream string) { - r[layer] = append(r[layer], stream) + return gorillahandlers.MethodHandler{ + "DELETE": http.HandlerFunc(blobHandler.Delete), + } } -type DeleteLayersResponse struct { - Result string - Errors map[string][]string +// blobHandler handles http operations on blobs. +type blobHandler struct { + *handlers.Context + + Digest digest.Digest } -// deleteLayerFunc returns an http.HandlerFunc that is able to fully delete a -// layer from storage. -func DeleteLayersHandler(adminService distribution.AdminService) func(ctx *handlers.Context, r *http.Request) http.Handler { - return func(ctx *handlers.Context, r *http.Request) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - defer req.Body.Close() - log.Infof("deleteLayerFunc invoked") - - decoder := json.NewDecoder(req.Body) - deletions := DeleteLayersRequest{} - if err := decoder.Decode(&deletions); err != nil { - //TODO - log.Errorf("Error unmarshaling body: %v", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - errs := map[string][]error{} - for layer, repos := range deletions { - log.Infof("Deleting layer=%q, repos=%v", layer, repos) - errs[layer] = adminService.DeleteLayer(layer, repos) - } - - log.Infof("errs=%v", errs) - - var result string - switch len(errs) { - case 0: - result = "success" - default: - result = "failure" - } - - response := DeleteLayersResponse{ - Result: result, - Errors: map[string][]string{}, - } - - for layer, layerErrors := range errs { - response.Errors[layer] = []string{} - for _, err := range layerErrors { - response.Errors[layer] = append(response.Errors[layer], err.Error()) - } - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - encoder := json.NewEncoder(w) - if err := encoder.Encode(&response); err != nil { - w.Write([]byte(fmt.Sprintf("Error marshaling response: %v", err))) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - }) +// Delete deletes the blob from the storage backend. +func (bh *blobHandler) Delete(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + if len(bh.Digest) == 0 { + bh.Errors.Push(v2.ErrorCodeBlobUnknown) + w.WriteHeader(http.StatusNotFound) + return + } + + err := bh.Registry().Blobs().Delete(bh.Digest) + if err != nil { + bh.Errors.PushErr(fmt.Errorf("error deleting blob %q: %v", bh.Digest, err)) + w.WriteHeader(http.StatusBadRequest) + return } + + w.WriteHeader(http.StatusNoContent) } -type DeleteManifestsRequest map[string][]string +// LayerDispatcher takes the request context and builds the appropriate handler +// for handling layer requests. +func LayerDispatcher(ctx *handlers.Context, r *http.Request) http.Handler { + reference := ctxu.GetStringValue(ctx, "vars.digest") + dgst, _ := digest.ParseDigest(reference) + + layerHandler := &layerHandler{ + Context: ctx, + Digest: dgst, + } -func (r DeleteManifestsRequest) AddManifest(revision string) { - if _, ok := r[revision]; !ok { - r[revision] = []string{} + return gorillahandlers.MethodHandler{ + "DELETE": http.HandlerFunc(layerHandler.Delete), } } -func (r DeleteManifestsRequest) AddStream(revision, stream string) { - r[revision] = append(r[revision], stream) +// layerHandler handles http operations on layers. +type layerHandler struct { + *handlers.Context + + Digest digest.Digest } -type DeleteManifestsResponse struct { - Result string - Errors map[string][]string +// Delete deletes the layer link from the repository from the storage backend. +func (lh *layerHandler) Delete(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + + if len(lh.Digest) == 0 { + lh.Errors.Push(v2.ErrorCodeBlobUnknown) + w.WriteHeader(http.StatusNotFound) + return + } + + err := lh.Repository.Layers().Delete(lh.Digest) + if err != nil { + lh.Errors.PushErr(fmt.Errorf("error unlinking layer %q from repo %q: %v", lh.Digest, lh.Repository.Name(), err)) + w.WriteHeader(http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusNoContent) } -func DeleteManifestsHandler(adminService distribution.AdminService) func(ctx *handlers.Context, r *http.Request) http.Handler { - return func(ctx *handlers.Context, r *http.Request) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - defer req.Body.Close() - - decoder := json.NewDecoder(req.Body) - deletions := DeleteManifestsRequest{} - if err := decoder.Decode(&deletions); err != nil { - //TODO - log.Errorf("Error unmarshaling body: %v", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - errs := map[string][]error{} - for revision, repos := range deletions { - log.Infof("Deleting manifest revision=%q, repos=%v", revision, repos) - dgst, err := digest.ParseDigest(revision) - if err != nil { - errs[revision] = []error{fmt.Errorf("Error parsing revision %q: %v", revision, err)} - continue - } - errs[revision] = adminService.DeleteManifest(dgst, repos) - } - - log.Infof("errs=%v", errs) - - var result string - switch len(errs) { - case 0: - result = "success" - default: - result = "failure" - } - - response := DeleteManifestsResponse{ - Result: result, - Errors: map[string][]string{}, - } - - for revision, revisionErrors := range errs { - response.Errors[revision] = []string{} - for _, err := range revisionErrors { - response.Errors[revision] = append(response.Errors[revision], err.Error()) - } - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - encoder := json.NewEncoder(w) - if err := encoder.Encode(&response); err != nil { - w.Write([]byte(fmt.Sprintf("Error marshaling response: %v", err))) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - }) +// ManifestDispatcher takes the request context and builds the appropriate +// handler for handling manifest requests. +func ManifestDispatcher(ctx *handlers.Context, r *http.Request) http.Handler { + reference := ctxu.GetStringValue(ctx, "vars.digest") + dgst, _ := digest.ParseDigest(reference) + + manifestHandler := &manifestHandler{ + Context: ctx, + Digest: dgst, + } + + return gorillahandlers.MethodHandler{ + "DELETE": http.HandlerFunc(manifestHandler.Delete), } } -func DeleteRepositoryHandler(adminService distribution.AdminService) func(ctx *handlers.Context, r *http.Request) http.Handler { - return func(ctx *handlers.Context, r *http.Request) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - defer req.Body.Close() +// manifestHandler handles http operations on mainfests. +type manifestHandler struct { + *handlers.Context + + Digest digest.Digest +} + +// Delete deletes the manifest information from the repository from the storage +// backend. +func (mh *manifestHandler) Delete(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() - if err := adminService.DeleteRepository(ctx.Repository.Name()); err != nil { - w.Write([]byte(fmt.Sprintf("Error deleting repository %q: %v", ctx.Repository.Name(), err))) - } + if len(mh.Digest) == 0 { + mh.Errors.Push(v2.ErrorCodeManifestUnknown) + w.WriteHeader(http.StatusNotFound) + return + } - w.WriteHeader(http.StatusNoContent) - }) + err := mh.Repository.Manifests().Delete(mh.Context, mh.Digest) + if err != nil { + mh.Errors.PushErr(fmt.Errorf("error deleting repo %q, manifest %q: %v", mh.Repository.Name(), mh.Digest, err)) + w.WriteHeader(http.StatusBadRequest) + return } + + w.WriteHeader(http.StatusNoContent) } diff --git a/pkg/dockerregistry/server/auth.go b/pkg/dockerregistry/server/auth.go index 08c7535af720..813abae9fbee 100644 --- a/pkg/dockerregistry/server/auth.go +++ b/pkg/dockerregistry/server/auth.go @@ -166,6 +166,8 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg challenge.err = err return nil, challenge } + + return WithUserClient(ctx, client), nil case "admin": switch access.Action { case "prune": @@ -173,13 +175,16 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg challenge.err = err return nil, challenge } + + return WithUserClient(ctx, client), nil default: challenge.err = fmt.Errorf("Unknown action: %s", access.Action) return nil, challenge } } } - return WithUserClient(ctx, client), nil + + return ctx, nil } func verifyOpenShiftUser(user string, client *client.Client) error { diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index b0fe8491fb8f..a187cee2df1b 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -1,19 +1,12 @@ package prune import ( - "bytes" "encoding/json" - "errors" "fmt" - "io" - "io/ioutil" "net/http" "time" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" - "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" gonum "github.com/gonum/graph" @@ -22,7 +15,6 @@ import ( buildutil "github.com/openshift/origin/pkg/build/util" "github.com/openshift/origin/pkg/client" deployapi "github.com/openshift/origin/pkg/deploy/api" - "github.com/openshift/origin/pkg/dockerregistry/server" imageapi "github.com/openshift/origin/pkg/image/api" "github.com/openshift/origin/pkg/image/registry/imagestreamimage" ) @@ -41,12 +33,15 @@ type ImagePruneFunc func(image *imageapi.Image, streams []*imageapi.ImageStream) // LayerPruneFunc is a function that is invoked for each registry, along with // a DeleteLayersRequest that contains the layers that can be pruned and the // image stream names that reference each layer. -type LayerPruneFunc func(registryURL string, req server.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) +//type LayerPruneFunc func(registryURL string, req server.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) +type LayerPruneFunc func(registryURL, repo, layer string) error +type BlobPruneFunc func(registryURL, blob string) error +type ManifestPruneFunc func(registryURL, repo, manifest string) error // ImagePruner knows how to prune images and layers. type ImagePruner interface { // Run prunes images and layers. - Run(imagePruneFunc ImagePruneFunc, layerPruneFunc LayerPruneFunc) + Run(pruneImage ImagePruneFunc, pruneLayer LayerPruneFunc, pruneBlob BlobPruneFunc, pruneManifest ManifestPruneFunc) } // imagePruner implements ImagePruner. @@ -70,8 +65,10 @@ tagRevisionsToKeep is the number of revisions per tag in an image stream's status.tags that are preserved and ineligible for pruning. Any revision older than tagRevisionsToKeep is eligible for pruning. -images, streams, pods, rcs, bcs, builds, and dcs are client interfaces for -retrieving each respective resource type. +images, streams, pods, rcs, bcs, builds, and dcs are the resources used to run +the pruning algorithm. These should be the full list for each type from the +cluster; otherwise, the pruning algorithm might result in incorrect +calculations and premature pruning. The ImagePruner performs the following logic: remove any image contaning the annotation openshift.io/image.managed=true that was created at least *n* @@ -93,47 +90,7 @@ ImageStreams having a reference to the image in `status.tags`. Also automatically remove any image layer that is no longer referenced by any images. */ -func NewImagePruner(keepYoungerThan time.Duration, tagRevisionsToKeep int, images client.ImagesInterfacer, streams client.ImageStreamsNamespacer, pods kclient.PodsNamespacer, rcs kclient.ReplicationControllersNamespacer, bcs client.BuildConfigsNamespacer, builds client.BuildsNamespacer, dcs client.DeploymentConfigsNamespacer) (ImagePruner, error) { - allImages, err := images.Images().List(labels.Everything(), fields.Everything()) - if err != nil { - return nil, fmt.Errorf("Error listing images: %v", err) - } - - allStreams, err := streams.ImageStreams(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) - if err != nil { - return nil, fmt.Errorf("Error listing image streams: %v", err) - } - - allPods, err := pods.Pods(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) - if err != nil { - return nil, fmt.Errorf("Error listing pods: %v", err) - } - - allRCs, err := rcs.ReplicationControllers(kapi.NamespaceAll).List(labels.Everything()) - if err != nil { - return nil, fmt.Errorf("Error listing replication controllers: %v", err) - } - - allBCs, err := bcs.BuildConfigs(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) - if err != nil { - return nil, fmt.Errorf("Error listing build configs: %v", err) - } - - allBuilds, err := builds.Builds(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) - if err != nil { - return nil, fmt.Errorf("Error listing builds: %v", err) - } - - allDCs, err := dcs.DeploymentConfigs(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) - if err != nil { - return nil, fmt.Errorf("Error listing deployment configs: %v", err) - } - - return newImagePruner(keepYoungerThan, tagRevisionsToKeep, allImages, allStreams, allPods, allRCs, allBCs, allBuilds, allDCs), nil -} - -// newImagePruner creates a new ImagePruner. -func newImagePruner(keepYoungerThan time.Duration, tagRevisionsToKeep int, images *imageapi.ImageList, streams *imageapi.ImageStreamList, pods *kapi.PodList, rcs *kapi.ReplicationControllerList, bcs *buildapi.BuildConfigList, builds *buildapi.BuildList, dcs *deployapi.DeploymentConfigList) ImagePruner { +func NewImagePruner(keepYoungerThan time.Duration, tagRevisionsToKeep int, images *imageapi.ImageList, streams *imageapi.ImageStreamList, pods *kapi.PodList, rcs *kapi.ReplicationControllerList, bcs *buildapi.BuildConfigList, builds *buildapi.BuildList, dcs *deployapi.DeploymentConfigList) ImagePruner { g := graph.New() glog.V(1).Infof("Creating image pruner with keepYoungerThan=%v, tagRevisionsToKeep=%d", keepYoungerThan, tagRevisionsToKeep) @@ -244,6 +201,13 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, al default: kind = oldImageRevisionReferenceKind } + + glog.V(4).Infof("Checking for existing strong reference from stream %s/%s to image %s", stream.Namespace, stream.Name, imageNode.Image.Name) + if edge := g.EdgeBetween(imageStreamNode, imageNode); edge != nil && g.EdgeKind(edge) == graph.ReferencedImageGraphEdgeKind { + glog.V(4).Infof("Strong reference found") + continue + } + glog.V(4).Infof("Adding edge (kind=%d) from %q to %q", kind, imageStreamNode.UniqueName.UniqueName(), imageNode.UniqueName.UniqueName()) g.AddEdge(imageStreamNode, imageNode, kind) @@ -463,7 +427,7 @@ func imageIsPrunable(g graph.Graph, imageNode *graph.ImageNode) bool { // with the image streams that reference the image. After imagePruneFunc is // invoked, the image node is removed from the graph, so that layers eligible // for pruning may later be identified. -func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, imagePruneFunc ImagePruneFunc) { +func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, pruneImage ImagePruneFunc, pruneManifest ManifestPruneFunc) { for _, imageNode := range imageNodes { glog.V(4).Infof("Examining image %q", imageNode.Image.Name) @@ -475,10 +439,24 @@ func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, imagePruneFunc Im glog.V(4).Infof("Image has only weak references - pruning") streams := imageStreamPredecessors(g, imageNode) - if errs := imagePruneFunc(imageNode.Image, streams); len(errs) > 0 { + if errs := pruneImage(imageNode.Image, streams); len(errs) > 0 { glog.Errorf("Error pruning image %q: %v", imageNode.Image.Name, errs) } + for _, stream := range streams { + ref, err := imageapi.DockerImageReferenceForStream(stream) + repoName := fmt.Sprintf("%s/%s", ref.Namespace, ref.Name) + if err != nil { + glog.Errorf("Error constructing DockerImageReference for %q: %v", repoName, err) + continue + } + + glog.V(4).Infof("Invoking pruneManifest for registry %q, repo %q, image %q", ref.Registry, repoName, imageNode.Image.Name) + if err := pruneManifest(ref.Registry, repoName, imageNode.Image.Name); err != nil { + glog.Errorf("Error pruning manifest for registry %q, repo %q, image %q: %v", ref.Registry, repoName, imageNode.Image.Name, err) + } + } + // remove pruned image node from graph, for layer pruning later g.RemoveNode(imageNode) } @@ -487,14 +465,14 @@ func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, imagePruneFunc Im // Run identifies images eligible for pruning, invoking imagePruneFunc for each // image, and then it identifies layers eligible for pruning, invoking // layerPruneFunc for each registry URL that has layers that can be pruned. -func (p *imagePruner) Run(imagePruneFunc ImagePruneFunc, layerPruneFunc LayerPruneFunc) { +func (p *imagePruner) Run(pruneImage ImagePruneFunc, pruneLayer LayerPruneFunc, pruneBlob BlobPruneFunc, pruneManifest ManifestPruneFunc) { allNodes := p.g.NodeList() imageNodes := imageNodeSubgraph(allNodes) - pruneImages(p.g, imageNodes, imagePruneFunc) + pruneImages(p.g, imageNodes, pruneImage, pruneManifest) layerNodes := layerNodeSubgraph(allNodes) - pruneLayers(p.g, layerNodes, layerPruneFunc) + pruneLayers(p.g, layerNodes, pruneLayer, pruneBlob) } // layerNodeSubgraph returns the subset of nodes that are ImageLayerNodes. @@ -540,9 +518,7 @@ func streamLayerReferences(g graph.Graph, layerNode *graph.ImageLayerNode) []*gr // pruneLayers creates a mapping of registryURLs to // server.DeleteLayersRequest objects, invoking layerPruneFunc for each // registryURL and request. -func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFunc LayerPruneFunc) { - registryDeletionRequests := map[string]server.DeleteLayersRequest{} - +func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, pruneLayer LayerPruneFunc, pruneBlob BlobPruneFunc) { for _, layerNode := range layerNodes { glog.V(4).Infof("Examining layer %q", layerNode.Layer) @@ -551,10 +527,11 @@ func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFu continue } + registries := util.NewStringSet() + // get streams that reference layer streamNodes := streamLayerReferences(g, layerNode) - // for each stream, get its registry for _, streamNode := range streamNodes { stream := streamNode.ImageStream streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) @@ -566,46 +543,20 @@ func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, layerPruneFu continue } - // update registry layer deletion request - glog.V(4).Infof("Looking for existing deletion request for registry %q", ref.Registry) - deletionRequest, ok := registryDeletionRequests[ref.Registry] - if !ok { - glog.V(4).Infof("Request not found - creating new one") - deletionRequest = server.DeleteLayersRequest{} - registryDeletionRequests[ref.Registry] = deletionRequest + if !registries.Has(ref.Registry) { + registries.Insert(ref.Registry) + glog.V(4).Infof("Invoking pruneBlob with registry=%q, blob=%q", ref.Registry, layerNode.Layer) + if err := pruneBlob(ref.Registry, layerNode.Layer); err != nil { + glog.Errorf("Error invoking pruneBlob: %v", err) + } } - glog.V(4).Infof("Adding or updating layer %q in deletion request", layerNode.Layer) - deletionRequest.AddLayer(layerNode.Layer) - - glog.V(4).Infof("Adding stream %q", streamName) - deletionRequest.AddStream(layerNode.Layer, streamName) - } - } - - // TODO this really should be a registryPruneFunc instead of a layerPruneFunc, - // sending a map of manifest->streams and a map of layer->streams to the registry. - // The registry should delete each manifest directory from each stream, delete - // each layer dir from each stream, and finally delete each layer from blobs. - // - // Right now this only handles the layer portion. - for registryURL, req := range registryDeletionRequests { - glog.V(4).Infof("Invoking layerPruneFunc with registry=%q, req=%#v", registryURL, req) - requestError, layerErrors := layerPruneFunc(registryURL, req) - glog.V(4).Infof("layerPruneFunc requestError=%v, layerErrors=%#v", requestError, layerErrors) - } -} - -// DescribingImagePruneFunc returns an ImagePruneFunc that writes information -// about the images that are eligible for pruning to out. -func DescribingImagePruneFunc(out io.Writer) ImagePruneFunc { - return func(image *imageapi.Image, referencedStreams []*imageapi.ImageStream) []error { - streamNames := []string{} - for _, stream := range referencedStreams { - streamNames = append(streamNames, fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)) + repoName := fmt.Sprintf("%s/%s", ref.Namespace, ref.Name) + glog.V(4).Infof("Invoking pruneLayer with registry=%q, repo=%q, layer=%q", ref.Registry, repoName, layerNode.Layer) + if err := pruneLayer(ref.Registry, repoName, layerNode.Layer); err != nil { + glog.Errorf("Error invoking pruneLayer: %v", err) + } } - fmt.Fprintf(out, "Pruning image %q and updating image streams %v\n", image.Name, streamNames) - return []error{} } } @@ -651,27 +602,6 @@ func DeletingImagePruneFunc(images client.ImageInterface, streams client.ImageSt } } -// DescribingLayerPruneFunc returns a LayerPruneFunc that writes information -// about the layers that are eligible for pruning to out. -func DescribingLayerPruneFunc(out io.Writer) LayerPruneFunc { - return func(registryURL string, deletions server.DeleteLayersRequest) (error, map[string][]error) { - result := map[string][]error{} - - fmt.Fprintf(out, "Pruning from registry %q\n", registryURL) - for layer, repos := range deletions { - result[layer] = []error{} - fmt.Fprintf(out, "\tLayer %q\n", layer) - if len(repos) > 0 { - fmt.Fprint(out, "\tReferenced streams:\n") - } - for _, repo := range repos { - fmt.Fprintf(out, "\t\t%q\n", repo) - } - } - return nil, result - } -} - // DeletingLayerPruneFunc returns a LayerPruneFunc that sends the // DeleteLayersRequest to the Docker registry. // @@ -682,56 +612,32 @@ func DescribingLayerPruneFunc(out io.Writer) LayerPruneFunc { // key being a layer, and each value being a list of Docker image repository // names referenced by the layer. func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { - return func(registryURL string, deletions server.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) { - glog.V(4).Infof("Starting pruning of layers from %q, req %#v", registryURL, deletions) - body, err := json.Marshal(&deletions) - if err != nil { - glog.Errorf("Error marshaling request body: %v", err) - return fmt.Errorf("Error creating request body: %v", err), nil - } + return func(registryURL, repoName, layer string) error { + glog.V(4).Infof("Pruning registry %q, repo %q, layer %q", registryURL, repoName, layer) //TODO https - req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/admin/layers", registryURL), bytes.NewReader(body)) + req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/admin/%s/layers/%s", registryURL, repoName, layer), nil) if err != nil { glog.Errorf("Error creating request: %v", err) - return fmt.Errorf("Error creating request: %v", err), nil + return fmt.Errorf("Error creating request: %v", err) } glog.V(4).Infof("Sending request to registry") resp, err := registryClient.Do(req) if err != nil { glog.Errorf("Error sending request: %v", err) - return fmt.Errorf("Error sending request: %v", err), nil + return fmt.Errorf("Error sending request: %v", err) } defer resp.Body.Close() - buf, err := ioutil.ReadAll(resp.Body) - if err != nil { - glog.Errorf("Error reading response body: %v", err) - return fmt.Errorf("Error reading response body: %v", err), nil - } - - if resp.StatusCode != http.StatusOK { + if resp.StatusCode != http.StatusNoContent { glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) - return fmt.Errorf("Unexpected status code %d in response %s", resp.StatusCode, buf), nil - } - - var deleteResponse server.DeleteLayersResponse - if err := json.Unmarshal(buf, &deleteResponse); err != nil { - glog.Errorf("Error unmarshaling response: %v", err) - return fmt.Errorf("Error unmarshaling response: %v", err), nil + //TODO decode error response + //decoder := json.NewDecoder(resp.Body) + return fmt.Errorf("Unexpected status code %d in response", resp.StatusCode) } - errs := map[string][]error{} - - for layer, layerErrors := range deleteResponse.Errors { - errs[layer] = []error{} - for _, err := range layerErrors { - errs[layer] = append(errs[layer], errors.New(err)) - } - } - - return nil, errs + return nil } } @@ -748,3 +654,65 @@ func imageStreamPredecessors(g graph.Graph, imageNode *graph.ImageNode) []*image return streams } + +func DeletingBlobPruneFunc(registryClient *http.Client) BlobPruneFunc { + return func(registryURL, blob string) error { + glog.V(4).Infof("Pruning registry %q, blob %q", registryURL, blob) + + //TODO https + req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/admin/blobs/%s", registryURL, blob), nil) + if err != nil { + glog.Errorf("Error creating request: %v", err) + return fmt.Errorf("Error creating request: %v", err) + } + + glog.V(4).Infof("Sending request to registry") + resp, err := registryClient.Do(req) + if err != nil { + glog.Errorf("Error sending request: %v", err) + return fmt.Errorf("Error sending request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) + //TODO decode error response + //decoder := json.NewDecoder(resp.Body) + return fmt.Errorf("Unexpected status code %d in response", resp.StatusCode) + } + + return nil + } +} + +func DeletingManifestPruneFunc(registryClient *http.Client) ManifestPruneFunc { + return func(registryURL, repoName, manifest string) error { + glog.V(4).Infof("Pruning manifest for registry %q, repo %q, manifest %q", registryURL, repoName, manifest) + + //TODO https + req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/admin/%s/manifests/%s", registryURL, repoName, manifest), nil) + if err != nil { + glog.Errorf("Error creating request: %v", err) + return fmt.Errorf("Error creating request: %v", err) + } + + glog.V(4).Infof("Sending request to registry") + resp, err := registryClient.Do(req) + if err != nil { + glog.Errorf("Error sending request: %v", err) + return fmt.Errorf("Error sending request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) + //TODO decode error response + decoder := json.NewDecoder(resp.Body) + response := make(map[string]interface{}) + decoder.Decode(&response) + return fmt.Errorf("Unexpected status code %d in response: %#v", resp.StatusCode, response) + } + + return nil + } +} diff --git a/pkg/image/prune/imagepruner_test.go b/pkg/image/prune/imagepruner_test.go index 606e1000d085..3cfdddc6df88 100644 --- a/pkg/image/prune/imagepruner_test.go +++ b/pkg/image/prune/imagepruner_test.go @@ -4,20 +4,15 @@ import ( "encoding/json" "flag" "fmt" - "net/http" - "net/http/httptest" "reflect" - "strings" "testing" "time" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" buildapi "github.com/openshift/origin/pkg/build/api" "github.com/openshift/origin/pkg/client" deployapi "github.com/openshift/origin/pkg/deploy/api" - "github.com/openshift/origin/pkg/dockerregistry/server" imageapi "github.com/openshift/origin/pkg/image/api" ) @@ -499,6 +494,22 @@ func TestImagePruning(t *testing.T) { expectedDeletions: []string{"id4"}, expectedUpdatedStreams: []string{"foo/bar"}, }, + "image stream - same manifest listed multiple times in tag history": { + images: imageList( + image("id1", registryURL+"/foo/bar@id1"), + image("id2", registryURL+"/foo/bar@id2"), + ), + streams: streamList( + stream(registryURL, "foo", "bar", tags( + tag("latest", + tagEvent("id1", registryURL+"/foo/bar@id1"), + tagEvent("id2", registryURL+"/foo/bar@id2"), + tagEvent("id1", registryURL+"/foo/bar@id1"), + tagEvent("id2", registryURL+"/foo/bar@id2"), + ), + )), + ), + }, "image stream age less than min pruning age - don't prune": { images: imageList( image("id", registryURL+"/foo/bar@id"), @@ -580,11 +591,11 @@ func TestImagePruning(t *testing.T) { if len(tcFilter) > 0 && name != tcFilter { continue } - p := newImagePruner(60*time.Minute, 3, &test.images, &test.streams, &test.pods, &test.rcs, &test.bcs, &test.builds, &test.dcs) + p := NewImagePruner(60*time.Minute, 3, &test.images, &test.streams, &test.pods, &test.rcs, &test.bcs, &test.builds, &test.dcs) actualDeletions := util.NewStringSet() actualUpdatedStreams := util.NewStringSet() - imagePruneFunc := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { + pruneImage := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { actualDeletions.Insert(image.Name) for _, stream := range streams { actualUpdatedStreams.Insert(fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)) @@ -592,11 +603,19 @@ func TestImagePruning(t *testing.T) { return []error{} } - layerPruneFunc := func(registryURL string, req server.DeleteLayersRequest) (error, map[string][]error) { - return nil, map[string][]error{} + pruneLayer := func(registryURL, repo, layer string) error { + return nil } - p.Run(imagePruneFunc, layerPruneFunc) + pruneBlob := func(registryURL, blob string) error { + return nil + } + + pruneManifest := func(registryURL, repo, manifest string) error { + return nil + } + + p.Run(pruneImage, pruneLayer, pruneBlob, pruneManifest) expectedDeletions := util.NewStringSet(test.expectedDeletions...) if !reflect.DeepEqual(expectedDeletions, actualDeletions) { @@ -730,6 +749,7 @@ func TestDeletingImagePruneFunc(t *testing.T) { } } +/* func TestLayerPruning(t *testing.T) { flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel)) registryURL := "registry1" @@ -823,42 +843,9 @@ func TestLayerPruning(t *testing.T) { } } } +*/ -func TestNewImagePruner(t *testing.T) { - osFake := &client.Fake{} - - kFake := &testclient.Fake{} - p, err := NewImagePruner(60, 3, osFake, osFake, kFake, kFake, osFake, osFake, osFake) - if err != nil { - t.Fatalf("unexpected error creating image pruner: %v", err) - } - if p == nil { - t.Fatalf("unexpected nil pruner") - } - - seen := util.NewStringSet() - for _, action := range osFake.Actions { - seen.Insert(action.Action) - } - for _, action := range kFake.Actions { - seen.Insert(action.Action) - } - - expected := util.NewStringSet( - "list-images", - "list-imagestreams", - "list-pods", - "list-replicationControllers", - "list-buildconfig", - "list-builds", - "list-deploymentconfig", - ) - - if e, a := expected, seen; !reflect.DeepEqual(e, a) { - t.Errorf("Expected actions=%v, got: %v", e.List(), a.List()) - } -} - +/* func TestDeletingLayerPruneFunc(t *testing.T) { tests := map[string]struct { simulateClientError bool @@ -942,3 +929,4 @@ func TestDeletingLayerPruneFunc(t *testing.T) { } } } +*/ diff --git a/pkg/image/prune/summary.go b/pkg/image/prune/summary.go index 1e80f492f898..2a7cf287bcef 100644 --- a/pkg/image/prune/summary.go +++ b/pkg/image/prune/summary.go @@ -1,5 +1,6 @@ package prune +/* import ( "fmt" "io" @@ -16,18 +17,18 @@ type summarizingPruner struct { imageFailures []string imageErrors []error - /* - { - registry1: { - layer1: { - requestError: nil, - layerErrors: [err1, err2], - }, - ..., - }, - registry2: ... - } - */ + + //{ + // registry1: { + // layer1: { + // requestError: nil, + // layerErrors: [err1, err2], + // }, + // ..., + // }, + // registry2: ... + //} + registryResults map[string]registryResult } @@ -102,3 +103,4 @@ func (p *summarizingPruner) layerPruneFunc(base LayerPruneFunc) LayerPruneFunc { return requestError, layerErrors } } +*/ From 74dbf1d70cad95b6bf7fecac0c016a9df146a6bc Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Wed, 20 May 2015 14:30:34 -0400 Subject: [PATCH 15/21] Update pruning tests --- pkg/api/graph/types.go | 2 +- pkg/image/prune/imagepruner_test.go | 197 +++++++++------------------- 2 files changed, 65 insertions(+), 134 deletions(-) diff --git a/pkg/api/graph/types.go b/pkg/api/graph/types.go index 2ac9d6662fb4..0812bbf14d2d 100644 --- a/pkg/api/graph/types.go +++ b/pkg/api/graph/types.go @@ -507,7 +507,7 @@ func (*ImageStreamNode) Kind() int { // ImageStream adds a graph node for the Image Stream if it does not already exist. func ImageStream(g MutableUniqueGraph, stream *image.ImageStream) graph.Node { return EnsureUnique(g, - UniqueName(fmt.Sprintf("%d|%s/%s", ImageStreamGraphKind, stream.Namespace, stream.Name)), + UniqueName(fmt.Sprintf("%d|%s", ImageStreamGraphKind, stream.Status.DockerImageRepository)), func(node Node) graph.Node { return &ImageStreamNode{node, stream} }, diff --git a/pkg/image/prune/imagepruner_test.go b/pkg/image/prune/imagepruner_test.go index 3cfdddc6df88..41c4b197c489 100644 --- a/pkg/image/prune/imagepruner_test.go +++ b/pkg/image/prune/imagepruner_test.go @@ -749,16 +749,15 @@ func TestDeletingImagePruneFunc(t *testing.T) { } } -/* -func TestLayerPruning(t *testing.T) { +func TestRegistryPruning(t *testing.T) { flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel)) - registryURL := "registry1" tests := map[string]struct { - images imageapi.ImageList - streams imageapi.ImageStreamList - expectedDeletions map[string]util.StringSet - expectedStreamUpdates map[string]util.StringSet + images imageapi.ImageList + streams imageapi.ImageStreamList + expectedLayerDeletions util.StringSet + expectedBlobDeletions util.StringSet + expectedManifestDeletions util.StringSet }{ "layers unique to id1 pruned": { images: imageList( @@ -766,167 +765,99 @@ func TestLayerPruning(t *testing.T) { imageWithLayers("id2", "registry1/foo/bar@id2", "layer3", "layer4", "layer5", "layer6"), ), streams: streamList( - stream(registryURL, "foo", "bar", tags( + stream("registry1", "foo", "bar", tags( tag("latest", - tagEvent("id2", registryURL+"/foo/bar@id2"), - tagEvent("id1", registryURL+"/foo/bar@id1"), + tagEvent("id2", "registry1/foo/bar@id2"), + tagEvent("id1", "registry1/foo/bar@id1"), + ), + )), + stream("registry1", "foo", "other", tags( + tag("latest", + tagEvent("id2", "registry1/foo/other@id2"), + ), + )), + stream("registry2", "foo", "bar", tags( + tag("latest", + tagEvent("id2", "registry2/foo/bar@id2"), + tagEvent("id1", "registry2/foo/bar@id1"), ), )), - stream(registryURL, "foo", "other", tags( + stream("registry2", "foo", "other", tags( tag("latest", - tagEvent("id2", registryURL+"/foo/other@id2"), + tagEvent("id2", "registry2/foo/other@id2"), ), )), ), - expectedDeletions: map[string]util.StringSet{ - "registry1": util.NewStringSet("layer1", "layer2"), - }, - expectedStreamUpdates: map[string]util.StringSet{ - "registry1": util.NewStringSet("foo/bar"), - }, + expectedLayerDeletions: util.NewStringSet( + "registry1|foo/bar|layer1", + "registry1|foo/bar|layer2", + "registry2|foo/bar|layer1", + "registry2|foo/bar|layer2", + ), + expectedBlobDeletions: util.NewStringSet( + "registry1|layer1", + "registry1|layer2", + "registry2|layer1", + "registry2|layer2", + ), + expectedManifestDeletions: util.NewStringSet( + "registry1|foo/bar|id1", + "registry2|foo/bar|id1", + ), }, "no pruning when no images are pruned": { images: imageList( imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"), ), streams: streamList( - stream(registryURL, "foo", "bar", tags( + stream("registry1", "foo", "bar", tags( tag("latest", - tagEvent("id1", registryURL+"/foo/bar@id1"), + tagEvent("id1", "registry1/foo/bar@id1"), ), )), ), - expectedDeletions: map[string]util.StringSet{}, - expectedStreamUpdates: map[string]util.StringSet{}, + expectedLayerDeletions: util.NewStringSet(), + expectedBlobDeletions: util.NewStringSet(), + expectedManifestDeletions: util.NewStringSet(), }, } for name, test := range tests { - actualDeletions := map[string]util.StringSet{} - actualUpdatedStreams := map[string]util.StringSet{} + actualLayerDeletions := util.NewStringSet() + actualBlobDeletions := util.NewStringSet() + actualManifestDeletions := util.NewStringSet() - imagePruneFunc := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { + pruneImage := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { return []error{} } - layerPruneFunc := func(registryURL string, req server.DeleteLayersRequest) (error, map[string][]error) { - registryDeletions, ok := actualDeletions[registryURL] - if !ok { - registryDeletions = util.NewStringSet() - } - streamUpdates, ok := actualUpdatedStreams[registryURL] - if !ok { - streamUpdates = util.NewStringSet() - } - - for layer, streams := range req { - registryDeletions.Insert(layer) - streamUpdates.Insert(streams...) - } - - actualDeletions[registryURL] = registryDeletions - actualUpdatedStreams[registryURL] = streamUpdates - - return nil, map[string][]error{} - } - - p := newImagePruner(60, 1, &test.images, &test.streams, &kapi.PodList{}, &kapi.ReplicationControllerList{}, &buildapi.BuildConfigList{}, &buildapi.BuildList{}, &deployapi.DeploymentConfigList{}) - - p.Run(imagePruneFunc, layerPruneFunc) - - if !reflect.DeepEqual(test.expectedDeletions, actualDeletions) { - t.Errorf("%s: expected layer deletions %#v, got %#v", name, test.expectedDeletions, actualDeletions) + pruneLayer := func(registryURL, repo, layer string) error { + actualLayerDeletions.Insert(fmt.Sprintf("%s|%s|%s", registryURL, repo, layer)) + return nil } - if !reflect.DeepEqual(test.expectedStreamUpdates, actualUpdatedStreams) { - t.Errorf("%s: expected stream updates %q, got %q", name, test.expectedStreamUpdates, actualUpdatedStreams) + pruneBlob := func(registryURL, blob string) error { + actualBlobDeletions.Insert(fmt.Sprintf("%s|%s", registryURL, blob)) + return nil } - } -} -*/ -/* -func TestDeletingLayerPruneFunc(t *testing.T) { - tests := map[string]struct { - simulateClientError bool - registryResponseStatusCode int - registryResponse string - expectedRequestError string - expectedErrors []string - }{ - "client error": { - simulateClientError: true, - expectedRequestError: "Error sending request:", - }, - "non-200 response": { - registryResponseStatusCode: http.StatusInternalServerError, - expectedRequestError: fmt.Sprintf("Unexpected status code %d in response", http.StatusInternalServerError), - registryResponse: "{}", - }, - "error unmarshaling response body": { - registryResponseStatusCode: http.StatusOK, - registryResponse: "foo", - expectedRequestError: "Error unmarshaling response:", - }, - "happy path - no response errors": { - registryResponseStatusCode: http.StatusOK, - registryResponse: `{"result":"success"}`, - expectedErrors: []string{}, - }, - "happy path - with response errors": { - registryResponseStatusCode: http.StatusOK, - registryResponse: `{"result":"failure","errors":{"layer1":["error1","error2","error3"]}}`, - expectedErrors: []string{"error1", "error2", "error3"}, - }, - } - - for name, test := range tests { - client := http.DefaultClient - - testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(test.registryResponseStatusCode) - w.Write([]byte(test.registryResponse)) - })) - registry := testServer.Listener.Addr().String() - - if !test.simulateClientError { - testServer.Start() - defer testServer.Close() - } else { - registry = "noregistryhere!" + pruneManifest := func(registryURL, repo, manifest string) error { + actualManifestDeletions.Insert(fmt.Sprintf("%s|%s|%s", registryURL, repo, manifest)) + return nil } - pruneFunc := DeletingLayerPruneFunc(client) + p := NewImagePruner(60, 1, &test.images, &test.streams, &kapi.PodList{}, &kapi.ReplicationControllerList{}, &buildapi.BuildConfigList{}, &buildapi.BuildList{}, &deployapi.DeploymentConfigList{}) - deletions := server.DeleteLayersRequest{ - "layer1": {"aaa/stream1", "bbb/stream2"}, - } - - requestError, layerErrors := pruneFunc(registry, deletions) + p.Run(pruneImage, pruneLayer, pruneBlob, pruneManifest) - gotError := requestError != nil - expectError := len(test.expectedRequestError) != 0 - if e, a := expectError, gotError; e != a { - t.Errorf("%s: requestError: expected %t, got %t: %v", name, e, a, requestError) - continue - } - if gotError { - if e, a := test.expectedRequestError, requestError; !strings.HasPrefix(a.Error(), e) { - t.Errorf("%s: expected request error %q, got %q", name, e, a) - } + if !reflect.DeepEqual(test.expectedLayerDeletions, actualLayerDeletions) { + t.Errorf("%s: expected layer deletions %#v, got %#v", name, test.expectedLayerDeletions, actualLayerDeletions) } - - errs := layerErrors["layer1"] - if e, a := len(test.expectedErrors), len(errs); e != a { - t.Errorf("%s: expected %d errors (%v), got %d (%v)", name, e, test.expectedErrors, a, errs) - continue + if !reflect.DeepEqual(test.expectedBlobDeletions, actualBlobDeletions) { + t.Errorf("%s: expected blob deletions %#v, got %#v", name, test.expectedBlobDeletions, actualBlobDeletions) } - for i, e := range test.expectedErrors { - a := errs[i].Error() - if !strings.HasPrefix(a, e) { - t.Errorf("%s: expected error starting with %q, got %q", name, e, a) - } + if !reflect.DeepEqual(test.expectedManifestDeletions, actualManifestDeletions) { + t.Errorf("%s: expected manifest deletions %#v, got %#v", name, test.expectedManifestDeletions, actualManifestDeletions) } } } -*/ From 907d8c67c0c412f10c8bd875bcd7f2754577110c Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Wed, 20 May 2015 14:41:10 -0400 Subject: [PATCH 16/21] Remove summary pruner --- pkg/image/prune/summary.go | 106 ------------------------------------- 1 file changed, 106 deletions(-) delete mode 100644 pkg/image/prune/summary.go diff --git a/pkg/image/prune/summary.go b/pkg/image/prune/summary.go deleted file mode 100644 index 2a7cf287bcef..000000000000 --- a/pkg/image/prune/summary.go +++ /dev/null @@ -1,106 +0,0 @@ -package prune - -/* -import ( - "fmt" - "io" - - "github.com/openshift/origin/pkg/dockerregistry/server" - imageapi "github.com/openshift/origin/pkg/image/api" -) - -type summarizingPruner struct { - delegate ImagePruner - out io.Writer - - imageSuccesses []string - imageFailures []string - imageErrors []error - - - //{ - // registry1: { - // layer1: { - // requestError: nil, - // layerErrors: [err1, err2], - // }, - // ..., - // }, - // registry2: ... - //} - - registryResults map[string]registryResult -} - -type registryResult struct { - requestError error - layerErrors map[string][]error -} - -var _ ImagePruner = &summarizingPruner{} - -func NewSummarizingImagePruner(pruner ImagePruner, out io.Writer) ImagePruner { - return &summarizingPruner{ - delegate: pruner, - out: out, - registryResults: map[string]registryResult{}, - } -} - -func (p *summarizingPruner) Run(baseImagePruneFunc ImagePruneFunc, baseLayerPruneFunc LayerPruneFunc) { - p.delegate.Run(p.imagePruneFunc(baseImagePruneFunc), p.layerPruneFunc(baseLayerPruneFunc)) - p.summarize() -} - -func (p *summarizingPruner) summarize() { - fmt.Fprintln(p.out, "\nIMAGE PRUNING SUMMARY:") - fmt.Fprintf(p.out, "# Image prune successes: %d\n", len(p.imageSuccesses)) - fmt.Fprintf(p.out, "# Image prune errors: %d\n", len(p.imageFailures)) - - fmt.Fprintln(p.out, "\nLAYER PRUNING SUMMARY:") - for registry, result := range p.registryResults { - p.summarizeRegistry(registry, result) - } -} - -func (p *summarizingPruner) summarizeRegistry(registry string, result registryResult) { - fmt.Fprintf(p.out, "\tRegistry: %s\n", registry) - fmt.Fprintf(p.out, "\t\tRequest sent successfully: %t\n", result.requestError == nil) - successes, failures := 0, 0 - for _, errs := range result.layerErrors { - switch len(errs) { - case 0: - successes++ - default: - failures++ - } - } - fmt.Fprintf(p.out, "\t\t# Layer prune successes: %d\n", successes) - fmt.Fprintf(p.out, "\t\t# Layer prune errors: %d\n", failures) -} - -func (p *summarizingPruner) imagePruneFunc(base ImagePruneFunc) ImagePruneFunc { - return func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { - errs := base(image, streams) - switch len(errs) { - case 0: - p.imageSuccesses = append(p.imageSuccesses, image.Name) - default: - p.imageFailures = append(p.imageFailures, image.Name) - p.imageErrors = append(p.imageErrors, errs...) - } - return errs - } -} - -func (p *summarizingPruner) layerPruneFunc(base LayerPruneFunc) LayerPruneFunc { - return func(registryURL string, req server.DeleteLayersRequest) (error, map[string][]error) { - requestError, layerErrors := base(registryURL, req) - p.registryResults[registryURL] = registryResult{ - requestError: requestError, - layerErrors: layerErrors, - } - return requestError, layerErrors - } -} -*/ From 7d7c0665c0ad4733955bb8168a35e4e731ae6881 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Wed, 20 May 2015 15:02:06 -0400 Subject: [PATCH 17/21] Retry stream updates when pruning --- pkg/image/prune/imagepruner.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index a187cee2df1b..092808fdde3d 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -7,6 +7,7 @@ import ( "time" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" gonum "github.com/gonum/graph" @@ -560,6 +561,8 @@ func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, pruneLayer L } } +const retryCount = 2 + // DeletingImagePruneFunc returns an ImagePruneFunc that deletes the image and // removes it from each referencing ImageStream's status.tags. func DeletingImagePruneFunc(images client.ImageInterface, streams client.ImageStreamsNamespacer) ImagePruneFunc { @@ -574,7 +577,8 @@ func DeletingImagePruneFunc(images client.ImageInterface, streams client.ImageSt return result } - for _, stream := range referencedStreams { + var updateStream func(*imageapi.Image, *imageapi.ImageStream, int) error + updateStream = func(image *imageapi.Image, stream *imageapi.ImageStream, retry int) error { glog.V(4).Infof("Checking if stream %s/%s has references to image in status.tags", stream.Namespace, stream.Name) for tag, history := range stream.Status.Tags { glog.V(4).Infof("Checking tag %q", tag) @@ -591,7 +595,21 @@ func DeletingImagePruneFunc(images client.ImageInterface, streams client.ImageSt glog.V(4).Infof("Updating image stream %s/%s", stream.Namespace, stream.Name) glog.V(5).Infof("Updated stream: %#v", stream) + //TODO use the updated ImageStream that's returned and update the entry in the graph + //to hopefully avoid having to re-get the stream? if _, err := streams.ImageStreams(stream.Namespace).UpdateStatus(stream); err != nil { + if errors.IsConflict(err) && retry > 0 { + if stream, err := streams.ImageStreams(stream.Namespace).Get(stream.Name); err == nil { + return updateStream(image, stream, retry-1) + } + } + return err + } + return nil + } + + for _, stream := range referencedStreams { + if err := updateStream(image, stream, retryCount); err != nil { e := fmt.Errorf("Unable to update image stream status %s/%s: %v", stream.Namespace, stream.Name, err) glog.Error(e) result = append(result, e) From 2cbf0c90f40dc1e2fe078154678a8281b0157379 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Wed, 20 May 2015 20:34:01 -0400 Subject: [PATCH 18/21] add prune images command --- pkg/cmd/admin/prune/images.go | 155 ++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 pkg/cmd/admin/prune/images.go diff --git a/pkg/cmd/admin/prune/images.go b/pkg/cmd/admin/prune/images.go new file mode 100644 index 000000000000..65e1589a05a0 --- /dev/null +++ b/pkg/cmd/admin/prune/images.go @@ -0,0 +1,155 @@ +package prune + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" + "text/tabwriter" + "time" + + kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" + cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" + "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" + "github.com/openshift/origin/pkg/cmd/util/clientcmd" + imageapi "github.com/openshift/origin/pkg/image/api" + "github.com/openshift/origin/pkg/image/prune" + "github.com/spf13/cobra" +) + +const imagesLongDesc = ` +` + +const PruneImagesRecommendedName = "images" + +type pruneImagesConfig struct { + DryRun bool + KeepYoungerThan time.Duration + TagRevisionsToKeep int +} + +func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command { + cfg := &pruneImagesConfig{ + DryRun: true, + KeepYoungerThan: 60 * time.Minute, + TagRevisionsToKeep: 3, + } + + cmd := &cobra.Command{ + Use: name, + Short: "Prune images", + Long: fmt.Sprintf(imagesLongDesc, parentName, name), + + Run: func(cmd *cobra.Command, args []string) { + if len(args) > 0 { + glog.Fatalf("No arguments are allowed to this command") + } + + osClient, kClient, err := f.Clients() + cmdutil.CheckErr(err) + + allImages, err := osClient.Images().List(labels.Everything(), fields.Everything()) + cmdutil.CheckErr(err) + + allStreams, err := osClient.ImageStreams(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) + cmdutil.CheckErr(err) + + allPods, err := kClient.Pods(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) + cmdutil.CheckErr(err) + + allRCs, err := kClient.ReplicationControllers(kapi.NamespaceAll).List(labels.Everything()) + cmdutil.CheckErr(err) + + allBCs, err := osClient.BuildConfigs(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) + cmdutil.CheckErr(err) + + allBuilds, err := osClient.Builds(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) + cmdutil.CheckErr(err) + + allDCs, err := osClient.DeploymentConfigs(kapi.NamespaceAll).List(labels.Everything(), fields.Everything()) + cmdutil.CheckErr(err) + + pruner := prune.NewImagePruner( + cfg.KeepYoungerThan, + cfg.TagRevisionsToKeep, + allImages, + allStreams, + allPods, + allRCs, + allBCs, + allBuilds, + allDCs, + ) + + w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0) + defer w.Flush() + + printImageHeader := true + describingImagePruneFunc := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { + if printImageHeader { + printImageHeader = false + fmt.Fprintf(w, "IMAGE\tSTREAMS\n") + } + streamNames := util.NewStringSet() + for _, stream := range streams { + streamNames.Insert(fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)) + } + fmt.Fprintf(w, "%s\t%s\n", image.Name, strings.Join(streamNames.List(), ", ")) + return nil + } + + printLayerHeader := true + describingLayerPruneFunc := func(registryURL, repo, layer string) error { + if printLayerHeader { + printLayerHeader = false + fmt.Fprintf(w, "\nREGISTRY\tSTREAM\tLAYER\n") + } + fmt.Fprintf(w, "%s\t%s\t%s\n", registryURL, repo, layer) + return nil + } + + var ( + imagePruneFunc prune.ImagePruneFunc + layerPruneFunc prune.LayerPruneFunc + blobPruneFunc prune.BlobPruneFunc + manifestPruneFunc prune.ManifestPruneFunc + ) + + switch cfg.DryRun { + case false: + imagePruneFunc = func(image *imageapi.Image, referencedStreams []*imageapi.ImageStream) []error { + describingImagePruneFunc(image, referencedStreams) + return prune.DeletingImagePruneFunc(osClient.Images(), osClient)(image, referencedStreams) + } + layerPruneFunc = func(registryURL, repo, layer string) error { + describingLayerPruneFunc(registryURL, repo, layer) + return prune.DeletingLayerPruneFunc(http.DefaultClient)(registryURL, repo, layer) + } + blobPruneFunc = prune.DeletingBlobPruneFunc(http.DefaultClient) + manifestPruneFunc = prune.DeletingManifestPruneFunc(http.DefaultClient) + default: + fmt.Fprintln(os.Stderr, "Dry run enabled - no modifications will be made.") + imagePruneFunc = describingImagePruneFunc + layerPruneFunc = describingLayerPruneFunc + blobPruneFunc = func(registryURL, blob string) error { + return nil + } + manifestPruneFunc = func(registryURL, repo, manifest string) error { + return nil + } + } + + pruner.Run(imagePruneFunc, layerPruneFunc, blobPruneFunc, manifestPruneFunc) + }, + } + + cmd.Flags().BoolVar(&cfg.DryRun, "dry-run", cfg.DryRun, "Perform a build pruning dry-run, displaying what would be deleted but not actually deleting anything.") + cmd.Flags().DurationVar(&cfg.KeepYoungerThan, "keep-younger-than", cfg.KeepYoungerThan, "Specify the minimum age of a build for it to be considered a candidate for pruning.") + cmd.Flags().IntVar(&cfg.TagRevisionsToKeep, "keep-tag-revisions", cfg.TagRevisionsToKeep, "Specify the number of image revisions for a tag in an image stream that will be preserved.") + + return cmd +} From 7bbd24dd839b582d2fe8f4724e87300c60a24f13 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Thu, 21 May 2015 10:52:16 -0400 Subject: [PATCH 19/21] Add func for pruning an image from a stream --- pkg/api/graph/graph.go | 2 +- pkg/api/graph/types.go | 10 +- pkg/client/fake_images.go | 1 - pkg/cmd/admin/prune/images.go | 49 +++++--- pkg/image/prune/imagepruner.go | 173 ++++++++++++---------------- pkg/image/prune/imagepruner_test.go | 131 +++++---------------- 6 files changed, 144 insertions(+), 222 deletions(-) diff --git a/pkg/api/graph/graph.go b/pkg/api/graph/graph.go index 8d31fbdd1386..b6e13aad4022 100644 --- a/pkg/api/graph/graph.go +++ b/pkg/api/graph/graph.go @@ -9,7 +9,7 @@ import ( ) type Node struct { - graph.Node + concrete.Node UniqueName } diff --git a/pkg/api/graph/types.go b/pkg/api/graph/types.go index 0812bbf14d2d..c142687349a7 100644 --- a/pkg/api/graph/types.go +++ b/pkg/api/graph/types.go @@ -504,16 +504,24 @@ func (*ImageStreamNode) Kind() int { return ImageStreamGraphKind } +func imageStreamName(stream *image.ImageStream) UniqueName { + return UniqueName(fmt.Sprintf("%d|%s", ImageStreamGraphKind, stream.Status.DockerImageRepository)) +} + // ImageStream adds a graph node for the Image Stream if it does not already exist. func ImageStream(g MutableUniqueGraph, stream *image.ImageStream) graph.Node { return EnsureUnique(g, - UniqueName(fmt.Sprintf("%d|%s", ImageStreamGraphKind, stream.Status.DockerImageRepository)), + imageStreamName(stream), func(node Node) graph.Node { return &ImageStreamNode{node, stream} }, ) } +func FindImageStream(g MutableUniqueGraph, stream *image.ImageStream) graph.Node { + return g.Find(imageStreamName(stream)) +} + type ReplicationControllerNode struct { Node *kapi.ReplicationController diff --git a/pkg/client/fake_images.go b/pkg/client/fake_images.go index 978c3616f4c1..23a9272c8f4e 100644 --- a/pkg/client/fake_images.go +++ b/pkg/client/fake_images.go @@ -32,7 +32,6 @@ func (c *FakeImages) Create(image *imageapi.Image) (*imageapi.Image, error) { } func (c *FakeImages) Delete(name string) error { - c.Fake.Actions = append(c.Fake.Actions, FakeAction{Action: "delete-image", Value: name}) _, err := c.Fake.Invokes(FakeAction{Action: "delete-image", Value: name}, nil) return err } diff --git a/pkg/cmd/admin/prune/images.go b/pkg/cmd/admin/prune/images.go index 65e1589a05a0..c915153da938 100644 --- a/pkg/cmd/admin/prune/images.go +++ b/pkg/cmd/admin/prune/images.go @@ -88,42 +88,60 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0) defer w.Flush() + var streams util.StringSet printImageHeader := true - describingImagePruneFunc := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { + describingImagePruneFunc := func(image *imageapi.Image) error { if printImageHeader { printImageHeader = false - fmt.Fprintf(w, "IMAGE\tSTREAMS\n") + fmt.Fprintf(w, "IMAGE\tSTREAMS") } - streamNames := util.NewStringSet() - for _, stream := range streams { - streamNames.Insert(fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)) + + if streams.Len() > 0 { + fmt.Fprintf(w, strings.Join(streams.List(), ", ")) } - fmt.Fprintf(w, "%s\t%s\n", image.Name, strings.Join(streamNames.List(), ", ")) + + fmt.Fprintf(w, "\n%s\t", image.Name) + streams = util.NewStringSet() + return nil } + describingImageStreamPruneFunc := func(stream *imageapi.ImageStream, image *imageapi.Image) (*imageapi.ImageStream, error) { + streams.Insert(stream.Status.DockerImageRepository) + return stream, nil + } + printLayerHeader := true describingLayerPruneFunc := func(registryURL, repo, layer string) error { if printLayerHeader { printLayerHeader = false - fmt.Fprintf(w, "\nREGISTRY\tSTREAM\tLAYER\n") + // need to print the remaining streams for the last image + if streams.Len() > 0 { + fmt.Fprintf(w, strings.Join(streams.List(), ", ")) + } + fmt.Fprintf(w, "\n\nREGISTRY\tSTREAM\tLAYER\n") } fmt.Fprintf(w, "%s\t%s\t%s\n", registryURL, repo, layer) return nil } var ( - imagePruneFunc prune.ImagePruneFunc - layerPruneFunc prune.LayerPruneFunc - blobPruneFunc prune.BlobPruneFunc - manifestPruneFunc prune.ManifestPruneFunc + imagePruneFunc prune.ImagePruneFunc + imageStreamPruneFunc prune.ImageStreamPruneFunc + layerPruneFunc prune.LayerPruneFunc + blobPruneFunc prune.BlobPruneFunc + manifestPruneFunc prune.ManifestPruneFunc ) switch cfg.DryRun { case false: - imagePruneFunc = func(image *imageapi.Image, referencedStreams []*imageapi.ImageStream) []error { - describingImagePruneFunc(image, referencedStreams) - return prune.DeletingImagePruneFunc(osClient.Images(), osClient)(image, referencedStreams) + imagePruneFunc = func(image *imageapi.Image) error { + describingImagePruneFunc(image) + return prune.DeletingImagePruneFunc(osClient.Images())(image) + } + imageStreamPruneFunc = func(stream *imageapi.ImageStream, image *imageapi.Image) (*imageapi.ImageStream, error) { + describingImageStreamPruneFunc(stream, image) + return prune.DeletingImageStreamPruneFunc(osClient)(stream, image) } layerPruneFunc = func(registryURL, repo, layer string) error { describingLayerPruneFunc(registryURL, repo, layer) @@ -134,6 +152,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri default: fmt.Fprintln(os.Stderr, "Dry run enabled - no modifications will be made.") imagePruneFunc = describingImagePruneFunc + imageStreamPruneFunc = describingImageStreamPruneFunc layerPruneFunc = describingLayerPruneFunc blobPruneFunc = func(registryURL, blob string) error { return nil @@ -143,7 +162,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri } } - pruner.Run(imagePruneFunc, layerPruneFunc, blobPruneFunc, manifestPruneFunc) + pruner.Run(imagePruneFunc, imageStreamPruneFunc, layerPruneFunc, blobPruneFunc, manifestPruneFunc) }, } diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index 092808fdde3d..8b521e70896f 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -7,7 +7,6 @@ import ( "time" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" gonum "github.com/gonum/graph" @@ -28,13 +27,9 @@ type pruneAlgorithm struct { } // ImagePruneFunc is a function that is invoked for each image that is -// prunable, along with the list of image streams that reference it. -type ImagePruneFunc func(image *imageapi.Image, streams []*imageapi.ImageStream) []error - -// LayerPruneFunc is a function that is invoked for each registry, along with -// a DeleteLayersRequest that contains the layers that can be pruned and the -// image stream names that reference each layer. -//type LayerPruneFunc func(registryURL string, req server.DeleteLayersRequest) (requestError error, layerErrors map[string][]error) +// prunable. +type ImagePruneFunc func(image *imageapi.Image) error +type ImageStreamPruneFunc func(stream *imageapi.ImageStream, image *imageapi.Image) (*imageapi.ImageStream, error) type LayerPruneFunc func(registryURL, repo, layer string) error type BlobPruneFunc func(registryURL, blob string) error type ManifestPruneFunc func(registryURL, repo, manifest string) error @@ -42,7 +37,7 @@ type ManifestPruneFunc func(registryURL, repo, manifest string) error // ImagePruner knows how to prune images and layers. type ImagePruner interface { // Run prunes images and layers. - Run(pruneImage ImagePruneFunc, pruneLayer LayerPruneFunc, pruneBlob BlobPruneFunc, pruneManifest ManifestPruneFunc) + Run(pruneImage ImagePruneFunc, pruneStream ImageStreamPruneFunc, pruneLayer LayerPruneFunc, pruneBlob BlobPruneFunc, pruneManifest ManifestPruneFunc) } // imagePruner implements ImagePruner. @@ -428,7 +423,7 @@ func imageIsPrunable(g graph.Graph, imageNode *graph.ImageNode) bool { // with the image streams that reference the image. After imagePruneFunc is // invoked, the image node is removed from the graph, so that layers eligible // for pruning may later be identified. -func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, pruneImage ImagePruneFunc, pruneManifest ManifestPruneFunc) { +func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, pruneImage ImagePruneFunc, pruneStream ImageStreamPruneFunc, pruneManifest ManifestPruneFunc) { for _, imageNode := range imageNodes { glog.V(4).Infof("Examining image %q", imageNode.Image.Name) @@ -439,22 +434,34 @@ func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, pruneImage ImageP glog.V(4).Infof("Image has only weak references - pruning") - streams := imageStreamPredecessors(g, imageNode) - if errs := pruneImage(imageNode.Image, streams); len(errs) > 0 { - glog.Errorf("Error pruning image %q: %v", imageNode.Image.Name, errs) + if err := pruneImage(imageNode.Image); err != nil { + glog.Errorf("Error pruning image %q: %v", imageNode.Image.Name, err) } - for _, stream := range streams { - ref, err := imageapi.DockerImageReferenceForStream(stream) - repoName := fmt.Sprintf("%s/%s", ref.Namespace, ref.Name) - if err != nil { - glog.Errorf("Error constructing DockerImageReference for %q: %v", repoName, err) - continue - } + for _, n := range g.Predecessors(imageNode) { + if streamNode, ok := n.(*graph.ImageStreamNode); ok { + stream := streamNode.ImageStream + repoName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name) + + glog.V(4).Infof("Pruning image from stream %s", repoName) + updatedStream, err := pruneStream(stream, imageNode.Image) + if err != nil { + glog.Errorf("Error pruning image from stream: %v", err) + continue + } + + streamNode.ImageStream = updatedStream + + ref, err := imageapi.DockerImageReferenceForStream(stream) + if err != nil { + glog.Errorf("Error constructing DockerImageReference for %q: %v", repoName, err) + continue + } - glog.V(4).Infof("Invoking pruneManifest for registry %q, repo %q, image %q", ref.Registry, repoName, imageNode.Image.Name) - if err := pruneManifest(ref.Registry, repoName, imageNode.Image.Name); err != nil { - glog.Errorf("Error pruning manifest for registry %q, repo %q, image %q: %v", ref.Registry, repoName, imageNode.Image.Name, err) + glog.V(4).Infof("Invoking pruneManifest for registry %q, repo %q, image %q", ref.Registry, repoName, imageNode.Image.Name) + if err := pruneManifest(ref.Registry, repoName, imageNode.Image.Name); err != nil { + glog.Errorf("Error pruning manifest for registry %q, repo %q, image %q: %v", ref.Registry, repoName, imageNode.Image.Name, err) + } } } @@ -466,11 +473,11 @@ func pruneImages(g graph.Graph, imageNodes []*graph.ImageNode, pruneImage ImageP // Run identifies images eligible for pruning, invoking imagePruneFunc for each // image, and then it identifies layers eligible for pruning, invoking // layerPruneFunc for each registry URL that has layers that can be pruned. -func (p *imagePruner) Run(pruneImage ImagePruneFunc, pruneLayer LayerPruneFunc, pruneBlob BlobPruneFunc, pruneManifest ManifestPruneFunc) { +func (p *imagePruner) Run(pruneImage ImagePruneFunc, pruneStream ImageStreamPruneFunc, pruneLayer LayerPruneFunc, pruneBlob BlobPruneFunc, pruneManifest ManifestPruneFunc) { allNodes := p.g.NodeList() imageNodes := imageNodeSubgraph(allNodes) - pruneImages(p.g, imageNodes, pruneImage, pruneManifest) + pruneImages(p.g, imageNodes, pruneImage, pruneStream, pruneManifest) layerNodes := layerNodeSubgraph(allNodes) pruneLayers(p.g, layerNodes, pruneLayer, pruneBlob) @@ -561,74 +568,50 @@ func pruneLayers(g graph.Graph, layerNodes []*graph.ImageLayerNode, pruneLayer L } } -const retryCount = 2 - -// DeletingImagePruneFunc returns an ImagePruneFunc that deletes the image and -// removes it from each referencing ImageStream's status.tags. -func DeletingImagePruneFunc(images client.ImageInterface, streams client.ImageStreamsNamespacer) ImagePruneFunc { - return func(image *imageapi.Image, referencedStreams []*imageapi.ImageStream) []error { - result := []error{} - +// DeletingImagePruneFunc returns an ImagePruneFunc that deletes the image. +func DeletingImagePruneFunc(images client.ImageInterface) ImagePruneFunc { + return func(image *imageapi.Image) error { glog.V(4).Infof("Deleting image %q", image.Name) if err := images.Delete(image.Name); err != nil { e := fmt.Errorf("Error deleting image: %v", err) glog.Error(e) - result = append(result, e) - return result - } - - var updateStream func(*imageapi.Image, *imageapi.ImageStream, int) error - updateStream = func(image *imageapi.Image, stream *imageapi.ImageStream, retry int) error { - glog.V(4).Infof("Checking if stream %s/%s has references to image in status.tags", stream.Namespace, stream.Name) - for tag, history := range stream.Status.Tags { - glog.V(4).Infof("Checking tag %q", tag) - newHistory := imageapi.TagEventList{} - for i, tagEvent := range history.Items { - glog.V(4).Infof("Checking tag event %d with image %q", i, tagEvent.Image) - if tagEvent.Image != image.Name { - glog.V(4).Infof("Tag event doesn't match deleting image - keeping") - newHistory.Items = append(newHistory.Items, tagEvent) - } - } - stream.Status.Tags[tag] = newHistory - } + return e + } + return nil + } +} - glog.V(4).Infof("Updating image stream %s/%s", stream.Namespace, stream.Name) - glog.V(5).Infof("Updated stream: %#v", stream) - //TODO use the updated ImageStream that's returned and update the entry in the graph - //to hopefully avoid having to re-get the stream? - if _, err := streams.ImageStreams(stream.Namespace).UpdateStatus(stream); err != nil { - if errors.IsConflict(err) && retry > 0 { - if stream, err := streams.ImageStreams(stream.Namespace).Get(stream.Name); err == nil { - return updateStream(image, stream, retry-1) - } +func DeletingImageStreamPruneFunc(streams client.ImageStreamsNamespacer) ImageStreamPruneFunc { + return func(stream *imageapi.ImageStream, image *imageapi.Image) (*imageapi.ImageStream, error) { + glog.V(4).Infof("Checking if stream %s/%s has references to image in status.tags", stream.Namespace, stream.Name) + for tag, history := range stream.Status.Tags { + glog.V(4).Infof("Checking tag %q", tag) + newHistory := imageapi.TagEventList{} + for i, tagEvent := range history.Items { + glog.V(4).Infof("Checking tag event %d with image %q", i, tagEvent.Image) + if tagEvent.Image != image.Name { + glog.V(4).Infof("Tag event doesn't match deleting image - keeping") + newHistory.Items = append(newHistory.Items, tagEvent) } - return err } - return nil + stream.Status.Tags[tag] = newHistory } - for _, stream := range referencedStreams { - if err := updateStream(image, stream, retryCount); err != nil { - e := fmt.Errorf("Unable to update image stream status %s/%s: %v", stream.Namespace, stream.Name, err) - glog.Error(e) - result = append(result, e) - } + glog.V(4).Infof("Updating image stream %s/%s", stream.Namespace, stream.Name) + glog.V(5).Infof("Updated stream: %#v", stream) + updatedStream, err := streams.ImageStreams(stream.Namespace).UpdateStatus(stream) + if err != nil { + return nil, err } - - return result + return updatedStream, nil } } -// DeletingLayerPruneFunc returns a LayerPruneFunc that sends the -// DeleteLayersRequest to the Docker registry. +// DeletingLayerPruneFunc returns a LayerPruneFunc that uses registryClient to +// send a layer deletion request to the regsitry. // -// The request URL is http://registryURL/admin/layers and it is a DELETE -// request. -// -// The body of the request is JSON, and it is a map[string][]string, with each -// key being a layer, and each value being a list of Docker image repository -// names referenced by the layer. +// The request URL is http://registryURL/admin//layers/ and it is +// a DELETE request. func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { return func(registryURL, repoName, layer string) error { glog.V(4).Infof("Pruning registry %q, repo %q, layer %q", registryURL, repoName, layer) @@ -650,29 +633,17 @@ func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { if resp.StatusCode != http.StatusNoContent { glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) - //TODO decode error response - //decoder := json.NewDecoder(resp.Body) - return fmt.Errorf("Unexpected status code %d in response", resp.StatusCode) + //TODO do a better job of decoding and reporting the errors? + decoder := json.NewDecoder(resp.Body) + response := make(map[string]interface{}) + decoder.Decode(&response) + return fmt.Errorf("Unexpected status code %d in response: %#v", resp.StatusCode, response) } return nil } } -// imageStreamPredecessors returns a list of ImageStreams that are predecessors -// of imageNode. -func imageStreamPredecessors(g graph.Graph, imageNode *graph.ImageNode) []*imageapi.ImageStream { - streams := []*imageapi.ImageStream{} - - for _, n := range g.Predecessors(imageNode) { - if streamNode, ok := n.(*graph.ImageStreamNode); ok { - streams = append(streams, streamNode.ImageStream) - } - } - - return streams -} - func DeletingBlobPruneFunc(registryClient *http.Client) BlobPruneFunc { return func(registryURL, blob string) error { glog.V(4).Infof("Pruning registry %q, blob %q", registryURL, blob) @@ -694,9 +665,11 @@ func DeletingBlobPruneFunc(registryClient *http.Client) BlobPruneFunc { if resp.StatusCode != http.StatusNoContent { glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) - //TODO decode error response - //decoder := json.NewDecoder(resp.Body) - return fmt.Errorf("Unexpected status code %d in response", resp.StatusCode) + //TODO do a better job of decoding and reporting the errors? + decoder := json.NewDecoder(resp.Body) + response := make(map[string]interface{}) + decoder.Decode(&response) + return fmt.Errorf("Unexpected status code %d in response: %#v", resp.StatusCode, response) } return nil @@ -724,7 +697,7 @@ func DeletingManifestPruneFunc(registryClient *http.Client) ManifestPruneFunc { if resp.StatusCode != http.StatusNoContent { glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) - //TODO decode error response + //TODO do a better job of decoding and reporting the errors? decoder := json.NewDecoder(resp.Body) response := make(map[string]interface{}) decoder.Decode(&response) diff --git a/pkg/image/prune/imagepruner_test.go b/pkg/image/prune/imagepruner_test.go index 41c4b197c489..269411721f12 100644 --- a/pkg/image/prune/imagepruner_test.go +++ b/pkg/image/prune/imagepruner_test.go @@ -595,12 +595,14 @@ func TestImagePruning(t *testing.T) { actualDeletions := util.NewStringSet() actualUpdatedStreams := util.NewStringSet() - pruneImage := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { + pruneImage := func(image *imageapi.Image) error { actualDeletions.Insert(image.Name) - for _, stream := range streams { - actualUpdatedStreams.Insert(fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)) - } - return []error{} + return nil + } + + pruneStream := func(stream *imageapi.ImageStream, image *imageapi.Image) (*imageapi.ImageStream, error) { + actualUpdatedStreams.Insert(fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)) + return stream, nil } pruneLayer := func(registryURL, repo, layer string) error { @@ -615,7 +617,7 @@ func TestImagePruning(t *testing.T) { return nil } - p.Run(pruneImage, pruneLayer, pruneBlob, pruneManifest) + p.Run(pruneImage, pruneStream, pruneLayer, pruneBlob, pruneManifest) expectedDeletions := util.NewStringSet(test.expectedDeletions...) if !reflect.DeepEqual(expectedDeletions, actualDeletions) { @@ -630,121 +632,37 @@ func TestImagePruning(t *testing.T) { } func TestDeletingImagePruneFunc(t *testing.T) { - registryURL := "registry" + flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel)) tests := map[string]struct { - referencedStreams []*imageapi.ImageStream - expectedUpdates []*imageapi.ImageStream imageDeletionError error - streamUpdateError error }{ - "no referenced streams": { - referencedStreams: []*imageapi.ImageStream{}, - expectedUpdates: []*imageapi.ImageStream{}, - }, - "1 tag, 1 image revision": { - referencedStreams: []*imageapi.ImageStream{ - streamPtr(registryURL, "foo", "bar", tags( - tag("latest", - tagEvent("id", "registry/foo/bar@id"), - ), - )), - }, - expectedUpdates: []*imageapi.ImageStream{}, - }, - "1 tag, multiple image revisions": { - referencedStreams: []*imageapi.ImageStream{ - streamPtr(registryURL, "foo", "bar", tags( - tag("latest", - tagEvent("id", "registry/foo/bar@id"), - tagEvent("id2", "registry/foo/bar@id2"), - ), - )), - }, - expectedUpdates: []*imageapi.ImageStream{ - streamPtr(registryURL, "foo", "bar", tags( - tag("latest", - tagEvent("id", "registry/foo/bar@id"), - ), - )), - }, - }, - "image deletion error": { - referencedStreams: []*imageapi.ImageStream{ - streamPtr(registryURL, "foo", "bar", tags( - tag("latest", - tagEvent("id", "registry/foo/bar@id"), - ), - )), - }, + "no error": {}, + "delete error": { imageDeletionError: fmt.Errorf("foo"), }, - "stream update error": { - referencedStreams: []*imageapi.ImageStream{ - streamPtr(registryURL, "foo", "bar", tags( - tag("latest", - tagEvent("id", "registry/foo/bar@id"), - ), - )), - streamPtr(registryURL, "bar", "baz", tags( - tag("latest", - tagEvent("id", "registry/foo/bar@id"), - ), - )), - }, - streamUpdateError: fmt.Errorf("foo"), - }, } for name, test := range tests { imageClient := client.Fake{ Err: test.imageDeletionError, } - streamClient := client.Fake{ - Err: test.streamUpdateError, - } - pruneFunc := DeletingImagePruneFunc(imageClient.Images(), &streamClient) - errs := pruneFunc(&imageapi.Image{ObjectMeta: kapi.ObjectMeta{Name: "id2"}}, test.referencedStreams) + pruneFunc := DeletingImagePruneFunc(imageClient.Images()) + err := pruneFunc(&imageapi.Image{ObjectMeta: kapi.ObjectMeta{Name: "id2"}}) if test.imageDeletionError != nil { - if e, a := 1, len(errs); e != a { - t.Errorf("%s: # of errors: expected %v, got %v", name, e, a) - continue - } - if e, a := fmt.Sprintf("Error deleting image: %v", test.imageDeletionError), errs[0].Error(); e != a { - t.Errorf("%s: errs: expected %v, got %v", name, e, a) + if e, a := fmt.Sprintf("Error deleting image: %v", test.imageDeletionError), err.Error(); e != a { + t.Errorf("%s: err: expected %v, got %v", name, e, a) } continue } - if test.streamUpdateError != nil { - if e, a := len(test.referencedStreams), len(errs); e != a { - t.Errorf("%s: # of errors: expected %v, got %v", name, e, a) - continue - } - for i, stream := range test.referencedStreams { - if e, a := fmt.Sprintf("Unable to update image stream status %s/%s: %v", stream.Namespace, stream.Name, test.streamUpdateError), errs[i].Error(); e != a { - t.Errorf("%s: errs: expected %v, got %v", name, e, a) - } - } + if e, a := 1, len(imageClient.Actions); e != a { + t.Errorf("%s: expected %d actions, got %d: %#v", name, e, a, imageClient.Actions) continue } - if len(imageClient.Actions) < 1 { - t.Fatalf("%s: expected image deletion", name) - } - - if e, a := len(test.referencedStreams), len(streamClient.Actions); e != a { - t.Errorf("%s: expected %d stream updates, got %d", name, e, a) - } - - for i := range test.expectedUpdates { - if e, a := "update-status-imagestream", streamClient.Actions[i].Action; e != a { - t.Errorf("%s: unexpected action %q", name, a) - } - updatedStream := streamClient.Actions[i].Value.(*imageapi.ImageStream) - if e, a := test.expectedUpdates[i], updatedStream; !reflect.DeepEqual(e, a) { - t.Errorf("%s: unexpected updated stream: %s", name, util.ObjectDiff(e, a)) - } + if e, a := "delete-image", imageClient.Actions[0].Action; e != a { + t.Errorf("%s: expected action %q, got %q", name, e, a) } } } @@ -823,12 +741,17 @@ func TestRegistryPruning(t *testing.T) { } for name, test := range tests { + t.Logf("Running test case %s", name) actualLayerDeletions := util.NewStringSet() actualBlobDeletions := util.NewStringSet() actualManifestDeletions := util.NewStringSet() - pruneImage := func(image *imageapi.Image, streams []*imageapi.ImageStream) []error { - return []error{} + pruneImage := func(image *imageapi.Image) error { + return nil + } + + pruneStream := func(stream *imageapi.ImageStream, image *imageapi.Image) (*imageapi.ImageStream, error) { + return stream, nil } pruneLayer := func(registryURL, repo, layer string) error { @@ -848,7 +771,7 @@ func TestRegistryPruning(t *testing.T) { p := NewImagePruner(60, 1, &test.images, &test.streams, &kapi.PodList{}, &kapi.ReplicationControllerList{}, &buildapi.BuildConfigList{}, &buildapi.BuildList{}, &deployapi.DeploymentConfigList{}) - p.Run(pruneImage, pruneLayer, pruneBlob, pruneManifest) + p.Run(pruneImage, pruneStream, pruneLayer, pruneBlob, pruneManifest) if !reflect.DeepEqual(test.expectedLayerDeletions, actualLayerDeletions) { t.Errorf("%s: expected layer deletions %#v, got %#v", name, test.expectedLayerDeletions, actualLayerDeletions) From 4d219ff2d2055571e47bebf4c244de3205961767 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Thu, 21 May 2015 14:42:01 -0400 Subject: [PATCH 20/21] Use client config's CA for registry pruning --- pkg/cmd/admin/prune/images.go | 18 +++++-- pkg/image/prune/imagepruner.go | 89 ++++++++++------------------------ 2 files changed, 41 insertions(+), 66 deletions(-) diff --git a/pkg/cmd/admin/prune/images.go b/pkg/cmd/admin/prune/images.go index c915153da938..96d60220cf65 100644 --- a/pkg/cmd/admin/prune/images.go +++ b/pkg/cmd/admin/prune/images.go @@ -10,6 +10,7 @@ import ( "time" kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" @@ -133,6 +134,17 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri manifestPruneFunc prune.ManifestPruneFunc ) + clientConfig, err := f.OpenShiftClientConfig.ClientConfig() + cmdutil.CheckErr(err) + + tlsConfig, err := kclient.TLSConfigFor(clientConfig) + cmdutil.CheckErr(err) + + tr := http.Transport{ + TLSClientConfig: tlsConfig, + } + registryClient := &http.Client{Transport: &tr} + switch cfg.DryRun { case false: imagePruneFunc = func(image *imageapi.Image) error { @@ -145,10 +157,10 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri } layerPruneFunc = func(registryURL, repo, layer string) error { describingLayerPruneFunc(registryURL, repo, layer) - return prune.DeletingLayerPruneFunc(http.DefaultClient)(registryURL, repo, layer) + return prune.DeletingLayerPruneFunc(registryClient)(registryURL, repo, layer) } - blobPruneFunc = prune.DeletingBlobPruneFunc(http.DefaultClient) - manifestPruneFunc = prune.DeletingManifestPruneFunc(http.DefaultClient) + blobPruneFunc = prune.DeletingBlobPruneFunc(registryClient) + manifestPruneFunc = prune.DeletingManifestPruneFunc(registryClient) default: fmt.Fprintln(os.Stderr, "Dry run enabled - no modifications will be made.") imagePruneFunc = describingImagePruneFunc diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index 8b521e70896f..aed778cdeb41 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -607,17 +607,9 @@ func DeletingImageStreamPruneFunc(streams client.ImageStreamsNamespacer) ImageSt } } -// DeletingLayerPruneFunc returns a LayerPruneFunc that uses registryClient to -// send a layer deletion request to the regsitry. -// -// The request URL is http://registryURL/admin//layers/ and it is -// a DELETE request. -func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { - return func(registryURL, repoName, layer string) error { - glog.V(4).Infof("Pruning registry %q, repo %q, layer %q", registryURL, repoName, layer) - - //TODO https - req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/admin/%s/layers/%s", registryURL, repoName, layer), nil) +func deleteFromRegistry(registryClient *http.Client, url string) error { + deleteFunc := func(proto, url string) error { + req, err := http.NewRequest("DELETE", url, nil) if err != nil { glog.Errorf("Error creating request: %v", err) return fmt.Errorf("Error creating request: %v", err) @@ -642,68 +634,39 @@ func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { return nil } + + var err error + for _, proto := range []string{"https", "http"} { + err = deleteFunc(proto, fmt.Sprintf("%s://%s", proto, url)) + if err == nil { + return nil + } + } + return err +} + +// DeletingLayerPruneFunc returns a LayerPruneFunc that uses registryClient to +// send a layer deletion request to the regsitry. +// +// The request URL is http://registryURL/admin//layers/ and it is +// a DELETE request. +func DeletingLayerPruneFunc(registryClient *http.Client) LayerPruneFunc { + return func(registryURL, repoName, layer string) error { + glog.V(4).Infof("Pruning registry %q, repo %q, layer %q", registryURL, repoName, layer) + return deleteFromRegistry(registryClient, fmt.Sprintf("%s/admin/%s/layers/%s", registryURL, repoName, layer)) + } } func DeletingBlobPruneFunc(registryClient *http.Client) BlobPruneFunc { return func(registryURL, blob string) error { glog.V(4).Infof("Pruning registry %q, blob %q", registryURL, blob) - - //TODO https - req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/admin/blobs/%s", registryURL, blob), nil) - if err != nil { - glog.Errorf("Error creating request: %v", err) - return fmt.Errorf("Error creating request: %v", err) - } - - glog.V(4).Infof("Sending request to registry") - resp, err := registryClient.Do(req) - if err != nil { - glog.Errorf("Error sending request: %v", err) - return fmt.Errorf("Error sending request: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusNoContent { - glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) - //TODO do a better job of decoding and reporting the errors? - decoder := json.NewDecoder(resp.Body) - response := make(map[string]interface{}) - decoder.Decode(&response) - return fmt.Errorf("Unexpected status code %d in response: %#v", resp.StatusCode, response) - } - - return nil + return deleteFromRegistry(registryClient, fmt.Sprintf("%s/admin/blobs/%s", registryURL, blob)) } } func DeletingManifestPruneFunc(registryClient *http.Client) ManifestPruneFunc { return func(registryURL, repoName, manifest string) error { glog.V(4).Infof("Pruning manifest for registry %q, repo %q, manifest %q", registryURL, repoName, manifest) - - //TODO https - req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/admin/%s/manifests/%s", registryURL, repoName, manifest), nil) - if err != nil { - glog.Errorf("Error creating request: %v", err) - return fmt.Errorf("Error creating request: %v", err) - } - - glog.V(4).Infof("Sending request to registry") - resp, err := registryClient.Do(req) - if err != nil { - glog.Errorf("Error sending request: %v", err) - return fmt.Errorf("Error sending request: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusNoContent { - glog.Errorf("Unexpected status code in response: %d", resp.StatusCode) - //TODO do a better job of decoding and reporting the errors? - decoder := json.NewDecoder(resp.Body) - response := make(map[string]interface{}) - decoder.Decode(&response) - return fmt.Errorf("Unexpected status code %d in response: %#v", resp.StatusCode, response) - } - - return nil + return deleteFromRegistry(registryClient, fmt.Sprintf("%s/admin/%s/manifests/%s", registryURL, repoName, manifest)) } } From 05d1daea7e1b38ddc6ae4be60c644518ab1dd4ee Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Thu, 21 May 2015 15:49:40 -0400 Subject: [PATCH 21/21] Support custom CA for registry pruning --- pkg/cmd/admin/prune/images.go | 23 ++++++++++++++++++++--- pkg/image/prune/imagepruner.go | 1 - 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/admin/prune/images.go b/pkg/cmd/admin/prune/images.go index 96d60220cf65..76db887fc9c0 100644 --- a/pkg/cmd/admin/prune/images.go +++ b/pkg/cmd/admin/prune/images.go @@ -1,8 +1,10 @@ package prune import ( + "crypto/x509" "fmt" "io" + "io/ioutil" "net/http" "os" "strings" @@ -31,6 +33,7 @@ type pruneImagesConfig struct { DryRun bool KeepYoungerThan time.Duration TagRevisionsToKeep int + CABundle string } func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command { @@ -134,16 +137,29 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri manifestPruneFunc prune.ManifestPruneFunc ) + // get the client config so we can get the TLS config clientConfig, err := f.OpenShiftClientConfig.ClientConfig() cmdutil.CheckErr(err) tlsConfig, err := kclient.TLSConfigFor(clientConfig) cmdutil.CheckErr(err) - tr := http.Transport{ - TLSClientConfig: tlsConfig, + // if the user specified a CA on the command line, add it to the + // client config's CA roots + if len(cfg.CABundle) > 0 { + data, err := ioutil.ReadFile(cfg.CABundle) + cmdutil.CheckErr(err) + if tlsConfig.RootCAs == nil { + tlsConfig.RootCAs = x509.NewCertPool() + } + tlsConfig.RootCAs.AppendCertsFromPEM(data) + } + + registryClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, } - registryClient := &http.Client{Transport: &tr} switch cfg.DryRun { case false: @@ -181,6 +197,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri cmd.Flags().BoolVar(&cfg.DryRun, "dry-run", cfg.DryRun, "Perform a build pruning dry-run, displaying what would be deleted but not actually deleting anything.") cmd.Flags().DurationVar(&cfg.KeepYoungerThan, "keep-younger-than", cfg.KeepYoungerThan, "Specify the minimum age of a build for it to be considered a candidate for pruning.") cmd.Flags().IntVar(&cfg.TagRevisionsToKeep, "keep-tag-revisions", cfg.TagRevisionsToKeep, "Specify the number of image revisions for a tag in an image stream that will be preserved.") + cmd.Flags().StringVar(&cfg.CABundle, "certificate-authority", cfg.CABundle, "The path to a certificate authority bundle to use when communicating with the OpenShift-managed registries. Defaults to the certificate authority data from the current user's config file.") return cmd } diff --git a/pkg/image/prune/imagepruner.go b/pkg/image/prune/imagepruner.go index aed778cdeb41..19d4e08dbe5e 100644 --- a/pkg/image/prune/imagepruner.go +++ b/pkg/image/prune/imagepruner.go @@ -618,7 +618,6 @@ func deleteFromRegistry(registryClient *http.Client, url string) error { glog.V(4).Infof("Sending request to registry") resp, err := registryClient.Do(req) if err != nil { - glog.Errorf("Error sending request: %v", err) return fmt.Errorf("Error sending request: %v", err) } defer resp.Body.Close()