Skip to content

Commit

Permalink
Add a migration framework for mutable resources
Browse files Browse the repository at this point in the history
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
smarterclayton committed Jun 9, 2016
1 parent 381559d commit d8358f6
Show file tree
Hide file tree
Showing 8 changed files with 550 additions and 4 deletions.
8 changes: 8 additions & 0 deletions pkg/client/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type ImageInterface interface {
List(opts kapi.ListOptions) (*imageapi.ImageList, error)
Get(name string) (*imageapi.Image, error)
Create(image *imageapi.Image) (*imageapi.Image, error)
Update(image *imageapi.Image) (*imageapi.Image, error)
Delete(name string) error
}

Expand Down Expand Up @@ -56,6 +57,13 @@ func (c *images) Create(image *imageapi.Image) (result *imageapi.Image, err erro
return
}

// Update alters an existingimage. Returns the server's representation of the image and error if one occurs.
func (c *images) Update(image *imageapi.Image) (result *imageapi.Image, err error) {
result = &imageapi.Image{}
err = c.r.Put().Resource("images").Name(image.Name).Body(image).Do().Into(result)
return
}

// Delete deletes an image, returns error if one occurs.
func (c *images) Delete(name string) (err error) {
err = c.r.Delete().Resource("images").Name(name).Do().Error()
Expand Down
9 changes: 8 additions & 1 deletion pkg/cmd/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/openshift/origin/pkg/cmd/admin/cert"
diagnostics "github.com/openshift/origin/pkg/cmd/admin/diagnostics"
"github.com/openshift/origin/pkg/cmd/admin/groups"
"github.com/openshift/origin/pkg/cmd/admin/migrate"
migrateimages "github.com/openshift/origin/pkg/cmd/admin/migrate/images"
"github.com/openshift/origin/pkg/cmd/admin/node"
"github.com/openshift/origin/pkg/cmd/admin/policy"
"github.com/openshift/origin/pkg/cmd/admin/project"
Expand All @@ -32,7 +34,7 @@ Administrative Commands
Commands for managing a cluster are exposed here. Many administrative
actions involve interaction with the command-line client as well.`

func NewCommandAdmin(name, fullName string, out io.Writer, errout io.Writer) *cobra.Command {
func NewCommandAdmin(name, fullName string, in io.Reader, out io.Writer, errout io.Writer) *cobra.Command {
// Main command
cmds := &cobra.Command{
Use: name,
Expand Down Expand Up @@ -67,6 +69,11 @@ func NewCommandAdmin(name, fullName string, out io.Writer, errout io.Writer) *co
diagnostics.NewCmdDiagnostics(diagnostics.DiagnosticsRecommendedName, fullName+" "+diagnostics.DiagnosticsRecommendedName, out),
node.NewCommandManageNode(f, node.ManageNodeCommandName, fullName+" "+node.ManageNodeCommandName, out),
prune.NewCommandPrune(prune.PruneRecommendedName, fullName+" "+prune.PruneRecommendedName, f, out),
migrate.NewCommandMigrate(
migrate.MigrateRecommendedName, fullName+" "+migrate.MigrateRecommendedName, f, out,
// Migration commands
migrateimages.NewCmdMigrateImageReferences("image-references", fullName+" "+migrate.MigrateRecommendedName+" image-references", f, in, out, errout),
),
},
},
{
Expand Down
272 changes: 272 additions & 0 deletions pkg/cmd/admin/migrate/images/images.go
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
}
62 changes: 62 additions & 0 deletions pkg/cmd/admin/migrate/images/images_test.go
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
}
}
}
}
Loading

0 comments on commit d8358f6

Please sign in to comment.