Skip to content

Commit 57a223b

Browse files
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.
1 parent efb4acc commit 57a223b

File tree

12 files changed

+561
-105
lines changed

12 files changed

+561
-105
lines changed

pkg/cmd/server/origin/legacy.go

+2
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,8 @@ func LegacyStorage(storage map[schema.GroupVersion]map[string]rest.Storage) map[
207207

208208
case *imagestreametcd.REST:
209209
legacyStorage[resource] = &imagestreametcd.LegacyREST{REST: storage}
210+
case *imagestreametcd.LayersREST:
211+
delete(legacyStorage, resource)
210212

211213
case *routeetcd.REST:
212214
store := *storage.Store

pkg/image/apiserver/apiserver.go

+15-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type ExtraConfig struct {
5656
makeV1Storage sync.Once
5757
v1Storage map[string]rest.Storage
5858
v1StorageErr error
59+
startFns []func(<-chan struct{})
5960
}
6061

6162
type ImageAPIServerConfig struct {
@@ -109,6 +110,15 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
109110
return nil, err
110111
}
111112

113+
if err := s.GenericAPIServer.AddPostStartHook("image.openshift.io-apiserver-caches", func(context genericapiserver.PostStartHookContext) error {
114+
for _, fn := range c.ExtraConfig.startFns {
115+
go fn(context.StopCh)
116+
}
117+
return nil
118+
}); err != nil {
119+
return nil, err
120+
}
121+
112122
return s, nil
113123
}
114124

@@ -193,10 +203,13 @@ func (c *completedConfig) newV1RESTStorage() (map[string]rest.Storage, error) {
193203
whitelister = whitelist.WhitelistAllRegistries()
194204
}
195205

206+
imageLayerIndex := imagestreametcd.NewImageLayerIndex(imageV1Client.Image().Images())
207+
c.ExtraConfig.startFns = append(c.ExtraConfig.startFns, imageLayerIndex.Run)
208+
196209
imageRegistry := image.NewRegistry(imageStorage)
197210
imageSignatureStorage := imagesignature.NewREST(imageClient.Image())
198211
imageStreamSecretsStorage := imagesecret.NewREST(coreClient)
199-
imageStreamStorage, imageStreamStatusStorage, internalImageStreamStorage, err := imagestreametcd.NewREST(c.GenericConfig.RESTOptionsGetter, c.ExtraConfig.RegistryHostnameRetriever, authorizationClient.SubjectAccessReviews(), c.ExtraConfig.LimitVerifier, whitelister)
212+
imageStreamStorage, imageStreamLayersStorage, imageStreamStatusStorage, internalImageStreamStorage, err := imagestreametcd.NewREST(c.GenericConfig.RESTOptionsGetter, c.ExtraConfig.RegistryHostnameRetriever, authorizationClient.SubjectAccessReviews(), c.ExtraConfig.LimitVerifier, whitelister, imageLayerIndex)
200213
if err != nil {
201214
return nil, fmt.Errorf("error building REST storage: %v", err)
202215
}
@@ -231,6 +244,7 @@ func (c *completedConfig) newV1RESTStorage() (map[string]rest.Storage, error) {
231244
v1Storage["imagesignatures"] = imageSignatureStorage
232245
v1Storage["imageStreams/secrets"] = imageStreamSecretsStorage
233246
v1Storage["imageStreams"] = imageStreamStorage
247+
v1Storage["imageStreams/layers"] = imageStreamLayersStorage
234248
v1Storage["imageStreams/status"] = imageStreamStatusStorage
235249
v1Storage["imageStreamImports"] = imageStreamImportStorage
236250
v1Storage["imageStreamImages"] = imageStreamImageStorage

pkg/image/registry/imagestream/etcd/etcd.go

+73-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package etcd
33
import (
44
"context"
55

6+
"k8s.io/apimachinery/pkg/api/errors"
67
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
78
"k8s.io/apimachinery/pkg/runtime"
89
"k8s.io/apiserver/pkg/registry/generic"
@@ -47,7 +48,8 @@ func NewREST(
4748
subjectAccessReviewRegistry authorizationclient.SubjectAccessReviewInterface,
4849
limitVerifier imageadmission.LimitVerifier,
4950
registryWhitelister whitelist.RegistryWhitelister,
50-
) (*REST, *StatusREST, *InternalREST, error) {
51+
imageLayerIndex ImageLayerIndex,
52+
) (*REST, *LayersREST, *StatusREST, *InternalREST, error) {
5153
store := registry.Store{
5254
NewFunc: func() runtime.Object { return &imageapi.ImageStream{} },
5355
NewListFunc: func() runtime.Object { return &imageapi.ImageStreamList{} },
@@ -72,9 +74,11 @@ func NewREST(
7274
AttrFunc: storage.AttrFunc(storage.DefaultNamespaceScopedAttr).WithFieldMutation(imageapi.ImageStreamSelector),
7375
}
7476
if err := store.CompleteWithOptions(options); err != nil {
75-
return nil, nil, nil, err
77+
return nil, nil, nil, nil, err
7678
}
7779

80+
layersREST := &LayersREST{index: imageLayerIndex, store: &store}
81+
7882
statusStrategy := imagestream.NewStatusStrategy(strategy)
7983
statusStore := store
8084
statusStore.Decorator = nil
@@ -89,7 +93,7 @@ func NewREST(
8993
internalStore.UpdateStrategy = internalStrategy
9094

9195
internalREST := &InternalREST{store: &internalStore}
92-
return rest, statusREST, internalREST, nil
96+
return rest, layersREST, statusREST, internalREST, nil
9397
}
9498

9599
// StatusREST implements the REST endpoint for changing the status of an image stream.
@@ -139,6 +143,72 @@ func (r *InternalREST) Update(ctx context.Context, name string, objInfo rest.Upd
139143
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation)
140144
}
141145

146+
// LayersREST implements the REST endpoint for changing both the spec and status of an image stream.
147+
type LayersREST struct {
148+
store *registry.Store
149+
index ImageLayerIndex
150+
}
151+
152+
var _ rest.Getter = &LayersREST{}
153+
154+
func (r *LayersREST) New() runtime.Object {
155+
return &imageapi.ImageStreamLayers{}
156+
}
157+
158+
// Get returns the layers for an image stream.
159+
func (r *LayersREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
160+
if !r.index.HasSynced() {
161+
return nil, errors.NewServerTimeout(r.store.DefaultQualifiedResource, "get", 2)
162+
}
163+
obj, err := r.store.Get(ctx, name, options)
164+
if err != nil {
165+
return nil, err
166+
}
167+
is := obj.(*imageapi.ImageStream)
168+
isl := &imageapi.ImageStreamLayers{
169+
ObjectMeta: is.ObjectMeta,
170+
Blobs: make(map[string]imageapi.ImageLayerData),
171+
Images: make(map[string]imageapi.ImageBlobReferences),
172+
}
173+
174+
for _, status := range is.Status.Tags {
175+
for _, item := range status.Items {
176+
if len(item.Image) == 0 {
177+
continue
178+
}
179+
180+
obj, _, _ := r.index.GetByKey(item.Image)
181+
entry, ok := obj.(*ImageLayers)
182+
if !ok {
183+
continue
184+
}
185+
186+
if _, ok := isl.Images[item.Image]; !ok {
187+
var reference imageapi.ImageBlobReferences
188+
for _, layer := range entry.Layers {
189+
reference.Layers = append(reference.Layers, layer.Name)
190+
if _, ok := isl.Blobs[layer.Name]; !ok {
191+
isl.Blobs[layer.Name] = imageapi.ImageLayerData{LayerSize: &layer.LayerSize, MediaType: layer.MediaType}
192+
}
193+
}
194+
if blob := entry.Manifest; blob != nil {
195+
reference.Manifest = &blob.Name
196+
if _, ok := isl.Blobs[blob.Name]; !ok {
197+
if blob.LayerSize == 0 {
198+
// only send media type since we don't the size of the manifest
199+
isl.Blobs[blob.Name] = imageapi.ImageLayerData{MediaType: blob.MediaType}
200+
} else {
201+
isl.Blobs[blob.Name] = imageapi.ImageLayerData{LayerSize: &blob.LayerSize, MediaType: blob.MediaType}
202+
}
203+
}
204+
}
205+
isl.Images[item.Image] = reference
206+
}
207+
}
208+
}
209+
return isl, nil
210+
}
211+
142212
// LegacyREST allows us to wrap and alter some behavior
143213
type LegacyREST struct {
144214
*REST

pkg/image/registry/imagestream/etcd/etcd_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,14 @@ func newStorage(t *testing.T) (*REST, *StatusREST, *InternalREST, *etcdtesting.E
5353
server, etcdStorage := etcdtesting.NewUnsecuredEtcd3TestClientServer(t)
5454
etcdStorage.Codec = legacyscheme.Codecs.LegacyCodec(schema.GroupVersion{Group: "image.openshift.io", Version: "v1"})
5555
registry := imageapi.DefaultRegistryHostnameRetriever(noDefaultRegistry, "", "")
56-
imageStorage, statusStorage, internalStorage, err := NewREST(
56+
imageStorage, _, statusStorage, internalStorage, err := NewREST(
5757
restoptions.NewSimpleGetter(etcdStorage),
5858
registry,
5959
&fakeSubjectAccessReviewRegistry{},
6060
&admfake.ImageStreamLimitVerifier{},
61-
&fake.RegistryWhitelister{})
61+
&fake.RegistryWhitelister{},
62+
NewEmptyLayerIndex(),
63+
)
6264
if err != nil {
6365
t.Fatal(err)
6466
}

0 commit comments

Comments
 (0)