Repository: tektoncd/operator (go)

Issue:
Add External Logs feature for dashboard in Tekton Config
### Feature request

Request to add a feature in Tekton Config CRD to allow configuration of external logs for tekton dashboard.

### Use case

I am currently installing Tekton dashboard using Tekton operator. It would be good if, tekton config CRD can accept external logs as a spec field that will enable to configure URL for external logs, to avoid manual update of the configMap.


apiVersion: operator.tekton.dev/v1alpha1
kind: TektonConfig
metadata:
  name: config
  namespace: tekton-operator
spec:
  profile: all
  targetNamespace: tekton-pipelines
  dashboard:
    readonly: true
    externalLogs: <<URL>>


Currently there is no spec to configure external logs in TektonConfig.


Pull request description:
Provides option to configure external logs for dashboard
```
apiVersion: operator.tekton.dev/v1alpha1
kind: TektonConfig
metadata:
  name: config
spec:
  dashboard:
    readonly: false
    external-logs: <>
```

Closes #623 

Signed-off-by: Shivam Mukhade <smukhade@redhat.com>

# Changes

<!-- 🎉🎉🎉 Thank you for the PR!!! 🎉🎉🎉 -->

<!-- Describe your changes here- ideally you can get that description straight from
your descriptive commit message(s)! -->

# Submitter Checklist

These are the criteria that every PR should meet, please check them off as you
review them:

- [ ] Run `make test lint` before submitting a PR
- [ ] Includes [tests](https://github.com/tektoncd/community/blob/master/standards.md#principles) (if functionality changed/added)
- [ ] Includes [docs](https://github.com/tektoncd/community/blob/master/standards.md#principles) (if user facing)
- [ ] Commit messages follow [commit message best practices](https://github.com/tektoncd/community/blob/master/standards.md#commit-messages)

_See [the contribution guide](https://github.com/tektoncd/operator/blob/master/CONTRIBUTING.md) for more details._

# Release Notes

<!--
Describe any user facing changes here, or delete this block.

Examples of user facing changes:
- API changes
- Bug fixes
- Any changes in behavior
- Changes requiring upgrade notices or deprecation warnings

For pull requests with a release note:

```release-note
Your release note here
```

For pull requests that require additional action from users switching to the new release, include the string "action required" (case insensitive) in the release note:

```release-note
action required: your release note here
```

For pull requests that don't need to be mentioned at release time, use the `/release-note-none` Prow command to add the `release-note-none` label to the PR. You can also write the string "NONE" as a release note in your PR description:

```release-note
NONE
```
-->


Patch:
diff --git a/README.md b/README.md
index 55b311d8a..16894d387 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,10 @@
 [![Go Report Card](https://goreportcard.com/badge/tektoncd/operator)](https://goreportcard.com/report/tektoncd/operator)
 [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/6548/badge)](https://bestpractices.coreinfrastructure.org/projects/6548)
 
+<p align="center">
+<img width="250" height="325"  src="tekton-operator.png" alt="Operator Icon" title="Operator Icon"></img>
+</p>
+
 The quickest and easiest way to install, upgrade and manage TektonCD [Pipelines](https://github.com/tektoncd/pipeline),
 [Dashboard](https://github.com/tektoncd/dashboard), [Triggers](https://github.com/tektoncd/triggers)
 on any Kubernetes Cluster.
diff --git a/docs/TektonDashboard.md b/docs/TektonDashboard.md
index 4582c3bf3..be562f9b1 100644
--- a/docs/TektonDashboard.md
+++ b/docs/TektonDashboard.md
@@ -27,6 +27,10 @@ You can install this component using [TektonConfig](./TektonConfig.md) by choosi
 
 - `readonly` (Default: `false`)
 
-    If set to true, installs the Dashboard in read-only mode.
+  If set to true, installs the Dashboard in read-only mode.
+
+- `external-logs`
+
+  External URL from which to fetch logs when logs are not available in the cluster  
 
 [dashboard]:https://github.com/tektoncd/dashboard
diff --git a/pkg/apis/operator/v1alpha1/tektondashboard_types.go b/pkg/apis/operator/v1alpha1/tektondashboard_types.go
index 96f21028b..b06e00f93 100644
--- a/pkg/apis/operator/v1alpha1/tektondashboard_types.go
+++ b/pkg/apis/operator/v1alpha1/tektondashboard_types.go
@@ -88,4 +88,6 @@ type Dashboard struct {
 type DashboardProperties struct {
 	// Readonly when set to true configures the Tekton dashboard in read-only mode
 	Readonly bool `json:"readonly"`
+	// +optional
+	ExternalLogs string `json:"external-logs,omitempty"`
 }
diff --git a/pkg/reconciler/common/transformers.go b/pkg/reconciler/common/transformers.go
index ea580601d..86f42ee42 100644
--- a/pkg/reconciler/common/transformers.go
+++ b/pkg/reconciler/common/transformers.go
@@ -777,3 +777,33 @@ func CopyConfigMap(configMapName string, expectedValues map[string]string) mf.Tr
 		return nil
 	}
 }
+
+func ReplaceDeploymentArg(deploymentName, existingArg, newArg string) mf.Transformer {
+	return func(u *unstructured.Unstructured) error {
+		if u.GetKind() != "Deployment" {
+			return nil
+		}
+		if u.GetName() != deploymentName {
+			return nil
+		}
+
+		d := &appsv1.Deployment{}
+		err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, d)
+		if err != nil {
+			return err
+		}
+
+		for i, arg := range d.Spec.Template.Spec.Containers[0].Args {
+			if arg == existingArg {
+				d.Spec.Template.Spec.Containers[0].Args[i] = newArg
+			}
+		}
+
+		unstrObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d)
+		if err != nil {
+			return err
+		}
+		u.SetUnstructuredContent(unstrObj)
+		return nil
+	}
+}
diff --git a/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go b/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go
index be4c9d1ed..90163f05f 100644
--- a/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go
+++ b/pkg/reconciler/kubernetes/tektonconfig/extension/dashboard.go
@@ -19,7 +19,6 @@ package extension
 import (
 	"context"
 	"fmt"
-	"knative.dev/pkg/apis"
 	"reflect"
 	"strings"
 
@@ -27,6 +26,7 @@ import (
 	op "github.com/tektoncd/operator/pkg/client/clientset/versioned/typed/operator/v1alpha1"
 	apierrs "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"knative.dev/pkg/apis"
 )
 
 func EnsureTektonDashboardExists(ctx context.Context, clients op.TektonDashboardInterface, config *v1alpha1.TektonConfig) (*v1alpha1.TektonDashboard, error) {
diff --git a/pkg/reconciler/kubernetes/tektondashboard/finalize.go b/pkg/reconciler/kubernetes/tektondashboard/finalize.go
index f385a7610..78d7d13f8 100644
--- a/pkg/reconciler/kubernetes/tektondashboard/finalize.go
+++ b/pkg/reconciler/kubernetes/tektondashboard/finalize.go
@@ -18,6 +18,7 @@ package tektondashboard
 
 import (
 	"context"
+
 	mf "github.com/manifestival/manifestival"
 	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
 	"knative.dev/pkg/logging"
diff --git a/pkg/reconciler/kubernetes/tektondashboard/transform.go b/pkg/reconciler/kubernetes/tektondashboard/transform.go
index 34e3fcb97..98bab42e9 100644
--- a/pkg/reconciler/kubernetes/tektondashboard/transform.go
+++ b/pkg/reconciler/kubernetes/tektondashboard/transform.go
@@ -18,15 +18,19 @@ package tektondashboard
 
 import (
 	"context"
+
 	mf "github.com/manifestival/manifestival"
 	"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
 	"github.com/tektoncd/operator/pkg/reconciler/common"
 	"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektoninstallerset/client"
 )
 
+const (
+	externalLogsArg = "--external-logs="
+)
+
 func filterAndTransform(extension common.Extension) client.FilterAndTransform {
 	return func(ctx context.Context, manifest *mf.Manifest, comp v1alpha1.TektonComponent) (*mf.Manifest, error) {
-
 		dashboard := comp.(*v1alpha1.TektonDashboard)
 		trns := extension.Transformers(dashboard)
 		extra := []mf.Transformer{
@@ -36,10 +40,13 @@ func filterAndTransform(extension common.Extension) client.FilterAndTransform {
 			common.AddDeploymentRestrictedPSA(),
 		}
 		trns = append(trns, extra...)
+		if dashboard.Spec.ExternalLogs != "" {
+			updatedExternalLogsArg := externalLogsArg + dashboard.Spec.ExternalLogs
+			trns = append(trns, common.ReplaceDeploymentArg("tekton-dashboard", externalLogsArg, updatedExternalLogsArg))
+		}
 		if err := common.Transform(ctx, manifest, dashboard, trns...); err != nil {
 			return nil, err
 		}
-
 		return manifest, nil
 	}
 }


Test patch:
diff --git a/pkg/reconciler/common/testdata/test-dashboard-deployment.yaml b/pkg/reconciler/common/testdata/test-dashboard-deployment.yaml
new file mode 100644
index 000000000..1349487c1
--- /dev/null
+++ b/pkg/reconciler/common/testdata/test-dashboard-deployment.yaml
@@ -0,0 +1,76 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app: tekton-dashboard
+    app.kubernetes.io/component: dashboard
+    app.kubernetes.io/instance: default
+    app.kubernetes.io/name: dashboard
+    app.kubernetes.io/part-of: tekton-dashboard
+    app.kubernetes.io/version: v0.30.0
+    dashboard.tekton.dev/release: v0.30.0
+    version: v0.30.0
+  name: tekton-dashboard
+  namespace: tekton-pipelines
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app.kubernetes.io/component: dashboard
+      app.kubernetes.io/instance: default
+      app.kubernetes.io/name: dashboard
+      app.kubernetes.io/part-of: tekton-dashboard
+  template:
+    metadata:
+      labels:
+        app: tekton-dashboard
+        app.kubernetes.io/component: dashboard
+        app.kubernetes.io/instance: default
+        app.kubernetes.io/name: dashboard
+        app.kubernetes.io/part-of: tekton-dashboard
+        app.kubernetes.io/version: v0.30.0
+      name: tekton-dashboard
+    spec:
+      containers:
+        - args:
+            - --port=9097
+            - --logout-url=
+            - --pipelines-namespace=tekton-pipelines
+            - --triggers-namespace=tekton-pipelines
+            - --read-only=true
+            - --log-level=info
+            - --log-format=json
+            - --namespace=
+            - --stream-logs=true
+            - --external-logs=
+          env:
+            - name: INSTALLED_NAMESPACE
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.namespace
+          image: gcr.io/tekton-releases/github.com/tektoncd/dashboard/cmd/dashboard:v0.30.0@sha256:85f7d38086fadb07556052ce873d44861c29ef690f47735f32d7e6a153ca8a92
+          livenessProbe:
+            httpGet:
+              path: /health
+              port: 9097
+          name: tekton-dashboard
+          ports:
+            - containerPort: 9097
+          readinessProbe:
+            httpGet:
+              path: /readiness
+              port: 9097
+          securityContext:
+            allowPrivilegeEscalation: false
+            capabilities:
+              drop:
+                - ALL
+            runAsGroup: 65532
+            runAsNonRoot: true
+            runAsUser: 65532
+            seccompProfile:
+              type: RuntimeDefault
+      nodeSelector:
+        kubernetes.io/os: linux
+      serviceAccountName: tekton-dashboard
+      volumes: []
diff --git a/pkg/reconciler/common/transformers_test.go b/pkg/reconciler/common/transformers_test.go
index 30be23ee5..6c5835fa9 100644
--- a/pkg/reconciler/common/transformers_test.go
+++ b/pkg/reconciler/common/transformers_test.go
@@ -602,3 +602,32 @@ func TestCopyConfigMapValues(t *testing.T) {
 	assert.Equal(t, cm.Data["ignore-me-field"], "")
 
 }
+
+func TestReplaceDeploymentArg(t *testing.T) {
+	testData := path.Join("testdata", "test-dashboard-deployment.yaml")
+	manifest, err := mf.ManifestFrom(mf.Recursive(testData))
+	assert.NilError(t, err)
+
+	existingArg := "--external-logs="
+	newArg := "--external-logs=abc"
+
+	newManifest, err := manifest.Transform(ReplaceDeploymentArg("tekton-dashboard", existingArg, newArg))
+	assert.NilError(t, err)
+
+	got := &appsv1.Deployment{}
+	err = runtime.DefaultUnstructuredConverter.FromUnstructured(newManifest.Resources()[0].Object, got)
+	if err != nil {
+		t.Errorf("failed to load deployment yaml")
+	}
+
+	found := false
+	for _, a := range got.Spec.Template.Spec.Containers[0].Args {
+		if a == newArg {
+			found = true
+		}
+	}
+
+	if !found {
+		t.Fatalf("failed to find new arg in deployment")
+	}
+}