Skip to content

Commit 6b1a836

Browse files
authoredOct 31, 2017
Merge pull request #16821 from miminar/image-pruning-dereference-istags
Automatic merge from submit-queue. image-pruning: dereference ImageStreamTags Create strong references to images for each pod/bc/dc/etc that uses `<host>/<repo>:tag` reference. Resolves [bz#1498604](https://bugzilla.redhat.com/show_bug.cgi?id=1498604) and https://bugzilla.redhat.com/show_bug.cgi?id=1386917 Images can manually removed using `oc delete`. Image stream tags having references to these images become obsolete - we may delete them. To be sure that we don't remove reference to image that has just been created (and we don't know about it), make sure to honor `--keep-younger-than`.
2 parents 1bae363 + 1cda0bb commit 6b1a836

File tree

13 files changed

+2232
-1046
lines changed

13 files changed

+2232
-1046
lines changed
 

‎pkg/apps/graph/nodes/nodes.go

+86-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,69 @@ package nodes
33
import (
44
"github.com/gonum/graph"
55

6+
kapisext "k8s.io/kubernetes/pkg/apis/extensions"
7+
68
osgraph "github.com/openshift/origin/pkg/api/graph"
79
kubegraph "github.com/openshift/origin/pkg/api/kubegraph/nodes"
8-
depoyapi "github.com/openshift/origin/pkg/apps/apis/apps"
10+
deployapi "github.com/openshift/origin/pkg/apps/apis/apps"
911
)
1012

13+
// EnsureDaemonSetNode adds the provided daemon set to the graph if it does not exist
14+
func EnsureDaemonSetNode(g osgraph.MutableUniqueGraph, ds *kapisext.DaemonSet) *DaemonSetNode {
15+
dsName := DaemonSetNodeName(ds)
16+
dsNode := osgraph.EnsureUnique(
17+
g,
18+
dsName,
19+
func(node osgraph.Node) graph.Node {
20+
return &DaemonSetNode{Node: node, DaemonSet: ds, IsFound: true}
21+
},
22+
).(*DaemonSetNode)
23+
24+
podTemplateSpecNode := kubegraph.EnsurePodTemplateSpecNode(g, &ds.Spec.Template, ds.Namespace, dsName)
25+
g.AddEdge(dsNode, podTemplateSpecNode, osgraph.ContainsEdgeKind)
26+
27+
return dsNode
28+
}
29+
30+
func FindOrCreateSyntheticDaemonSetNode(g osgraph.MutableUniqueGraph, ds *kapisext.DaemonSet) *DaemonSetNode {
31+
return osgraph.EnsureUnique(
32+
g,
33+
DaemonSetNodeName(ds),
34+
func(node osgraph.Node) graph.Node {
35+
return &DaemonSetNode{Node: node, DaemonSet: ds, IsFound: false}
36+
},
37+
).(*DaemonSetNode)
38+
}
39+
40+
// EnsureDeploymentNode adds the provided upstream deployment to the graph if it does not exist
41+
func EnsureDeploymentNode(g osgraph.MutableUniqueGraph, deployment *kapisext.Deployment) *DeploymentNode {
42+
deploymentName := DeploymentNodeName(deployment)
43+
deploymentNode := osgraph.EnsureUnique(
44+
g,
45+
deploymentName,
46+
func(node osgraph.Node) graph.Node {
47+
return &DeploymentNode{Node: node, Deployment: deployment, IsFound: true}
48+
},
49+
).(*DeploymentNode)
50+
51+
podTemplateSpecNode := kubegraph.EnsurePodTemplateSpecNode(g, &deployment.Spec.Template, deployment.Namespace, deploymentName)
52+
g.AddEdge(deploymentNode, podTemplateSpecNode, osgraph.ContainsEdgeKind)
53+
54+
return deploymentNode
55+
}
56+
57+
func FindOrCreateSyntheticDeploymentNode(g osgraph.MutableUniqueGraph, deployment *kapisext.Deployment) *DeploymentNode {
58+
return osgraph.EnsureUnique(
59+
g,
60+
DeploymentNodeName(deployment),
61+
func(node osgraph.Node) graph.Node {
62+
return &DeploymentNode{Node: node, Deployment: deployment, IsFound: false}
63+
},
64+
).(*DeploymentNode)
65+
}
66+
1167
// EnsureDeploymentConfigNode adds the provided deployment config to the graph if it does not exist
12-
func EnsureDeploymentConfigNode(g osgraph.MutableUniqueGraph, dc *depoyapi.DeploymentConfig) *DeploymentConfigNode {
68+
func EnsureDeploymentConfigNode(g osgraph.MutableUniqueGraph, dc *deployapi.DeploymentConfig) *DeploymentConfigNode {
1369
dcName := DeploymentConfigNodeName(dc)
1470
dcNode := osgraph.EnsureUnique(
1571
g,
@@ -27,7 +83,7 @@ func EnsureDeploymentConfigNode(g osgraph.MutableUniqueGraph, dc *depoyapi.Deplo
2783
return dcNode
2884
}
2985

30-
func FindOrCreateSyntheticDeploymentConfigNode(g osgraph.MutableUniqueGraph, dc *depoyapi.DeploymentConfig) *DeploymentConfigNode {
86+
func FindOrCreateSyntheticDeploymentConfigNode(g osgraph.MutableUniqueGraph, dc *deployapi.DeploymentConfig) *DeploymentConfigNode {
3187
return osgraph.EnsureUnique(
3288
g,
3389
DeploymentConfigNodeName(dc),
@@ -36,3 +92,30 @@ func FindOrCreateSyntheticDeploymentConfigNode(g osgraph.MutableUniqueGraph, dc
3692
},
3793
).(*DeploymentConfigNode)
3894
}
95+
96+
// EnsureReplicaSetNode adds the provided replica set to the graph if it does not exist
97+
func EnsureReplicaSetNode(g osgraph.MutableUniqueGraph, rs *kapisext.ReplicaSet) *ReplicaSetNode {
98+
rsName := ReplicaSetNodeName(rs)
99+
rsNode := osgraph.EnsureUnique(
100+
g,
101+
rsName,
102+
func(node osgraph.Node) graph.Node {
103+
return &ReplicaSetNode{Node: node, ReplicaSet: rs, IsFound: true}
104+
},
105+
).(*ReplicaSetNode)
106+
107+
podTemplateSpecNode := kubegraph.EnsurePodTemplateSpecNode(g, &rs.Spec.Template, rs.Namespace, rsName)
108+
g.AddEdge(rsNode, podTemplateSpecNode, osgraph.ContainsEdgeKind)
109+
110+
return rsNode
111+
}
112+
113+
func FindOrCreateSyntheticReplicaSetNode(g osgraph.MutableUniqueGraph, rs *kapisext.ReplicaSet) *ReplicaSetNode {
114+
return osgraph.EnsureUnique(
115+
g,
116+
ReplicaSetNodeName(rs),
117+
func(node osgraph.Node) graph.Node {
118+
return &ReplicaSetNode{Node: node, ReplicaSet: rs, IsFound: false}
119+
},
120+
).(*ReplicaSetNode)
121+
}

‎pkg/apps/graph/nodes/types.go

+86
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,73 @@ package nodes
33
import (
44
"reflect"
55

6+
kapisext "k8s.io/kubernetes/pkg/apis/extensions"
7+
68
osgraph "github.com/openshift/origin/pkg/api/graph"
79
deployapi "github.com/openshift/origin/pkg/apps/apis/apps"
810
)
911

1012
var (
13+
DaemonSetNodeKind = reflect.TypeOf(kapisext.DaemonSet{}).Name()
14+
DeploymentNodeKind = reflect.TypeOf(kapisext.Deployment{}).Name()
1115
DeploymentConfigNodeKind = reflect.TypeOf(deployapi.DeploymentConfig{}).Name()
16+
ReplicaSetNodeKind = reflect.TypeOf(kapisext.ReplicaSet{}).Name()
1217
)
1318

19+
func DaemonSetNodeName(o *kapisext.DaemonSet) osgraph.UniqueName {
20+
return osgraph.GetUniqueRuntimeObjectNodeName(DaemonSetNodeKind, o)
21+
}
22+
23+
type DaemonSetNode struct {
24+
osgraph.Node
25+
DaemonSet *kapisext.DaemonSet
26+
27+
IsFound bool
28+
}
29+
30+
func (n DaemonSetNode) Found() bool {
31+
return n.IsFound
32+
}
33+
34+
func (n DaemonSetNode) Object() interface{} {
35+
return n.DaemonSet
36+
}
37+
38+
func (n DaemonSetNode) String() string {
39+
return string(DaemonSetNodeName(n.DaemonSet))
40+
}
41+
42+
func (*DaemonSetNode) Kind() string {
43+
return DaemonSetNodeKind
44+
}
45+
46+
func DeploymentNodeName(o *kapisext.Deployment) osgraph.UniqueName {
47+
return osgraph.GetUniqueRuntimeObjectNodeName(DeploymentNodeKind, o)
48+
}
49+
50+
type DeploymentNode struct {
51+
osgraph.Node
52+
Deployment *kapisext.Deployment
53+
54+
IsFound bool
55+
}
56+
57+
func (n DeploymentNode) Found() bool {
58+
return n.IsFound
59+
}
60+
61+
func (n DeploymentNode) Object() interface{} {
62+
return n.Deployment
63+
}
64+
65+
func (n DeploymentNode) String() string {
66+
return string(DeploymentNodeName(n.Deployment))
67+
}
68+
69+
func (*DeploymentNode) Kind() string {
70+
return DeploymentNodeKind
71+
}
72+
1473
func DeploymentConfigNodeName(o *deployapi.DeploymentConfig) osgraph.UniqueName {
1574
return osgraph.GetUniqueRuntimeObjectNodeName(DeploymentConfigNodeKind, o)
1675
}
@@ -37,3 +96,30 @@ func (n DeploymentConfigNode) String() string {
3796
func (*DeploymentConfigNode) Kind() string {
3897
return DeploymentConfigNodeKind
3998
}
99+
100+
func ReplicaSetNodeName(o *kapisext.ReplicaSet) osgraph.UniqueName {
101+
return osgraph.GetUniqueRuntimeObjectNodeName(ReplicaSetNodeKind, o)
102+
}
103+
104+
type ReplicaSetNode struct {
105+
osgraph.Node
106+
ReplicaSet *kapisext.ReplicaSet
107+
108+
IsFound bool
109+
}
110+
111+
func (n ReplicaSetNode) Found() bool {
112+
return n.IsFound
113+
}
114+
115+
func (n ReplicaSetNode) Object() interface{} {
116+
return n.ReplicaSet
117+
}
118+
119+
func (n ReplicaSetNode) String() string {
120+
return string(ReplicaSetNodeName(n.ReplicaSet))
121+
}
122+
123+
func (*ReplicaSetNode) Kind() string {
124+
return ReplicaSetNodeKind
125+
}

‎pkg/cmd/server/bootstrappolicy/policy.go

+3
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,9 @@ func GetOpenshiftBootstrapClusterRoles() []rbac.ClusterRole {
551551
rbac.NewRule("list").Groups(kapiGroup).Resources("limitranges").RuleOrDie(),
552552
rbac.NewRule("get", "list").Groups(buildGroup, legacyBuildGroup).Resources("buildconfigs", "builds").RuleOrDie(),
553553
rbac.NewRule("get", "list").Groups(deployGroup, legacyDeployGroup).Resources("deploymentconfigs").RuleOrDie(),
554+
rbac.NewRule("get", "list").Groups(extensionsGroup).Resources("daemonsets").RuleOrDie(),
555+
rbac.NewRule("get", "list").Groups(extensionsGroup).Resources("deployments").RuleOrDie(),
556+
rbac.NewRule("get", "list").Groups(extensionsGroup).Resources("replicasets").RuleOrDie(),
554557

555558
rbac.NewRule("delete").Groups(imageGroup, legacyImageGroup).Resources("images").RuleOrDie(),
556559
rbac.NewRule("get", "list").Groups(imageGroup, legacyImageGroup).Resources("images", "imagestreams").RuleOrDie(),

‎pkg/image/graph/nodes/nodes.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@ func EnsureAllImageStreamTagNodes(g osgraph.MutableUniqueGraph, is *imageapi.Ima
3636
return ret
3737
}
3838

39-
func FindImage(g osgraph.MutableUniqueGraph, imageName string) graph.Node {
40-
return g.Find(ImageNodeName(&imageapi.Image{ObjectMeta: metav1.ObjectMeta{Name: imageName}}))
39+
func FindImage(g osgraph.MutableUniqueGraph, imageName string) *ImageNode {
40+
n := g.Find(ImageNodeName(&imageapi.Image{ObjectMeta: metav1.ObjectMeta{Name: imageName}}))
41+
if imageNode, ok := n.(*ImageNode); ok {
42+
return imageNode
43+
}
44+
return nil
4145
}
4246

4347
// EnsureDockerRepositoryNode adds the named Docker repository tag reference to the graph if it does

‎pkg/image/prune/helper.go

+59
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"sort"
88
"strings"
99

10+
kapi "k8s.io/kubernetes/pkg/api"
11+
1012
"github.com/docker/distribution/registry/api/errcode"
1113
"github.com/golang/glog"
1214

@@ -206,3 +208,60 @@ func TryProtocolsWithRegistryURL(registry string, allowInsecure bool, action fun
206208
type retryPath struct{ err error }
207209

208210
func (rp *retryPath) Error() string { return rp.err.Error() }
211+
212+
// ErrBadReference denotes an invalid reference to image, imagestreamtag or imagestreamimage stored in a
213+
// particular object. The object is identified by kind, namespace and name.
214+
type ErrBadReference struct {
215+
kind string
216+
namespace string
217+
name string
218+
targetKind string
219+
reference string
220+
reason string
221+
}
222+
223+
func newErrBadReferenceToImage(reference string, obj *kapi.ObjectReference, reason string) error {
224+
kind := "<UnknownType>"
225+
namespace := ""
226+
name := "<unknown-name>"
227+
if obj != nil {
228+
kind = obj.Kind
229+
namespace = obj.Namespace
230+
name = obj.Name
231+
}
232+
233+
return &ErrBadReference{
234+
kind: kind,
235+
namespace: namespace,
236+
name: name,
237+
reference: reference,
238+
reason: reason,
239+
}
240+
}
241+
242+
func newErrBadReferenceTo(targetKind, reference string, obj *kapi.ObjectReference, reason string) error {
243+
return &ErrBadReference{
244+
kind: obj.Kind,
245+
namespace: obj.Namespace,
246+
name: obj.Name,
247+
targetKind: targetKind,
248+
reference: reference,
249+
reason: reason,
250+
}
251+
}
252+
253+
func (e *ErrBadReference) Error() string {
254+
return e.String()
255+
}
256+
257+
func (e *ErrBadReference) String() string {
258+
name := e.name
259+
if len(e.namespace) > 0 {
260+
name = e.namespace + "/" + name
261+
}
262+
targetKind := "docker image"
263+
if len(e.targetKind) > 0 {
264+
targetKind = e.targetKind
265+
}
266+
return fmt.Sprintf("%s[%s]: invalid %s reference %q: %s", e.kind, name, targetKind, e.reference, e.reason)
267+
}

‎pkg/image/prune/prune.go

+370-126
Large diffs are not rendered by default.

‎pkg/image/prune/prune_test.go

+841-888
Large diffs are not rendered by default.

‎pkg/image/prune/testutil/util.go

+444
Large diffs are not rendered by default.

‎pkg/oc/admin/prune/images.go

+151-24
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package prune
22

33
import (
44
"crypto/x509"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"io"
@@ -15,9 +16,14 @@ import (
1516
"time"
1617

1718
"github.com/spf13/cobra"
19+
20+
kerrors "k8s.io/apimachinery/pkg/api/errors"
1821
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
kutilerrors "k8s.io/apimachinery/pkg/util/errors"
1923
knet "k8s.io/apimachinery/pkg/util/net"
24+
discovery "k8s.io/client-go/discovery"
2025
restclient "k8s.io/client-go/rest"
26+
kclientcmd "k8s.io/client-go/tools/clientcmd"
2127
kapi "k8s.io/kubernetes/pkg/api"
2228
kclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
2329
"k8s.io/kubernetes/pkg/kubectl/cmd/templates"
@@ -32,6 +38,7 @@ import (
3238
"github.com/openshift/origin/pkg/image/prune"
3339
oserrors "github.com/openshift/origin/pkg/util/errors"
3440
"github.com/openshift/origin/pkg/util/netutils"
41+
"github.com/openshift/origin/pkg/version"
3542
)
3643

3744
// PruneImagesRecommendedName is the recommended command name
@@ -72,8 +79,8 @@ var (
7279
--insecure-skip-tls-verify or allowed for insecure connection)`)
7380

7481
imagesExample = templates.Examples(`
75-
# See, what the prune command would delete if only images more than an hour old and obsoleted
76-
# by 3 newer revisions under the same tag were considered.
82+
# See, what the prune command would delete if only images and their referrers were more than an hour old
83+
# and obsoleted by 3 newer revisions under the same tag were considered.
7784
%[1]s %[2]s --keep-tag-revisions=3 --keep-younger-than=60m
7885
7986
# To actually perform the prune operation, the confirm flag must be appended
@@ -111,12 +118,15 @@ type PruneImagesOptions struct {
111118
Namespace string
112119
ForceInsecure bool
113120

114-
ClientConfig *restclient.Config
115-
AppsClient appsclient.AppsInterface
116-
BuildClient buildclient.BuildInterface
117-
ImageClient imageclient.ImageInterface
118-
KubeClient kclientset.Interface
119-
Out io.Writer
121+
ClientConfig *restclient.Config
122+
AppsClient appsclient.AppsInterface
123+
BuildClient buildclient.BuildInterface
124+
ImageClient imageclient.ImageInterface
125+
DiscoveryClient discovery.DiscoveryInterface
126+
KubeClient kclientset.Interface
127+
Timeout time.Duration
128+
Out io.Writer
129+
ErrOut io.Writer
120130
}
121131

122132
// NewCmdPruneImages implements the OpenShift cli prune images command.
@@ -146,7 +156,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
146156

147157
cmd.Flags().BoolVar(&opts.Confirm, "confirm", opts.Confirm, "If true, specify that image pruning should proceed. Defaults to false, displaying what would be deleted but not actually deleting anything. Requires a valid route to the integrated Docker registry (see --registry-url).")
148158
cmd.Flags().BoolVar(opts.AllImages, "all", *opts.AllImages, "Include images that were imported from external registries as candidates for pruning. If pruned, all the mirrored objects associated with them will also be removed from the integrated registry.")
149-
cmd.Flags().DurationVar(opts.KeepYoungerThan, "keep-younger-than", *opts.KeepYoungerThan, "Specify the minimum age of an image for it to be considered a candidate for pruning.")
159+
cmd.Flags().DurationVar(opts.KeepYoungerThan, "keep-younger-than", *opts.KeepYoungerThan, "Specify the minimum age of an image and its referrers for it to be considered a candidate for pruning.")
150160
cmd.Flags().IntVar(opts.KeepTagRevisions, "keep-tag-revisions", *opts.KeepTagRevisions, "Specify the number of image revisions for a tag in an image stream that will be preserved.")
151161
cmd.Flags().BoolVar(opts.PruneOverSizeLimit, "prune-over-size-limit", *opts.PruneOverSizeLimit, "Specify if images which are exceeding LimitRanges (see 'openshift.io/Image'), specified in the same namespace, should be considered for pruning. This flag cannot be combined with --keep-younger-than nor --keep-tag-revisions.")
152162
cmd.Flags().StringVar(&opts.CABundle, "certificate-authority", opts.CABundle, "The path to a certificate authority bundle to use when communicating with the managed Docker registries. Defaults to the certificate authority data from the current user's config file. It cannot be used together with --force-insecure.")
@@ -183,13 +193,13 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
183193
}
184194
}
185195
o.Out = out
196+
o.ErrOut = os.Stderr
186197

187198
clientConfig, err := f.ClientConfig()
188199
if err != nil {
189200
return err
190201
}
191202
o.ClientConfig = clientConfig
192-
193203
appsClient, buildClient, imageClient, kubeClient, err := getClients(f)
194204
if err != nil {
195205
return err
@@ -198,6 +208,12 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
198208
o.BuildClient = buildClient
199209
o.ImageClient = imageClient
200210
o.KubeClient = kubeClient
211+
o.DiscoveryClient = kubeClient.Discovery()
212+
213+
o.Timeout = clientConfig.Timeout
214+
if o.Timeout == 0 {
215+
o.Timeout = time.Duration(10 * time.Second)
216+
}
201217

202218
return nil
203219
}
@@ -261,11 +277,38 @@ func (o PruneImagesOptions) Run() error {
261277
return err
262278
}
263279

280+
allDSs, err := o.KubeClient.Extensions().DaemonSets(o.Namespace).List(metav1.ListOptions{})
281+
if err != nil {
282+
// TODO: remove in future (3.9) release
283+
if !kerrors.IsForbidden(err) {
284+
return err
285+
}
286+
fmt.Fprintf(o.ErrOut, "Failed to list daemonsets: %v\n - * Make sure to update clusterRoleBindings.\n", err)
287+
}
288+
289+
allDeployments, err := o.KubeClient.Extensions().Deployments(o.Namespace).List(metav1.ListOptions{})
290+
if err != nil {
291+
// TODO: remove in future (3.9) release
292+
if !kerrors.IsForbidden(err) {
293+
return err
294+
}
295+
fmt.Fprintf(o.ErrOut, "Failed to list deployments: %v\n - * Make sure to update clusterRoleBindings.\n", err)
296+
}
297+
264298
allDCs, err := o.AppsClient.DeploymentConfigs(o.Namespace).List(metav1.ListOptions{})
265299
if err != nil {
266300
return err
267301
}
268302

303+
allRSs, err := o.KubeClient.Extensions().ReplicaSets(o.Namespace).List(metav1.ListOptions{})
304+
if err != nil {
305+
// TODO: remove in future (3.9) release
306+
if !kerrors.IsForbidden(err) {
307+
return err
308+
}
309+
fmt.Fprintf(o.ErrOut, "Failed to list replicasets: %v\n - * Make sure to update clusterRoleBindings.\n", err)
310+
}
311+
269312
limitRangesList, err := o.KubeClient.Core().LimitRanges(o.Namespace).List(metav1.ListOptions{})
270313
if err != nil {
271314
return err
@@ -332,7 +375,10 @@ func (o PruneImagesOptions) Run() error {
332375
RCs: allRCs,
333376
BCs: allBCs,
334377
Builds: allBuilds,
378+
DSs: allDSs,
379+
Deployments: allDeployments,
335380
DCs: allDCs,
381+
RSs: allRSs,
336382
LimitRanges: limitRangesMap,
337383
DryRun: o.Confirm == false,
338384
RegistryClient: registryClient,
@@ -341,19 +387,20 @@ func (o PruneImagesOptions) Run() error {
341387
if o.Namespace != metav1.NamespaceAll {
342388
options.Namespace = o.Namespace
343389
}
344-
pruner, err := prune.NewPruner(options)
345-
if err != nil {
346-
return err
390+
pruner, errs := prune.NewPruner(options)
391+
if errs != nil {
392+
o.printGraphBuildErrors(errs)
393+
return fmt.Errorf("failed to build graph - no changes made")
347394
}
348395

349396
w := tabwriter.NewWriter(o.Out, 10, 4, 3, ' ', 0)
350397
defer w.Flush()
351398

352-
imageDeleter := &describingImageDeleter{w: w}
353-
imageStreamDeleter := &describingImageStreamDeleter{w: w}
354-
layerLinkDeleter := &describingLayerLinkDeleter{w: w}
355-
blobDeleter := &describingBlobDeleter{w: w}
356-
manifestDeleter := &describingManifestDeleter{w: w}
399+
imageDeleter := &describingImageDeleter{w: w, errOut: o.ErrOut}
400+
imageStreamDeleter := &describingImageStreamDeleter{w: w, errOut: o.ErrOut}
401+
layerLinkDeleter := &describingLayerLinkDeleter{w: w, errOut: o.ErrOut}
402+
blobDeleter := &describingBlobDeleter{w: w, errOut: o.ErrOut}
403+
manifestDeleter := &describingManifestDeleter{w: w, errOut: o.ErrOut}
357404

358405
if o.Confirm {
359406
imageDeleter.delegate = prune.NewImageDeleter(o.ImageClient)
@@ -362,18 +409,49 @@ func (o PruneImagesOptions) Run() error {
362409
blobDeleter.delegate = prune.NewBlobDeleter()
363410
manifestDeleter.delegate = prune.NewManifestDeleter()
364411
} else {
365-
fmt.Fprintln(os.Stderr, "Dry run enabled - no modifications will be made. Add --confirm to remove images")
412+
fmt.Fprintln(o.ErrOut, "Dry run enabled - no modifications will be made. Add --confirm to remove images")
366413
}
367414

368415
return pruner.Prune(imageDeleter, imageStreamDeleter, layerLinkDeleter, blobDeleter, manifestDeleter)
369416
}
370417

418+
func (o *PruneImagesOptions) printGraphBuildErrors(errs kutilerrors.Aggregate) {
419+
refErrors := []error{}
420+
421+
fmt.Fprintf(o.ErrOut, "Failed to build graph!\n")
422+
423+
for _, err := range errs.Errors() {
424+
if _, ok := err.(*prune.ErrBadReference); ok {
425+
refErrors = append(refErrors, err)
426+
} else {
427+
fmt.Fprintf(o.ErrOut, "%v\n", err)
428+
}
429+
}
430+
431+
if len(refErrors) > 0 {
432+
clientVersion, masterVersion, err := getClientAndMasterVersions(o.DiscoveryClient, o.Timeout)
433+
if err != nil {
434+
fmt.Fprintf(o.ErrOut, "Failed to get master API version: %v\n", err)
435+
}
436+
fmt.Fprintf(o.ErrOut, "\nThe following objects have invalid references:\n\n")
437+
for _, err := range refErrors {
438+
fmt.Fprintf(o.ErrOut, " %s\n", err)
439+
}
440+
fmt.Fprintf(o.ErrOut, "\nEither fix the references or delete the objects to make the pruner proceed.\n")
441+
442+
if masterVersion != nil && (clientVersion.Major != masterVersion.Major || clientVersion.Minor != masterVersion.Minor) {
443+
fmt.Fprintf(o.ErrOut, "Client version (%s) doesn't match master (%s), which may allow for different image references. Try to re-run this binary with the same version.\n", clientVersion, masterVersion)
444+
}
445+
}
446+
}
447+
371448
// describingImageStreamDeleter prints information about each image stream update.
372449
// If a delegate exists, its DeleteImageStream function is invoked prior to returning.
373450
type describingImageStreamDeleter struct {
374451
w io.Writer
375452
delegate prune.ImageStreamDeleter
376453
headerPrinted bool
454+
errOut io.Writer
377455
}
378456

379457
var _ prune.ImageStreamDeleter = &describingImageStreamDeleter{}
@@ -389,7 +467,7 @@ func (p *describingImageStreamDeleter) UpdateImageStream(stream *imageapi.ImageS
389467

390468
updatedStream, err := p.delegate.UpdateImageStream(stream)
391469
if err != nil {
392-
fmt.Fprintf(os.Stderr, "error updating image stream %s/%s to remove image references: %v\n", stream.Namespace, stream.Name, err)
470+
fmt.Fprintf(p.errOut, "error updating image stream %s/%s to remove image references: %v\n", stream.Namespace, stream.Name, err)
393471
}
394472

395473
return updatedStream, err
@@ -416,6 +494,7 @@ type describingImageDeleter struct {
416494
w io.Writer
417495
delegate prune.ImageDeleter
418496
headerPrinted bool
497+
errOut io.Writer
419498
}
420499

421500
var _ prune.ImageDeleter = &describingImageDeleter{}
@@ -435,7 +514,7 @@ func (p *describingImageDeleter) DeleteImage(image *imageapi.Image) error {
435514

436515
err := p.delegate.DeleteImage(image)
437516
if err != nil {
438-
fmt.Fprintf(os.Stderr, "error deleting image %s from server: %v\n", image.Name, err)
517+
fmt.Fprintf(p.errOut, "error deleting image %s from server: %v\n", image.Name, err)
439518
}
440519

441520
return err
@@ -447,6 +526,7 @@ type describingLayerLinkDeleter struct {
447526
w io.Writer
448527
delegate prune.LayerLinkDeleter
449528
headerPrinted bool
529+
errOut io.Writer
450530
}
451531

452532
var _ prune.LayerLinkDeleter = &describingLayerLinkDeleter{}
@@ -466,7 +546,7 @@ func (p *describingLayerLinkDeleter) DeleteLayerLink(registryClient *http.Client
466546

467547
err := p.delegate.DeleteLayerLink(registryClient, registryURL, repo, name)
468548
if err != nil {
469-
fmt.Fprintf(os.Stderr, "error deleting repository %s layer link %s from the registry: %v\n", repo, name, err)
549+
fmt.Fprintf(p.errOut, "error deleting repository %s layer link %s from the registry: %v\n", repo, name, err)
470550
}
471551

472552
return err
@@ -478,6 +558,7 @@ type describingBlobDeleter struct {
478558
w io.Writer
479559
delegate prune.BlobDeleter
480560
headerPrinted bool
561+
errOut io.Writer
481562
}
482563

483564
var _ prune.BlobDeleter = &describingBlobDeleter{}
@@ -497,7 +578,7 @@ func (p *describingBlobDeleter) DeleteBlob(registryClient *http.Client, registry
497578

498579
err := p.delegate.DeleteBlob(registryClient, registryURL, layer)
499580
if err != nil {
500-
fmt.Fprintf(os.Stderr, "error deleting blob %s from the registry: %v\n", layer, err)
581+
fmt.Fprintf(p.errOut, "error deleting blob %s from the registry: %v\n", layer, err)
501582
}
502583

503584
return err
@@ -510,6 +591,7 @@ type describingManifestDeleter struct {
510591
w io.Writer
511592
delegate prune.ManifestDeleter
512593
headerPrinted bool
594+
errOut io.Writer
513595
}
514596

515597
var _ prune.ManifestDeleter = &describingManifestDeleter{}
@@ -529,7 +611,7 @@ func (p *describingManifestDeleter) DeleteManifest(registryClient *http.Client,
529611

530612
err := p.delegate.DeleteManifest(registryClient, registryURL, repo, manifest)
531613
if err != nil {
532-
fmt.Fprintf(os.Stderr, "error deleting manifest %s from repository %s: %v\n", manifest, repo, err)
614+
fmt.Fprintf(p.errOut, "error deleting manifest %s from repository %s: %v\n", manifest, repo, err)
533615
}
534616

535617
return err
@@ -643,3 +725,48 @@ func getRegistryClient(clientConfig *restclient.Config, registryCABundle string,
643725
Transport: wrappedTransport,
644726
}, nil
645727
}
728+
729+
// getClientAndMasterVersions returns version info for client and master binaries. If it takes too long to get
730+
// a response from the master, timeout error is returned.
731+
func getClientAndMasterVersions(client discovery.DiscoveryInterface, timeout time.Duration) (clientVersion, masterVersion *version.Info, err error) {
732+
done := make(chan error)
733+
734+
go func() {
735+
defer close(done)
736+
737+
ocVersionBody, err := client.RESTClient().Get().AbsPath("/version/openshift").Do().Raw()
738+
switch {
739+
case err == nil:
740+
var ocServerInfo version.Info
741+
err = json.Unmarshal(ocVersionBody, &ocServerInfo)
742+
if err != nil && len(ocVersionBody) > 0 {
743+
done <- err
744+
return
745+
}
746+
masterVersion = &ocServerInfo
747+
748+
case kerrors.IsNotFound(err) || kerrors.IsUnauthorized(err) || kerrors.IsForbidden(err):
749+
default:
750+
done <- err
751+
return
752+
}
753+
}()
754+
755+
select {
756+
case err, closed := <-done:
757+
if strings.HasSuffix(fmt.Sprintf("%v", err), "connection refused") || clientcmd.IsConfigurationMissing(err) || kclientcmd.IsConfigurationInvalid(err) {
758+
return nil, nil, err
759+
}
760+
if closed && err != nil {
761+
return nil, nil, err
762+
}
763+
// do not block error printing if the master is busy
764+
case <-time.After(timeout):
765+
return nil, nil, fmt.Errorf("error: server took too long to respond with version information.")
766+
}
767+
768+
v := version.Get()
769+
clientVersion = &v
770+
771+
return
772+
}

‎pkg/oc/admin/prune/images_test.go

+139
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,39 @@
11
package prune
22

33
import (
4+
"bytes"
5+
"encoding/json"
6+
"flag"
7+
"fmt"
8+
"io"
49
"io/ioutil"
10+
"net/http"
11+
"os"
12+
"strings"
513
"testing"
614

15+
"k8s.io/apimachinery/pkg/runtime"
16+
"k8s.io/apimachinery/pkg/util/sets"
17+
fakediscovery "k8s.io/client-go/discovery/fake"
18+
"k8s.io/client-go/kubernetes/scheme"
19+
restclient "k8s.io/client-go/rest"
20+
kapi "k8s.io/kubernetes/pkg/api"
721
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
22+
"k8s.io/kubernetes/staging/src/k8s.io/apimachinery/pkg/util/diff"
23+
24+
restfake "k8s.io/client-go/rest/fake"
825

926
appsclient "github.com/openshift/origin/pkg/apps/generated/internalclientset/fake"
1027
buildclient "github.com/openshift/origin/pkg/build/generated/internalclientset/fake"
1128
imageclient "github.com/openshift/origin/pkg/image/generated/internalclientset/fake"
29+
"github.com/openshift/origin/pkg/image/prune/testutil"
30+
"github.com/openshift/origin/pkg/version"
1231
)
1332

33+
var logLevel = flag.Int("loglevel", 0, "")
34+
1435
func TestImagePruneNamespaced(t *testing.T) {
36+
flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
1537
kFake := fake.NewSimpleClientset()
1638
imageFake := imageclient.NewSimpleClientset()
1739
opts := &PruneImagesOptions{
@@ -22,6 +44,7 @@ func TestImagePruneNamespaced(t *testing.T) {
2244
ImageClient: imageFake.Image(),
2345
KubeClient: kFake,
2446
Out: ioutil.Discard,
47+
ErrOut: os.Stderr,
2548
}
2649

2750
if err := opts.Run(); err != nil {
@@ -46,3 +69,119 @@ func TestImagePruneNamespaced(t *testing.T) {
4669
}
4770
}
4871
}
72+
73+
func TestImagePruneErrOnBadReference(t *testing.T) {
74+
flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
75+
podBad := testutil.Pod("foo", "pod1", kapi.PodRunning, "invalid image reference")
76+
podGood := testutil.Pod("foo", "pod2", kapi.PodRunning, "example.com/foo/bar@sha256:0000000000000000000000000000000000000000000000000000000000000000")
77+
dep := testutil.Deployment("foo", "dep1", "do not blame me")
78+
bcBad := testutil.BC("foo", "bc1", "source", "ImageStreamImage", "foo", "bar:invalid-digest")
79+
80+
kFake := fake.NewSimpleClientset(&podBad, &podGood, &dep)
81+
imageFake := imageclient.NewSimpleClientset()
82+
fakeDiscovery := &fakeVersionDiscovery{
83+
masterVersion: version.Get(),
84+
}
85+
86+
switch d := kFake.Discovery().(type) {
87+
case *fakediscovery.FakeDiscovery:
88+
fakeDiscovery.FakeDiscovery = d
89+
default:
90+
t.Fatalf("unexpected discovery type: %T != %T", d, &fakediscovery.FakeDiscovery{})
91+
}
92+
93+
errBuf := bytes.NewBuffer(make([]byte, 0, 4096))
94+
opts := &PruneImagesOptions{
95+
AppsClient: appsclient.NewSimpleClientset().Apps(),
96+
BuildClient: buildclient.NewSimpleClientset(&bcBad).Build(),
97+
ImageClient: imageFake.Image(),
98+
KubeClient: kFake,
99+
DiscoveryClient: fakeDiscovery,
100+
Out: ioutil.Discard,
101+
ErrOut: errBuf,
102+
}
103+
104+
verifyOutput := func(out string, expectClientVersionMismatch bool) {
105+
t.Logf("pruner error output: %s\n", out)
106+
107+
badRefErrors := sets.NewString()
108+
for _, l := range strings.Split(out, "\n") {
109+
if strings.HasPrefix(l, " ") {
110+
badRefErrors.Insert(l[2:])
111+
}
112+
}
113+
expBadRefErrors := sets.NewString(
114+
`Pod[foo/pod1]: invalid docker image reference "invalid image reference": invalid reference format`,
115+
`BuildConfig[foo/bc1]: invalid ImageStreamImage reference "bar:invalid-digest": expected exactly one @ in the isimage name "bar:invalid-digest"`,
116+
`Deployment[foo/dep1]: invalid docker image reference "do not blame me": invalid reference format`)
117+
118+
if a, e := badRefErrors, expBadRefErrors; !a.Equal(e) {
119+
t.Fatalf("got unexpected invalid reference errors: %s", diff.ObjectDiff(a, e))
120+
}
121+
122+
if expectClientVersionMismatch {
123+
if msg := "client version"; !strings.Contains(strings.ToLower(out), msg) {
124+
t.Errorf("expected message %q is not contained in the output", msg)
125+
}
126+
} else {
127+
for _, msg := range []string{"failed to get master api version", "client version"} {
128+
if strings.Contains(strings.ToLower(out), msg) {
129+
t.Errorf("got unexpected message %q in the output", msg)
130+
}
131+
}
132+
}
133+
}
134+
135+
err := opts.Run()
136+
if err == nil {
137+
t.Fatal("Unexpected non-error")
138+
}
139+
140+
t.Logf("pruner error: %s\n", err)
141+
verifyOutput(errBuf.String(), false)
142+
143+
t.Logf("bump master version and try again")
144+
fakeDiscovery.masterVersion.Minor += "1"
145+
errBuf.Reset()
146+
err = opts.Run()
147+
if err == nil {
148+
t.Fatal("Unexpected non-error")
149+
}
150+
151+
t.Logf("pruner error: %s\n", err)
152+
verifyOutput(errBuf.String(), true)
153+
}
154+
155+
type fakeVersionDiscovery struct {
156+
*fakediscovery.FakeDiscovery
157+
masterVersion version.Info
158+
}
159+
160+
func (f *fakeVersionDiscovery) RESTClient() restclient.Interface {
161+
return &restfake.RESTClient{
162+
APIRegistry: kapi.Registry,
163+
NegotiatedSerializer: scheme.Codecs,
164+
Client: restfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
165+
if req.URL.Path != "/version/openshift" {
166+
return &http.Response{
167+
StatusCode: http.StatusNotFound,
168+
}, nil
169+
}
170+
header := http.Header{}
171+
header.Set("Content-Type", runtime.ContentTypeJSON)
172+
return &http.Response{
173+
StatusCode: http.StatusOK,
174+
Header: header,
175+
Body: objBody(&f.masterVersion),
176+
}, nil
177+
}),
178+
}
179+
}
180+
181+
func objBody(object interface{}) io.ReadCloser {
182+
output, err := json.MarshalIndent(object, "", "")
183+
if err != nil {
184+
panic(err)
185+
}
186+
return ioutil.NopCloser(bytes.NewReader([]byte(output)))
187+
}

‎pkg/oc/admin/top/graph.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,12 @@ func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList) {
7474
for tag, history := range stream.Status.Tags {
7575
for i := range history.Items {
7676
image := history.Items[i]
77-
n := imagegraph.FindImage(g, image.Image)
78-
if n == nil {
77+
imageNode := imagegraph.FindImage(g, image.Image)
78+
if imageNode == nil {
7979
glog.V(2).Infof("Unable to find image %q in graph (from tag=%q, dockerImageReference=%s)",
8080
history.Items[i].Image, tag, image.DockerImageReference)
8181
continue
8282
}
83-
imageNode := n.(*imagegraph.ImageNode)
8483
glog.V(4).Infof("Adding edge from %q to %q", imageStreamNode.UniqueName(), imageNode.UniqueName())
8584
edgeKind := ImageStreamImageEdgeKind
8685
if i > 1 {

‎test/testdata/bootstrappolicy/bootstrap_cluster_roles.yaml

+21
Original file line numberDiff line numberDiff line change
@@ -1871,6 +1871,27 @@ items:
18711871
verbs:
18721872
- get
18731873
- list
1874+
- apiGroups:
1875+
- extensions
1876+
resources:
1877+
- daemonsets
1878+
verbs:
1879+
- get
1880+
- list
1881+
- apiGroups:
1882+
- extensions
1883+
resources:
1884+
- deployments
1885+
verbs:
1886+
- get
1887+
- list
1888+
- apiGroups:
1889+
- extensions
1890+
resources:
1891+
- replicasets
1892+
verbs:
1893+
- get
1894+
- list
18741895
- apiGroups:
18751896
- ""
18761897
- image.openshift.io

‎test/testdata/bootstrappolicy/bootstrap_policy_file.yaml

+24
Original file line numberDiff line numberDiff line change
@@ -2041,6 +2041,30 @@ items:
20412041
verbs:
20422042
- get
20432043
- list
2044+
- apiGroups:
2045+
- extensions
2046+
attributeRestrictions: null
2047+
resources:
2048+
- daemonsets
2049+
verbs:
2050+
- get
2051+
- list
2052+
- apiGroups:
2053+
- extensions
2054+
attributeRestrictions: null
2055+
resources:
2056+
- deployments
2057+
verbs:
2058+
- get
2059+
- list
2060+
- apiGroups:
2061+
- extensions
2062+
attributeRestrictions: null
2063+
resources:
2064+
- replicasets
2065+
verbs:
2066+
- get
2067+
- list
20442068
- apiGroups:
20452069
- ""
20462070
- image.openshift.io

0 commit comments

Comments
 (0)
Please sign in to comment.