Implement a migration framework and an image reference migrator:
oadm migrate image-references registry1.com/*=registry2.com/* --confirm
Reuse resource builder to manage output
... | ... |
@@ -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) |