Repository: jenkins-x/lighthouse
Pull request #1338 (2021-07-01 15:46:45)

fix: allow external plugins to be disabled


Patch:
diff --git a/charts/lighthouse/templates/config-external-plugins.yaml b/charts/lighthouse/templates/config-external-plugins.yaml
new file mode 100644
index 00000000..5aaa2308
--- /dev/null
+++ b/charts/lighthouse/templates/config-external-plugins.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: lighthouse-external-plugins
+  labels:
+    app: {{ template "fullname" . }}
+    chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
+    release: "{{ .Release.Name }}"
+data:
+  config.yaml: |-
+{{ toYaml .Values.externalPlugins | indent 4 }}
diff --git a/charts/lighthouse/values.yaml b/charts/lighthouse/values.yaml
index 2d6a31e5..e5e8bbd2 100644
--- a/charts/lighthouse/values.yaml
+++ b/charts/lighthouse/values.yaml
@@ -60,6 +60,19 @@ image:
 env:
   JX_DEFAULT_IMAGE: ""
 
+
+externalPlugins:
+- name: cd-indicators
+  requiredResources:
+  - kind: Service
+    namespace: jx
+    name: cd-indicators
+- name: lighthouse-webui-plugin
+  requiredResources:
+  - kind: Service
+    namespace: jx
+    name: lighthouse-webui-plugin
+
 gcJobs:
   # gcJobs.maxAge -- Max age from which `LighthouseJob`s will be deleted
   maxAge: 168h
diff --git a/pkg/externalplugincfg/load.go b/pkg/externalplugincfg/load.go
new file mode 100644
index 00000000..9e0dde44
--- /dev/null
+++ b/pkg/externalplugincfg/load.go
@@ -0,0 +1,91 @@
+package externalplugincfg
+
+import (
+	"context"
+	"github.com/jenkins-x/lighthouse/pkg/util"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+	"sigs.k8s.io/yaml"
+	"strings"
+)
+
+func LoadDisabledPlugins(l *logrus.Entry, client kubernetes.Interface, namespace string) ([]string, error) {
+	answer := []string{}
+
+	ctx := context.TODO()
+
+	l = l.WithFields(map[string]interface{}{
+		"Namespace": namespace,
+		"ConfigMap": ConfigMapName,
+	})
+	cm, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, ConfigMapName, metav1.GetOptions{})
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			l.Warnf("no ConfigMap for the external plugin configuration")
+			return answer, nil
+		}
+		if err != nil {
+			return nil, errors.Wrapf(err, "failed to load ConfigMap %s in namespace %s", ConfigMapName, namespace)
+		}
+	}
+	text := ""
+	if cm != nil && cm.Data != nil {
+		text = cm.Data["config.yaml"]
+	}
+	if text == "" {
+		l.Warnf("no config.yaml entry in external plugin ConfigMap")
+		return answer, nil
+	}
+
+	var externalPlugins []ExternalPlugin
+	err = yaml.Unmarshal([]byte(text), &externalPlugins)
+	if err != nil {
+		return answer, errors.Wrapf(err, "failed to unmarshal external plugin configuration")
+	}
+
+	for _, p := range externalPlugins {
+		if util.StringArrayIndex(answer, p.Name) >= 0 {
+			continue
+		}
+		for _, r := range p.RequiredResources {
+			exists, err := CheckResourceExists(ctx, client, namespace, r)
+			if err != nil {
+				return answer, errors.Wrapf(err, "failed to check if resource exists %s", r.String())
+			}
+			if !exists {
+				l.WithField("ExternalPlugin", p.Name).Infof("disabling external plugin as resource is not available %s", r.String())
+				answer = append(answer, p.Name)
+				break
+			}
+		}
+	}
+	return answer, nil
+}
+
+// CheckResourceExists checks if a service exists
+func CheckResourceExists(ctx context.Context, client kubernetes.Interface, namespace string, r Resource) (bool, error) {
+	k := strings.ToLower(r.Kind)
+	ns := r.Namespace
+	if ns == "" {
+		ns = namespace
+	}
+	name := r.Name
+	if name == "" {
+		return true, nil
+	}
+	if k == "service" {
+		_, err := client.CoreV1().Services(ns).Get(ctx, name, metav1.GetOptions{})
+		if apierrors.IsNotFound(err) {
+			return false, nil
+		}
+		if err != nil {
+			return false, errors.Wrapf(err, "failed to find Service %s/%s", ns, name)
+		}
+		return true, nil
+	}
+	// TODO
+	return true, nil
+}
diff --git a/pkg/externalplugincfg/types.go b/pkg/externalplugincfg/types.go
new file mode 100644
index 00000000..228cd090
--- /dev/null
+++ b/pkg/externalplugincfg/types.go
@@ -0,0 +1,32 @@
+package externalplugincfg
+
+const (
+	// ConfigMapName name of the config map for external plugins
+	ConfigMapName = "lighthouse-external-plugins"
+)
+
+// ExternalPlugin holds configuration for registering an external
+// plugin in prow.
+type ExternalPlugin struct {
+	// Name of the plugin.
+	Name string `json:"name"`
+	// RequiredResources the kubernetes resources required to enable this external plugin
+	RequiredResources []Resource `json:"requiredResources,omitempty"`
+}
+
+// Resource represents a kubernetes resource
+type Resource struct {
+	// Kind of the resource.
+	Kind string `json:"kind"`
+	// Name of the resource.
+	Name string `json:"name"`
+	// Namespace of the resource.
+	Namespace string `json:"namespace"`
+}
+
+func (r *Resource) String() string {
+	if r.Namespace != "" {
+		return r.Kind + "/" + r.Namespace + "/" + r.Name
+	}
+	return r.Kind + "/" + r.Name
+}
diff --git a/pkg/foghorn/controller.go b/pkg/foghorn/controller.go
index 08f971e9..7cdf819c 100644
--- a/pkg/foghorn/controller.go
+++ b/pkg/foghorn/controller.go
@@ -236,7 +236,7 @@ func (r *LighthouseJobReconciler) reportStatus(activity *lighthousev1alpha1.Acti
 	}
 
 	// Trigger external plugins if appropriate
-	if external := util.ExternalPluginsForEvent(r.pluginConfig, util.LighthousePayloadTypeActivity, fmt.Sprintf("%s/%s", owner, repo)); len(external) > 0 {
+	if external := util.ExternalPluginsForEvent(r.pluginConfig, util.LighthousePayloadTypeActivity, fmt.Sprintf("%s/%s", owner, repo), nil); len(external) > 0 {
 		go util.CallExternalPluginsWithActivityRecord(r.logger, external, activity, util.HMACToken(), r.wg)
 	}
 
diff --git a/pkg/util/externalplugins.go b/pkg/util/externalplugins.go
index c9b7380b..e9781250 100644
--- a/pkg/util/externalplugins.go
+++ b/pkg/util/externalplugins.go
@@ -251,7 +251,7 @@ func dispatch(endpoint string, payload []byte, h http.Header) error {
 
 // ExternalPluginsForEvent returns whether there are any external plugins that need to
 // get the present event.
-func ExternalPluginsForEvent(pluginConfig *plugins.ConfigAgent, eventKind string, srcRepo string) []plugins.ExternalPlugin {
+func ExternalPluginsForEvent(pluginConfig *plugins.ConfigAgent, eventKind string, srcRepo string, disabledExternalPlugins []string) []plugins.ExternalPlugin {
 	var matching []plugins.ExternalPlugin
 	if pluginConfig.Config() == nil {
 		return matching
@@ -267,6 +267,9 @@ func ExternalPluginsForEvent(pluginConfig *plugins.ConfigAgent, eventKind string
 
 		// Make sure the events match
 		for _, p := range extPlugins {
+			if StringArrayIndex(disabledExternalPlugins, p.Name) >= 0 {
+				continue
+			}
 			if len(p.Events) == 0 {
 				matching = append(matching, p)
 			} else {
diff --git a/pkg/util/strings.go b/pkg/util/strings.go
new file mode 100644
index 00000000..3768c4ca
--- /dev/null
+++ b/pkg/util/strings.go
@@ -0,0 +1,11 @@
+package util
+
+// StringArrayIndex returns the index in the slice which equals the given value
+func StringArrayIndex(array []string, value string) int {
+	for i, v := range array {
+		if v == value {
+			return i
+		}
+	}
+	return -1
+}
diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go
index 6433d839..8d9e9220 100644
--- a/pkg/webhook/webhook.go
+++ b/pkg/webhook/webhook.go
@@ -3,6 +3,7 @@ package webhook
 import (
 	"bytes"
 	"fmt"
+	"github.com/jenkins-x/lighthouse/pkg/externalplugincfg"
 	"io/ioutil"
 	"net/http"
 	"net/url"
@@ -30,15 +31,16 @@ import (
 type WebhooksController struct {
 	ConfigMapWatcher *watcher.ConfigMapWatcher
 
-	path           string
-	namespace      string
-	pluginFilename string
-	configFilename string
-	server         *Server
-	botName        string
-	gitServerURL   string
-	gitClient      git.Client
-	launcher       launcher.PipelineLauncher
+	path                    string
+	namespace               string
+	pluginFilename          string
+	configFilename          string
+	server                  *Server
+	botName                 string
+	gitServerURL            string
+	gitClient               git.Client
+	launcher                launcher.PipelineLauncher
+	disabledExternalPlugins []string
 }
 
 // NewWebhooksController creates and configures the controller
@@ -242,13 +244,22 @@ func (o *WebhooksController) HandleWebhookRequests(w http.ResponseWriter, r *htt
 			return
 		}
 	}
+	entry := logrus.WithField("Webhook", webhook.Kind())
+	if o.disabledExternalPlugins == nil {
+		o.disabledExternalPlugins, err = externalplugincfg.LoadDisabledPlugins(entry, kubeClient, o.namespace)
+		if err != nil {
+			err = errors.Wrap(err, "failed to load disabled external plugins")
+			responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: %s", err.Error()))
+			return
+		}
+	}
 
-	l, output, err := o.ProcessWebHook(logrus.WithField("Webhook", webhook.Kind()), webhook)
+	l, output, err := o.ProcessWebHook(entry, webhook)
 	if err != nil {
 		responseHTTPError(w, http.StatusInternalServerError, fmt.Sprintf("500 Internal Server Error: %s", err.Error()))
 	}
 	// Demux events only to external plugins that require this event.
-	if external := util.ExternalPluginsForEvent(o.server.Plugins, string(webhook.Kind()), webhook.Repository().FullName); len(external) > 0 {
+	if external := util.ExternalPluginsForEvent(o.server.Plugins, string(webhook.Kind()), webhook.Repository().FullName, o.disabledExternalPlugins); len(external) > 0 {
 		go util.CallExternalPluginsWithWebhook(l, external, webhook, util.HMACToken(), &o.server.wg)
 	}
 


Test patch:
diff --git a/pkg/externalplugincfg/load_test.go b/pkg/externalplugincfg/load_test.go
new file mode 100644
index 00000000..2a4b4b9d
--- /dev/null
+++ b/pkg/externalplugincfg/load_test.go
@@ -0,0 +1,52 @@
+package externalplugincfg_test
+
+import (
+	"github.com/jenkins-x/lighthouse/pkg/externalplugincfg"
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes/fake"
+	"testing"
+)
+
+var testYaml = `- name: cd-indicators
+  requiredResources:
+  - kind: Service
+    name: cd-indicators
+    namespace: jx
+- name: lighthouse-webui-plugin
+  requiredResources:
+  - kind: Service
+    name: lighthouse-webui-plugin
+    namespace: jx`
+
+func TestLoadExternalPluginConfig(t *testing.T) {
+	ns := "jx"
+
+	expected := []string{"lighthouse-webui-plugin"}
+	l := logrus.StandardLogger().WithField("Testing", true)
+
+	client := fake.NewSimpleClientset(
+		&corev1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: ns,
+				Name:      externalplugincfg.ConfigMapName,
+			},
+			Data: map[string]string{
+				"config.yaml": testYaml,
+			},
+		},
+		&corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Namespace: ns,
+				Name:      "cd-indicators",
+			},
+		},
+	)
+
+	got, err := externalplugincfg.LoadDisabledPlugins(l, client, ns)
+	require.NoError(t, err, "failed to load disabled external plugins")
+	assert.Equal(t, expected, got, "for disabled external plugins")
+}
diff --git a/pkg/util/externalplugins_test.go b/pkg/util/externalplugins_test.go
index 5d10f74b..41d666c4 100644
--- a/pkg/util/externalplugins_test.go
+++ b/pkg/util/externalplugins_test.go
@@ -10,6 +10,6 @@ import (
 
 func Test_ExternalPluginsForEvent_returns_empty_slice_for_nil_configuration(t *testing.T) {
 	configAgent := &plugins.ConfigAgent{}
-	plugins := util.ExternalPluginsForEvent(configAgent, util.LighthousePayloadTypeActivity, "myorg/myrepo")
+	plugins := util.ExternalPluginsForEvent(configAgent, util.LighthousePayloadTypeActivity, "myorg/myrepo", nil)
 	require.Empty(t, plugins)
 }
diff --git a/pkg/webhook/webhook_test.go b/pkg/webhook/webhook_test.go
index 1dd0916a..892076a0 100644
--- a/pkg/webhook/webhook_test.go
+++ b/pkg/webhook/webhook_test.go
@@ -253,7 +253,7 @@ func TestNeedDemux(t *testing.T) {
 			})
 			s := &Server{Plugins: pa}
 
-			gotPlugins := util.ExternalPluginsForEvent(s.Plugins, string(test.eventType), test.srcRepo)
+			gotPlugins := util.ExternalPluginsForEvent(s.Plugins, string(test.eventType), test.srcRepo, nil)
 			if len(gotPlugins) != len(test.expected) {
 				t.Fatalf("expected plugins: %+v, got: %+v", test.expected, gotPlugins)
 			}