Browse code

Merge pull request #9481 from soltysh/card425_prune

Merged by openshift-bot

OpenShift Bot authored on 2016/07/06 22:54:27
Showing 25 changed files
... ...
@@ -1931,6 +1931,7 @@ _oadm_prune_images()
1931 1931
     flags+=("--confirm")
1932 1932
     flags+=("--keep-tag-revisions=")
1933 1933
     flags+=("--keep-younger-than=")
1934
+    flags+=("--prune-over-size-limit")
1934 1935
     flags+=("--registry-url=")
1935 1936
     flags+=("--api-version=")
1936 1937
     flags+=("--as=")
... ...
@@ -5881,6 +5881,7 @@ _oc_adm_prune_images()
5881 5881
     flags+=("--confirm")
5882 5882
     flags+=("--keep-tag-revisions=")
5883 5883
     flags+=("--keep-younger-than=")
5884
+    flags+=("--prune-over-size-limit")
5884 5885
     flags+=("--registry-url=")
5885 5886
     flags+=("--api-version=")
5886 5887
     flags+=("--as=")
... ...
@@ -2749,6 +2749,7 @@ _openshift_admin_prune_images()
2749 2749
     flags+=("--confirm")
2750 2750
     flags+=("--keep-tag-revisions=")
2751 2751
     flags+=("--keep-younger-than=")
2752
+    flags+=("--prune-over-size-limit")
2752 2753
     flags+=("--registry-url=")
2753 2754
     flags+=("--api-version=")
2754 2755
     flags+=("--as=")
... ...
@@ -10029,6 +10030,7 @@ _openshift_cli_adm_prune_images()
10029 10029
     flags+=("--confirm")
10030 10030
     flags+=("--keep-tag-revisions=")
10031 10031
     flags+=("--keep-younger-than=")
10032
+    flags+=("--prune-over-size-limit")
10032 10033
     flags+=("--registry-url=")
10033 10034
     flags+=("--api-version=")
10034 10035
     flags+=("--as=")
... ...
@@ -2092,6 +2092,7 @@ _oadm_prune_images()
2092 2092
     flags+=("--confirm")
2093 2093
     flags+=("--keep-tag-revisions=")
2094 2094
     flags+=("--keep-younger-than=")
2095
+    flags+=("--prune-over-size-limit")
2095 2096
     flags+=("--registry-url=")
2096 2097
     flags+=("--api-version=")
2097 2098
     flags+=("--as=")
... ...
@@ -6042,6 +6042,7 @@ _oc_adm_prune_images()
6042 6042
     flags+=("--confirm")
6043 6043
     flags+=("--keep-tag-revisions=")
6044 6044
     flags+=("--keep-younger-than=")
6045
+    flags+=("--prune-over-size-limit")
6045 6046
     flags+=("--registry-url=")
6046 6047
     flags+=("--api-version=")
6047 6048
     flags+=("--as=")
... ...
@@ -2910,6 +2910,7 @@ _openshift_admin_prune_images()
2910 2910
     flags+=("--confirm")
2911 2911
     flags+=("--keep-tag-revisions=")
2912 2912
     flags+=("--keep-younger-than=")
2913
+    flags+=("--prune-over-size-limit")
2913 2914
     flags+=("--registry-url=")
2914 2915
     flags+=("--api-version=")
2915 2916
     flags+=("--as=")
... ...
@@ -10190,6 +10191,7 @@ _openshift_cli_adm_prune_images()
10190 10190
     flags+=("--confirm")
10191 10191
     flags+=("--keep-tag-revisions=")
10192 10192
     flags+=("--keep-younger-than=")
10193
+    flags+=("--prune-over-size-limit")
10193 10194
     flags+=("--registry-url=")
10194 10195
     flags+=("--api-version=")
10195 10196
     flags+=("--as=")
... ...
@@ -523,6 +523,13 @@ Remove unreferenced images
523 523
 
524 524
   # To actually perform the prune operation, the confirm flag must be appended
525 525
   oadm prune images --keep-tag-revisions=3 --keep-younger-than=60m --confirm
526
+
527
+  # See, what the prune command would delete if we're interested in removing images
528
+  # exceeding currently set LimitRanges ('openshift.io/Image')
529
+  oadm prune images --prune-over-size-limit
530
+
531
+  # To actually perform the prune operation, the confirm flag must be appended
532
+  oadm prune images --prune-over-size-limit --confirm
526 533
 ----
527 534
 ====
528 535
 
... ...
@@ -523,6 +523,13 @@ Remove unreferenced images
523 523
 
524 524
   # To actually perform the prune operation, the confirm flag must be appended
525 525
   oc adm prune images --keep-tag-revisions=3 --keep-younger-than=60m --confirm
526
+
527
+  # See, what the prune command would delete if we're interested in removing images
528
+  # exceeding currently set LimitRanges ('openshift.io/Image')
529
+  oc adm prune images --prune-over-size-limit
530
+
531
+  # To actually perform the prune operation, the confirm flag must be appended
532
+  oc adm prune images --prune-over-size-limit --confirm
526 533
 ----
527 534
 ====
528 535
 
... ...
@@ -42,6 +42,10 @@ images.
42 42
     Specify the minimum age of an image for it to be considered a candidate for pruning.
43 43
 
44 44
 .PP
45
+\fB\-\-prune\-over\-size\-limit\fP=false
46
+    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.
47
+
48
+.PP
45 49
 \fB\-\-registry\-url\fP=""
46 50
     The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster\-internal URL) but you do have an alternative route that works.
47 51
 
... ...
@@ -120,6 +124,13 @@ images.
120 120
   # To actually perform the prune operation, the confirm flag must be appended
121 121
   oadm prune images \-\-keep\-tag\-revisions=3 \-\-keep\-younger\-than=60m \-\-confirm
122 122
 
123
+  # See, what the prune command would delete if we're interested in removing images
124
+  # exceeding currently set LimitRanges ('openshift.io/Image')
125
+  oadm prune images \-\-prune\-over\-size\-limit
126
+
127
+  # To actually perform the prune operation, the confirm flag must be appended
128
+  oadm prune images \-\-prune\-over\-size\-limit \-\-confirm
129
+
123 130
 .fi
124 131
 .RE
125 132
 
... ...
@@ -42,6 +42,10 @@ images.
42 42
     Specify the minimum age of an image for it to be considered a candidate for pruning.
43 43
 
44 44
 .PP
45
+\fB\-\-prune\-over\-size\-limit\fP=false
46
+    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.
47
+
48
+.PP
45 49
 \fB\-\-registry\-url\fP=""
46 50
     The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster\-internal URL) but you do have an alternative route that works.
47 51
 
... ...
@@ -120,6 +124,13 @@ images.
120 120
   # To actually perform the prune operation, the confirm flag must be appended
121 121
   oc adm prune images \-\-keep\-tag\-revisions=3 \-\-keep\-younger\-than=60m \-\-confirm
122 122
 
123
+  # See, what the prune command would delete if we're interested in removing images
124
+  # exceeding currently set LimitRanges ('openshift.io/Image')
125
+  oc adm prune images \-\-prune\-over\-size\-limit
126
+
127
+  # To actually perform the prune operation, the confirm flag must be appended
128
+  oc adm prune images \-\-prune\-over\-size\-limit \-\-confirm
129
+
123 130
 .fi
124 131
 .RE
125 132
 
... ...
@@ -42,6 +42,10 @@ images.
42 42
     Specify the minimum age of an image for it to be considered a candidate for pruning.
43 43
 
44 44
 .PP
45
+\fB\-\-prune\-over\-size\-limit\fP=false
46
+    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.
47
+
48
+.PP
45 49
 \fB\-\-registry\-url\fP=""
46 50
     The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster\-internal URL) but you do have an alternative route that works.
47 51
 
... ...
@@ -120,6 +124,13 @@ images.
120 120
   # To actually perform the prune operation, the confirm flag must be appended
121 121
   openshift admin prune images \-\-keep\-tag\-revisions=3 \-\-keep\-younger\-than=60m \-\-confirm
122 122
 
123
+  # See, what the prune command would delete if we're interested in removing images
124
+  # exceeding currently set LimitRanges ('openshift.io/Image')
125
+  openshift admin prune images \-\-prune\-over\-size\-limit
126
+
127
+  # To actually perform the prune operation, the confirm flag must be appended
128
+  openshift admin prune images \-\-prune\-over\-size\-limit \-\-confirm
129
+
123 130
 .fi
124 131
 .RE
125 132
 
... ...
@@ -42,6 +42,10 @@ images.
42 42
     Specify the minimum age of an image for it to be considered a candidate for pruning.
43 43
 
44 44
 .PP
45
+\fB\-\-prune\-over\-size\-limit\fP=false
46
+    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.
47
+
48
+.PP
45 49
 \fB\-\-registry\-url\fP=""
46 50
     The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster\-internal URL) but you do have an alternative route that works.
47 51
 
... ...
@@ -120,6 +124,13 @@ images.
120 120
   # To actually perform the prune operation, the confirm flag must be appended
121 121
   openshift cli adm prune images \-\-keep\-tag\-revisions=3 \-\-keep\-younger\-than=60m \-\-confirm
122 122
 
123
+  # See, what the prune command would delete if we're interested in removing images
124
+  # exceeding currently set LimitRanges ('openshift.io/Image')
125
+  openshift cli adm prune images \-\-prune\-over\-size\-limit
126
+
127
+  # To actually perform the prune operation, the confirm flag must be appended
128
+  openshift cli adm prune images \-\-prune\-over\-size\-limit \-\-confirm
129
+
123 130
 .fi
124 131
 .RE
125 132
 
... ...
@@ -3,37 +3,58 @@ package prune
3 3
 import (
4 4
 	"time"
5 5
 
6
+	"github.com/golang/glog"
7
+
6 8
 	buildapi "github.com/openshift/origin/pkg/build/api"
9
+	"github.com/openshift/origin/pkg/client"
7 10
 )
8 11
 
9
-// PruneFunc is a function that is invoked for each item during Prune
10
-type PruneFunc func(build *buildapi.Build) error
12
+type Pruner interface {
13
+	// Prune is responsible for actual removal of builds identified as candidates
14
+	// for pruning based on pruning algorithm.
15
+	Prune(deleter BuildDeleter) error
16
+}
11 17
 
12
-type PruneTasker interface {
13
-	// PruneTask is an object that knows how to execute a single iteration of a Prune
14
-	PruneTask() error
18
+// BuildDeleter knows how to delete builds from OpenShift.
19
+type BuildDeleter interface {
20
+	// DeleteBuild removes the build from OpenShift's storage.
21
+	DeleteBuild(build *buildapi.Build) error
15 22
 }
16 23
 
17
-// pruneTask is an object that knows how to prune a data set
18
-type pruneTask struct {
24
+// pruner is an object that knows how to prune a data set
25
+type pruner struct {
19 26
 	resolver Resolver
20
-	handler  PruneFunc
21 27
 }
22 28
 
23
-// NewPruneTasker returns a PruneTasker over specified data using specified flags
24
-// keepYoungerThan will filter out all objects from prune data set that are younger than the specified time duration
25
-// orphans if true will include inactive orphan builds in candidate prune set
26
-// keepComplete is per BuildConfig how many of the most recent builds should be preserved
27
-// keepFailed is per BuildConfig how many of the most recent failed builds should be preserved
28
-func NewPruneTasker(buildConfigs []*buildapi.BuildConfig, builds []*buildapi.Build, keepYoungerThan time.Duration, orphans bool, keepComplete int, keepFailed int, handler PruneFunc) PruneTasker {
29
+var _ Pruner = &pruner{}
30
+
31
+// PrunerOptions contains the fields used to initialize a new Pruner.
32
+type PrunerOptions struct {
33
+	// KeepYoungerThan indicates the minimum age a BuildConfig must be to be a
34
+	// candidate for pruning.
35
+	KeepYoungerThan time.Duration
36
+	// Orphans if true will include inactive orphan builds in candidate prune set
37
+	Orphans bool
38
+	// KeepComplete is per BuildConfig how many of the most recent builds should be preserved
39
+	KeepComplete int
40
+	// KeepFailed is per BuildConfig how many of the most recent failed builds should be preserved
41
+	KeepFailed int
42
+	// BuildConfigs is the entire list of buildconfigs across all namespaces in the cluster.
43
+	BuildConfigs []*buildapi.BuildConfig
44
+	// Builds is the entire list of builds across all namespaces in the cluster.
45
+	Builds []*buildapi.Build
46
+}
47
+
48
+// NewPruner returns a Pruner over specified data using specified options.
49
+func NewPruner(options PrunerOptions) Pruner {
29 50
 	filter := &andFilter{
30
-		filterPredicates: []FilterPredicate{NewFilterBeforePredicate(keepYoungerThan)},
51
+		filterPredicates: []FilterPredicate{NewFilterBeforePredicate(options.KeepYoungerThan)},
31 52
 	}
32
-	builds = filter.Filter(builds)
33
-	dataSet := NewDataSet(buildConfigs, builds)
53
+	builds := filter.Filter(options.Builds)
54
+	dataSet := NewDataSet(options.BuildConfigs, builds)
34 55
 
35 56
 	resolvers := []Resolver{}
36
-	if orphans {
57
+	if options.Orphans {
37 58
 		inactiveBuildStatus := []buildapi.BuildPhase{
38 59
 			buildapi.BuildPhaseCancelled,
39 60
 			buildapi.BuildPhaseComplete,
... ...
@@ -42,24 +63,42 @@ func NewPruneTasker(buildConfigs []*buildapi.BuildConfig, builds []*buildapi.Bui
42 42
 		}
43 43
 		resolvers = append(resolvers, NewOrphanBuildResolver(dataSet, inactiveBuildStatus))
44 44
 	}
45
-	resolvers = append(resolvers, NewPerBuildConfigResolver(dataSet, keepComplete, keepFailed))
46
-	return &pruneTask{
45
+	resolvers = append(resolvers, NewPerBuildConfigResolver(dataSet, options.KeepComplete, options.KeepFailed))
46
+
47
+	return &pruner{
47 48
 		resolver: &mergeResolver{resolvers: resolvers},
48
-		handler:  handler,
49 49
 	}
50 50
 }
51 51
 
52
-// PruneTask will visit each item in the prunable set and invoke the associated handler
53
-func (t *pruneTask) PruneTask() error {
54
-	builds, err := t.resolver.Resolve()
52
+// Prune will visit each item in the prunable set and invoke the associated BuildDeleter.
53
+func (p *pruner) Prune(deleter BuildDeleter) error {
54
+	builds, err := p.resolver.Resolve()
55 55
 	if err != nil {
56 56
 		return err
57 57
 	}
58 58
 	for _, build := range builds {
59
-		err = t.handler(build)
60
-		if err != nil {
59
+		if err := deleter.DeleteBuild(build); err != nil {
61 60
 			return err
62 61
 		}
63 62
 	}
64 63
 	return nil
65 64
 }
65
+
66
+// buildDeleter removes a build from OpenShift.
67
+type buildDeleter struct {
68
+	builds client.BuildsNamespacer
69
+}
70
+
71
+var _ BuildDeleter = &buildDeleter{}
72
+
73
+// NewBuildDeleter creates a new buildDeleter.
74
+func NewBuildDeleter(builds client.BuildsNamespacer) BuildDeleter {
75
+	return &buildDeleter{
76
+		builds: builds,
77
+	}
78
+}
79
+
80
+func (p *buildDeleter) DeleteBuild(build *buildapi.Build) error {
81
+	glog.V(4).Infof("Deleting build %q", build.Name)
82
+	return p.builds.Builds(build.Namespace).Delete(build.Name)
83
+}
... ...
@@ -11,17 +11,19 @@ import (
11 11
 	buildapi "github.com/openshift/origin/pkg/build/api"
12 12
 )
13 13
 
14
-type mockPruneRecorder struct {
14
+type mockDeleteRecorder struct {
15 15
 	set sets.String
16 16
 	err error
17 17
 }
18 18
 
19
-func (m *mockPruneRecorder) Handler(build *buildapi.Build) error {
19
+var _ BuildDeleter = &mockDeleteRecorder{}
20
+
21
+func (m *mockDeleteRecorder) DeleteBuild(build *buildapi.Build) error {
20 22
 	m.set.Insert(build.Name)
21 23
 	return m.err
22 24
 }
23 25
 
24
-func (m *mockPruneRecorder) Verify(t *testing.T, expected sets.String) {
26
+func (m *mockDeleteRecorder) Verify(t *testing.T, expected sets.String) {
25 27
 	if len(m.set) != len(expected) || !m.set.HasAll(expected.List()...) {
26 28
 		expectedValues := expected.List()
27 29
 		actualValues := m.set.List()
... ...
@@ -84,14 +86,25 @@ func TestPruneTask(t *testing.T) {
84 84
 				}
85 85
 			}
86 86
 			expectedBuilds, err := resolver.Resolve()
87
+			if err != nil {
88
+				t.Errorf("Unexpected error %v", err)
89
+			}
87 90
 			for _, build := range expectedBuilds {
88 91
 				expectedValues.Insert(build.Name)
89 92
 			}
90 93
 
91
-			recorder := &mockPruneRecorder{set: sets.String{}}
92
-			task := NewPruneTasker(buildConfigs, builds, keepYoungerThan, orphans, keepComplete, keepFailed, recorder.Handler)
93
-			err = task.PruneTask()
94
-			if err != nil {
94
+			recorder := &mockDeleteRecorder{set: sets.String{}}
95
+
96
+			options := PrunerOptions{
97
+				KeepYoungerThan: keepYoungerThan,
98
+				Orphans:         orphans,
99
+				KeepComplete:    keepComplete,
100
+				KeepFailed:      keepFailed,
101
+				BuildConfigs:    buildConfigs,
102
+				Builds:          builds,
103
+			}
104
+			pruner := NewPruner(options)
105
+			if err := pruner.Prune(recorder); err != nil {
95 106
 				t.Errorf("Unexpected error %v", err)
96 107
 			}
97 108
 			recorder.Verify(t, expectedValues)
... ...
@@ -7,14 +7,14 @@ import (
7 7
 	"text/tabwriter"
8 8
 	"time"
9 9
 
10
-	"github.com/golang/glog"
11 10
 	"github.com/spf13/cobra"
12 11
 
13 12
 	kapi "k8s.io/kubernetes/pkg/api"
14
-	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
13
+	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
15 14
 
16 15
 	buildapi "github.com/openshift/origin/pkg/build/api"
17 16
 	"github.com/openshift/origin/pkg/build/prune"
17
+	"github.com/openshift/origin/pkg/client"
18 18
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
19 19
 )
20 20
 
... ...
@@ -34,98 +34,151 @@ By default, the prune operation performs a dry run making no changes to internal
34 34
   %[1]s %[2]s --orphans --confirm`
35 35
 )
36 36
 
37
-type pruneBuildsConfig struct {
37
+// PruneBuildsOptions holds all the required options for pruning builds.
38
+type PruneBuildsOptions struct {
38 39
 	Confirm         bool
39
-	KeepYoungerThan time.Duration
40 40
 	Orphans         bool
41
+	KeepYoungerThan time.Duration
41 42
 	KeepComplete    int
42 43
 	KeepFailed      int
44
+
45
+	Pruner prune.Pruner
46
+	Client client.Interface
47
+	Out    io.Writer
43 48
 }
44 49
 
50
+// NewCmdPruneBuilds implements the OpenShift cli prune builds command.
45 51
 func NewCmdPruneBuilds(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command {
46
-	cfg := &pruneBuildsConfig{
52
+	opts := &PruneBuildsOptions{
47 53
 		Confirm:         false,
48
-		KeepYoungerThan: 60 * time.Minute,
49 54
 		Orphans:         false,
55
+		KeepYoungerThan: 60 * time.Minute,
50 56
 		KeepComplete:    5,
51 57
 		KeepFailed:      1,
52 58
 	}
53
-
54 59
 	cmd := &cobra.Command{
55 60
 		Use:     name,
56 61
 		Short:   "Remove old completed and failed builds",
57 62
 		Long:    buildsLongDesc,
58 63
 		Example: fmt.Sprintf(buildsExample, parentName, name),
59
-
60 64
 		Run: func(cmd *cobra.Command, args []string) {
61
-			if len(args) > 0 {
62
-				glog.Fatalf("No arguments are allowed to this command")
63
-			}
64
-
65
-			osClient, _, err := f.Clients()
66
-			if err != nil {
67
-				cmdutil.CheckErr(err)
68
-			}
69
-
70
-			buildConfigList, err := osClient.BuildConfigs(kapi.NamespaceAll).List(kapi.ListOptions{})
71
-			if err != nil {
72
-				cmdutil.CheckErr(err)
73
-			}
74
-
75
-			buildList, err := osClient.Builds(kapi.NamespaceAll).List(kapi.ListOptions{})
76
-			if err != nil {
77
-				cmdutil.CheckErr(err)
78
-			}
79
-
80
-			buildConfigs := []*buildapi.BuildConfig{}
81
-			for i := range buildConfigList.Items {
82
-				buildConfigs = append(buildConfigs, &buildConfigList.Items[i])
83
-			}
84
-
85
-			builds := []*buildapi.Build{}
86
-			for i := range buildList.Items {
87
-				builds = append(builds, &buildList.Items[i])
88
-			}
89
-
90
-			var buildPruneFunc prune.PruneFunc
91
-
92
-			w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0)
93
-			defer w.Flush()
94
-
95
-			describingPruneBuildFunc := func(build *buildapi.Build) error {
96
-				fmt.Fprintf(w, "%s\t%s\n", build.Namespace, build.Name)
97
-				return nil
98
-			}
99
-
100
-			switch cfg.Confirm {
101
-			case true:
102
-				buildPruneFunc = func(build *buildapi.Build) error {
103
-					describingPruneBuildFunc(build)
104
-					err := osClient.Builds(build.Namespace).Delete(build.Name)
105
-					if err != nil {
106
-						return err
107
-					}
108
-					return nil
109
-				}
110
-			default:
111
-				fmt.Fprintln(os.Stderr, "Dry run enabled - no modifications will be made. Add --confirm to remove builds")
112
-				buildPruneFunc = describingPruneBuildFunc
113
-			}
114
-
115
-			fmt.Fprintln(w, "NAMESPACE\tNAME")
116
-			pruneTask := prune.NewPruneTasker(buildConfigs, builds, cfg.KeepYoungerThan, cfg.Orphans, cfg.KeepComplete, cfg.KeepFailed, buildPruneFunc)
117
-			err = pruneTask.PruneTask()
118
-			if err != nil {
119
-				cmdutil.CheckErr(err)
120
-			}
65
+			kcmdutil.CheckErr(opts.Complete(f, cmd, args, out))
66
+			kcmdutil.CheckErr(opts.Validate())
67
+			kcmdutil.CheckErr(opts.Run())
121 68
 		},
122 69
 	}
123 70
 
124
-	cmd.Flags().BoolVar(&cfg.Confirm, "confirm", cfg.Confirm, "Specify that build pruning should proceed. Defaults to false, displaying what would be deleted but not actually deleting anything.")
125
-	cmd.Flags().BoolVar(&cfg.Orphans, "orphans", cfg.Orphans, "Prune all builds whose associated BuildConfig no longer exists and whose status is complete, failed, error, or cancelled.")
126
-	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.")
127
-	cmd.Flags().IntVar(&cfg.KeepComplete, "keep-complete", cfg.KeepComplete, "Per BuildConfig, specify the number of builds whose status is complete that will be preserved.")
128
-	cmd.Flags().IntVar(&cfg.KeepFailed, "keep-failed", cfg.KeepFailed, "Per BuildConfig, specify the number of builds whose status is failed, error, or cancelled that will be preserved.")
71
+	cmd.Flags().BoolVar(&opts.Confirm, "confirm", opts.Confirm, "Specify that build pruning should proceed. Defaults to false, displaying what would be deleted but not actually deleting anything.")
72
+	cmd.Flags().BoolVar(&opts.Orphans, "orphans", opts.Orphans, "Prune all builds whose associated BuildConfig no longer exists and whose status is complete, failed, error, or cancelled.")
73
+	cmd.Flags().DurationVar(&opts.KeepYoungerThan, "keep-younger-than", opts.KeepYoungerThan, "Specify the minimum age of a Build for it to be considered a candidate for pruning.")
74
+	cmd.Flags().IntVar(&opts.KeepComplete, "keep-complete", opts.KeepComplete, "Per BuildConfig, specify the number of builds whose status is complete that will be preserved.")
75
+	cmd.Flags().IntVar(&opts.KeepFailed, "keep-failed", opts.KeepFailed, "Per BuildConfig, specify the number of builds whose status is failed, error, or cancelled that will be preserved.")
129 76
 
130 77
 	return cmd
131 78
 }
79
+
80
+// Complete turns a partially defined PruneBuildsOptions into a solvent structure
81
+// which can be validated and used for pruning builds.
82
+func (o *PruneBuildsOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
83
+	if len(args) > 0 {
84
+		return kcmdutil.UsageError(cmd, "no arguments are allowed to this command")
85
+	}
86
+
87
+	o.Out = out
88
+
89
+	osClient, _, err := f.Clients()
90
+	if err != nil {
91
+		return err
92
+	}
93
+	o.Client = osClient
94
+
95
+	buildConfigList, err := osClient.BuildConfigs(kapi.NamespaceAll).List(kapi.ListOptions{})
96
+	if err != nil {
97
+		return err
98
+	}
99
+	buildConfigs := []*buildapi.BuildConfig{}
100
+	for i := range buildConfigList.Items {
101
+		buildConfigs = append(buildConfigs, &buildConfigList.Items[i])
102
+	}
103
+
104
+	buildList, err := osClient.Builds(kapi.NamespaceAll).List(kapi.ListOptions{})
105
+	if err != nil {
106
+		return err
107
+	}
108
+	builds := []*buildapi.Build{}
109
+	for i := range buildList.Items {
110
+		builds = append(builds, &buildList.Items[i])
111
+	}
112
+
113
+	options := prune.PrunerOptions{
114
+		KeepYoungerThan: o.KeepYoungerThan,
115
+		Orphans:         o.Orphans,
116
+		KeepComplete:    o.KeepComplete,
117
+		KeepFailed:      o.KeepFailed,
118
+		BuildConfigs:    buildConfigs,
119
+		Builds:          builds,
120
+	}
121
+
122
+	o.Pruner = prune.NewPruner(options)
123
+
124
+	return nil
125
+}
126
+
127
+// Validate ensures that a PruneBuildsOptions is valid and can be used to execute pruning.
128
+func (o PruneBuildsOptions) Validate() error {
129
+	if o.KeepYoungerThan < 0 {
130
+		return fmt.Errorf("--keep-younger-than must be greater than or equal to 0")
131
+	}
132
+	if o.KeepComplete < 0 {
133
+		return fmt.Errorf("--keep-complete must be greater than or equal to 0")
134
+	}
135
+	if o.KeepFailed < 0 {
136
+		return fmt.Errorf("--keep-failed must be greater than or equal to 0")
137
+	}
138
+	return nil
139
+}
140
+
141
+// Run contains all the necessary functionality for the OpenShift cli prune builds command.
142
+func (o PruneBuildsOptions) Run() error {
143
+	w := tabwriter.NewWriter(o.Out, 10, 4, 3, ' ', 0)
144
+	defer w.Flush()
145
+
146
+	buildDeleter := &describingBuildDeleter{w: w}
147
+
148
+	if o.Confirm {
149
+		buildDeleter.delegate = prune.NewBuildDeleter(o.Client)
150
+	} else {
151
+		fmt.Fprintln(os.Stderr, "Dry run enabled - no modifications will be made. Add --confirm to remove builds")
152
+	}
153
+
154
+	return o.Pruner.Prune(buildDeleter)
155
+}
156
+
157
+// describingBuildDeleter prints information about each build it removes.
158
+// If a delegate exists, its DeleteBuild function is invoked prior to returning.
159
+type describingBuildDeleter struct {
160
+	w             io.Writer
161
+	delegate      prune.BuildDeleter
162
+	headerPrinted bool
163
+}
164
+
165
+var _ prune.BuildDeleter = &describingBuildDeleter{}
166
+
167
+func (p *describingBuildDeleter) DeleteBuild(build *buildapi.Build) error {
168
+	if !p.headerPrinted {
169
+		p.headerPrinted = true
170
+		fmt.Fprintln(p.w, "NAMESPACE\tNAME")
171
+	}
172
+
173
+	fmt.Fprintf(p.w, "%s\t%s\n", build.Namespace, build.Name)
174
+
175
+	if p.delegate == nil {
176
+		return nil
177
+	}
178
+
179
+	if err := p.delegate.DeleteBuild(build); err != nil {
180
+		return err
181
+	}
182
+
183
+	return nil
184
+}
... ...
@@ -7,16 +7,15 @@ import (
7 7
 	"text/tabwriter"
8 8
 	"time"
9 9
 
10
-	"github.com/golang/glog"
11 10
 	"github.com/spf13/cobra"
12 11
 
13 12
 	kapi "k8s.io/kubernetes/pkg/api"
14
-	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
13
+	kclient "k8s.io/kubernetes/pkg/client/unversioned"
14
+	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
15 15
 
16 16
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
17 17
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
18 18
 	"github.com/openshift/origin/pkg/deploy/prune"
19
-	deployutil "github.com/openshift/origin/pkg/deploy/util"
20 19
 )
21 20
 
22 21
 const PruneDeploymentsRecommendedName = "deployments"
... ...
@@ -35,16 +34,22 @@ A --confirm flag is needed for changes to be effective.
35 35
   %[1]s %[2]s --keep-complete=1 --confirm`
36 36
 )
37 37
 
38
-type pruneDeploymentConfig struct {
38
+// PruneDeploymentsOptions holds all the required options for pruning deployments.
39
+type PruneDeploymentsOptions struct {
39 40
 	Confirm         bool
40
-	KeepYoungerThan time.Duration
41 41
 	Orphans         bool
42
+	KeepYoungerThan time.Duration
42 43
 	KeepComplete    int
43 44
 	KeepFailed      int
45
+
46
+	Pruner prune.Pruner
47
+	Client kclient.Interface
48
+	Out    io.Writer
44 49
 }
45 50
 
51
+// NewCmdPruneDeployments implements the OpenShift cli prune deployments command.
46 52
 func NewCmdPruneDeployments(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command {
47
-	cfg := &pruneDeploymentConfig{
53
+	opts := &PruneDeploymentsOptions{
48 54
 		Confirm:         false,
49 55
 		KeepYoungerThan: 60 * time.Minute,
50 56
 		KeepComplete:    5,
... ...
@@ -58,84 +63,123 @@ func NewCmdPruneDeployments(f *clientcmd.Factory, parentName, name string, out i
58 58
 		Example:    fmt.Sprintf(deploymentsExample, parentName, name),
59 59
 		SuggestFor: []string{"deployment", "deployments"},
60 60
 		Run: func(cmd *cobra.Command, args []string) {
61
-			if len(args) > 0 {
62
-				glog.Fatalf("No arguments are allowed to this command")
63
-			}
64
-
65
-			osClient, kclient, err := f.Clients()
66
-			if err != nil {
67
-				cmdutil.CheckErr(err)
68
-			}
69
-
70
-			deploymentConfigList, err := osClient.DeploymentConfigs(kapi.NamespaceAll).List(kapi.ListOptions{})
71
-			if err != nil {
72
-				cmdutil.CheckErr(err)
73
-			}
74
-
75
-			deploymentList, err := kclient.ReplicationControllers(kapi.NamespaceAll).List(kapi.ListOptions{})
76
-			if err != nil {
77
-				cmdutil.CheckErr(err)
78
-			}
79
-
80
-			deploymentConfigs := []*deployapi.DeploymentConfig{}
81
-			for i := range deploymentConfigList.Items {
82
-				deploymentConfigs = append(deploymentConfigs, &deploymentConfigList.Items[i])
83
-			}
84
-
85
-			deployments := []*kapi.ReplicationController{}
86
-			for i := range deploymentList.Items {
87
-				deployments = append(deployments, &deploymentList.Items[i])
88
-			}
89
-
90
-			var deploymentPruneFunc prune.PruneFunc
91
-
92
-			w := tabwriter.NewWriter(out, 10, 4, 3, ' ', 0)
93
-			defer w.Flush()
94
-
95
-			describingPruneDeploymentFunc := func(deployment *kapi.ReplicationController) error {
96
-				fmt.Fprintf(w, "%s\t%s\n", deployment.Namespace, deployment.Name)
97
-				return nil
98
-			}
99
-
100
-			switch cfg.Confirm {
101
-			case true:
102
-				deploymentPruneFunc = func(deployment *kapi.ReplicationController) error {
103
-					describingPruneDeploymentFunc(deployment)
104
-					// If the deployment is failed we need to remove its deployer pods, too.
105
-					if deployutil.DeploymentStatusFor(deployment) == deployapi.DeploymentStatusFailed {
106
-						dpSelector := deployutil.DeployerPodSelector(deployment.Name)
107
-						deployers, err := kclient.Pods(deployment.Namespace).List(kapi.ListOptions{LabelSelector: dpSelector})
108
-						if err != nil {
109
-							fmt.Fprintf(os.Stderr, "Cannot list deployer pods for %q: %v\n", deployment.Name, err)
110
-						} else {
111
-							for _, pod := range deployers.Items {
112
-								if err := kclient.Pods(pod.Namespace).Delete(pod.Name, nil); err != nil {
113
-									fmt.Fprintf(os.Stderr, "Cannot remove deployer pod %q: %v\n", pod.Name, err)
114
-								}
115
-							}
116
-						}
117
-					}
118
-					return kclient.ReplicationControllers(deployment.Namespace).Delete(deployment.Name)
119
-				}
120
-			default:
121
-				fmt.Fprintln(os.Stderr, "Dry run enabled - no modifications will be made. Add --confirm to remove deployments")
122
-				deploymentPruneFunc = describingPruneDeploymentFunc
123
-			}
124
-
125
-			fmt.Fprintln(w, "NAMESPACE\tNAME")
126
-			pruneTask := prune.NewPruneTasker(deploymentConfigs, deployments, cfg.KeepYoungerThan, cfg.Orphans, cfg.KeepComplete, cfg.KeepFailed, deploymentPruneFunc)
127
-			err = pruneTask.PruneTask()
128
-			if err != nil {
129
-				cmdutil.CheckErr(err)
130
-			}
61
+			kcmdutil.CheckErr(opts.Complete(f, cmd, args, out))
62
+			kcmdutil.CheckErr(opts.Validate())
63
+			kcmdutil.CheckErr(opts.Run())
131 64
 		},
132 65
 	}
133 66
 
134
-	cmd.Flags().BoolVar(&cfg.Confirm, "confirm", cfg.Confirm, "Specify that deployment pruning should proceed. Defaults to false, displaying what would be deleted but not actually deleting anything.")
135
-	cmd.Flags().BoolVar(&cfg.Orphans, "orphans", cfg.Orphans, "Prune all deployments where the associated DeploymentConfig no longer exists, the status is complete or failed, and the replica size is 0.")
136
-	cmd.Flags().DurationVar(&cfg.KeepYoungerThan, "keep-younger-than", cfg.KeepYoungerThan, "Specify the minimum age of a deployment for it to be considered a candidate for pruning.")
137
-	cmd.Flags().IntVar(&cfg.KeepComplete, "keep-complete", cfg.KeepComplete, "Per DeploymentConfig, specify the number of deployments whose status is complete that will be preserved whose replica size is 0.")
138
-	cmd.Flags().IntVar(&cfg.KeepFailed, "keep-failed", cfg.KeepFailed, "Per DeploymentConfig, specify the number of deployments whose status is failed that will be preserved whose replica size is 0.")
67
+	cmd.Flags().BoolVar(&opts.Confirm, "confirm", opts.Confirm, "Specify that deployment pruning should proceed. Defaults to false, displaying what would be deleted but not actually deleting anything.")
68
+	cmd.Flags().BoolVar(&opts.Orphans, "orphans", opts.Orphans, "Prune all deployments where the associated DeploymentConfig no longer exists, the status is complete or failed, and the replica size is 0.")
69
+	cmd.Flags().DurationVar(&opts.KeepYoungerThan, "keep-younger-than", opts.KeepYoungerThan, "Specify the minimum age of a deployment for it to be considered a candidate for pruning.")
70
+	cmd.Flags().IntVar(&opts.KeepComplete, "keep-complete", opts.KeepComplete, "Per DeploymentConfig, specify the number of deployments whose status is complete that will be preserved whose replica size is 0.")
71
+	cmd.Flags().IntVar(&opts.KeepFailed, "keep-failed", opts.KeepFailed, "Per DeploymentConfig, specify the number of deployments whose status is failed that will be preserved whose replica size is 0.")
139 72
 
140 73
 	return cmd
141 74
 }
75
+
76
+// Complete turns a partially defined PruneDeploymentsOptions into a solvent structure
77
+// which can be validated and used for pruning deployments.
78
+func (o *PruneDeploymentsOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
79
+	if len(args) > 0 {
80
+		return kcmdutil.UsageError(cmd, "no arguments are allowed to this command")
81
+	}
82
+
83
+	o.Out = out
84
+
85
+	osClient, kClient, err := f.Clients()
86
+	if err != nil {
87
+		return err
88
+	}
89
+	o.Client = kClient
90
+
91
+	deploymentConfigList, err := osClient.DeploymentConfigs(kapi.NamespaceAll).List(kapi.ListOptions{})
92
+	if err != nil {
93
+		return err
94
+	}
95
+	deploymentConfigs := []*deployapi.DeploymentConfig{}
96
+	for i := range deploymentConfigList.Items {
97
+		deploymentConfigs = append(deploymentConfigs, &deploymentConfigList.Items[i])
98
+	}
99
+
100
+	deploymentList, err := kClient.ReplicationControllers(kapi.NamespaceAll).List(kapi.ListOptions{})
101
+	if err != nil {
102
+		return err
103
+	}
104
+	deployments := []*kapi.ReplicationController{}
105
+	for i := range deploymentList.Items {
106
+		deployments = append(deployments, &deploymentList.Items[i])
107
+	}
108
+
109
+	options := prune.PrunerOptions{
110
+		KeepYoungerThan:   o.KeepYoungerThan,
111
+		Orphans:           o.Orphans,
112
+		KeepComplete:      o.KeepComplete,
113
+		KeepFailed:        o.KeepFailed,
114
+		DeploymentConfigs: deploymentConfigs,
115
+		Deployments:       deployments,
116
+	}
117
+
118
+	o.Pruner = prune.NewPruner(options)
119
+
120
+	return nil
121
+}
122
+
123
+// Validate ensures that a PruneDeploymentsOptions is valid and can be used to execute pruning.
124
+func (o PruneDeploymentsOptions) Validate() error {
125
+	if o.KeepYoungerThan < 0 {
126
+		return fmt.Errorf("--keep-younger-than must be greater than or equal to 0")
127
+	}
128
+	if o.KeepComplete < 0 {
129
+		return fmt.Errorf("--keep-complete must be greater than or equal to 0")
130
+	}
131
+	if o.KeepFailed < 0 {
132
+		return fmt.Errorf("--keep-failed must be greater than or equal to 0")
133
+	}
134
+	return nil
135
+}
136
+
137
+// Run contains all the necessary functionality for the OpenShift cli prune deployments command.
138
+func (o PruneDeploymentsOptions) Run() error {
139
+	w := tabwriter.NewWriter(o.Out, 10, 4, 3, ' ', 0)
140
+	defer w.Flush()
141
+
142
+	deploymentDeleter := &describingDeploymentDeleter{w: w}
143
+
144
+	if o.Confirm {
145
+		deploymentDeleter.delegate = prune.NewDeploymentDeleter(o.Client, o.Client)
146
+	} else {
147
+		fmt.Fprintln(os.Stderr, "Dry run enabled - no modifications will be made. Add --confirm to remove deployments")
148
+	}
149
+
150
+	return o.Pruner.Prune(deploymentDeleter)
151
+}
152
+
153
+// describingDeploymentDeleter prints information about each deployment it removes.
154
+// If a delegate exists, its DeleteDeployment function is invoked prior to returning.
155
+type describingDeploymentDeleter struct {
156
+	w             io.Writer
157
+	delegate      prune.DeploymentDeleter
158
+	headerPrinted bool
159
+}
160
+
161
+var _ prune.DeploymentDeleter = &describingDeploymentDeleter{}
162
+
163
+func (p *describingDeploymentDeleter) DeleteDeployment(deployment *kapi.ReplicationController) error {
164
+	if !p.headerPrinted {
165
+		p.headerPrinted = true
166
+		fmt.Fprintln(p.w, "NAMESPACE\tNAME")
167
+	}
168
+
169
+	fmt.Fprintf(p.w, "%s\t%s\n", deployment.Namespace, deployment.Name)
170
+
171
+	if p.delegate == nil {
172
+		return nil
173
+	}
174
+
175
+	if err := p.delegate.DeleteDeployment(deployment); err != nil {
176
+		return err
177
+	}
178
+
179
+	return nil
180
+}
... ...
@@ -7,6 +7,7 @@ import (
7 7
 	"io"
8 8
 	"io/ioutil"
9 9
 	"net/http"
10
+	"net/url"
10 11
 	"os"
11 12
 	"strings"
12 13
 	"text/tabwriter"
... ...
@@ -16,7 +17,7 @@ import (
16 16
 	kapi "k8s.io/kubernetes/pkg/api"
17 17
 	"k8s.io/kubernetes/pkg/client/restclient"
18 18
 	kclient "k8s.io/kubernetes/pkg/client/unversioned"
19
-	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
19
+	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
20 20
 	knet "k8s.io/kubernetes/pkg/util/net"
21 21
 
22 22
 	"github.com/openshift/origin/pkg/client"
... ...
@@ -44,29 +45,43 @@ images.`
44 44
   %[1]s %[2]s --keep-tag-revisions=3 --keep-younger-than=60m
45 45
 
46 46
   # To actually perform the prune operation, the confirm flag must be appended
47
-  %[1]s %[2]s --keep-tag-revisions=3 --keep-younger-than=60m --confirm`
48
-)
47
+  %[1]s %[2]s --keep-tag-revisions=3 --keep-younger-than=60m --confirm
49 48
 
50
-// PruneImagesOptions holds all the required options for prune images
51
-type PruneImagesOptions struct {
52
-	Pruner prune.ImageRegistryPruner
53
-	Client client.Interface
54
-	Out    io.Writer
49
+  # See, what the prune command would delete if we're interested in removing images
50
+  # exceeding currently set LimitRanges ('openshift.io/Image')
51
+  %[1]s %[2]s --prune-over-size-limit
55 52
 
56
-	Confirm          bool
57
-	KeepYoungerThan  time.Duration
58
-	KeepTagRevisions int
53
+  # To actually perform the prune operation, the confirm flag must be appended
54
+  %[1]s %[2]s --prune-over-size-limit --confirm`
55
+)
59 56
 
57
+var (
58
+	defaultKeepYoungerThan         = 60 * time.Minute
59
+	defaultKeepTagRevisions        = 3
60
+	defaultPruneImageOverSizeLimit = false
61
+)
62
+
63
+// PruneImagesOptions holds all the required options for pruning images.
64
+type PruneImagesOptions struct {
65
+	Confirm             bool
66
+	KeepYoungerThan     *time.Duration
67
+	KeepTagRevisions    *int
68
+	PruneOverSizeLimit  *bool
60 69
 	CABundle            string
61 70
 	RegistryUrlOverride string
71
+
72
+	Pruner prune.Pruner
73
+	Client client.Interface
74
+	Out    io.Writer
62 75
 }
63 76
 
64
-// NewCmdPruneImages implements the OpenShift cli prune images command
77
+// NewCmdPruneImages implements the OpenShift cli prune images command.
65 78
 func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command {
66 79
 	opts := &PruneImagesOptions{
67
-		Confirm:          false,
68
-		KeepYoungerThan:  60 * time.Minute,
69
-		KeepTagRevisions: 3,
80
+		Confirm:            false,
81
+		KeepYoungerThan:    &defaultKeepYoungerThan,
82
+		KeepTagRevisions:   &defaultKeepTagRevisions,
83
+		PruneOverSizeLimit: &defaultPruneImageOverSizeLimit,
70 84
 	}
71 85
 
72 86
 	cmd := &cobra.Command{
... ...
@@ -77,33 +92,37 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
77 77
 		Example: fmt.Sprintf(imagesExample, parentName, name),
78 78
 
79 79
 		Run: func(cmd *cobra.Command, args []string) {
80
-			if err := opts.Complete(f, args, out); err != nil {
81
-				cmdutil.CheckErr(err)
82
-			}
83
-
84
-			if err := opts.Validate(); err != nil {
85
-				cmdutil.CheckErr(cmdutil.UsageError(cmd, err.Error()))
86
-			}
87
-
88
-			if err := opts.RunPruneImages(); err != nil {
89
-				cmdutil.CheckErr(err)
90
-			}
80
+			kcmdutil.CheckErr(opts.Complete(f, cmd, args, out))
81
+			kcmdutil.CheckErr(opts.Validate())
82
+			kcmdutil.CheckErr(opts.Run())
91 83
 		},
92 84
 	}
93 85
 
94 86
 	cmd.Flags().BoolVar(&opts.Confirm, "confirm", opts.Confirm, "Specify that image pruning should proceed. Defaults to false, displaying what would be deleted but not actually deleting anything.")
95
-	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.")
96
-	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.")
87
+	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.")
88
+	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.")
89
+	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.")
97 90
 	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.")
98 91
 	cmd.Flags().StringVar(&opts.RegistryUrlOverride, "registry-url", opts.RegistryUrlOverride, "The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster-internal URL) but you do have an alternative route that works.")
99 92
 
100 93
 	return cmd
101 94
 }
102 95
 
103
-// Complete the options for prune images
104
-func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, args []string, out io.Writer) error {
96
+// Complete turns a partially defined PruneImagesOptions into a solvent structure
97
+// which can be validated and used for pruning images.
98
+func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string, out io.Writer) error {
105 99
 	if len(args) > 0 {
106
-		return errors.New("no arguments are allowed to this command")
100
+		return kcmdutil.UsageError(cmd, "no arguments are allowed to this command")
101
+	}
102
+
103
+	if !cmd.Flags().Lookup("keep-younger-than").Changed {
104
+		o.KeepYoungerThan = nil
105
+	}
106
+	if !cmd.Flags().Lookup("keep-tag-revisions").Changed {
107
+		o.KeepTagRevisions = nil
108
+	}
109
+	if !cmd.Flags().Lookup("prune-over-size-limit").Changed {
110
+		o.PruneOverSizeLimit = nil
107 111
 	}
108 112
 
109 113
 	o.Out = out
... ...
@@ -153,76 +172,95 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, args []string, out i
153 153
 		return err
154 154
 	}
155 155
 
156
-	options := prune.ImageRegistryPrunerOptions{
157
-		KeepYoungerThan:  o.KeepYoungerThan,
158
-		KeepTagRevisions: o.KeepTagRevisions,
159
-		Images:           allImages,
160
-		Streams:          allStreams,
161
-		Pods:             allPods,
162
-		RCs:              allRCs,
163
-		BCs:              allBCs,
164
-		Builds:           allBuilds,
165
-		DCs:              allDCs,
166
-		DryRun:           o.Confirm == false,
167
-		RegistryClient:   registryClient,
168
-		RegistryURL:      o.RegistryUrlOverride,
156
+	limitRangesList, err := kClient.LimitRanges(kapi.NamespaceAll).List(kapi.ListOptions{})
157
+	if err != nil {
158
+		return err
159
+	}
160
+	limitRangesMap := make(map[string][]*kapi.LimitRange)
161
+	for i := range limitRangesList.Items {
162
+		limit := limitRangesList.Items[i]
163
+		limits, ok := limitRangesMap[limit.Namespace]
164
+		if !ok {
165
+			limits = []*kapi.LimitRange{}
166
+		}
167
+		limits = append(limits, &limit)
168
+		limitRangesMap[limit.Namespace] = limits
169
+	}
170
+
171
+	options := prune.PrunerOptions{
172
+		KeepYoungerThan:    o.KeepYoungerThan,
173
+		KeepTagRevisions:   o.KeepTagRevisions,
174
+		PruneOverSizeLimit: o.PruneOverSizeLimit,
175
+		Images:             allImages,
176
+		Streams:            allStreams,
177
+		Pods:               allPods,
178
+		RCs:                allRCs,
179
+		BCs:                allBCs,
180
+		Builds:             allBuilds,
181
+		DCs:                allDCs,
182
+		LimitRanges:        limitRangesMap,
183
+		DryRun:             o.Confirm == false,
184
+		RegistryClient:     registryClient,
185
+		RegistryURL:        o.RegistryUrlOverride,
169 186
 	}
170 187
 
171
-	o.Pruner = prune.NewImageRegistryPruner(options)
188
+	o.Pruner = prune.NewPruner(options)
172 189
 
173 190
 	return nil
174 191
 }
175 192
 
176
-// Validate the options for prune images
177
-func (o *PruneImagesOptions) Validate() error {
178
-	if o.Pruner == nil && o.Confirm {
179
-		return errors.New("an image pruner needs to be specified")
193
+// Validate ensures that a PruneImagesOptions is valid and can be used to execute pruning.
194
+func (o PruneImagesOptions) Validate() error {
195
+	if o.PruneOverSizeLimit != nil && (o.KeepYoungerThan != nil || o.KeepTagRevisions != nil) {
196
+		return fmt.Errorf("--prune-over-size-limit cannot be specified with --keep-tag-revisions nor --keep-younger-than")
197
+	}
198
+	if o.KeepYoungerThan != nil && *o.KeepYoungerThan < 0 {
199
+		return fmt.Errorf("--keep-younger-than must be greater than or equal to 0")
180 200
 	}
181
-	if o.Client == nil {
182
-		return errors.New("a client needs to be specified")
201
+	if o.KeepTagRevisions != nil && *o.KeepTagRevisions < 0 {
202
+		return fmt.Errorf("--keep-tag-revisions must be greater than or equal to 0")
183 203
 	}
184
-	if o.Out == nil {
185
-		return errors.New("a writer needs to be specified")
204
+	if _, err := url.Parse(o.RegistryUrlOverride); err != nil {
205
+		return fmt.Errorf("invalid --registry-url flag: %v", err)
186 206
 	}
187 207
 	return nil
188 208
 }
189 209
 
190
-// RunPruneImages runs the prune images cli command
191
-func (o *PruneImagesOptions) RunPruneImages() error {
192
-	// this tabwriter is used by the describing*Pruners below for their output
210
+// Run contains all the necessary functionality for the OpenShift cli prune images command.
211
+func (o PruneImagesOptions) Run() error {
193 212
 	w := tabwriter.NewWriter(o.Out, 10, 4, 3, ' ', 0)
194 213
 	defer w.Flush()
195 214
 
196
-	imagePruner := &describingImagePruner{w: w}
197
-	imageStreamPruner := &describingImageStreamPruner{w: w}
198
-	layerPruner := &describingLayerPruner{w: w}
199
-	blobPruner := &describingBlobPruner{w: w}
200
-	manifestPruner := &describingManifestPruner{w: w}
215
+	imageDeleter := &describingImageDeleter{w: w}
216
+	imageStreamDeleter := &describingImageStreamDeleter{w: w}
217
+	layerDeleter := &describingLayerDeleter{w: w}
218
+	blobDeleter := &describingBlobDeleter{w: w}
219
+	manifestDeleter := &describingManifestDeleter{w: w}
201 220
 
202 221
 	if o.Confirm {
203
-		imagePruner.delegate = prune.NewDeletingImagePruner(o.Client.Images())
204
-		imageStreamPruner.delegate = prune.NewDeletingImageStreamPruner(o.Client)
205
-		layerPruner.delegate = prune.NewDeletingLayerPruner()
206
-		blobPruner.delegate = prune.NewDeletingBlobPruner()
207
-		manifestPruner.delegate = prune.NewDeletingManifestPruner()
222
+		imageDeleter.delegate = prune.NewImageDeleter(o.Client.Images())
223
+		imageStreamDeleter.delegate = prune.NewImageStreamDeleter(o.Client)
224
+		layerDeleter.delegate = prune.NewLayerDeleter()
225
+		blobDeleter.delegate = prune.NewBlobDeleter()
226
+		manifestDeleter.delegate = prune.NewManifestDeleter()
208 227
 	} else {
209 228
 		fmt.Fprintln(os.Stderr, "Dry run enabled - no modifications will be made. Add --confirm to remove images")
210 229
 	}
211 230
 
212
-	return o.Pruner.Prune(imagePruner, imageStreamPruner, layerPruner, blobPruner, manifestPruner)
231
+	return o.Pruner.Prune(imageDeleter, imageStreamDeleter, layerDeleter, blobDeleter, manifestDeleter)
213 232
 }
214 233
 
215
-// describingImageStreamPruner prints information about each image stream update.
216
-// If a delegate exists, its PruneImageStream function is invoked prior to returning.
217
-type describingImageStreamPruner struct {
234
+// describingImageStreamDeleter prints information about each image stream update.
235
+// If a delegate exists, its DeleteImageStream function is invoked prior to returning.
236
+type describingImageStreamDeleter struct {
218 237
 	w             io.Writer
219
-	delegate      prune.ImageStreamPruner
238
+	delegate      prune.ImageStreamDeleter
220 239
 	headerPrinted bool
221 240
 }
222 241
 
223
-var _ prune.ImageStreamPruner = &describingImageStreamPruner{}
242
+var _ prune.ImageStreamDeleter = &describingImageStreamDeleter{}
224 243
 
225
-func (p *describingImageStreamPruner) PruneImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error) {
244
+func (p *describingImageStreamDeleter) DeleteImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error) {
226 245
 	if !p.headerPrinted {
227 246
 		p.headerPrinted = true
228 247
 		fmt.Fprintln(p.w, "Deleting references from image streams to images ...")
... ...
@@ -235,7 +273,7 @@ func (p *describingImageStreamPruner) PruneImageStream(stream *imageapi.ImageStr
235 235
 		return stream, nil
236 236
 	}
237 237
 
238
-	updatedStream, err := p.delegate.PruneImageStream(stream, image, updatedTags)
238
+	updatedStream, err := p.delegate.DeleteImageStream(stream, image, updatedTags)
239 239
 	if err != nil {
240 240
 		fmt.Fprintf(os.Stderr, "error updating image stream %s/%s to remove references to image %s: %v\n", stream.Namespace, stream.Name, image.Name, err)
241 241
 	}
... ...
@@ -243,17 +281,17 @@ func (p *describingImageStreamPruner) PruneImageStream(stream *imageapi.ImageStr
243 243
 	return updatedStream, err
244 244
 }
245 245
 
246
-// describingImagePruner prints information about each image being deleted.
247
-// If a delegate exists, its PruneImage function is invoked prior to returning.
248
-type describingImagePruner struct {
246
+// describingImageDeleter prints information about each image being deleted.
247
+// If a delegate exists, its DeleteImage function is invoked prior to returning.
248
+type describingImageDeleter struct {
249 249
 	w             io.Writer
250
-	delegate      prune.ImagePruner
250
+	delegate      prune.ImageDeleter
251 251
 	headerPrinted bool
252 252
 }
253 253
 
254
-var _ prune.ImagePruner = &describingImagePruner{}
254
+var _ prune.ImageDeleter = &describingImageDeleter{}
255 255
 
256
-func (p *describingImagePruner) PruneImage(image *imageapi.Image) error {
256
+func (p *describingImageDeleter) DeleteImage(image *imageapi.Image) error {
257 257
 	if !p.headerPrinted {
258 258
 		p.headerPrinted = true
259 259
 		fmt.Fprintln(p.w, "\nDeleting images from server ...")
... ...
@@ -266,7 +304,7 @@ func (p *describingImagePruner) PruneImage(image *imageapi.Image) error {
266 266
 		return nil
267 267
 	}
268 268
 
269
-	err := p.delegate.PruneImage(image)
269
+	err := p.delegate.DeleteImage(image)
270 270
 	if err != nil {
271 271
 		fmt.Fprintf(os.Stderr, "error deleting image %s from server: %v\n", image.Name, err)
272 272
 	}
... ...
@@ -274,18 +312,18 @@ func (p *describingImagePruner) PruneImage(image *imageapi.Image) error {
274 274
 	return err
275 275
 }
276 276
 
277
-// describingLayerPruner prints information about each repo layer link being
278
-// deleted. If a delegate exists, its PruneLayer function is invoked prior to
277
+// describingLayerDeleter prints information about each repo layer link being
278
+// deleted. If a delegate exists, its DeleteLayer function is invoked prior to
279 279
 // returning.
280
-type describingLayerPruner struct {
280
+type describingLayerDeleter struct {
281 281
 	w             io.Writer
282
-	delegate      prune.LayerPruner
282
+	delegate      prune.LayerDeleter
283 283
 	headerPrinted bool
284 284
 }
285 285
 
286
-var _ prune.LayerPruner = &describingLayerPruner{}
286
+var _ prune.LayerDeleter = &describingLayerDeleter{}
287 287
 
288
-func (p *describingLayerPruner) PruneLayer(registryClient *http.Client, registryURL, repo, layer string) error {
288
+func (p *describingLayerDeleter) DeleteLayer(registryClient *http.Client, registryURL, repo, layer string) error {
289 289
 	if !p.headerPrinted {
290 290
 		p.headerPrinted = true
291 291
 		fmt.Fprintln(p.w, "\nDeleting registry repository layer links ...")
... ...
@@ -298,7 +336,7 @@ func (p *describingLayerPruner) PruneLayer(registryClient *http.Client, registry
298 298
 		return nil
299 299
 	}
300 300
 
301
-	err := p.delegate.PruneLayer(registryClient, registryURL, repo, layer)
301
+	err := p.delegate.DeleteLayer(registryClient, registryURL, repo, layer)
302 302
 	if err != nil {
303 303
 		fmt.Fprintf(os.Stderr, "error deleting repository %s layer link %s from the registry: %v\n", repo, layer, err)
304 304
 	}
... ...
@@ -306,17 +344,17 @@ func (p *describingLayerPruner) PruneLayer(registryClient *http.Client, registry
306 306
 	return err
307 307
 }
308 308
 
309
-// describingBlobPruner prints information about each blob being deleted. If a
310
-// delegate exists, its PruneBlob function is invoked prior to returning.
311
-type describingBlobPruner struct {
309
+// describingBlobDeleter prints information about each blob being deleted. If a
310
+// delegate exists, its DeleteBlob function is invoked prior to returning.
311
+type describingBlobDeleter struct {
312 312
 	w             io.Writer
313
-	delegate      prune.BlobPruner
313
+	delegate      prune.BlobDeleter
314 314
 	headerPrinted bool
315 315
 }
316 316
 
317
-var _ prune.BlobPruner = &describingBlobPruner{}
317
+var _ prune.BlobDeleter = &describingBlobDeleter{}
318 318
 
319
-func (p *describingBlobPruner) PruneBlob(registryClient *http.Client, registryURL, layer string) error {
319
+func (p *describingBlobDeleter) DeleteBlob(registryClient *http.Client, registryURL, layer string) error {
320 320
 	if !p.headerPrinted {
321 321
 		p.headerPrinted = true
322 322
 		fmt.Fprintln(p.w, "\nDeleting registry layer blobs ...")
... ...
@@ -329,7 +367,7 @@ func (p *describingBlobPruner) PruneBlob(registryClient *http.Client, registryUR
329 329
 		return nil
330 330
 	}
331 331
 
332
-	err := p.delegate.PruneBlob(registryClient, registryURL, layer)
332
+	err := p.delegate.DeleteBlob(registryClient, registryURL, layer)
333 333
 	if err != nil {
334 334
 		fmt.Fprintf(os.Stderr, "error deleting blob %s from the registry: %v\n", layer, err)
335 335
 	}
... ...
@@ -337,18 +375,18 @@ func (p *describingBlobPruner) PruneBlob(registryClient *http.Client, registryUR
337 337
 	return err
338 338
 }
339 339
 
340
-// describingManifestPruner prints information about each repo manifest being
341
-// deleted. If a delegate exists, its PruneManifest function is invoked prior
340
+// describingManifestDeleter prints information about each repo manifest being
341
+// deleted. If a delegate exists, its DeleteManifest function is invoked prior
342 342
 // to returning.
343
-type describingManifestPruner struct {
343
+type describingManifestDeleter struct {
344 344
 	w             io.Writer
345
-	delegate      prune.ManifestPruner
345
+	delegate      prune.ManifestDeleter
346 346
 	headerPrinted bool
347 347
 }
348 348
 
349
-var _ prune.ManifestPruner = &describingManifestPruner{}
349
+var _ prune.ManifestDeleter = &describingManifestDeleter{}
350 350
 
351
-func (p *describingManifestPruner) PruneManifest(registryClient *http.Client, registryURL, repo, manifest string) error {
351
+func (p *describingManifestDeleter) DeleteManifest(registryClient *http.Client, registryURL, repo, manifest string) error {
352 352
 	if !p.headerPrinted {
353 353
 		p.headerPrinted = true
354 354
 		fmt.Fprintln(p.w, "\nDeleting registry repository manifest data ...")
... ...
@@ -361,7 +399,7 @@ func (p *describingManifestPruner) PruneManifest(registryClient *http.Client, re
361 361
 		return nil
362 362
 	}
363 363
 
364
-	err := p.delegate.PruneManifest(registryClient, registryURL, repo, manifest)
364
+	err := p.delegate.DeleteManifest(registryClient, registryURL, repo, manifest)
365 365
 	if err != nil {
366 366
 		fmt.Fprintf(os.Stderr, "error deleting data for repository %s image manifest %s from the registry: %v\n", repo, manifest, err)
367 367
 	}
... ...
@@ -391,7 +429,7 @@ func getClients(f *clientcmd.Factory, caBundle string) (*client.Client, *kclient
391 391
 		}
392 392
 		token = clientConfig.BearerToken
393 393
 	default:
394
-		err = errors.New("You must use a client config with a token")
394
+		err = errors.New("you must use a client config with a token")
395 395
 		return nil, nil, nil, err
396 396
 	}
397 397
 
... ...
@@ -430,6 +430,7 @@ func GetBootstrapClusterRoles() []authorizationapi.ClusterRole {
430 430
 			},
431 431
 			Rules: []authorizationapi.PolicyRule{
432 432
 				authorizationapi.NewRule("get", "list").Groups(kapiGroup).Resources("pods", "replicationcontrollers").RuleOrDie(),
433
+				authorizationapi.NewRule("list").Groups(kapiGroup).Resources("limitranges").RuleOrDie(),
433 434
 				authorizationapi.NewRule("get", "list").Groups(buildGroup).Resources("buildconfigs", "builds").RuleOrDie(),
434 435
 				authorizationapi.NewRule("get", "list").Groups(deployGroup).Resources("deploymentconfigs").RuleOrDie(),
435 436
 
... ...
@@ -3,67 +3,123 @@ package prune
3 3
 import (
4 4
 	"time"
5 5
 
6
+	"github.com/golang/glog"
7
+
6 8
 	kapi "k8s.io/kubernetes/pkg/api"
9
+	kclient "k8s.io/kubernetes/pkg/client/unversioned"
7 10
 
8 11
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
12
+	deployutil "github.com/openshift/origin/pkg/deploy/util"
9 13
 )
10 14
 
11
-// PruneFunc is a function that is invoked for each item during Prune
12
-type PruneFunc func(item *kapi.ReplicationController) error
15
+type Pruner interface {
16
+	// Prune is responsible for actual removal of deployments identified as candidates
17
+	// for pruning based on pruning algorithm.
18
+	Prune(deleter DeploymentDeleter) error
19
+}
13 20
 
14
-type PruneTasker interface {
15
-	// PruneTask is an object that knows how to execute a single iteration of a Prune
16
-	PruneTask() error
21
+// DeploymentDeleter knows how to delete deployments from OpenShift.
22
+type DeploymentDeleter interface {
23
+	// DeleteDeployment removes the deployment from OpenShift's storage.
24
+	DeleteDeployment(deployment *kapi.ReplicationController) error
17 25
 }
18 26
 
19
-// pruneTask is an object that knows how to prune a data set
20
-type pruneTask struct {
27
+// pruner is an object that knows how to prune a data set
28
+type pruner struct {
21 29
 	resolver Resolver
22
-	handler  PruneFunc
23 30
 }
24 31
 
25
-// NewPruneTasker returns a PruneTasker over specified data using specified flags
26
-// keepYoungerThan will filter out all objects from prune data set that are younger than the specified time duration
27
-// orphans if true will include inactive orphan deployments in candidate prune set
28
-// keepComplete is per DeploymentConfig how many of the most recent deployments should be preserved
29
-// keepFailed is per DeploymentConfig how many of the most recent failed deployments should be preserved
30
-func NewPruneTasker(deploymentConfigs []*deployapi.DeploymentConfig, deployments []*kapi.ReplicationController, keepYoungerThan time.Duration, orphans bool, keepComplete int, keepFailed int, handler PruneFunc) PruneTasker {
32
+var _ Pruner = &pruner{}
33
+
34
+// PrunerOptions contains the fields used to initialize a new Pruner.
35
+type PrunerOptions struct {
36
+	// KeepYoungerThan will filter out all objects from prune data set that are younger than the specified time duration.
37
+	KeepYoungerThan time.Duration
38
+	// Orphans if true will include inactive orphan deployments in candidate prune set.
39
+	Orphans bool
40
+	// KeepComplete is per DeploymentConfig how many of the most recent deployments should be preserved.
41
+	KeepComplete int
42
+	// KeepFailed is per DeploymentConfig how many of the most recent failed deployments should be preserved.
43
+	KeepFailed int
44
+	// DeploymentConfigs is the entire list of deploymentconfigs across all namespaces in the cluster.
45
+	DeploymentConfigs []*deployapi.DeploymentConfig
46
+	// Deployments is the entire list of deployments across all namespaces in the cluster.
47
+	Deployments []*kapi.ReplicationController
48
+}
49
+
50
+// NewPruner returns a Pruner over specified data using specified options.
51
+// deploymentConfigs, deployments, opts.KeepYoungerThan, opts.Orphans, opts.KeepComplete, opts.KeepFailed, deploymentPruneFunc
52
+func NewPruner(options PrunerOptions) Pruner {
31 53
 	filter := &andFilter{
32 54
 		filterPredicates: []FilterPredicate{
33 55
 			FilterDeploymentsPredicate,
34 56
 			FilterZeroReplicaSize,
35
-			NewFilterBeforePredicate(keepYoungerThan),
57
+			NewFilterBeforePredicate(options.KeepYoungerThan),
36 58
 		},
37 59
 	}
38
-	deployments = filter.Filter(deployments)
39
-	dataSet := NewDataSet(deploymentConfigs, deployments)
60
+	deployments := filter.Filter(options.Deployments)
61
+	dataSet := NewDataSet(options.DeploymentConfigs, deployments)
40 62
 
41 63
 	resolvers := []Resolver{}
42
-	if orphans {
64
+	if options.Orphans {
43 65
 		inactiveDeploymentStatus := []deployapi.DeploymentStatus{
44 66
 			deployapi.DeploymentStatusComplete,
45 67
 			deployapi.DeploymentStatusFailed,
46 68
 		}
47 69
 		resolvers = append(resolvers, NewOrphanDeploymentResolver(dataSet, inactiveDeploymentStatus))
48 70
 	}
49
-	resolvers = append(resolvers, NewPerDeploymentConfigResolver(dataSet, keepComplete, keepFailed))
50
-	return &pruneTask{
71
+	resolvers = append(resolvers, NewPerDeploymentConfigResolver(dataSet, options.KeepComplete, options.KeepFailed))
72
+
73
+	return &pruner{
51 74
 		resolver: &mergeResolver{resolvers: resolvers},
52
-		handler:  handler,
53 75
 	}
54 76
 }
55 77
 
56
-// PruneTask will visit each item in the prunable set and invoke the associated handler
57
-func (t *pruneTask) PruneTask() error {
58
-	deployments, err := t.resolver.Resolve()
78
+// Prune will visit each item in the prunable set and invoke the associated DeploymentDeleter.
79
+func (p *pruner) Prune(deleter DeploymentDeleter) error {
80
+	deployments, err := p.resolver.Resolve()
59 81
 	if err != nil {
60 82
 		return err
61 83
 	}
62 84
 	for _, deployment := range deployments {
63
-		err = t.handler(deployment)
64
-		if err != nil {
85
+		if err := deleter.DeleteDeployment(deployment); err != nil {
65 86
 			return err
66 87
 		}
67 88
 	}
68 89
 	return nil
69 90
 }
91
+
92
+// deploymentDeleter removes a deployment from OpenShift.
93
+type deploymentDeleter struct {
94
+	deployments kclient.ReplicationControllersNamespacer
95
+	pods        kclient.PodsNamespacer
96
+}
97
+
98
+var _ DeploymentDeleter = &deploymentDeleter{}
99
+
100
+// NewDeploymentDeleter creates a new deploymentDeleter.
101
+func NewDeploymentDeleter(deployments kclient.ReplicationControllersNamespacer, pods kclient.PodsNamespacer) DeploymentDeleter {
102
+	return &deploymentDeleter{
103
+		deployments: deployments,
104
+		pods:        pods,
105
+	}
106
+}
107
+
108
+func (p *deploymentDeleter) DeleteDeployment(deployment *kapi.ReplicationController) error {
109
+	glog.V(4).Infof("Deleting deployment %q", deployment.Name)
110
+	// If the deployment is failed we need to remove its deployer pods, too.
111
+	if deployutil.DeploymentStatusFor(deployment) == deployapi.DeploymentStatusFailed {
112
+		dpSelector := deployutil.DeployerPodSelector(deployment.Name)
113
+		deployers, err := p.pods.Pods(deployment.Namespace).List(kapi.ListOptions{LabelSelector: dpSelector})
114
+		if err != nil {
115
+			glog.Warning("Cannot list deployer pods for %q: %v\n", deployment.Name, err)
116
+		} else {
117
+			for _, pod := range deployers.Items {
118
+				if err := p.pods.Pods(pod.Namespace).Delete(pod.Name, nil); err != nil {
119
+					glog.Warning("Cannot remove deployer pod %q: %v\n", pod.Name, err)
120
+				}
121
+			}
122
+		}
123
+	}
124
+	return p.deployments.ReplicationControllers(deployment.Namespace).Delete(deployment.Name)
125
+}
... ...
@@ -12,17 +12,19 @@ import (
12 12
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
13 13
 )
14 14
 
15
-type mockPruneRecorder struct {
15
+type mockDeleteRecorder struct {
16 16
 	set sets.String
17 17
 	err error
18 18
 }
19 19
 
20
-func (m *mockPruneRecorder) Handler(deployment *kapi.ReplicationController) error {
20
+var _ DeploymentDeleter = &mockDeleteRecorder{}
21
+
22
+func (m *mockDeleteRecorder) DeleteDeployment(deployment *kapi.ReplicationController) error {
21 23
 	m.set.Insert(deployment.Name)
22 24
 	return m.err
23 25
 }
24 26
 
25
-func (m *mockPruneRecorder) Verify(t *testing.T, expected sets.String) {
27
+func (m *mockDeleteRecorder) Verify(t *testing.T, expected sets.String) {
26 28
 	if len(m.set) != len(expected) || !m.set.HasAll(expected.List()...) {
27 29
 		expectedValues := expected.List()
28 30
 		actualValues := m.set.List()
... ...
@@ -87,14 +89,25 @@ func TestPruneTask(t *testing.T) {
87 87
 				}
88 88
 			}
89 89
 			expectedDeployments, err := resolver.Resolve()
90
+			if err != nil {
91
+				t.Errorf("Unexpected error %v", err)
92
+			}
90 93
 			for _, item := range expectedDeployments {
91 94
 				expectedValues.Insert(item.Name)
92 95
 			}
93 96
 
94
-			recorder := &mockPruneRecorder{set: sets.String{}}
95
-			task := NewPruneTasker(deploymentConfigs, deployments, keepYoungerThan, orphans, keepComplete, keepFailed, recorder.Handler)
96
-			err = task.PruneTask()
97
-			if err != nil {
97
+			recorder := &mockDeleteRecorder{set: sets.String{}}
98
+
99
+			options := PrunerOptions{
100
+				KeepYoungerThan:   keepYoungerThan,
101
+				Orphans:           orphans,
102
+				KeepComplete:      keepComplete,
103
+				KeepFailed:        keepFailed,
104
+				DeploymentConfigs: deploymentConfigs,
105
+				Deployments:       deployments,
106
+			}
107
+			pruner := NewPruner(options)
108
+			if err := pruner.Prune(recorder); err != nil {
98 109
 				t.Errorf("Unexpected error %v", err)
99 110
 			}
100 111
 			recorder.Verify(t, expectedValues)
101 112
deleted file mode 100644
... ...
@@ -1,1010 +0,0 @@
1
-package prune
2
-
3
-import (
4
-	"encoding/json"
5
-	"fmt"
6
-	"net/http"
7
-	"time"
8
-
9
-	"github.com/docker/distribution/registry/api/errcode"
10
-	"github.com/golang/glog"
11
-	gonum "github.com/gonum/graph"
12
-
13
-	kapi "k8s.io/kubernetes/pkg/api"
14
-	"k8s.io/kubernetes/pkg/api/unversioned"
15
-	kerrors "k8s.io/kubernetes/pkg/util/errors"
16
-	utilruntime "k8s.io/kubernetes/pkg/util/runtime"
17
-	"k8s.io/kubernetes/pkg/util/sets"
18
-
19
-	"github.com/openshift/origin/pkg/api/graph"
20
-	kubegraph "github.com/openshift/origin/pkg/api/kubegraph/nodes"
21
-	buildapi "github.com/openshift/origin/pkg/build/api"
22
-	buildgraph "github.com/openshift/origin/pkg/build/graph/nodes"
23
-	buildutil "github.com/openshift/origin/pkg/build/util"
24
-	"github.com/openshift/origin/pkg/client"
25
-	deployapi "github.com/openshift/origin/pkg/deploy/api"
26
-	deploygraph "github.com/openshift/origin/pkg/deploy/graph/nodes"
27
-	imageapi "github.com/openshift/origin/pkg/image/api"
28
-	imagegraph "github.com/openshift/origin/pkg/image/graph/nodes"
29
-)
30
-
31
-// TODO these edges should probably have an `Add***Edges` method in images/graph and be moved there
32
-const (
33
-	// ReferencedImageEdgeKind defines a "strong" edge where the tail is an
34
-	// ImageNode, with strong indicating that the ImageNode tail is not a
35
-	// candidate for pruning.
36
-	ReferencedImageEdgeKind = "ReferencedImage"
37
-	// WeakReferencedImageEdgeKind defines a "weak" edge where the tail is
38
-	// an ImageNode, with weak indicating that this particular edge does
39
-	// not keep an ImageNode from being a candidate for pruning.
40
-	WeakReferencedImageEdgeKind = "WeakReferencedImage"
41
-
42
-	// ReferencedImageLayerEdgeKind defines an edge from an ImageStreamNode or an
43
-	// ImageNode to an ImageLayerNode.
44
-	ReferencedImageLayerEdgeKind = "ReferencedImageLayer"
45
-)
46
-
47
-// pruneAlgorithm contains the various settings to use when evaluating images
48
-// and layers for pruning.
49
-type pruneAlgorithm struct {
50
-	keepYoungerThan  time.Duration
51
-	keepTagRevisions int
52
-}
53
-
54
-// ImagePruner knows how to delete images from OpenShift.
55
-type ImagePruner interface {
56
-	// PruneImage deletes the image from OpenShift's storage.
57
-	PruneImage(image *imageapi.Image) error
58
-}
59
-
60
-// ImageStreamPruner knows how to remove an image reference from an image
61
-// stream.
62
-type ImageStreamPruner interface {
63
-	// PruneImageStream deletes all references to the image from the image
64
-	// stream's status.tags. The updated image stream is returned.
65
-	PruneImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error)
66
-}
67
-
68
-// BlobPruner knows how to delete a blob from the Docker registry.
69
-type BlobPruner interface {
70
-	// PruneBlob uses registryClient to ask the registry at registryURL to delete
71
-	// the blob.
72
-	PruneBlob(registryClient *http.Client, registryURL, blob string) error
73
-}
74
-
75
-// LayerPruner knows how to delete a repository layer link from the Docker
76
-// registry.
77
-type LayerPruner interface {
78
-	// PruneLayer uses registryClient to ask the registry at registryURL to
79
-	// delete the repository layer link.
80
-	PruneLayer(registryClient *http.Client, registryURL, repo, layer string) error
81
-}
82
-
83
-// ManifestPruner knows how to delete image manifest data for a repository from
84
-// the Docker registry.
85
-type ManifestPruner interface {
86
-	// PruneManifest uses registryClient to ask the registry at registryURL to
87
-	// delete the repository's image manifest data.
88
-	PruneManifest(registryClient *http.Client, registryURL, repo, manifest string) error
89
-}
90
-
91
-// ImageRegistryPrunerOptions contains the fields used to initialize a new
92
-// ImageRegistryPruner.
93
-type ImageRegistryPrunerOptions struct {
94
-	// KeepYoungerThan indicates the minimum age an Image must be to be a
95
-	// candidate for pruning.
96
-	KeepYoungerThan time.Duration
97
-	// KeepTagRevisions is the minimum number of tag revisions to preserve;
98
-	// revisions older than this value are candidates for pruning.
99
-	KeepTagRevisions int
100
-	// Images is the entire list of images in OpenShift. An image must be in this
101
-	// list to be a candidate for pruning.
102
-	Images *imageapi.ImageList
103
-	// Streams is the entire list of image streams across all namespaces in the
104
-	// cluster.
105
-	Streams *imageapi.ImageStreamList
106
-	// Pods is the entire list of pods across all namespaces in the cluster.
107
-	Pods *kapi.PodList
108
-	// RCs is the entire list of replication controllers across all namespaces in
109
-	// the cluster.
110
-	RCs *kapi.ReplicationControllerList
111
-	// BCs is the entire list of build configs across all namespaces in the
112
-	// cluster.
113
-	BCs *buildapi.BuildConfigList
114
-	// Builds is the entire list of builds across all namespaces in the cluster.
115
-	Builds *buildapi.BuildList
116
-	// DCs is the entire list of deployment configs across all namespaces in the
117
-	// cluster.
118
-	DCs *deployapi.DeploymentConfigList
119
-	// DryRun indicates that no changes will be made to the cluster and nothing
120
-	// will be removed.
121
-	DryRun bool
122
-	// RegistryClient is the http.Client to use when contacting the registry.
123
-	RegistryClient *http.Client
124
-	// RegistryURL is the URL for the registry.
125
-	RegistryURL string
126
-}
127
-
128
-// ImageRegistryPruner knows how to prune images and layers.
129
-type ImageRegistryPruner interface {
130
-	// Prune uses imagePruner, streamPruner, layerPruner, blobPruner, and
131
-	// manifestPruner to remove images that have been identified as candidates
132
-	// for pruning based on the ImageRegistryPruner's internal pruning algorithm.
133
-	// Please see NewImageRegistryPruner for details on the algorithm.
134
-	Prune(imagePruner ImagePruner, streamPruner ImageStreamPruner, layerPruner LayerPruner, blobPruner BlobPruner, manifestPruner ManifestPruner) error
135
-}
136
-
137
-// imageRegistryPruner implements ImageRegistryPruner.
138
-type imageRegistryPruner struct {
139
-	g              graph.Graph
140
-	algorithm      pruneAlgorithm
141
-	registryPinger registryPinger
142
-	registryClient *http.Client
143
-	registryURL    string
144
-}
145
-
146
-var _ ImageRegistryPruner = &imageRegistryPruner{}
147
-
148
-// registryPinger performs a health check against a registry.
149
-type registryPinger interface {
150
-	// ping performs a health check against registry.
151
-	ping(registry string) error
152
-}
153
-
154
-// defaultRegistryPinger implements registryPinger.
155
-type defaultRegistryPinger struct {
156
-	client *http.Client
157
-}
158
-
159
-func (drp *defaultRegistryPinger) ping(registry string) error {
160
-	healthCheck := func(proto, registry string) error {
161
-		// TODO: `/healthz` route is deprecated by `/`; remove it in future versions
162
-		healthResponse, err := drp.client.Get(fmt.Sprintf("%s://%s/healthz", proto, registry))
163
-		if err != nil {
164
-			return err
165
-		}
166
-		defer healthResponse.Body.Close()
167
-
168
-		if healthResponse.StatusCode != http.StatusOK {
169
-			return fmt.Errorf("unexpected status code %d", healthResponse.StatusCode)
170
-		}
171
-
172
-		return nil
173
-	}
174
-
175
-	var err error
176
-	for _, proto := range []string{"https", "http"} {
177
-		glog.V(4).Infof("Trying %s for %s", proto, registry)
178
-		err = healthCheck(proto, registry)
179
-		if err == nil {
180
-			break
181
-		}
182
-		glog.V(4).Infof("Error with %s for %s: %v", proto, registry, err)
183
-	}
184
-
185
-	return err
186
-}
187
-
188
-// dryRunRegistryPinger implements registryPinger.
189
-type dryRunRegistryPinger struct {
190
-}
191
-
192
-func (*dryRunRegistryPinger) ping(registry string) error {
193
-	return nil
194
-}
195
-
196
-/*
197
-NewImageRegistryPruner creates a new ImageRegistryPruner.
198
-
199
-Images younger than keepYoungerThan and images referenced by image streams
200
-and/or pods younger than keepYoungerThan are preserved. All other images are
201
-candidates for pruning. For example, if keepYoungerThan is 60m, and an
202
-ImageStream is only 59 minutes old, none of the images it references are
203
-eligible for pruning.
204
-
205
-keepTagRevisions is the number of revisions per tag in an image stream's
206
-status.tags that are preserved and ineligible for pruning. Any revision older
207
-than keepTagRevisions is eligible for pruning.
208
-
209
-images, streams, pods, rcs, bcs, builds, and dcs are the resources used to run
210
-the pruning algorithm. These should be the full list for each type from the
211
-cluster; otherwise, the pruning algorithm might result in incorrect
212
-calculations and premature pruning.
213
-
214
-The ImagePruner performs the following logic: remove any image containing the
215
-annotation openshift.io/image.managed=true that was created at least *n*
216
-minutes ago and is *not* currently referenced by:
217
-
218
-- any pod created less than *n* minutes ago
219
-- any image stream created less than *n* minutes ago
220
-- any running pods
221
-- any pending pods
222
-- any replication controllers
223
-- any deployment configs
224
-- any build configs
225
-- any builds
226
-- the n most recent tag revisions in an image stream's status.tags
227
-
228
-When removing an image, remove all references to the image from all
229
-ImageStreams having a reference to the image in `status.tags`.
230
-
231
-Also automatically remove any image layer that is no longer referenced by any
232
-images.
233
-*/
234
-func NewImageRegistryPruner(options ImageRegistryPrunerOptions) ImageRegistryPruner {
235
-	g := graph.New()
236
-
237
-	glog.V(1).Infof("Creating image pruner with keepYoungerThan=%v, keepTagRevisions=%d", options.KeepYoungerThan, options.KeepTagRevisions)
238
-
239
-	algorithm := pruneAlgorithm{
240
-		keepYoungerThan:  options.KeepYoungerThan,
241
-		keepTagRevisions: options.KeepTagRevisions,
242
-	}
243
-
244
-	addImagesToGraph(g, options.Images, algorithm)
245
-	addImageStreamsToGraph(g, options.Streams, algorithm)
246
-	addPodsToGraph(g, options.Pods, algorithm)
247
-	addReplicationControllersToGraph(g, options.RCs)
248
-	addBuildConfigsToGraph(g, options.BCs)
249
-	addBuildsToGraph(g, options.Builds)
250
-	addDeploymentConfigsToGraph(g, options.DCs)
251
-
252
-	var rp registryPinger
253
-	if options.DryRun {
254
-		rp = &dryRunRegistryPinger{}
255
-	} else {
256
-		rp = &defaultRegistryPinger{options.RegistryClient}
257
-	}
258
-
259
-	return &imageRegistryPruner{
260
-		g:              g,
261
-		algorithm:      algorithm,
262
-		registryPinger: rp,
263
-		registryClient: options.RegistryClient,
264
-		registryURL:    options.RegistryURL,
265
-	}
266
-}
267
-
268
-// addImagesToGraph adds all images to the graph that belong to one of the
269
-// registries in the algorithm and are at least as old as the minimum age
270
-// threshold as specified by the algorithm. It also adds all the images' layers
271
-// to the graph.
272
-func addImagesToGraph(g graph.Graph, images *imageapi.ImageList, algorithm pruneAlgorithm) {
273
-	for i := range images.Items {
274
-		image := &images.Items[i]
275
-
276
-		glog.V(4).Infof("Examining image %q", image.Name)
277
-
278
-		if image.Annotations == nil {
279
-			glog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", image.Name, image.DockerImageReference)
280
-			continue
281
-		}
282
-		if value, ok := image.Annotations[imageapi.ManagedByOpenShiftAnnotation]; !ok || value != "true" {
283
-			glog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", image.Name, image.DockerImageReference)
284
-			continue
285
-		}
286
-
287
-		age := unversioned.Now().Sub(image.CreationTimestamp.Time)
288
-		if age < algorithm.keepYoungerThan {
289
-			glog.V(4).Infof("Image %q is younger than minimum pruning age, skipping (age=%v)", image.Name, age)
290
-			continue
291
-		}
292
-
293
-		glog.V(4).Infof("Adding image %q to graph", image.Name)
294
-		imageNode := imagegraph.EnsureImageNode(g, image)
295
-
296
-		manifest := imageapi.DockerImageManifest{}
297
-		if err := json.Unmarshal([]byte(image.DockerImageManifest), &manifest); err != nil {
298
-			utilruntime.HandleError(fmt.Errorf("unable to extract manifest from image: %v. This image's layers won't be pruned if the image is pruned now.", err))
299
-			continue
300
-		}
301
-
302
-		for _, layer := range manifest.FSLayers {
303
-			glog.V(4).Infof("Adding image layer %q to graph", layer.DockerBlobSum)
304
-			layerNode := imagegraph.EnsureImageLayerNode(g, layer.DockerBlobSum)
305
-			g.AddEdge(imageNode, layerNode, ReferencedImageLayerEdgeKind)
306
-		}
307
-	}
308
-}
309
-
310
-// addImageStreamsToGraph adds all the streams to the graph. The most recent n
311
-// image revisions for a tag will be preserved, where n is specified by the
312
-// algorithm's keepTagRevisions. Image revisions older than n are candidates
313
-// for pruning.  if the image stream's age is at least as old as the minimum
314
-// threshold in algorithm.  Otherwise, if the image stream is younger than the
315
-// threshold, all image revisions for that stream are ineligible for pruning.
316
-//
317
-// addImageStreamsToGraph also adds references from each stream to all the
318
-// layers it references (via each image a stream references).
319
-func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, algorithm pruneAlgorithm) {
320
-	for i := range streams.Items {
321
-		stream := &streams.Items[i]
322
-
323
-		glog.V(4).Infof("Examining ImageStream %s/%s", stream.Namespace, stream.Name)
324
-
325
-		// use a weak reference for old image revisions by default
326
-		oldImageRevisionReferenceKind := WeakReferencedImageEdgeKind
327
-
328
-		age := unversioned.Now().Sub(stream.CreationTimestamp.Time)
329
-		if age < algorithm.keepYoungerThan {
330
-			// stream's age is below threshold - use a strong reference for old image revisions instead
331
-			glog.V(4).Infof("Stream %s/%s is below age threshold - none of its images are eligible for pruning", stream.Namespace, stream.Name)
332
-			oldImageRevisionReferenceKind = ReferencedImageEdgeKind
333
-		}
334
-
335
-		glog.V(4).Infof("Adding ImageStream %s/%s to graph", stream.Namespace, stream.Name)
336
-		isNode := imagegraph.EnsureImageStreamNode(g, stream)
337
-		imageStreamNode := isNode.(*imagegraph.ImageStreamNode)
338
-
339
-		for tag, history := range stream.Status.Tags {
340
-			for i := range history.Items {
341
-				n := imagegraph.FindImage(g, history.Items[i].Image)
342
-				if n == nil {
343
-					glog.V(2).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)
344
-					continue
345
-				}
346
-				imageNode := n.(*imagegraph.ImageNode)
347
-
348
-				var kind string
349
-				switch {
350
-				case i < algorithm.keepTagRevisions:
351
-					kind = ReferencedImageEdgeKind
352
-				default:
353
-					kind = oldImageRevisionReferenceKind
354
-				}
355
-
356
-				glog.V(4).Infof("Checking for existing strong reference from stream %s/%s to image %s", stream.Namespace, stream.Name, imageNode.Image.Name)
357
-				if edge := g.Edge(imageStreamNode, imageNode); edge != nil && g.EdgeKinds(edge).Has(ReferencedImageEdgeKind) {
358
-					glog.V(4).Infof("Strong reference found")
359
-					continue
360
-				}
361
-
362
-				glog.V(4).Infof("Adding edge (kind=%s) from %q to %q", kind, imageStreamNode.UniqueName(), imageNode.UniqueName())
363
-				g.AddEdge(imageStreamNode, imageNode, kind)
364
-
365
-				glog.V(4).Infof("Adding stream->layer references")
366
-				// add stream -> layer references so we can prune them later
367
-				for _, s := range g.From(imageNode) {
368
-					if g.Kind(s) != imagegraph.ImageLayerNodeKind {
369
-						continue
370
-					}
371
-					glog.V(4).Infof("Adding reference from stream %q to layer %q", stream.Name, s.(*imagegraph.ImageLayerNode).Layer)
372
-					g.AddEdge(imageStreamNode, s, ReferencedImageLayerEdgeKind)
373
-				}
374
-			}
375
-		}
376
-	}
377
-}
378
-
379
-// addPodsToGraph adds pods to the graph.
380
-//
381
-// A pod is only *excluded* from being added to the graph if its phase is not
382
-// pending or running and it is at least as old as the minimum age threshold
383
-// defined by algorithm.
384
-//
385
-// Edges are added to the graph from each pod to the images specified by that
386
-// pod's list of containers, as long as the image is managed by OpenShift.
387
-func addPodsToGraph(g graph.Graph, pods *kapi.PodList, algorithm pruneAlgorithm) {
388
-	for i := range pods.Items {
389
-		pod := &pods.Items[i]
390
-
391
-		glog.V(4).Infof("Examining pod %s/%s", pod.Namespace, pod.Name)
392
-
393
-		if pod.Status.Phase != kapi.PodRunning && pod.Status.Phase != kapi.PodPending {
394
-			age := unversioned.Now().Sub(pod.CreationTimestamp.Time)
395
-			if age >= algorithm.keepYoungerThan {
396
-				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)
397
-				// not pending or running, age is at least minimum pruning age, skip
398
-				continue
399
-			}
400
-		}
401
-
402
-		glog.V(4).Infof("Adding pod %s/%s to graph", pod.Namespace, pod.Name)
403
-		podNode := kubegraph.EnsurePodNode(g, pod)
404
-
405
-		addPodSpecToGraph(g, &pod.Spec, podNode)
406
-	}
407
-}
408
-
409
-// Edges are added to the graph from each predecessor (pod or replication
410
-// controller) to the images specified by the pod spec's list of containers, as
411
-// long as the image is managed by OpenShift.
412
-func addPodSpecToGraph(g graph.Graph, spec *kapi.PodSpec, predecessor gonum.Node) {
413
-	for j := range spec.Containers {
414
-		container := spec.Containers[j]
415
-
416
-		glog.V(4).Infof("Examining container image %q", container.Image)
417
-
418
-		ref, err := imageapi.ParseDockerImageReference(container.Image)
419
-		if err != nil {
420
-			utilruntime.HandleError(fmt.Errorf("unable to parse DockerImageReference %q: %v", container.Image, err))
421
-			continue
422
-		}
423
-
424
-		if len(ref.ID) == 0 {
425
-			glog.V(4).Infof("%q has no image ID", container.Image)
426
-			continue
427
-		}
428
-
429
-		imageNode := imagegraph.FindImage(g, ref.ID)
430
-		if imageNode == nil {
431
-			glog.Infof("Unable to find image %q in the graph", ref.ID)
432
-			continue
433
-		}
434
-
435
-		glog.V(4).Infof("Adding edge from pod to image")
436
-		g.AddEdge(predecessor, imageNode, ReferencedImageEdgeKind)
437
-	}
438
-}
439
-
440
-// addReplicationControllersToGraph adds replication controllers to the graph.
441
-//
442
-// Edges are added to the graph from each replication controller to the images
443
-// specified by its pod spec's list of containers, as long as the image is
444
-// managed by OpenShift.
445
-func addReplicationControllersToGraph(g graph.Graph, rcs *kapi.ReplicationControllerList) {
446
-	for i := range rcs.Items {
447
-		rc := &rcs.Items[i]
448
-		glog.V(4).Infof("Examining replication controller %s/%s", rc.Namespace, rc.Name)
449
-		rcNode := kubegraph.EnsureReplicationControllerNode(g, rc)
450
-		addPodSpecToGraph(g, &rc.Spec.Template.Spec, rcNode)
451
-	}
452
-}
453
-
454
-// addDeploymentConfigsToGraph adds deployment configs to the graph.
455
-//
456
-// Edges are added to the graph from each deployment config to the images
457
-// specified by its pod spec's list of containers, as long as the image is
458
-// managed by OpenShift.
459
-func addDeploymentConfigsToGraph(g graph.Graph, dcs *deployapi.DeploymentConfigList) {
460
-	for i := range dcs.Items {
461
-		dc := &dcs.Items[i]
462
-		glog.V(4).Infof("Examining DeploymentConfig %s/%s", dc.Namespace, dc.Name)
463
-		dcNode := deploygraph.EnsureDeploymentConfigNode(g, dc)
464
-		addPodSpecToGraph(g, &dc.Spec.Template.Spec, dcNode)
465
-	}
466
-}
467
-
468
-// addBuildConfigsToGraph adds build configs to the graph.
469
-//
470
-// Edges are added to the graph from each build config to the image specified by its strategy.from.
471
-func addBuildConfigsToGraph(g graph.Graph, bcs *buildapi.BuildConfigList) {
472
-	for i := range bcs.Items {
473
-		bc := &bcs.Items[i]
474
-		glog.V(4).Infof("Examining BuildConfig %s/%s", bc.Namespace, bc.Name)
475
-		bcNode := buildgraph.EnsureBuildConfigNode(g, bc)
476
-		addBuildStrategyImageReferencesToGraph(g, bc.Spec.Strategy, bcNode)
477
-	}
478
-}
479
-
480
-// addBuildsToGraph adds builds to the graph.
481
-//
482
-// Edges are added to the graph from each build to the image specified by its strategy.from.
483
-func addBuildsToGraph(g graph.Graph, builds *buildapi.BuildList) {
484
-	for i := range builds.Items {
485
-		build := &builds.Items[i]
486
-		glog.V(4).Infof("Examining build %s/%s", build.Namespace, build.Name)
487
-		buildNode := buildgraph.EnsureBuildNode(g, build)
488
-		addBuildStrategyImageReferencesToGraph(g, build.Spec.Strategy, buildNode)
489
-	}
490
-}
491
-
492
-// addBuildStrategyImageReferencesToGraph ads references from the build strategy's parent node to the image
493
-// the build strategy references.
494
-//
495
-// Edges are added to the graph from each predecessor (build or build config)
496
-// to the image specified by strategy.from, as long as the image is managed by
497
-// OpenShift.
498
-func addBuildStrategyImageReferencesToGraph(g graph.Graph, strategy buildapi.BuildStrategy, predecessor gonum.Node) {
499
-	from := buildutil.GetInputReference(strategy)
500
-	if from == nil {
501
-		glog.V(4).Infof("Unable to determine 'from' reference - skipping")
502
-		return
503
-	}
504
-
505
-	glog.V(4).Infof("Examining build strategy with from: %#v", from)
506
-
507
-	var imageID string
508
-
509
-	switch from.Kind {
510
-	case "ImageStreamImage":
511
-		_, id, err := imageapi.ParseImageStreamImageName(from.Name)
512
-		if err != nil {
513
-			glog.V(2).Infof("Error parsing ImageStreamImage name %q: %v - skipping", from.Name, err)
514
-			return
515
-		}
516
-		imageID = id
517
-	case "DockerImage":
518
-		ref, err := imageapi.ParseDockerImageReference(from.Name)
519
-		if err != nil {
520
-			glog.V(2).Infof("Error parsing DockerImage name %q: %v - skipping", from.Name, err)
521
-			return
522
-		}
523
-		imageID = ref.ID
524
-	default:
525
-		return
526
-	}
527
-
528
-	glog.V(4).Infof("Looking for image %q in graph", imageID)
529
-	imageNode := imagegraph.FindImage(g, imageID)
530
-	if imageNode == nil {
531
-		glog.V(4).Infof("Unable to find image %q in graph - skipping", imageID)
532
-		return
533
-	}
534
-
535
-	glog.V(4).Infof("Adding edge from %v to %v", predecessor, imageNode)
536
-	g.AddEdge(predecessor, imageNode, ReferencedImageEdgeKind)
537
-}
538
-
539
-// getImageNodes returns only nodes of type ImageNode.
540
-func getImageNodes(nodes []gonum.Node) []*imagegraph.ImageNode {
541
-	ret := []*imagegraph.ImageNode{}
542
-	for i := range nodes {
543
-		if node, ok := nodes[i].(*imagegraph.ImageNode); ok {
544
-			ret = append(ret, node)
545
-		}
546
-	}
547
-	return ret
548
-}
549
-
550
-// edgeKind returns true if the edge from "from" to "to" is of the desired kind.
551
-func edgeKind(g graph.Graph, from, to gonum.Node, desiredKind string) bool {
552
-	edge := g.Edge(from, to)
553
-	kinds := g.EdgeKinds(edge)
554
-	return kinds.Has(desiredKind)
555
-}
556
-
557
-// imageIsPrunable returns true iff the image node only has weak references
558
-// from its predecessors to it. A weak reference to an image is a reference
559
-// from an image stream to an image where the image is not the current image
560
-// for a tag and the image stream is at least as old as the minimum pruning
561
-// age.
562
-func imageIsPrunable(g graph.Graph, imageNode *imagegraph.ImageNode) bool {
563
-	onlyWeakReferences := true
564
-
565
-	for _, n := range g.To(imageNode) {
566
-		glog.V(4).Infof("Examining predecessor %#v", n)
567
-		if !edgeKind(g, n, imageNode, WeakReferencedImageEdgeKind) {
568
-			glog.V(4).Infof("Strong reference detected")
569
-			onlyWeakReferences = false
570
-			break
571
-		}
572
-	}
573
-
574
-	return onlyWeakReferences
575
-
576
-}
577
-
578
-// calculatePrunableImages returns the list of prunable images and a
579
-// graph.NodeSet containing the image node IDs.
580
-func calculatePrunableImages(g graph.Graph, imageNodes []*imagegraph.ImageNode) ([]*imagegraph.ImageNode, graph.NodeSet) {
581
-	prunable := []*imagegraph.ImageNode{}
582
-	ids := make(graph.NodeSet)
583
-
584
-	for _, imageNode := range imageNodes {
585
-		glog.V(4).Infof("Examining image %q", imageNode.Image.Name)
586
-
587
-		if imageIsPrunable(g, imageNode) {
588
-			glog.V(4).Infof("Image %q is prunable", imageNode.Image.Name)
589
-			prunable = append(prunable, imageNode)
590
-			ids.Add(imageNode.ID())
591
-		}
592
-	}
593
-
594
-	return prunable, ids
595
-}
596
-
597
-// subgraphWithoutPrunableImages creates a subgraph from g with prunable image
598
-// nodes excluded.
599
-func subgraphWithoutPrunableImages(g graph.Graph, prunableImageIDs graph.NodeSet) graph.Graph {
600
-	return g.Subgraph(
601
-		func(g graph.Interface, node gonum.Node) bool {
602
-			return !prunableImageIDs.Has(node.ID())
603
-		},
604
-		func(g graph.Interface, from, to gonum.Node, edgeKinds sets.String) bool {
605
-			if prunableImageIDs.Has(from.ID()) {
606
-				return false
607
-			}
608
-			if prunableImageIDs.Has(to.ID()) {
609
-				return false
610
-			}
611
-			return true
612
-		},
613
-	)
614
-}
615
-
616
-// calculatePrunableLayers returns the list of prunable layers.
617
-func calculatePrunableLayers(g graph.Graph) []*imagegraph.ImageLayerNode {
618
-	prunable := []*imagegraph.ImageLayerNode{}
619
-
620
-	nodes := g.Nodes()
621
-	for i := range nodes {
622
-		layerNode, ok := nodes[i].(*imagegraph.ImageLayerNode)
623
-		if !ok {
624
-			continue
625
-		}
626
-
627
-		glog.V(4).Infof("Examining layer %q", layerNode.Layer)
628
-
629
-		if layerIsPrunable(g, layerNode) {
630
-			glog.V(4).Infof("Layer %q is prunable", layerNode.Layer)
631
-			prunable = append(prunable, layerNode)
632
-		}
633
-	}
634
-
635
-	return prunable
636
-}
637
-
638
-// pruneStreams removes references from all image streams' status.tags entries
639
-// to prunable images, invoking streamPruner.PruneImageStream for each updated
640
-// stream.
641
-func pruneStreams(g graph.Graph, imageNodes []*imagegraph.ImageNode, streamPruner ImageStreamPruner) []error {
642
-	errs := []error{}
643
-
644
-	glog.V(4).Infof("Removing pruned image references from streams")
645
-	for _, imageNode := range imageNodes {
646
-		for _, n := range g.To(imageNode) {
647
-			streamNode, ok := n.(*imagegraph.ImageStreamNode)
648
-			if !ok {
649
-				continue
650
-			}
651
-
652
-			stream := streamNode.ImageStream
653
-			updatedTags := sets.NewString()
654
-
655
-			glog.V(4).Infof("Checking if ImageStream %s/%s has references to image %s in status.tags", stream.Namespace, stream.Name, imageNode.Image.Name)
656
-
657
-			for tag, history := range stream.Status.Tags {
658
-				glog.V(4).Infof("Checking tag %q", tag)
659
-
660
-				newHistory := imageapi.TagEventList{}
661
-
662
-				for i, tagEvent := range history.Items {
663
-					glog.V(4).Infof("Checking tag event %d with image %q", i, tagEvent.Image)
664
-
665
-					if tagEvent.Image != imageNode.Image.Name {
666
-						glog.V(4).Infof("Tag event doesn't match deleted image - keeping")
667
-						newHistory.Items = append(newHistory.Items, tagEvent)
668
-					} else {
669
-						glog.V(4).Infof("Tag event matches deleted image - removing reference")
670
-						updatedTags.Insert(tag)
671
-					}
672
-				}
673
-				if len(newHistory.Items) == 0 {
674
-					glog.V(4).Infof("Removing tag %q from status.tags of ImageStream %s/%s", tag, stream.Namespace, stream.Name)
675
-					delete(stream.Status.Tags, tag)
676
-				} else {
677
-					stream.Status.Tags[tag] = newHistory
678
-				}
679
-			}
680
-
681
-			updatedStream, err := streamPruner.PruneImageStream(stream, imageNode.Image, updatedTags.List())
682
-			if err != nil {
683
-				errs = append(errs, fmt.Errorf("error pruning image from stream: %v", err))
684
-				continue
685
-			}
686
-
687
-			streamNode.ImageStream = updatedStream
688
-		}
689
-	}
690
-	glog.V(4).Infof("Done removing pruned image references from streams")
691
-	return errs
692
-}
693
-
694
-// pruneImages invokes imagePruner.PruneImage with each image that is prunable.
695
-func pruneImages(g graph.Graph, imageNodes []*imagegraph.ImageNode, imagePruner ImagePruner) []error {
696
-	errs := []error{}
697
-
698
-	for _, imageNode := range imageNodes {
699
-		if err := imagePruner.PruneImage(imageNode.Image); err != nil {
700
-			errs = append(errs, fmt.Errorf("error pruning image %q: %v", imageNode.Image.Name, err))
701
-		}
702
-	}
703
-
704
-	return errs
705
-}
706
-
707
-func (p *imageRegistryPruner) determineRegistry(imageNodes []*imagegraph.ImageNode) (string, error) {
708
-	if len(p.registryURL) > 0 {
709
-		return p.registryURL, nil
710
-	}
711
-
712
-	// we only support a single internal registry, and all images have the same registry
713
-	// so we just take the 1st one and use it
714
-	pullSpec := imageNodes[0].Image.DockerImageReference
715
-
716
-	ref, err := imageapi.ParseDockerImageReference(pullSpec)
717
-	if err != nil {
718
-		return "", fmt.Errorf("unable to parse %q: %v", pullSpec, err)
719
-	}
720
-
721
-	if len(ref.Registry) == 0 {
722
-		return "", fmt.Errorf("%s does not include a registry", pullSpec)
723
-	}
724
-
725
-	return ref.Registry, nil
726
-}
727
-
728
-// Run identifies images eligible for pruning, invoking imagePruneFunc for each
729
-// image, and then it identifies layers eligible for pruning, invoking
730
-// layerPruneFunc for each registry URL that has layers that can be pruned.
731
-func (p *imageRegistryPruner) Prune(imagePruner ImagePruner, streamPruner ImageStreamPruner, layerPruner LayerPruner, blobPruner BlobPruner, manifestPruner ManifestPruner) error {
732
-	allNodes := p.g.Nodes()
733
-
734
-	imageNodes := getImageNodes(allNodes)
735
-	if len(imageNodes) == 0 {
736
-		return nil
737
-	}
738
-
739
-	registryURL, err := p.determineRegistry(imageNodes)
740
-	if err != nil {
741
-		return fmt.Errorf("unable to determine registry: %v", err)
742
-	}
743
-	glog.V(1).Infof("Using registry: %s", registryURL)
744
-
745
-	if err := p.registryPinger.ping(registryURL); err != nil {
746
-		return fmt.Errorf("error communicating with registry: %v", err)
747
-	}
748
-
749
-	prunableImageNodes, prunableImageIDs := calculatePrunableImages(p.g, imageNodes)
750
-	graphWithoutPrunableImages := subgraphWithoutPrunableImages(p.g, prunableImageIDs)
751
-	prunableLayers := calculatePrunableLayers(graphWithoutPrunableImages)
752
-
753
-	errs := []error{}
754
-
755
-	errs = append(errs, pruneStreams(p.g, prunableImageNodes, streamPruner)...)
756
-	errs = append(errs, pruneLayers(p.g, p.registryClient, registryURL, prunableLayers, layerPruner)...)
757
-	errs = append(errs, pruneBlobs(p.g, p.registryClient, registryURL, prunableLayers, blobPruner)...)
758
-	errs = append(errs, pruneManifests(p.g, p.registryClient, registryURL, prunableImageNodes, manifestPruner)...)
759
-
760
-	if len(errs) > 0 {
761
-		// If we had any errors removing image references from image streams or deleting
762
-		// layers, blobs, or manifest data from the registry, stop here and don't
763
-		// delete any images. This way, you can rerun prune and retry things that failed.
764
-		return kerrors.NewAggregate(errs)
765
-	}
766
-
767
-	errs = append(errs, pruneImages(p.g, prunableImageNodes, imagePruner)...)
768
-	return kerrors.NewAggregate(errs)
769
-}
770
-
771
-// layerIsPrunable returns true if the layer is not referenced by any images.
772
-func layerIsPrunable(g graph.Graph, layerNode *imagegraph.ImageLayerNode) bool {
773
-	for _, predecessor := range g.To(layerNode) {
774
-		glog.V(4).Infof("Examining layer predecessor %#v", predecessor)
775
-		if g.Kind(predecessor) == imagegraph.ImageNodeKind {
776
-			glog.V(4).Infof("Layer has an image predecessor")
777
-			return false
778
-		}
779
-	}
780
-
781
-	return true
782
-}
783
-
784
-// streamLayerReferences returns a list of ImageStreamNodes that reference a
785
-// given ImageLayerNode.
786
-func streamLayerReferences(g graph.Graph, layerNode *imagegraph.ImageLayerNode) []*imagegraph.ImageStreamNode {
787
-	ret := []*imagegraph.ImageStreamNode{}
788
-
789
-	for _, predecessor := range g.To(layerNode) {
790
-		if g.Kind(predecessor) != imagegraph.ImageStreamNodeKind {
791
-			continue
792
-		}
793
-
794
-		ret = append(ret, predecessor.(*imagegraph.ImageStreamNode))
795
-	}
796
-
797
-	return ret
798
-}
799
-
800
-// pruneLayers invokes layerPruner.PruneLayer for each repository layer link to
801
-// be deleted from the registry.
802
-func pruneLayers(g graph.Graph, registryClient *http.Client, registryURL string, layerNodes []*imagegraph.ImageLayerNode, layerPruner LayerPruner) []error {
803
-	errs := []error{}
804
-
805
-	for _, layerNode := range layerNodes {
806
-		// get streams that reference layer
807
-		streamNodes := streamLayerReferences(g, layerNode)
808
-
809
-		for _, streamNode := range streamNodes {
810
-			stream := streamNode.ImageStream
811
-			streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)
812
-
813
-			glog.V(4).Infof("Pruning registry=%q, repo=%q, layer=%q", registryURL, streamName, layerNode.Layer)
814
-			if err := layerPruner.PruneLayer(registryClient, registryURL, streamName, layerNode.Layer); err != nil {
815
-				errs = append(errs, fmt.Errorf("error pruning repo %q layer link %q: %v", streamName, layerNode.Layer, err))
816
-			}
817
-		}
818
-	}
819
-
820
-	return errs
821
-}
822
-
823
-// pruneBlobs invokes blobPruner.PruneBlob for each blob to be deleted from the
824
-// registry.
825
-func pruneBlobs(g graph.Graph, registryClient *http.Client, registryURL string, layerNodes []*imagegraph.ImageLayerNode, blobPruner BlobPruner) []error {
826
-	errs := []error{}
827
-
828
-	for _, layerNode := range layerNodes {
829
-		glog.V(4).Infof("Pruning registry=%q, blob=%q", registryURL, layerNode.Layer)
830
-		if err := blobPruner.PruneBlob(registryClient, registryURL, layerNode.Layer); err != nil {
831
-			errs = append(errs, fmt.Errorf("error pruning blob %q: %v", layerNode.Layer, err))
832
-		}
833
-	}
834
-
835
-	return errs
836
-}
837
-
838
-// pruneManifests invokes manifestPruner.PruneManifest for each repository
839
-// manifest to be deleted from the registry.
840
-func pruneManifests(g graph.Graph, registryClient *http.Client, registryURL string, imageNodes []*imagegraph.ImageNode, manifestPruner ManifestPruner) []error {
841
-	errs := []error{}
842
-
843
-	for _, imageNode := range imageNodes {
844
-		for _, n := range g.To(imageNode) {
845
-			streamNode, ok := n.(*imagegraph.ImageStreamNode)
846
-			if !ok {
847
-				continue
848
-			}
849
-
850
-			stream := streamNode.ImageStream
851
-			repoName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)
852
-
853
-			glog.V(4).Infof("Pruning manifest for registry %q, repo %q, image %q", registryURL, repoName, imageNode.Image.Name)
854
-			if err := manifestPruner.PruneManifest(registryClient, registryURL, repoName, imageNode.Image.Name); err != nil {
855
-				errs = append(errs, fmt.Errorf("error pruning manifest for registry %q, repo %q, image %q: %v", registryURL, repoName, imageNode.Image.Name, err))
856
-			}
857
-		}
858
-	}
859
-
860
-	return errs
861
-}
862
-
863
-// deletingImagePruner deletes an image from OpenShift.
864
-type deletingImagePruner struct {
865
-	images client.ImageInterface
866
-}
867
-
868
-var _ ImagePruner = &deletingImagePruner{}
869
-
870
-// NewDeletingImagePruner creates a new deletingImagePruner.
871
-func NewDeletingImagePruner(images client.ImageInterface) ImagePruner {
872
-	return &deletingImagePruner{
873
-		images: images,
874
-	}
875
-}
876
-
877
-func (p *deletingImagePruner) PruneImage(image *imageapi.Image) error {
878
-	glog.V(4).Infof("Deleting image %q", image.Name)
879
-	return p.images.Delete(image.Name)
880
-}
881
-
882
-// deletingImageStreamPruner updates an image stream in OpenShift.
883
-type deletingImageStreamPruner struct {
884
-	streams client.ImageStreamsNamespacer
885
-}
886
-
887
-var _ ImageStreamPruner = &deletingImageStreamPruner{}
888
-
889
-// NewDeletingImageStreamPruner creates a new deletingImageStreamPruner.
890
-func NewDeletingImageStreamPruner(streams client.ImageStreamsNamespacer) ImageStreamPruner {
891
-	return &deletingImageStreamPruner{
892
-		streams: streams,
893
-	}
894
-}
895
-
896
-func (p *deletingImageStreamPruner) PruneImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error) {
897
-	glog.V(4).Infof("Updating ImageStream %s/%s", stream.Namespace, stream.Name)
898
-	glog.V(5).Infof("Updated stream: %#v", stream)
899
-	return p.streams.ImageStreams(stream.Namespace).UpdateStatus(stream)
900
-}
901
-
902
-// deleteFromRegistry uses registryClient to send a DELETE request to the
903
-// provided url. It attempts an https request first; if that fails, it fails
904
-// back to http.
905
-func deleteFromRegistry(registryClient *http.Client, url string) error {
906
-	deleteFunc := func(proto, url string) error {
907
-		req, err := http.NewRequest("DELETE", url, nil)
908
-		if err != nil {
909
-			glog.Errorf("Error creating request: %v", err)
910
-			return fmt.Errorf("error creating request: %v", err)
911
-		}
912
-
913
-		glog.V(4).Infof("Sending request to registry")
914
-		resp, err := registryClient.Do(req)
915
-		if err != nil {
916
-			return fmt.Errorf("error sending request: %v", err)
917
-		}
918
-		defer resp.Body.Close()
919
-
920
-		// TODO: investigate why we're getting non-existent layers, for now we're logging
921
-		// them out and continue working
922
-		if resp.StatusCode == http.StatusNotFound {
923
-			glog.Warningf("Unable to prune layer %s, returned %v", url, resp.Status)
924
-			return nil
925
-		}
926
-		// non-2xx/3xx response doesn't cause an error, so we need to check for it
927
-		// manually and return it to caller
928
-		if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
929
-			return fmt.Errorf(resp.Status)
930
-		}
931
-		if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusAccepted {
932
-			glog.V(1).Infof("Unexpected status code in response: %d", resp.StatusCode)
933
-			var response errcode.Errors
934
-			decoder := json.NewDecoder(resp.Body)
935
-			if err := decoder.Decode(&response); err != nil {
936
-				return err
937
-			}
938
-			glog.V(1).Infof("Response: %#v", response)
939
-			return &response
940
-		}
941
-
942
-		return nil
943
-	}
944
-
945
-	var err error
946
-	for _, proto := range []string{"https", "http"} {
947
-		glog.V(4).Infof("Trying %s for %s", proto, url)
948
-		err = deleteFunc(proto, fmt.Sprintf("%s://%s", proto, url))
949
-		if err == nil {
950
-			return nil
951
-		}
952
-
953
-		if _, ok := err.(*errcode.Errors); ok {
954
-			// we got a response back from the registry, so return it
955
-			return err
956
-		}
957
-
958
-		// we didn't get a success or a errcode.Errors response back from the registry
959
-		glog.V(4).Infof("Error with %s for %s: %v", proto, url, err)
960
-	}
961
-	return err
962
-}
963
-
964
-// deletingLayerPruner deletes a repository layer link from the registry.
965
-type deletingLayerPruner struct {
966
-}
967
-
968
-var _ LayerPruner = &deletingLayerPruner{}
969
-
970
-// NewDeletingLayerPruner creates a new deletingLayerPruner.
971
-func NewDeletingLayerPruner() LayerPruner {
972
-	return &deletingLayerPruner{}
973
-}
974
-
975
-func (p *deletingLayerPruner) PruneLayer(registryClient *http.Client, registryURL, repoName, layer string) error {
976
-	glog.V(4).Infof("Pruning registry %q, repo %q, layer %q", registryURL, repoName, layer)
977
-	return deleteFromRegistry(registryClient, fmt.Sprintf("%s/v2/%s/blobs/%s", registryURL, repoName, layer))
978
-}
979
-
980
-// deletingBlobPruner deletes a blob from the registry.
981
-type deletingBlobPruner struct {
982
-}
983
-
984
-var _ BlobPruner = &deletingBlobPruner{}
985
-
986
-// NewDeletingLayerPruner creates a new deletingBlobPruner.
987
-func NewDeletingBlobPruner() BlobPruner {
988
-	return &deletingBlobPruner{}
989
-}
990
-
991
-func (p *deletingBlobPruner) PruneBlob(registryClient *http.Client, registryURL, blob string) error {
992
-	glog.V(4).Infof("Pruning registry %q, blob %q", registryURL, blob)
993
-	return deleteFromRegistry(registryClient, fmt.Sprintf("%s/admin/blobs/%s", registryURL, blob))
994
-}
995
-
996
-// deletingManifestPruner deletes repository manifest data from the registry.
997
-type deletingManifestPruner struct {
998
-}
999
-
1000
-var _ ManifestPruner = &deletingManifestPruner{}
1001
-
1002
-// NewDeletingManifestPruner creates a new deletingManifestPruner.
1003
-func NewDeletingManifestPruner() ManifestPruner {
1004
-	return &deletingManifestPruner{}
1005
-}
1006
-
1007
-func (p *deletingManifestPruner) PruneManifest(registryClient *http.Client, registryURL, repoName, manifest string) error {
1008
-	glog.V(4).Infof("Pruning manifest for registry %q, repo %q, manifest %q", registryURL, repoName, manifest)
1009
-	return deleteFromRegistry(registryClient, fmt.Sprintf("%s/v2/%s/manifests/%s", registryURL, repoName, manifest))
1010
-}
1011 1
deleted file mode 100644
... ...
@@ -1,915 +0,0 @@
1
-package prune
2
-
3
-import (
4
-	"bytes"
5
-	"encoding/json"
6
-	"errors"
7
-	"flag"
8
-	"fmt"
9
-	"io/ioutil"
10
-	"net/http"
11
-	"reflect"
12
-	"testing"
13
-	"time"
14
-
15
-	kapi "k8s.io/kubernetes/pkg/api"
16
-	"k8s.io/kubernetes/pkg/api/unversioned"
17
-	"k8s.io/kubernetes/pkg/client/unversioned/fake"
18
-	ktc "k8s.io/kubernetes/pkg/client/unversioned/testclient"
19
-	"k8s.io/kubernetes/pkg/runtime"
20
-	"k8s.io/kubernetes/pkg/util/sets"
21
-
22
-	buildapi "github.com/openshift/origin/pkg/build/api"
23
-	"github.com/openshift/origin/pkg/client/testclient"
24
-	deployapi "github.com/openshift/origin/pkg/deploy/api"
25
-	imageapi "github.com/openshift/origin/pkg/image/api"
26
-)
27
-
28
-type fakeRegistryPinger struct {
29
-	err      error
30
-	requests []string
31
-}
32
-
33
-func (f *fakeRegistryPinger) ping(registry string) error {
34
-	f.requests = append(f.requests, registry)
35
-	return f.err
36
-}
37
-
38
-func imageList(images ...imageapi.Image) imageapi.ImageList {
39
-	return imageapi.ImageList{
40
-		Items: images,
41
-	}
42
-}
43
-
44
-func agedImage(id, ref string, ageInMinutes int64) imageapi.Image {
45
-	image := imageWithLayers(id, ref,
46
-		"tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
47
-		"tarsum.dev+sha256:b194de3772ebbcdc8f244f663669799ac1cb141834b7cb8b69100285d357a2b0",
48
-		"tarsum.dev+sha256:c937c4bb1c1a21cc6d94340812262c6472092028972ae69b551b1a70d4276171",
49
-		"tarsum.dev+sha256:2aaacc362ac6be2b9e9ae8c6029f6f616bb50aec63746521858e47841b90fabd",
50
-		"tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
51
-	)
52
-
53
-	if ageInMinutes >= 0 {
54
-		image.CreationTimestamp = unversioned.NewTime(unversioned.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute))
55
-	}
56
-
57
-	return image
58
-}
59
-
60
-func image(id, ref string) imageapi.Image {
61
-	return agedImage(id, ref, -1)
62
-}
63
-
64
-func imageWithLayers(id, ref string, layers ...string) imageapi.Image {
65
-	image := imageapi.Image{
66
-		ObjectMeta: kapi.ObjectMeta{
67
-			Name: id,
68
-			Annotations: map[string]string{
69
-				imageapi.ManagedByOpenShiftAnnotation: "true",
70
-			},
71
-		},
72
-		DockerImageReference: ref,
73
-	}
74
-
75
-	manifest := imageapi.DockerImageManifest{
76
-		FSLayers: []imageapi.DockerFSLayer{},
77
-	}
78
-
79
-	for _, layer := range layers {
80
-		manifest.FSLayers = append(manifest.FSLayers, imageapi.DockerFSLayer{DockerBlobSum: layer})
81
-	}
82
-
83
-	manifestBytes, err := json.Marshal(&manifest)
84
-	if err != nil {
85
-		panic(err)
86
-	}
87
-
88
-	image.DockerImageManifest = string(manifestBytes)
89
-
90
-	return image
91
-}
92
-
93
-func unmanagedImage(id, ref string, hasAnnotations bool, annotation, value string) imageapi.Image {
94
-	image := imageWithLayers(id, ref)
95
-	if !hasAnnotations {
96
-		image.Annotations = nil
97
-	} else {
98
-		delete(image.Annotations, imageapi.ManagedByOpenShiftAnnotation)
99
-		image.Annotations[annotation] = value
100
-	}
101
-	return image
102
-}
103
-
104
-func imageWithBadManifest(id, ref string) imageapi.Image {
105
-	image := image(id, ref)
106
-	image.DockerImageManifest = "asdf"
107
-	return image
108
-}
109
-
110
-func podList(pods ...kapi.Pod) kapi.PodList {
111
-	return kapi.PodList{
112
-		Items: pods,
113
-	}
114
-}
115
-
116
-func pod(namespace, name string, phase kapi.PodPhase, containerImages ...string) kapi.Pod {
117
-	return agedPod(namespace, name, phase, -1, containerImages...)
118
-}
119
-
120
-func agedPod(namespace, name string, phase kapi.PodPhase, ageInMinutes int64, containerImages ...string) kapi.Pod {
121
-	pod := kapi.Pod{
122
-		ObjectMeta: kapi.ObjectMeta{
123
-			Namespace: namespace,
124
-			Name:      name,
125
-		},
126
-		Spec: podSpec(containerImages...),
127
-		Status: kapi.PodStatus{
128
-			Phase: phase,
129
-		},
130
-	}
131
-
132
-	if ageInMinutes >= 0 {
133
-		pod.CreationTimestamp = unversioned.NewTime(unversioned.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute))
134
-	}
135
-
136
-	return pod
137
-}
138
-
139
-func podSpec(containerImages ...string) kapi.PodSpec {
140
-	spec := kapi.PodSpec{
141
-		Containers: []kapi.Container{},
142
-	}
143
-	for _, image := range containerImages {
144
-		container := kapi.Container{
145
-			Image: image,
146
-		}
147
-		spec.Containers = append(spec.Containers, container)
148
-	}
149
-	return spec
150
-}
151
-
152
-func streamList(streams ...imageapi.ImageStream) imageapi.ImageStreamList {
153
-	return imageapi.ImageStreamList{
154
-		Items: streams,
155
-	}
156
-}
157
-
158
-func stream(registry, namespace, name string, tags map[string]imageapi.TagEventList) imageapi.ImageStream {
159
-	return agedStream(registry, namespace, name, -1, tags)
160
-}
161
-
162
-func agedStream(registry, namespace, name string, ageInMinutes int64, tags map[string]imageapi.TagEventList) imageapi.ImageStream {
163
-	stream := imageapi.ImageStream{
164
-		ObjectMeta: kapi.ObjectMeta{
165
-			Namespace: namespace,
166
-			Name:      name,
167
-		},
168
-		Status: imageapi.ImageStreamStatus{
169
-			DockerImageRepository: fmt.Sprintf("%s/%s/%s", registry, namespace, name),
170
-			Tags: tags,
171
-		},
172
-	}
173
-
174
-	if ageInMinutes >= 0 {
175
-		stream.CreationTimestamp = unversioned.NewTime(unversioned.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute))
176
-	}
177
-
178
-	return stream
179
-}
180
-
181
-func streamPtr(registry, namespace, name string, tags map[string]imageapi.TagEventList) *imageapi.ImageStream {
182
-	s := stream(registry, namespace, name, tags)
183
-	return &s
184
-}
185
-
186
-func tags(list ...namedTagEventList) map[string]imageapi.TagEventList {
187
-	m := make(map[string]imageapi.TagEventList, len(list))
188
-	for _, tag := range list {
189
-		m[tag.name] = tag.events
190
-	}
191
-	return m
192
-}
193
-
194
-type namedTagEventList struct {
195
-	name   string
196
-	events imageapi.TagEventList
197
-}
198
-
199
-func tag(name string, events ...imageapi.TagEvent) namedTagEventList {
200
-	return namedTagEventList{
201
-		name: name,
202
-		events: imageapi.TagEventList{
203
-			Items: events,
204
-		},
205
-	}
206
-}
207
-
208
-func tagEvent(id, ref string) imageapi.TagEvent {
209
-	return imageapi.TagEvent{
210
-		Image:                id,
211
-		DockerImageReference: ref,
212
-	}
213
-}
214
-
215
-func rcList(rcs ...kapi.ReplicationController) kapi.ReplicationControllerList {
216
-	return kapi.ReplicationControllerList{
217
-		Items: rcs,
218
-	}
219
-}
220
-
221
-func rc(namespace, name string, containerImages ...string) kapi.ReplicationController {
222
-	return kapi.ReplicationController{
223
-		ObjectMeta: kapi.ObjectMeta{
224
-			Namespace: namespace,
225
-			Name:      name,
226
-		},
227
-		Spec: kapi.ReplicationControllerSpec{
228
-			Template: &kapi.PodTemplateSpec{
229
-				Spec: podSpec(containerImages...),
230
-			},
231
-		},
232
-	}
233
-}
234
-
235
-func dcList(dcs ...deployapi.DeploymentConfig) deployapi.DeploymentConfigList {
236
-	return deployapi.DeploymentConfigList{
237
-		Items: dcs,
238
-	}
239
-}
240
-
241
-func dc(namespace, name string, containerImages ...string) deployapi.DeploymentConfig {
242
-	return deployapi.DeploymentConfig{
243
-		ObjectMeta: kapi.ObjectMeta{
244
-			Namespace: namespace,
245
-			Name:      name,
246
-		},
247
-		Spec: deployapi.DeploymentConfigSpec{
248
-			Template: &kapi.PodTemplateSpec{
249
-				Spec: podSpec(containerImages...),
250
-			},
251
-		},
252
-	}
253
-}
254
-
255
-func bcList(bcs ...buildapi.BuildConfig) buildapi.BuildConfigList {
256
-	return buildapi.BuildConfigList{
257
-		Items: bcs,
258
-	}
259
-}
260
-
261
-func bc(namespace, name, strategyType, fromKind, fromNamespace, fromName string) buildapi.BuildConfig {
262
-	return buildapi.BuildConfig{
263
-		ObjectMeta: kapi.ObjectMeta{
264
-			Namespace: namespace,
265
-			Name:      name,
266
-		},
267
-		Spec: buildapi.BuildConfigSpec{
268
-			CommonSpec: commonSpec(strategyType, fromKind, fromNamespace, fromName),
269
-		},
270
-	}
271
-}
272
-
273
-func buildList(builds ...buildapi.Build) buildapi.BuildList {
274
-	return buildapi.BuildList{
275
-		Items: builds,
276
-	}
277
-}
278
-
279
-func build(namespace, name, strategyType, fromKind, fromNamespace, fromName string) buildapi.Build {
280
-	return buildapi.Build{
281
-		ObjectMeta: kapi.ObjectMeta{
282
-			Namespace: namespace,
283
-			Name:      name,
284
-		},
285
-		Spec: buildapi.BuildSpec{
286
-			CommonSpec: commonSpec(strategyType, fromKind, fromNamespace, fromName),
287
-		},
288
-	}
289
-}
290
-
291
-func commonSpec(strategyType, fromKind, fromNamespace, fromName string) buildapi.CommonSpec {
292
-	spec := buildapi.CommonSpec{
293
-		Strategy: buildapi.BuildStrategy{},
294
-	}
295
-	switch strategyType {
296
-	case "source":
297
-		spec.Strategy.SourceStrategy = &buildapi.SourceBuildStrategy{
298
-			From: kapi.ObjectReference{
299
-				Kind:      fromKind,
300
-				Namespace: fromNamespace,
301
-				Name:      fromName,
302
-			},
303
-		}
304
-	case "docker":
305
-		spec.Strategy.DockerStrategy = &buildapi.DockerBuildStrategy{
306
-			From: &kapi.ObjectReference{
307
-				Kind:      fromKind,
308
-				Namespace: fromNamespace,
309
-				Name:      fromName,
310
-			},
311
-		}
312
-	case "custom":
313
-		spec.Strategy.CustomStrategy = &buildapi.CustomBuildStrategy{
314
-			From: kapi.ObjectReference{
315
-				Kind:      fromKind,
316
-				Namespace: fromNamespace,
317
-				Name:      fromName,
318
-			},
319
-		}
320
-	}
321
-
322
-	return spec
323
-}
324
-
325
-type fakeImagePruner struct {
326
-	invocations sets.String
327
-	err         error
328
-}
329
-
330
-var _ ImagePruner = &fakeImagePruner{}
331
-
332
-func (p *fakeImagePruner) PruneImage(image *imageapi.Image) error {
333
-	p.invocations.Insert(image.Name)
334
-	return p.err
335
-}
336
-
337
-type fakeImageStreamPruner struct {
338
-	invocations sets.String
339
-	err         error
340
-}
341
-
342
-var _ ImageStreamPruner = &fakeImageStreamPruner{}
343
-
344
-func (p *fakeImageStreamPruner) PruneImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error) {
345
-	p.invocations.Insert(fmt.Sprintf("%s/%s|%s", stream.Namespace, stream.Name, image.Name))
346
-	return stream, p.err
347
-}
348
-
349
-type fakeBlobPruner struct {
350
-	invocations sets.String
351
-	err         error
352
-}
353
-
354
-var _ BlobPruner = &fakeBlobPruner{}
355
-
356
-func (p *fakeBlobPruner) PruneBlob(registryClient *http.Client, registryURL, blob string) error {
357
-	p.invocations.Insert(fmt.Sprintf("%s|%s", registryURL, blob))
358
-	return p.err
359
-}
360
-
361
-type fakeLayerPruner struct {
362
-	invocations sets.String
363
-	err         error
364
-}
365
-
366
-var _ LayerPruner = &fakeLayerPruner{}
367
-
368
-func (p *fakeLayerPruner) PruneLayer(registryClient *http.Client, registryURL, repo, layer string) error {
369
-	p.invocations.Insert(fmt.Sprintf("%s|%s|%s", registryURL, repo, layer))
370
-	return p.err
371
-}
372
-
373
-type fakeManifestPruner struct {
374
-	invocations sets.String
375
-	err         error
376
-}
377
-
378
-var _ ManifestPruner = &fakeManifestPruner{}
379
-
380
-func (p *fakeManifestPruner) PruneManifest(registryClient *http.Client, registryURL, repo, manifest string) error {
381
-	p.invocations.Insert(fmt.Sprintf("%s|%s|%s", registryURL, repo, manifest))
382
-	return p.err
383
-}
384
-
385
-var logLevel = flag.Int("loglevel", 0, "")
386
-var testCase = flag.String("testcase", "", "")
387
-
388
-func TestImagePruning(t *testing.T) {
389
-	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
390
-	registryURL := "registry"
391
-
392
-	tests := map[string]struct {
393
-		registryURLs           []string
394
-		images                 imageapi.ImageList
395
-		pods                   kapi.PodList
396
-		streams                imageapi.ImageStreamList
397
-		rcs                    kapi.ReplicationControllerList
398
-		bcs                    buildapi.BuildConfigList
399
-		builds                 buildapi.BuildList
400
-		dcs                    deployapi.DeploymentConfigList
401
-		expectedDeletions      []string
402
-		expectedUpdatedStreams []string
403
-	}{
404
-		"1 pod - phase pending - don't prune": {
405
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
406
-			pods:              podList(pod("foo", "pod1", kapi.PodPending, registryURL+"/foo/bar@id")),
407
-			expectedDeletions: []string{},
408
-		},
409
-		"3 pods - last phase pending - don't prune": {
410
-			images: imageList(image("id", registryURL+"/foo/bar@id")),
411
-			pods: podList(
412
-				pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id"),
413
-				pod("foo", "pod2", kapi.PodSucceeded, registryURL+"/foo/bar@id"),
414
-				pod("foo", "pod3", kapi.PodPending, registryURL+"/foo/bar@id"),
415
-			),
416
-			expectedDeletions: []string{},
417
-		},
418
-		"1 pod - phase running - don't prune": {
419
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
420
-			pods:              podList(pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@id")),
421
-			expectedDeletions: []string{},
422
-		},
423
-		"3 pods - last phase running - don't prune": {
424
-			images: imageList(image("id", registryURL+"/foo/bar@id")),
425
-			pods: podList(
426
-				pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id"),
427
-				pod("foo", "pod2", kapi.PodSucceeded, registryURL+"/foo/bar@id"),
428
-				pod("foo", "pod3", kapi.PodRunning, registryURL+"/foo/bar@id"),
429
-			),
430
-			expectedDeletions: []string{},
431
-		},
432
-		"pod phase succeeded - prune": {
433
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
434
-			pods:              podList(pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id")),
435
-			expectedDeletions: []string{"id"},
436
-		},
437
-		"pod phase succeeded, pod less than min pruning age - don't prune": {
438
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
439
-			pods:              podList(agedPod("foo", "pod1", kapi.PodSucceeded, 5, registryURL+"/foo/bar@id")),
440
-			expectedDeletions: []string{},
441
-		},
442
-		"pod phase succeeded, image less than min pruning age - don't prune": {
443
-			images:            imageList(agedImage("id", registryURL+"/foo/bar@id", 5)),
444
-			pods:              podList(pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id")),
445
-			expectedDeletions: []string{},
446
-		},
447
-		"pod phase failed - prune": {
448
-			images: imageList(image("id", registryURL+"/foo/bar@id")),
449
-			pods: podList(
450
-				pod("foo", "pod1", kapi.PodFailed, registryURL+"/foo/bar@id"),
451
-				pod("foo", "pod2", kapi.PodFailed, registryURL+"/foo/bar@id"),
452
-				pod("foo", "pod3", kapi.PodFailed, registryURL+"/foo/bar@id"),
453
-			),
454
-			expectedDeletions: []string{"id"},
455
-		},
456
-		"pod phase unknown - prune": {
457
-			images: imageList(image("id", registryURL+"/foo/bar@id")),
458
-			pods: podList(
459
-				pod("foo", "pod1", kapi.PodUnknown, registryURL+"/foo/bar@id"),
460
-				pod("foo", "pod2", kapi.PodUnknown, registryURL+"/foo/bar@id"),
461
-				pod("foo", "pod3", kapi.PodUnknown, registryURL+"/foo/bar@id"),
462
-			),
463
-			expectedDeletions: []string{"id"},
464
-		},
465
-		"pod container image not parsable": {
466
-			images: imageList(image("id", registryURL+"/foo/bar@id")),
467
-			pods: podList(
468
-				pod("foo", "pod1", kapi.PodRunning, "a/b/c/d/e"),
469
-			),
470
-			expectedDeletions: []string{"id"},
471
-		},
472
-		"pod container image doesn't have an id": {
473
-			images: imageList(image("id", registryURL+"/foo/bar@id")),
474
-			pods: podList(
475
-				pod("foo", "pod1", kapi.PodRunning, "foo/bar:latest"),
476
-			),
477
-			expectedDeletions: []string{"id"},
478
-		},
479
-		"pod refers to image not in graph": {
480
-			images: imageList(image("id", registryURL+"/foo/bar@id")),
481
-			pods: podList(
482
-				pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@otherid"),
483
-			),
484
-			expectedDeletions: []string{"id"},
485
-		},
486
-		"referenced by rc - don't prune": {
487
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
488
-			rcs:               rcList(rc("foo", "rc1", registryURL+"/foo/bar@id")),
489
-			expectedDeletions: []string{},
490
-		},
491
-		"referenced by dc - don't prune": {
492
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
493
-			dcs:               dcList(dc("foo", "rc1", registryURL+"/foo/bar@id")),
494
-			expectedDeletions: []string{},
495
-		},
496
-		"referenced by bc - sti - ImageStreamImage - don't prune": {
497
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
498
-			bcs:               bcList(bc("foo", "bc1", "source", "ImageStreamImage", "foo", "bar@id")),
499
-			expectedDeletions: []string{},
500
-		},
501
-		"referenced by bc - docker - ImageStreamImage - don't prune": {
502
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
503
-			bcs:               bcList(bc("foo", "bc1", "docker", "ImageStreamImage", "foo", "bar@id")),
504
-			expectedDeletions: []string{},
505
-		},
506
-		"referenced by bc - custom - ImageStreamImage - don't prune": {
507
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
508
-			bcs:               bcList(bc("foo", "bc1", "custom", "ImageStreamImage", "foo", "bar@id")),
509
-			expectedDeletions: []string{},
510
-		},
511
-		"referenced by bc - sti - DockerImage - don't prune": {
512
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
513
-			bcs:               bcList(bc("foo", "bc1", "source", "DockerImage", "foo", registryURL+"/foo/bar@id")),
514
-			expectedDeletions: []string{},
515
-		},
516
-		"referenced by bc - docker - DockerImage - don't prune": {
517
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
518
-			bcs:               bcList(bc("foo", "bc1", "docker", "DockerImage", "foo", registryURL+"/foo/bar@id")),
519
-			expectedDeletions: []string{},
520
-		},
521
-		"referenced by bc - custom - DockerImage - don't prune": {
522
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
523
-			bcs:               bcList(bc("foo", "bc1", "custom", "DockerImage", "foo", registryURL+"/foo/bar@id")),
524
-			expectedDeletions: []string{},
525
-		},
526
-		"referenced by build - sti - ImageStreamImage - don't prune": {
527
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
528
-			builds:            buildList(build("foo", "build1", "source", "ImageStreamImage", "foo", "bar@id")),
529
-			expectedDeletions: []string{},
530
-		},
531
-		"referenced by build - docker - ImageStreamImage - don't prune": {
532
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
533
-			builds:            buildList(build("foo", "build1", "docker", "ImageStreamImage", "foo", "bar@id")),
534
-			expectedDeletions: []string{},
535
-		},
536
-		"referenced by build - custom - ImageStreamImage - don't prune": {
537
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
538
-			builds:            buildList(build("foo", "build1", "custom", "ImageStreamImage", "foo", "bar@id")),
539
-			expectedDeletions: []string{},
540
-		},
541
-		"referenced by build - sti - DockerImage - don't prune": {
542
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
543
-			builds:            buildList(build("foo", "build1", "source", "DockerImage", "foo", registryURL+"/foo/bar@id")),
544
-			expectedDeletions: []string{},
545
-		},
546
-		"referenced by build - docker - DockerImage - don't prune": {
547
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
548
-			builds:            buildList(build("foo", "build1", "docker", "DockerImage", "foo", registryURL+"/foo/bar@id")),
549
-			expectedDeletions: []string{},
550
-		},
551
-		"referenced by build - custom - DockerImage - don't prune": {
552
-			images:            imageList(image("id", registryURL+"/foo/bar@id")),
553
-			builds:            buildList(build("foo", "build1", "custom", "DockerImage", "foo", registryURL+"/foo/bar@id")),
554
-			expectedDeletions: []string{},
555
-		},
556
-		"image stream - keep most recent n images": {
557
-			images: imageList(
558
-				unmanagedImage("id", "otherregistry/foo/bar@id", false, "", ""),
559
-				image("id2", registryURL+"/foo/bar@id2"),
560
-				image("id3", registryURL+"/foo/bar@id3"),
561
-				image("id4", registryURL+"/foo/bar@id4"),
562
-			),
563
-			streams: streamList(
564
-				stream(registryURL, "foo", "bar", tags(
565
-					tag("latest",
566
-						tagEvent("id", "otherregistry/foo/bar@id"),
567
-						tagEvent("id2", registryURL+"/foo/bar@id2"),
568
-						tagEvent("id3", registryURL+"/foo/bar@id3"),
569
-						tagEvent("id4", registryURL+"/foo/bar@id4"),
570
-					),
571
-				)),
572
-			),
573
-			expectedDeletions:      []string{"id4"},
574
-			expectedUpdatedStreams: []string{"foo/bar|id4"},
575
-		},
576
-		"image stream - same manifest listed multiple times in tag history": {
577
-			images: imageList(
578
-				image("id1", registryURL+"/foo/bar@id1"),
579
-				image("id2", registryURL+"/foo/bar@id2"),
580
-			),
581
-			streams: streamList(
582
-				stream(registryURL, "foo", "bar", tags(
583
-					tag("latest",
584
-						tagEvent("id1", registryURL+"/foo/bar@id1"),
585
-						tagEvent("id2", registryURL+"/foo/bar@id2"),
586
-						tagEvent("id1", registryURL+"/foo/bar@id1"),
587
-						tagEvent("id2", registryURL+"/foo/bar@id2"),
588
-					),
589
-				)),
590
-			),
591
-		},
592
-		"image stream age less than min pruning age - don't prune": {
593
-			images: imageList(
594
-				image("id", registryURL+"/foo/bar@id"),
595
-				image("id2", registryURL+"/foo/bar@id2"),
596
-				image("id3", registryURL+"/foo/bar@id3"),
597
-				image("id4", registryURL+"/foo/bar@id4"),
598
-			),
599
-			streams: streamList(
600
-				agedStream(registryURL, "foo", "bar", 5, tags(
601
-					tag("latest",
602
-						tagEvent("id", registryURL+"/foo/bar@id"),
603
-						tagEvent("id2", registryURL+"/foo/bar@id2"),
604
-						tagEvent("id3", registryURL+"/foo/bar@id3"),
605
-						tagEvent("id4", registryURL+"/foo/bar@id4"),
606
-					),
607
-				)),
608
-			),
609
-			expectedDeletions:      []string{},
610
-			expectedUpdatedStreams: []string{},
611
-		},
612
-		"multiple resources pointing to image - don't prune": {
613
-			images: imageList(
614
-				image("id", registryURL+"/foo/bar@id"),
615
-				image("id2", registryURL+"/foo/bar@id2"),
616
-			),
617
-			streams: streamList(
618
-				stream(registryURL, "foo", "bar", tags(
619
-					tag("latest",
620
-						tagEvent("id", registryURL+"/foo/bar@id"),
621
-						tagEvent("id2", registryURL+"/foo/bar@id2"),
622
-					),
623
-				)),
624
-			),
625
-			rcs:                    rcList(rc("foo", "rc1", registryURL+"/foo/bar@id2")),
626
-			pods:                   podList(pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@id2")),
627
-			dcs:                    dcList(dc("foo", "rc1", registryURL+"/foo/bar@id")),
628
-			bcs:                    bcList(bc("foo", "bc1", "source", "DockerImage", "foo", registryURL+"/foo/bar@id")),
629
-			builds:                 buildList(build("foo", "build1", "custom", "ImageStreamImage", "foo", "bar@id")),
630
-			expectedDeletions:      []string{},
631
-			expectedUpdatedStreams: []string{},
632
-		},
633
-		"image with nil annotations": {
634
-			images: imageList(
635
-				unmanagedImage("id", "someregistry/foo/bar@id", false, "", ""),
636
-			),
637
-			expectedDeletions:      []string{},
638
-			expectedUpdatedStreams: []string{},
639
-		},
640
-		"image missing managed annotation": {
641
-			images: imageList(
642
-				unmanagedImage("id", "someregistry/foo/bar@id", true, "foo", "bar"),
643
-			),
644
-			expectedDeletions:      []string{},
645
-			expectedUpdatedStreams: []string{},
646
-		},
647
-		"image with managed annotation != true": {
648
-			images: imageList(
649
-				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "false"),
650
-				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "0"),
651
-				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "1"),
652
-				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "True"),
653
-				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "yes"),
654
-				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "Yes"),
655
-			),
656
-			expectedDeletions:      []string{},
657
-			expectedUpdatedStreams: []string{},
658
-		},
659
-		"image with bad manifest is pruned ok": {
660
-			images: imageList(
661
-				imageWithBadManifest("id", "someregistry/foo/bar@id"),
662
-			),
663
-			expectedDeletions:      []string{"id"},
664
-			expectedUpdatedStreams: []string{},
665
-		},
666
-	}
667
-
668
-	for name, test := range tests {
669
-		tcFilter := flag.Lookup("testcase").Value.String()
670
-		if len(tcFilter) > 0 && name != tcFilter {
671
-			continue
672
-		}
673
-
674
-		options := ImageRegistryPrunerOptions{
675
-			KeepYoungerThan:  60 * time.Minute,
676
-			KeepTagRevisions: 3,
677
-			Images:           &test.images,
678
-			Streams:          &test.streams,
679
-			Pods:             &test.pods,
680
-			RCs:              &test.rcs,
681
-			BCs:              &test.bcs,
682
-			Builds:           &test.builds,
683
-			DCs:              &test.dcs,
684
-		}
685
-		p := NewImageRegistryPruner(options)
686
-		p.(*imageRegistryPruner).registryPinger = &fakeRegistryPinger{}
687
-
688
-		imagePruner := &fakeImagePruner{invocations: sets.NewString()}
689
-		streamPruner := &fakeImageStreamPruner{invocations: sets.NewString()}
690
-		layerPruner := &fakeLayerPruner{invocations: sets.NewString()}
691
-		blobPruner := &fakeBlobPruner{invocations: sets.NewString()}
692
-		manifestPruner := &fakeManifestPruner{invocations: sets.NewString()}
693
-
694
-		p.Prune(imagePruner, streamPruner, layerPruner, blobPruner, manifestPruner)
695
-
696
-		expectedDeletions := sets.NewString(test.expectedDeletions...)
697
-		if !reflect.DeepEqual(expectedDeletions, imagePruner.invocations) {
698
-			t.Errorf("%s: expected image deletions %q, got %q", name, expectedDeletions.List(), imagePruner.invocations.List())
699
-		}
700
-
701
-		expectedUpdatedStreams := sets.NewString(test.expectedUpdatedStreams...)
702
-		if !reflect.DeepEqual(expectedUpdatedStreams, streamPruner.invocations) {
703
-			t.Errorf("%s: expected stream updates %q, got %q", name, expectedUpdatedStreams.List(), streamPruner.invocations.List())
704
-		}
705
-	}
706
-}
707
-
708
-func TestDeletingImagePruner(t *testing.T) {
709
-	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
710
-
711
-	tests := map[string]struct {
712
-		imageDeletionError error
713
-	}{
714
-		"no error": {},
715
-		"delete error": {
716
-			imageDeletionError: fmt.Errorf("foo"),
717
-		},
718
-	}
719
-
720
-	for name, test := range tests {
721
-		imageClient := testclient.Fake{}
722
-		imageClient.AddReactor("delete", "images", func(action ktc.Action) (handled bool, ret runtime.Object, err error) {
723
-			return true, nil, test.imageDeletionError
724
-		})
725
-		imagePruner := NewDeletingImagePruner(imageClient.Images())
726
-		err := imagePruner.PruneImage(&imageapi.Image{ObjectMeta: kapi.ObjectMeta{Name: "id2"}})
727
-		if test.imageDeletionError != nil {
728
-			if e, a := test.imageDeletionError, err; e != a {
729
-				t.Errorf("%s: err: expected %v, got %v", name, e, a)
730
-			}
731
-			continue
732
-		}
733
-
734
-		if e, a := 1, len(imageClient.Actions()); e != a {
735
-			t.Errorf("%s: expected %d actions, got %d: %#v", name, e, a, imageClient.Actions())
736
-			continue
737
-		}
738
-
739
-		if !imageClient.Actions()[0].Matches("delete", "images") {
740
-			t.Errorf("%s: expected action %s, got %v", name, "delete-images", imageClient.Actions()[0])
741
-		}
742
-	}
743
-}
744
-
745
-func TestDeletingLayerPruner(t *testing.T) {
746
-	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
747
-
748
-	var actions []string
749
-	client := fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
750
-		actions = append(actions, req.Method+":"+req.URL.String())
751
-		return &http.Response{StatusCode: http.StatusServiceUnavailable, Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
752
-	})
753
-	layerPruner := NewDeletingLayerPruner()
754
-	layerPruner.PruneLayer(client, "registry1", "repo", "layer1")
755
-
756
-	if !reflect.DeepEqual(actions, []string{"DELETE:https://registry1/v2/repo/blobs/layer1",
757
-		"DELETE:http://registry1/v2/repo/blobs/layer1"}) {
758
-		t.Errorf("Unexpected actions %v", actions)
759
-	}
760
-}
761
-
762
-func TestDeletingNotFoundLayerPruner(t *testing.T) {
763
-	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
764
-
765
-	var actions []string
766
-	client := fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
767
-		actions = append(actions, req.Method+":"+req.URL.String())
768
-		return &http.Response{StatusCode: http.StatusNotFound, Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
769
-	})
770
-	layerPruner := NewDeletingLayerPruner()
771
-	layerPruner.PruneLayer(client, "registry1", "repo", "layer1")
772
-
773
-	if !reflect.DeepEqual(actions, []string{"DELETE:https://registry1/v2/repo/blobs/layer1"}) {
774
-		t.Errorf("Unexpected actions %v", actions)
775
-	}
776
-}
777
-
778
-func TestRegistryPruning(t *testing.T) {
779
-	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
780
-
781
-	tests := map[string]struct {
782
-		images                    imageapi.ImageList
783
-		streams                   imageapi.ImageStreamList
784
-		expectedLayerDeletions    sets.String
785
-		expectedBlobDeletions     sets.String
786
-		expectedManifestDeletions sets.String
787
-		pingErr                   error
788
-	}{
789
-		"layers unique to id1 pruned": {
790
-			images: imageList(
791
-				imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"),
792
-				imageWithLayers("id2", "registry1/foo/bar@id2", "layer3", "layer4", "layer5", "layer6"),
793
-			),
794
-			streams: streamList(
795
-				stream("registry1", "foo", "bar", tags(
796
-					tag("latest",
797
-						tagEvent("id2", "registry1/foo/bar@id2"),
798
-						tagEvent("id1", "registry1/foo/bar@id1"),
799
-					),
800
-				)),
801
-				stream("registry1", "foo", "other", tags(
802
-					tag("latest",
803
-						tagEvent("id2", "registry1/foo/other@id2"),
804
-					),
805
-				)),
806
-			),
807
-			expectedLayerDeletions: sets.NewString(
808
-				"registry1|foo/bar|layer1",
809
-				"registry1|foo/bar|layer2",
810
-			),
811
-			expectedBlobDeletions: sets.NewString(
812
-				"registry1|layer1",
813
-				"registry1|layer2",
814
-			),
815
-			expectedManifestDeletions: sets.NewString(
816
-				"registry1|foo/bar|id1",
817
-			),
818
-		},
819
-		"no pruning when no images are pruned": {
820
-			images: imageList(
821
-				imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"),
822
-			),
823
-			streams: streamList(
824
-				stream("registry1", "foo", "bar", tags(
825
-					tag("latest",
826
-						tagEvent("id1", "registry1/foo/bar@id1"),
827
-					),
828
-				)),
829
-			),
830
-			expectedLayerDeletions:    sets.NewString(),
831
-			expectedBlobDeletions:     sets.NewString(),
832
-			expectedManifestDeletions: sets.NewString(),
833
-		},
834
-		"blobs pruned when streams have already been deleted": {
835
-			images: imageList(
836
-				imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"),
837
-				imageWithLayers("id2", "registry1/foo/bar@id2", "layer3", "layer4", "layer5", "layer6"),
838
-			),
839
-			expectedLayerDeletions: sets.NewString(),
840
-			expectedBlobDeletions: sets.NewString(
841
-				"registry1|layer1",
842
-				"registry1|layer2",
843
-				"registry1|layer3",
844
-				"registry1|layer4",
845
-				"registry1|layer5",
846
-				"registry1|layer6",
847
-			),
848
-			expectedManifestDeletions: sets.NewString(),
849
-		},
850
-		"ping error": {
851
-			images: imageList(
852
-				imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"),
853
-				imageWithLayers("id2", "registry1/foo/bar@id2", "layer3", "layer4", "layer5", "layer6"),
854
-			),
855
-			streams: streamList(
856
-				stream("registry1", "foo", "bar", tags(
857
-					tag("latest",
858
-						tagEvent("id2", "registry1/foo/bar@id2"),
859
-						tagEvent("id1", "registry1/foo/bar@id1"),
860
-					),
861
-				)),
862
-				stream("registry1", "foo", "other", tags(
863
-					tag("latest",
864
-						tagEvent("id2", "registry1/foo/other@id2"),
865
-					),
866
-				)),
867
-			),
868
-			expectedLayerDeletions:    sets.NewString(),
869
-			expectedBlobDeletions:     sets.NewString(),
870
-			expectedManifestDeletions: sets.NewString(),
871
-			pingErr:                   errors.New("foo"),
872
-		},
873
-	}
874
-
875
-	for name, test := range tests {
876
-		tcFilter := flag.Lookup("testcase").Value.String()
877
-		if len(tcFilter) > 0 && name != tcFilter {
878
-			continue
879
-		}
880
-
881
-		t.Logf("Running test case %s", name)
882
-
883
-		options := ImageRegistryPrunerOptions{
884
-			KeepYoungerThan:  60 * time.Minute,
885
-			KeepTagRevisions: 1,
886
-			Images:           &test.images,
887
-			Streams:          &test.streams,
888
-			Pods:             &kapi.PodList{},
889
-			RCs:              &kapi.ReplicationControllerList{},
890
-			BCs:              &buildapi.BuildConfigList{},
891
-			Builds:           &buildapi.BuildList{},
892
-			DCs:              &deployapi.DeploymentConfigList{},
893
-		}
894
-		p := NewImageRegistryPruner(options)
895
-		p.(*imageRegistryPruner).registryPinger = &fakeRegistryPinger{err: test.pingErr}
896
-
897
-		imagePruner := &fakeImagePruner{invocations: sets.NewString()}
898
-		streamPruner := &fakeImageStreamPruner{invocations: sets.NewString()}
899
-		layerPruner := &fakeLayerPruner{invocations: sets.NewString()}
900
-		blobPruner := &fakeBlobPruner{invocations: sets.NewString()}
901
-		manifestPruner := &fakeManifestPruner{invocations: sets.NewString()}
902
-
903
-		p.Prune(imagePruner, streamPruner, layerPruner, blobPruner, manifestPruner)
904
-
905
-		if !reflect.DeepEqual(test.expectedLayerDeletions, layerPruner.invocations) {
906
-			t.Errorf("%s: expected layer deletions %#v, got %#v", name, test.expectedLayerDeletions, layerPruner.invocations)
907
-		}
908
-		if !reflect.DeepEqual(test.expectedBlobDeletions, blobPruner.invocations) {
909
-			t.Errorf("%s: expected blob deletions %#v, got %#v", name, test.expectedBlobDeletions, blobPruner.invocations)
910
-		}
911
-		if !reflect.DeepEqual(test.expectedManifestDeletions, manifestPruner.invocations) {
912
-			t.Errorf("%s: expected manifest deletions %#v, got %#v", name, test.expectedManifestDeletions, manifestPruner.invocations)
913
-		}
914
-	}
915
-}
916 1
new file mode 100644
... ...
@@ -0,0 +1,1064 @@
0
+package prune
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"net/http"
6
+	"time"
7
+
8
+	"github.com/docker/distribution/registry/api/errcode"
9
+	"github.com/golang/glog"
10
+	gonum "github.com/gonum/graph"
11
+
12
+	kapi "k8s.io/kubernetes/pkg/api"
13
+	"k8s.io/kubernetes/pkg/api/resource"
14
+	"k8s.io/kubernetes/pkg/api/unversioned"
15
+	kerrors "k8s.io/kubernetes/pkg/util/errors"
16
+	utilruntime "k8s.io/kubernetes/pkg/util/runtime"
17
+	"k8s.io/kubernetes/pkg/util/sets"
18
+
19
+	"github.com/openshift/origin/pkg/api/graph"
20
+	kubegraph "github.com/openshift/origin/pkg/api/kubegraph/nodes"
21
+	buildapi "github.com/openshift/origin/pkg/build/api"
22
+	buildgraph "github.com/openshift/origin/pkg/build/graph/nodes"
23
+	buildutil "github.com/openshift/origin/pkg/build/util"
24
+	"github.com/openshift/origin/pkg/client"
25
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
26
+	deploygraph "github.com/openshift/origin/pkg/deploy/graph/nodes"
27
+	imageapi "github.com/openshift/origin/pkg/image/api"
28
+	imagegraph "github.com/openshift/origin/pkg/image/graph/nodes"
29
+)
30
+
31
+// TODO these edges should probably have an `Add***Edges` method in images/graph and be moved there
32
+const (
33
+	// ReferencedImageEdgeKind defines a "strong" edge where the tail is an
34
+	// ImageNode, with strong indicating that the ImageNode tail is not a
35
+	// candidate for pruning.
36
+	ReferencedImageEdgeKind = "ReferencedImage"
37
+	// WeakReferencedImageEdgeKind defines a "weak" edge where the tail is
38
+	// an ImageNode, with weak indicating that this particular edge does
39
+	// not keep an ImageNode from being a candidate for pruning.
40
+	WeakReferencedImageEdgeKind = "WeakReferencedImage"
41
+
42
+	// ReferencedImageLayerEdgeKind defines an edge from an ImageStreamNode or an
43
+	// ImageNode to an ImageLayerNode.
44
+	ReferencedImageLayerEdgeKind = "ReferencedImageLayer"
45
+)
46
+
47
+// pruneAlgorithm contains the various settings to use when evaluating images
48
+// and layers for pruning.
49
+type pruneAlgorithm struct {
50
+	keepYoungerThan    time.Duration
51
+	keepTagRevisions   int
52
+	pruneOverSizeLimit bool
53
+}
54
+
55
+// ImageDeleter knows how to remove images from OpenShift.
56
+type ImageDeleter interface {
57
+	// DeleteImage removes the image from OpenShift's storage.
58
+	DeleteImage(image *imageapi.Image) error
59
+}
60
+
61
+// ImageStreamDeleter knows how to remove an image reference from an image stream.
62
+type ImageStreamDeleter interface {
63
+	// DeleteImageStream removes all references to the image from the image
64
+	// stream's status.tags. The updated image stream is returned.
65
+	DeleteImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error)
66
+}
67
+
68
+// BlobDeleter knows how to delete a blob from the Docker registry.
69
+type BlobDeleter interface {
70
+	// DeleteBlob uses registryClient to ask the registry at registryURL
71
+	// to remove the blob.
72
+	DeleteBlob(registryClient *http.Client, registryURL, blob string) error
73
+}
74
+
75
+// LayerDeleter knows how to delete a repository layer link from the Docker registry.
76
+type LayerDeleter interface {
77
+	// DeleteLayer uses registryClient to ask the registry at registryURL to
78
+	// delete the repository layer link.
79
+	DeleteLayer(registryClient *http.Client, registryURL, repo, layer string) error
80
+}
81
+
82
+// ManifestDeleter knows how to delete image manifest data for a repository from
83
+// the Docker registry.
84
+type ManifestDeleter interface {
85
+	// DeleteManifest uses registryClient to ask the registry at registryURL to
86
+	// delete the repository's image manifest data.
87
+	DeleteManifest(registryClient *http.Client, registryURL, repo, manifest string) error
88
+}
89
+
90
+// PrunerOptions contains the fields used to initialize a new Pruner.
91
+type PrunerOptions struct {
92
+	// KeepYoungerThan indicates the minimum age an Image must be to be a
93
+	// candidate for pruning.
94
+	KeepYoungerThan *time.Duration
95
+	// KeepTagRevisions is the minimum number of tag revisions to preserve;
96
+	// revisions older than this value are candidates for pruning.
97
+	KeepTagRevisions *int
98
+	// PruneOverSizeLimit indicates that images exceeding defined limits (openshift.io/Image)
99
+	// will be considered as candidates for pruning.
100
+	PruneOverSizeLimit *bool
101
+	// Images is the entire list of images in OpenShift. An image must be in this
102
+	// list to be a candidate for pruning.
103
+	Images *imageapi.ImageList
104
+	// Streams is the entire list of image streams across all namespaces in the
105
+	// cluster.
106
+	Streams *imageapi.ImageStreamList
107
+	// Pods is the entire list of pods across all namespaces in the cluster.
108
+	Pods *kapi.PodList
109
+	// RCs is the entire list of replication controllers across all namespaces in
110
+	// the cluster.
111
+	RCs *kapi.ReplicationControllerList
112
+	// BCs is the entire list of build configs across all namespaces in the
113
+	// cluster.
114
+	BCs *buildapi.BuildConfigList
115
+	// Builds is the entire list of builds across all namespaces in the cluster.
116
+	Builds *buildapi.BuildList
117
+	// DCs is the entire list of deployment configs across all namespaces in the
118
+	// cluster.
119
+	DCs *deployapi.DeploymentConfigList
120
+	// LimitRanges is a map of LimitRanges across namespaces, being keys in this map.
121
+	LimitRanges map[string][]*kapi.LimitRange
122
+	// DryRun indicates that no changes will be made to the cluster and nothing
123
+	// will be removed.
124
+	DryRun bool
125
+	// RegistryClient is the http.Client to use when contacting the registry.
126
+	RegistryClient *http.Client
127
+	// RegistryURL is the URL for the registry.
128
+	RegistryURL string
129
+}
130
+
131
+// Pruner knows how to prune images and layers.
132
+type Pruner interface {
133
+	// Prune uses imagePruner, streamPruner, layerPruner, blobPruner, and
134
+	// manifestPruner to remove images that have been identified as candidates
135
+	// for pruning based on the Pruner's internal pruning algorithm.
136
+	// Please see NewPruner for details on the algorithm.
137
+	Prune(imagePruner ImageDeleter, streamPruner ImageStreamDeleter, layerPruner LayerDeleter,
138
+		blobPruner BlobDeleter, manifestPruner ManifestDeleter) error
139
+}
140
+
141
+// pruner is an object that knows how to prune a data set
142
+type pruner struct {
143
+	g              graph.Graph
144
+	algorithm      pruneAlgorithm
145
+	registryPinger registryPinger
146
+	registryClient *http.Client
147
+	registryURL    string
148
+}
149
+
150
+var _ Pruner = &pruner{}
151
+
152
+// registryPinger performs a health check against a registry.
153
+type registryPinger interface {
154
+	// ping performs a health check against registry.
155
+	ping(registry string) error
156
+}
157
+
158
+// defaultRegistryPinger implements registryPinger.
159
+type defaultRegistryPinger struct {
160
+	client *http.Client
161
+}
162
+
163
+func (drp *defaultRegistryPinger) ping(registry string) error {
164
+	healthCheck := func(proto, registry string) error {
165
+		// TODO: `/healthz` route is deprecated by `/`; remove it in future versions
166
+		healthResponse, err := drp.client.Get(fmt.Sprintf("%s://%s/healthz", proto, registry))
167
+		if err != nil {
168
+			return err
169
+		}
170
+		defer healthResponse.Body.Close()
171
+
172
+		if healthResponse.StatusCode != http.StatusOK {
173
+			return fmt.Errorf("unexpected status code %d", healthResponse.StatusCode)
174
+		}
175
+
176
+		return nil
177
+	}
178
+
179
+	var err error
180
+	for _, proto := range []string{"https", "http"} {
181
+		glog.V(4).Infof("Trying %s for %s", proto, registry)
182
+		err = healthCheck(proto, registry)
183
+		if err == nil {
184
+			break
185
+		}
186
+		glog.V(4).Infof("Error with %s for %s: %v", proto, registry, err)
187
+	}
188
+
189
+	return err
190
+}
191
+
192
+// dryRunRegistryPinger implements registryPinger.
193
+type dryRunRegistryPinger struct {
194
+}
195
+
196
+func (*dryRunRegistryPinger) ping(registry string) error {
197
+	return nil
198
+}
199
+
200
+// NewPruner creates a Pruner.
201
+//
202
+// Images younger than keepYoungerThan and images referenced by image streams
203
+// and/or pods younger than keepYoungerThan are preserved. All other images are
204
+// candidates for pruning. For example, if keepYoungerThan is 60m, and an
205
+// ImageStream is only 59 minutes old, none of the images it references are
206
+// eligible for pruning.
207
+//
208
+// keepTagRevisions is the number of revisions per tag in an image stream's
209
+// status.tags that are preserved and ineligible for pruning. Any revision older
210
+// than keepTagRevisions is eligible for pruning.
211
+//
212
+// pruneOverSizeLimit is a boolean flag speyfing that all images exceeding limits
213
+// defined in their namespace will be considered for pruning. Important to note is
214
+// the fact that this flag does not work in any combination with the keep* flags.
215
+//
216
+// images, streams, pods, rcs, bcs, builds, and dcs are the resources used to run
217
+// the pruning algorithm. These should be the full list for each type from the
218
+// cluster; otherwise, the pruning algorithm might result in incorrect
219
+// calculations and premature pruning.
220
+//
221
+// The ImageDeleter performs the following logic: remove any image containing the
222
+// annotation openshift.io/image.managed=true that was created at least *n*
223
+// minutes ago and is *not* currently referenced by:
224
+//
225
+// - any pod created less than *n* minutes ago
226
+// - any image stream created less than *n* minutes ago
227
+// - any running pods
228
+// - any pending pods
229
+// - any replication controllers
230
+// - any deployment configs
231
+// - any build configs
232
+// - any builds
233
+// - the n most recent tag revisions in an image stream's status.tags
234
+//
235
+// When removing an image, remove all references to the image from all
236
+// ImageStreams having a reference to the image in `status.tags`.
237
+//
238
+// Also automatically remove any image layer that is no longer referenced by any
239
+// images.
240
+func NewPruner(options PrunerOptions) Pruner {
241
+	g := graph.New()
242
+
243
+	glog.V(1).Infof("Creating image pruner with keepYoungerThan=%v, keepTagRevisions=%v, pruneOverSizeLimit=%v",
244
+		options.KeepYoungerThan, options.KeepTagRevisions, options.PruneOverSizeLimit)
245
+
246
+	algorithm := pruneAlgorithm{}
247
+	if options.KeepYoungerThan != nil {
248
+		algorithm.keepYoungerThan = *options.KeepYoungerThan
249
+	}
250
+	if options.KeepTagRevisions != nil {
251
+		algorithm.keepTagRevisions = *options.KeepTagRevisions
252
+	}
253
+	if options.PruneOverSizeLimit != nil {
254
+		algorithm.pruneOverSizeLimit = *options.PruneOverSizeLimit
255
+	}
256
+
257
+	addImagesToGraph(g, options.Images, algorithm)
258
+	addImageStreamsToGraph(g, options.Streams, options.LimitRanges, algorithm)
259
+	addPodsToGraph(g, options.Pods, algorithm)
260
+	addReplicationControllersToGraph(g, options.RCs)
261
+	addBuildConfigsToGraph(g, options.BCs)
262
+	addBuildsToGraph(g, options.Builds)
263
+	addDeploymentConfigsToGraph(g, options.DCs)
264
+
265
+	var rp registryPinger
266
+	if options.DryRun {
267
+		rp = &dryRunRegistryPinger{}
268
+	} else {
269
+		rp = &defaultRegistryPinger{options.RegistryClient}
270
+	}
271
+
272
+	return &pruner{
273
+		g:              g,
274
+		algorithm:      algorithm,
275
+		registryPinger: rp,
276
+		registryClient: options.RegistryClient,
277
+		registryURL:    options.RegistryURL,
278
+	}
279
+}
280
+
281
+// addImagesToGraph adds all images to the graph that belong to one of the
282
+// registries in the algorithm and are at least as old as the minimum age
283
+// threshold as specified by the algorithm. It also adds all the images' layers
284
+// to the graph.
285
+func addImagesToGraph(g graph.Graph, images *imageapi.ImageList, algorithm pruneAlgorithm) {
286
+	for i := range images.Items {
287
+		image := &images.Items[i]
288
+
289
+		glog.V(4).Infof("Examining image %q", image.Name)
290
+
291
+		if image.Annotations == nil {
292
+			glog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", image.Name, image.DockerImageReference)
293
+			continue
294
+		}
295
+		if value, ok := image.Annotations[imageapi.ManagedByOpenShiftAnnotation]; !ok || value != "true" {
296
+			glog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", image.Name, image.DockerImageReference)
297
+			continue
298
+		}
299
+
300
+		age := unversioned.Now().Sub(image.CreationTimestamp.Time)
301
+		if !algorithm.pruneOverSizeLimit && age < algorithm.keepYoungerThan {
302
+			glog.V(4).Infof("Image %q is younger than minimum pruning age, skipping (age=%v)", image.Name, age)
303
+			continue
304
+		}
305
+
306
+		glog.V(4).Infof("Adding image %q to graph", image.Name)
307
+		imageNode := imagegraph.EnsureImageNode(g, image)
308
+
309
+		manifest := imageapi.DockerImageManifest{}
310
+		if err := json.Unmarshal([]byte(image.DockerImageManifest), &manifest); err != nil {
311
+			utilruntime.HandleError(fmt.Errorf("unable to extract manifest from image: %v. This image's layers won't be pruned if the image is pruned now.", err))
312
+			continue
313
+		}
314
+
315
+		for _, layer := range manifest.FSLayers {
316
+			glog.V(4).Infof("Adding image layer %q to graph", layer.DockerBlobSum)
317
+			layerNode := imagegraph.EnsureImageLayerNode(g, layer.DockerBlobSum)
318
+			g.AddEdge(imageNode, layerNode, ReferencedImageLayerEdgeKind)
319
+		}
320
+	}
321
+}
322
+
323
+// addImageStreamsToGraph adds all the streams to the graph. The most recent n
324
+// image revisions for a tag will be preserved, where n is specified by the
325
+// algorithm's keepTagRevisions. Image revisions older than n are candidates
326
+// for pruning if the image stream's age is at least as old as the minimum
327
+// threshold in algorithm.  Otherwise, if the image stream is younger than the
328
+// threshold, all image revisions for that stream are ineligible for pruning.
329
+// If pruneOverSizeLimit flag is set to true, above does not matter, instead
330
+// all images size is checked against LimitRanges defined in that same namespace,
331
+// and whenever its size exceeds the smallest limit in that namespace, it will be
332
+// considered a candidate for pruning.
333
+//
334
+// addImageStreamsToGraph also adds references from each stream to all the
335
+// layers it references (via each image a stream references).
336
+func addImageStreamsToGraph(g graph.Graph, streams *imageapi.ImageStreamList, limits map[string][]*kapi.LimitRange, algorithm pruneAlgorithm) {
337
+	for i := range streams.Items {
338
+		stream := &streams.Items[i]
339
+
340
+		glog.V(4).Infof("Examining ImageStream %s/%s", stream.Namespace, stream.Name)
341
+
342
+		// use a weak reference for old image revisions by default
343
+		oldImageRevisionReferenceKind := WeakReferencedImageEdgeKind
344
+
345
+		age := unversioned.Now().Sub(stream.CreationTimestamp.Time)
346
+		if !algorithm.pruneOverSizeLimit && age < algorithm.keepYoungerThan {
347
+			// stream's age is below threshold - use a strong reference for old image revisions instead
348
+			oldImageRevisionReferenceKind = ReferencedImageEdgeKind
349
+		}
350
+
351
+		glog.V(4).Infof("Adding ImageStream %s/%s to graph", stream.Namespace, stream.Name)
352
+		isNode := imagegraph.EnsureImageStreamNode(g, stream)
353
+		imageStreamNode := isNode.(*imagegraph.ImageStreamNode)
354
+
355
+		for tag, history := range stream.Status.Tags {
356
+			for i := range history.Items {
357
+				n := imagegraph.FindImage(g, history.Items[i].Image)
358
+				if n == nil {
359
+					glog.V(2).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)
360
+					continue
361
+				}
362
+				imageNode := n.(*imagegraph.ImageNode)
363
+
364
+				kind := oldImageRevisionReferenceKind
365
+				if algorithm.pruneOverSizeLimit {
366
+					if exceedsLimits(stream, imageNode.Image, limits) {
367
+						kind = WeakReferencedImageEdgeKind
368
+					} else {
369
+						kind = ReferencedImageEdgeKind
370
+					}
371
+				} else {
372
+					if i < algorithm.keepTagRevisions {
373
+						kind = ReferencedImageEdgeKind
374
+					}
375
+				}
376
+
377
+				glog.V(4).Infof("Checking for existing strong reference from stream %s/%s to image %s", stream.Namespace, stream.Name, imageNode.Image.Name)
378
+				if edge := g.Edge(imageStreamNode, imageNode); edge != nil && g.EdgeKinds(edge).Has(ReferencedImageEdgeKind) {
379
+					glog.V(4).Infof("Strong reference found")
380
+					continue
381
+				}
382
+
383
+				glog.V(4).Infof("Adding edge (kind=%s) from %q to %q", kind, imageStreamNode.UniqueName(), imageNode.UniqueName())
384
+				g.AddEdge(imageStreamNode, imageNode, kind)
385
+
386
+				glog.V(4).Infof("Adding stream->layer references")
387
+				// add stream -> layer references so we can prune them later
388
+				for _, s := range g.From(imageNode) {
389
+					if g.Kind(s) != imagegraph.ImageLayerNodeKind {
390
+						continue
391
+					}
392
+					glog.V(4).Infof("Adding reference from stream %q to layer %q", stream.Name, s.(*imagegraph.ImageLayerNode).Layer)
393
+					g.AddEdge(imageStreamNode, s, ReferencedImageLayerEdgeKind)
394
+				}
395
+			}
396
+		}
397
+	}
398
+}
399
+
400
+// exceedsLimits checks if given image exceeds LimitRanges defined in ImageStream's namespace.
401
+func exceedsLimits(is *imageapi.ImageStream, image *imageapi.Image, limits map[string][]*kapi.LimitRange) bool {
402
+	limitRanges, ok := limits[is.Namespace]
403
+	if !ok {
404
+		return false
405
+	}
406
+	if len(limitRanges) == 0 {
407
+		return false
408
+	}
409
+
410
+	imageSize := resource.NewQuantity(image.DockerImageMetadata.Size, resource.BinarySI)
411
+	for _, limitRange := range limitRanges {
412
+		if limitRange == nil {
413
+			continue
414
+		}
415
+		for _, limit := range limitRange.Spec.Limits {
416
+			if limit.Type != imageapi.LimitTypeImage {
417
+				continue
418
+			}
419
+
420
+			limitQuantity, ok := limit.Max[kapi.ResourceStorage]
421
+			if !ok {
422
+				continue
423
+			}
424
+			if limitQuantity.Cmp(*imageSize) < 0 {
425
+				// image size is larger than the permitted limit range max size
426
+				glog.V(4).Infof("Image %s in stream %s/%s exceeds limit %s: %v vs %v",
427
+					image.Name, is.Namespace, is.Name, limitRange.Name, *imageSize, limitQuantity)
428
+				return true
429
+			}
430
+		}
431
+	}
432
+	return false
433
+}
434
+
435
+// addPodsToGraph adds pods to the graph.
436
+//
437
+// A pod is only *excluded* from being added to the graph if its phase is not
438
+// pending or running and it is at least as old as the minimum age threshold
439
+// defined by algorithm.
440
+//
441
+// Edges are added to the graph from each pod to the images specified by that
442
+// pod's list of containers, as long as the image is managed by OpenShift.
443
+func addPodsToGraph(g graph.Graph, pods *kapi.PodList, algorithm pruneAlgorithm) {
444
+	for i := range pods.Items {
445
+		pod := &pods.Items[i]
446
+
447
+		glog.V(4).Infof("Examining pod %s/%s", pod.Namespace, pod.Name)
448
+
449
+		if pod.Status.Phase != kapi.PodRunning && pod.Status.Phase != kapi.PodPending {
450
+			age := unversioned.Now().Sub(pod.CreationTimestamp.Time)
451
+			if age >= algorithm.keepYoungerThan {
452
+				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)
453
+				// not pending or running, age is at least minimum pruning age, skip
454
+				continue
455
+			}
456
+		}
457
+
458
+		glog.V(4).Infof("Adding pod %s/%s to graph", pod.Namespace, pod.Name)
459
+		podNode := kubegraph.EnsurePodNode(g, pod)
460
+
461
+		addPodSpecToGraph(g, &pod.Spec, podNode)
462
+	}
463
+}
464
+
465
+// Edges are added to the graph from each predecessor (pod or replication
466
+// controller) to the images specified by the pod spec's list of containers, as
467
+// long as the image is managed by OpenShift.
468
+func addPodSpecToGraph(g graph.Graph, spec *kapi.PodSpec, predecessor gonum.Node) {
469
+	for j := range spec.Containers {
470
+		container := spec.Containers[j]
471
+
472
+		glog.V(4).Infof("Examining container image %q", container.Image)
473
+
474
+		ref, err := imageapi.ParseDockerImageReference(container.Image)
475
+		if err != nil {
476
+			utilruntime.HandleError(fmt.Errorf("unable to parse DockerImageReference %q: %v", container.Image, err))
477
+			continue
478
+		}
479
+
480
+		if len(ref.ID) == 0 {
481
+			glog.V(4).Infof("%q has no image ID", container.Image)
482
+			continue
483
+		}
484
+
485
+		imageNode := imagegraph.FindImage(g, ref.ID)
486
+		if imageNode == nil {
487
+			glog.Infof("Unable to find image %q in the graph", ref.ID)
488
+			continue
489
+		}
490
+
491
+		glog.V(4).Infof("Adding edge from pod to image")
492
+		g.AddEdge(predecessor, imageNode, ReferencedImageEdgeKind)
493
+	}
494
+}
495
+
496
+// addReplicationControllersToGraph adds replication controllers to the graph.
497
+//
498
+// Edges are added to the graph from each replication controller to the images
499
+// specified by its pod spec's list of containers, as long as the image is
500
+// managed by OpenShift.
501
+func addReplicationControllersToGraph(g graph.Graph, rcs *kapi.ReplicationControllerList) {
502
+	for i := range rcs.Items {
503
+		rc := &rcs.Items[i]
504
+		glog.V(4).Infof("Examining replication controller %s/%s", rc.Namespace, rc.Name)
505
+		rcNode := kubegraph.EnsureReplicationControllerNode(g, rc)
506
+		addPodSpecToGraph(g, &rc.Spec.Template.Spec, rcNode)
507
+	}
508
+}
509
+
510
+// addDeploymentConfigsToGraph adds deployment configs to the graph.
511
+//
512
+// Edges are added to the graph from each deployment config to the images
513
+// specified by its pod spec's list of containers, as long as the image is
514
+// managed by OpenShift.
515
+func addDeploymentConfigsToGraph(g graph.Graph, dcs *deployapi.DeploymentConfigList) {
516
+	for i := range dcs.Items {
517
+		dc := &dcs.Items[i]
518
+		glog.V(4).Infof("Examining DeploymentConfig %s/%s", dc.Namespace, dc.Name)
519
+		dcNode := deploygraph.EnsureDeploymentConfigNode(g, dc)
520
+		addPodSpecToGraph(g, &dc.Spec.Template.Spec, dcNode)
521
+	}
522
+}
523
+
524
+// addBuildConfigsToGraph adds build configs to the graph.
525
+//
526
+// Edges are added to the graph from each build config to the image specified by its strategy.from.
527
+func addBuildConfigsToGraph(g graph.Graph, bcs *buildapi.BuildConfigList) {
528
+	for i := range bcs.Items {
529
+		bc := &bcs.Items[i]
530
+		glog.V(4).Infof("Examining BuildConfig %s/%s", bc.Namespace, bc.Name)
531
+		bcNode := buildgraph.EnsureBuildConfigNode(g, bc)
532
+		addBuildStrategyImageReferencesToGraph(g, bc.Spec.Strategy, bcNode)
533
+	}
534
+}
535
+
536
+// addBuildsToGraph adds builds to the graph.
537
+//
538
+// Edges are added to the graph from each build to the image specified by its strategy.from.
539
+func addBuildsToGraph(g graph.Graph, builds *buildapi.BuildList) {
540
+	for i := range builds.Items {
541
+		build := &builds.Items[i]
542
+		glog.V(4).Infof("Examining build %s/%s", build.Namespace, build.Name)
543
+		buildNode := buildgraph.EnsureBuildNode(g, build)
544
+		addBuildStrategyImageReferencesToGraph(g, build.Spec.Strategy, buildNode)
545
+	}
546
+}
547
+
548
+// addBuildStrategyImageReferencesToGraph ads references from the build strategy's parent node to the image
549
+// the build strategy references.
550
+//
551
+// Edges are added to the graph from each predecessor (build or build config)
552
+// to the image specified by strategy.from, as long as the image is managed by
553
+// OpenShift.
554
+func addBuildStrategyImageReferencesToGraph(g graph.Graph, strategy buildapi.BuildStrategy, predecessor gonum.Node) {
555
+	from := buildutil.GetInputReference(strategy)
556
+	if from == nil {
557
+		glog.V(4).Infof("Unable to determine 'from' reference - skipping")
558
+		return
559
+	}
560
+
561
+	glog.V(4).Infof("Examining build strategy with from: %#v", from)
562
+
563
+	var imageID string
564
+
565
+	switch from.Kind {
566
+	case "ImageStreamImage":
567
+		_, id, err := imageapi.ParseImageStreamImageName(from.Name)
568
+		if err != nil {
569
+			glog.V(2).Infof("Error parsing ImageStreamImage name %q: %v - skipping", from.Name, err)
570
+			return
571
+		}
572
+		imageID = id
573
+	case "DockerImage":
574
+		ref, err := imageapi.ParseDockerImageReference(from.Name)
575
+		if err != nil {
576
+			glog.V(2).Infof("Error parsing DockerImage name %q: %v - skipping", from.Name, err)
577
+			return
578
+		}
579
+		imageID = ref.ID
580
+	default:
581
+		return
582
+	}
583
+
584
+	glog.V(4).Infof("Looking for image %q in graph", imageID)
585
+	imageNode := imagegraph.FindImage(g, imageID)
586
+	if imageNode == nil {
587
+		glog.V(4).Infof("Unable to find image %q in graph - skipping", imageID)
588
+		return
589
+	}
590
+
591
+	glog.V(4).Infof("Adding edge from %v to %v", predecessor, imageNode)
592
+	g.AddEdge(predecessor, imageNode, ReferencedImageEdgeKind)
593
+}
594
+
595
+// getImageNodes returns only nodes of type ImageNode.
596
+func getImageNodes(nodes []gonum.Node) []*imagegraph.ImageNode {
597
+	ret := []*imagegraph.ImageNode{}
598
+	for i := range nodes {
599
+		if node, ok := nodes[i].(*imagegraph.ImageNode); ok {
600
+			ret = append(ret, node)
601
+		}
602
+	}
603
+	return ret
604
+}
605
+
606
+// edgeKind returns true if the edge from "from" to "to" is of the desired kind.
607
+func edgeKind(g graph.Graph, from, to gonum.Node, desiredKind string) bool {
608
+	edge := g.Edge(from, to)
609
+	kinds := g.EdgeKinds(edge)
610
+	return kinds.Has(desiredKind)
611
+}
612
+
613
+// imageIsPrunable returns true iff the image node only has weak references
614
+// from its predecessors to it. A weak reference to an image is a reference
615
+// from an image stream to an image where the image is not the current image
616
+// for a tag and the image stream is at least as old as the minimum pruning
617
+// age.
618
+func imageIsPrunable(g graph.Graph, imageNode *imagegraph.ImageNode) bool {
619
+	onlyWeakReferences := true
620
+
621
+	for _, n := range g.To(imageNode) {
622
+		glog.V(4).Infof("Examining predecessor %#v", n)
623
+		if !edgeKind(g, n, imageNode, WeakReferencedImageEdgeKind) {
624
+			glog.V(4).Infof("Strong reference detected")
625
+			onlyWeakReferences = false
626
+			break
627
+		}
628
+	}
629
+
630
+	return onlyWeakReferences
631
+
632
+}
633
+
634
+// calculatePrunableImages returns the list of prunable images and a
635
+// graph.NodeSet containing the image node IDs.
636
+func calculatePrunableImages(g graph.Graph, imageNodes []*imagegraph.ImageNode) ([]*imagegraph.ImageNode, graph.NodeSet) {
637
+	prunable := []*imagegraph.ImageNode{}
638
+	ids := make(graph.NodeSet)
639
+
640
+	for _, imageNode := range imageNodes {
641
+		glog.V(4).Infof("Examining image %q", imageNode.Image.Name)
642
+
643
+		if imageIsPrunable(g, imageNode) {
644
+			glog.V(4).Infof("Image %q is prunable", imageNode.Image.Name)
645
+			prunable = append(prunable, imageNode)
646
+			ids.Add(imageNode.ID())
647
+		}
648
+	}
649
+
650
+	return prunable, ids
651
+}
652
+
653
+// subgraphWithoutPrunableImages creates a subgraph from g with prunable image
654
+// nodes excluded.
655
+func subgraphWithoutPrunableImages(g graph.Graph, prunableImageIDs graph.NodeSet) graph.Graph {
656
+	return g.Subgraph(
657
+		func(g graph.Interface, node gonum.Node) bool {
658
+			return !prunableImageIDs.Has(node.ID())
659
+		},
660
+		func(g graph.Interface, from, to gonum.Node, edgeKinds sets.String) bool {
661
+			if prunableImageIDs.Has(from.ID()) {
662
+				return false
663
+			}
664
+			if prunableImageIDs.Has(to.ID()) {
665
+				return false
666
+			}
667
+			return true
668
+		},
669
+	)
670
+}
671
+
672
+// calculatePrunableLayers returns the list of prunable layers.
673
+func calculatePrunableLayers(g graph.Graph) []*imagegraph.ImageLayerNode {
674
+	prunable := []*imagegraph.ImageLayerNode{}
675
+
676
+	nodes := g.Nodes()
677
+	for i := range nodes {
678
+		layerNode, ok := nodes[i].(*imagegraph.ImageLayerNode)
679
+		if !ok {
680
+			continue
681
+		}
682
+
683
+		glog.V(4).Infof("Examining layer %q", layerNode.Layer)
684
+
685
+		if layerIsPrunable(g, layerNode) {
686
+			glog.V(4).Infof("Layer %q is prunable", layerNode.Layer)
687
+			prunable = append(prunable, layerNode)
688
+		}
689
+	}
690
+
691
+	return prunable
692
+}
693
+
694
+// pruneStreams removes references from all image streams' status.tags entries
695
+// to prunable images, invoking streamPruner.DeleteImageStream for each updated
696
+// stream.
697
+func pruneStreams(g graph.Graph, imageNodes []*imagegraph.ImageNode, streamPruner ImageStreamDeleter) []error {
698
+	errs := []error{}
699
+
700
+	glog.V(4).Infof("Removing pruned image references from streams")
701
+	for _, imageNode := range imageNodes {
702
+		for _, n := range g.To(imageNode) {
703
+			streamNode, ok := n.(*imagegraph.ImageStreamNode)
704
+			if !ok {
705
+				continue
706
+			}
707
+
708
+			stream := streamNode.ImageStream
709
+			updatedTags := sets.NewString()
710
+
711
+			glog.V(4).Infof("Checking if ImageStream %s/%s has references to image %s in status.tags", stream.Namespace, stream.Name, imageNode.Image.Name)
712
+
713
+			for tag, history := range stream.Status.Tags {
714
+				glog.V(4).Infof("Checking tag %q", tag)
715
+
716
+				newHistory := imageapi.TagEventList{}
717
+
718
+				for i, tagEvent := range history.Items {
719
+					glog.V(4).Infof("Checking tag event %d with image %q", i, tagEvent.Image)
720
+
721
+					if tagEvent.Image != imageNode.Image.Name {
722
+						glog.V(4).Infof("Tag event doesn't match deleted image - keeping")
723
+						newHistory.Items = append(newHistory.Items, tagEvent)
724
+					} else {
725
+						glog.V(4).Infof("Tag event matches deleted image - removing reference")
726
+						updatedTags.Insert(tag)
727
+					}
728
+				}
729
+				if len(newHistory.Items) == 0 {
730
+					glog.V(4).Infof("Removing tag %q from status.tags of ImageStream %s/%s", tag, stream.Namespace, stream.Name)
731
+					delete(stream.Status.Tags, tag)
732
+				} else {
733
+					stream.Status.Tags[tag] = newHistory
734
+				}
735
+			}
736
+
737
+			updatedStream, err := streamPruner.DeleteImageStream(stream, imageNode.Image, updatedTags.List())
738
+			if err != nil {
739
+				errs = append(errs, fmt.Errorf("error pruning image from stream: %v", err))
740
+				continue
741
+			}
742
+
743
+			streamNode.ImageStream = updatedStream
744
+		}
745
+	}
746
+	glog.V(4).Infof("Done removing pruned image references from streams")
747
+	return errs
748
+}
749
+
750
+// pruneImages invokes imagePruner.DeleteImage with each image that is prunable.
751
+func pruneImages(g graph.Graph, imageNodes []*imagegraph.ImageNode, imagePruner ImageDeleter) []error {
752
+	errs := []error{}
753
+
754
+	for _, imageNode := range imageNodes {
755
+		if err := imagePruner.DeleteImage(imageNode.Image); err != nil {
756
+			errs = append(errs, fmt.Errorf("error pruning image %q: %v", imageNode.Image.Name, err))
757
+		}
758
+	}
759
+
760
+	return errs
761
+}
762
+
763
+func (p *pruner) determineRegistry(imageNodes []*imagegraph.ImageNode) (string, error) {
764
+	if len(p.registryURL) > 0 {
765
+		return p.registryURL, nil
766
+	}
767
+
768
+	// we only support a single internal registry, and all images have the same registry
769
+	// so we just take the 1st one and use it
770
+	pullSpec := imageNodes[0].Image.DockerImageReference
771
+
772
+	ref, err := imageapi.ParseDockerImageReference(pullSpec)
773
+	if err != nil {
774
+		return "", fmt.Errorf("unable to parse %q: %v", pullSpec, err)
775
+	}
776
+
777
+	if len(ref.Registry) == 0 {
778
+		return "", fmt.Errorf("%s does not include a registry", pullSpec)
779
+	}
780
+
781
+	return ref.Registry, nil
782
+}
783
+
784
+// Run identifies images eligible for pruning, invoking imagePruneFunc for each
785
+// image, and then it identifies layers eligible for pruning, invoking
786
+// layerPruneFunc for each registry URL that has layers that can be pruned.
787
+func (p *pruner) Prune(imagePruner ImageDeleter, streamPruner ImageStreamDeleter, layerPruner LayerDeleter, blobPruner BlobDeleter, manifestPruner ManifestDeleter) error {
788
+	allNodes := p.g.Nodes()
789
+
790
+	imageNodes := getImageNodes(allNodes)
791
+	if len(imageNodes) == 0 {
792
+		return nil
793
+	}
794
+
795
+	registryURL, err := p.determineRegistry(imageNodes)
796
+	if err != nil {
797
+		return fmt.Errorf("unable to determine registry: %v", err)
798
+	}
799
+	glog.V(1).Infof("Using registry: %s", registryURL)
800
+
801
+	if err := p.registryPinger.ping(registryURL); err != nil {
802
+		return fmt.Errorf("error communicating with registry: %v", err)
803
+	}
804
+
805
+	prunableImageNodes, prunableImageIDs := calculatePrunableImages(p.g, imageNodes)
806
+	graphWithoutPrunableImages := subgraphWithoutPrunableImages(p.g, prunableImageIDs)
807
+	prunableLayers := calculatePrunableLayers(graphWithoutPrunableImages)
808
+
809
+	errs := []error{}
810
+
811
+	errs = append(errs, pruneStreams(p.g, prunableImageNodes, streamPruner)...)
812
+	errs = append(errs, pruneLayers(p.g, p.registryClient, registryURL, prunableLayers, layerPruner)...)
813
+	errs = append(errs, pruneBlobs(p.g, p.registryClient, registryURL, prunableLayers, blobPruner)...)
814
+	errs = append(errs, pruneManifests(p.g, p.registryClient, registryURL, prunableImageNodes, manifestPruner)...)
815
+
816
+	if len(errs) > 0 {
817
+		// If we had any errors removing image references from image streams or deleting
818
+		// layers, blobs, or manifest data from the registry, stop here and don't
819
+		// delete any images. This way, you can rerun prune and retry things that failed.
820
+		return kerrors.NewAggregate(errs)
821
+	}
822
+
823
+	errs = append(errs, pruneImages(p.g, prunableImageNodes, imagePruner)...)
824
+	return kerrors.NewAggregate(errs)
825
+}
826
+
827
+// layerIsPrunable returns true if the layer is not referenced by any images.
828
+func layerIsPrunable(g graph.Graph, layerNode *imagegraph.ImageLayerNode) bool {
829
+	for _, predecessor := range g.To(layerNode) {
830
+		glog.V(4).Infof("Examining layer predecessor %#v", predecessor)
831
+		if g.Kind(predecessor) == imagegraph.ImageNodeKind {
832
+			glog.V(4).Infof("Layer has an image predecessor")
833
+			return false
834
+		}
835
+	}
836
+
837
+	return true
838
+}
839
+
840
+// streamLayerReferences returns a list of ImageStreamNodes that reference a
841
+// given ImageLayerNode.
842
+func streamLayerReferences(g graph.Graph, layerNode *imagegraph.ImageLayerNode) []*imagegraph.ImageStreamNode {
843
+	ret := []*imagegraph.ImageStreamNode{}
844
+
845
+	for _, predecessor := range g.To(layerNode) {
846
+		if g.Kind(predecessor) != imagegraph.ImageStreamNodeKind {
847
+			continue
848
+		}
849
+
850
+		ret = append(ret, predecessor.(*imagegraph.ImageStreamNode))
851
+	}
852
+
853
+	return ret
854
+}
855
+
856
+// pruneLayers invokes layerPruner.DeleteLayer for each repository layer link to
857
+// be deleted from the registry.
858
+func pruneLayers(g graph.Graph, registryClient *http.Client, registryURL string, layerNodes []*imagegraph.ImageLayerNode, layerPruner LayerDeleter) []error {
859
+	errs := []error{}
860
+
861
+	for _, layerNode := range layerNodes {
862
+		// get streams that reference layer
863
+		streamNodes := streamLayerReferences(g, layerNode)
864
+
865
+		for _, streamNode := range streamNodes {
866
+			stream := streamNode.ImageStream
867
+			streamName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)
868
+
869
+			glog.V(4).Infof("Pruning registry=%q, repo=%q, layer=%q", registryURL, streamName, layerNode.Layer)
870
+			if err := layerPruner.DeleteLayer(registryClient, registryURL, streamName, layerNode.Layer); err != nil {
871
+				errs = append(errs, fmt.Errorf("error pruning repo %q layer link %q: %v", streamName, layerNode.Layer, err))
872
+			}
873
+		}
874
+	}
875
+
876
+	return errs
877
+}
878
+
879
+// pruneBlobs invokes blobPruner.DeleteBlob for each blob to be deleted from the
880
+// registry.
881
+func pruneBlobs(g graph.Graph, registryClient *http.Client, registryURL string, layerNodes []*imagegraph.ImageLayerNode, blobPruner BlobDeleter) []error {
882
+	errs := []error{}
883
+
884
+	for _, layerNode := range layerNodes {
885
+		glog.V(4).Infof("Pruning registry=%q, blob=%q", registryURL, layerNode.Layer)
886
+		if err := blobPruner.DeleteBlob(registryClient, registryURL, layerNode.Layer); err != nil {
887
+			errs = append(errs, fmt.Errorf("error pruning blob %q: %v", layerNode.Layer, err))
888
+		}
889
+	}
890
+
891
+	return errs
892
+}
893
+
894
+// pruneManifests invokes manifestPruner.DeleteManifest for each repository
895
+// manifest to be deleted from the registry.
896
+func pruneManifests(g graph.Graph, registryClient *http.Client, registryURL string, imageNodes []*imagegraph.ImageNode, manifestPruner ManifestDeleter) []error {
897
+	errs := []error{}
898
+
899
+	for _, imageNode := range imageNodes {
900
+		for _, n := range g.To(imageNode) {
901
+			streamNode, ok := n.(*imagegraph.ImageStreamNode)
902
+			if !ok {
903
+				continue
904
+			}
905
+
906
+			stream := streamNode.ImageStream
907
+			repoName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)
908
+
909
+			glog.V(4).Infof("Pruning manifest for registry %q, repo %q, image %q", registryURL, repoName, imageNode.Image.Name)
910
+			if err := manifestPruner.DeleteManifest(registryClient, registryURL, repoName, imageNode.Image.Name); err != nil {
911
+				errs = append(errs, fmt.Errorf("error pruning manifest for registry %q, repo %q, image %q: %v", registryURL, repoName, imageNode.Image.Name, err))
912
+			}
913
+		}
914
+	}
915
+
916
+	return errs
917
+}
918
+
919
+// imageDeleter removes an image from OpenShift.
920
+type imageDeleter struct {
921
+	images client.ImageInterface
922
+}
923
+
924
+var _ ImageDeleter = &imageDeleter{}
925
+
926
+// NewImageDeleter creates a new imageDeleter.
927
+func NewImageDeleter(images client.ImageInterface) ImageDeleter {
928
+	return &imageDeleter{
929
+		images: images,
930
+	}
931
+}
932
+
933
+func (p *imageDeleter) DeleteImage(image *imageapi.Image) error {
934
+	glog.V(4).Infof("Deleting image %q", image.Name)
935
+	return p.images.Delete(image.Name)
936
+}
937
+
938
+// imageStreamDeleter updates an image stream in OpenShift.
939
+type imageStreamDeleter struct {
940
+	streams client.ImageStreamsNamespacer
941
+}
942
+
943
+var _ ImageStreamDeleter = &imageStreamDeleter{}
944
+
945
+// NewImageStreamDeleter creates a new imageStreamDeleter.
946
+func NewImageStreamDeleter(streams client.ImageStreamsNamespacer) ImageStreamDeleter {
947
+	return &imageStreamDeleter{
948
+		streams: streams,
949
+	}
950
+}
951
+
952
+func (p *imageStreamDeleter) DeleteImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error) {
953
+	glog.V(4).Infof("Updating ImageStream %s/%s", stream.Namespace, stream.Name)
954
+	glog.V(5).Infof("Updated stream: %#v", stream)
955
+	return p.streams.ImageStreams(stream.Namespace).UpdateStatus(stream)
956
+}
957
+
958
+// deleteFromRegistry uses registryClient to send a DELETE request to the
959
+// provided url. It attempts an https request first; if that fails, it fails
960
+// back to http.
961
+func deleteFromRegistry(registryClient *http.Client, url string) error {
962
+	deleteFunc := func(proto, url string) error {
963
+		req, err := http.NewRequest("DELETE", url, nil)
964
+		if err != nil {
965
+			glog.Errorf("Error creating request: %v", err)
966
+			return fmt.Errorf("error creating request: %v", err)
967
+		}
968
+
969
+		glog.V(4).Infof("Sending request to registry")
970
+		resp, err := registryClient.Do(req)
971
+		if err != nil {
972
+			return fmt.Errorf("error sending request: %v", err)
973
+		}
974
+		defer resp.Body.Close()
975
+
976
+		// TODO: investigate why we're getting non-existent layers, for now we're logging
977
+		// them out and continue working
978
+		if resp.StatusCode == http.StatusNotFound {
979
+			glog.Warningf("Unable to prune layer %s, returned %v", url, resp.Status)
980
+			return nil
981
+		}
982
+		// non-2xx/3xx response doesn't cause an error, so we need to check for it
983
+		// manually and return it to caller
984
+		if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
985
+			return fmt.Errorf(resp.Status)
986
+		}
987
+		if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusAccepted {
988
+			glog.V(1).Infof("Unexpected status code in response: %d", resp.StatusCode)
989
+			var response errcode.Errors
990
+			decoder := json.NewDecoder(resp.Body)
991
+			if err := decoder.Decode(&response); err != nil {
992
+				return err
993
+			}
994
+			glog.V(1).Infof("Response: %#v", response)
995
+			return &response
996
+		}
997
+
998
+		return nil
999
+	}
1000
+
1001
+	var err error
1002
+	for _, proto := range []string{"https", "http"} {
1003
+		glog.V(4).Infof("Trying %s for %s", proto, url)
1004
+		err = deleteFunc(proto, fmt.Sprintf("%s://%s", proto, url))
1005
+		if err == nil {
1006
+			return nil
1007
+		}
1008
+
1009
+		if _, ok := err.(*errcode.Errors); ok {
1010
+			// we got a response back from the registry, so return it
1011
+			return err
1012
+		}
1013
+
1014
+		// we didn't get a success or a errcode.Errors response back from the registry
1015
+		glog.V(4).Infof("Error with %s for %s: %v", proto, url, err)
1016
+	}
1017
+	return err
1018
+}
1019
+
1020
+// layerDeleter removes a repository layer link from the registry.
1021
+type layerDeleter struct{}
1022
+
1023
+var _ LayerDeleter = &layerDeleter{}
1024
+
1025
+// NewLayerDeleter creates a new layerDeleter.
1026
+func NewLayerDeleter() LayerDeleter {
1027
+	return &layerDeleter{}
1028
+}
1029
+
1030
+func (p *layerDeleter) DeleteLayer(registryClient *http.Client, registryURL, repoName, layer string) error {
1031
+	glog.V(4).Infof("Pruning registry %q, repo %q, layer %q", registryURL, repoName, layer)
1032
+	return deleteFromRegistry(registryClient, fmt.Sprintf("%s/v2/%s/blobs/%s", registryURL, repoName, layer))
1033
+}
1034
+
1035
+// blobDeleter removes a blob from the registry.
1036
+type blobDeleter struct{}
1037
+
1038
+var _ BlobDeleter = &blobDeleter{}
1039
+
1040
+// NewBlobDeleter creates a new blobDeleter.
1041
+func NewBlobDeleter() BlobDeleter {
1042
+	return &blobDeleter{}
1043
+}
1044
+
1045
+func (p *blobDeleter) DeleteBlob(registryClient *http.Client, registryURL, blob string) error {
1046
+	glog.V(4).Infof("Pruning registry %q, blob %q", registryURL, blob)
1047
+	return deleteFromRegistry(registryClient, fmt.Sprintf("%s/admin/blobs/%s", registryURL, blob))
1048
+}
1049
+
1050
+// manifestDeleter deletes repository manifest data from the registry.
1051
+type manifestDeleter struct{}
1052
+
1053
+var _ ManifestDeleter = &manifestDeleter{}
1054
+
1055
+// NewManifestDeleter creates a new manifestDeleter.
1056
+func NewManifestDeleter() ManifestDeleter {
1057
+	return &manifestDeleter{}
1058
+}
1059
+
1060
+func (p *manifestDeleter) DeleteManifest(registryClient *http.Client, registryURL, repoName, manifest string) error {
1061
+	glog.V(4).Infof("Pruning manifest for registry %q, repo %q, manifest %q", registryURL, repoName, manifest)
1062
+	return deleteFromRegistry(registryClient, fmt.Sprintf("%s/v2/%s/manifests/%s", registryURL, repoName, manifest))
1063
+}
0 1064
new file mode 100644
... ...
@@ -0,0 +1,1041 @@
0
+package prune
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"errors"
6
+	"flag"
7
+	"fmt"
8
+	"io/ioutil"
9
+	"net/http"
10
+	"reflect"
11
+	"testing"
12
+	"time"
13
+
14
+	kapi "k8s.io/kubernetes/pkg/api"
15
+	"k8s.io/kubernetes/pkg/api/resource"
16
+	"k8s.io/kubernetes/pkg/api/unversioned"
17
+	"k8s.io/kubernetes/pkg/client/unversioned/fake"
18
+	ktc "k8s.io/kubernetes/pkg/client/unversioned/testclient"
19
+	"k8s.io/kubernetes/pkg/runtime"
20
+	"k8s.io/kubernetes/pkg/util/sets"
21
+
22
+	buildapi "github.com/openshift/origin/pkg/build/api"
23
+	"github.com/openshift/origin/pkg/client/testclient"
24
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
25
+	imageapi "github.com/openshift/origin/pkg/image/api"
26
+)
27
+
28
+type fakeRegistryPinger struct {
29
+	err      error
30
+	requests []string
31
+}
32
+
33
+func (f *fakeRegistryPinger) ping(registry string) error {
34
+	f.requests = append(f.requests, registry)
35
+	return f.err
36
+}
37
+
38
+func imageList(images ...imageapi.Image) imageapi.ImageList {
39
+	return imageapi.ImageList{
40
+		Items: images,
41
+	}
42
+}
43
+
44
+func agedImage(id, ref string, ageInMinutes int64) imageapi.Image {
45
+	image := imageWithLayers(id, ref,
46
+		"tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
47
+		"tarsum.dev+sha256:b194de3772ebbcdc8f244f663669799ac1cb141834b7cb8b69100285d357a2b0",
48
+		"tarsum.dev+sha256:c937c4bb1c1a21cc6d94340812262c6472092028972ae69b551b1a70d4276171",
49
+		"tarsum.dev+sha256:2aaacc362ac6be2b9e9ae8c6029f6f616bb50aec63746521858e47841b90fabd",
50
+		"tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
51
+	)
52
+
53
+	if ageInMinutes >= 0 {
54
+		image.CreationTimestamp = unversioned.NewTime(unversioned.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute))
55
+	}
56
+
57
+	return image
58
+}
59
+
60
+func sizedImage(id, ref string, size int64) imageapi.Image {
61
+	image := imageWithLayers(id, ref,
62
+		"tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
63
+		"tarsum.dev+sha256:b194de3772ebbcdc8f244f663669799ac1cb141834b7cb8b69100285d357a2b0",
64
+		"tarsum.dev+sha256:c937c4bb1c1a21cc6d94340812262c6472092028972ae69b551b1a70d4276171",
65
+		"tarsum.dev+sha256:2aaacc362ac6be2b9e9ae8c6029f6f616bb50aec63746521858e47841b90fabd",
66
+		"tarsum.dev+sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
67
+	)
68
+
69
+	image.CreationTimestamp = unversioned.NewTime(unversioned.Now().Add(time.Duration(-1) * time.Minute))
70
+	image.DockerImageMetadata.Size = size
71
+
72
+	return image
73
+}
74
+
75
+func image(id, ref string) imageapi.Image {
76
+	return agedImage(id, ref, -1)
77
+}
78
+
79
+func imageWithLayers(id, ref string, layers ...string) imageapi.Image {
80
+	image := imageapi.Image{
81
+		ObjectMeta: kapi.ObjectMeta{
82
+			Name: id,
83
+			Annotations: map[string]string{
84
+				imageapi.ManagedByOpenShiftAnnotation: "true",
85
+			},
86
+		},
87
+		DockerImageReference: ref,
88
+	}
89
+
90
+	manifest := imageapi.DockerImageManifest{
91
+		FSLayers: []imageapi.DockerFSLayer{},
92
+	}
93
+
94
+	for _, layer := range layers {
95
+		manifest.FSLayers = append(manifest.FSLayers, imageapi.DockerFSLayer{DockerBlobSum: layer})
96
+	}
97
+
98
+	manifestBytes, err := json.Marshal(&manifest)
99
+	if err != nil {
100
+		panic(err)
101
+	}
102
+
103
+	image.DockerImageManifest = string(manifestBytes)
104
+
105
+	return image
106
+}
107
+
108
+func unmanagedImage(id, ref string, hasAnnotations bool, annotation, value string) imageapi.Image {
109
+	image := imageWithLayers(id, ref)
110
+	if !hasAnnotations {
111
+		image.Annotations = nil
112
+	} else {
113
+		delete(image.Annotations, imageapi.ManagedByOpenShiftAnnotation)
114
+		image.Annotations[annotation] = value
115
+	}
116
+	return image
117
+}
118
+
119
+func imageWithBadManifest(id, ref string) imageapi.Image {
120
+	image := image(id, ref)
121
+	image.DockerImageManifest = "asdf"
122
+	return image
123
+}
124
+
125
+func podList(pods ...kapi.Pod) kapi.PodList {
126
+	return kapi.PodList{
127
+		Items: pods,
128
+	}
129
+}
130
+
131
+func pod(namespace, name string, phase kapi.PodPhase, containerImages ...string) kapi.Pod {
132
+	return agedPod(namespace, name, phase, -1, containerImages...)
133
+}
134
+
135
+func agedPod(namespace, name string, phase kapi.PodPhase, ageInMinutes int64, containerImages ...string) kapi.Pod {
136
+	pod := kapi.Pod{
137
+		ObjectMeta: kapi.ObjectMeta{
138
+			Namespace: namespace,
139
+			Name:      name,
140
+		},
141
+		Spec: podSpec(containerImages...),
142
+		Status: kapi.PodStatus{
143
+			Phase: phase,
144
+		},
145
+	}
146
+
147
+	if ageInMinutes >= 0 {
148
+		pod.CreationTimestamp = unversioned.NewTime(unversioned.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute))
149
+	}
150
+
151
+	return pod
152
+}
153
+
154
+func podSpec(containerImages ...string) kapi.PodSpec {
155
+	spec := kapi.PodSpec{
156
+		Containers: []kapi.Container{},
157
+	}
158
+	for _, image := range containerImages {
159
+		container := kapi.Container{
160
+			Image: image,
161
+		}
162
+		spec.Containers = append(spec.Containers, container)
163
+	}
164
+	return spec
165
+}
166
+
167
+func streamList(streams ...imageapi.ImageStream) imageapi.ImageStreamList {
168
+	return imageapi.ImageStreamList{
169
+		Items: streams,
170
+	}
171
+}
172
+
173
+func stream(registry, namespace, name string, tags map[string]imageapi.TagEventList) imageapi.ImageStream {
174
+	return agedStream(registry, namespace, name, -1, tags)
175
+}
176
+
177
+func agedStream(registry, namespace, name string, ageInMinutes int64, tags map[string]imageapi.TagEventList) imageapi.ImageStream {
178
+	stream := imageapi.ImageStream{
179
+		ObjectMeta: kapi.ObjectMeta{
180
+			Namespace: namespace,
181
+			Name:      name,
182
+		},
183
+		Status: imageapi.ImageStreamStatus{
184
+			DockerImageRepository: fmt.Sprintf("%s/%s/%s", registry, namespace, name),
185
+			Tags: tags,
186
+		},
187
+	}
188
+
189
+	if ageInMinutes >= 0 {
190
+		stream.CreationTimestamp = unversioned.NewTime(unversioned.Now().Add(time.Duration(-1*ageInMinutes) * time.Minute))
191
+	}
192
+
193
+	return stream
194
+}
195
+
196
+func streamPtr(registry, namespace, name string, tags map[string]imageapi.TagEventList) *imageapi.ImageStream {
197
+	s := stream(registry, namespace, name, tags)
198
+	return &s
199
+}
200
+
201
+func tags(list ...namedTagEventList) map[string]imageapi.TagEventList {
202
+	m := make(map[string]imageapi.TagEventList, len(list))
203
+	for _, tag := range list {
204
+		m[tag.name] = tag.events
205
+	}
206
+	return m
207
+}
208
+
209
+type namedTagEventList struct {
210
+	name   string
211
+	events imageapi.TagEventList
212
+}
213
+
214
+func tag(name string, events ...imageapi.TagEvent) namedTagEventList {
215
+	return namedTagEventList{
216
+		name: name,
217
+		events: imageapi.TagEventList{
218
+			Items: events,
219
+		},
220
+	}
221
+}
222
+
223
+func tagEvent(id, ref string) imageapi.TagEvent {
224
+	return imageapi.TagEvent{
225
+		Image:                id,
226
+		DockerImageReference: ref,
227
+	}
228
+}
229
+
230
+func rcList(rcs ...kapi.ReplicationController) kapi.ReplicationControllerList {
231
+	return kapi.ReplicationControllerList{
232
+		Items: rcs,
233
+	}
234
+}
235
+
236
+func rc(namespace, name string, containerImages ...string) kapi.ReplicationController {
237
+	return kapi.ReplicationController{
238
+		ObjectMeta: kapi.ObjectMeta{
239
+			Namespace: namespace,
240
+			Name:      name,
241
+		},
242
+		Spec: kapi.ReplicationControllerSpec{
243
+			Template: &kapi.PodTemplateSpec{
244
+				Spec: podSpec(containerImages...),
245
+			},
246
+		},
247
+	}
248
+}
249
+
250
+func dcList(dcs ...deployapi.DeploymentConfig) deployapi.DeploymentConfigList {
251
+	return deployapi.DeploymentConfigList{
252
+		Items: dcs,
253
+	}
254
+}
255
+
256
+func dc(namespace, name string, containerImages ...string) deployapi.DeploymentConfig {
257
+	return deployapi.DeploymentConfig{
258
+		ObjectMeta: kapi.ObjectMeta{
259
+			Namespace: namespace,
260
+			Name:      name,
261
+		},
262
+		Spec: deployapi.DeploymentConfigSpec{
263
+			Template: &kapi.PodTemplateSpec{
264
+				Spec: podSpec(containerImages...),
265
+			},
266
+		},
267
+	}
268
+}
269
+
270
+func bcList(bcs ...buildapi.BuildConfig) buildapi.BuildConfigList {
271
+	return buildapi.BuildConfigList{
272
+		Items: bcs,
273
+	}
274
+}
275
+
276
+func bc(namespace, name, strategyType, fromKind, fromNamespace, fromName string) buildapi.BuildConfig {
277
+	return buildapi.BuildConfig{
278
+		ObjectMeta: kapi.ObjectMeta{
279
+			Namespace: namespace,
280
+			Name:      name,
281
+		},
282
+		Spec: buildapi.BuildConfigSpec{
283
+			CommonSpec: commonSpec(strategyType, fromKind, fromNamespace, fromName),
284
+		},
285
+	}
286
+}
287
+
288
+func buildList(builds ...buildapi.Build) buildapi.BuildList {
289
+	return buildapi.BuildList{
290
+		Items: builds,
291
+	}
292
+}
293
+
294
+func build(namespace, name, strategyType, fromKind, fromNamespace, fromName string) buildapi.Build {
295
+	return buildapi.Build{
296
+		ObjectMeta: kapi.ObjectMeta{
297
+			Namespace: namespace,
298
+			Name:      name,
299
+		},
300
+		Spec: buildapi.BuildSpec{
301
+			CommonSpec: commonSpec(strategyType, fromKind, fromNamespace, fromName),
302
+		},
303
+	}
304
+}
305
+
306
+func limitList(limits ...int64) []*kapi.LimitRange {
307
+	list := make([]*kapi.LimitRange, len(limits))
308
+	for _, limit := range limits {
309
+		quantity := resource.NewQuantity(limit, resource.BinarySI)
310
+		list = append(list, &kapi.LimitRange{
311
+			Spec: kapi.LimitRangeSpec{
312
+				Limits: []kapi.LimitRangeItem{
313
+					{
314
+						Type: imageapi.LimitTypeImage,
315
+						Max: kapi.ResourceList{
316
+							kapi.ResourceStorage: *quantity,
317
+						},
318
+					},
319
+				},
320
+			},
321
+		})
322
+	}
323
+	return list
324
+}
325
+
326
+func commonSpec(strategyType, fromKind, fromNamespace, fromName string) buildapi.CommonSpec {
327
+	spec := buildapi.CommonSpec{
328
+		Strategy: buildapi.BuildStrategy{},
329
+	}
330
+	switch strategyType {
331
+	case "source":
332
+		spec.Strategy.SourceStrategy = &buildapi.SourceBuildStrategy{
333
+			From: kapi.ObjectReference{
334
+				Kind:      fromKind,
335
+				Namespace: fromNamespace,
336
+				Name:      fromName,
337
+			},
338
+		}
339
+	case "docker":
340
+		spec.Strategy.DockerStrategy = &buildapi.DockerBuildStrategy{
341
+			From: &kapi.ObjectReference{
342
+				Kind:      fromKind,
343
+				Namespace: fromNamespace,
344
+				Name:      fromName,
345
+			},
346
+		}
347
+	case "custom":
348
+		spec.Strategy.CustomStrategy = &buildapi.CustomBuildStrategy{
349
+			From: kapi.ObjectReference{
350
+				Kind:      fromKind,
351
+				Namespace: fromNamespace,
352
+				Name:      fromName,
353
+			},
354
+		}
355
+	}
356
+
357
+	return spec
358
+}
359
+
360
+type fakeImageDeleter struct {
361
+	invocations sets.String
362
+	err         error
363
+}
364
+
365
+var _ ImageDeleter = &fakeImageDeleter{}
366
+
367
+func (p *fakeImageDeleter) DeleteImage(image *imageapi.Image) error {
368
+	p.invocations.Insert(image.Name)
369
+	return p.err
370
+}
371
+
372
+type fakeImageStreamDeleter struct {
373
+	invocations sets.String
374
+	err         error
375
+}
376
+
377
+var _ ImageStreamDeleter = &fakeImageStreamDeleter{}
378
+
379
+func (p *fakeImageStreamDeleter) DeleteImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error) {
380
+	p.invocations.Insert(fmt.Sprintf("%s/%s|%s", stream.Namespace, stream.Name, image.Name))
381
+	return stream, p.err
382
+}
383
+
384
+type fakeBlobDeleter struct {
385
+	invocations sets.String
386
+	err         error
387
+}
388
+
389
+var _ BlobDeleter = &fakeBlobDeleter{}
390
+
391
+func (p *fakeBlobDeleter) DeleteBlob(registryClient *http.Client, registryURL, blob string) error {
392
+	p.invocations.Insert(fmt.Sprintf("%s|%s", registryURL, blob))
393
+	return p.err
394
+}
395
+
396
+type fakeLayerDeleter struct {
397
+	invocations sets.String
398
+	err         error
399
+}
400
+
401
+var _ LayerDeleter = &fakeLayerDeleter{}
402
+
403
+func (p *fakeLayerDeleter) DeleteLayer(registryClient *http.Client, registryURL, repo, layer string) error {
404
+	p.invocations.Insert(fmt.Sprintf("%s|%s|%s", registryURL, repo, layer))
405
+	return p.err
406
+}
407
+
408
+type fakeManifestDeleter struct {
409
+	invocations sets.String
410
+	err         error
411
+}
412
+
413
+var _ ManifestDeleter = &fakeManifestDeleter{}
414
+
415
+func (p *fakeManifestDeleter) DeleteManifest(registryClient *http.Client, registryURL, repo, manifest string) error {
416
+	p.invocations.Insert(fmt.Sprintf("%s|%s|%s", registryURL, repo, manifest))
417
+	return p.err
418
+}
419
+
420
+var logLevel = flag.Int("loglevel", 0, "")
421
+var testCase = flag.String("testcase", "", "")
422
+
423
+func TestImagePruning(t *testing.T) {
424
+	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
425
+	registryURL := "registry"
426
+
427
+	tests := map[string]struct {
428
+		pruneOverSizeLimit     *bool
429
+		registryURLs           []string
430
+		images                 imageapi.ImageList
431
+		pods                   kapi.PodList
432
+		streams                imageapi.ImageStreamList
433
+		rcs                    kapi.ReplicationControllerList
434
+		bcs                    buildapi.BuildConfigList
435
+		builds                 buildapi.BuildList
436
+		dcs                    deployapi.DeploymentConfigList
437
+		limits                 map[string][]*kapi.LimitRange
438
+		expectedDeletions      []string
439
+		expectedUpdatedStreams []string
440
+	}{
441
+		"1 pod - phase pending - don't prune": {
442
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
443
+			pods:              podList(pod("foo", "pod1", kapi.PodPending, registryURL+"/foo/bar@id")),
444
+			expectedDeletions: []string{},
445
+		},
446
+		"3 pods - last phase pending - don't prune": {
447
+			images: imageList(image("id", registryURL+"/foo/bar@id")),
448
+			pods: podList(
449
+				pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id"),
450
+				pod("foo", "pod2", kapi.PodSucceeded, registryURL+"/foo/bar@id"),
451
+				pod("foo", "pod3", kapi.PodPending, registryURL+"/foo/bar@id"),
452
+			),
453
+			expectedDeletions: []string{},
454
+		},
455
+		"1 pod - phase running - don't prune": {
456
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
457
+			pods:              podList(pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@id")),
458
+			expectedDeletions: []string{},
459
+		},
460
+		"3 pods - last phase running - don't prune": {
461
+			images: imageList(image("id", registryURL+"/foo/bar@id")),
462
+			pods: podList(
463
+				pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id"),
464
+				pod("foo", "pod2", kapi.PodSucceeded, registryURL+"/foo/bar@id"),
465
+				pod("foo", "pod3", kapi.PodRunning, registryURL+"/foo/bar@id"),
466
+			),
467
+			expectedDeletions: []string{},
468
+		},
469
+		"pod phase succeeded - prune": {
470
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
471
+			pods:              podList(pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id")),
472
+			expectedDeletions: []string{"id"},
473
+		},
474
+		"pod phase succeeded, pod less than min pruning age - don't prune": {
475
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
476
+			pods:              podList(agedPod("foo", "pod1", kapi.PodSucceeded, 5, registryURL+"/foo/bar@id")),
477
+			expectedDeletions: []string{},
478
+		},
479
+		"pod phase succeeded, image less than min pruning age - don't prune": {
480
+			images:            imageList(agedImage("id", registryURL+"/foo/bar@id", 5)),
481
+			pods:              podList(pod("foo", "pod1", kapi.PodSucceeded, registryURL+"/foo/bar@id")),
482
+			expectedDeletions: []string{},
483
+		},
484
+		"pod phase failed - prune": {
485
+			images: imageList(image("id", registryURL+"/foo/bar@id")),
486
+			pods: podList(
487
+				pod("foo", "pod1", kapi.PodFailed, registryURL+"/foo/bar@id"),
488
+				pod("foo", "pod2", kapi.PodFailed, registryURL+"/foo/bar@id"),
489
+				pod("foo", "pod3", kapi.PodFailed, registryURL+"/foo/bar@id"),
490
+			),
491
+			expectedDeletions: []string{"id"},
492
+		},
493
+		"pod phase unknown - prune": {
494
+			images: imageList(image("id", registryURL+"/foo/bar@id")),
495
+			pods: podList(
496
+				pod("foo", "pod1", kapi.PodUnknown, registryURL+"/foo/bar@id"),
497
+				pod("foo", "pod2", kapi.PodUnknown, registryURL+"/foo/bar@id"),
498
+				pod("foo", "pod3", kapi.PodUnknown, registryURL+"/foo/bar@id"),
499
+			),
500
+			expectedDeletions: []string{"id"},
501
+		},
502
+		"pod container image not parsable": {
503
+			images: imageList(image("id", registryURL+"/foo/bar@id")),
504
+			pods: podList(
505
+				pod("foo", "pod1", kapi.PodRunning, "a/b/c/d/e"),
506
+			),
507
+			expectedDeletions: []string{"id"},
508
+		},
509
+		"pod container image doesn't have an id": {
510
+			images: imageList(image("id", registryURL+"/foo/bar@id")),
511
+			pods: podList(
512
+				pod("foo", "pod1", kapi.PodRunning, "foo/bar:latest"),
513
+			),
514
+			expectedDeletions: []string{"id"},
515
+		},
516
+		"pod refers to image not in graph": {
517
+			images: imageList(image("id", registryURL+"/foo/bar@id")),
518
+			pods: podList(
519
+				pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@otherid"),
520
+			),
521
+			expectedDeletions: []string{"id"},
522
+		},
523
+		"referenced by rc - don't prune": {
524
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
525
+			rcs:               rcList(rc("foo", "rc1", registryURL+"/foo/bar@id")),
526
+			expectedDeletions: []string{},
527
+		},
528
+		"referenced by dc - don't prune": {
529
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
530
+			dcs:               dcList(dc("foo", "rc1", registryURL+"/foo/bar@id")),
531
+			expectedDeletions: []string{},
532
+		},
533
+		"referenced by bc - sti - ImageStreamImage - don't prune": {
534
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
535
+			bcs:               bcList(bc("foo", "bc1", "source", "ImageStreamImage", "foo", "bar@id")),
536
+			expectedDeletions: []string{},
537
+		},
538
+		"referenced by bc - docker - ImageStreamImage - don't prune": {
539
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
540
+			bcs:               bcList(bc("foo", "bc1", "docker", "ImageStreamImage", "foo", "bar@id")),
541
+			expectedDeletions: []string{},
542
+		},
543
+		"referenced by bc - custom - ImageStreamImage - don't prune": {
544
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
545
+			bcs:               bcList(bc("foo", "bc1", "custom", "ImageStreamImage", "foo", "bar@id")),
546
+			expectedDeletions: []string{},
547
+		},
548
+		"referenced by bc - sti - DockerImage - don't prune": {
549
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
550
+			bcs:               bcList(bc("foo", "bc1", "source", "DockerImage", "foo", registryURL+"/foo/bar@id")),
551
+			expectedDeletions: []string{},
552
+		},
553
+		"referenced by bc - docker - DockerImage - don't prune": {
554
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
555
+			bcs:               bcList(bc("foo", "bc1", "docker", "DockerImage", "foo", registryURL+"/foo/bar@id")),
556
+			expectedDeletions: []string{},
557
+		},
558
+		"referenced by bc - custom - DockerImage - don't prune": {
559
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
560
+			bcs:               bcList(bc("foo", "bc1", "custom", "DockerImage", "foo", registryURL+"/foo/bar@id")),
561
+			expectedDeletions: []string{},
562
+		},
563
+		"referenced by build - sti - ImageStreamImage - don't prune": {
564
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
565
+			builds:            buildList(build("foo", "build1", "source", "ImageStreamImage", "foo", "bar@id")),
566
+			expectedDeletions: []string{},
567
+		},
568
+		"referenced by build - docker - ImageStreamImage - don't prune": {
569
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
570
+			builds:            buildList(build("foo", "build1", "docker", "ImageStreamImage", "foo", "bar@id")),
571
+			expectedDeletions: []string{},
572
+		},
573
+		"referenced by build - custom - ImageStreamImage - don't prune": {
574
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
575
+			builds:            buildList(build("foo", "build1", "custom", "ImageStreamImage", "foo", "bar@id")),
576
+			expectedDeletions: []string{},
577
+		},
578
+		"referenced by build - sti - DockerImage - don't prune": {
579
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
580
+			builds:            buildList(build("foo", "build1", "source", "DockerImage", "foo", registryURL+"/foo/bar@id")),
581
+			expectedDeletions: []string{},
582
+		},
583
+		"referenced by build - docker - DockerImage - don't prune": {
584
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
585
+			builds:            buildList(build("foo", "build1", "docker", "DockerImage", "foo", registryURL+"/foo/bar@id")),
586
+			expectedDeletions: []string{},
587
+		},
588
+		"referenced by build - custom - DockerImage - don't prune": {
589
+			images:            imageList(image("id", registryURL+"/foo/bar@id")),
590
+			builds:            buildList(build("foo", "build1", "custom", "DockerImage", "foo", registryURL+"/foo/bar@id")),
591
+			expectedDeletions: []string{},
592
+		},
593
+		"image stream - keep most recent n images": {
594
+			images: imageList(
595
+				unmanagedImage("id", "otherregistry/foo/bar@id", false, "", ""),
596
+				image("id2", registryURL+"/foo/bar@id2"),
597
+				image("id3", registryURL+"/foo/bar@id3"),
598
+				image("id4", registryURL+"/foo/bar@id4"),
599
+			),
600
+			streams: streamList(
601
+				stream(registryURL, "foo", "bar", tags(
602
+					tag("latest",
603
+						tagEvent("id", "otherregistry/foo/bar@id"),
604
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
605
+						tagEvent("id3", registryURL+"/foo/bar@id3"),
606
+						tagEvent("id4", registryURL+"/foo/bar@id4"),
607
+					),
608
+				)),
609
+			),
610
+			expectedDeletions:      []string{"id4"},
611
+			expectedUpdatedStreams: []string{"foo/bar|id4"},
612
+		},
613
+		"image stream - same manifest listed multiple times in tag history": {
614
+			images: imageList(
615
+				image("id1", registryURL+"/foo/bar@id1"),
616
+				image("id2", registryURL+"/foo/bar@id2"),
617
+			),
618
+			streams: streamList(
619
+				stream(registryURL, "foo", "bar", tags(
620
+					tag("latest",
621
+						tagEvent("id1", registryURL+"/foo/bar@id1"),
622
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
623
+						tagEvent("id1", registryURL+"/foo/bar@id1"),
624
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
625
+					),
626
+				)),
627
+			),
628
+		},
629
+		"image stream age less than min pruning age - don't prune": {
630
+			images: imageList(
631
+				image("id", registryURL+"/foo/bar@id"),
632
+				image("id2", registryURL+"/foo/bar@id2"),
633
+				image("id3", registryURL+"/foo/bar@id3"),
634
+				image("id4", registryURL+"/foo/bar@id4"),
635
+			),
636
+			streams: streamList(
637
+				agedStream(registryURL, "foo", "bar", 5, tags(
638
+					tag("latest",
639
+						tagEvent("id", registryURL+"/foo/bar@id"),
640
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
641
+						tagEvent("id3", registryURL+"/foo/bar@id3"),
642
+						tagEvent("id4", registryURL+"/foo/bar@id4"),
643
+					),
644
+				)),
645
+			),
646
+			expectedDeletions:      []string{},
647
+			expectedUpdatedStreams: []string{},
648
+		},
649
+		"multiple resources pointing to image - don't prune": {
650
+			images: imageList(
651
+				image("id", registryURL+"/foo/bar@id"),
652
+				image("id2", registryURL+"/foo/bar@id2"),
653
+			),
654
+			streams: streamList(
655
+				stream(registryURL, "foo", "bar", tags(
656
+					tag("latest",
657
+						tagEvent("id", registryURL+"/foo/bar@id"),
658
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
659
+					),
660
+				)),
661
+			),
662
+			rcs:                    rcList(rc("foo", "rc1", registryURL+"/foo/bar@id2")),
663
+			pods:                   podList(pod("foo", "pod1", kapi.PodRunning, registryURL+"/foo/bar@id2")),
664
+			dcs:                    dcList(dc("foo", "rc1", registryURL+"/foo/bar@id")),
665
+			bcs:                    bcList(bc("foo", "bc1", "source", "DockerImage", "foo", registryURL+"/foo/bar@id")),
666
+			builds:                 buildList(build("foo", "build1", "custom", "ImageStreamImage", "foo", "bar@id")),
667
+			expectedDeletions:      []string{},
668
+			expectedUpdatedStreams: []string{},
669
+		},
670
+		"image with nil annotations": {
671
+			images: imageList(
672
+				unmanagedImage("id", "someregistry/foo/bar@id", false, "", ""),
673
+			),
674
+			expectedDeletions:      []string{},
675
+			expectedUpdatedStreams: []string{},
676
+		},
677
+		"image missing managed annotation": {
678
+			images: imageList(
679
+				unmanagedImage("id", "someregistry/foo/bar@id", true, "foo", "bar"),
680
+			),
681
+			expectedDeletions:      []string{},
682
+			expectedUpdatedStreams: []string{},
683
+		},
684
+		"image with managed annotation != true": {
685
+			images: imageList(
686
+				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "false"),
687
+				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "0"),
688
+				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "1"),
689
+				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "True"),
690
+				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "yes"),
691
+				unmanagedImage("id", "someregistry/foo/bar@id", true, imageapi.ManagedByOpenShiftAnnotation, "Yes"),
692
+			),
693
+			expectedDeletions:      []string{},
694
+			expectedUpdatedStreams: []string{},
695
+		},
696
+		"image with bad manifest is pruned ok": {
697
+			images: imageList(
698
+				imageWithBadManifest("id", "someregistry/foo/bar@id"),
699
+			),
700
+			expectedDeletions:      []string{"id"},
701
+			expectedUpdatedStreams: []string{},
702
+		},
703
+		"image exceeding limits": {
704
+			pruneOverSizeLimit: newBool(true),
705
+			images: imageList(
706
+				unmanagedImage("id", "otherregistry/foo/bar@id", false, "", ""),
707
+				sizedImage("id2", registryURL+"/foo/bar@id2", 100),
708
+				sizedImage("id3", registryURL+"/foo/bar@id3", 200),
709
+			),
710
+			streams: streamList(
711
+				stream(registryURL, "foo", "bar", tags(
712
+					tag("latest",
713
+						tagEvent("id", "otherregistry/foo/bar@id"),
714
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
715
+						tagEvent("id3", registryURL+"/foo/bar@id3"),
716
+					),
717
+				)),
718
+			),
719
+			limits: map[string][]*kapi.LimitRange{
720
+				"foo": limitList(100, 200),
721
+			},
722
+			expectedDeletions:      []string{"id3"},
723
+			expectedUpdatedStreams: []string{"foo/bar|id3"},
724
+		},
725
+		"multiple images in different namespaces exceeding different limits": {
726
+			pruneOverSizeLimit: newBool(true),
727
+			images: imageList(
728
+				sizedImage("id1", registryURL+"/foo/bar@id1", 100),
729
+				sizedImage("id2", registryURL+"/foo/bar@id2", 200),
730
+				sizedImage("id3", registryURL+"/bar/foo@id3", 500),
731
+				sizedImage("id4", registryURL+"/bar/foo@id4", 600),
732
+			),
733
+			streams: streamList(
734
+				stream(registryURL, "foo", "bar", tags(
735
+					tag("latest",
736
+						tagEvent("id1", registryURL+"/foo/bar@id1"),
737
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
738
+					),
739
+				)),
740
+				stream(registryURL, "bar", "foo", tags(
741
+					tag("latest",
742
+						tagEvent("id3", registryURL+"/bar/foo@id3"),
743
+						tagEvent("id4", registryURL+"/bar/foo@id4"),
744
+					),
745
+				)),
746
+			),
747
+			limits: map[string][]*kapi.LimitRange{
748
+				"foo": limitList(150),
749
+				"bar": limitList(550),
750
+			},
751
+			expectedDeletions:      []string{"id2", "id4"},
752
+			expectedUpdatedStreams: []string{"foo/bar|id2", "bar/foo|id4"},
753
+		},
754
+		"image within allowed limits": {
755
+			pruneOverSizeLimit: newBool(true),
756
+			images: imageList(
757
+				unmanagedImage("id", "otherregistry/foo/bar@id", false, "", ""),
758
+				sizedImage("id2", registryURL+"/foo/bar@id2", 100),
759
+				sizedImage("id3", registryURL+"/foo/bar@id3", 200),
760
+			),
761
+			streams: streamList(
762
+				stream(registryURL, "foo", "bar", tags(
763
+					tag("latest",
764
+						tagEvent("id", "otherregistry/foo/bar@id"),
765
+						tagEvent("id2", registryURL+"/foo/bar@id2"),
766
+						tagEvent("id3", registryURL+"/foo/bar@id3"),
767
+					),
768
+				)),
769
+			),
770
+			limits: map[string][]*kapi.LimitRange{
771
+				"foo": limitList(300),
772
+			},
773
+			expectedDeletions:      []string{},
774
+			expectedUpdatedStreams: []string{},
775
+		},
776
+	}
777
+
778
+	for name, test := range tests {
779
+		tcFilter := flag.Lookup("testcase").Value.String()
780
+		if len(tcFilter) > 0 && name != tcFilter {
781
+			continue
782
+		}
783
+
784
+		options := PrunerOptions{
785
+			Images:      &test.images,
786
+			Streams:     &test.streams,
787
+			Pods:        &test.pods,
788
+			RCs:         &test.rcs,
789
+			BCs:         &test.bcs,
790
+			Builds:      &test.builds,
791
+			DCs:         &test.dcs,
792
+			LimitRanges: test.limits,
793
+		}
794
+		if test.pruneOverSizeLimit != nil {
795
+			options.PruneOverSizeLimit = test.pruneOverSizeLimit
796
+		} else {
797
+			keepYoungerThan := 60 * time.Minute
798
+			keepTagRevisions := 3
799
+			options.KeepYoungerThan = &keepYoungerThan
800
+			options.KeepTagRevisions = &keepTagRevisions
801
+		}
802
+		p := NewPruner(options)
803
+		p.(*pruner).registryPinger = &fakeRegistryPinger{}
804
+
805
+		imageDeleter := &fakeImageDeleter{invocations: sets.NewString()}
806
+		streamDeleter := &fakeImageStreamDeleter{invocations: sets.NewString()}
807
+		layerDeleter := &fakeLayerDeleter{invocations: sets.NewString()}
808
+		blobDeleter := &fakeBlobDeleter{invocations: sets.NewString()}
809
+		manifestDeleter := &fakeManifestDeleter{invocations: sets.NewString()}
810
+
811
+		p.Prune(imageDeleter, streamDeleter, layerDeleter, blobDeleter, manifestDeleter)
812
+
813
+		expectedDeletions := sets.NewString(test.expectedDeletions...)
814
+		if !reflect.DeepEqual(expectedDeletions, imageDeleter.invocations) {
815
+			t.Errorf("%s: expected image deletions %q, got %q", name, expectedDeletions.List(), imageDeleter.invocations.List())
816
+		}
817
+
818
+		expectedUpdatedStreams := sets.NewString(test.expectedUpdatedStreams...)
819
+		if !reflect.DeepEqual(expectedUpdatedStreams, streamDeleter.invocations) {
820
+			t.Errorf("%s: expected stream updates %q, got %q", name, expectedUpdatedStreams.List(), streamDeleter.invocations.List())
821
+		}
822
+	}
823
+}
824
+
825
+func TestImageDeleter(t *testing.T) {
826
+	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
827
+
828
+	tests := map[string]struct {
829
+		imageDeletionError error
830
+	}{
831
+		"no error": {},
832
+		"delete error": {
833
+			imageDeletionError: fmt.Errorf("foo"),
834
+		},
835
+	}
836
+
837
+	for name, test := range tests {
838
+		imageClient := testclient.Fake{}
839
+		imageClient.AddReactor("delete", "images", func(action ktc.Action) (handled bool, ret runtime.Object, err error) {
840
+			return true, nil, test.imageDeletionError
841
+		})
842
+		imageDeleter := NewImageDeleter(imageClient.Images())
843
+		err := imageDeleter.DeleteImage(&imageapi.Image{ObjectMeta: kapi.ObjectMeta{Name: "id2"}})
844
+		if test.imageDeletionError != nil {
845
+			if e, a := test.imageDeletionError, err; e != a {
846
+				t.Errorf("%s: err: expected %v, got %v", name, e, a)
847
+			}
848
+			continue
849
+		}
850
+
851
+		if e, a := 1, len(imageClient.Actions()); e != a {
852
+			t.Errorf("%s: expected %d actions, got %d: %#v", name, e, a, imageClient.Actions())
853
+			continue
854
+		}
855
+
856
+		if !imageClient.Actions()[0].Matches("delete", "images") {
857
+			t.Errorf("%s: expected action %s, got %v", name, "delete-images", imageClient.Actions()[0])
858
+		}
859
+	}
860
+}
861
+
862
+func TestLayerDeleter(t *testing.T) {
863
+	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
864
+
865
+	var actions []string
866
+	client := fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
867
+		actions = append(actions, req.Method+":"+req.URL.String())
868
+		return &http.Response{StatusCode: http.StatusServiceUnavailable, Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
869
+	})
870
+	layerDeleter := NewLayerDeleter()
871
+	layerDeleter.DeleteLayer(client, "registry1", "repo", "layer1")
872
+
873
+	if !reflect.DeepEqual(actions, []string{"DELETE:https://registry1/v2/repo/blobs/layer1",
874
+		"DELETE:http://registry1/v2/repo/blobs/layer1"}) {
875
+		t.Errorf("Unexpected actions %v", actions)
876
+	}
877
+}
878
+
879
+func TestNotFoundLayerDeleter(t *testing.T) {
880
+	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
881
+
882
+	var actions []string
883
+	client := fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
884
+		actions = append(actions, req.Method+":"+req.URL.String())
885
+		return &http.Response{StatusCode: http.StatusNotFound, Body: ioutil.NopCloser(bytes.NewReader([]byte{}))}, nil
886
+	})
887
+	layerDeleter := NewLayerDeleter()
888
+	layerDeleter.DeleteLayer(client, "registry1", "repo", "layer1")
889
+
890
+	if !reflect.DeepEqual(actions, []string{"DELETE:https://registry1/v2/repo/blobs/layer1"}) {
891
+		t.Errorf("Unexpected actions %v", actions)
892
+	}
893
+}
894
+
895
+func TestRegistryPruning(t *testing.T) {
896
+	flag.Lookup("v").Value.Set(fmt.Sprint(*logLevel))
897
+
898
+	tests := map[string]struct {
899
+		images                    imageapi.ImageList
900
+		streams                   imageapi.ImageStreamList
901
+		expectedLayerDeletions    sets.String
902
+		expectedBlobDeletions     sets.String
903
+		expectedManifestDeletions sets.String
904
+		pingErr                   error
905
+	}{
906
+		"layers unique to id1 pruned": {
907
+			images: imageList(
908
+				imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"),
909
+				imageWithLayers("id2", "registry1/foo/bar@id2", "layer3", "layer4", "layer5", "layer6"),
910
+			),
911
+			streams: streamList(
912
+				stream("registry1", "foo", "bar", tags(
913
+					tag("latest",
914
+						tagEvent("id2", "registry1/foo/bar@id2"),
915
+						tagEvent("id1", "registry1/foo/bar@id1"),
916
+					),
917
+				)),
918
+				stream("registry1", "foo", "other", tags(
919
+					tag("latest",
920
+						tagEvent("id2", "registry1/foo/other@id2"),
921
+					),
922
+				)),
923
+			),
924
+			expectedLayerDeletions: sets.NewString(
925
+				"registry1|foo/bar|layer1",
926
+				"registry1|foo/bar|layer2",
927
+			),
928
+			expectedBlobDeletions: sets.NewString(
929
+				"registry1|layer1",
930
+				"registry1|layer2",
931
+			),
932
+			expectedManifestDeletions: sets.NewString(
933
+				"registry1|foo/bar|id1",
934
+			),
935
+		},
936
+		"no pruning when no images are pruned": {
937
+			images: imageList(
938
+				imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"),
939
+			),
940
+			streams: streamList(
941
+				stream("registry1", "foo", "bar", tags(
942
+					tag("latest",
943
+						tagEvent("id1", "registry1/foo/bar@id1"),
944
+					),
945
+				)),
946
+			),
947
+			expectedLayerDeletions:    sets.NewString(),
948
+			expectedBlobDeletions:     sets.NewString(),
949
+			expectedManifestDeletions: sets.NewString(),
950
+		},
951
+		"blobs pruned when streams have already been deleted": {
952
+			images: imageList(
953
+				imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"),
954
+				imageWithLayers("id2", "registry1/foo/bar@id2", "layer3", "layer4", "layer5", "layer6"),
955
+			),
956
+			expectedLayerDeletions: sets.NewString(),
957
+			expectedBlobDeletions: sets.NewString(
958
+				"registry1|layer1",
959
+				"registry1|layer2",
960
+				"registry1|layer3",
961
+				"registry1|layer4",
962
+				"registry1|layer5",
963
+				"registry1|layer6",
964
+			),
965
+			expectedManifestDeletions: sets.NewString(),
966
+		},
967
+		"ping error": {
968
+			images: imageList(
969
+				imageWithLayers("id1", "registry1/foo/bar@id1", "layer1", "layer2", "layer3", "layer4"),
970
+				imageWithLayers("id2", "registry1/foo/bar@id2", "layer3", "layer4", "layer5", "layer6"),
971
+			),
972
+			streams: streamList(
973
+				stream("registry1", "foo", "bar", tags(
974
+					tag("latest",
975
+						tagEvent("id2", "registry1/foo/bar@id2"),
976
+						tagEvent("id1", "registry1/foo/bar@id1"),
977
+					),
978
+				)),
979
+				stream("registry1", "foo", "other", tags(
980
+					tag("latest",
981
+						tagEvent("id2", "registry1/foo/other@id2"),
982
+					),
983
+				)),
984
+			),
985
+			expectedLayerDeletions:    sets.NewString(),
986
+			expectedBlobDeletions:     sets.NewString(),
987
+			expectedManifestDeletions: sets.NewString(),
988
+			pingErr:                   errors.New("foo"),
989
+		},
990
+	}
991
+
992
+	for name, test := range tests {
993
+		tcFilter := flag.Lookup("testcase").Value.String()
994
+		if len(tcFilter) > 0 && name != tcFilter {
995
+			continue
996
+		}
997
+
998
+		t.Logf("Running test case %s", name)
999
+
1000
+		keepYoungerThan := 60 * time.Minute
1001
+		keepTagRevisions := 1
1002
+		options := PrunerOptions{
1003
+			KeepYoungerThan:  &keepYoungerThan,
1004
+			KeepTagRevisions: &keepTagRevisions,
1005
+			Images:           &test.images,
1006
+			Streams:          &test.streams,
1007
+			Pods:             &kapi.PodList{},
1008
+			RCs:              &kapi.ReplicationControllerList{},
1009
+			BCs:              &buildapi.BuildConfigList{},
1010
+			Builds:           &buildapi.BuildList{},
1011
+			DCs:              &deployapi.DeploymentConfigList{},
1012
+		}
1013
+		p := NewPruner(options)
1014
+		p.(*pruner).registryPinger = &fakeRegistryPinger{err: test.pingErr}
1015
+
1016
+		imageDeleter := &fakeImageDeleter{invocations: sets.NewString()}
1017
+		streamDeleter := &fakeImageStreamDeleter{invocations: sets.NewString()}
1018
+		layerDeleter := &fakeLayerDeleter{invocations: sets.NewString()}
1019
+		blobDeleter := &fakeBlobDeleter{invocations: sets.NewString()}
1020
+		manifestDeleter := &fakeManifestDeleter{invocations: sets.NewString()}
1021
+
1022
+		p.Prune(imageDeleter, streamDeleter, layerDeleter, blobDeleter, manifestDeleter)
1023
+
1024
+		if !reflect.DeepEqual(test.expectedLayerDeletions, layerDeleter.invocations) {
1025
+			t.Errorf("%s: expected layer deletions %#v, got %#v", name, test.expectedLayerDeletions, layerDeleter.invocations)
1026
+		}
1027
+		if !reflect.DeepEqual(test.expectedBlobDeletions, blobDeleter.invocations) {
1028
+			t.Errorf("%s: expected blob deletions %#v, got %#v", name, test.expectedBlobDeletions, blobDeleter.invocations)
1029
+		}
1030
+		if !reflect.DeepEqual(test.expectedManifestDeletions, manifestDeleter.invocations) {
1031
+			t.Errorf("%s: expected manifest deletions %#v, got %#v", name, test.expectedManifestDeletions, manifestDeleter.invocations)
1032
+		}
1033
+	}
1034
+}
1035
+
1036
+func newBool(a bool) *bool {
1037
+	r := new(bool)
1038
+	*r = a
1039
+	return r
1040
+}
... ...
@@ -1430,6 +1430,13 @@ items:
1430 1430
     - ""
1431 1431
     attributeRestrictions: null
1432 1432
     resources:
1433
+    - limitranges
1434
+    verbs:
1435
+    - list
1436
+  - apiGroups:
1437
+    - ""
1438
+    attributeRestrictions: null
1439
+    resources:
1433 1440
     - buildconfigs
1434 1441
     - builds
1435 1442
     verbs: