From 741bf227a17bc8e29ed8a6c9599d02c253d7f4f0 Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Thu, 26 Aug 2021 15:57:22 -0700 Subject: [PATCH] Add MakeCustomObject as well. --- pkg/reconciler/eventlistener/eventlistener.go | 110 +----- .../eventlistener/resources/custom.go | 136 +++++++ .../eventlistener/resources/custom_test.go | 363 ++++++++++++++++++ 3 files changed, 503 insertions(+), 106 deletions(-) create mode 100644 pkg/reconciler/eventlistener/resources/custom.go create mode 100644 pkg/reconciler/eventlistener/resources/custom_test.go diff --git a/pkg/reconciler/eventlistener/eventlistener.go b/pkg/reconciler/eventlistener/eventlistener.go index 42f89366a9..9392fac0bd 100644 --- a/pkg/reconciler/eventlistener/eventlistener.go +++ b/pkg/reconciler/eventlistener/eventlistener.go @@ -17,12 +17,10 @@ limitations under the License. package eventlistener import ( - "bytes" "context" "encoding/json" stdError "errors" "fmt" - "os" "reflect" "strings" "sync" @@ -393,113 +391,13 @@ func (r *Reconciler) reconcileCustomObject(ctx context.Context, el *v1beta1.Even return err } - original := &duckv1.WithPod{} - decoder := json.NewDecoder(bytes.NewBuffer(el.Spec.Resources.CustomResource.Raw)) - if err := decoder.Decode(&original); err != nil { - logging.FromContext(ctx).Errorf("unable to decode object", err) - return err - } - - customObjectData := original.DeepCopy() - - namespace := original.GetNamespace() - // Default the resource creation to the EventListenerNamespace if not found in the resource object - if namespace == "" { - namespace = el.GetNamespace() - } - - container := resources.MakeContainer(el, r.config, func(c *corev1.Container) { - // handle env and resources for custom object - if len(original.Spec.Template.Spec.Containers) == 1 { - for i := range original.Spec.Template.Spec.Containers[0].Env { - c.Env = append(c.Env, original.Spec.Template.Spec.Containers[0].Env[i]) - } - c.Resources = original.Spec.Template.Spec.Containers[0].Resources - } - - // TODO(mattmoor): Knative's sharedmain no longer looks for this, so confirm this is still needed. - c.VolumeMounts = []corev1.VolumeMount{{ - Name: "config-logging", - MountPath: "/etc/config-logging", - ReadOnly: true, - }} - - c.Env = append(c.Env, corev1.EnvVar{ - Name: "SYSTEM_NAMESPACE", - // Cannot use FieldRef here because Knative Serving mask that field under feature gate - // https://github.com/knative/serving/blob/master/pkg/apis/config/features.go#L48 - Value: el.Namespace, - }) - - c.Env = append(c.Env, corev1.EnvVar{ - Name: "CONFIG_OBSERVABILITY_NAME", - Value: metrics.ConfigMapName(), - }, corev1.EnvVar{ - Name: "METRICS_DOMAIN", - Value: resources.TriggersMetricsDomain, - }, corev1.EnvVar{ - // METRICS_PROMETHEUS_PORT defines the port exposed by the EventListener metrics endpoint - // env METRICS_PROMETHEUS_PORT set by controller - Name: "METRICS_PROMETHEUS_PORT", - Value: os.Getenv("METRICS_PROMETHEUS_PORT"), - }) - - c.ReadinessProbe = &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/live", - Scheme: corev1.URISchemeHTTP, - }, - }, - SuccessThreshold: 1, - } - }) - - podlabels := kmeta.UnionMaps(el.Labels, resources.GenerateLabels(el.Name, r.config.StaticResourceLabels)) - - podlabels = kmeta.UnionMaps(podlabels, customObjectData.Labels) - - original.Labels = podlabels - original.Annotations = customObjectData.Annotations - original.Spec.Template.ObjectMeta = metav1.ObjectMeta{ - Name: customObjectData.Spec.Template.Name, - Labels: customObjectData.Spec.Template.Labels, - Annotations: customObjectData.Spec.Template.Annotations, - } - original.Spec.Template.Spec = corev1.PodSpec{ - Tolerations: customObjectData.Spec.Template.Spec.Tolerations, - NodeSelector: customObjectData.Spec.Template.Spec.NodeSelector, - ServiceAccountName: customObjectData.Spec.Template.Spec.ServiceAccountName, - Containers: []corev1.Container{container}, - Volumes: []corev1.Volume{{ - Name: "config-logging", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: resources.EventListenerConfigMapName, - }, - }, - }, - }}, - } - marshaledData, err := json.Marshal(original) + data, container, err := resources.MakeCustomObject(el, r.config) if err != nil { - logging.FromContext(ctx).Errorf("failed to marshal custom object", err) - return err - } - data := new(unstructured.Unstructured) - if err := data.UnmarshalJSON(marshaledData); err != nil { - logging.FromContext(ctx).Errorf("failed to unmarshal to unstructured object", err) + logging.FromContext(ctx).Errorf("unable to construct custom object", err) return err } - if data.GetName() == "" { - data.SetName(el.Status.Configuration.GeneratedResourceName) - } gvr, _ := meta.UnsafeGuessKindToResource(data.GetObjectKind().GroupVersionKind()) - - data.SetOwnerReferences([]metav1.OwnerReference{*kmeta.NewControllerRef(el)}) - var watchError error r.onlyOnce.Do(func() { watchError = r.podspecableTracker.WatchOnDynamicObject(ctx, gvr) @@ -509,7 +407,7 @@ func (r *Reconciler) reconcileCustomObject(ctx context.Context, el *v1beta1.Even return watchError } - existingCustomObject, err := r.DynamicClientSet.Resource(gvr).Namespace(namespace).Get(ctx, data.GetName(), metav1.GetOptions{}) + existingCustomObject, err := r.DynamicClientSet.Resource(gvr).Namespace(data.GetNamespace()).Get(ctx, data.GetName(), metav1.GetOptions{}) switch { case err == nil: if _, err := r.deploymentLister.Deployments(el.Namespace).Get(el.Status.Configuration.GeneratedResourceName); err == nil { @@ -566,7 +464,7 @@ func (r *Reconciler) reconcileCustomObject(ctx context.Context, el *v1beta1.Even } if len(existingObject.Spec.Template.Spec.Containers) == 0 || len(existingObject.Spec.Template.Spec.Containers) > 1 { - existingObject.Spec.Template.Spec.Containers = []corev1.Container{container} + existingObject.Spec.Template.Spec.Containers = []corev1.Container{*container} updated = true } else { if existingObject.Spec.Template.Spec.Containers[0].Name != container.Name { diff --git a/pkg/reconciler/eventlistener/resources/custom.go b/pkg/reconciler/eventlistener/resources/custom.go new file mode 100644 index 0000000000..7b288e8e10 --- /dev/null +++ b/pkg/reconciler/eventlistener/resources/custom.go @@ -0,0 +1,136 @@ +/* +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 resources + +import ( + "bytes" + "encoding/json" + "os" + + "github.com/tektoncd/triggers/pkg/apis/triggers/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/kmeta" + "knative.dev/pkg/metrics" +) + +func MakeCustomObject(el *v1beta1.EventListener, c Config) (*unstructured.Unstructured, *corev1.Container, error) { + original := &duckv1.WithPod{} + decoder := json.NewDecoder(bytes.NewBuffer(el.Spec.Resources.CustomResource.Raw)) + if err := decoder.Decode(&original); err != nil { + return nil, nil, err + } + + customObjectData := original.DeepCopy() + + namespace := original.GetNamespace() + // Default the resource creation to the EventListenerNamespace if not found in the resource object + if namespace == "" { + namespace = el.GetNamespace() + } + + container := MakeContainer(el, c, func(c *corev1.Container) { + // handle env and resources for custom object + if len(original.Spec.Template.Spec.Containers) == 1 { + for i := range original.Spec.Template.Spec.Containers[0].Env { + c.Env = append(c.Env, original.Spec.Template.Spec.Containers[0].Env[i]) + } + c.Resources = original.Spec.Template.Spec.Containers[0].Resources + } + + // TODO(mattmoor): Knative's sharedmain no longer looks for this, so confirm this is still needed. + c.VolumeMounts = []corev1.VolumeMount{{ + Name: "config-logging", + MountPath: "/etc/config-logging", + ReadOnly: true, + }} + + c.Env = append(c.Env, corev1.EnvVar{ + Name: "SYSTEM_NAMESPACE", + // Cannot use FieldRef here because Knative Serving mask that field under feature gate + // https://github.com/knative/serving/blob/master/pkg/apis/config/features.go#L48 + Value: el.Namespace, + }, corev1.EnvVar{ + Name: "CONFIG_OBSERVABILITY_NAME", + Value: metrics.ConfigMapName(), + }, corev1.EnvVar{ + Name: "METRICS_DOMAIN", + Value: TriggersMetricsDomain, + }, corev1.EnvVar{ + // METRICS_PROMETHEUS_PORT defines the port exposed by the EventListener metrics endpoint + // env METRICS_PROMETHEUS_PORT set by controller + Name: "METRICS_PROMETHEUS_PORT", + Value: os.Getenv("METRICS_PROMETHEUS_PORT"), + }) + + c.ReadinessProbe = &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/live", + Scheme: corev1.URISchemeHTTP, + }, + }, + SuccessThreshold: 1, + } + }) + + podlabels := kmeta.UnionMaps(el.Labels, GenerateLabels(el.Name, c.StaticResourceLabels)) + + podlabels = kmeta.UnionMaps(podlabels, customObjectData.Labels) + + original.Labels = podlabels + original.Annotations = customObjectData.Annotations + original.Spec.Template.ObjectMeta = metav1.ObjectMeta{ + Name: customObjectData.Spec.Template.Name, + Labels: customObjectData.Spec.Template.Labels, + Annotations: customObjectData.Spec.Template.Annotations, + } + original.Spec.Template.Spec = corev1.PodSpec{ + Tolerations: customObjectData.Spec.Template.Spec.Tolerations, + NodeSelector: customObjectData.Spec.Template.Spec.NodeSelector, + ServiceAccountName: customObjectData.Spec.Template.Spec.ServiceAccountName, + Containers: []corev1.Container{container}, + Volumes: []corev1.Volume{{ + Name: "config-logging", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: EventListenerConfigMapName, + }, + }, + }, + }}, + } + marshaledData, err := json.Marshal(original) + if err != nil { + return nil, nil, err + } + data := new(unstructured.Unstructured) + if err := data.UnmarshalJSON(marshaledData); err != nil { + return nil, nil, err + } + + if data.GetName() == "" { + data.SetName(el.Status.Configuration.GeneratedResourceName) + } + data.SetNamespace(namespace) + data.SetOwnerReferences([]metav1.OwnerReference{*kmeta.NewControllerRef(el)}) + + return data, &container, nil +} diff --git a/pkg/reconciler/eventlistener/resources/custom_test.go b/pkg/reconciler/eventlistener/resources/custom_test.go new file mode 100644 index 0000000000..0ce2249991 --- /dev/null +++ b/pkg/reconciler/eventlistener/resources/custom_test.go @@ -0,0 +1,363 @@ +/* +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 resources + +import ( + "os" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tektoncd/triggers/pkg/apis/triggers/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func TestCustomObject(t *testing.T) { + err := os.Setenv("METRICS_PROMETHEUS_PORT", "9000") + if err != nil { + t.Fatal(err) + } + err = os.Setenv("SYSTEM_NAMESPACE", "tekton-pipelines") + if err != nil { + t.Fatal(err) + } + + config := *MakeConfig() + metadata := map[string]interface{}{ + "creationTimestamp": nil, + "labels": map[string]interface{}{ + "app.kubernetes.io/managed-by": "EventListener", + "app.kubernetes.io/part-of": "Triggers", + "eventlistener": eventListenerName, + }, + "namespace": namespace, + "ownerReferences": []interface{}{ + map[string]interface{}{ + "apiVersion": "triggers.tekton.dev/v1beta1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "EventListener", + "name": eventListenerName, + "uid": "", + }, + }, + } + args := []interface{}{ + "--el-name=" + eventListenerName, + "--el-namespace=" + namespace, + "--port=" + strconv.Itoa(eventListenerContainerPort), + "--readtimeout=" + strconv.FormatInt(DefaultReadTimeout, 10), + "--writetimeout=" + strconv.FormatInt(DefaultWriteTimeout, 10), + "--idletimeout=" + strconv.FormatInt(DefaultIdleTimeout, 10), + "--timeouthandler=" + strconv.FormatInt(DefaultTimeOutHandler, 10), + "--is-multi-ns=" + strconv.FormatBool(false), + "--payload-validation=" + strconv.FormatBool(true), + } + + env := []interface{}{ + map[string]interface{}{ + "name": "SYSTEM_NAMESPACE", + "value": namespace, + }, + map[string]interface{}{ + "name": "CONFIG_OBSERVABILITY_NAME", + "value": "config-observability", + }, + map[string]interface{}{ + "name": "METRICS_DOMAIN", + "value": TriggersMetricsDomain, + }, + map[string]interface{}{ + "name": "METRICS_PROMETHEUS_PORT", + "value": "9000", + }, + } + + tests := []struct { + name string + el *v1beta1.EventListener + want *unstructured.Unstructured + }{{ + name: "vanilla", + el: makeEL(func(el *v1beta1.EventListener) { + el.Spec.Resources.CustomResource = &v1beta1.CustomResource{ + RawExtension: runtime.RawExtension{ + Raw: []byte(`{ + "apiVersion": "serving.knative.dev/v1", + "kind": "Service" + }`), + }, + } + }), + want: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "serving.knative.dev/v1", + "kind": "Service", + "metadata": metadata, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "creationTimestamp": nil, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "event-listener", + "image": DefaultImage, + "args": args, + "env": env, + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": int64(8080), + "protocol": "TCP", + }, + }, + "resources": map[string]interface{}{}, + "volumeMounts": []interface{}{ + map[string]interface{}{ + "mountPath": "/etc/config-logging", + "name": "config-logging", + "readOnly": true, + }, + }, + "readinessProbe": map[string]interface{}{ + "httpGet": map[string]interface{}{ + "path": "/live", + "port": int64(0), + "scheme": "HTTP", + }, + "successThreshold": int64(1), + }, + }, + }, + "volumes": []interface{}{ + map[string]interface{}{ + "configMap": map[string]interface{}{ + "name": EventListenerConfigMapName, + }, + "name": "config-logging", + }, + }, + }, + }, + }, + }, + }, + }, { + name: "with env vars", + el: makeEL(func(el *v1beta1.EventListener) { + el.Spec.Resources.CustomResource = &v1beta1.CustomResource{ + RawExtension: runtime.RawExtension{ + Raw: []byte(`{ + "apiVersion": "serving.knative.dev/v1", + "kind": "Service", + "spec": { + "template": { + "spec": { + "containers": [{ + "env": [{ + "name": "FOO", + "value": "bar" + }] + }] + } + } + } + }`), + }, + } + }), + want: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "serving.knative.dev/v1", + "kind": "Service", + "metadata": metadata, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "creationTimestamp": nil, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "event-listener", + "image": DefaultImage, + "args": args, + "env": append([]interface{}{ + map[string]interface{}{ + "name": "FOO", + "value": "bar", + }, + }, env...), + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": int64(8080), + "protocol": "TCP", + }, + }, + "resources": map[string]interface{}{}, + "volumeMounts": []interface{}{ + map[string]interface{}{ + "mountPath": "/etc/config-logging", + "name": "config-logging", + "readOnly": true, + }, + }, + "readinessProbe": map[string]interface{}{ + "httpGet": map[string]interface{}{ + "path": "/live", + "port": int64(0), + "scheme": "HTTP", + }, + "successThreshold": int64(1), + }, + }, + }, + "volumes": []interface{}{ + map[string]interface{}{ + "configMap": map[string]interface{}{ + "name": EventListenerConfigMapName, + }, + "name": "config-logging", + }, + }, + }, + }, + }, + }, + }}, { + name: "with resources", + el: makeEL(func(el *v1beta1.EventListener) { + el.Spec.Resources.CustomResource = &v1beta1.CustomResource{ + RawExtension: runtime.RawExtension{ + Raw: []byte(`{ + "apiVersion": "serving.knative.dev/v1", + "kind": "Service", + "spec": { + "template": { + "spec": { + "containers": [{ + "resources": { + "limits": { + "cpu": "101m" + } + } + }] + } + } + } + }`), + }, + } + }), + want: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "serving.knative.dev/v1", + "kind": "Service", + "metadata": metadata, + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "creationTimestamp": nil, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "event-listener", + "image": DefaultImage, + "args": args, + "env": env, + "ports": []interface{}{ + map[string]interface{}{ + "containerPort": int64(8080), + "protocol": "TCP", + }, + }, + "resources": map[string]interface{}{ + "limits": map[string]interface{}{ + "cpu": "101m", + }, + }, + "volumeMounts": []interface{}{ + map[string]interface{}{ + "mountPath": "/etc/config-logging", + "name": "config-logging", + "readOnly": true, + }, + }, + "readinessProbe": map[string]interface{}{ + "httpGet": map[string]interface{}{ + "path": "/live", + "port": int64(0), + "scheme": "HTTP", + }, + "successThreshold": int64(1), + }, + }, + }, + "volumes": []interface{}{ + map[string]interface{}{ + "configMap": map[string]interface{}{ + "name": EventListenerConfigMapName, + }, + "name": "config-logging", + }, + }, + }, + }, + }, + }, + }, + }} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _, err := MakeCustomObject(tt.el, config) + if err != nil { + t.Fatalf("MakeCustomObject() = %v", err) + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("MakeCustomObject() did not return expected. -want, +got: %s", diff) + } + }) + } +} + +func TestCustomObjectError(t *testing.T) { + err := os.Setenv("METRICS_PROMETHEUS_PORT", "9000") + if err != nil { + t.Fatal(err) + } + err = os.Setenv("SYSTEM_NAMESPACE", "tekton-pipelines") + if err != nil { + t.Fatal(err) + } + + config := *MakeConfig() + + got, _, err := MakeCustomObject(makeEL(func(el *v1beta1.EventListener) { + el.Spec.Resources.CustomResource = &v1beta1.CustomResource{ + RawExtension: runtime.RawExtension{ + Raw: []byte(`garbage`), + }, + } + }), config) + if err == nil { + t.Fatalf("MakeCustomObject() = %v, wanted error", got) + } +}