-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Provide layers referenced by an image stream as layers subresource
Adds a new GET endpoint to an image stream as a subresource `layers` that returns an array of every layer referenced by the image stream and the tags and images included by the image. The subresource is fed by a store driven informer that caches and indexes only the layers. Clients get a 500 retry error if the cache has not initialized yet (the client will silently retry). Turns the registry access check for a given layer into an O(1) check instead of O(N) where N is the number of images in the image stream.
- Loading branch information
1 parent
b5b4a14
commit 4744012
Showing
4 changed files
with
275 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
package etcd | ||
|
||
import ( | ||
"fmt" | ||
|
||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/runtime" | ||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
"k8s.io/apimachinery/pkg/types" | ||
"k8s.io/apimachinery/pkg/watch" | ||
apirequest "k8s.io/apiserver/pkg/endpoints/request" | ||
"k8s.io/apiserver/pkg/registry/rest" | ||
"k8s.io/client-go/tools/cache" | ||
|
||
imageapi "github.com/openshift/origin/pkg/image/apis/image" | ||
) | ||
|
||
// ImageLayerIndex is a cache of image digests to the layers they contain. | ||
// Because a very large number of images can exist on a cluster, we only | ||
// hold in memory a small subset of the full image object. | ||
type ImageLayerIndex interface { | ||
cache.SharedIndexInformer | ||
} | ||
|
||
type ImageStore interface { | ||
rest.Watcher | ||
rest.Lister | ||
} | ||
|
||
// NewImageLayerIndex creates a new index over a store that must return | ||
// images. | ||
func NewImageLayerIndex(store ImageStore) ImageLayerIndex { | ||
ctx := apirequest.NewContext() | ||
informer := cache.NewSharedIndexInformer(&cache.ListWatch{ | ||
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { | ||
obj, err := store.List(ctx, &metainternalversion.ListOptions{ | ||
ResourceVersion: options.ResourceVersion, | ||
Limit: options.Limit, | ||
Continue: options.Continue, | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
list, ok := obj.(*imageapi.ImageList) | ||
if !ok { | ||
return nil, fmt.Errorf("unexpected store type %T for layer index", obj) | ||
} | ||
// reduce the full image list to a smaller subset. | ||
out := &metainternalversion.List{ | ||
Items: make([]runtime.Object, len(list.Items)), | ||
} | ||
out.Continue = list.Continue | ||
out.ResourceVersion = list.ResourceVersion | ||
for i, image := range list.Items { | ||
out.Items[i] = &ImageLayers{ | ||
Name: image.Name, | ||
Layers: image.DockerImageLayers, | ||
Manifest: manifestFromImage(&image), | ||
} | ||
} | ||
return out, nil | ||
}, | ||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { | ||
w, err := store.Watch(ctx, &metainternalversion.ListOptions{ | ||
ResourceVersion: options.ResourceVersion, | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return watch.Filter(w, func(in watch.Event) (out watch.Event, keep bool) { | ||
if in.Object == nil { | ||
return in, true | ||
} | ||
// reduce each object to the minimal subset we need for the cache | ||
image, ok := in.Object.(*imageapi.Image) | ||
if !ok { | ||
return in, true | ||
} | ||
in.Object = &ImageLayers{ | ||
Name: image.Name, | ||
ResourceVersion: image.ResourceVersion, | ||
Layers: image.DockerImageLayers, | ||
Manifest: manifestFromImage(image), | ||
} | ||
return in, true | ||
}), nil | ||
}, | ||
}, &ImageLayers{}, 0, cache.Indexers{ | ||
// layers allows fast access to the images with a given layer | ||
"layers": func(obj interface{}) ([]string, error) { | ||
entry, ok := obj.(*ImageLayers) | ||
if !ok { | ||
return nil, fmt.Errorf("unexpected cache object %T", obj) | ||
} | ||
keys := make([]string, 0, len(entry.Layers)) | ||
for _, layer := range entry.Layers { | ||
keys = append(keys, layer.Name) | ||
} | ||
return keys, nil | ||
}, | ||
}) | ||
return informer | ||
} | ||
|
||
// manifestFromImage attempts to find a manifest blob description from | ||
// an image. Images older than schema2 in Docker do not have a manifest blob. | ||
func manifestFromImage(image *imageapi.Image) *imageapi.ImageLayer { | ||
if image.DockerImageManifestMediaType != "application/vnd.docker.distribution.manifest.v2+json" { | ||
return nil | ||
} | ||
return &imageapi.ImageLayer{ | ||
Name: image.DockerImageMetadata.ID, | ||
MediaType: image.DockerImageManifestMediaType, | ||
} | ||
} | ||
|
||
// ImageLayers is the minimal set of data we need to retain to provide the cache. | ||
// Unlike a more general informer cache, we do not retain the full object because of | ||
// the potential size of the objects being stored. Even a small cluster may have 20k | ||
// or more images in active use. | ||
type ImageLayers struct { | ||
Name string | ||
ResourceVersion string | ||
Manifest *imageapi.ImageLayer | ||
Layers []imageapi.ImageLayer | ||
} | ||
|
||
var ( | ||
_ runtime.Object = &ImageLayers{} | ||
_ metav1.Object = &ImageLayers{} | ||
) | ||
|
||
func (l *ImageLayers) GetObjectKind() schema.ObjectKind { return &metav1.TypeMeta{} } | ||
func (l *ImageLayers) DeepCopyObject() runtime.Object { | ||
var layers []imageapi.ImageLayer | ||
if l.Layers != nil { | ||
layers = make([]imageapi.ImageLayer, len(l.Layers)) | ||
copy(layers, l.Layers) | ||
} | ||
return &ImageLayers{ | ||
Name: l.Name, | ||
Layers: layers, | ||
} | ||
} | ||
|
||
// client-go/cache.SharedIndexInformer hardcodes the key function to assume ObjectMeta. | ||
// Here we implement the relevant accessors to allow a minimal index to be created. | ||
// SharedIndexInformer will be refactored to require a more minimal subset of actions | ||
// in the near future. | ||
|
||
func (l *ImageLayers) GetName() string { return l.Name } | ||
func (l *ImageLayers) GetNamespace() string { return "" } | ||
func (l *ImageLayers) GetResourceVersion() string { return l.ResourceVersion } | ||
func (l *ImageLayers) SetResourceVersion(version string) { l.ResourceVersion = version } | ||
|
||
// These methods are unused stubs to satisfy meta.Object. | ||
|
||
func (l *ImageLayers) SetNamespace(namespace string) {} | ||
func (l *ImageLayers) SetName(name string) {} | ||
func (l *ImageLayers) GetGenerateName() string { return "" } | ||
func (l *ImageLayers) SetGenerateName(name string) {} | ||
func (l *ImageLayers) GetUID() types.UID { return "" } | ||
func (l *ImageLayers) SetUID(uid types.UID) {} | ||
func (l *ImageLayers) GetGeneration() int64 { return 0 } | ||
func (l *ImageLayers) SetGeneration(generation int64) {} | ||
func (l *ImageLayers) GetSelfLink() string { return "" } | ||
func (l *ImageLayers) SetSelfLink(selfLink string) {} | ||
func (l *ImageLayers) GetCreationTimestamp() metav1.Time { return metav1.Time{} } | ||
func (l *ImageLayers) SetCreationTimestamp(timestamp metav1.Time) {} | ||
func (l *ImageLayers) GetDeletionTimestamp() *metav1.Time { return nil } | ||
func (l *ImageLayers) SetDeletionTimestamp(timestamp *metav1.Time) {} | ||
func (l *ImageLayers) GetDeletionGracePeriodSeconds() *int64 { return nil } | ||
func (l *ImageLayers) SetDeletionGracePeriodSeconds(*int64) {} | ||
func (l *ImageLayers) GetLabels() map[string]string { return nil } | ||
func (l *ImageLayers) SetLabels(labels map[string]string) {} | ||
func (l *ImageLayers) GetAnnotations() map[string]string { return nil } | ||
func (l *ImageLayers) SetAnnotations(annotations map[string]string) {} | ||
func (l *ImageLayers) GetInitializers() *metav1.Initializers { return nil } | ||
func (l *ImageLayers) SetInitializers(initializers *metav1.Initializers) {} | ||
func (l *ImageLayers) GetFinalizers() []string { return nil } | ||
func (l *ImageLayers) SetFinalizers(finalizers []string) {} | ||
func (l *ImageLayers) GetOwnerReferences() []metav1.OwnerReference { return nil } | ||
func (l *ImageLayers) SetOwnerReferences([]metav1.OwnerReference) {} | ||
func (l *ImageLayers) GetClusterName() string { return "" } | ||
func (l *ImageLayers) SetClusterName(clusterName string) {} |