From 5dc99d1deffb1f009c3d3a6f788cb6db3c923686 Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Wed, 25 Jul 2018 13:16:09 -0400 Subject: [PATCH] Show tags / versions when doing odo catalog list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using `odo catalog list` the tags will now be listed: ``` github.com/redhat-developer/odo add-versioning ✗ 7d ⚑ ⍉ ▶ ./odo catalog list NAME TAGS dotnet 2.0,latest httpd 2.4,latest nginx 1.10,1.12,1.8,latest nodejs 0.10,4,6,8,latest perl 5.16,5.20,5.24,latest php 5.5,5.6,7.0,7.1,latest python 2.7,3.3,3.4,3.5,3.6,latest ruby 2.0,2.2,2.3,2.4,latest wildfly 10.0,10.1,8.1,9.0,latest ``` --- cmd/catalog.go | 12 +++- pkg/catalog/catalog.go | 53 ++++++++++------ pkg/catalog/catalog_test.go | 121 ++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 pkg/catalog/catalog_test.go diff --git a/cmd/catalog.go b/cmd/catalog.go index ad76e6d307f..97aecb44784 100644 --- a/cmd/catalog.go +++ b/cmd/catalog.go @@ -2,6 +2,10 @@ package cmd import ( "fmt" + "os" + "strings" + "text/tabwriter" + "github.com/redhat-developer/odo/pkg/catalog" "github.com/spf13/cobra" ) @@ -34,10 +38,14 @@ var catalogListCmd = &cobra.Command{ case 0: fmt.Printf("No deployable components found\n") default: - fmt.Println("The following components can be deployed:") + + w := tabwriter.NewWriter(os.Stdout, 5, 2, 3, ' ', tabwriter.TabIndent) + fmt.Fprintln(w, "NAME", "\t", "TAGS") for _, component := range catalogList { - fmt.Printf("- %v\n", component) + fmt.Fprintln(w, component.Name, "\t", strings.Join(component.Tags, ",")) } + w.Flush() + } }, } diff --git a/pkg/catalog/catalog.go b/pkg/catalog/catalog.go index f4bf0acabca..08c3cdaa18c 100644 --- a/pkg/catalog/catalog.go +++ b/pkg/catalog/catalog.go @@ -8,21 +8,22 @@ import ( log "github.com/sirupsen/logrus" ) +type CatalogImage struct { + Name string + Tags []string +} + // List lists all the available component types -func List(client *occlient.Client) ([]string, error) { - var catalogList []string - imageStreams, err := getDefaultBuilderImages(client) +func List(client *occlient.Client) ([]CatalogImage, error) { + + catalogList, err := getDefaultBuilderImages(client) if err != nil { return nil, errors.Wrap(err, "unable to get image streams") } - catalogList = append(catalogList, imageStreams...) - // TODO: uncomment when component create supports template creation - //clusterServiceClasses, err := client.GetClusterServiceClassExternalNames() - //if err != nil { - // return nil, errors.Wrap(err, "unable to get cluster service classes") - //} - //catalogList = append(catalogList, clusterServiceClasses...) + if len(catalogList) == 0 { + return nil, errors.New("unable to retrieve any catalog images from the OpenShift cluster") + } return catalogList, nil } @@ -37,8 +38,8 @@ func Search(client *occlient.Client, name string) ([]string, error) { // do a partial search in all the components for _, component := range componentList { - if strings.Contains(component, name) { - result = append(result, component) + if strings.Contains(component.Name, name) { + result = append(result, component.Name) } } @@ -53,7 +54,7 @@ func Exists(client *occlient.Client, componentType string) (bool, error) { } for _, supported := range catalogList { - if componentType == supported { + if componentType == supported.Name { return true, nil } } @@ -62,27 +63,43 @@ func Exists(client *occlient.Client, componentType string) (bool, error) { // getDefaultBuilderImages returns the default builder images available in the // openshift namespace -func getDefaultBuilderImages(client *occlient.Client) ([]string, error) { +func getDefaultBuilderImages(client *occlient.Client) ([]CatalogImage, error) { imageStreams, err := client.GetImageStreams(occlient.OpenShiftNameSpace) if err != nil { return nil, errors.Wrap(err, "unable to get Image Streams") } - var builderImages []string + var builderImages []CatalogImage + // Get builder images from the available imagestreams -outer: for _, imageStream := range imageStreams { + var allTags []string + buildImage := false + for _, tag := range imageStream.Spec.Tags { + + allTags = append(allTags, tag.Name) + + // Check to see if it is a "builder" image if _, ok := tag.Annotations["tags"]; ok { for _, t := range strings.Split(tag.Annotations["tags"], ",") { + + // If the tag has "builder" then we will add the image to the list if t == "builder" { - builderImages = append(builderImages, imageStream.Name) - continue outer + buildImage = true } } } + } + + // Append to the list of images if a "builder" tag was found + if buildImage { + builderImages = append(builderImages, CatalogImage{Name: imageStream.Name, Tags: allTags}) + } + } + log.Debugf("Found builder images: %v", builderImages) return builderImages, nil } diff --git a/pkg/catalog/catalog_test.go b/pkg/catalog/catalog_test.go new file mode 100644 index 00000000000..43260a77462 --- /dev/null +++ b/pkg/catalog/catalog_test.go @@ -0,0 +1,121 @@ +package catalog + +import ( + "reflect" + "testing" + + imagev1 "github.com/openshift/api/image/v1" + "github.com/redhat-developer/odo/pkg/occlient" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ktesting "k8s.io/client-go/testing" +) + +// Function taken from occlient_test.go +// fakeImageStream gets imagestream for the reactor +func fakeImageStream(imageName string, namespace string, tags []string) *imagev1.ImageStream { + image := &imagev1.ImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: imageName, + Namespace: namespace, + }, + Status: imagev1.ImageStreamStatus{ + Tags: []imagev1.NamedTagEventList{ + { + Tag: "latest", + Items: []imagev1.TagEvent{ + {DockerImageReference: "example/" + imageName + ":latest"}, + {Generation: 1}, + {Image: imageName + "@sha256:9579a93ee"}, + }, + }, + }, + }, + } + + for _, tag := range tags { + imageTag := imagev1.TagReference{Name: tag, Annotations: map[string]string{"tags": "builder"}} + image.Spec.Tags = append(image.Spec.Tags, imageTag) + } + + return image +} + +// Function taken from occlient_test.go +// fakeImageStreams lists the imagestreams for the reactor +func fakeImageStreams(imageName string, namespace string, tags []string) *imagev1.ImageStreamList { + return &imagev1.ImageStreamList{ + Items: []imagev1.ImageStream{*fakeImageStream(imageName, namespace, tags)}, + } +} + +func TestList(t *testing.T) { + type args struct { + name string + namespace string + tags []string + } + tests := []struct { + name string + args args + wantErr bool + wantTags []string + }{ + { + name: "Case 1: Valid image output with one tag", + args: args{ + name: "foobar", + namespace: "openshift", + tags: []string{"latest"}, + }, + wantErr: false, + wantTags: []string{"latest"}, + }, + { + name: "Case 2: Valid image output with multiple tags", + args: args{ + name: "foobar", + namespace: "openshift", + tags: []string{"1.0.0", "1.0.1", "0.0.1", "latest"}, + }, + wantErr: false, + wantTags: []string{"1.0.0", "1.0.1", "0.0.1", "latest"}, + }, + { + name: "Case 3: Invalid image output with no tags", + args: args{ + name: "foobar", + namespace: "foo", + tags: []string{}, + }, + wantErr: true, + wantTags: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + // Fake the client with the appropriate arguments + client, fakeClientSet := occlient.FakeNew() + fakeClientSet.ImageClientset.PrependReactor("list", "imagestreams", func(action ktesting.Action) (bool, runtime.Object, error) { + return true, fakeImageStreams(tt.args.name, tt.args.namespace, tt.args.tags), nil + }) + + // The function we are testing + output, err := List(client) + + //Checks for error in positive cases + if !tt.wantErr == (err != nil) { + t.Errorf("component List() unexpected error %v, wantErr %v", err, tt.wantErr) + } + + // Check if the output is the same as what's expected (tags) + // and only if output is more than 0 (something is actually returned) + if len(output) > 0 && !(reflect.DeepEqual(output[0].Tags, tt.wantTags)) { + t.Errorf("expected tags: %s, got: %s", tt.wantTags, output[0].Tags) + } + + }) + } +}