From 6633356585c0d6be461d1ba6aeb055e7fdaf3004 Mon Sep 17 00:00:00 2001 From: savitaashture Date: Sat, 31 Oct 2020 20:30:46 +0530 Subject: [PATCH] Allow secure connection to eventlistener pod --- cmd/eventlistenersink/main.go | 10 +- docs/eventlisteners.md | 14 + .../eventlistener-tls-connection/README.md | 78 ++++ .../eventlistener-tls-connection/role.yaml | 62 ++++ .../eventlistener-tls-connection/secret.yaml | 7 + .../tls-eventlistener-interceptor.yaml | 86 +++++ .../v1alpha1/event_listener_validation.go | 83 ++++- .../event_listener_validation_test.go | 80 ++++- .../v1alpha1/eventlistener/eventlistener.go | 333 +++++++++++------- .../eventlistener/eventlistener_test.go | 155 ++++++++ pkg/sink/initialization.go | 10 + 11 files changed, 786 insertions(+), 132 deletions(-) create mode 100644 examples/eventlistener-tls-connection/README.md create mode 100644 examples/eventlistener-tls-connection/role.yaml create mode 100644 examples/eventlistener-tls-connection/secret.yaml create mode 100644 examples/eventlistener-tls-connection/tls-eventlistener-interceptor.yaml diff --git a/cmd/eventlistenersink/main.go b/cmd/eventlistenersink/main.go index fbce6ab798..538c8701b7 100644 --- a/cmd/eventlistenersink/main.go +++ b/cmd/eventlistenersink/main.go @@ -136,7 +136,13 @@ func main() { sinkArgs.ELTimeOutHandler*time.Second, "EventListener Timeout!\n"), } - if err := srv.ListenAndServe(); err != nil { - logger.Fatalf("failed to start eventlistener sink: %v", err) + if sinkArgs.Cert == "" && sinkArgs.Key == "" { + if err := srv.ListenAndServe(); err != nil { + logger.Fatalf("failed to start eventlistener sink: %v", err) + } + } else { + if err := srv.ListenAndServeTLS(sinkArgs.Cert, sinkArgs.Key); err != nil { + logger.Fatalf("failed to start eventlistener sink: %v", err) + } } } diff --git a/docs/eventlisteners.md b/docs/eventlisteners.md index a875261b86..c58e2bf47f 100644 --- a/docs/eventlisteners.md +++ b/docs/eventlisteners.md @@ -42,6 +42,8 @@ using [Event Interceptors](#Interceptors). - [Multiple EventListeners (One EventListener Per Namespace)](#multiple-eventlisteners-one-eventlistener-per-namespace) - [Multiple EventListeners (Multiple EventListeners per Namespace)](#multiple-eventlisteners-multiple-eventlisteners-per-namespace) - [ServiceAccount per EventListenerTrigger](#serviceaccount-per-eventlistenertrigger) + - [EventListener Secure Connection](#eventlistener-secure-connection) + - [Prerequisites](#prerequisites) ## Syntax @@ -277,8 +279,11 @@ Right now the allowed values as part of `podSpec` are ServiceAccountName NodeSelector Tolerations +Volumes Containers - Resources +- VolumeMounts +- Env ``` ### Logging @@ -821,3 +826,12 @@ Except as otherwise noted, the content of this page is licensed under the [Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/), and code samples are licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). + +## EventListener Secure Connection + +Triggers now support both `HTTP` and `HTTPS` connection by adding few configuration to eventlistener. + +To setup TLS connection add two set of reserved environment variables `TLS_CERT` and `TLS_KEY` using `secretKeyRef` env type +where we need to specify the `secret` which contains `cert` and `key` files. See the full [example]((../examples/eventlistener-tls-connection/README.md)) for more details. + +Refer [TEP-0027](https://github.com/tektoncd/community/blob/master/teps/0027-https-connection-to-triggers-eventlistener.md) for more information on design and user stories. diff --git a/examples/eventlistener-tls-connection/README.md b/examples/eventlistener-tls-connection/README.md new file mode 100644 index 0000000000..5ce3718311 --- /dev/null +++ b/examples/eventlistener-tls-connection/README.md @@ -0,0 +1,78 @@ +## EventListener Secure Connection + +Triggers now support both `HTTP` and `HTTPS` connection by adding some configurations to eventlistener. + +### Prerequisites +* Certificates with Key and Cert +* Secret which includes those certificates + +### Try it out locally: + +#### Creating Prerequisites + +* #### Certificates with Key and Cert. + +##### 1. Steps to generate root key, cert +1. Create Root Key + ```text + openssl genrsa -des3 -out rootCA.key 4096 + ``` +2. Create and self sign the Root Certificate + ```text + openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt + ``` +##### 2. Steps to generate certificate (for each server) +1. Create the certificate key + ```text + openssl genrsa -out tls.key 2048 + ``` +2. Create the signing (csr) + +* The CSR is where you specify the details for the certificate you want to generate. +This request will be processed by the owner of the root key to generate the certificate. + +* **Important:** While creating the csr it is important to specify the `Common Name` providing the IP address or domain name for the service, otherwise the certificate cannot be verified. + ```text + openssl req -new -key tls.key -out tls.csr + ``` +3. Generate the certificate using the tls csr and key along with the CA Root key + ```text + openssl x509 -req -in tls.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out tls.crt -days 500 -sha256 + ``` +##### 3. Follow same steps from 2 to generate certificates for client also + +* #### Create secret which includes those certificates + ```text + kubectl create secret generic tls-secret-key --from-file=tls.crt --from-file=tls.key + ``` + +#### Configuring EventListener for TLS connection +1. To create the TLS connection for EventListener and all related resources, run: + + ```bash + kubectl apply -f examples/eventlistener-tls-connection/ + ``` + +1. Test by sending the sample payload. + + ```bash + curl -v \ + -H 'X-GitHub-Event: pull_request' \ + -H 'X-Hub-Signature: sha1=ba0cdc263b3492a74b601d240c27efe81c4720cb' \ + -H 'Content-Type: application/json' \ + -d '{"action": "opened", "pull_request":{"head":{"sha": "28911bbb5a3e2ea034daf1f6be0a822d50e31e73"}},"repository":{"clone_url": "https://github.com/tektoncd/triggers.git"}}' \ + https:// --cacert rootCA.crt --key client.key --cert client.crt + ``` + + The response status code should be `201 Created` + + [`HMAC`](https://www.freeformatter.com/hmac-generator.html) tool used to create X-Hub-Signature. + + In [`HMAC`](https://www.freeformatter.com/hmac-generator.html) `string` is the *body payload ex:* `{"action": "opened", "pull_request":{"head":{"sha": "28911bbb5a3e2ea034daf1f6be0a822d50e31e73"}},"repository":{"clone_url": "https://github.com/tektoncd/triggers.git"}}` + and `secretKey` is the *given secretToken ex:* `1234567`. + +1. You should see a new TaskRun that got created: + + ```bash + kubectl get taskruns | grep tls-run- + ``` diff --git a/examples/eventlistener-tls-connection/role.yaml b/examples/eventlistener-tls-connection/role.yaml new file mode 100644 index 0000000000..fee3778402 --- /dev/null +++ b/examples/eventlistener-tls-connection/role.yaml @@ -0,0 +1,62 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: tekton-triggers-tls-sa +secrets: + - name: github-secret +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: tekton-triggers-tls-binding +subjects: + - kind: ServiceAccount + name: tekton-triggers-tls-sa +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: tekton-triggers-tls-minimal +--- +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: tekton-triggers-tls-minimal +rules: + # Permissions for every EventListener deployment to function + - apiGroups: ["triggers.tekton.dev"] + resources: ["eventlisteners", "triggerbindings", "triggertemplates", "triggers"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + # secrets are only needed for GitHub/GitLab interceptors, serviceaccounts only for per trigger authorization + resources: ["configmaps", "secrets", "serviceaccounts"] + verbs: ["get", "list", "watch"] + # Permissions to create resources in associated TriggerTemplates + - apiGroups: ["tekton.dev"] + resources: ["pipelineruns", "pipelineresources", "taskruns"] + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: tekton-triggers-tls-binding +subjects: + - kind: ServiceAccount + name: tekton-triggers-tls-sa + namespace: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tekton-triggers-tls-minimal +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: tekton-triggers-tls-minimal + labels: + app.kubernetes.io/instance: default + app.kubernetes.io/part-of: tekton-triggers +rules: + # Permissions for every EventListener deployment to function + - apiGroups: ["triggers.tekton.dev"] + resources: ["clustertriggerbindings"] + verbs: ["get", "list", "watch"] diff --git a/examples/eventlistener-tls-connection/secret.yaml b/examples/eventlistener-tls-connection/secret.yaml new file mode 100644 index 0000000000..beb4f9c894 --- /dev/null +++ b/examples/eventlistener-tls-connection/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: github-secret +type: Opaque +stringData: + secretToken: "1234567" diff --git a/examples/eventlistener-tls-connection/tls-eventlistener-interceptor.yaml b/examples/eventlistener-tls-connection/tls-eventlistener-interceptor.yaml new file mode 100644 index 0000000000..79467aa363 --- /dev/null +++ b/examples/eventlistener-tls-connection/tls-eventlistener-interceptor.yaml @@ -0,0 +1,86 @@ +--- +apiVersion: triggers.tekton.dev/v1alpha1 +kind: EventListener +metadata: + name: tls-listener-interceptor +spec: + triggers: + - name: tls-listener + interceptors: + - github: + secretRef: + secretName: github-secret + secretKey: secretToken + eventTypes: + - pull_request + - cel: + filter: "body.action in ['opened', 'synchronize', 'reopened']" + bindings: + - ref: tls-pr-binding + template: + ref: tls-template + resources: + kubernetesResource: + spec: + template: + spec: + serviceAccountName: tekton-triggers-tls-sa + containers: + - env: + - name: TLS_CERT + valueFrom: + secretKeyRef: + name: tls-key-secret + key: tls.crt + - name: TLS_KEY + valueFrom: + secretKeyRef: + name: tls-key-secret + key: tls.key +--- +apiVersion: triggers.tekton.dev/v1alpha1 +kind: TriggerBinding +metadata: + name: tls-pr-binding +spec: + params: + - name: gitrevision + value: $(body.pull_request.head.sha) + - name: gitrepositoryurl + value: $(body.repository.clone_url) + +--- +apiVersion: triggers.tekton.dev/v1alpha1 +kind: TriggerTemplate +metadata: + name: tls-template +spec: + params: + - name: gitrevision + - name: gitrepositoryurl + resourcetemplates: + - apiVersion: tekton.dev/v1alpha1 + kind: TaskRun + metadata: + generateName: tls-run- + spec: + taskSpec: + inputs: + resources: + - name: source + type: git + steps: + - image: ubuntu + script: | + #! /bin/bash + ls -al $(inputs.resources.source.path) + inputs: + resources: + - name: source + resourceSpec: + type: git + params: + - name: revision + value: $(tt.params.gitrevision) + - name: url + value: $(tt.params.gitrepositoryurl) diff --git a/pkg/apis/triggers/v1alpha1/event_listener_validation.go b/pkg/apis/triggers/v1alpha1/event_listener_validation.go index 5afce7b509..9b918fcc8b 100644 --- a/pkg/apis/triggers/v1alpha1/event_listener_validation.go +++ b/pkg/apis/triggers/v1alpha1/event_listener_validation.go @@ -21,10 +21,18 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "knative.dev/pkg/apis" ) +var ( + reservedEnvVars = sets.NewString( + "TLS_CERT", + "TLS_KEY", + ) +) + // Validate EventListener. func (e *EventListener) Validate(ctx context.Context) *apis.FieldError { return e.Spec.validate(ctx) @@ -40,7 +48,7 @@ func (s *EventListenerSpec) validate(ctx context.Context) (errs *apis.FieldError errs = errs.Also(trigger.validate(ctx).ViaField(fmt.Sprintf("spec.triggers[%d]", i))) } if s.Resources.KubernetesResource != nil { - errs = errs.Also(validateKubernetesObject(s.Resources.KubernetesResource)) + errs = errs.Also(validateKubernetesObject(s.Resources.KubernetesResource).ViaField("spec.resources.kubernetesResource")) } return errs } @@ -56,14 +64,86 @@ func validateKubernetesObject(orig *KubernetesResource) (errs *apis.FieldError) if len(orig.Template.Spec.Containers) == 1 { errs = errs.Also(apis.CheckDisallowedFields(orig.Template.Spec.Containers[0], *containerFieldMask(&orig.Template.Spec.Containers[0])).ViaField("spec.template.spec.containers[0]")) + // validate env + errs = errs.Also(validateEnv(orig.Template.Spec.Containers[0].Env).ViaField("spec.template.spec.containers[0].env")) } return errs } +func validateEnv(envVars []corev1.EnvVar) (errs *apis.FieldError) { + var ( + count = 0 + envValue string + ) + for i, env := range envVars { + errs = errs.Also(validateEnvVar(env).ViaIndex(i)) + if reservedEnvVars.Has(env.Name) { + count++ + envValue = env.Name + } + } + // This is to make sure both TLS_CERT and TLS_KEY is set for tls connection + if count == 1 { + errs = errs.Also(&apis.FieldError{ + Message: fmt.Sprintf("Expected env's are TLS_CERT and TLS_KEY, but got only one env %s", envValue), + }) + } + return errs +} + +func validateEnvVar(env corev1.EnvVar) (errs *apis.FieldError) { + errs = errs.Also(apis.CheckDisallowedFields(env, *envVarMask(&env))) + + return errs.Also(validateEnvValueFrom(env.ValueFrom).ViaField("valueFrom")) +} + +func validateEnvValueFrom(source *corev1.EnvVarSource) *apis.FieldError { + if source == nil { + return nil + } + return apis.CheckDisallowedFields(*source, *envVarSourceMask(source)) +} + +// envVarSourceMask performs a _shallow_ copy of the Kubernetes EnvVarSource object to a new +// Kubernetes EnvVarSource object bringing over only the fields allowed in the Triggers EventListener API. +func envVarSourceMask(in *corev1.EnvVarSource) *corev1.EnvVarSource { + if in == nil { + return nil + } + out := new(corev1.EnvVarSource) + // Allowed fields + out.SecretKeyRef = in.SecretKeyRef + + // Disallowed fields + out.ConfigMapKeyRef = nil + out.FieldRef = nil + out.ResourceFieldRef = nil + + return out +} + +// envVarMask performs a _shallow_ copy of the Kubernetes EnvVar object to a new +// Kubernetes EnvVar object bringing over only the fields allowed in the Triggers EventListener API. +func envVarMask(in *corev1.EnvVar) *corev1.EnvVar { + if in == nil { + return nil + } + out := new(corev1.EnvVar) + // Allowed fields + out.Name = in.Name + out.ValueFrom = in.ValueFrom + + // Disallowed fields + out.Value = "" + + return out +} + func containerFieldMask(in *corev1.Container) *corev1.Container { out := new(corev1.Container) out.Resources = in.Resources + out.Env = in.Env // Disallowed fields // This list clarifies which all container attributes are not allowed. @@ -87,7 +167,6 @@ func containerFieldMask(in *corev1.Container) *corev1.Container { out.TTY = false out.VolumeDevices = nil out.EnvFrom = nil - out.Env = nil return out } diff --git a/pkg/apis/triggers/v1alpha1/event_listener_validation_test.go b/pkg/apis/triggers/v1alpha1/event_listener_validation_test.go index 914066ab40..7bd93f3030 100644 --- a/pkg/apis/triggers/v1alpha1/event_listener_validation_test.go +++ b/pkg/apis/triggers/v1alpha1/event_listener_validation_test.go @@ -164,7 +164,7 @@ func Test_EventListenerValidate(t *testing.T) { bldr.EventListenerCELInterceptor("", bldr.EventListenerCELOverlay("body.value", "'testing'")), ))), }, { - name: "Valid EventListener with kubernetes resource for podspec", + name: "Valid EventListener with kubernetes env for podspec", el: bldr.EventListener("name", "namespace", bldr.EventListenerSpec( bldr.EventListenerTrigger("tt", "v1alpha1"), @@ -198,6 +198,41 @@ func Test_EventListenerValidate(t *testing.T) { }), )), )), + }, { + name: "Valid EventListener with env for TLS connection", + el: bldr.EventListener("name", "namespace", + bldr.EventListenerSpec( + bldr.EventListenerTrigger("tt", "v1alpha1"), + bldr.EventListenerResources( + bldr.EventListenerKubernetesResources( + bldr.EventListenerPodSpec(duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + ServiceAccountName: "k8sresource", + Containers: []corev1.Container{{ + Env: []corev1.EnvVar{{ + Name: "TLS_CERT", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "secret-name"}, + Key: "tls.crt", + }, + }, + }, { + Name: "TLS_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "secret-name"}, + Key: "tls.key", + }, + }, + }}, + }}, + }, + }, + }), + )), + )), }} for _, test := range tests { @@ -519,7 +554,7 @@ func TestEventListenerValidate_error(t *testing.T) { )), )), }, { - name: "user specifies an unsupported container field", + name: "user specifies an unsupported container fields", el: bldr.EventListener("name", "namespace", bldr.EventListenerSpec( bldr.EventListenerTrigger("tt", "v1alpha1", @@ -532,6 +567,47 @@ func TestEventListenerValidate_error(t *testing.T) { Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "containername", + Env: []corev1.EnvVar{{ + Name: "key", + Value: "value", + }, { + Name: "key1", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + Key: "key", + }, + }, + }}, + }}, + }, + }, + }), + )), + )), + }, { + name: "user specifies an invalid env for TLS connection", + el: bldr.EventListener("name", "namespace", + bldr.EventListenerSpec( + bldr.EventListenerTrigger("tt", "v1alpha1", + bldr.EventListenerTriggerBinding("tb", "TriggerBinding", "v1alpha1"), + ), + bldr.EventListenerResources( + bldr.EventListenerKubernetesResources( + bldr.EventListenerPodSpec(duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Env: []corev1.EnvVar{{ + Name: "TLS_CERT", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "secret-name", + }, + Key: "tls.key", + }, + }, + }}, }}, }, }, diff --git a/pkg/reconciler/v1alpha1/eventlistener/eventlistener.go b/pkg/reconciler/v1alpha1/eventlistener/eventlistener.go index 777057e678..d5222b6582 100644 --- a/pkg/reconciler/v1alpha1/eventlistener/eventlistener.go +++ b/pkg/reconciler/v1alpha1/eventlistener/eventlistener.go @@ -52,7 +52,7 @@ const ( // eventListenerConfigMapName is for the automatically created ConfigMap eventListenerConfigMapName = "config-logging-triggers" // eventListenerServicePortName defines service port name for EventListener Service - eventListenerServicePortName = "http-listener" + eventListenerServicePortName = "listener" // GeneratedResourcePrefix is the name prefix for resources generated in the // EventListener reconciler GeneratedResourcePrefix = "el" @@ -132,8 +132,8 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, el *v1alpha1.EventListen // and may not have had all of the assumed default specified. el.SetDefaults(v1alpha1.WithUpgradeViaDefaulting(ctx)) - serviceReconcileError := r.reconcileService(ctx, logger, el) deploymentReconcileError := r.reconcileDeployment(ctx, logger, el) + serviceReconcileError := r.reconcileService(ctx, logger, el) return wrapError(serviceReconcileError, deploymentReconcileError) } @@ -273,129 +273,9 @@ func (r *Reconciler) reconcileDeployment(ctx context.Context, logger *zap.Sugare return err } - var replicas = ptr.Int32(1) - if el.Spec.Replicas != nil { - replicas = el.Spec.Replicas - } - var ( - tolerations []corev1.Toleration - nodeSelector, annotations, podlabels map[string]string - serviceAccountName string - resources corev1.ResourceRequirements - ) - podlabels = mergeMaps(el.Labels, GenerateResourceLabels(el.Name)) - - // For backward compatibility with podTemplate, serviceAccountName field as part of eventlistener. - tolerations = el.Spec.PodTemplate.Tolerations - nodeSelector = el.Spec.PodTemplate.NodeSelector - serviceAccountName = el.Spec.ServiceAccountName - - if el.Spec.Resources.KubernetesResource != nil { - if len(el.Spec.Resources.KubernetesResource.Template.Spec.Tolerations) != 0 { - tolerations = el.Spec.Resources.KubernetesResource.Template.Spec.Tolerations - } - if len(el.Spec.Resources.KubernetesResource.Template.Spec.NodeSelector) != 0 { - nodeSelector = el.Spec.Resources.KubernetesResource.Template.Spec.NodeSelector - } - if el.Spec.Resources.KubernetesResource.Template.Spec.ServiceAccountName != "" { - serviceAccountName = el.Spec.Resources.KubernetesResource.Template.Spec.ServiceAccountName - } - annotations = el.Spec.Resources.KubernetesResource.Template.Annotations - podlabels = mergeMaps(podlabels, el.Spec.Resources.KubernetesResource.Template.Labels) - if len(el.Spec.Resources.KubernetesResource.Template.Spec.Containers) != 0 { - resources = el.Spec.Resources.KubernetesResource.Template.Spec.Containers[0].Resources - } - } - isMultiNS := true - if len(el.Spec.NamespaceSelector.MatchNames) == 0 { - isMultiNS = false - } - container := corev1.Container{ - Name: "event-listener", - Image: *elImage, - Ports: []corev1.ContainerPort{{ - ContainerPort: int32(*ElPort), - Protocol: corev1.ProtocolTCP, - }}, - LivenessProbe: &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/live", - Scheme: corev1.URISchemeHTTP, - Port: intstr.FromInt((*ElPort)), - }, - }, - PeriodSeconds: int32(*PeriodSeconds), - FailureThreshold: int32(*FailureThreshold), - }, - ReadinessProbe: &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/live", - Scheme: corev1.URISchemeHTTP, - Port: intstr.FromInt((*ElPort)), - }, - }, - PeriodSeconds: int32(*PeriodSeconds), - FailureThreshold: int32(*FailureThreshold), - }, - Resources: resources, - Args: []string{ - "-el-name", el.Name, - "-el-namespace", el.Namespace, - "-port", strconv.Itoa(*ElPort), - "-readtimeout", strconv.FormatInt(*ELReadTimeOut, 10), - "-writetimeout", strconv.FormatInt(*ELWriteTimeOut, 10), - "-idletimeout", strconv.FormatInt(*ELIdleTimeOut, 10), - "-timeouthandler", strconv.FormatInt(*ELTimeOutHandler, 10), - "-is-multi-ns", strconv.FormatBool(isMultiNS), - }, - VolumeMounts: []corev1.VolumeMount{{ - Name: "config-logging", - MountPath: "/etc/config-logging", - }}, - Env: []corev1.EnvVar{{ - Name: "SYSTEM_NAMESPACE", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.namespace", - }, - }, - }}, - } - - deployment := &appsv1.Deployment{ - ObjectMeta: generateObjectMeta(el), - Spec: appsv1.DeploymentSpec{ - Replicas: replicas, - Selector: &metav1.LabelSelector{ - MatchLabels: GenerateResourceLabels(el.Name), - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: podlabels, - Annotations: annotations, - }, - Spec: corev1.PodSpec{ - Tolerations: tolerations, - NodeSelector: nodeSelector, - ServiceAccountName: serviceAccountName, - Containers: []corev1.Container{container}, + container := getContainer(el) + deployment := getDeployment(el) - Volumes: []corev1.Volume{{ - Name: "config-logging", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: eventListenerConfigMapName, - }, - }, - }, - }}, - }, - }, - }, - } existingDeployment, err := r.deploymentLister.Deployments(el.Namespace).Get(el.Status.Configuration.GeneratedResourceName) switch { case err == nil: @@ -406,7 +286,7 @@ func (r *Reconciler) reconcileDeployment(ctx context.Context, logger *zap.Sugare updated := reconcileObjectMeta(&existingDeployment.ObjectMeta, deployment.ObjectMeta) if *existingDeployment.Spec.Replicas != *deployment.Spec.Replicas { if el.Spec.Replicas != nil { - existingDeployment.Spec.Replicas = replicas + existingDeployment.Spec.Replicas = deployment.Spec.Replicas updated = true } // if no replicas found as part of el.Spec then replicas from existingDeployment will be considered @@ -456,6 +336,14 @@ func (r *Reconciler) reconcileDeployment(ctx context.Context, logger *zap.Sugare existingDeployment.Spec.Template.Spec.Containers[0].Args = container.Args updated = true } + if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[0].LivenessProbe, container.LivenessProbe) { + existingDeployment.Spec.Template.Spec.Containers[0].LivenessProbe = container.LivenessProbe + updated = true + } + if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[0].ReadinessProbe, container.ReadinessProbe) { + existingDeployment.Spec.Template.Spec.Containers[0].ReadinessProbe = container.ReadinessProbe + updated = true + } if existingDeployment.Spec.Template.Spec.Containers[0].Command != nil { existingDeployment.Spec.Template.Spec.Containers[0].Command = nil updated = true @@ -464,7 +352,11 @@ func (r *Reconciler) reconcileDeployment(ctx context.Context, logger *zap.Sugare existingDeployment.Spec.Template.Spec.Containers[0].Resources = container.Resources updated = true } - if len(existingDeployment.Spec.Template.Spec.Containers[0].VolumeMounts) == 0 { + if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[0].Env, container.Env) { + existingDeployment.Spec.Template.Spec.Containers[0].Env = container.Env + updated = true + } + if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[0].VolumeMounts, deployment.Spec.Template.Spec.Containers[0].VolumeMounts) { existingDeployment.Spec.Template.Spec.Containers[0].VolumeMounts = container.VolumeMounts updated = true } @@ -497,6 +389,195 @@ func (r *Reconciler) reconcileDeployment(ctx context.Context, logger *zap.Sugare return nil } +func getDeployment(el *v1alpha1.EventListener) *appsv1.Deployment { + var replicas = ptr.Int32(1) + if el.Spec.Replicas != nil { + replicas = el.Spec.Replicas + } + var ( + tolerations []corev1.Toleration + nodeSelector, annotations, podlabels map[string]string + serviceAccountName string + ) + podlabels = mergeMaps(el.Labels, GenerateResourceLabels(el.Name)) + + // For backward compatibility with podTemplate, serviceAccountName field as part of eventlistener. + tolerations = el.Spec.PodTemplate.Tolerations + nodeSelector = el.Spec.PodTemplate.NodeSelector + serviceAccountName = el.Spec.ServiceAccountName + + vol := []corev1.Volume{{ + Name: "config-logging", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: eventListenerConfigMapName, + }, + }, + }, + }} + + container := getContainer(el) + for _, v := range container.Env { + // If TLS related env are set then mount secret volume which will be used while starting the eventlistener. + if v.Name == "TLS_CERT" { + vol = append(vol, corev1.Volume{ + Name: "https-connection", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: v.ValueFrom.SecretKeyRef.Name, + }, + }, + }) + } + } + if el.Spec.Resources.KubernetesResource != nil { + if len(el.Spec.Resources.KubernetesResource.Template.Spec.Tolerations) != 0 { + tolerations = el.Spec.Resources.KubernetesResource.Template.Spec.Tolerations + } + if len(el.Spec.Resources.KubernetesResource.Template.Spec.NodeSelector) != 0 { + nodeSelector = el.Spec.Resources.KubernetesResource.Template.Spec.NodeSelector + } + if el.Spec.Resources.KubernetesResource.Template.Spec.ServiceAccountName != "" { + serviceAccountName = el.Spec.Resources.KubernetesResource.Template.Spec.ServiceAccountName + } + annotations = el.Spec.Resources.KubernetesResource.Template.Annotations + podlabels = mergeMaps(podlabels, el.Spec.Resources.KubernetesResource.Template.Labels) + } + + return &appsv1.Deployment{ + ObjectMeta: generateObjectMeta(el), + Spec: appsv1.DeploymentSpec{ + Replicas: replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: GenerateResourceLabels(el.Name), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: podlabels, + Annotations: annotations, + }, + Spec: corev1.PodSpec{ + Tolerations: tolerations, + NodeSelector: nodeSelector, + ServiceAccountName: serviceAccountName, + Containers: []corev1.Container{container}, + Volumes: vol, + }, + }, + }, + } +} + +func getContainer(el *v1alpha1.EventListener) corev1.Container { + var ( + elCert, elKey string + resources corev1.ResourceRequirements + ) + + vMount := []corev1.VolumeMount{{ + Name: "config-logging", + MountPath: "/etc/config-logging", + }} + + env := []corev1.EnvVar{{ + Name: "SYSTEM_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }} + + certEnv := map[string]*corev1.EnvVarSource{} + if el.Spec.Resources.KubernetesResource != nil { + if len(el.Spec.Resources.KubernetesResource.Template.Spec.Containers) != 0 { + resources = el.Spec.Resources.KubernetesResource.Template.Spec.Containers[0].Resources + for i, e := range el.Spec.Resources.KubernetesResource.Template.Spec.Containers[0].Env { + env = append(env, e) + certEnv[el.Spec.Resources.KubernetesResource.Template.Spec.Containers[0].Env[i].Name] = + el.Spec.Resources.KubernetesResource.Template.Spec.Containers[0].Env[i].ValueFrom + } + } + } + + var scheme corev1.URIScheme + if v, ok := certEnv["TLS_CERT"]; ok { + elCert = "/etc/triggers/tls/" + v.SecretKeyRef.Key + } else { + elCert = "" + } + if v, ok := certEnv["TLS_KEY"]; ok { + elKey = "/etc/triggers/tls/" + v.SecretKeyRef.Key + } else { + elKey = "" + } + + if elCert != "" && elKey != "" { + *ElPort = 8443 + scheme = corev1.URISchemeHTTPS + vMount = append(vMount, corev1.VolumeMount{ + Name: "https-connection", + ReadOnly: true, + MountPath: "/etc/triggers/tls", + }) + } else { + *ElPort = 8080 + scheme = corev1.URISchemeHTTP + } + + isMultiNS := true + if len(el.Spec.NamespaceSelector.MatchNames) == 0 { + isMultiNS = false + } + + return corev1.Container{ + Name: "event-listener", + Image: *elImage, + Ports: []corev1.ContainerPort{{ + ContainerPort: int32(*ElPort), + Protocol: corev1.ProtocolTCP, + }}, + LivenessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/live", + Scheme: scheme, + Port: intstr.FromInt((*ElPort)), + }, + }, + PeriodSeconds: int32(*PeriodSeconds), + FailureThreshold: int32(*FailureThreshold), + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/live", + Scheme: scheme, + Port: intstr.FromInt((*ElPort)), + }, + }, + PeriodSeconds: int32(*PeriodSeconds), + FailureThreshold: int32(*FailureThreshold), + }, + Resources: resources, + Args: []string{ + "-el-name", el.Name, + "-el-namespace", el.Namespace, + "-port", strconv.Itoa(*ElPort), + "-readtimeout", strconv.FormatInt(*ELReadTimeOut, 10), + "-writetimeout", strconv.FormatInt(*ELWriteTimeOut, 10), + "-idletimeout", strconv.FormatInt(*ELIdleTimeOut, 10), + "-timeouthandler", strconv.FormatInt(*ELTimeOutHandler, 10), + "-is-multi-ns", strconv.FormatBool(isMultiNS), + "-tls-cert", elCert, + "-tls-key", elKey, + }, + VolumeMounts: vMount, + Env: env, + } +} + // GenerateResourceLabels generates the labels to be used on all generated resources. func GenerateResourceLabels(eventListenerName string) map[string]string { resourceLabels := make(map[string]string, len(StaticResourceLabels)+1) diff --git a/pkg/reconciler/v1alpha1/eventlistener/eventlistener_test.go b/pkg/reconciler/v1alpha1/eventlistener/eventlistener_test.go index e5011f43db..a5b9061cef 100644 --- a/pkg/reconciler/v1alpha1/eventlistener/eventlistener_test.go +++ b/pkg/reconciler/v1alpha1/eventlistener/eventlistener_test.go @@ -250,6 +250,99 @@ func makeDeployment(ops ...func(d *appsv1.Deployment)) *appsv1.Deployment { return &d } +var withTLSConfig = func(d *appsv1.Deployment) { + d.Spec.Template.Spec.Containers = []corev1.Container{{ + Name: "event-listener", + Image: *elImage, + Ports: []corev1.ContainerPort{{ + ContainerPort: int32(8443), + Protocol: corev1.ProtocolTCP, + }}, + LivenessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/live", + Scheme: corev1.URISchemeHTTPS, + Port: intstr.FromInt((8443)), + }, + }, + PeriodSeconds: int32(*PeriodSeconds), + FailureThreshold: int32(*FailureThreshold), + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/live", + Scheme: corev1.URISchemeHTTPS, + Port: intstr.FromInt((8443)), + }, + }, + PeriodSeconds: int32(*PeriodSeconds), + FailureThreshold: int32(*FailureThreshold), + }, + Args: []string{ + "-el-name", eventListenerName, + "-el-namespace", namespace, + "-port", strconv.Itoa(8443), + "-tls-cert", "/etc/triggers/tls/tls.pem", + "-tls-key", "/etc/triggers/tls/tls.key", + }, + VolumeMounts: []corev1.VolumeMount{{ + Name: "config-logging", + MountPath: "/etc/config-logging", + }, { + Name: "https-connection", + MountPath: "/etc/triggers/tls", + ReadOnly: true, + }}, + Env: []corev1.EnvVar{{ + Name: "SYSTEM_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, { + Name: "TLS_CERT", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "tls-secret-key", + }, + Key: "tls.crt", + }, + }, + }, { + Name: "TLS_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "tls-secret-key", + }, + Key: "tls.key", + }, + }, + }}, + }} + d.Spec.Template.Spec.Volumes = []corev1.Volume{{ + Name: "config-logging", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: eventListenerConfigMapName, + }, + }, + }, + }, { + Name: "https-connection", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "tls-secret-key", + }, + }, + }} +} + // makeService is a helper to build a Service that is created by an EventListener. // It generates a basic Service for the simplest EventListener and accepts functions for modification. func makeService(ops ...func(*corev1.Service)) *corev1.Service { @@ -289,6 +382,10 @@ func logConfig(ns string) *corev1.ConfigMap { return lc } +var withTLSPort = bldr.EventListenerStatus( + bldr.EventListenerAddress(listenerHostname(generatedResourceName, namespace, 8443)), +) + var withStatus = bldr.EventListenerStatus( bldr.EventListenerConfig(generatedResourceName), bldr.EventListenerAddress(listenerHostname(generatedResourceName, namespace, *ElPort)), @@ -400,6 +497,40 @@ func TestReconcile(t *testing.T) { } }) + elWithTLSConnection := makeEL(withStatus, withTLSPort, func(el *v1alpha1.EventListener) { + el.Spec.Resources.KubernetesResource = &v1alpha1.KubernetesResource{ + WithPodSpec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Env: []corev1.EnvVar{{ + Name: "TLS_CERT", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "tls-secret-key", + }, + Key: "tls.crt", + }, + }, + }, { + Name: "TLS_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "tls-secret-key", + }, + Key: "tls.key", + }, + }, + }}, + }}, + }, + }, + }, + } + }) + elWithKubernetesResourceForObjectMeta := makeEL(withStatus, func(el *v1alpha1.EventListener) { el.Spec.Resources.KubernetesResource = &v1alpha1.KubernetesResource{ WithPodSpec: duckv1.WithPodSpec{ @@ -464,6 +595,8 @@ func TestReconcile(t *testing.T) { } }) + deploymentWithTLSConnection := makeDeployment(withTLSConfig) + deploymentForKubernetesResourceObjectMeta := makeDeployment(func(d *appsv1.Deployment) { d.Spec.Template.ObjectMeta.Labels = map[string]string{ "app.kubernetes.io/managed-by": "EventListener", @@ -493,6 +626,13 @@ func TestReconcile(t *testing.T) { s.Spec.Ports[0].NodePort = 30000 }) + elServiceWithTLSConnection := makeService(func(s *corev1.Service) { + s.Spec.Ports[0].Port = int32(8443) + s.Spec.Ports[0].TargetPort = intstr.IntOrString{ + IntVal: int32(8443), + } + }) + loggingConfigMap := defaultLoggingConfigMap() loggingConfigMap.ObjectMeta.Namespace = namespace reconcilerLoggingConfigMap := defaultLoggingConfigMap() @@ -798,6 +938,21 @@ func TestReconcile(t *testing.T) { Deployments: []*appsv1.Deployment{deploymentForKubernetesResourceObjectMeta}, Services: []*corev1.Service{elService}, }, + }, { + name: "eventlistener with TLS connection", + key: reconcileKey, + startResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithTLSConnection}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + }, + endResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithTLSConnection}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + Deployments: []*appsv1.Deployment{deploymentWithTLSConnection}, + Services: []*corev1.Service{elServiceWithTLSConnection}, + }, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/sink/initialization.go b/pkg/sink/initialization.go index 794a030759..67a271c4b9 100644 --- a/pkg/sink/initialization.go +++ b/pkg/sink/initialization.go @@ -53,6 +53,10 @@ var ( "The timeout for Timeout Handler of EventListener Server.") isMultiNSFlag = flag.Bool("is-multi-ns", false, "Whether EventListener serve Multiple NS.") + tlsCertFlag = flag.String("tls-cert", "", + "The filename for the TLS certificate.") + tlsKeyFlag = flag.String("tls-key", "", + "The filename for the TLS key.") ) // Args define the arguments for Sink. @@ -73,6 +77,10 @@ type Args struct { ELTimeOutHandler time.Duration // IsMultiNS determines whether el functions as namespaced or clustered IsMultiNS bool + // Key defines the filename for tls Key. + Key string + // Cert defines the filename for tls Cert. + Cert string } // Clients define the set of client dependencies Sink requires. @@ -104,6 +112,8 @@ func GetArgs() (Args, error) { ELWriteTimeOut: time.Duration(*elWriteTimeOut), ELIdleTimeOut: time.Duration(*elIdleTimeOut), ELTimeOutHandler: time.Duration(*elTimeOutHandler), + Cert: *tlsCertFlag, + Key: *tlsKeyFlag, }, nil }