Skip to content

Commit

Permalink
CLI: tkn hub install command
Browse files Browse the repository at this point in the history
This adds install command in tkn hub, has 2 subcommand task and
pipeline. by default latest version is installed.
'--version' flag can be used to pass a specific version.
'--from' flag can be used to pass catalog name.
If a resource is already installed with the passed name, will return
an error.

Signed-off-by: Shivam Mukhade <smukhade@redhat.com>
  • Loading branch information
SM43 committed Nov 23, 2020
1 parent be7952f commit 6dcca4f
Show file tree
Hide file tree
Showing 17 changed files with 1,197 additions and 117 deletions.
24 changes: 10 additions & 14 deletions api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,28 @@ require (
github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.5.1
github.com/tektoncd/pipeline v0.15.2
github.com/tektoncd/pipeline v0.17.1-0.20201007165454-9611f3e4509e
go.uber.org/zap v1.15.0
goa.design/goa/v3 v3.2.2
goa.design/plugins/v3 v3.1.3
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d // indirect
golang.org/x/tools v0.0.0-20200811215021-48a8ffc5b207 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/h2non/gock.v1 v1.0.15
gorm.io/driver/postgres v1.0.2
gorm.io/gorm v1.20.7
gotest.tools/v3 v3.0.2
k8s.io/apimachinery v0.17.6
k8s.io/apimachinery v0.19.0
k8s.io/client-go v11.0.1-0.20190805182717-6502b5e7b1b5+incompatible
knative.dev/pkg v0.0.0-20200702222342-ea4d6e985ba0
knative.dev/pkg v0.0.0-20200922164940-4bf40ad82aab
)

// Copied from Catlin
// Pin k8s deps to 1.17.6
// Pin k8s deps to 0.18.9
replace (
k8s.io/api => k8s.io/api v0.17.6
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.17.6
k8s.io/apimachinery => k8s.io/apimachinery v0.17.6
k8s.io/apiserver => k8s.io/apiserver v0.17.6
k8s.io/client-go => k8s.io/client-go v0.17.6
k8s.io/code-generator => k8s.io/code-generator v0.17.6
k8s.io/api => k8s.io/api v0.18.9
k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.18.9
k8s.io/apimachinery => k8s.io/apimachinery v0.18.9
k8s.io/apiserver => k8s.io/apiserver v0.18.9
k8s.io/client-go => k8s.io/client-go v0.18.9
k8s.io/code-generator => k8s.io/code-generator v0.18.9
k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20200410145947-bcb3869e6f29
)

Expand Down
410 changes: 324 additions & 86 deletions api/go.sum

Large diffs are not rendered by default.

177 changes: 177 additions & 0 deletions api/pkg/cli/cmd/install/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright © 2020 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 install

import (
"fmt"
"strings"

"github.com/spf13/cobra"
"github.com/tektoncd/hub/api/pkg/cli/app"
"github.com/tektoncd/hub/api/pkg/cli/flag"
"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"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

type options struct {
cli app.CLI
from string
version string
kind string
args []string
kc kube.Config
cs kube.ClientSet
resource hub.ResourceResult
}

var cmdExamples string = `
Install a %S of name 'foo':
tkn hub install %s foo
or
Install a %S of name 'foo' of version '0.3' from Catalog 'Tekton':
tkn hub install %s foo --version 0.3 --from tekton
`

func Command(cli app.CLI) *cobra.Command {

opts := &options{cli: cli}

cmd := &cobra.Command{
Use: "install",
Short: "Install a resource from a catalog by its kind, name and version",
Long: ``,
Annotations: map[string]string{
"commandType": "main",
},
SilenceUsage: true,
}
cmd.AddCommand(
commandForKind("task", opts),
commandForKind("pipeline", opts),
)

cmd.PersistentFlags().StringVar(&opts.from, "from", "tekton", "Name of Catalog to which resource belongs.")
cmd.PersistentFlags().StringVar(&opts.version, "version", "", "Version of Resource")

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: "Install " + kind + " by name, catalog and version",
Long: ``,
SilenceUsage: true,
Example: examples(kind),
Annotations: map[string]string{
"commandType": "main",
},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.kind = kind
opts.args = args
return opts.run()
},
}
}

func (opts *options) run() error {

if err := opts.validate(); err != nil {
return err
}

hubClient := opts.cli.Hub()
opts.resource = hubClient.GetResource(hub.ResourceOption{
Name: opts.name(),
Catalog: opts.from,
Kind: opts.kind,
Version: opts.version,
})

manifest, err := opts.resource.Manifest()
if err != nil {
return err
}

// This allows fake clients to be inserted while testing
if opts.cs == nil {
opts.cs, err = kube.NewClientSet(opts.kc)
if err != nil {
return err
}
}

installer := installer.New(opts.cs)
res, err := installer.Install(manifest, opts.cs.Namespace())
if err != nil {
return opts.checkError(err)
}

out := opts.cli.Stream().Out
return printer.New(out).String(msg(res))
}

func msg(res *unstructured.Unstructured) string {
version := res.GetLabels()["app.kubernetes.io/version"]
return fmt.Sprintf("%s %s(%s) installed in %s namespace",
strings.Title(res.GetKind()), res.GetName(), version, res.GetNamespace())
}

func (opts *options) validate() error {
return flag.ValidateVersion(opts.version)
}

func (opts *options) name() string {
return strings.TrimSpace(opts.args[0])
}

func (opts *options) checkError(err error) error {

if errors.IsAlreadyExists(err) {
return fmt.Errorf("%s %s already exists in %s namespace",
strings.Title(opts.kind), opts.name(), opts.cs.Namespace())
}

if strings.Contains(err.Error(), "mutation failed: cannot decode incoming new object") {
version, vErr := opts.resource.MinPipelinesVersion()
if vErr != nil {
return vErr
}
return fmt.Errorf("%v \nMake sure the pipeline version you are running is not lesser than %s and %s have correct spec fields",
err, version, opts.kind)
}
return err
}

func examples(kind string) string {
replacer := strings.NewReplacer("%s", kind, "%S", strings.Title(kind))
return replacer.Replace(cmdExamples)
}
148 changes: 148 additions & 0 deletions api/pkg/cli/cmd/install/install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright © 2020 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 install

import (
"bytes"
"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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic/fake"
)

var resVersion = &res.ResourceVersionData{
ID: 11,
Version: "0.3",
DisplayName: "foo-bar",
Description: "v0.3 Task to run foo",
MinPipelinesVersion: "0.12",
RawURL: "http://raw.github.url/foo/0.3/foo.yaml",
WebURL: "http://web.github.com/foo/0.3/foo.yaml",
UpdatedAt: "2020-01-01 12:00:00 +0000 UTC",
Resource: &res.ResourceData{
ID: 1,
Name: "foo",
Kind: "Task",
Catalog: &res.Catalog{
ID: 1,
Name: "tekton",
Type: "community",
},
Rating: 4.8,
Tags: []*res.Tag{
&res.Tag{
ID: 3,
Name: "cli",
},
},
},
}

func TestInstall_NewResource(t *testing.T) {
cli := test.NewCLI()

defer gock.Off()

resVersion := &res.ResourceVersion{Data: resVersion}
res := res.NewViewedResourceVersion(resVersion, "default")
gock.New(test.API).
Get("/resource/tekton/task/foo/0.3").
Reply(200).
JSON(&res.Projected)

gock.New("http://raw.github.url").
Get("/foo/0.3/foo.yaml").
Reply(200).
File("./testdata/foo-v0.3.yaml")

buf := new(bytes.Buffer)
cli.SetStream(buf, buf)

version := "v1beta1"
dynamic := fake.NewSimpleDynamicClient(runtime.NewScheme())

cs, _ := test.SeedV1beta1TestData(t, pipelinev1beta1test.Data{})
cs.Pipeline.Resources = cb.APIResourceList(version, []string{"task"})

opts := &options{
cs: test.FakeClientSet(cs.Pipeline, dynamic, "hub"),
cli: cli,
kind: "task",
args: []string{"foo"},
from: "tekton",
version: "0.3",
}

err := opts.run()
assert.NoError(t, err)
assert.Equal(t, "Task foo(0.1) installed in hub namespace\n", buf.String())
assert.Equal(t, gock.IsDone(), true)
}

func TestInstall_ResourceAlreadyExistError(t *testing.T) {
cli := test.NewCLI()

defer gock.Off()

resVersion := &res.ResourceVersion{Data: resVersion}
res := res.NewViewedResourceVersion(resVersion, "default")
gock.New(test.API).
Get("/resource/tekton/task/foo/0.3").
Reply(200).
JSON(&res.Projected)

gock.New("http://raw.github.url").
Get("/foo/0.3/foo.yaml").
Reply(200).
File("./testdata/foo-v0.3.yaml")

buf := new(bytes.Buffer)
cli.SetStream(buf, buf)

existingTask := &v1beta1.Task{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "hub",
},
}

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",
args: []string{"foo"},
from: "tekton",
version: "0.3",
}

err := opts.run()
assert.Error(t, err)
assert.EqualError(t, err, "Task foo already exists in hub namespace")
assert.Equal(t, gock.IsDone(), true)
}
14 changes: 14 additions & 0 deletions api/pkg/cli/cmd/install/testdata/foo-v0.3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: foo
labels:
app.kubernetes.io/version: '0.3'
annotations:
tekton.dev/pipelines.minVersion: '0.13.1'
tekton.dev/tags: cli
tekton.dev/displayName: 'foo-bar'
spec:
description: >-
v0.3 Task to run foo
Loading

0 comments on commit 6dcca4f

Please sign in to comment.