diff --git a/api/go.mod b/api/go.mod index 80738806b4..eb2d189f81 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 412edf6af6..de797e07c0 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 0000000000..322e26c984 --- /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 0000000000..85111b927b --- /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 0000000000..48f5b14652 --- /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 0000000000..190f247b54 --- /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 0000000000..55d6afeef0 --- /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 0000000000..b390423ff6 --- /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 0000000000..1c423e163e --- /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 0000000000..861969bcaa --- /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 805385f30e..08b361c8e7 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 87ecf8e76a..2c0216913b 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 2aeb204929..2cb8848282 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 20b424c36d..3abb7f964d 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 545b2732c4..3ef5f8d6c3 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 ae5893c6a1..94151d10af 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 f88e85f7a5..38c4a3634d 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 08f6fcb6d0..b9f3bf469e 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 bf66448359..cdd9d5c1b4 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 +}