From 0966e95e2e0b6a5d442896bbe5b924aac16c2ba3 Mon Sep 17 00:00:00 2001 From: vinamra28 Date: Tue, 20 Apr 2021 12:33:55 +0530 Subject: [PATCH] CLI: Add command to check upgrades Add new subcommand which can check upgrades for resources which are installed via Hub CLI. This command will skip those tasks which are not installed via Hub CLI or rather those resources which doesn't have the label `hub.tekton.dev/catalog: `. Signed-off-by: vinamra28 --- api/go.mod | 1 + api/go.sum | 3 + .../cmd/check_upgrade/check_upgrade_test.go | 368 ++++++++++++++++++ api/pkg/cli/cmd/check_upgrade/check_uprade.go | 227 +++++++++++ .../testdata/TestNoUpdateAvailable.golden | 3 + ...Available_TaskNotInstalledViaHubCLI.golden | 8 + .../testdata/TestUpdateAvailable.golden | 4 + ...estUpdateAvailable_PipelinesUnknown.golden | 6 + ...estUpdateAvailable_WithSkippedTasks.golden | 9 + ...e_WithSkippedTasks_PipelinesUnknown.golden | 11 + api/pkg/cli/cmd/info/info_test.go | 6 +- api/pkg/cli/cmd/root.go | 2 + api/pkg/cli/formatter/field.go | 13 + api/pkg/cli/formatter/field_test.go | 9 +- api/pkg/cli/hub/get_resource.go | 15 +- api/pkg/cli/hub/get_resource_test.go | 4 + api/pkg/cli/installer/action.go | 10 + api/pkg/cli/installer/action_test.go | 32 ++ api/pkg/cli/installer/kube_action.go | 15 + 19 files changed, 737 insertions(+), 9 deletions(-) create mode 100644 api/pkg/cli/cmd/check_upgrade/check_upgrade_test.go create mode 100644 api/pkg/cli/cmd/check_upgrade/check_uprade.go create mode 100644 api/pkg/cli/cmd/check_upgrade/testdata/TestNoUpdateAvailable.golden create mode 100644 api/pkg/cli/cmd/check_upgrade/testdata/TestNoUpdateAvailable_TaskNotInstalledViaHubCLI.golden create mode 100644 api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable.golden create mode 100644 api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_PipelinesUnknown.golden create mode 100644 api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_WithSkippedTasks.golden create mode 100644 api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_WithSkippedTasks_PipelinesUnknown.golden diff --git a/api/go.mod b/api/go.mod index 80738806b..eb2d189f8 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dimfeld/httptreemux/v5 v5.3.0 // indirect + github.com/fatih/color v1.7.0 github.com/go-gormigrate/gormigrate/v2 v2.0.0 github.com/go-testfixtures/testfixtures/v3 v3.2.0 github.com/golang/protobuf v1.5.2 // indirect diff --git a/api/go.sum b/api/go.sum index 412edf6af..de797e07c 100644 --- a/api/go.sum +++ b/api/go.sum @@ -259,6 +259,7 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= @@ -748,6 +749,7 @@ github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kN github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -755,6 +757,7 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= diff --git a/api/pkg/cli/cmd/check_upgrade/check_upgrade_test.go b/api/pkg/cli/cmd/check_upgrade/check_upgrade_test.go new file mode 100644 index 000000000..322e26c98 --- /dev/null +++ b/api/pkg/cli/cmd/check_upgrade/check_upgrade_test.go @@ -0,0 +1,368 @@ +// Copyright © 2021 The Tekton Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package check_upgrade + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + res "github.com/tektoncd/hub/api/gen/resource" + "github.com/tektoncd/hub/api/pkg/cli/test" + cb "github.com/tektoncd/hub/api/pkg/cli/test/builder" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + pipelinev1beta1test "github.com/tektoncd/pipeline/test" + "gopkg.in/h2non/gock.v1" + "gotest.tools/v3/golden" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" +) + +var resVersion = &res.ResourceData{ + ID: 1, + Name: "foo", + Kind: "Task", + Catalog: &res.Catalog{ + ID: 1, + Name: "tekton", + Type: "community", + }, + Rating: 4.8, + LatestVersion: &res.ResourceVersionData{ + ID: 12, + Version: "0.2", + Description: "v0.1 Task to run foo", + DisplayName: "foo-bar", + MinPipelinesVersion: "0.11", + RawURL: "http://raw.github.url/foo/0.1/foo.yaml", + WebURL: "http://web.github.com/foo/0.1/foo.yaml", + UpdatedAt: "2020-01-01 12:00:00 +0000 UTC", + }, + Tags: []*res.Tag{ + { + ID: 3, + Name: "cli", + }, + }, + Versions: []*res.ResourceVersionData{ + { + ID: 12, + Version: "0.2", + }, + }, +} + +func TestUpdateAvailable(t *testing.T) { + cli := test.NewCLI() + + defer gock.Off() + + resource := &res.Resource{Data: resVersion} + res := res.NewViewedResource(resource, "default") + gock.New(test.API). + Get("/resource/tekton/task/foo"). + MatchParam("pipelinesversion", "0.14"). + Reply(200). + JSON(&res.Projected) + + buf := new(bytes.Buffer) + cli.SetStream(buf, buf) + + existingTask := &v1beta1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "hub", + Labels: map[string]string{ + "hub.tekton.dev/catalog": "tekton", + "app.kubernetes.io/version": "0.1", + }}, + } + + version := "v1beta1" + dynamic := fake.NewSimpleDynamicClient(runtime.NewScheme(), cb.UnstructuredV1beta1T(existingTask, version)) + + cs, _ := test.SeedV1beta1TestData(t, pipelinev1beta1test.Data{Tasks: []*v1beta1.Task{existingTask}}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task"}) + + opts := &options{ + cs: test.FakeClientSet(cs.Pipeline, dynamic, "hub"), + cli: cli, + kind: "task", + } + + err := test.CreateTektonPipelineController(dynamic, "v0.14.0") + if err != nil { + t.Errorf("%s", err.Error()) + } + + err = opts.run() + assert.NoError(t, err) + golden.Assert(t, buf.String(), fmt.Sprintf("%s.golden", t.Name())) + assert.Equal(t, gock.IsDone(), true) +} + +func TestUpdateAvailable_WithSkippedTasks(t *testing.T) { + cli := test.NewCLI() + + defer gock.Off() + + resource := &res.Resource{Data: resVersion} + res := res.NewViewedResource(resource, "default") + gock.New(test.API). + Get("/resource/tekton/task/foo"). + MatchParam("pipelinesversion", "0.14"). + Reply(200). + JSON(&res.Projected) + + buf := new(bytes.Buffer) + cli.SetStream(buf, buf) + + existingTasks := []*v1beta1.Task{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "hub", + Labels: map[string]string{ + "hub.tekton.dev/catalog": "tekton", + "app.kubernetes.io/version": "0.1", + }}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-bar", + Namespace: "hub", + Labels: map[string]string{ + "app.kubernetes.io/version": "0.1", + }}, + }, + } + + version := "v1beta1" + dynamic := fake.NewSimpleDynamicClient(runtime.NewScheme(), cb.UnstructuredV1beta1T(existingTasks[0], version), cb.UnstructuredV1beta1T(existingTasks[1], version)) + + cs, _ := test.SeedV1beta1TestData(t, pipelinev1beta1test.Data{Tasks: existingTasks}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task"}) + + opts := &options{ + cs: test.FakeClientSet(cs.Pipeline, dynamic, "hub"), + cli: cli, + kind: "task", + } + + err := test.CreateTektonPipelineController(dynamic, "v0.14.0") + if err != nil { + t.Errorf("%s", err.Error()) + } + + err = opts.run() + assert.NoError(t, err) + golden.Assert(t, buf.String(), fmt.Sprintf("%s.golden", t.Name())) + assert.Equal(t, gock.IsDone(), true) +} + +func TestNoUpdateAvailable(t *testing.T) { + cli := test.NewCLI() + + defer gock.Off() + + resource := &res.Resource{Data: resVersion} + res := res.NewViewedResource(resource, "default") + gock.New(test.API). + Get("/resource/tekton/task/foo"). + MatchParam("pipelinesversion", "0.14"). + Reply(200). + JSON(&res.Projected) + + buf := new(bytes.Buffer) + cli.SetStream(buf, buf) + + existingTask := &v1beta1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "hub", + Labels: map[string]string{ + "hub.tekton.dev/catalog": "tekton", + "app.kubernetes.io/version": "0.2", + }}, + } + + version := "v1beta1" + dynamic := fake.NewSimpleDynamicClient(runtime.NewScheme(), cb.UnstructuredV1beta1T(existingTask, version)) + + cs, _ := test.SeedV1beta1TestData(t, pipelinev1beta1test.Data{Tasks: []*v1beta1.Task{existingTask}}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task"}) + + opts := &options{ + cs: test.FakeClientSet(cs.Pipeline, dynamic, "hub"), + cli: cli, + kind: "task", + } + + err := test.CreateTektonPipelineController(dynamic, "v0.14.0") + if err != nil { + t.Errorf("%s", err.Error()) + } + + err = opts.run() + assert.NoError(t, err) + golden.Assert(t, buf.String(), fmt.Sprintf("%s.golden", t.Name())) + assert.Equal(t, gock.IsDone(), true) +} + +func TestNoUpdateAvailable_TaskNotInstalledViaHubCLI(t *testing.T) { + cli := test.NewCLI() + + defer gock.Off() + + resource := &res.Resource{Data: resVersion} + res := res.NewViewedResource(resource, "default") + gock.New(test.API). + Get("/resource/tekton/task/foo"). + MatchParam("pipelinesversion", "0.14"). + Reply(200). + JSON(&res.Projected) + + buf := new(bytes.Buffer) + cli.SetStream(buf, buf) + + existingTask := &v1beta1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "hub", + Labels: map[string]string{ + "app.kubernetes.io/version": "0.1", + }}, + } + + version := "v1beta1" + dynamic := fake.NewSimpleDynamicClient(runtime.NewScheme(), cb.UnstructuredV1beta1T(existingTask, version)) + + cs, _ := test.SeedV1beta1TestData(t, pipelinev1beta1test.Data{Tasks: []*v1beta1.Task{existingTask}}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task"}) + + opts := &options{ + cs: test.FakeClientSet(cs.Pipeline, dynamic, "hub"), + cli: cli, + kind: "task", + } + + err := test.CreateTektonPipelineController(dynamic, "v0.14.0") + if err != nil { + t.Errorf("%s", err.Error()) + } + + err = opts.run() + assert.NoError(t, err) + golden.Assert(t, buf.String(), fmt.Sprintf("%s.golden", t.Name())) + assert.Equal(t, gock.IsDone(), false) +} + +func TestUpdateAvailable_PipelinesUnknown(t *testing.T) { + cli := test.NewCLI() + + defer gock.Off() + + resource := &res.Resource{Data: resVersion} + res := res.NewViewedResource(resource, "default") + gock.New(test.API). + Get("/resource/tekton/task/foo"). + Reply(200). + JSON(&res.Projected) + + buf := new(bytes.Buffer) + cli.SetStream(buf, buf) + + existingTask := &v1beta1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "hub", + Labels: map[string]string{ + "hub.tekton.dev/catalog": "tekton", + "app.kubernetes.io/version": "0.1", + }}, + } + + version := "v1beta1" + dynamic := fake.NewSimpleDynamicClient(runtime.NewScheme(), cb.UnstructuredV1beta1T(existingTask, version)) + + cs, _ := test.SeedV1beta1TestData(t, pipelinev1beta1test.Data{Tasks: []*v1beta1.Task{existingTask}}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task"}) + + opts := &options{ + cs: test.FakeClientSet(cs.Pipeline, dynamic, "hub"), + cli: cli, + kind: "task", + } + + err := opts.run() + assert.NoError(t, err) + golden.Assert(t, buf.String(), fmt.Sprintf("%s.golden", t.Name())) + assert.Equal(t, gock.IsDone(), true) +} + +func TestUpdateAvailable_WithSkippedTasks_PipelinesUnknown(t *testing.T) { + cli := test.NewCLI() + + defer gock.Off() + + resource := &res.Resource{Data: resVersion} + res := res.NewViewedResource(resource, "default") + gock.New(test.API). + Get("/resource/tekton/task/foo"). + Reply(200). + JSON(&res.Projected) + + buf := new(bytes.Buffer) + cli.SetStream(buf, buf) + + existingTasks := []*v1beta1.Task{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "hub", + Labels: map[string]string{ + "hub.tekton.dev/catalog": "tekton", + "app.kubernetes.io/version": "0.1", + }}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-bar", + Namespace: "hub", + Labels: map[string]string{ + "app.kubernetes.io/version": "0.1", + }}, + }, + } + + version := "v1beta1" + dynamic := fake.NewSimpleDynamicClient(runtime.NewScheme(), cb.UnstructuredV1beta1T(existingTasks[0], version), cb.UnstructuredV1beta1T(existingTasks[1], version)) + + cs, _ := test.SeedV1beta1TestData(t, pipelinev1beta1test.Data{Tasks: existingTasks}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task"}) + + opts := &options{ + cs: test.FakeClientSet(cs.Pipeline, dynamic, "hub"), + cli: cli, + kind: "task", + } + + err := opts.run() + assert.NoError(t, err) + golden.Assert(t, buf.String(), fmt.Sprintf("%s.golden", t.Name())) + assert.Equal(t, gock.IsDone(), true) +} diff --git a/api/pkg/cli/cmd/check_upgrade/check_uprade.go b/api/pkg/cli/cmd/check_upgrade/check_uprade.go new file mode 100644 index 000000000..85111b927 --- /dev/null +++ b/api/pkg/cli/cmd/check_upgrade/check_uprade.go @@ -0,0 +1,227 @@ +// Copyright © 2021 The Tekton Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package check_upgrade + +import ( + "strings" + "text/template" + + "github.com/spf13/cobra" + "github.com/tektoncd/hub/api/pkg/cli/app" + "github.com/tektoncd/hub/api/pkg/cli/formatter" + "github.com/tektoncd/hub/api/pkg/cli/hub" + "github.com/tektoncd/hub/api/pkg/cli/installer" + "github.com/tektoncd/hub/api/pkg/cli/kube" + "github.com/tektoncd/hub/api/pkg/cli/printer" +) + +const upgradeTemplate = `{{decorate "underline bold" "Upgrades Available\n" }} + +{{- if ne (len .HubResources) 0 }} +{{- if .IsPipelineVersionUnknown }} +NAME CATALOG CURRENT_VERSION LATEST_VERSION +{{- else }} +NAME CATALOG CURRENT_VERSION LATEST_COMPATIBLE_VERSION +{{- end }} +{{- range $el := .HubResources }} +{{ $el.Name }} {{ $el.Catalog }} {{ $el.CurrentVersion }} {{ $el.LatestVersion }} +{{- end }} +{{- else }} +No {{ .Kind }} for upgrade +{{- end -}} + +{{ if ne (len .NonHubResources) 0 }} +{{ decorate "underline bold" "\nSkipped Resources\n" }} +NAME +{{- range $el := .NonHubResources }} +{{ icon "bullet" }}{{ $el }} +{{- end -}} +{{- end -}} + +{{ if .IsPipelineVersionUnknown }} +{{ decorate "bold" "\nWARN: Pipelines version unknown. Check your pipelines version before upgrading." }} +{{- end }} +` + +var ( + funcMap = template.FuncMap{ + "icon": formatter.Icon, + "decorate": formatter.DecorateAttr, + } + tmpl = template.Must(template.New("Check Upgrade").Funcs(funcMap).Parse(upgradeTemplate)) +) + +type hubRes struct { + Name string + Catalog string + CurrentVersion string + LatestVersion string +} + +type templateData struct { + Kind string + NonHubResources []string + HubResources []hubRes + IsPipelineVersionUnknown bool +} + +const ( + versionLabel = "app.kubernetes.io/version" + hubLabel = "hub.tekton.dev/catalog" +) + +type options struct { + cli app.CLI + kind string + version string + args []string + kc kube.Config + cs kube.ClientSet +} + +var cmdExamples string = ` +Check for Upgrades of %S installed via Tekton Hub CLI: + + tkn hub check-upgrades %s + +The above command will check for upgrades of %Ss installed via Tekton Hub CLI +and will skip the %Ss which are not installed by Tekton Hub CLI. + +NOTE: If Pipelines version is unknown it will show the latest version available +else it will show latest compatible version. +` + +func Command(cli app.CLI) *cobra.Command { + opts := &options{cli: cli} + + cmd := &cobra.Command{ + Use: "check-upgrade", + Short: "Check for upgrades of resources if present", + Long: ``, + Annotations: map[string]string{ + "commandType": "main", + }, + SilenceUsage: true, + } + cmd.AddCommand( + commandForKind("task", opts), + ) + + cmd.PersistentFlags().StringVarP(&opts.kc.Path, "kubeconfig", "k", "", "Kubectl config file (default: $HOME/.kube/config)") + cmd.PersistentFlags().StringVarP(&opts.kc.Context, "context", "c", "", "Name of the kubeconfig context to use (default: kubectl config current-context)") + cmd.PersistentFlags().StringVarP(&opts.kc.Namespace, "namespace", "n", "", "Namespace to use (default: from $KUBECONFIG)") + + return cmd +} + +// commandForKind creates a cobra.Command that when run sets +// opts.Kind and opts.Args and invokes opts.run +func commandForKind(kind string, opts *options) *cobra.Command { + + return &cobra.Command{ + Use: kind, + Short: "Check updates for " + strings.Title(kind) + " installed via Hub CLI", + Long: ``, + SilenceUsage: true, + Example: examples(kind), + Annotations: map[string]string{ + "commandType": "main", + }, + + RunE: func(cmd *cobra.Command, args []string) error { + opts.kind = kind + opts.args = args + return opts.run() + }, + } +} + +func (opts *options) run() error { + + var err error + if opts.cs == nil { + opts.cs, err = kube.NewClientSet(opts.kc) + if err != nil { + return err + } + } + + resInstaller := installer.New(opts.cs) + + // List all Tekton resources installed on Cluster in a particular namespace + resources, _ := resInstaller.ListInstalled(opts.kind, opts.cs.Namespace()) + + hubClient := opts.cli.Hub() + + // Init 2 arrays to store respective data + nonHubResources := make([]string, 0) + hubResources := make([]hubRes, 0) + + for _, resource := range resources { + + opts.version = resource.GetLabels()[versionLabel] + + // Check whether hub label is present or not + resourceCatalogLabel := resource.GetLabels()[hubLabel] + + // If not hub resource then add that resource to nonHubResources array + if resourceCatalogLabel == "" { + nonHubResources = append(nonHubResources, resource.GetName()) + continue + } + + // Call the endpoint /resource///?pipelinesversion= + res := hubClient.GetResource(hub.ResourceOption{ + Name: resource.GetName(), + Catalog: resourceCatalogLabel, + Kind: opts.kind, + PipelineVersion: resInstaller.GetPipelineVersion(), + }) + + resourceDetails, err := res.Resource() + if err != nil { + return err + } + + hubResource := resourceDetails.(hub.ResourceData) + + // Check if higher version available + if opts.version < *hubResource.LatestVersion.Version { + hubRes := hubRes{ + Name: *hubResource.Name, + Catalog: strings.Title(resourceCatalogLabel), + CurrentVersion: opts.version, + LatestVersion: *hubResource.LatestVersion.Version, + } + hubResources = append(hubResources, hubRes) + } + } + + tmplData := templateData{ + Kind: opts.kind, + NonHubResources: nonHubResources, + HubResources: hubResources, + IsPipelineVersionUnknown: resInstaller.GetPipelineVersion() == "", + } + + out := opts.cli.Stream().Out + + return printer.New(out).Tabbed(tmpl, tmplData) +} + +func examples(kind string) string { + replacer := strings.NewReplacer("%s", kind, "%S", strings.Title(kind)) + return replacer.Replace(cmdExamples) +} diff --git a/api/pkg/cli/cmd/check_upgrade/testdata/TestNoUpdateAvailable.golden b/api/pkg/cli/cmd/check_upgrade/testdata/TestNoUpdateAvailable.golden new file mode 100644 index 000000000..48f5b1465 --- /dev/null +++ b/api/pkg/cli/cmd/check_upgrade/testdata/TestNoUpdateAvailable.golden @@ -0,0 +1,3 @@ +Upgrades Available + +No task for upgrade diff --git a/api/pkg/cli/cmd/check_upgrade/testdata/TestNoUpdateAvailable_TaskNotInstalledViaHubCLI.golden b/api/pkg/cli/cmd/check_upgrade/testdata/TestNoUpdateAvailable_TaskNotInstalledViaHubCLI.golden new file mode 100644 index 000000000..190f247b5 --- /dev/null +++ b/api/pkg/cli/cmd/check_upgrade/testdata/TestNoUpdateAvailable_TaskNotInstalledViaHubCLI.golden @@ -0,0 +1,8 @@ +Upgrades Available + +No task for upgrade + +Skipped Resources + +NAME +∙ foo diff --git a/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable.golden b/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable.golden new file mode 100644 index 000000000..55d6afeef --- /dev/null +++ b/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable.golden @@ -0,0 +1,4 @@ +Upgrades Available + +NAME CATALOG CURRENT_VERSION LATEST_COMPATIBLE_VERSION +foo Tekton 0.1 0.2 diff --git a/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_PipelinesUnknown.golden b/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_PipelinesUnknown.golden new file mode 100644 index 000000000..b390423ff --- /dev/null +++ b/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_PipelinesUnknown.golden @@ -0,0 +1,6 @@ +Upgrades Available + +NAME CATALOG CURRENT_VERSION LATEST_VERSION +foo Tekton 0.1 0.2 + +WARN: Pipelines version unknown. Check your pipelines version before upgrading. diff --git a/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_WithSkippedTasks.golden b/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_WithSkippedTasks.golden new file mode 100644 index 000000000..1c423e163 --- /dev/null +++ b/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_WithSkippedTasks.golden @@ -0,0 +1,9 @@ +Upgrades Available + +NAME CATALOG CURRENT_VERSION LATEST_COMPATIBLE_VERSION +foo Tekton 0.1 0.2 + +Skipped Resources + +NAME +∙ foo-bar diff --git a/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_WithSkippedTasks_PipelinesUnknown.golden b/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_WithSkippedTasks_PipelinesUnknown.golden new file mode 100644 index 000000000..861969bca --- /dev/null +++ b/api/pkg/cli/cmd/check_upgrade/testdata/TestUpdateAvailable_WithSkippedTasks_PipelinesUnknown.golden @@ -0,0 +1,11 @@ +Upgrades Available + +NAME CATALOG CURRENT_VERSION LATEST_VERSION +foo Tekton 0.1 0.2 + +Skipped Resources + +NAME +∙ foo-bar + +WARN: Pipelines version unknown. Check your pipelines version before upgrading. diff --git a/api/pkg/cli/cmd/info/info_test.go b/api/pkg/cli/cmd/info/info_test.go index 805385f30..08b361c8e 100644 --- a/api/pkg/cli/cmd/info/info_test.go +++ b/api/pkg/cli/cmd/info/info_test.go @@ -47,13 +47,13 @@ var resource = &res.ResourceData{ UpdatedAt: "2020-01-01 12:00:00 +0000 UTC", }, Tags: []*res.Tag{ - &res.Tag{ + { ID: 3, Name: "cli", }, }, Versions: []*res.ResourceVersionData{ - &res.ResourceVersionData{ + { ID: 11, Version: "0.1", }, @@ -79,7 +79,7 @@ var pipelineResWithVersion = &res.ResourceVersionData{ }, Rating: 4.3, Tags: []*res.Tag{ - &res.Tag{ + { ID: 3, Name: "fruit", }, diff --git a/api/pkg/cli/cmd/root.go b/api/pkg/cli/cmd/root.go index 87ecf8e76..2c0216913 100644 --- a/api/pkg/cli/cmd/root.go +++ b/api/pkg/cli/cmd/root.go @@ -17,6 +17,7 @@ package cmd import ( "github.com/spf13/cobra" "github.com/tektoncd/hub/api/pkg/cli/app" + "github.com/tektoncd/hub/api/pkg/cli/cmd/check_upgrade" "github.com/tektoncd/hub/api/pkg/cli/cmd/downgrade" "github.com/tektoncd/hub/api/pkg/cli/cmd/get" "github.com/tektoncd/hub/api/pkg/cli/cmd/info" @@ -55,6 +56,7 @@ func Root(cli app.CLI) *cobra.Command { reinstall.Command(cli), search.Command(cli), upgrade.Command(cli), + check_upgrade.Command(cli), ) cmd.PersistentFlags().StringVar(&apiURL, "api-server", hub.URL(), "Hub API Server URL") diff --git a/api/pkg/cli/formatter/field.go b/api/pkg/cli/formatter/field.go index 2aeb20492..2cb884828 100644 --- a/api/pkg/cli/formatter/field.go +++ b/api/pkg/cli/formatter/field.go @@ -18,6 +18,7 @@ import ( "fmt" "strings" + "github.com/fatih/color" "github.com/tektoncd/hub/api/gen/http/resource/client" "github.com/tektoncd/hub/api/pkg/cli/hub" "golang.org/x/term" @@ -165,3 +166,15 @@ func FormatInstallCMD(res hub.ResourceData, resVer hub.ResourceWithVersionData, } return sb.String() } + +func DecorateAttr(attrString, message string) string { + attr := color.Reset + switch attrString { + case "underline bold": + return color.New(color.Underline).Add(color.Bold).Sprintf(message) + case "bold": + attr = color.Bold + } + + return color.New(attr).Sprintf(message) +} diff --git a/api/pkg/cli/formatter/field_test.go b/api/pkg/cli/formatter/field_test.go index 20b424c36..3abb7f964 100644 --- a/api/pkg/cli/formatter/field_test.go +++ b/api/pkg/cli/formatter/field_test.go @@ -52,10 +52,10 @@ func TestFormatTags(t *testing.T) { tagName2 := "tag2" res := []*client.TagResponseBody{ - &client.TagResponseBody{ + { Name: &tagName1, }, - &client.TagResponseBody{ + { Name: &tagName2, }, } @@ -91,3 +91,8 @@ func TestIcon(t *testing.T) { got := Icon("bullet") assert.Equal(t, "∙ ", got) } + +func TestDecorate(t *testing.T) { + got := DecorateAttr("bold", "world") + assert.Equal(t, "world", got) +} diff --git a/api/pkg/cli/hub/get_resource.go b/api/pkg/cli/hub/get_resource.go index 545b2732c..3ef5f8d6c 100644 --- a/api/pkg/cli/hub/get_resource.go +++ b/api/pkg/cli/hub/get_resource.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" rclient "github.com/tektoncd/hub/api/gen/http/resource/client" ) @@ -25,10 +26,11 @@ import ( // ResourceOption defines option associated with API to fetch a // particular resource type ResourceOption struct { - Name string - Catalog string - Version string - Kind string + Name string + Catalog string + Version string + Kind string + PipelineVersion string } // ResourceResult defines API response @@ -75,6 +77,11 @@ func (opt ResourceOption) Endpoint() string { // API: /resource//// return fmt.Sprintf("/resource/%s/%s/%s/%s", opt.Catalog, opt.Kind, opt.Name, opt.Version) } + if opt.PipelineVersion != "" { + opt.PipelineVersion = strings.TrimLeft(opt.PipelineVersion, "v") + // API: /resource///?pipelinesversion= + return fmt.Sprintf("/resource/%s/%s/%s?pipelinesversion=%s", opt.Catalog, opt.Kind, opt.Name, opt.PipelineVersion) + } // API: /resource/// return fmt.Sprintf("/resource/%s/%s/%s", opt.Catalog, opt.Kind, opt.Name) } diff --git a/api/pkg/cli/hub/get_resource_test.go b/api/pkg/cli/hub/get_resource_test.go index ae5893c6a..94151d10a 100644 --- a/api/pkg/cli/hub/get_resource_test.go +++ b/api/pkg/cli/hub/get_resource_test.go @@ -31,6 +31,10 @@ func TestGetResourceEndpoint(t *testing.T) { url := opt.Endpoint() assert.Equal(t, "/resource/tekton/task/abc", url) + opt.PipelineVersion = "0.17" + url = opt.Endpoint() + assert.Equal(t, "/resource/tekton/task/abc?pipelinesversion=0.17", url) + opt.Version = "0.1.1" url = opt.Endpoint() assert.Equal(t, "/resource/tekton/task/abc/0.1.1", url) diff --git a/api/pkg/cli/installer/action.go b/api/pkg/cli/installer/action.go index f88e85f7a..38c4a3634 100644 --- a/api/pkg/cli/installer/action.go +++ b/api/pkg/cli/installer/action.go @@ -145,6 +145,16 @@ func (i *Installer) LookupInstalled(name, kind, namespace string) (*unstructured return i.existingRes, nil } +func (i *Installer) ListInstalled(kind, namespace string) ([]unstructured.Unstructured, error) { + i.TektonPipelinesVersion() + listResources, err := i.list(kind, namespace, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + return listResources.Items, nil +} + // Update will updates an existing resource with the passed resource if exist func (i *Installer) Update(data []byte, catalog, namespace string) (*unstructured.Unstructured, []error) { return i.updateByAction(data, catalog, namespace, update) diff --git a/api/pkg/cli/installer/action_test.go b/api/pkg/cli/installer/action_test.go index 08f6fcb6d..b9f3bf469 100644 --- a/api/pkg/cli/installer/action_test.go +++ b/api/pkg/cli/installer/action_test.go @@ -18,6 +18,13 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/tektoncd/hub/api/pkg/cli/test" + cb "github.com/tektoncd/hub/api/pkg/cli/test/builder" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + pipelinev1beta1test "github.com/tektoncd/pipeline/test" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" ) const res = `--- @@ -45,3 +52,28 @@ func TestToUnstructuredAndAddLabel(t *testing.T) { addCatalogLabel(obj, "tekton") assert.Equal(t, "tekton", obj.GetLabels()[catalogLabel]) } + +func TestListInstalled(t *testing.T) { + existingTask := &v1beta1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "hub", + Labels: map[string]string{ + "hub.tekton.dev/catalog": "tekton", + "app.kubernetes.io/version": "0.1", + }}, + } + + version := "v1beta1" + dynamic := fake.NewSimpleDynamicClient(runtime.NewScheme(), cb.UnstructuredV1beta1T(existingTask, version)) + + cs, _ := test.SeedV1beta1TestData(t, pipelinev1beta1test.Data{Tasks: []*v1beta1.Task{existingTask}}) + cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task"}) + + clientSet := test.FakeClientSet(cs.Pipeline, dynamic, "hub") + + installer := New(clientSet) + list, _ := installer.ListInstalled("task", "hub") + + assert.Equal(t, len(list), 1) +} diff --git a/api/pkg/cli/installer/kube_action.go b/api/pkg/cli/installer/kube_action.go index bf6644835..cdd9d5c1b 100644 --- a/api/pkg/cli/installer/kube_action.go +++ b/api/pkg/cli/installer/kube_action.go @@ -16,6 +16,7 @@ package installer import ( "context" + "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -66,3 +67,17 @@ func (i *Installer) update(object *unstructured.Unstructured, namespace string, } return obj, nil } + +func (i *Installer) list(kind, namespace string, op metav1.ListOptions) (*unstructured.UnstructuredList, error) { + + gvrObj := schema.GroupVersionResource{Group: tektonGroup, Resource: strings.ToLower(kind) + "s"} + gvr, err := getGroupVersionResource(gvrObj, i.cs.Tekton().Discovery()) + if err != nil { + return nil, err + } + obj, err := i.cs.Dynamic().Resource(*gvr).Namespace(namespace).List(context.Background(), op) + if err != nil { + return nil, err + } + return obj, nil +}