| ... | ... |
@@ -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: |