Browse code

Add a migration framework for mutable resources

Implement a migration framework and an image reference migrator:

oadm migrate image-references registry1.com/*=registry2.com/* --confirm

Reuse resource builder to manage output

Clayton Coleman authored on 2016/07/23 02:00:36
Showing 13 changed files
... ...
@@ -11,6 +11,9 @@ import (
11 11
 	"github.com/openshift/origin/pkg/cmd/admin/cert"
12 12
 	diagnostics "github.com/openshift/origin/pkg/cmd/admin/diagnostics"
13 13
 	"github.com/openshift/origin/pkg/cmd/admin/groups"
14
+	"github.com/openshift/origin/pkg/cmd/admin/migrate"
15
+	migrateimages "github.com/openshift/origin/pkg/cmd/admin/migrate/images"
16
+	migratestorage "github.com/openshift/origin/pkg/cmd/admin/migrate/storage"
14 17
 	"github.com/openshift/origin/pkg/cmd/admin/network"
15 18
 	"github.com/openshift/origin/pkg/cmd/admin/node"
16 19
 	"github.com/openshift/origin/pkg/cmd/admin/policy"
... ...
@@ -34,7 +37,7 @@ Administrative Commands
34 34
 Commands for managing a cluster are exposed here. Many administrative
35 35
 actions involve interaction with the command-line client as well.`
36 36
 
37
-func NewCommandAdmin(name, fullName string, out io.Writer, errout io.Writer) *cobra.Command {
37
+func NewCommandAdmin(name, fullName string, in io.Reader, out io.Writer, errout io.Writer) *cobra.Command {
38 38
 	// Main command
39 39
 	cmds := &cobra.Command{
40 40
 		Use:   name,
... ...
@@ -82,6 +85,12 @@ func NewCommandAdmin(name, fullName string, out io.Writer, errout io.Writer) *co
82 82
 				diagnostics.NewCmdDiagnostics(diagnostics.DiagnosticsRecommendedName, fullName+" "+diagnostics.DiagnosticsRecommendedName, out),
83 83
 				prune.NewCommandPrune(prune.PruneRecommendedName, fullName+" "+prune.PruneRecommendedName, f, out),
84 84
 				buildchain.NewCmdBuildChain(name, fullName+" "+buildchain.BuildChainRecommendedCommandName, f, out),
85
+				migrate.NewCommandMigrate(
86
+					migrate.MigrateRecommendedName, fullName+" "+migrate.MigrateRecommendedName, f, out,
87
+					// Migration commands
88
+					migrateimages.NewCmdMigrateImageReferences("image-references", fullName+" "+migrate.MigrateRecommendedName+" image-references", f, in, out, errout),
89
+					migratestorage.NewCmdMigrateAPIStorage("storage", fullName+" "+migrate.MigrateRecommendedName+" storage", f, in, out, errout),
90
+				),
85 91
 			},
86 92
 		},
87 93
 		{
88 94
new file mode 100644
... ...
@@ -0,0 +1,468 @@
0
+package images
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"io"
6
+	"net/url"
7
+	"strings"
8
+
9
+	"github.com/spf13/cobra"
10
+
11
+	kapi "k8s.io/kubernetes/pkg/api"
12
+	"k8s.io/kubernetes/pkg/credentialprovider"
13
+	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
14
+	"k8s.io/kubernetes/pkg/kubectl/resource"
15
+	"k8s.io/kubernetes/pkg/runtime"
16
+
17
+	buildapi "github.com/openshift/origin/pkg/build/api"
18
+	"github.com/openshift/origin/pkg/client"
19
+	"github.com/openshift/origin/pkg/cmd/admin/migrate"
20
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
21
+	imageapi "github.com/openshift/origin/pkg/image/api"
22
+)
23
+
24
+const (
25
+	internalMigrateImagesLong = `
26
+Migrate references to Docker images
27
+
28
+This command updates embedded Docker image references on the server in place. By default it
29
+will update image streams and images, and may be used to update resources with a pod template
30
+(deployments, replication controllers, daemon sets).
31
+
32
+References are changed by providing a mapping between a source registry and name and the
33
+desired registry and name. Either name or registry can be set to '*' to change all values.
34
+The registry value "docker.io" is special and will handle any image reference that refers to
35
+the DockerHub. You may pass multiple mappings - the first matching mapping will be applied
36
+per resource.
37
+
38
+The following resource types may be migrated by this command:
39
+
40
+* images               * daemonsets
41
+* imagestreams         * jobs
42
+* buildconfigs         * replicationcontrollers
43
+* deploymentconfigs    * pods
44
+* secrets (docker)
45
+
46
+Only images, imagestreams, and secrets are updated by default. Updating images and image
47
+streams requires administrative privileges.`
48
+
49
+	internalMigrateImagesExample = `  # Perform a dry-run of migrating all "docker.io" references to "myregistry.com"
50
+  %[1]s docker.io/*=myregistry.com/*
51
+
52
+  # To actually perform the migration, the confirm flag must be appended
53
+  %[1]s docker.io/*=myregistry.com/* --confirm
54
+
55
+  # To see more details of what will be migrated, use the loglevel and output flags
56
+  %[1]s docker.io/*=myregistry.com/* --loglevel=2 -o yaml
57
+
58
+  # Migrate from a service IP to an internal service DNS name
59
+  %[1]s 172.30.1.54/*=registry.openshift.svc.cluster.local/*
60
+
61
+  # Migrate from a service IP to an internal service DNS name for all deployment configs and builds
62
+  %[1]s 172.30.1.54/*=registry.openshift.svc.cluster.local/* --include=buildconfigs,deploymentconfigs`
63
+)
64
+
65
+type MigrateImageReferenceOptions struct {
66
+	migrate.ResourceOptions
67
+
68
+	Client          client.Interface
69
+	Mappings        ImageReferenceMappings
70
+	UpdatePodSpecFn func(obj runtime.Object, fn func(*kapi.PodSpec) error) (bool, error)
71
+}
72
+
73
+// NewCmdMigrateImageReferences implements a MigrateImages command
74
+func NewCmdMigrateImageReferences(name, fullName string, f *clientcmd.Factory, in io.Reader, out, errout io.Writer) *cobra.Command {
75
+	options := &MigrateImageReferenceOptions{
76
+		ResourceOptions: migrate.ResourceOptions{
77
+			In:      in,
78
+			Out:     out,
79
+			ErrOut:  errout,
80
+			Include: []string{"imagestream", "image", "secrets"},
81
+		},
82
+	}
83
+	cmd := &cobra.Command{
84
+		Use:     fmt.Sprintf("%s REGISTRY/NAME=REGISTRY/NAME [...]", name),
85
+		Short:   "Update embedded Docker image references",
86
+		Long:    internalMigrateImagesLong,
87
+		Example: fmt.Sprintf(internalMigrateImagesExample, fullName),
88
+		Run: func(cmd *cobra.Command, args []string) {
89
+			kcmdutil.CheckErr(options.Complete(f, cmd, args))
90
+			kcmdutil.CheckErr(options.Validate())
91
+			kcmdutil.CheckErr(options.Run())
92
+		},
93
+	}
94
+	options.ResourceOptions.Bind(cmd)
95
+
96
+	return cmd
97
+}
98
+
99
+func (o *MigrateImageReferenceOptions) Complete(f *clientcmd.Factory, c *cobra.Command, args []string) error {
100
+	var remainingArgs []string
101
+	for _, s := range args {
102
+		if !strings.Contains(s, "=") {
103
+			remainingArgs = append(remainingArgs, s)
104
+			continue
105
+		}
106
+		mapping, err := ParseMapping(s)
107
+		if err != nil {
108
+			return err
109
+		}
110
+		o.Mappings = append(o.Mappings, mapping)
111
+	}
112
+
113
+	o.UpdatePodSpecFn = f.UpdatePodSpecForObject
114
+
115
+	if len(remainingArgs) > 0 {
116
+		return fmt.Errorf("all arguments must be valid FROM=TO mappings")
117
+	}
118
+
119
+	o.ResourceOptions.SaveFn = o.save
120
+	if err := o.ResourceOptions.Complete(f, c); err != nil {
121
+		return err
122
+	}
123
+
124
+	osclient, _, err := f.Clients()
125
+	if err != nil {
126
+		return err
127
+	}
128
+	o.Client = osclient
129
+
130
+	return nil
131
+}
132
+
133
+func (o MigrateImageReferenceOptions) Validate() error {
134
+	if len(o.Mappings) == 0 {
135
+		return fmt.Errorf("at least one mapping argument must be specified: REGISTRY/NAME=REGISTRY/NAME")
136
+	}
137
+	return o.ResourceOptions.Validate()
138
+}
139
+
140
+func (o MigrateImageReferenceOptions) Run() error {
141
+	return o.ResourceOptions.Visitor().Visit(func(info *resource.Info) (migrate.Reporter, error) {
142
+		return o.transform(info.Object)
143
+	})
144
+}
145
+
146
+// save invokes the API to alter an object. The reporter passed to this method is the same returned by
147
+// the migration visitor method (for this type, transformImageReferences). It should return an error
148
+// if the input type cannot be saved. It returns migrate.ErrRecalculate if migration should be re-run
149
+// on the provided object.
150
+func (o *MigrateImageReferenceOptions) save(info *resource.Info, reporter migrate.Reporter) error {
151
+	switch t := info.Object.(type) {
152
+	case *imageapi.ImageStream:
153
+		// update status first so that a subsequent spec update won't pull incorrect values
154
+		if reporter.(imageChangeInfo).status {
155
+			updated, err := o.Client.ImageStreams(t.Namespace).UpdateStatus(t)
156
+			if err != nil {
157
+				return migrate.DefaultRetriable(info, err)
158
+			}
159
+			info.Refresh(updated, true)
160
+			return migrate.ErrRecalculate
161
+		}
162
+		if reporter.(imageChangeInfo).spec {
163
+			updated, err := o.Client.ImageStreams(t.Namespace).Update(t)
164
+			if err != nil {
165
+				return migrate.DefaultRetriable(info, err)
166
+			}
167
+			info.Refresh(updated, true)
168
+		}
169
+		return nil
170
+	default:
171
+		if _, err := resource.NewHelper(info.Client, info.Mapping).Replace(info.Namespace, info.Name, false, info.Object); err != nil {
172
+			return migrate.DefaultRetriable(info, err)
173
+		}
174
+	}
175
+	return nil
176
+}
177
+
178
+// transform checks image references on the provided object and returns either a reporter (indicating
179
+// that the object was recognized and whether it was updated) or an error.
180
+func (o *MigrateImageReferenceOptions) transform(obj runtime.Object) (migrate.Reporter, error) {
181
+	fn := o.Mappings.MapReference
182
+	switch t := obj.(type) {
183
+	case *imageapi.Image:
184
+		var changed bool
185
+		if updated := fn(t.DockerImageReference); updated != t.DockerImageReference {
186
+			changed = true
187
+			t.DockerImageReference = updated
188
+		}
189
+		return reporter(changed), nil
190
+	case *imageapi.ImageStream:
191
+		var info imageChangeInfo
192
+		if len(t.Spec.DockerImageRepository) > 0 {
193
+			info.spec = updateString(&t.Spec.DockerImageRepository, fn)
194
+		}
195
+		for _, ref := range t.Spec.Tags {
196
+			if ref.From == nil || ref.From.Kind != "DockerImage" {
197
+				continue
198
+			}
199
+			info.spec = updateString(&ref.From.Name, fn) || info.spec
200
+		}
201
+		for _, events := range t.Status.Tags {
202
+			for i := range events.Items {
203
+				info.status = updateString(&events.Items[i].DockerImageReference, fn) || info.status
204
+			}
205
+		}
206
+		return info, nil
207
+	case *kapi.Secret:
208
+		switch t.Type {
209
+		case kapi.SecretTypeDockercfg:
210
+			var v credentialprovider.DockerConfig
211
+			if err := json.Unmarshal(t.Data[kapi.DockerConfigKey], &v); err != nil {
212
+				return nil, err
213
+			}
214
+			if !updateDockerConfig(v, o.Mappings.MapDockerAuthKey) {
215
+				return reporter(false), nil
216
+			}
217
+			data, err := json.Marshal(v)
218
+			if err != nil {
219
+				return nil, err
220
+			}
221
+			t.Data[kapi.DockerConfigKey] = data
222
+			return reporter(true), nil
223
+		case kapi.SecretTypeDockerConfigJson:
224
+			var v credentialprovider.DockerConfigJson
225
+			if err := json.Unmarshal(t.Data[kapi.DockerConfigJsonKey], &v); err != nil {
226
+				return nil, err
227
+			}
228
+			if !updateDockerConfig(v.Auths, o.Mappings.MapDockerAuthKey) {
229
+				return reporter(false), nil
230
+			}
231
+			data, err := json.Marshal(v)
232
+			if err != nil {
233
+				return nil, err
234
+			}
235
+			t.Data[kapi.DockerConfigJsonKey] = data
236
+			return reporter(true), nil
237
+		default:
238
+			return reporter(false), nil
239
+		}
240
+	case *buildapi.BuildConfig:
241
+		var changed bool
242
+		if to := t.Spec.Output.To; to != nil && to.Kind == "DockerImage" {
243
+			changed = updateString(&to.Name, fn) || changed
244
+		}
245
+		for i, image := range t.Spec.Source.Images {
246
+			if image.From.Kind == "DockerImage" {
247
+				changed = updateString(&t.Spec.Source.Images[i].From.Name, fn) || changed
248
+			}
249
+		}
250
+		if c := t.Spec.Strategy.CustomStrategy; c != nil && c.From.Kind == "DockerImage" {
251
+			changed = updateString(&c.From.Name, fn) || changed
252
+		}
253
+		if c := t.Spec.Strategy.DockerStrategy; c != nil && c.From.Kind == "DockerImage" {
254
+			changed = updateString(&c.From.Name, fn) || changed
255
+		}
256
+		if c := t.Spec.Strategy.SourceStrategy; c != nil && c.From.Kind == "DockerImage" {
257
+			changed = updateString(&c.From.Name, fn) || changed
258
+		}
259
+		return reporter(changed), nil
260
+	default:
261
+		if o.UpdatePodSpecFn != nil {
262
+			var changed bool
263
+			supports, err := o.UpdatePodSpecFn(obj, func(spec *kapi.PodSpec) error {
264
+				changed = updatePodSpec(spec, fn)
265
+				return nil
266
+			})
267
+			if !supports {
268
+				return nil, nil
269
+			}
270
+			if err != nil {
271
+				return nil, err
272
+			}
273
+			return reporter(changed), nil
274
+		}
275
+	}
276
+	// TODO: implement use of the generic PodTemplate accessor from the factory to handle
277
+	// any object with a pod template
278
+	return nil, nil
279
+}
280
+
281
+// reporter implements the Reporter interface for a boolean.
282
+type reporter bool
283
+
284
+func (r reporter) Changed() bool {
285
+	return bool(r)
286
+}
287
+
288
+// imageChangeInfo indicates whether the spec or status of an image stream was changed.
289
+type imageChangeInfo struct {
290
+	spec, status bool
291
+}
292
+
293
+func (i imageChangeInfo) Changed() bool {
294
+	return i.spec || i.status
295
+}
296
+
297
+type TransformImageFunc func(in string) string
298
+
299
+func updateString(value *string, fn TransformImageFunc) bool {
300
+	result := fn(*value)
301
+	if result != *value {
302
+		*value = result
303
+		return true
304
+	}
305
+	return false
306
+}
307
+
308
+func updatePodSpec(spec *kapi.PodSpec, fn TransformImageFunc) bool {
309
+	var changed bool
310
+	for i := range spec.Containers {
311
+		changed = updateString(&spec.Containers[i].Image, fn) || changed
312
+	}
313
+	return changed
314
+}
315
+
316
+func updateDockerConfig(cfg credentialprovider.DockerConfig, fn TransformImageFunc) bool {
317
+	var changed bool
318
+	for k, v := range cfg {
319
+		original := k
320
+		if updateString(&k, fn) {
321
+			changed = true
322
+			delete(cfg, original)
323
+			cfg[k] = v
324
+		}
325
+	}
326
+	return changed
327
+}
328
+
329
+// ImageReferenceMapping represents a transformation of an image reference.
330
+type ImageReferenceMapping struct {
331
+	FromRegistry string
332
+	FromName     string
333
+	ToRegistry   string
334
+	ToName       string
335
+}
336
+
337
+// ParseMapping converts a string in the form "(REGISTRY|*)/(NAME|*)" to an ImageReferenceMapping
338
+// or returns a user-facing error. REGISTRY is the image registry value (hostname) or "docker.io".
339
+// NAME is the full repository name (the path relative to the registry root).
340
+// TODO: handle v2 repository names, which can have multiple segments (must fix
341
+//   ParseDockerImageReference)
342
+func ParseMapping(s string) (ImageReferenceMapping, error) {
343
+	parts := strings.SplitN(s, "=", 2)
344
+	from := strings.SplitN(parts[0], "/", 2)
345
+	to := strings.SplitN(parts[1], "/", 2)
346
+	if len(from) < 2 || len(to) < 2 {
347
+		return ImageReferenceMapping{}, fmt.Errorf("all arguments must be of the form REGISTRY/NAME=REGISTRY/NAME, where registry or name may be '*' or a value")
348
+	}
349
+	if len(from[0]) == 0 {
350
+		return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid source: registry must be specified (may be '*')", parts[0])
351
+	}
352
+	if len(from[1]) == 0 {
353
+		return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid source: name must be specified (may be '*')", parts[0])
354
+	}
355
+	if len(to[0]) == 0 {
356
+		return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid target: registry must be specified (may be '*')", parts[1])
357
+	}
358
+	if len(to[1]) == 0 {
359
+		return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid target: name must be specified (may be '*')", parts[1])
360
+	}
361
+	if from[0] == "*" {
362
+		from[0] = ""
363
+	}
364
+	if from[1] == "*" {
365
+		from[1] = ""
366
+	}
367
+	if to[0] == "*" {
368
+		to[0] = ""
369
+	}
370
+	if to[1] == "*" {
371
+		to[1] = ""
372
+	}
373
+	if to[0] == "" && to[1] == "" {
374
+		return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid target: at least one change must be specified", parts[1])
375
+	}
376
+	if from[0] == to[0] && from[1] == to[1] {
377
+		return ImageReferenceMapping{}, fmt.Errorf("%q is not valid: must target at least one field to change", s)
378
+	}
379
+	return ImageReferenceMapping{
380
+		FromRegistry: from[0],
381
+		FromName:     from[1],
382
+		ToRegistry:   to[0],
383
+		ToName:       to[1],
384
+	}, nil
385
+}
386
+
387
+// ImageReferenceMappings provide a convenience method for transforming an input reference
388
+type ImageReferenceMappings []ImageReferenceMapping
389
+
390
+// MapReference transforms the provided Docker image reference if any mapping matches the
391
+// input. If the reference cannot be parsed, it will not be modified.
392
+func (m ImageReferenceMappings) MapReference(in string) string {
393
+	ref, err := imageapi.ParseDockerImageReference(in)
394
+	if err != nil {
395
+		return in
396
+	}
397
+	registry := ref.DockerClientDefaults().Registry
398
+	name := ref.RepositoryName()
399
+	for _, mapping := range m {
400
+		if len(mapping.FromRegistry) > 0 && mapping.FromRegistry != registry {
401
+			continue
402
+		}
403
+		if len(mapping.FromName) > 0 && mapping.FromName != name {
404
+			continue
405
+		}
406
+		if len(mapping.ToRegistry) > 0 {
407
+			ref.Registry = mapping.ToRegistry
408
+		}
409
+		if len(mapping.ToName) > 0 {
410
+			ref.Namespace = ""
411
+			ref.Name = mapping.ToName
412
+		}
413
+		return ref.Exact()
414
+	}
415
+	return in
416
+}
417
+
418
+// MapDockerAuthKey transforms the provided Docker Config host key if any mapping matches
419
+// the input. If the reference cannot be parsed, it will not be modified.
420
+func (m ImageReferenceMappings) MapDockerAuthKey(in string) string {
421
+	value := in
422
+	if len(value) == 0 {
423
+		value = imageapi.DockerDefaultV1Registry
424
+	}
425
+	if !strings.HasPrefix(value, "https://") && !strings.HasPrefix(value, "http://") {
426
+		value = "https://" + value
427
+	}
428
+	parsed, err := url.Parse(value)
429
+	if err != nil {
430
+		return in
431
+	}
432
+	// The docker client allows exact matches:
433
+	//    foo.bar.com/namespace
434
+	// Or hostname matches:
435
+	//    foo.bar.com
436
+	// It also considers /v2/  and /v1/ equivalent to the hostname
437
+	// See ResolveAuthConfig in docker/registry/auth.go.
438
+	registry := parsed.Host
439
+	name := parsed.Path
440
+	switch {
441
+	case name == "/":
442
+		name = ""
443
+	case strings.HasPrefix(name, "/v2/") || strings.HasPrefix(name, "/v1/"):
444
+		name = name[4:]
445
+	case strings.HasPrefix(name, "/"):
446
+		name = name[1:]
447
+	}
448
+	for _, mapping := range m {
449
+		if len(mapping.FromRegistry) > 0 && mapping.FromRegistry != registry {
450
+			continue
451
+		}
452
+		if len(mapping.FromName) > 0 && mapping.FromName != name {
453
+			continue
454
+		}
455
+		if len(mapping.ToRegistry) > 0 {
456
+			registry = mapping.ToRegistry
457
+		}
458
+		if len(mapping.ToName) > 0 {
459
+			name = mapping.ToName
460
+		}
461
+		if len(name) > 0 {
462
+			return registry + "/" + name
463
+		}
464
+		return registry
465
+	}
466
+	return in
467
+}
0 468
new file mode 100644
... ...
@@ -0,0 +1,601 @@
0
+package images
1
+
2
+import (
3
+	"testing"
4
+
5
+	kapi "k8s.io/kubernetes/pkg/api"
6
+	kbatch "k8s.io/kubernetes/pkg/apis/batch"
7
+	kextensions "k8s.io/kubernetes/pkg/apis/extensions"
8
+	"k8s.io/kubernetes/pkg/runtime"
9
+	"k8s.io/kubernetes/pkg/util/diff"
10
+
11
+	buildapi "github.com/openshift/origin/pkg/build/api"
12
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
13
+	deployapi "github.com/openshift/origin/pkg/deploy/api"
14
+	imageapi "github.com/openshift/origin/pkg/image/api"
15
+)
16
+
17
+func TestImageReferenceMappingsMapReference(t *testing.T) {
18
+	testCases := []struct {
19
+		mappings ImageReferenceMappings
20
+		results  map[string]string
21
+	}{
22
+		{
23
+			mappings: ImageReferenceMappings{{FromRegistry: "docker.io", ToRegistry: "index.docker.io"}},
24
+			results: map[string]string{
25
+				"mysql":                "index.docker.io/mysql",
26
+				"mysql:latest":         "index.docker.io/mysql:latest",
27
+				"default/mysql:latest": "index.docker.io/default/mysql:latest",
28
+
29
+				"mysql@sha256:b2f400f4a5e003b0543decf61a0a010939f3fba07bafa226f11ed7b5f1e81237": "index.docker.io/mysql@sha256:b2f400f4a5e003b0543decf61a0a010939f3fba07bafa226f11ed7b5f1e81237",
30
+
31
+				"docker.io/mysql":                "index.docker.io/library/mysql",
32
+				"docker.io/mysql:latest":         "index.docker.io/library/mysql:latest",
33
+				"docker.io/default/mysql:latest": "index.docker.io/default/mysql:latest",
34
+
35
+				"docker.io/mysql@sha256:b2f400f4a5e003b0543decf61a0a010939f3fba07bafa226f11ed7b5f1e81237": "index.docker.io/library/mysql@sha256:b2f400f4a5e003b0543decf61a0a010939f3fba07bafa226f11ed7b5f1e81237",
36
+			},
37
+		},
38
+		{
39
+			mappings: ImageReferenceMappings{{FromName: "test/other", ToRegistry: "another.registry"}},
40
+			results: map[string]string{
41
+				"test/other":                       "another.registry/test/other",
42
+				"test/other:latest":                "another.registry/test/other:latest",
43
+				"myregistry.com/test/other:latest": "another.registry/test/other:latest",
44
+
45
+				"myregistry.com/b/test/other:latest": "myregistry.com/b/test/other:latest",
46
+			},
47
+		},
48
+		{
49
+			mappings: ImageReferenceMappings{{FromName: "test/other", ToName: "other/test"}},
50
+			results: map[string]string{
51
+				"test/other":                       "other/test",
52
+				"test/other:latest":                "other/test:latest",
53
+				"myregistry.com/test/other:latest": "myregistry.com/other/test:latest",
54
+
55
+				"test/other/b:latest": "test/other/b:latest",
56
+
57
+				// TODO: this is possibly wrong with V2 and latest daemon
58
+				"b/test/other:latest": "b/other/test:latest",
59
+			},
60
+		},
61
+	}
62
+
63
+	for i, test := range testCases {
64
+		for in, out := range test.results {
65
+			result := test.mappings.MapReference(in)
66
+			if result != out {
67
+				t.Errorf("%d: expect %s -> %s, got %q", i, in, out, result)
68
+				continue
69
+			}
70
+		}
71
+	}
72
+}
73
+
74
+func TestImageReferenceMappingsMapDockerAuthKey(t *testing.T) {
75
+	testCases := []struct {
76
+		mappings ImageReferenceMappings
77
+		results  map[string]string
78
+	}{
79
+		{
80
+			mappings: ImageReferenceMappings{{FromRegistry: "docker.io", ToRegistry: "index.docker.io"}},
81
+			results: map[string]string{
82
+				"docker.io":                   "index.docker.io",
83
+				"index.docker.io":             "index.docker.io",
84
+				"https://index.docker.io/v1/": "https://index.docker.io/v1/",
85
+				"https://docker.io/v1/":       "index.docker.io",
86
+
87
+				"other.docker.io":             "other.docker.io",
88
+				"other.docker.io/names":       "other.docker.io/names",
89
+				"other.docker.io:5000/names":  "other.docker.io:5000/names",
90
+				"https://other.docker.io/v1/": "https://other.docker.io/v1/",
91
+			},
92
+		},
93
+		{
94
+			mappings: ImageReferenceMappings{{FromRegistry: "index.docker.io", ToRegistry: "another.registry"}},
95
+			results: map[string]string{
96
+				"index.docker.io":                  "another.registry",
97
+				"index.docker.io/other":            "another.registry/other",
98
+				"https://index.docker.io/v1/other": "another.registry/other",
99
+				"https://index.docker.io/v1/":      "another.registry",
100
+				"https://index.docker.io/":         "another.registry",
101
+				"https://index.docker.io":          "another.registry",
102
+
103
+				"docker.io":                   "docker.io",
104
+				"https://docker.io/v1/":       "https://docker.io/v1/",
105
+				"other.docker.io":             "other.docker.io",
106
+				"other.docker.io/names":       "other.docker.io/names",
107
+				"other.docker.io:5000/names":  "other.docker.io:5000/names",
108
+				"https://other.docker.io/v1/": "https://other.docker.io/v1/",
109
+			},
110
+		},
111
+		{
112
+			mappings: ImageReferenceMappings{{FromRegistry: "index.docker.io", ToRegistry: "another.registry", ToName: "extra"}},
113
+			results: map[string]string{
114
+				"index.docker.io":                  "another.registry/extra",
115
+				"index.docker.io/other":            "another.registry/extra",
116
+				"https://index.docker.io/v1/other": "another.registry/extra",
117
+				"https://index.docker.io/v1/":      "another.registry/extra",
118
+				"https://index.docker.io/":         "another.registry/extra",
119
+
120
+				"docker.io":                   "docker.io",
121
+				"https://docker.io/v1/":       "https://docker.io/v1/",
122
+				"other.docker.io":             "other.docker.io",
123
+				"other.docker.io/names":       "other.docker.io/names",
124
+				"other.docker.io:5000/names":  "other.docker.io:5000/names",
125
+				"https://other.docker.io/v1/": "https://other.docker.io/v1/",
126
+			},
127
+		},
128
+	}
129
+
130
+	for i, test := range testCases {
131
+		for in, out := range test.results {
132
+			result := test.mappings.MapDockerAuthKey(in)
133
+			if result != out {
134
+				t.Errorf("%d: expect %s -> %s, got %q", i, in, out, result)
135
+				continue
136
+			}
137
+		}
138
+	}
139
+}
140
+
141
+func TestTransform(t *testing.T) {
142
+	type variant struct {
143
+		changed       bool
144
+		nilReporter   bool
145
+		err           bool
146
+		obj, expected runtime.Object
147
+	}
148
+	testCases := []struct {
149
+		mappings ImageReferenceMappings
150
+		variants []variant
151
+	}{
152
+		{
153
+			mappings: ImageReferenceMappings{{FromRegistry: "docker.io", ToRegistry: "index.docker.io"}},
154
+			variants: []variant{
155
+				{
156
+					obj: &kapi.Pod{
157
+						Spec: kapi.PodSpec{
158
+							Containers: []kapi.Container{
159
+								{Image: "docker.io/foo/bar"},
160
+								{Image: "foo/bar"},
161
+							},
162
+						},
163
+					},
164
+					changed: true,
165
+					expected: &kapi.Pod{
166
+						Spec: kapi.PodSpec{
167
+							Containers: []kapi.Container{
168
+								{Image: "index.docker.io/foo/bar"},
169
+								{Image: "index.docker.io/foo/bar"},
170
+							},
171
+						},
172
+					},
173
+				},
174
+				{
175
+					obj: &kapi.ReplicationController{
176
+						Spec: kapi.ReplicationControllerSpec{
177
+							Template: &kapi.PodTemplateSpec{
178
+								Spec: kapi.PodSpec{
179
+									Containers: []kapi.Container{
180
+										{Image: "docker.io/foo/bar"},
181
+										{Image: "foo/bar"},
182
+									},
183
+								},
184
+							},
185
+						},
186
+					},
187
+					changed: true,
188
+					expected: &kapi.ReplicationController{
189
+						Spec: kapi.ReplicationControllerSpec{
190
+							Template: &kapi.PodTemplateSpec{
191
+								Spec: kapi.PodSpec{
192
+									Containers: []kapi.Container{
193
+										{Image: "index.docker.io/foo/bar"},
194
+										{Image: "index.docker.io/foo/bar"},
195
+									},
196
+								},
197
+							},
198
+						},
199
+					},
200
+				},
201
+				{
202
+					obj: &kextensions.Deployment{
203
+						Spec: kextensions.DeploymentSpec{
204
+							Template: kapi.PodTemplateSpec{
205
+								Spec: kapi.PodSpec{
206
+									Containers: []kapi.Container{
207
+										{Image: "docker.io/foo/bar"},
208
+										{Image: "foo/bar"},
209
+									},
210
+								},
211
+							},
212
+						},
213
+					},
214
+					changed: true,
215
+					expected: &kextensions.Deployment{
216
+						Spec: kextensions.DeploymentSpec{
217
+							Template: kapi.PodTemplateSpec{
218
+								Spec: kapi.PodSpec{
219
+									Containers: []kapi.Container{
220
+										{Image: "index.docker.io/foo/bar"},
221
+										{Image: "index.docker.io/foo/bar"},
222
+									},
223
+								},
224
+							},
225
+						},
226
+					},
227
+				},
228
+				{
229
+					obj: &deployapi.DeploymentConfig{
230
+						Spec: deployapi.DeploymentConfigSpec{
231
+							Template: &kapi.PodTemplateSpec{
232
+								Spec: kapi.PodSpec{
233
+									Containers: []kapi.Container{
234
+										{Image: "docker.io/foo/bar"},
235
+										{Image: "foo/bar"},
236
+									},
237
+								},
238
+							},
239
+						},
240
+					},
241
+					changed: true,
242
+					expected: &deployapi.DeploymentConfig{
243
+						Spec: deployapi.DeploymentConfigSpec{
244
+							Template: &kapi.PodTemplateSpec{
245
+								Spec: kapi.PodSpec{
246
+									Containers: []kapi.Container{
247
+										{Image: "index.docker.io/foo/bar"},
248
+										{Image: "index.docker.io/foo/bar"},
249
+									},
250
+								},
251
+							},
252
+						},
253
+					},
254
+				},
255
+				{
256
+					obj: &kextensions.DaemonSet{
257
+						Spec: kextensions.DaemonSetSpec{
258
+							Template: kapi.PodTemplateSpec{
259
+								Spec: kapi.PodSpec{
260
+									Containers: []kapi.Container{
261
+										{Image: "docker.io/foo/bar"},
262
+										{Image: "foo/bar"},
263
+									},
264
+								},
265
+							},
266
+						},
267
+					},
268
+					changed: true,
269
+					expected: &kextensions.DaemonSet{
270
+						Spec: kextensions.DaemonSetSpec{
271
+							Template: kapi.PodTemplateSpec{
272
+								Spec: kapi.PodSpec{
273
+									Containers: []kapi.Container{
274
+										{Image: "index.docker.io/foo/bar"},
275
+										{Image: "index.docker.io/foo/bar"},
276
+									},
277
+								},
278
+							},
279
+						},
280
+					},
281
+				},
282
+				{
283
+					obj: &kextensions.ReplicaSet{
284
+						Spec: kextensions.ReplicaSetSpec{
285
+							Template: kapi.PodTemplateSpec{
286
+								Spec: kapi.PodSpec{
287
+									Containers: []kapi.Container{
288
+										{Image: "docker.io/foo/bar"},
289
+										{Image: "foo/bar"},
290
+									},
291
+								},
292
+							},
293
+						},
294
+					},
295
+					changed: true,
296
+					expected: &kextensions.ReplicaSet{
297
+						Spec: kextensions.ReplicaSetSpec{
298
+							Template: kapi.PodTemplateSpec{
299
+								Spec: kapi.PodSpec{
300
+									Containers: []kapi.Container{
301
+										{Image: "index.docker.io/foo/bar"},
302
+										{Image: "index.docker.io/foo/bar"},
303
+									},
304
+								},
305
+							},
306
+						},
307
+					},
308
+				},
309
+				{
310
+					obj: &kbatch.Job{
311
+						Spec: kbatch.JobSpec{
312
+							Template: kapi.PodTemplateSpec{
313
+								Spec: kapi.PodSpec{
314
+									Containers: []kapi.Container{
315
+										{Image: "docker.io/foo/bar"},
316
+										{Image: "foo/bar"},
317
+									},
318
+								},
319
+							},
320
+						},
321
+					},
322
+					changed: true,
323
+					expected: &kbatch.Job{
324
+						Spec: kbatch.JobSpec{
325
+							Template: kapi.PodTemplateSpec{
326
+								Spec: kapi.PodSpec{
327
+									Containers: []kapi.Container{
328
+										{Image: "index.docker.io/foo/bar"},
329
+										{Image: "index.docker.io/foo/bar"},
330
+									},
331
+								},
332
+							},
333
+						},
334
+					},
335
+				},
336
+				{
337
+					obj:         &kapi.Node{},
338
+					nilReporter: true,
339
+				},
340
+				{
341
+					obj: &buildapi.BuildConfig{
342
+						Spec: buildapi.BuildConfigSpec{
343
+							CommonSpec: buildapi.CommonSpec{
344
+								Output: buildapi.BuildOutput{To: &kapi.ObjectReference{Kind: "DockerImage", Name: "docker.io/foo/bar"}},
345
+								Source: buildapi.BuildSource{
346
+									Images: []buildapi.ImageSource{
347
+										{From: kapi.ObjectReference{Kind: "DockerImage", Name: "docker.io/foo/bar"}},
348
+										{From: kapi.ObjectReference{Kind: "DockerImage", Name: "foo/bar"}},
349
+									},
350
+								},
351
+								Strategy: buildapi.BuildStrategy{
352
+									DockerStrategy: &buildapi.DockerBuildStrategy{From: &kapi.ObjectReference{Kind: "DockerImage", Name: "docker.io/foo/bar"}},
353
+									SourceStrategy: &buildapi.SourceBuildStrategy{From: kapi.ObjectReference{Kind: "DockerImage", Name: "docker.io/foo/bar"}},
354
+									CustomStrategy: &buildapi.CustomBuildStrategy{From: kapi.ObjectReference{Kind: "DockerImage", Name: "docker.io/foo/bar"}},
355
+								},
356
+							},
357
+						},
358
+					},
359
+					changed: true,
360
+					expected: &buildapi.BuildConfig{
361
+						Spec: buildapi.BuildConfigSpec{
362
+							CommonSpec: buildapi.CommonSpec{
363
+								Output: buildapi.BuildOutput{To: &kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/foo/bar"}},
364
+								Source: buildapi.BuildSource{
365
+									Images: []buildapi.ImageSource{
366
+										{From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/foo/bar"}},
367
+										{From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/foo/bar"}},
368
+									},
369
+								},
370
+								Strategy: buildapi.BuildStrategy{
371
+									DockerStrategy: &buildapi.DockerBuildStrategy{From: &kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/foo/bar"}},
372
+									SourceStrategy: &buildapi.SourceBuildStrategy{From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/foo/bar"}},
373
+									CustomStrategy: &buildapi.CustomBuildStrategy{From: kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/foo/bar"}},
374
+								},
375
+							},
376
+						},
377
+					},
378
+				},
379
+				{
380
+					obj: &kapi.Secret{
381
+						Type: kapi.SecretTypeDockercfg,
382
+						Data: map[string][]byte{
383
+							kapi.DockerConfigKey: []byte(`{"docker.io":{"auth":"Og=="},"other.server":{"auth":"Og=="}}`),
384
+							"another":            []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
385
+						},
386
+					},
387
+					changed: true,
388
+					expected: &kapi.Secret{
389
+						Type: kapi.SecretTypeDockercfg,
390
+						Data: map[string][]byte{
391
+							kapi.DockerConfigKey: []byte(`{"index.docker.io":{"auth":"Og=="},"other.server":{"auth":"Og=="}}`),
392
+							"another":            []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
393
+						},
394
+					},
395
+				},
396
+				{
397
+					obj: &kapi.Secret{
398
+						Type: kapi.SecretTypeDockercfg,
399
+						Data: map[string][]byte{
400
+							kapi.DockerConfigKey: []byte(`{"myserver.com":{"auth":"Og=="},"other.server":{"auth":"Og=="}}`),
401
+							"another":            []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
402
+						},
403
+					},
404
+					expected: &kapi.Secret{
405
+						Type: kapi.SecretTypeDockercfg,
406
+						Data: map[string][]byte{
407
+							kapi.DockerConfigKey: []byte(`{"myserver.com":{"auth":"Og=="},"other.server":{"auth":"Og=="}}`),
408
+							"another":            []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
409
+						},
410
+					},
411
+				},
412
+				{
413
+					obj: &kapi.Secret{
414
+						Type: kapi.SecretTypeDockerConfigJson,
415
+						Data: map[string][]byte{
416
+							kapi.DockerConfigJsonKey: []byte(`{"auths":{"docker.io":{"auth":"Og=="},"other.server":{"auth":"Og=="}}}`),
417
+							"another":                []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
418
+						},
419
+					},
420
+					changed: true,
421
+					expected: &kapi.Secret{
422
+						Type: kapi.SecretTypeDockerConfigJson,
423
+						Data: map[string][]byte{
424
+							kapi.DockerConfigJsonKey: []byte(`{"auths":{"index.docker.io":{"auth":"Og=="},"other.server":{"auth":"Og=="}}}`),
425
+							"another":                []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
426
+						},
427
+					},
428
+				},
429
+				{
430
+					obj: &kapi.Secret{
431
+						Type: kapi.SecretTypeDockerConfigJson,
432
+						Data: map[string][]byte{
433
+							kapi.DockerConfigJsonKey: []byte(`{"auths":{"myserver.com":{},"other.server":{}}}`),
434
+							"another":                []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
435
+						},
436
+					},
437
+					expected: &kapi.Secret{
438
+						Type: kapi.SecretTypeDockerConfigJson,
439
+						Data: map[string][]byte{
440
+							kapi.DockerConfigJsonKey: []byte(`{"auths":{"myserver.com":{},"other.server":{}}}`),
441
+							"another":                []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
442
+						},
443
+					},
444
+				},
445
+				{
446
+					obj: &kapi.Secret{
447
+						Type: kapi.SecretTypeDockercfg,
448
+						Data: map[string][]byte{
449
+							kapi.DockerConfigKey: []byte(`{"auths":{`),
450
+							"another":            []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
451
+						},
452
+					},
453
+					err: true,
454
+					expected: &kapi.Secret{
455
+						Type: kapi.SecretTypeDockercfg,
456
+						Data: map[string][]byte{
457
+							kapi.DockerConfigKey: []byte(`{"auths":{`),
458
+							"another":            []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
459
+						},
460
+					},
461
+				},
462
+				{
463
+					obj: &kapi.Secret{
464
+						Type: kapi.SecretTypeDockerConfigJson,
465
+						Data: map[string][]byte{
466
+							kapi.DockerConfigJsonKey: []byte(`{"auths":{`),
467
+							"another":                []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
468
+						},
469
+					},
470
+					err: true,
471
+					expected: &kapi.Secret{
472
+						Type: kapi.SecretTypeDockerConfigJson,
473
+						Data: map[string][]byte{
474
+							kapi.DockerConfigJsonKey: []byte(`{"auths":{`),
475
+							"another":                []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
476
+						},
477
+					},
478
+				},
479
+				{
480
+					obj: &kapi.Secret{
481
+						Type: kapi.SecretTypeOpaque,
482
+						Data: map[string][]byte{
483
+							kapi.DockerConfigJsonKey: []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
484
+						},
485
+					},
486
+					expected: &kapi.Secret{
487
+						Type: kapi.SecretTypeOpaque,
488
+						Data: map[string][]byte{
489
+							kapi.DockerConfigJsonKey: []byte(`{"auths":{"docker.io":{},"other.server":{}}}`),
490
+						},
491
+					},
492
+				},
493
+				{
494
+					obj: &imageapi.Image{
495
+						DockerImageReference: "docker.io/foo/bar",
496
+					},
497
+					changed: true,
498
+					expected: &imageapi.Image{
499
+						DockerImageReference: "index.docker.io/foo/bar",
500
+					},
501
+				},
502
+				{
503
+					obj: &imageapi.Image{
504
+						DockerImageReference: "other.docker.io/foo/bar",
505
+					},
506
+					expected: &imageapi.Image{
507
+						DockerImageReference: "other.docker.io/foo/bar",
508
+					},
509
+				},
510
+				{
511
+					obj: &imageapi.ImageStream{
512
+						Spec: imageapi.ImageStreamSpec{
513
+							Tags: map[string]imageapi.TagReference{
514
+								"foo": {From: &kapi.ObjectReference{Kind: "DockerImage", Name: "docker.io/foo/bar"}},
515
+								"bar": {From: &kapi.ObjectReference{Kind: "ImageStream", Name: "docker.io/foo/bar"}},
516
+								"baz": {},
517
+							},
518
+							DockerImageRepository: "docker.io/foo/bar",
519
+						},
520
+						Status: imageapi.ImageStreamStatus{
521
+							DockerImageRepository: "docker.io/foo/bar",
522
+							Tags: map[string]imageapi.TagEventList{
523
+								"bar": {Items: []imageapi.TagEvent{
524
+									{DockerImageReference: "docker.io/foo/bar"},
525
+									{DockerImageReference: "docker.io/foo/bar"},
526
+								}},
527
+								"baz": {Items: []imageapi.TagEvent{
528
+									{DockerImageReference: "some.other/reference"},
529
+									{DockerImageReference: "docker.io/foo/bar"},
530
+								}},
531
+							},
532
+						},
533
+					},
534
+					changed: true,
535
+					expected: &imageapi.ImageStream{
536
+						Spec: imageapi.ImageStreamSpec{
537
+							Tags: map[string]imageapi.TagReference{
538
+								"foo": {From: &kapi.ObjectReference{Kind: "DockerImage", Name: "index.docker.io/foo/bar"}},
539
+								"bar": {From: &kapi.ObjectReference{Kind: "ImageStream", Name: "docker.io/foo/bar"}},
540
+								"baz": {},
541
+							},
542
+							DockerImageRepository: "index.docker.io/foo/bar",
543
+						},
544
+						Status: imageapi.ImageStreamStatus{
545
+							DockerImageRepository: "docker.io/foo/bar",
546
+							Tags: map[string]imageapi.TagEventList{
547
+								"bar": {Items: []imageapi.TagEvent{
548
+									{DockerImageReference: "index.docker.io/foo/bar"},
549
+									{DockerImageReference: "index.docker.io/foo/bar"},
550
+								}},
551
+								"baz": {Items: []imageapi.TagEvent{
552
+									{DockerImageReference: "some.other/reference"},
553
+									{DockerImageReference: "index.docker.io/foo/bar"},
554
+								}},
555
+							},
556
+						},
557
+					},
558
+				},
559
+			},
560
+		},
561
+		{
562
+			mappings: ImageReferenceMappings{{FromRegistry: "index.docker.io", ToRegistry: "another.registry"}},
563
+		},
564
+		{
565
+			mappings: ImageReferenceMappings{{FromRegistry: "index.docker.io", ToRegistry: "another.registry", ToName: "extra"}},
566
+		},
567
+	}
568
+
569
+	for _, test := range testCases {
570
+		for i, v := range test.variants {
571
+			o := MigrateImageReferenceOptions{
572
+				Mappings:        test.mappings,
573
+				UpdatePodSpecFn: clientcmd.NewFactory(nil).UpdatePodSpecForObject,
574
+			}
575
+			reporter, err := o.transform(v.obj)
576
+			if (err != nil) != v.err {
577
+				t.Errorf("%d: %v %t", i, err, v.err)
578
+				continue
579
+			}
580
+			if err != nil {
581
+				continue
582
+			}
583
+			if (reporter == nil) != v.nilReporter {
584
+				t.Errorf("%d: reporter %#v %t", i, reporter, v.nilReporter)
585
+				continue
586
+			}
587
+			if reporter == nil {
588
+				continue
589
+			}
590
+			if reporter.Changed() != v.changed {
591
+				t.Errorf("%d: changed %#v %t", i, reporter, v.changed)
592
+				continue
593
+			}
594
+			if !kapi.Semantic.DeepEqual(v.expected, v.obj) {
595
+				t.Errorf("%d: object: %s", i, diff.ObjectDiff(v.expected, v.obj))
596
+				continue
597
+			}
598
+		}
599
+	}
600
+}
0 601
new file mode 100644
... ...
@@ -0,0 +1,28 @@
0
+package migrate
1
+
2
+import (
3
+	"io"
4
+
5
+	"github.com/spf13/cobra"
6
+
7
+	cmdutil "github.com/openshift/origin/pkg/cmd/util"
8
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
9
+)
10
+
11
+const MigrateRecommendedName = "migrate"
12
+
13
+const migrateLong = `Migrate resources on the cluster
14
+
15
+These commands assist administrators in performing preventative maintenance on a cluster.`
16
+
17
+func NewCommandMigrate(name, fullName string, f *clientcmd.Factory, out io.Writer, cmds ...*cobra.Command) *cobra.Command {
18
+	// Parent command to which all subcommands are added.
19
+	cmd := &cobra.Command{
20
+		Use:   name,
21
+		Short: "Migrate data in the cluster",
22
+		Long:  migrateLong,
23
+		Run:   cmdutil.DefaultSubCommandRun(out),
24
+	}
25
+	cmd.AddCommand(cmds...)
26
+	return cmd
27
+}
0 28
new file mode 100644
... ...
@@ -0,0 +1,479 @@
0
+package migrate
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"strings"
6
+
7
+	"github.com/golang/glog"
8
+	"github.com/spf13/cobra"
9
+
10
+	"k8s.io/kubernetes/pkg/api/errors"
11
+	"k8s.io/kubernetes/pkg/api/meta"
12
+	"k8s.io/kubernetes/pkg/api/unversioned"
13
+	"k8s.io/kubernetes/pkg/kubectl"
14
+	"k8s.io/kubernetes/pkg/kubectl/resource"
15
+	"k8s.io/kubernetes/pkg/util/sets"
16
+
17
+	cmdutil "github.com/openshift/origin/pkg/cmd/util"
18
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
19
+)
20
+
21
+// MigrateVisitFunc is invoked for each returned object, and may return a
22
+// Reporter that can contain info to be used by save.
23
+type MigrateVisitFunc func(info *resource.Info) (Reporter, error)
24
+
25
+// MigrateActionFunc is expected to persist the altered info.Object. The
26
+// Reporter returned from Visit is passed to this function and may be used
27
+// to carry additional information about what to save on an object.
28
+type MigrateActionFunc func(info *resource.Info, reporter Reporter) error
29
+
30
+// MigrateFilterFunc can return false to skip an item, or an error.
31
+type MigrateFilterFunc func(info *resource.Info) (bool, error)
32
+
33
+// Reporter indicates whether a resource requires migration.
34
+type Reporter interface {
35
+	// Changed returns true if the resource requires migration.
36
+	Changed() bool
37
+}
38
+
39
+// ResourceOptions assists in performing migrations on any object that
40
+// can be retrieved via the API.
41
+type ResourceOptions struct {
42
+	In          io.Reader
43
+	Out, ErrOut io.Writer
44
+
45
+	AllNamespaces bool
46
+	Include       []string
47
+	Filenames     []string
48
+	Confirm       bool
49
+	Output        string
50
+	FromKey       string
51
+	ToKey         string
52
+
53
+	OverlappingResources []sets.String
54
+	DefaultExcludes      []unversioned.GroupResource
55
+
56
+	Builder   *resource.Builder
57
+	SaveFn    MigrateActionFunc
58
+	PrintFn   MigrateActionFunc
59
+	FilterFn  MigrateFilterFunc
60
+	DryRun    bool
61
+	Summarize bool
62
+}
63
+
64
+func (o *ResourceOptions) Bind(c *cobra.Command) {
65
+	c.Flags().StringVarP(&o.Output, "output", "o", o.Output, "Output the modified objects instead of saving them, valid values are 'yaml' or 'json'")
66
+	c.Flags().StringSliceVar(&o.Include, "include", o.Include, "Resource types to migrate. Passing --filename will override this flag.")
67
+	c.Flags().BoolVar(&o.AllNamespaces, "all-namespaces", true, "Migrate objects in all namespaces. Defaults to true.")
68
+	c.Flags().BoolVar(&o.Confirm, "confirm", false, "If true, all requested objects will be migrated. Defaults to false.")
69
+
70
+	c.Flags().StringVar(&o.FromKey, "from-key", o.FromKey, "If specified, only migrate items with a key (namespace/name or name) greater than or equal to this value")
71
+	c.Flags().StringVar(&o.ToKey, "to-key", o.ToKey, "If specified, only migrate items with a key (namespace/name or name) less than this value")
72
+
73
+	usage := "Filename, directory, or URL to docker-compose.yml file to use"
74
+	kubectl.AddJsonFilenameFlag(c, &o.Filenames, usage)
75
+	c.MarkFlagRequired("filename")
76
+}
77
+
78
+func (o *ResourceOptions) Complete(f *clientcmd.Factory, c *cobra.Command) error {
79
+	switch {
80
+	case len(o.Output) > 0:
81
+		printer, _, err := kubectl.GetPrinter(o.Output, "")
82
+		if err != nil {
83
+			return err
84
+		}
85
+		first := true
86
+		o.PrintFn = func(info *resource.Info, _ Reporter) error {
87
+			obj, err := info.Mapping.ConvertToVersion(info.Object, info.Mapping.GroupVersionKind.GroupVersion())
88
+			if err != nil {
89
+				return err
90
+			}
91
+			// TODO: PrintObj is not correct for YAML - it should inject document separators itself
92
+			if o.Output == "yaml" && !first {
93
+				fmt.Fprintln(o.Out, "---")
94
+			}
95
+			first = false
96
+			printer.PrintObj(obj, o.Out)
97
+			return nil
98
+		}
99
+		o.DryRun = true
100
+	case o.Confirm:
101
+		o.DryRun = false
102
+	default:
103
+		o.DryRun = true
104
+	}
105
+
106
+	namespace, explicitNamespace, err := f.Factory.DefaultNamespace()
107
+	if err != nil {
108
+		return err
109
+	}
110
+	allNamespaces := !explicitNamespace && o.AllNamespaces
111
+
112
+	if len(o.FromKey) > 0 || len(o.ToKey) > 0 {
113
+		o.FilterFn = func(info *resource.Info) (bool, error) {
114
+			var key string
115
+			if info.Mapping.Scope.Name() == meta.RESTScopeNameNamespace {
116
+				key = info.Namespace + "/" + info.Name
117
+			} else {
118
+				if !allNamespaces {
119
+					return false, nil
120
+				}
121
+				key = info.Name
122
+			}
123
+			if len(o.FromKey) > 0 && o.FromKey > key {
124
+				return false, nil
125
+			}
126
+			if len(o.ToKey) > 0 && o.ToKey <= key {
127
+				return false, nil
128
+			}
129
+			return true, nil
130
+		}
131
+	}
132
+
133
+	oclient, _, err := f.Clients()
134
+	if err != nil {
135
+		return err
136
+	}
137
+	mapper, _ := f.Object(false)
138
+
139
+	resourceNames := sets.NewString()
140
+	for i, s := range o.Include {
141
+		if resourceNames.Has(s) {
142
+			continue
143
+		}
144
+		if s != "*" {
145
+			resourceNames.Insert(s)
146
+			break
147
+		}
148
+
149
+		all, err := clientcmd.FindAllCanonicalResources(oclient.Discovery(), mapper)
150
+		if err != nil {
151
+			return fmt.Errorf("could not calculate the list of available resources: %v", err)
152
+		}
153
+		exclude := sets.NewString()
154
+		for _, gr := range o.DefaultExcludes {
155
+			exclude.Insert(gr.String())
156
+		}
157
+		candidate := sets.NewString()
158
+		for _, gr := range all {
159
+			// if the user specifies a resource that matches resource or resource+group, skip it
160
+			if resourceNames.Has(gr.Resource) || resourceNames.Has(gr.String()) || exclude.Has(gr.String()) {
161
+				continue
162
+			}
163
+			candidate.Insert(gr.String())
164
+		}
165
+		candidate.Delete(exclude.List()...)
166
+		include := candidate
167
+		if len(o.OverlappingResources) > 0 {
168
+			include = sets.NewString()
169
+			for _, k := range candidate.List() {
170
+				reduce := k
171
+				for _, others := range o.OverlappingResources {
172
+					if !others.Has(k) {
173
+						continue
174
+					}
175
+					reduce = others.List()[0]
176
+					break
177
+				}
178
+				include.Insert(reduce)
179
+			}
180
+		}
181
+		glog.V(4).Infof("Found the following resources from the server: %v", include.List())
182
+		last := o.Include[i+1:]
183
+		o.Include = append([]string{}, o.Include[:i]...)
184
+		o.Include = append(o.Include, include.List()...)
185
+		o.Include = append(o.Include, last...)
186
+		break
187
+	}
188
+
189
+	o.Builder = f.Factory.NewBuilder(false).
190
+		AllNamespaces(allNamespaces).
191
+		FilenameParam(false, false, o.Filenames...).
192
+		ContinueOnError().
193
+		DefaultNamespace().
194
+		RequireObject(true).
195
+		SelectAllParam(true).
196
+		Flatten()
197
+	if !allNamespaces {
198
+		o.Builder.NamespaceParam(namespace)
199
+	}
200
+	if len(o.Filenames) == 0 {
201
+		o.Builder.ResourceTypes(o.Include...)
202
+	}
203
+	return nil
204
+}
205
+
206
+func (o *ResourceOptions) Validate() error {
207
+	if len(o.Filenames) == 0 && len(o.Include) == 0 {
208
+		return fmt.Errorf("you must specify at least one resource or resource type to migrate with --include or --filenames")
209
+	}
210
+	return nil
211
+}
212
+
213
+func (o *ResourceOptions) Visitor() *ResourceVisitor {
214
+	return &ResourceVisitor{
215
+		Out:      o.Out,
216
+		Builder:  o.Builder,
217
+		SaveFn:   o.SaveFn,
218
+		PrintFn:  o.PrintFn,
219
+		FilterFn: o.FilterFn,
220
+		DryRun:   o.DryRun,
221
+	}
222
+}
223
+
224
+type ResourceVisitor struct {
225
+	Out io.Writer
226
+
227
+	Builder *resource.Builder
228
+
229
+	SaveFn   MigrateActionFunc
230
+	PrintFn  MigrateActionFunc
231
+	FilterFn MigrateFilterFunc
232
+
233
+	DryRun bool
234
+}
235
+
236
+func (o *ResourceVisitor) Visit(fn MigrateVisitFunc) error {
237
+	dryRun := o.DryRun
238
+	summarize := true
239
+	actionFn := o.SaveFn
240
+	switch {
241
+	case o.PrintFn != nil:
242
+		actionFn = o.PrintFn
243
+		dryRun = true
244
+		summarize = false
245
+	case dryRun:
246
+		actionFn = nil
247
+	}
248
+	out := o.Out
249
+
250
+	result := o.Builder.Do()
251
+	if result.Err() != nil {
252
+		return result.Err()
253
+	}
254
+
255
+	// Ignore any resource that does not support GET
256
+	result.IgnoreErrors(errors.IsMethodNotSupported, errors.IsNotFound)
257
+
258
+	t := migrateTracker{
259
+		out:       out,
260
+		migrateFn: fn,
261
+		actionFn:  actionFn,
262
+		dryRun:    dryRun,
263
+
264
+		resourcesWithErrors: sets.NewString(),
265
+	}
266
+
267
+	err := result.Visit(func(info *resource.Info, err error) error {
268
+		if err == nil && o.FilterFn != nil {
269
+			var ok bool
270
+			t.found++
271
+			if ok, err = o.FilterFn(info); err == nil && !ok {
272
+				t.ignored++
273
+				if glog.V(2) {
274
+					t.report("ignored:", info, nil)
275
+				}
276
+				return nil
277
+			}
278
+		}
279
+		if err != nil {
280
+			t.resourcesWithErrors.Insert(info.Mapping.Resource)
281
+			t.errors++
282
+			t.report("error:", info, err)
283
+			return nil
284
+		}
285
+		t.attempt(info, 10)
286
+		return nil
287
+	})
288
+
289
+	if summarize {
290
+		if dryRun {
291
+			fmt.Fprintf(out, "summary (DRY RUN): total=%d errors=%d ignored=%d unchanged=%d migrated=%d\n", t.found, t.errors, t.ignored, t.unchanged, t.found-t.errors-t.unchanged-t.ignored)
292
+		} else {
293
+			fmt.Fprintf(out, "summary: total=%d errors=%d ignored=%d unchanged=%d migrated=%d\n", t.found, t.errors, t.ignored, t.unchanged, t.found-t.errors-t.unchanged-t.ignored)
294
+		}
295
+	}
296
+
297
+	if t.resourcesWithErrors.Len() > 0 {
298
+		fmt.Fprintf(out, "info: to rerun only failing resources, add --include=%s\n", strings.Join(t.resourcesWithErrors.List(), ","))
299
+	}
300
+
301
+	switch {
302
+	case err != nil:
303
+		fmt.Fprintf(out, "error: exited without processing all resources: %v\n", err)
304
+		err = cmdutil.ErrExit
305
+	case t.errors > 0:
306
+		fmt.Fprintf(out, "error: %d resources failed to migrate\n", t.errors)
307
+		err = cmdutil.ErrExit
308
+	}
309
+	return err
310
+}
311
+
312
+// ErrUnchanged may be returned by MigrateActionFunc to indicate that the object
313
+// did not need migration (but that could only be determined when the action was taken).
314
+var ErrUnchanged = fmt.Errorf("migration was not necessary")
315
+
316
+// ErrRecalculate may be returned by MigrateActionFunc to indicate that the object
317
+// has changed and needs to have its information recalculated prior to being saved.
318
+// Use when a resource requries multiple API operations to persist (for instance,
319
+// both status and spec must be changed).
320
+var ErrRecalculate = fmt.Errorf("recalculate migration")
321
+
322
+// ErrRetriable is a wrapper for an error that a migrator may use to indicate the
323
+// specific error can be retried.
324
+type ErrRetriable struct {
325
+	error
326
+}
327
+
328
+func (ErrRetriable) Temporary() bool { return true }
329
+
330
+// ErrNotRetriable is a wrapper for an error that a migrator may use to indicate the
331
+// specific error cannot be retried.
332
+type ErrNotRetriable struct {
333
+	error
334
+}
335
+
336
+func (ErrNotRetriable) Temporary() bool { return false }
337
+
338
+type temporary interface {
339
+	// Temporary should return true if this is a temporary error
340
+	Temporary() bool
341
+}
342
+
343
+// attemptResult is an enumeration of the result of a migration
344
+type attemptResult int
345
+
346
+const (
347
+	attemptResultSuccess attemptResult = iota
348
+	attemptResultError
349
+	attemptResultUnchanged
350
+	attemptResultIgnore
351
+)
352
+
353
+// migrateTracker abstracts transforming and saving resources and can be used to keep track
354
+// of how many total resources have been updated.
355
+type migrateTracker struct {
356
+	out       io.Writer
357
+	migrateFn MigrateVisitFunc
358
+	actionFn  MigrateActionFunc
359
+	dryRun    bool
360
+
361
+	found, ignored, unchanged, errors int
362
+	retries                           int
363
+
364
+	resourcesWithErrors sets.String
365
+}
366
+
367
+// report prints a message to out that includes info about the current resource. If the optional error is
368
+// provided it will be written as well.
369
+func (t *migrateTracker) report(prefix string, info *resource.Info, err error) {
370
+	ns := info.Namespace
371
+	if len(ns) > 0 {
372
+		ns = "-n " + ns
373
+	}
374
+	if err != nil {
375
+		fmt.Fprintf(t.out, "%-10s %s/%s %s: %v\n", prefix, info.Mapping.Resource, info.Name, ns, err)
376
+	} else {
377
+		fmt.Fprintf(t.out, "%-10s %s/%s %s\n", prefix, info.Mapping.Resource, info.Name, ns)
378
+	}
379
+}
380
+
381
+// attempt will try to invoke the migrateFn and saveFn on info, retrying any recalculation requests up
382
+// to retries times.
383
+func (t *migrateTracker) attempt(info *resource.Info, retries int) {
384
+	t.found++
385
+	t.retries = retries
386
+	result, err := t.try(info)
387
+	switch {
388
+	case err != nil:
389
+		t.resourcesWithErrors.Insert(info.Mapping.Resource)
390
+		t.errors++
391
+		t.report("error:", info, err)
392
+	case result == attemptResultIgnore:
393
+		t.ignored++
394
+		if glog.V(2) {
395
+			t.report("ignored:", info, nil)
396
+		}
397
+	case result == attemptResultUnchanged:
398
+		t.unchanged++
399
+		if glog.V(2) {
400
+			t.report("unchanged:", info, nil)
401
+		}
402
+	case result == attemptResultSuccess:
403
+		if glog.V(1) {
404
+			if t.dryRun {
405
+				t.report("migrated (DRY RUN):", info, nil)
406
+			} else {
407
+				t.report("migrated:", info, nil)
408
+			}
409
+		}
410
+	}
411
+}
412
+
413
+// try will mutate the info and attempt to save, recalculating if there are any retries left.
414
+// The result of the attempt or an error will be returned.
415
+func (t *migrateTracker) try(info *resource.Info) (attemptResult, error) {
416
+	reporter, err := t.migrateFn(info)
417
+	if err != nil {
418
+		return attemptResultError, err
419
+	}
420
+	if reporter == nil {
421
+		return attemptResultIgnore, nil
422
+	}
423
+	if !reporter.Changed() {
424
+		return attemptResultUnchanged, nil
425
+	}
426
+	if t.actionFn != nil {
427
+		if err := t.actionFn(info, reporter); err != nil {
428
+			if err == ErrUnchanged {
429
+				return attemptResultUnchanged, nil
430
+			}
431
+			if canRetry(err) {
432
+				if t.retries > 0 {
433
+					if glog.V(1) && err != ErrRecalculate {
434
+						t.report("retry:", info, err)
435
+					}
436
+					result, err := t.try(info)
437
+					switch result {
438
+					case attemptResultUnchanged, attemptResultIgnore:
439
+						result = attemptResultSuccess
440
+					}
441
+					return result, err
442
+				}
443
+			}
444
+			return attemptResultError, err
445
+		}
446
+	}
447
+	return attemptResultSuccess, nil
448
+}
449
+
450
+// canRetry returns true if the provided error indicates a retry is possible.
451
+func canRetry(err error) bool {
452
+	if temp, ok := err.(temporary); ok && temp.Temporary() {
453
+		return true
454
+	}
455
+	return err == ErrRecalculate
456
+}
457
+
458
+// DefaultRetriable adds retry information to the provided error, and will refresh the
459
+// info if the client info is stale. If the refresh fails the error is made fatal.
460
+// All other errors are left in their natural state - they will not be retried unless
461
+// they define a Temporary() method that returns true.
462
+func DefaultRetriable(info *resource.Info, err error) error {
463
+	if err == nil {
464
+		return nil
465
+	}
466
+	switch {
467
+	case errors.IsMethodNotSupported(err):
468
+		return ErrNotRetriable{err}
469
+	case errors.IsConflict(err):
470
+		if refreshErr := info.Get(); refreshErr != nil {
471
+			return ErrNotRetriable{err}
472
+		}
473
+		return ErrRetriable{err}
474
+	case errors.IsServerTimeout(err):
475
+		return ErrRetriable{err}
476
+	}
477
+	return err
478
+}
0 479
new file mode 100644
... ...
@@ -0,0 +1,181 @@
0
+package images
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+
6
+	"github.com/golang/glog"
7
+	"github.com/spf13/cobra"
8
+
9
+	"k8s.io/kubernetes/pkg/api/meta"
10
+	"k8s.io/kubernetes/pkg/api/unversioned"
11
+	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
12
+	"k8s.io/kubernetes/pkg/kubectl/resource"
13
+	"k8s.io/kubernetes/pkg/runtime"
14
+	"k8s.io/kubernetes/pkg/util/sets"
15
+
16
+	"github.com/openshift/origin/pkg/cmd/admin/migrate"
17
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
18
+)
19
+
20
+const (
21
+	internalMigrateStorageLong = `
22
+Migrate internal object storage via update
23
+
24
+This command invokes an update operation on every API object reachable by the caller. This forces
25
+the server to write to the underlying storage if the object representation has changed. Use this
26
+command to ensure that the most recent storage changes have been applied to all objects (storage
27
+version, storage encoding, any newer object defaults).
28
+
29
+To operate on a subset of resources, use the --include flag. If you encounter errors during a run
30
+the command will output a list of resources that received errors, which you can then re-run the
31
+command on. You may also specify --from-key and --to-key to restrict the set of resource names
32
+to operate on (key is NAMESPACE/NAME for resources in namespaces or NAME for cluster scoped
33
+resources). --from-key is inclusive if specified, while --to-key is exclusive.
34
+
35
+By default, events are not migrated since they expire within a very short period of time. If you
36
+have significantly increased the expiration time of events, run a migration with --include=events
37
+
38
+WARNING: This is a slow command and will put significant load on an API server. It may also
39
+  result in significant intra-cluster traffic.`
40
+
41
+	internalMigrateStorageExample = `  # Perform a dry-run of updating all objects
42
+  %[1]s
43
+
44
+  # To actually perform the update, the confirm flag must be appended
45
+  %[1]s --confirm
46
+
47
+  # Only migrate pods
48
+  %[1]s --include=pods --confirm
49
+
50
+  # Only pods that are in namespaces starting with "bar"
51
+  %[1]s --include=pods --confirm --from-key=bar/ --to-key=bar/\xFF
52
+`
53
+)
54
+
55
+type MigrateAPIStorageOptions struct {
56
+	migrate.ResourceOptions
57
+}
58
+
59
+// NewCmdMigrateAPIStorage implements a MigrateStorage command
60
+func NewCmdMigrateAPIStorage(name, fullName string, f *clientcmd.Factory, in io.Reader, out, errout io.Writer) *cobra.Command {
61
+	options := &MigrateAPIStorageOptions{
62
+		ResourceOptions: migrate.ResourceOptions{
63
+			In:     in,
64
+			Out:    out,
65
+			ErrOut: errout,
66
+
67
+			Include: []string{"*"},
68
+			DefaultExcludes: []unversioned.GroupResource{
69
+				{Resource: "appliedclusterresourcequotas"},
70
+				{Resource: "bindings"},
71
+				{Resource: "deploymentconfigrollbacks"},
72
+				{Resource: "events"},
73
+				{Resource: "imagestreamimages"}, {Resource: "imagestreamtags"}, {Resource: "imagestreammappings"}, {Resource: "imagestreamimports"},
74
+				{Resource: "projectrequests"}, {Resource: "projects"},
75
+				{Resource: "componentstatuses"},
76
+				{Resource: "clusterrolebindings"}, {Resource: "rolebindings"},
77
+				{Resource: "clusterroles"}, {Resource: "roles"},
78
+				{Resource: "resourceaccessreviews"}, {Resource: "localresourceaccessreviews"}, {Resource: "subjectaccessreviews"},
79
+				{Resource: "selfsubjectrulesreviews"}, {Resource: "localsubjectaccessreviews"},
80
+				{Resource: "replicationcontrollerdummies.extensions"},
81
+				{Resource: "podtemplates"},
82
+				{Resource: "useridentitymappings"},
83
+			},
84
+			// Resources known to share the same storage
85
+			OverlappingResources: []sets.String{
86
+				sets.NewString("horizontalpodautoscalers.autoscaling", "horizontalpodautoscalers.extensions"),
87
+				sets.NewString("jobs.batch", "jobs.extensions"),
88
+			},
89
+		},
90
+	}
91
+	cmd := &cobra.Command{
92
+		Use:     fmt.Sprintf("%s REGISTRY/NAME=REGISTRY/NAME [...]", name),
93
+		Short:   "Update the stored version of API objects",
94
+		Long:    internalMigrateStorageLong,
95
+		Example: fmt.Sprintf(internalMigrateStorageExample, fullName),
96
+		Run: func(cmd *cobra.Command, args []string) {
97
+			kcmdutil.CheckErr(options.Complete(f, cmd, args))
98
+			kcmdutil.CheckErr(options.Validate())
99
+			kcmdutil.CheckErr(options.Run())
100
+		},
101
+	}
102
+	options.ResourceOptions.Bind(cmd)
103
+
104
+	return cmd
105
+}
106
+
107
+func (o *MigrateAPIStorageOptions) Complete(f *clientcmd.Factory, c *cobra.Command, args []string) error {
108
+	o.ResourceOptions.SaveFn = o.save
109
+	if err := o.ResourceOptions.Complete(f, c); err != nil {
110
+		return err
111
+	}
112
+	return nil
113
+}
114
+
115
+func (o MigrateAPIStorageOptions) Validate() error {
116
+	return o.ResourceOptions.Validate()
117
+}
118
+
119
+func (o MigrateAPIStorageOptions) Run() error {
120
+	return o.ResourceOptions.Visitor().Visit(func(info *resource.Info) (migrate.Reporter, error) {
121
+		return o.transform(info.Object)
122
+	})
123
+}
124
+
125
+// save invokes the API to alter an object. The reporter passed to this method is the same returned by
126
+// the migration visitor method (for this type, transformAPIStorage). It should return an error
127
+// if the input type cannot be saved. It returns migrate.ErrRecalculate if migration should be re-run
128
+// on the provided object.
129
+func (o *MigrateAPIStorageOptions) save(info *resource.Info, reporter migrate.Reporter) error {
130
+	switch info.Object.(type) {
131
+	// TODO: add any custom mutations necessary
132
+	default:
133
+		// load the body and save it back, without transformation to avoid losing fields
134
+		get := info.Client.Get().
135
+			Resource(info.Mapping.Resource).
136
+			NamespaceIfScoped(info.Namespace, info.Mapping.Scope.Name() == meta.RESTScopeNameNamespace).
137
+			Name(info.Name).Do()
138
+		data, err := get.Raw()
139
+		if err != nil {
140
+			return migrate.DefaultRetriable(info, err)
141
+		}
142
+		update := info.Client.Put().
143
+			Resource(info.Mapping.Resource).
144
+			NamespaceIfScoped(info.Namespace, info.Mapping.Scope.Name() == meta.RESTScopeNameNamespace).
145
+			Name(info.Name).Body(data).
146
+			Do()
147
+		if err := update.Error(); err != nil {
148
+			return migrate.DefaultRetriable(info, err)
149
+		}
150
+
151
+		if oldObject, err := get.Get(); err == nil {
152
+			info.Refresh(oldObject, true)
153
+			oldVersion := info.ResourceVersion
154
+			if object, err := update.Get(); err == nil {
155
+				info.Refresh(object, true)
156
+				if info.ResourceVersion == oldVersion {
157
+					return migrate.ErrUnchanged
158
+				}
159
+			} else {
160
+				glog.V(4).Infof("unable to calculate resource version: %v", err)
161
+			}
162
+		} else {
163
+			glog.V(4).Infof("unable to calculate resource version: %v", err)
164
+		}
165
+	}
166
+	return nil
167
+}
168
+
169
+// transform checks image references on the provided object and returns either a reporter (indicating
170
+// that the object was recognized and whether it was updated) or an error.
171
+func (o *MigrateAPIStorageOptions) transform(obj runtime.Object) (migrate.Reporter, error) {
172
+	return reporter(true), nil
173
+}
174
+
175
+// reporter implements the Reporter interface for a boolean.
176
+type reporter bool
177
+
178
+func (r reporter) Changed() bool {
179
+	return bool(r)
180
+}
... ...
@@ -150,7 +150,7 @@ func NewCommandCLI(name, fullName string, in io.Reader, out, errout io.Writer) *
150 150
 		{
151 151
 			Message: "Advanced Commands:",
152 152
 			Commands: []*cobra.Command{
153
-				admin.NewCommandAdmin("adm", fullName+" "+"adm", out, errout),
153
+				admin.NewCommandAdmin("adm", fullName+" "+"adm", in, out, errout),
154 154
 				cmd.NewCmdCreate(fullName, f, out),
155 155
 				cmd.NewCmdReplace(fullName, f, out),
156 156
 				cmd.NewCmdApply(fullName, f, out),
... ...
@@ -69,7 +69,7 @@ func CommandFor(basename string) *cobra.Command {
69 69
 	case "oc", "osc":
70 70
 		cmd = cli.NewCommandCLI(basename, basename, in, out, errout)
71 71
 	case "oadm", "osadm":
72
-		cmd = admin.NewCommandAdmin(basename, basename, out, errout)
72
+		cmd = admin.NewCommandAdmin(basename, basename, in, out, errout)
73 73
 	case "kubectl":
74 74
 		cmd = cli.NewCmdKubectl(basename, out)
75 75
 	case "kube-apiserver":
... ...
@@ -111,7 +111,7 @@ func NewCommandOpenShift(name string) *cobra.Command {
111 111
 
112 112
 	startAllInOne, _ := start.NewCommandStartAllInOne(name, out)
113 113
 	root.AddCommand(startAllInOne)
114
-	root.AddCommand(admin.NewCommandAdmin("admin", name+" admin", out, errout))
114
+	root.AddCommand(admin.NewCommandAdmin("admin", name+" admin", in, out, errout))
115 115
 	root.AddCommand(cli.NewCommandCLI("cli", name+" cli", in, out, errout))
116 116
 	root.AddCommand(cli.NewCmdKubectl("kube", out))
117 117
 	root.AddCommand(newExperimentalCommand("ex", name+" ex"))
... ...
@@ -414,6 +414,20 @@ func NewFactory(clientConfig kclientcmd.ClientConfig) *Factory {
414 414
 			return kAttachablePodForObjectFunc(object)
415 415
 		}
416 416
 	}
417
+	kUpdatePodSpecForObject := w.Factory.UpdatePodSpecForObject
418
+	w.UpdatePodSpecForObject = func(obj runtime.Object, fn func(*api.PodSpec) error) (bool, error) {
419
+		switch t := obj.(type) {
420
+		case *deployapi.DeploymentConfig:
421
+			template := t.Spec.Template
422
+			if template == nil {
423
+				t.Spec.Template = template
424
+				template = &api.PodTemplateSpec{}
425
+			}
426
+			return true, fn(&template.Spec)
427
+		default:
428
+			return kUpdatePodSpecForObject(obj, fn)
429
+		}
430
+	}
417 431
 	kProtocolsForObject := w.Factory.ProtocolsForObject
418 432
 	w.ProtocolsForObject = func(object runtime.Object) (map[string]string, error) {
419 433
 		switch t := object.(type) {
... ...
@@ -588,31 +602,6 @@ func (f *Factory) ExtractFileContents(obj runtime.Object) (map[string][]byte, bo
588 588
 	}
589 589
 }
590 590
 
591
-// UpdatePodSpecForObject update the pod specification for the provided object
592
-// TODO: move to upstream
593
-func (f *Factory) UpdatePodSpecForObject(obj runtime.Object, fn func(*api.PodSpec) error) (bool, error) {
594
-	// TODO: replace with a swagger schema based approach (identify pod template via schema introspection)
595
-	switch t := obj.(type) {
596
-	case *api.Pod:
597
-		return true, fn(&t.Spec)
598
-	case *api.PodTemplate:
599
-		return true, fn(&t.Template.Spec)
600
-	case *api.ReplicationController:
601
-		if t.Spec.Template == nil {
602
-			t.Spec.Template = &api.PodTemplateSpec{}
603
-		}
604
-		return true, fn(&t.Spec.Template.Spec)
605
-	case *deployapi.DeploymentConfig:
606
-		template := t.Spec.Template
607
-		if template == nil {
608
-			template = &api.PodTemplateSpec{}
609
-		}
610
-		return true, fn(&template.Spec)
611
-	default:
612
-		return false, fmt.Errorf("the object is not a pod or does not have a pod template")
613
-	}
614
-}
615
-
616 591
 // ApproximatePodTemplateForObject returns a pod template object for the provided source.
617 592
 // It may return both an error and a object. It attempt to return the best possible template
618 593
 // available at the current time.
... ...
@@ -184,6 +184,7 @@ func ParseDockerImageReference(spec string) (DockerImageReference, error) {
184 184
 		ref.ID = id
185 185
 		break
186 186
 	default:
187
+		// TODO: this is no longer true with V2
187 188
 		return ref, fmt.Errorf("the docker pull spec %q must be two or three segments separated by slashes", spec)
188 189
 	}
189 190
 
190 191
new file mode 100755
... ...
@@ -0,0 +1,70 @@
0
+#!/bin/bash
1
+
2
+set -o errexit
3
+set -o nounset
4
+set -o pipefail
5
+
6
+OS_ROOT=$(dirname "${BASH_SOURCE}")/../..
7
+source "${OS_ROOT}/hack/lib/init.sh"
8
+os::log::stacktrace::install
9
+trap os::test::junit::reconcile_output EXIT
10
+
11
+# Cleanup cluster resources created by this test
12
+(
13
+  set +e
14
+  oc delete all --all
15
+  exit 0
16
+) &>/dev/null
17
+
18
+os::test::junit::declare_suite_start "cmd/migrate"
19
+# This test validates storage migration
20
+
21
+os::cmd::expect_success 'oc login -u system:admin'
22
+
23
+os::test::junit::declare_suite_start "cmd/migrate/storage"
24
+os::cmd::expect_success_and_text     'oadm migrate storage' 'summary \(DRY RUN\)'
25
+os::cmd::expect_success_and_text     'oadm migrate storage --loglevel=2' 'migrated \(DRY RUN\): serviceaccounts/deployer'
26
+os::cmd::expect_success_and_not_text 'oadm migrate storage --loglevel=2 --include=pods' 'migrated \(DRY RUN\): serviceaccounts/deployer'
27
+os::cmd::expect_success_and_text     'oadm migrate storage --loglevel=2 --include=sa --from-key=default/ --to-key=default/\xFF' 'migrated \(DRY RUN\): serviceaccounts/deployer'
28
+os::cmd::expect_success_and_not_text 'oadm migrate storage --loglevel=2 --include=sa --from-key=default/ --to-key=default/deployer' 'migrated \(DRY RUN\): serviceaccounts/deployer'
29
+os::cmd::expect_success_and_text     'oadm migrate storage --loglevel=2 --confirm' 'unchanged:'
30
+os::test::junit::declare_suite_end
31
+
32
+os::test::junit::declare_suite_start "cmd/migrate/imagereferences"
33
+# create alternating items in history
34
+os::cmd::expect_success 'oc import-image --from=mysql:latest test:1 --confirm'
35
+os::cmd::expect_success 'oc tag --source=docker php:latest test:1'
36
+os::cmd::expect_success 'oc tag --source=docker mysql:latest test:1'
37
+os::cmd::expect_success 'oc import-image --from=php:latest test:2 --confirm'
38
+os::cmd::expect_success 'oc tag --source=docker mysql:latest test:2'
39
+os::cmd::expect_success 'oc tag --source=docker php:latest test:2'
40
+os::cmd::expect_success 'oc tag --source=docker myregistry.com/php:latest test:3'
41
+# verify error cases
42
+os::cmd::expect_failure_and_text     'oadm migrate image-references' 'at least one mapping argument must be specified: REGISTRY/NAME=REGISTRY/NAME'
43
+os::cmd::expect_failure_and_text     'oadm migrate image-references my.docker.io=docker.io/* --loglevel=1' 'all arguments'
44
+os::cmd::expect_failure_and_text     'oadm migrate image-references my.docker.io/=docker.io/* --loglevel=1' 'not a valid source'
45
+os::cmd::expect_failure_and_text     'oadm migrate image-references /*=docker.io/* --loglevel=1' 'not a valid source'
46
+os::cmd::expect_failure_and_text     'oadm migrate image-references my.docker.io/*=docker.io --loglevel=1' 'all arguments'
47
+os::cmd::expect_failure_and_text     'oadm migrate image-references my.docker.io/*=docker.io/ --loglevel=1' 'not a valid target'
48
+os::cmd::expect_failure_and_text     'oadm migrate image-references my.docker.io/*=/x --loglevel=1' 'not a valid target'
49
+os::cmd::expect_failure_and_text     'oadm migrate image-references my.docker.io/*=*/* --loglevel=1' 'at least one change'
50
+os::cmd::expect_failure_and_text     'oadm migrate image-references a/b=a/b --loglevel=1' 'at least one field'
51
+os::cmd::expect_failure_and_text     'oadm migrate image-references */*=*/* --loglevel=1' 'at least one change'
52
+# verify dry run
53
+os::cmd::expect_success_and_text     'oadm migrate image-references my.docker.io/*=docker.io/* --loglevel=1' 'migrated=0'
54
+os::cmd::expect_success_and_text     'oadm migrate image-references --include=imagestreams docker.io/*=my.docker.io/* --loglevel=1' 'migrated \(DRY RUN\): imagestreams/test -n '
55
+os::cmd::expect_success_and_text     'oadm migrate image-references --include=imagestreams docker.io/mysql=my.docker.io/* --all-namespaces=false --loglevel=1' 'migrated=1'
56
+os::cmd::expect_success_and_text     'oadm migrate image-references --include=imagestreams docker.io/mysql=my.docker.io/* --all-namespaces=false --loglevel=1 -o yaml' 'dockerImageReference: my.docker.io/mysql@sha256:'
57
+os::cmd::expect_success_and_text     'oadm migrate image-references --include=imagestreams docker.io/other=my.docker.io/* --all-namespaces=false --loglevel=1' 'migrated=0'
58
+# only mysql references are changed
59
+os::cmd::expect_success_and_text     'oadm migrate image-references --include=imagestreams docker.io/mysql=my.docker.io/mysql2 --all-namespaces=false --loglevel=1 --confirm' 'migrated=1'
60
+os::cmd::expect_success_and_text     'oc get istag test:1 --template "{{ .image.dockerImageReference }}"' '^my.docker.io/mysql2@sha256:'
61
+os::cmd::expect_success_and_text     'oc get istag test:2 --template "{{ .image.dockerImageReference }}"' '^php@sha256:'
62
+# all items in history are changed
63
+os::cmd::expect_success_and_text     'oadm migrate image-references --include=imagestreams docker.io/*=my.docker.io/* --all-namespaces=false --loglevel=1 --confirm' 'migrated=1'
64
+os::cmd::expect_success_and_not_text 'oc get is test --template "{{ range .status.tags }}{{ range .items }}{{ .dockerImageReference }}{{ \"\n\" }}{{ end }}{{ end }}"' '^php'
65
+os::cmd::expect_success_and_not_text 'oc get is test --template "{{ range .status.tags }}{{ range .items }}{{ .dockerImageReference }}{{ \"\n\" }}{{ end }}{{ end }}"' '^mysql'
66
+os::test::junit::declare_suite_end
67
+
68
+os::test::junit::declare_suite_end
69
+
... ...
@@ -51,6 +51,6 @@ func main() {
51 51
 	gendocs.GenDocs(cmd, outFile)
52 52
 
53 53
 	outFile = outDir + "oadm_by_example_content.adoc"
54
-	cmd = admin.NewCommandAdmin("oadm", "oadm", ioutil.Discard, ioutil.Discard)
54
+	cmd = admin.NewCommandAdmin("oadm", "oadm", &bytes.Buffer{}, ioutil.Discard, ioutil.Discard)
55 55
 	gendocs.GenDocs(cmd, outFile)
56 56
 }
... ...
@@ -45,7 +45,7 @@ func main() {
45 45
 	} else if strings.HasSuffix(os.Args[2], "openshift") {
46 46
 		genCmdMan("openshift", openshift.NewCommandOpenShift("openshift"))
47 47
 	} else if strings.HasSuffix(os.Args[2], "oadm") {
48
-		genCmdMan("oadm", admin.NewCommandAdmin("oadm", "oadm", os.Stdout, ioutil.Discard))
48
+		genCmdMan("oadm", admin.NewCommandAdmin("oadm", "oadm", &bytes.Buffer{}, os.Stdout, ioutil.Discard))
49 49
 	} else {
50 50
 		fmt.Fprintf(os.Stderr, "Root command not specified (os | oadm | openshift).")
51 51
 		os.Exit(1)