Repository: openshift-pipelines/pipelines-as-code
Pull request #5 (2021-04-28 12:26:03)

Add console url link to the finished PR
If it's installed and available, show the console url link when the PR
is finished.

Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>



Patch:
diff --git a/pkg/cli/interface.go b/pkg/cli/interface.go
index a8dbc5ca..f53b9c14 100644
--- a/pkg/cli/interface.go
+++ b/pkg/cli/interface.go
@@ -9,6 +9,7 @@ import (
 	"github.com/tektoncd/hub/api/pkg/cli/hub"
 	tektonversioned "github.com/tektoncd/pipeline/pkg/client/clientset/versioned"
 	"go.uber.org/zap"
+	"k8s.io/client-go/dynamic"
 	k8s "k8s.io/client-go/kubernetes"
 )
 
@@ -21,6 +22,7 @@ type Clients struct {
 	HTTPClient     http.Client
 	Log            *zap.SugaredLogger
 	GithubClient   webvcs.GithubVCS
+	Dynamic        dynamic.Interface
 }
 
 type Params interface {
diff --git a/pkg/cli/params.go b/pkg/cli/params.go
index 54bb2f2a..a51a4a6f 100644
--- a/pkg/cli/params.go
+++ b/pkg/cli/params.go
@@ -11,6 +11,7 @@ import (
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/tektoncli"
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/webvcs"
 	"go.uber.org/zap"
+	"k8s.io/client-go/dynamic"
 	k8s "k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
 	"k8s.io/client-go/tools/clientcmd"
@@ -44,6 +45,15 @@ func (p *PacParams) kubeClient(config *rest.Config) (k8s.Interface, error) {
 	return k8scs, nil
 }
 
+func (p *PacParams) dynamicClient(config *rest.Config) (dynamic.Interface, error) {
+	dynamicClient, err := dynamic.NewForConfig(config)
+	if err != nil {
+		return nil, errors.Wrapf(err, "failed to create dynamic client from config")
+
+	}
+	return dynamicClient, err
+}
+
 func (p *PacParams) config() (*rest.Config, error) {
 	loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
 	if p.kubeConfigPath != "" {
@@ -163,6 +173,11 @@ func (p *PacParams) Clients() (*Clients, error) {
 		return nil, err
 	}
 
+	dynamic, err := p.dynamicClient(config)
+	if err != nil {
+		return nil, err
+	}
+
 	p.clients = &Clients{
 		Tekton:         tekton,
 		TektonCli:      tektoncli,
@@ -171,6 +186,7 @@ func (p *PacParams) Clients() (*Clients, error) {
 		Log:            logger,
 		GithubClient:   ghClient,
 		Hub:            hub,
+		Dynamic:        dynamic,
 	}
 
 	return p.clients, nil
diff --git a/pkg/kubernetes/consoleui.go b/pkg/kubernetes/consoleui.go
new file mode 100644
index 00000000..acae81d4
--- /dev/null
+++ b/pkg/kubernetes/consoleui.go
@@ -0,0 +1,57 @@
+package kubernetes
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+const (
+	openShiftConsoleNS        = "openshift-console"
+	openShiftConsoleRouteName = "console"
+	openShiftPipelineViewURL  = "https://%s/k8s/ns/%s/tekton.dev~v1beta1~PipelineRun/%s/logs"
+	openShiftRouteGroup       = "route.openshift.io"
+	openShiftRouteVersion     = "v1"
+	openShiftRouteResource    = "routes"
+)
+
+// getOpenshiftConsole use dynamic client to get the route of the openshit
+// console where we can point to.
+func getOpenshiftConsole(cs *cli.Clients, ns, pr string) (string, error) {
+	gvr := schema.GroupVersionResource{Group: openShiftRouteGroup, Version: openShiftRouteVersion, Resource: openShiftRouteResource}
+	route, err := cs.Dynamic.Resource(gvr).Namespace(openShiftConsoleNS).Get(context.Background(), openShiftConsoleRouteName, metav1.GetOptions{})
+	if err != nil {
+		return "", err
+	}
+
+	spec, ok := route.Object["spec"].(map[string]interface{})
+	if !ok {
+		// this condition is satisfied if there's no metadata at all in the provided CRD
+		return "", fmt.Errorf("couldn't find \"spec\" in the OpenShift Console route")
+	}
+
+	host, ok := spec["host"].(string)
+	if !ok {
+		// this condition is satisfied if there's no metadata at all in the provided CRD
+		return "", fmt.Errorf("couldn't find \"spec.host\" in the OpenShift Console route")
+	}
+
+	return fmt.Sprintf(openShiftPipelineViewURL, host, ns, pr), err
+}
+
+// GetConsoleURL Get a Console URL, OpenShift Console or Tekton Dashboard.
+// don't error if we can't find it.
+func GetConsoleUI(cs *cli.Clients, ns, pr string) string {
+	var url string
+	var err error
+	if url, err = getOpenshiftConsole(cs, ns, pr); err != nil {
+		cs.Log.Debug("cannot find an openshift console on this cluster: %v", err)
+	}
+
+	cs.Log.Info(url)
+
+	return url
+}
diff --git a/pkg/pipelineascode/pipelineascode.go b/pkg/pipelineascode/pipelineascode.go
index 2ef0fd2c..c03658c2 100644
--- a/pkg/pipelineascode/pipelineascode.go
+++ b/pkg/pipelineascode/pipelineascode.go
@@ -65,7 +65,6 @@ func Run(cs *cli.Clients, runinfo *webvcs.RunInfo) error {
 			"😿 Could not find a <b>.tekton/</b> directory for this repository", "https://tenor.com/search/sad-cat-gifs")
 		return err
 	}
-
 	cs.Log.Infow("Loading payload",
 		"url", runinfo.URL,
 		"branch", runinfo.Branch,
diff --git a/pkg/pipelineascode/status.go b/pkg/pipelineascode/status.go
index 2ce1d655..8f09e2f7 100644
--- a/pkg/pipelineascode/status.go
+++ b/pkg/pipelineascode/status.go
@@ -8,6 +8,7 @@ import (
 	"text/template"
 
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/cli"
+	"github.com/openshift-pipelines/pipelines-as-code/pkg/kubernetes"
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/webvcs"
 	"github.com/tektoncd/cli/pkg/formatted"
 	tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
@@ -146,6 +147,8 @@ func postFinalStatus(ctx context.Context, cs *cli.Clients, runinfo *webvcs.RunIn
 		return pr, err
 	}
 
+	webConsoleURL := kubernetes.GetConsoleUI(cs, namespace, pr.Name)
+
 	taskStatus, err := statusOfAllTaskListForCheckRun(pr)
 	if err != nil {
 		return pr, err
@@ -165,7 +168,7 @@ func postFinalStatus(ctx context.Context, cs *cli.Clients, runinfo *webvcs.RunIn
 
 	_, err = cs.GithubClient.CreateStatus(runinfo,
 		"completed", pipelineRunStatus(pr),
-		outputBuffer.String(), "")
+		outputBuffer.String(), webConsoleURL)
 
 	return pr, err
 }
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 7b3274a8..2b6ac1ee 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -355,6 +355,8 @@ github.com/tektoncd/cli/pkg/taskrun/description
 github.com/tektoncd/cli/pkg/taskrun/list
 github.com/tektoncd/cli/pkg/taskrun/sort
 github.com/tektoncd/cli/pkg/test
+github.com/tektoncd/cli/pkg/test/dynamic
+github.com/tektoncd/cli/pkg/test/dynamic/clientset
 # github.com/tektoncd/hub/api v0.0.0-20210208113044-f2a63f81502c
 ## explicit
 github.com/tektoncd/hub/api/gen/http/resource/client


Test patch:
diff --git a/pkg/pipelineascode/pipelineascode_test.go b/pkg/pipelineascode/pipelineascode_test.go
index f6310c1e..aff917b1 100644
--- a/pkg/pipelineascode/pipelineascode_test.go
+++ b/pkg/pipelineascode/pipelineascode_test.go
@@ -9,6 +9,8 @@ import (
 	"testing"
 	"time"
 
+	testDynamic "github.com/tektoncd/cli/pkg/test/dynamic"
+
 	"github.com/google/go-github/v34/github"
 	"github.com/jonboulle/clockwork"
 	"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
@@ -247,6 +249,9 @@ func TestRun(t *testing.T) {
 	observer, log := zapobserver.New(zap.InfoLevel)
 	logger := zap.New(observer).Sugar()
 
+	tdc := testDynamic.Options{}
+	dc, _ := tdc.Client()
+
 	cs := &cli.Clients{
 		GithubClient:   gcvs,
 		PipelineAsCode: stdata.PipelineAsCode,
@@ -254,6 +259,7 @@ func TestRun(t *testing.T) {
 		Kube:           stdata.Kube,
 		Tekton:         stdata.Pipeline,
 		TektonCli:      &FakeTektonClient{logOutput: "HELLO MOTO", describeOutput: "DESCRIBE ZEMODO"},
+		Dynamic:        dc,
 	}
 
 	err = Run(cs, runinfo)
diff --git a/vendor/github.com/tektoncd/cli/pkg/test/dynamic/clientset/clientset.go b/vendor/github.com/tektoncd/cli/pkg/test/dynamic/clientset/clientset.go
new file mode 100644
index 00000000..4476a31f
--- /dev/null
+++ b/vendor/github.com/tektoncd/cli/pkg/test/dynamic/clientset/clientset.go
@@ -0,0 +1,57 @@
+// 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 clientset
+
+import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+// Clientset maps GroupVersionResources to underlying dynamic clients. If the
+// GVR does not exist, operations will return an error.
+type Clientset struct {
+	config map[schema.GroupVersionResource]dynamic.Interface
+}
+
+// Option defines optional configuration for the Clientset. Most commonly used
+// to initialize extensions.
+type Option func(*Clientset)
+
+// New creates a new Clientset with the provided options.
+func New(opts ...Option) *Clientset {
+	cs := &Clientset{
+		config: make(map[schema.GroupVersionResource]dynamic.Interface),
+	}
+	for _, o := range opts {
+		o(cs)
+	}
+
+	return cs
+}
+
+// Add adds a new mapping for the given resource.
+func (r *Clientset) Add(resource schema.GroupVersionResource, client dynamic.Interface) {
+	r.config[resource] = client
+}
+
+// Resource returns the dynamic Resource for the given GVR. If not configured,
+// an error resource is returned.
+func (r *Clientset) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface {
+	i, ok := r.config[resource]
+	if !ok {
+		return newErrorResource(resource)
+	}
+	return i.Resource(resource)
+}
diff --git a/vendor/github.com/tektoncd/cli/pkg/test/dynamic/clientset/errorclient.go b/vendor/github.com/tektoncd/cli/pkg/test/dynamic/clientset/errorclient.go
new file mode 100644
index 00000000..fa65a9cd
--- /dev/null
+++ b/vendor/github.com/tektoncd/cli/pkg/test/dynamic/clientset/errorclient.go
@@ -0,0 +1,79 @@
+// 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 clientset
+
+import (
+	"context"
+	"fmt"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/dynamic"
+)
+
+func newErrorResource(r schema.GroupVersionResource) errorResourceInterface {
+	return errorResourceInterface{resource: r}
+}
+
+type errorResourceInterface struct {
+	resource schema.GroupVersionResource
+}
+
+func (i errorResourceInterface) Namespace(string) dynamic.ResourceInterface {
+	return i
+}
+
+func (i errorResourceInterface) err() error {
+	return fmt.Errorf("resource %+v not supported", i.resource)
+}
+
+func (i errorResourceInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
+	return nil, i.err()
+}
+
+func (i errorResourceInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) {
+	return nil, i.err()
+}
+
+func (i errorResourceInterface) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error) {
+	return nil, i.err()
+}
+
+func (i errorResourceInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
+	return i.err()
+}
+
+func (i errorResourceInterface) DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error {
+	return i.err()
+}
+
+func (i errorResourceInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
+	return nil, i.err()
+}
+
+func (i errorResourceInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
+	return nil, i.err()
+}
+
+func (i errorResourceInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
+	return nil, i.err()
+}
+
+func (i errorResourceInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) {
+	return nil, i.err()
+}
diff --git a/vendor/github.com/tektoncd/cli/pkg/test/dynamic/clientset/tekton.go b/vendor/github.com/tektoncd/cli/pkg/test/dynamic/clientset/tekton.go
new file mode 100644
index 00000000..e102ead2
--- /dev/null
+++ b/vendor/github.com/tektoncd/cli/pkg/test/dynamic/clientset/tekton.go
@@ -0,0 +1,42 @@
+// 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 clientset
+
+import (
+	"github.com/tektoncd/pipeline/pkg/apis/pipeline"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/dynamic"
+)
+
+var allowedTektonTypes = map[string][]string{
+	"v1alpha1": {"pipelineresources", "pipelineruns", "taskruns", "pipelines", "clustertasks", "tasks", "conditions"},
+	"v1beta1":  {"pipelineresources", "pipelineruns", "taskruns", "pipelines", "clustertasks", "tasks", "conditions"},
+}
+
+// WithClient adds Tekton related clients to the Dynamic client.
+func WithClient(client dynamic.Interface) Option {
+	return func(cs *Clientset) {
+		for version, resources := range allowedTektonTypes {
+			for _, resource := range resources {
+				r := schema.GroupVersionResource{
+					Group:    pipeline.GroupName,
+					Version:  version,
+					Resource: resource,
+				}
+				cs.Add(r, client)
+			}
+		}
+	}
+}
diff --git a/vendor/github.com/tektoncd/cli/pkg/test/dynamic/dynamictestclinet.go b/vendor/github.com/tektoncd/cli/pkg/test/dynamic/dynamictestclinet.go
new file mode 100644
index 00000000..73dead9c
--- /dev/null
+++ b/vendor/github.com/tektoncd/cli/pkg/test/dynamic/dynamictestclinet.go
@@ -0,0 +1,59 @@
+// 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 dynamic
+
+import (
+	"github.com/tektoncd/cli/pkg/test/dynamic/clientset"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/watch"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/client-go/dynamic/fake"
+	k8stest "k8s.io/client-go/testing"
+)
+
+type Options struct {
+	WatchResource    string
+	Watcher          watch.Interface
+	WatchReactionFun k8stest.WatchReactionFunc
+	PrependReactors  []PrependOpt
+	AddReactorRes    string
+	AddReactorVerb   string
+	AddReactorFun    k8stest.ReactionFunc
+}
+
+type PrependOpt struct {
+	Resource string
+	Verb     string
+	Action   k8stest.ReactionFunc
+}
+
+func (opt *Options) Client(objects ...runtime.Object) (dynamic.Interface, error) {
+	dynamicClient := fake.NewSimpleDynamicClient(runtime.NewScheme(), objects...)
+	if opt.Watcher != nil {
+		dynamicClient.PrependWatchReactor(opt.WatchResource, k8stest.DefaultWatchReactor(opt.Watcher, nil))
+	}
+	if opt.WatchReactionFun != nil {
+		dynamicClient.PrependWatchReactor(opt.WatchResource, opt.WatchReactionFun)
+	}
+	if len(opt.PrependReactors) != 0 {
+		for _, res := range opt.PrependReactors {
+			dynamicClient.PrependReactor(res.Verb, res.Resource, res.Action)
+		}
+	}
+	if opt.AddReactorFun != nil {
+		dynamicClient.AddReactor(opt.AddReactorVerb, opt.AddReactorRes, opt.AddReactorFun)
+	}
+	return clientset.New(clientset.WithClient(dynamicClient)), nil
+}