-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
381559d
commit d8358f6
Showing
8 changed files
with
550 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
package images | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"os" | ||
"strings" | ||
|
||
"github.com/spf13/cobra" | ||
|
||
kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" | ||
"k8s.io/kubernetes/pkg/kubectl/resource" | ||
"k8s.io/kubernetes/pkg/runtime" | ||
|
||
"github.com/openshift/origin/pkg/client" | ||
"github.com/openshift/origin/pkg/cmd/admin/migrate" | ||
cmdutil "github.com/openshift/origin/pkg/cmd/util" | ||
"github.com/openshift/origin/pkg/cmd/util/clientcmd" | ||
imageapi "github.com/openshift/origin/pkg/image/api" | ||
) | ||
|
||
const ( | ||
internalMigrateImagesLong = ` | ||
Single line title | ||
Description body` | ||
|
||
internalMigrateImagesExample = `%s` | ||
) | ||
|
||
type ImageReferenceMapping struct { | ||
FromRegistry string | ||
FromName string | ||
ToRegistry string | ||
ToName string | ||
} | ||
|
||
func Parse(s string) (ImageReferenceMapping, error) { | ||
parts := strings.SplitN(s, "=", 2) | ||
from := strings.SplitN(parts[0], "/", 2) | ||
to := strings.SplitN(parts[1], "/", 2) | ||
if len(from) < 2 || len(to) < 2 { | ||
return ImageReferenceMapping{}, fmt.Errorf("all arguments must be of the form REGISTRY/NAME=REGISTRY/NAME, where registry or name may be '*' or a value") | ||
} | ||
if len(from[0]) == 0 { | ||
return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid source: registry must be specified (may be '*')", parts[0]) | ||
} | ||
if len(from[1]) == 0 { | ||
return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid source: name must be specified (may be '*')", parts[0]) | ||
} | ||
if len(to[0]) == 0 { | ||
return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid target: registry must be specified (may be '*')", parts[1]) | ||
} | ||
if len(to[1]) == 0 { | ||
return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid target: name must be specified (may be '*')", parts[1]) | ||
} | ||
if from[0] == "*" { | ||
from[0] = "" | ||
} | ||
if from[1] == "*" { | ||
from[1] = "" | ||
} | ||
if to[0] == "*" { | ||
to[0] = "" | ||
} | ||
if to[1] == "*" { | ||
to[1] = "" | ||
} | ||
if to[0] == "" && to[1] == "" { | ||
return ImageReferenceMapping{}, fmt.Errorf("%q is not a valid target: at least one change must be specified", parts[1]) | ||
} | ||
if from[0] == from[1] && to[0] == to[1] { | ||
return ImageReferenceMapping{}, fmt.Errorf("%q is not valid: must target at least one field to change", s) | ||
} | ||
return ImageReferenceMapping{ | ||
FromRegistry: from[0], | ||
FromName: from[1], | ||
ToRegistry: to[0], | ||
ToName: to[1], | ||
}, nil | ||
} | ||
|
||
type ImageReferenceMappings []ImageReferenceMapping | ||
|
||
func (m ImageReferenceMappings) Map(in string) string { | ||
ref, err := imageapi.ParseDockerImageReference(in) | ||
if err != nil { | ||
return in | ||
} | ||
for _, mapping := range m { | ||
registry := ref.Registry | ||
if len(registry) == 0 { | ||
registry = "docker.io" | ||
} | ||
if len(mapping.FromRegistry) > 0 && mapping.FromRegistry != registry { | ||
continue | ||
} | ||
name := ref.RepositoryName() | ||
if len(mapping.FromName) > 0 && mapping.FromName != name { | ||
continue | ||
} | ||
if len(mapping.ToRegistry) > 0 { | ||
ref.Registry = mapping.ToRegistry | ||
} | ||
if len(mapping.ToName) > 0 { | ||
ref.Namespace = "" | ||
ref.Name = mapping.ToName | ||
} | ||
return ref.Exact() | ||
} | ||
return in | ||
} | ||
|
||
type MigrateImageReferenceOptions struct { | ||
migrate.ResourceOptions | ||
|
||
Client client.Interface | ||
Mappings ImageReferenceMappings | ||
} | ||
|
||
// NewCmdMigrateImageReferences implements a MigrateImages command | ||
// This is an example type for templating. | ||
func NewCmdMigrateImageReferences(name, fullName string, f *clientcmd.Factory, in io.Reader, out, errout io.Writer) *cobra.Command { | ||
options := &MigrateImageReferenceOptions{ | ||
ResourceOptions: migrate.ResourceOptions{ | ||
In: in, | ||
Out: out, | ||
ErrOut: errout, | ||
}, | ||
} | ||
cmd := &cobra.Command{ | ||
Use: fmt.Sprintf("%s REGISTRY/NAME=REGISTRY/NAME [...]", name), | ||
Short: "A short description", | ||
Long: internalMigrateImagesLong, | ||
Example: fmt.Sprintf(internalMigrateImagesExample, fullName), | ||
Run: func(cmd *cobra.Command, args []string) { | ||
kcmdutil.CheckErr(options.Complete(f, cmd, args)) | ||
kcmdutil.CheckErr(options.Validate()) | ||
if err := options.Run(); err != nil { | ||
// TODO: move met to kcmdutil | ||
if err == cmdutil.ErrExit { | ||
os.Exit(1) | ||
} | ||
kcmdutil.CheckErr(err) | ||
} | ||
}, | ||
} | ||
options.ResourceOptions.Bind(cmd) | ||
|
||
return cmd | ||
} | ||
|
||
func (o *MigrateImageReferenceOptions) Complete(f *clientcmd.Factory, c *cobra.Command, args []string) error { | ||
var remainingArgs []string | ||
for _, s := range args { | ||
if !strings.Contains(s, "=") { | ||
remainingArgs = append(remainingArgs, s) | ||
continue | ||
} | ||
mapping, err := Parse(s) | ||
if err != nil { | ||
return err | ||
} | ||
o.Mappings = append(o.Mappings, mapping) | ||
} | ||
|
||
o.ResourceOptions.SaveFn = o.save | ||
if err := o.ResourceOptions.Complete(f, c, remainingArgs); err != nil { | ||
return err | ||
} | ||
o.Builder.ResourceTypes("imagestream", "image") | ||
|
||
osclient, _, err := f.Clients() | ||
if err != nil { | ||
return err | ||
} | ||
o.Client = osclient | ||
|
||
return nil | ||
} | ||
|
||
func (o *MigrateImageReferenceOptions) Validate() error { | ||
if len(o.Mappings) == 0 { | ||
return fmt.Errorf("at least one mapping argument must be specified: REGISTRY/NAME=REGISTRY/NAME") | ||
} | ||
return o.ResourceOptions.Validate() | ||
} | ||
|
||
func (o *MigrateImageReferenceOptions) Run() error { | ||
return o.ResourceOptions.Visit(func(info *resource.Info) (migrate.Reporter, error) { | ||
return transformImageReferences(info.Object, o.Mappings.Map) | ||
}) | ||
} | ||
|
||
// save invokes the API to alter an object | ||
func (o *MigrateImageReferenceOptions) save(info *resource.Info, reporter migrate.Reporter) error { | ||
switch t := info.Object.(type) { | ||
case *imageapi.Image: | ||
_, err := o.Client.Images().Update(t) | ||
return err | ||
case *imageapi.ImageStream: | ||
if reporter.(imageChangeInfo).status { | ||
updated, err := o.Client.ImageStreams(t.Namespace).UpdateStatus(t) | ||
if err != nil { | ||
return err | ||
} | ||
info.Refresh(updated, true) | ||
return migrate.ErrRecalculate | ||
} | ||
if reporter.(imageChangeInfo).spec { | ||
updated, err := o.Client.ImageStreams(t.Namespace).Update(t) | ||
if err != nil { | ||
return err | ||
} | ||
info.Refresh(updated, true) | ||
} | ||
return nil | ||
default: | ||
return fmt.Errorf("resource %q does not have a save method implemented (%T)", info.Mapping.Resource, t) | ||
} | ||
return nil | ||
} | ||
|
||
type reporter bool | ||
|
||
func (r reporter) Changed() bool { | ||
return bool(r) | ||
} | ||
|
||
// imageChangeInfo indicates whether the spec or status of an image stream was changed | ||
type imageChangeInfo struct { | ||
spec, status bool | ||
} | ||
|
||
func (i imageChangeInfo) Changed() bool { | ||
return i.spec || i.status | ||
} | ||
|
||
// transformImageReferences checks image references on the provided object and returns either a reporter (indicating | ||
// that the object was recognized and whether it was updated) or an error. | ||
func transformImageReferences(obj runtime.Object, fn func(s string) string) (migrate.Reporter, error) { | ||
switch t := obj.(type) { | ||
case *imageapi.Image: | ||
var changed bool | ||
if updated := fn(t.DockerImageReference); updated != t.DockerImageReference { | ||
changed = true | ||
t.DockerImageReference = updated | ||
} | ||
return reporter(changed), nil | ||
case *imageapi.ImageStream: | ||
var info imageChangeInfo | ||
for _, ref := range t.Spec.Tags { | ||
if ref.From == nil || ref.From.Kind != "DockerImage" { | ||
continue | ||
} | ||
if updated := fn(ref.From.Name); updated != ref.From.Name { | ||
info.spec = true | ||
ref.From.Name = updated | ||
} | ||
} | ||
for _, events := range t.Status.Tags { | ||
for i, event := range events.Items { | ||
if updated := fn(event.DockerImageReference); updated != event.DockerImageReference { | ||
info.status = true | ||
events.Items[i].DockerImageReference = updated | ||
} | ||
} | ||
} | ||
return info, nil | ||
} | ||
return nil, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package images | ||
|
||
import ( | ||
"testing" | ||
) | ||
|
||
func TestImageReferenceMappingsMap(t *testing.T) { | ||
testCases := []struct { | ||
mappings ImageReferenceMappings | ||
results map[string]string | ||
}{ | ||
{ | ||
mappings: ImageReferenceMappings{{FromRegistry: "docker.io", ToRegistry: "index.docker.io"}}, | ||
results: map[string]string{ | ||
"mysql": "index.docker.io/mysql", | ||
"mysql:latest": "index.docker.io/mysql:latest", | ||
"default/mysql:latest": "index.docker.io/default/mysql:latest", | ||
|
||
"mysql@sha256:b2f400f4a5e003b0543decf61a0a010939f3fba07bafa226f11ed7b5f1e81237": "index.docker.io/mysql@sha256:b2f400f4a5e003b0543decf61a0a010939f3fba07bafa226f11ed7b5f1e81237", | ||
|
||
"docker.io/mysql": "index.docker.io/library/mysql", | ||
"docker.io/mysql:latest": "index.docker.io/library/mysql:latest", | ||
"docker.io/default/mysql:latest": "index.docker.io/default/mysql:latest", | ||
|
||
"docker.io/mysql@sha256:b2f400f4a5e003b0543decf61a0a010939f3fba07bafa226f11ed7b5f1e81237": "index.docker.io/library/mysql@sha256:b2f400f4a5e003b0543decf61a0a010939f3fba07bafa226f11ed7b5f1e81237", | ||
}, | ||
}, | ||
{ | ||
mappings: ImageReferenceMappings{{FromName: "test/other", ToRegistry: "another.registry"}}, | ||
results: map[string]string{ | ||
"test/other": "another.registry/test/other", | ||
"test/other:latest": "another.registry/test/other:latest", | ||
"myregistry.com/test/other:latest": "another.registry/test/other:latest", | ||
|
||
"myregistry.com/b/test/other:latest": "myregistry.com/b/test/other:latest", | ||
}, | ||
}, | ||
{ | ||
mappings: ImageReferenceMappings{{FromName: "test/other", ToName: "other/test"}}, | ||
results: map[string]string{ | ||
"test/other": "other/test", | ||
"test/other:latest": "other/test:latest", | ||
"myregistry.com/test/other:latest": "myregistry.com/other/test:latest", | ||
|
||
"test/other/b:latest": "test/other/b:latest", | ||
|
||
// TODO: this is possibly wrong with V2 and latest daemon | ||
"b/test/other:latest": "b/other/test:latest", | ||
}, | ||
}, | ||
} | ||
|
||
for i, test := range testCases { | ||
for in, out := range test.results { | ||
result := test.mappings.Map(in) | ||
if result != out { | ||
t.Errorf("%d: expect %s -> %s, got %q", i, in, out, result) | ||
continue | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.