From 82440ed8c031afc125df1fe5096601d26afa5f64 Mon Sep 17 00:00:00 2001 From: Lex Cao Date: Tue, 10 Aug 2021 11:02:28 +0800 Subject: [PATCH] Support custom registry on pushing image --- cmd/convert.go | 3 + pkg/kobject/kobject.go | 1 + pkg/transformer/kubernetes/kubernetes.go | 18 ++--- pkg/transformer/openshift/openshift.go | 8 +- pkg/transformer/utils.go | 35 +++++++-- pkg/utils/docker/image.go | 58 ++++++++++++++ pkg/utils/docker/image_test.go | 96 ++++++++++++++++++++++++ pkg/utils/docker/push.go | 63 ++++++---------- pkg/utils/docker/tag.go | 29 +++++++ script/test/cmd/tests_new.sh | 8 ++ 10 files changed, 256 insertions(+), 63 deletions(-) create mode 100644 pkg/utils/docker/image.go create mode 100644 pkg/utils/docker/image_test.go create mode 100644 pkg/utils/docker/tag.go diff --git a/cmd/convert.go b/cmd/convert.go index a1257a3d..8e87627b 100644 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -47,6 +47,7 @@ var ( ConvertReplicas int ConvertController string ConvertPushImage bool + ConvertPushImageRegistry string ConvertOpt kobject.ConvertOptions ConvertYAMLIndent int @@ -88,6 +89,7 @@ var convertCmd = &cobra.Command{ BuildRepo: ConvertBuildRepo, BuildBranch: ConvertBuildBranch, PushImage: ConvertPushImage, + PushImageRegistry: ConvertPushImageRegistry, CreateDeploymentConfig: ConvertDeploymentConfig, EmptyVols: ConvertEmptyVols, Volumes: ConvertVolumes, @@ -147,6 +149,7 @@ func init() { // Standard between the two convertCmd.Flags().StringVar(&ConvertBuild, "build", "none", `Set the type of build ("local"|"build-config"(OpenShift only)|"none")`) convertCmd.Flags().BoolVar(&ConvertPushImage, "push-image", false, "If we should push the docker image we built") + convertCmd.Flags().StringVar(&ConvertPushImageRegistry, "push-image-registry", "", "Specify registry for pushing image, which will override registry from image name.") convertCmd.Flags().BoolVarP(&ConvertYaml, "yaml", "y", false, "Generate resource files into YAML format") convertCmd.Flags().MarkDeprecated("yaml", "YAML is the default format now.") convertCmd.Flags().MarkShorthandDeprecated("y", "YAML is the default format now.") diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index 2878db2b..c20b20a7 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -52,6 +52,7 @@ type ConvertOptions struct { BuildBranch string Build string PushImage bool + PushImageRegistry string CreateChart bool GenerateYaml bool GenerateJSON bool diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index dbde49be..bb386fdf 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -1167,12 +1167,9 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. } // Push the built image to the repo! - if opt.PushImage { - log.Infof("Push image enabled. Attempting to push image '%s'", service.Image) - err = transformer.PushDockerImage(service, name) - if err != nil { - return nil, errors.Wrapf(err, "Unable to push Docker image for service %v", name) - } + err = transformer.PushDockerImageWithOpt(service, name, opt) + if err != nil { + return nil, errors.Wrapf(err, "Unable to push Docker image for service %v", name) } } @@ -1316,12 +1313,9 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. } // Push the built image to the repo! - if opt.PushImage { - log.Infof("Push image enabled. Attempting to push image '%s'", service.Image) - err = transformer.PushDockerImage(service, name) - if err != nil { - return nil, errors.Wrapf(err, "Unable to push Docker image for service %v", name) - } + err = transformer.PushDockerImageWithOpt(service, name, opt) + if err != nil { + return nil, errors.Wrapf(err, "Unable to push Docker image for service %v", name) } } diff --git a/pkg/transformer/openshift/openshift.go b/pkg/transformer/openshift/openshift.go index 81033f1c..172d2350 100644 --- a/pkg/transformer/openshift/openshift.go +++ b/pkg/transformer/openshift/openshift.go @@ -318,11 +318,9 @@ func (o *OpenShift) Transform(komposeObject kobject.KomposeObject, opt kobject.C } // Push the built container to the repo! - if opt.PushImage { - err = transformer.PushDockerImage(service, name) - if err != nil { - log.Fatalf("Unable to push Docker image for service %v: %v", name, err) - } + err = transformer.PushDockerImageWithOpt(service, name, opt) + if err != nil { + log.Fatalf("Unable to push Docker image for service %v: %v", name, err) } } diff --git a/pkg/transformer/utils.go b/pkg/transformer/utils.go index 6627c480..98ca4df6 100644 --- a/pkg/transformer/utils.go +++ b/pkg/transformer/utils.go @@ -308,26 +308,47 @@ func BuildDockerImage(service kobject.ServiceConfig, name string) error { return nil } -// PushDockerImage pushes docker image -func PushDockerImage(service kobject.ServiceConfig, serviceName string) error { - log.Debugf("Pushing Docker image '%s'", service.Image) +// PushDockerImageWithOpt pushes docker image +func PushDockerImageWithOpt(service kobject.ServiceConfig, serviceName string, opt kobject.ConvertOptions) error { + if !opt.PushImage { + // Don't do anything if registry is specified but push is disabled, just WARN about it + if opt.PushImageRegistry != "" { + log.Warnf("Push image registry '%s' is specified but push image is disabled, skipping pushing to repository", opt.PushImageRegistry) + } + return nil + } + + log.Infof("Push image is enabled. Attempting to push image '%s'", service.Image) // Don't do anything if service.Image is blank, but at least WARN about it - // lse, let's push the image + // else, let's push the image if service.Image == "" { log.Warnf("No image name has been passed for service %s, skipping pushing to repository", serviceName) return nil } - // Connect to the Docker client + image, err := docker.ParseImage(service.Image, opt.PushImageRegistry) + if err != nil { + return err + } + client, err := docker.Client() if err != nil { return err } - push := docker.Push{Client: *client} - err = push.PushImage(service.Image) + if opt.PushImageRegistry != "" { + log.Info("Push image registry is specified. Tag the image into registry firstly.") + tag := docker.Tag{Client: *client} + err = tag.TagImage(image) + if err != nil { + return err + } + } + + push := docker.Push{Client: *client} + err = push.PushImage(image) if err != nil { return err } diff --git a/pkg/utils/docker/image.go b/pkg/utils/docker/image.go new file mode 100644 index 00000000..d1db94be --- /dev/null +++ b/pkg/utils/docker/image.go @@ -0,0 +1,58 @@ +package docker + +import ( + "path" + + dockerparser "github.com/novln/docker-parser" +) + +// Image contains the basic information parsed from full image name +// see github.com/novln/docker-parser Reference +type Image struct { + Name string // the image's name (ie: debian[:8.2]) + ShortName string // the image's name (ie: debian) + Tag string // the image's tag (or digest) + Registry string // the image's registry. (ie: host[:port]) + Repository string // the image's repository. (ie: registry/name) + Remote string // the image's remote identifier. (ie: registry/name[:tag]) +} + +func NewImageFromParsed(parsed *dockerparser.Reference) Image { + return Image{ + Name: parsed.Name(), + ShortName: parsed.ShortName(), + Tag: parsed.Tag(), + Registry: parsed.Registry(), + Repository: parsed.Repository(), + Remote: parsed.Remote(), + } +} + +// ParseImage Using https://github.com/novln/docker-parser in order to parse the appropriate name and registry. +// 1. Return default registry when the registry is not specified from image +// 2. Return target registry when the registry is specified from command line +func ParseImage(fullImageName string, targetRegistry string) (Image, error) { + var image Image + + // First parse to fill default fields for image + // See github.com/novln/docker-parser/docker/reference.go + parsedImage, err := dockerparser.Parse(fullImageName) + + if err != nil { + return image, err + } + + // Registry from command argument is high priority than parsed from name of image. + if targetRegistry != "" { + // Parse again for validating registry + fullImageName = path.Join(targetRegistry, parsedImage.Name()) + parsedImage, err = dockerparser.Parse(fullImageName) + if err != nil { + return image, err + } + } + + image = NewImageFromParsed(parsedImage) + + return image, nil +} diff --git a/pkg/utils/docker/image_test.go b/pkg/utils/docker/image_test.go new file mode 100644 index 00000000..65b243bf --- /dev/null +++ b/pkg/utils/docker/image_test.go @@ -0,0 +1,96 @@ +package docker + +import ( + "reflect" + "testing" +) + +func TestParseImage(t *testing.T) { + type args struct { + fullImageName string + targetRegistry string + } + tests := []struct { + name string + args args + want Image + wantErr bool + }{ + { + "Given empty registry Then default registry expected", + args{ + "foo/bar", + "", + }, + Image{ + "foo/bar:latest", + "foo/bar", + "latest", + "docker.io", + "docker.io/foo/bar", + "docker.io/foo/bar:latest", + }, + false, + }, + { + "Given registry from image Then parsed registry expected", + args{ + "docker.io/foo/bar", + "", + }, + Image{ + "foo/bar:latest", + "foo/bar", + "latest", + "docker.io", + "docker.io/foo/bar", + "docker.io/foo/bar:latest", + }, + false, + }, + { + "Given target registry Then target registry expected", + args{ + "foo/bar", + "localhost:5000", + }, + Image{ + "foo/bar:latest", + "foo/bar", + "latest", + "localhost:5000", + "localhost:5000/foo/bar", + "localhost:5000/foo/bar:latest", + }, + false, + }, + { + "Given registry from image and target registry Then target registry expected", + args{ + "docker.io/foo/bar", + "localhost:5000", + }, + Image{ + "foo/bar:latest", + "foo/bar", + "latest", + "localhost:5000", + "localhost:5000/foo/bar", + "localhost:5000/foo/bar:latest", + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseImage(tt.args.fullImageName, tt.args.targetRegistry) + if (err != nil) != tt.wantErr { + t.Errorf("ParseImage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseImage() got = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/pkg/utils/docker/push.go b/pkg/utils/docker/push.go index 11d72d51..1305fdf2 100644 --- a/pkg/utils/docker/push.go +++ b/pkg/utils/docker/push.go @@ -18,9 +18,9 @@ package docker import ( "bytes" + "strings" dockerlib "github.com/fsouza/go-dockerclient" - dockerparser "github.com/novln/docker-parser" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -35,23 +35,15 @@ PushImage pushes a Docker image via the Docker API. Takes the image name, parses the URL details and then push based on environment authentication credentials. */ -func (c *Push) PushImage(fullImageName string) error { - outputBuffer := bytes.NewBuffer(nil) - - // Using https://github.com/novln/docker-parser in order to parse the appropriate - // name and registry. - parsedImage, err := dockerparser.Parse(fullImageName) - if err != nil { - return err - } - image, registry := parsedImage.Name(), parsedImage.Registry() - - log.Infof("Pushing image '%s' to registry '%s'", image, registry) +func (c *Push) PushImage(image Image) error { + log.Infof("Pushing image '%s' to registry '%s'", image.Name, image.Registry) // Let's setup the push and authentication options + outputBuffer := bytes.NewBuffer(nil) options := dockerlib.PushImageOptions{ - Name: fullImageName, - Registry: parsedImage.Registry(), + Tag: image.Tag, + Name: image.Repository, + Registry: image.Registry, OutputStream: outputBuffer, } @@ -63,34 +55,27 @@ func (c *Push) PushImage(fullImageName string) error { log.Warn(errors.Wrap(err, "Unable to retrieve .docker/config.json authentication details. Check that 'docker login' works successfully on the command line.")) } - // Fallback to unauthenticated access in case if no auth credentials are retrieved - if credentials == nil || len(credentials.Configs) == 0 { - log.Info("Authentication credentials are not detected. Will try push without authentication.") - credentials = &dockerlib.AuthConfigurations{ - Configs: map[string]dockerlib.AuthConfiguration{ - registry: {}, - }, - } + // Handle legacy docker registry address + if strings.Contains(image.Registry, "docker.io") { + image.Registry = "https://index.docker.io/v1/" } - // Push the image to the repository (based on the URL) - // We will iterate through all available authentication configurations until we find one that pushes successfully - // and then return nil. - if len(credentials.Configs) > 1 { - log.Info("Multiple authentication credentials detected. Will try each configuration.") + // Find the authentication matched to registry + auth, ok := credentials.Configs[image.Registry] + if !ok { + // Fallback to unauthenticated access in case if no auth credentials are retrieved + log.Infof("Authentication credential of registry '%s' is not found. Will try push without authentication.", image.Registry) + auth = dockerlib.AuthConfiguration{} } - for k, v := range credentials.Configs { - log.Infof("Attempting authentication credentials '%s", k) - err = c.Client.PushImage(options, v) - if err != nil { - log.Errorf("Unable to push image '%s' to registry '%s'. Error: %s", image, registry, err) - } else { - log.Debugf("Image '%s' push output:\n%s", image, outputBuffer) - log.Infof("Successfully pushed image '%s' to registry '%s'", image, registry) - return nil - } + log.Debugf("Pushing image with options %+v", options) + err = c.Client.PushImage(options, auth) + if err != nil { + log.Errorf("Unable to push image '%s' to registry '%s'. Error: %s", image.Name, image.Registry, err) + return errors.New("unable to push docker image(s). Check that `docker login` works successfully on the command line") } - return errors.New("unable to push docker image(s). Check that `docker login` works successfully on the command line") + log.Debugf("Image '%+v' push output:\n%s", image, outputBuffer) + log.Infof("Successfully pushed image '%s' to registry '%s'", image.Name, image.Registry) + return nil } diff --git a/pkg/utils/docker/tag.go b/pkg/utils/docker/tag.go new file mode 100644 index 00000000..f0360948 --- /dev/null +++ b/pkg/utils/docker/tag.go @@ -0,0 +1,29 @@ +package docker + +import ( + dockerlib "github.com/fsouza/go-dockerclient" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" +) + +// Tag will provide methods for interaction with API regarding tagging images +type Tag struct { + Client dockerlib.Client +} + +func (c *Tag) TagImage(image Image) error { + options := dockerlib.TagImageOptions{ + Tag: image.Tag, + Repo: image.Repository, + } + + log.Infof("Tagging image '%s' into repository '%s'", image.Name, image.Repository) + err := c.Client.TagImage(image.ShortName, options) + if err != nil { + log.Errorf("Unable to tag image '%s' into repository '%s'. Error: %s", image.Name, image.Registry, err) + return errors.New("unable to tag docker image(s)") + } + + log.Infof("Successfully tagged image '%s'", image.Remote) + return nil +} diff --git a/script/test/cmd/tests_new.sh b/script/test/cmd/tests_new.sh index 53eb2fc3..014cdd9c 100755 --- a/script/test/cmd/tests_new.sh +++ b/script/test/cmd/tests_new.sh @@ -95,3 +95,11 @@ convert::check_artifacts_generated "kompose --build local -f $KOMPOSE_ROOT/scrip # Test build v3 relative compose file with context relative_path=$(realpath --relative-to="$PWD" "$KOMPOSE_ROOT/script/test/fixtures/buildconfig/docker-compose-v3.yml") convert::check_artifacts_generated "kompose --build local -f $relative_path convert -o $TEMP_DIR/output_file" "$TEMP_DIR/output_file" + +##### +# Test the build config with push image +# Default behavior with push image disabled +cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/buildconfig/docker-compose-build-no-image.yml -o $TEMP_DIR/output_file convert --build=local --push-image-registry=whatever" +convert::expect_warning "$cmd" "Push image registry 'whatever' is specified but push image is disabled, skipping pushing to repository" +# TODO Push image to docker.io as default. Then verify push success +# TODO Push image to a private registry. Then verify push success