From 39fb9d7a8311d4743940b542bfd6fe7d8f3d153d Mon Sep 17 00:00:00 2001 From: savitaashture Date: Wed, 17 Feb 2021 00:24:37 +0530 Subject: [PATCH] Add support for custom object to triggers eventlistener --- config/200-clusterrole.yaml | 4 +- config/300-eventlistener.yaml | 8 +- docs/eventlisteners.md | 84 ++- examples/custom-resource/README.md | 35 ++ ...ithub-knative-listener-customresource.yaml | 84 +++ examples/custom-resource/rbac.yaml | 1 + examples/custom-resource/secret.yaml | 7 + examples/rbac.yaml | 6 +- go.mod | 1 + go.sum | 13 + .../triggers/v1alpha1/event_listener_types.go | 18 + .../v1alpha1/event_listener_types_test.go | 11 + .../v1alpha1/event_listener_validation.go | 39 +- .../event_listener_validation_test.go | 131 +++++ .../v1alpha1/zz_generated.deepcopy.go | 22 + pkg/dynamic/custom_reconcile.go | 263 +++++++++ pkg/dynamic/custom_reconcile_test.go | 334 +++++++++++ pkg/dynamic/dynamic.go | 60 ++ .../v1alpha1/eventlistener/controller.go | 9 +- .../v1alpha1/eventlistener/eventlistener.go | 310 ++++++++--- .../eventlistener/eventlistener_test.go | 526 +++++++++++++++--- test/controller.go | 81 ++- test/controller_test.go | 24 + test/wait.go | 3 +- .../ducks/duck/v1/podspecable/fake/fake.go | 30 + .../ducks/duck/v1/podspecable/podspecable.go | 60 ++ .../clients/dynamicclient/dynamicclient.go | 49 ++ .../clients/dynamicclient/fake/fake.go | 53 ++ vendor/modules.txt | 5 + 29 files changed, 2099 insertions(+), 172 deletions(-) create mode 100644 examples/custom-resource/README.md create mode 100644 examples/custom-resource/github-knative-listener-customresource.yaml create mode 120000 examples/custom-resource/rbac.yaml create mode 100644 examples/custom-resource/secret.yaml create mode 100644 pkg/dynamic/custom_reconcile.go create mode 100644 pkg/dynamic/custom_reconcile_test.go create mode 100644 pkg/dynamic/dynamic.go create mode 100644 vendor/knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/fake/fake.go create mode 100644 vendor/knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/podspecable.go create mode 100644 vendor/knative.dev/pkg/injection/clients/dynamicclient/dynamicclient.go create mode 100644 vendor/knative.dev/pkg/injection/clients/dynamicclient/fake/fake.go diff --git a/config/200-clusterrole.yaml b/config/200-clusterrole.yaml index 813997383..10dfcdd89 100644 --- a/config/200-clusterrole.yaml +++ b/config/200-clusterrole.yaml @@ -39,7 +39,9 @@ rules: - apiGroups: ["coordination.k8s.io"] resources: ["leases"] verbs: ["get", "list", "create", "update", "delete", "patch", "watch"] - + - apiGroups: ["serving.knative.dev"] + resources: ["*", "*/status", "*/finalizers"] + verbs: ["get", "list", "create", "update", "delete", "deletecollection", "patch", "watch"] --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 diff --git a/config/300-eventlistener.yaml b/config/300-eventlistener.yaml index 969b98c2f..9956de060 100644 --- a/config/300-eventlistener.yaml +++ b/config/300-eventlistener.yaml @@ -61,4 +61,10 @@ spec: jsonPath: ".status.conditions[?(@.type=='Available')].status" - name: Reason type: string - jsonPath: ".status.conditions[?(@.type=='Available')].reason" \ No newline at end of file + jsonPath: ".status.conditions[?(@.type=='Available')].reason" + - name: Ready + type: string + jsonPath: ".status.conditions[?(@.type=='Ready')].status" + - name: Reason + type: string + jsonPath: ".status.conditions[?(@.type=='Ready')].reason" diff --git a/docs/eventlisteners.md b/docs/eventlisteners.md index c33981d6d..63014ed75 100644 --- a/docs/eventlisteners.md +++ b/docs/eventlisteners.md @@ -22,6 +22,9 @@ using [Event Interceptors](#Interceptors). - [Replicas](#replicas) - [PodTemplate](#podtemplate) - [Resources](#resources) + - [kubernetesResource](#kubernetesresource) + - [CustomResource](#customresource) + - [Contract](#contract) - [Logging](#logging) - [NamespaceSelector](#namespaceSelector) - [Labels](#labels) @@ -45,6 +48,7 @@ using [Event Interceptors](#Interceptors). - [ServiceAccount per EventListenerTrigger](#serviceaccount-per-eventlistenertrigger) - [EventListener Secure Connection](#eventlistener-secure-connection) - [Prerequisites](#prerequisites) + - [EventListener Response Format](#eventlistener-response-format) ## Syntax @@ -70,7 +74,7 @@ the following fields: - [`replicas`](#replicas) - Specifies the number of EventListener pods - [`podTemplate`](#podTemplate) - Specifies the PodTemplate for your EventListener pod - - [`resources`](#resources) - Specifies the Kubernetes Resource information + - [`resources`](#resources) - Specifies the Kubernetes/Custom Resource shape for the EventListener sink for your EventListener pod - [`namespaceSelector`](#namespaceSelector) - Specifies the namespaces where EventListener can fetch triggers from and create Tekton resources. @@ -225,8 +229,11 @@ For more info on the design refer [TEP-0008](https://github.com/tektoncd/communi Right now the `resources` field is optional in order to support backward compatibility with original behavior of `podTemplate`, `serviceType` and `serviceAccountName` fieds. In the future, we plan to remove `serviceType` and `podTemplate` from the EventListener spec in favor of the `resources` field. -For now `resources` has support for `kubernetesResource` but later it will have a support for Custom CRD`(ex: Knative Service)` as `customResource` +`resources` has support for +* **kubernetesResource** +* **CustomResource** for Custom CRD`(ex: Knative Service)` +#### kubernetesResource `kubernetesResource` have two fields * ServiceType * Spec(PodTemplateSpec) @@ -257,7 +264,60 @@ spec: With the help of `kubernetesResource` user can specify [PodTemplateSpec](https://github.com/kubernetes/api/blob/master/core/v1/types.go#L3704). -Right now the allowed values as part of `podSpec` are +#### CustomResource +A `CustomResource` object has one field that supports dynamic objects. +* runtime.RawExtension + +Here we will use a [Knative Service](https://knative.dev/docs/) as an example to demonstrate usage of `CustomResource` + +**Note:** `Knative Should be installed on the cluster` [ref](https://github.com/tektoncd/community/blob/main/teps/0008-support-knative-service-for-triggers-eventlistener-pod.md#note) +```yaml +spec: + resources: + customResource: + apiVersion: serving.knative.dev/v1 + kind: Service +# metadata: +# name: knativeservice # name field is optional if not provided Triggers will use el name with the el- prefix ex: el-github-knative-listener + spec: + template: + spec: + serviceAccountName: tekton-triggers-example-sa + containers: + - resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" +``` + +With the help of `CustomResource` user can specify any dynamic object which adheres to the Contract described below. + +##### Contract +For Knative or any new CRD should satisfy [WithPod{}](https://github.com/knative/pkg/blob/master/apis/duck/v1/podspec_types.go#L41) + +**Spec** +```spec + spec: + template: + metadata: + spec: +``` +**Status** +```status +type EventListenerStatus struct { + duckv1beta1.Status `json:",inline"` + + // EventListener is Addressable. It currently exposes the service DNS + // address of the the EventListener sink + duckv1alpha1.AddressStatus `json:",inline"` +} +``` + +##### Note +For both `CustomResource` and `kubernetesResource` the allowed values for `PodSpec` and `Containers` are ```text ServiceAccountName NodeSelector @@ -853,3 +913,21 @@ To setup TLS connection add two set of reserved environment variables `TLS_CERT` 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. + +## EventListener Response Format + +``` +kubectl get el +NAME ADDRESS AVAILABLE REASON READY REASON +tls-listener-interceptor http://el-tls-listener-interceptor.default.svc.cluster.local True MinimumReplicasAvailable +``` +Where + +* **NAME:** Name of the created eventlistener +* **ADDRESS:** Address of the eventlistener +* **AVAILABLE** This state indicates readiness of Kubernetes Deployment and Service +* **REASON** Shows the failure reason for Kubernetes Deployment and Service +* **READY** This state indicates readiness of Custom Resource ex: Knative Service +* **REASON** Shows the failure reason for Custom Resource + +**Note:** The response format will be refactored as part of [issue-932](https://github.com/tektoncd/triggers/issues/932) diff --git a/examples/custom-resource/README.md b/examples/custom-resource/README.md new file mode 100644 index 000000000..e88624910 --- /dev/null +++ b/examples/custom-resource/README.md @@ -0,0 +1,35 @@ +## GitHub Knative EventListener + +Creates an EventListener that listens for GitHub webhook events. + +### Try it out locally: + +1. To create the custom resource trigger and all related resources, run: + + ```bash + kubectl apply -f examples/custom-resource/ + ``` + +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"}}' \ + http://localhost:8080 + ``` + + The response status code is `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 will see the newly created TaskRun: + + ```bash + kubectl get taskruns | grep github-run- + ``` diff --git a/examples/custom-resource/github-knative-listener-customresource.yaml b/examples/custom-resource/github-knative-listener-customresource.yaml new file mode 100644 index 000000000..1db7be1b9 --- /dev/null +++ b/examples/custom-resource/github-knative-listener-customresource.yaml @@ -0,0 +1,84 @@ +--- +apiVersion: triggers.tekton.dev/v1alpha1 +kind: EventListener +metadata: + name: github-knative-listener +spec: + triggers: + - name: github-listener + interceptors: + - github: + secretRef: + secretName: github-secret + secretKey: secretToken + eventTypes: + - pull_request + - cel: + filter: "body.action in ['opened', 'synchronize', 'reopened']" + bindings: + - ref: github-pr-binding + template: + ref: github-template + resources: + customResource: + apiVersion: serving.knative.dev/v1 + kind: Service + spec: + template: + spec: + serviceAccountName: tekton-triggers-example-sa + containers: + - resources: + requests: + memory: "64Mi" + cpu: "250m" + limits: + memory: "128Mi" + cpu: "500m" +--- +apiVersion: triggers.tekton.dev/v1alpha1 +kind: TriggerBinding +metadata: + name: github-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: github-template +spec: + params: + - name: gitrevision + - name: gitrepositoryurl + resourcetemplates: + - apiVersion: tekton.dev/v1alpha1 + kind: TaskRun + metadata: + generateName: github-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/examples/custom-resource/rbac.yaml b/examples/custom-resource/rbac.yaml new file mode 120000 index 000000000..ba3671942 --- /dev/null +++ b/examples/custom-resource/rbac.yaml @@ -0,0 +1 @@ +../rbac.yaml \ No newline at end of file diff --git a/examples/custom-resource/secret.yaml b/examples/custom-resource/secret.yaml new file mode 100644 index 000000000..beb4f9c89 --- /dev/null +++ b/examples/custom-resource/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: github-secret +type: Opaque +stringData: + secretToken: "1234567" diff --git a/examples/rbac.yaml b/examples/rbac.yaml index 407b04a4e..a3d870341 100644 --- a/examples/rbac.yaml +++ b/examples/rbac.yaml @@ -13,11 +13,11 @@ rules: resources: ["eventlisteners", "triggerbindings", "triggertemplates", "triggers"] verbs: ["get", "list", "watch"] - apiGroups: [""] - # secrets are only needed for GitHub/GitLab interceptors - # configmaps is needed for updating logging config +# secrets are only needed for GitHub/GitLab interceptors +# configmaps is needed for updating logging config resources: ["configmaps", "secrets"] verbs: ["get", "list", "watch"] - # Permissions to create resources in associated TriggerTemplates +# Permissions to create resources in associated TriggerTemplates - apiGroups: ["tekton.dev"] resources: ["pipelineruns", "pipelineresources", "taskruns"] verbs: ["create"] diff --git a/go.mod b/go.mod index 2d03c8389..e895252c3 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/google/go-github/v31 v31.0.0 github.com/google/uuid v1.2.0 github.com/gorilla/mux v1.7.4 + github.com/sirupsen/logrus v1.7.0 github.com/spf13/cobra v1.0.0 github.com/tektoncd/pipeline v0.20.1-0.20210203144343-1b7a37f0d21d github.com/tektoncd/plumbing v0.0.0-20201021153918-6b7e894737b5 diff --git a/go.sum b/go.sum index 50650d249..0018b15fe 100644 --- a/go.sum +++ b/go.sum @@ -246,6 +246,7 @@ github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.15.0+incompatible h1:8KpYO/Xl/ZudZs5RNOEhWMBY4hmzlZhhRd9cu+jrZP4= github.com/emicklei/go-restful v2.15.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -362,6 +363,7 @@ github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -437,6 +439,7 @@ github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/licenseclassifier v0.0.0-20190926221455-842c0d70d702 h1:nVgx26pAe6l/02mYomOuZssv28XkacGw/0WeiTVorqw= github.com/google/licenseclassifier v0.0.0-20190926221455-842c0d70d702/go.mod h1:qsqn2hxC+vURpyBRygGUuinTO42MFRLcsmQ/P8v94+M= github.com/google/mako v0.0.0-20190821191249-122f8dcef9e3/go.mod h1:YzLcVlL+NqWnmUEPuhS1LxDDwGO9WNbVlEXaF4IH35g= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= @@ -535,6 +538,7 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/tdigest v0.0.0-20180711151920-a7d76c6f093a/go.mod h1:9GkyshztGufsdPQWjH+ifgnIr3xNUL5syI70g2dzU1o= github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= @@ -568,6 +572,7 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -618,6 +623,7 @@ github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3N github.com/miekg/dns v1.1.17/go.mod h1:WgzbA6oji13JREwiNsRDNfl7jYdPnmz+VEuLrA+/48M= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= @@ -691,6 +697,7 @@ github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.5 h1:UwtQQx2pyPIgWYHRg+epgdx1/HnBQTgN3/oIYEJTQzU= github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= +github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= @@ -775,6 +782,7 @@ github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= @@ -807,6 +815,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -848,6 +857,7 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/vdemeester/k8s-pkg-credentialprovider v1.19.7/go.mod h1:K2nMO14cgZitdwBqdQps9tInJgcaXcU/7q5F59lpbNI= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= @@ -1357,12 +1367,15 @@ gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLv gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/apis/triggers/v1alpha1/event_listener_types.go b/pkg/apis/triggers/v1alpha1/event_listener_types.go index 26ea2fbf4..10782c883 100644 --- a/pkg/apis/triggers/v1alpha1/event_listener_types.go +++ b/pkg/apis/triggers/v1alpha1/event_listener_types.go @@ -22,10 +22,12 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "knative.dev/pkg/apis" duckv1 "knative.dev/pkg/apis/duck/v1" duckv1alpha1 "knative.dev/pkg/apis/duck/v1alpha1" + "knative.dev/pkg/apis/duck/v1beta1" ) // Check that EventListener may be validated and defaulted. @@ -66,6 +68,11 @@ type EventListenerSpec struct { type Resources struct { KubernetesResource *KubernetesResource `json:"kubernetesResource,omitempty"` + CustomResource *CustomResource `json:"customResource,omitempty"` +} + +type CustomResource struct { + runtime.RawExtension `json:",inline"` } type KubernetesResource struct { @@ -224,6 +231,17 @@ func (els *EventListenerStatus) SetDeploymentConditions(deploymentConditions []a } } +func (els *EventListenerStatus) SetConditionsForDynamicObjects(conditions v1beta1.Conditions) { + for _, cond := range conditions { + els.SetCondition(&apis.Condition{ + Type: cond.Type, + Status: cond.Status, + Reason: cond.Reason, + Message: cond.Message, + }) + } +} + // SetExistsCondition simplifies setting the exists conditions on the // EventListenerStatus. func (els *EventListenerStatus) SetExistsCondition(cond apis.ConditionType, err error) { diff --git a/pkg/apis/triggers/v1alpha1/event_listener_types_test.go b/pkg/apis/triggers/v1alpha1/event_listener_types_test.go index 9a0ec8925..937b0d57d 100644 --- a/pkg/apis/triggers/v1alpha1/event_listener_types_test.go +++ b/pkg/apis/triggers/v1alpha1/event_listener_types_test.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/api/equality" "knative.dev/pkg/apis" duckv1 "knative.dev/pkg/apis/duck/v1" + "knative.dev/pkg/apis/duck/v1beta1" ) func TestSetGetCondition(t *testing.T) { @@ -272,3 +273,13 @@ func TestSetDeploymentConditions(t *testing.T) { }) } } + +func TestSetConditionsForDynamicObjects(t *testing.T) { + var status EventListenerStatus + status.SetConditionsForDynamicObjects(v1beta1.Conditions{{ + Type: apis.ConditionReady, + Status: corev1.ConditionTrue, + Reason: "Reason", + Message: "Message", + }}) +} diff --git a/pkg/apis/triggers/v1alpha1/event_listener_validation.go b/pkg/apis/triggers/v1alpha1/event_listener_validation.go index 4c6c938ba..b862a1125 100644 --- a/pkg/apis/triggers/v1alpha1/event_listener_validation.go +++ b/pkg/apis/triggers/v1alpha1/event_listener_validation.go @@ -17,13 +17,16 @@ limitations under the License. package v1alpha1 import ( + "bytes" "context" + "encoding/json" "fmt" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation" "knative.dev/pkg/apis" + duckv1 "knative.dev/pkg/apis/duck/v1" ) var ( @@ -57,9 +60,43 @@ func (s *EventListenerSpec) validate(ctx context.Context) (errs *apis.FieldError for i, trigger := range s.Triggers { errs = errs.Also(trigger.validate(ctx).ViaField(fmt.Sprintf("spec.triggers[%d]", i))) } + // Both Kubernetes and Custom resource can't be present at the same time + if s.Resources.KubernetesResource != nil && s.Resources.CustomResource != nil { + return apis.ErrMultipleOneOf("spec.resources.kubernetesResource", "spec.resources.customResource") + } + if s.Resources.KubernetesResource != nil { errs = errs.Also(validateKubernetesObject(s.Resources.KubernetesResource).ViaField("spec.resources.kubernetesResource")) } + + if s.Resources.CustomResource != nil { + errs = errs.Also(validateCustomObject(s.Resources.CustomResource).ViaField("spec.resources.customResource")) + } + return errs +} + +func validateCustomObject(customData *CustomResource) (errs *apis.FieldError) { + orig := duckv1.WithPod{} + decoder := json.NewDecoder(bytes.NewBuffer(customData.RawExtension.Raw)) + + if err := decoder.Decode(&orig); err != nil { + errs = errs.Also(apis.ErrInvalidValue(err, "spec")) + } + + if len(orig.Spec.Template.Spec.Containers) > 1 { + errs = errs.Also(apis.ErrMultipleOneOf("containers").ViaField("spec.template.spec")) + } + errs = errs.Also(apis.CheckDisallowedFields(orig.Spec.Template.Spec, + *podSpecMask(&orig.Spec.Template.Spec)).ViaField("spec.template.spec")) + + // bounded by condition because containers fields are optional so there is a chance that containers can be nil. + if len(orig.Spec.Template.Spec.Containers) == 1 { + errs = errs.Also(apis.CheckDisallowedFields(orig.Spec.Template.Spec.Containers[0], + *containerFieldMask(&orig.Spec.Template.Spec.Containers[0])).ViaField("spec.template.spec.containers[0]")) + // validate env + errs = errs.Also(validateEnv(orig.Spec.Template.Spec.Containers[0].Env).ViaField("spec.template.spec.containers[0].env")) + } + return errs } @@ -168,7 +205,6 @@ func containerFieldMask(in *corev1.Container) *corev1.Container { out.VolumeMounts = nil out.ImagePullPolicy = "" out.Lifecycle = nil - out.SecurityContext = nil out.Stdin = false out.StdinOnce = false out.TerminationMessagePath = "" @@ -195,7 +231,6 @@ func podSpecMask(in *corev1.PodSpec) *corev1.PodSpec { // Disallowed fields // This list clarifies which all podspec fields are not allowed. out.Volumes = nil - out.ImagePullSecrets = nil out.EnableServiceLinks = nil out.ImagePullSecrets = nil out.InitContainers = nil diff --git a/pkg/apis/triggers/v1alpha1/event_listener_validation_test.go b/pkg/apis/triggers/v1alpha1/event_listener_validation_test.go index 563a8041e..87cb3983c 100644 --- a/pkg/apis/triggers/v1alpha1/event_listener_validation_test.go +++ b/pkg/apis/triggers/v1alpha1/event_listener_validation_test.go @@ -21,10 +21,12 @@ import ( "testing" "github.com/tektoncd/triggers/pkg/apis/triggers/v1alpha1" + "github.com/tektoncd/triggers/test" bldr "github.com/tektoncd/triggers/test/builder" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/pkg/ptr" ) @@ -212,6 +214,29 @@ func Test_EventListenerValidate(t *testing.T) { }), )), )), + }, { + name: "Valid EventListener with custom resources", + el: &v1alpha1.EventListener{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: v1alpha1.EventListenerSpec{ + Triggers: []v1alpha1.EventListenerTrigger{{ + Bindings: []*v1alpha1.EventListenerBinding{{ + Ref: "tb", + Kind: "TriggerBinding", + APIVersion: "v1alpha1", + }}, + TriggerRef: "triggerref", + }}, + Resources: v1alpha1.Resources{ + CustomResource: &v1alpha1.CustomResource{ + RawExtension: getValidRawData(t), + }, + }, + }, + }, }} for _, test := range tests { @@ -575,6 +600,53 @@ func TestEventListenerValidate_error(t *testing.T) { }), )), )), + }, { + name: "user specify both kubernetes and custom resources", + el: &v1alpha1.EventListener{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: v1alpha1.EventListenerSpec{ + Triggers: []v1alpha1.EventListenerTrigger{{ + Bindings: []*v1alpha1.EventListenerBinding{{ + Ref: "tb", + Kind: "TriggerBinding", + APIVersion: "v1alpha1", + }}, + }}, + Resources: v1alpha1.Resources{ + KubernetesResource: &v1alpha1.KubernetesResource{ + ServiceType: "NodePort", + }, + CustomResource: &v1alpha1.CustomResource{ + RawExtension: runtime.RawExtension{Raw: []byte(`{"rt1": "value"}`)}, + }, + }, + }, + }, + }, { + name: "user specify multiple containers, unsupported podspec and container field in custom resources", + el: &v1alpha1.EventListener{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: v1alpha1.EventListenerSpec{ + Triggers: []v1alpha1.EventListenerTrigger{{ + Bindings: []*v1alpha1.EventListenerBinding{{ + Ref: "tb", + Kind: "TriggerBinding", + APIVersion: "v1alpha1", + }}, + }}, + Resources: v1alpha1.Resources{ + CustomResource: &v1alpha1.CustomResource{ + RawExtension: getRawData(t), + }, + }, + }, + }, }} for _, test := range tests { @@ -585,3 +657,62 @@ func TestEventListenerValidate_error(t *testing.T) { }) } } + +func getRawData(t *testing.T) runtime.RawExtension { + return test.RawExtension(t, duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "knativeservice", + }, + Spec: duckv1.WithPodSpec{Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + ServiceAccountName: "tekton-triggers-example-sa", + NodeName: "minikube", + Containers: []corev1.Container{{ + Name: "first-container", + }, { + Env: []corev1.EnvVar{{ + Name: "key", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "test"}, + Key: "a.crt", + }, + }, + }}, + }}, + }, + }}, + }) +} + +func getValidRawData(t *testing.T) runtime.RawExtension { + return test.RawExtension(t, duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "knativeservice", + }, + Spec: duckv1.WithPodSpec{Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + ServiceAccountName: "tekton-triggers-example-sa", + Containers: []corev1.Container{{ + Env: []corev1.EnvVar{{ + Name: "key", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "test"}, + Key: "a.crt", + }, + }, + }}, + }}, + }, + }}, + }) +} diff --git a/pkg/apis/triggers/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/triggers/v1alpha1/zz_generated.deepcopy.go index 19b387fff..6fae22063 100644 --- a/pkg/apis/triggers/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/triggers/v1alpha1/zz_generated.deepcopy.go @@ -150,6 +150,23 @@ func (in *ClusterTriggerBindingList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CustomResource) DeepCopyInto(out *CustomResource) { + *out = *in + in.RawExtension.DeepCopyInto(&out.RawExtension) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResource. +func (in *CustomResource) DeepCopy() *CustomResource { + if in == nil { + return nil + } + out := new(CustomResource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EventListener) DeepCopyInto(out *EventListener) { *out = *in @@ -485,6 +502,11 @@ func (in *Resources) DeepCopyInto(out *Resources) { *out = new(KubernetesResource) (*in).DeepCopyInto(*out) } + if in.CustomResource != nil { + in, out := &in.CustomResource, &out.CustomResource + *out = new(CustomResource) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/dynamic/custom_reconcile.go b/pkg/dynamic/custom_reconcile.go new file mode 100644 index 000000000..948d951af --- /dev/null +++ b/pkg/dynamic/custom_reconcile.go @@ -0,0 +1,263 @@ +/* +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 dynamic + +import ( + "encoding/json" + "reflect" + + logger "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "knative.dev/pkg/apis/duck/v1beta1" +) + +func ReconcileCustomObject(existing, desired *unstructured.Unstructured) (updated bool) { + originalMetaLabel, change := getNestedFieldCopyData(existing, desired, "metadata", "labels") + if !change { + updated = true + if err := unstructured.SetNestedField(existing.Object, originalMetaLabel, "metadata", "labels"); err != nil { + logger.Error("failed to set metadata labels to existing object: ", err) + updated = false + } + } + + originalMetaOwner, change := getNestedFieldCopyData(existing, desired, "metadata", "ownerReferences") + if !change { + updated = true + if err := unstructured.SetNestedField(existing.Object, originalMetaOwner, "metadata", "ownerReferences"); err != nil { + logger.Error("failed to set metadata ownerReferences to existing object: ", err) + updated = false + } + } + + existingMetaAnno, _, _ := unstructured.NestedFieldCopy(existing.Object, "metadata", "annotations") + originalMetaAnno, _, _ := unstructured.NestedFieldCopy(desired.Object, "metadata", "annotations") + originalAnno, _ := originalMetaAnno.(map[string]string) + existingAnno, _ := existingMetaAnno.(map[string]string) + if !reflect.DeepEqual(existingMetaAnno, MergeMaps(existingAnno, originalAnno)) { + updated = true + if err := unstructured.SetNestedField(existing.Object, originalMetaAnno, "metadata", "annotations"); err != nil { + logger.Error("failed to set metadata annotations to existing object: ", err) + updated = false + } + } + + originalSpecMetaName, change := getNestedFieldCopyData(existing, desired, "spec", "template", "metadata", "name") + if !change { + updated = true + if err := unstructured.SetNestedField(existing.Object, originalSpecMetaName, "spec", "template", "metadata", "name"); err != nil { + logger.Error("failed to set metadata name for spec to existing object: ", err) + updated = false + } + } + + originalSpecMetaLabel, change := getNestedFieldCopyData(existing, desired, "spec", "template", "metadata", "labels") + if !change { + updated = true + if err := unstructured.SetNestedField(existing.Object, originalSpecMetaLabel, "spec", "template", "metadata", "labels"); err != nil { + logger.Error("failed to set metadata labels for spec to existing object: ", err) + updated = false + } + } + + originalSpecMetaAnno, change := getNestedFieldCopyData(existing, desired, "spec", "template", "metadata", "annotations") + if !change { + updated = true + if err := unstructured.SetNestedField(existing.Object, originalSpecMetaAnno, "spec", "template", "metadata", "annotations"); err != nil { + logger.Error("failed to set metadata annotations for spec to existing object: ", err) + updated = false + } + } + + var ( + existingEnv, existingPorts, existingVolumeMount []interface{} + desiredEnv, desiredPorts, desiredVolumeMount []interface{} + desiredName, existingName, desiredImage, existingImage string + desiredArgs, existingArgs []string + existingResources, desiredResources interface{} + ) + + existingContainersData, _, _ := unstructured.NestedSlice(existing.Object, "spec", "template", "spec", "containers") + for i := range existingContainersData { + existingEnv, _, _ = unstructured.NestedSlice(existingContainersData[i].(map[string]interface{}), "env") + existingArgs, _, _ = unstructured.NestedStringSlice(existingContainersData[i].(map[string]interface{}), "args") + existingImage, _, _ = unstructured.NestedString(existingContainersData[i].(map[string]interface{}), "image") + existingName, _, _ = unstructured.NestedString(existingContainersData[i].(map[string]interface{}), "name") + existingPorts, _, _ = unstructured.NestedSlice(existingContainersData[i].(map[string]interface{}), "ports") + existingVolumeMount, _, _ = unstructured.NestedSlice(existingContainersData[i].(map[string]interface{}), "volumeMounts") + existingResources, _, _ = unstructured.NestedFieldCopy(existingContainersData[i].(map[string]interface{}), "resources") + } + + desiredContainersData, _, _ := unstructured.NestedSlice(desired.Object, "spec", "template", "spec", "containers") + for i := range desiredContainersData { + desiredEnv, _, _ = unstructured.NestedSlice(desiredContainersData[i].(map[string]interface{}), "env") + desiredArgs, _, _ = unstructured.NestedStringSlice(desiredContainersData[i].(map[string]interface{}), "args") + desiredImage, _, _ = unstructured.NestedString(desiredContainersData[i].(map[string]interface{}), "image") + desiredName, _, _ = unstructured.NestedString(desiredContainersData[i].(map[string]interface{}), "name") + desiredPorts, _, _ = unstructured.NestedSlice(desiredContainersData[i].(map[string]interface{}), "ports") + desiredVolumeMount, _, _ = unstructured.NestedSlice(desiredContainersData[i].(map[string]interface{}), "volumeMounts") + desiredResources, _, _ = unstructured.NestedFieldCopy(desiredContainersData[i].(map[string]interface{}), "resources") + } + + var cUpdated bool + if !reflect.DeepEqual(existingEnv, desiredEnv) { + cUpdated = true + for _, c := range existingEnv { + if err := unstructured.SetNestedSlice(c.(map[string]interface{}), desiredEnv, "env"); err != nil { + logger.Error("failed to set container env to existing object: ", err) + cUpdated = false + } + } + } + if !reflect.DeepEqual(existingArgs, desiredArgs) { + res := make(map[string]interface{}) + cUpdated = true + for _, c := range existingArgs { + res[c] = c + } + if err := unstructured.SetNestedStringSlice(res, desiredArgs, "args"); err != nil { + logger.Error("failed to set container args to existing object: ", err) + cUpdated = false + } + } + if !reflect.DeepEqual(existingImage, desiredImage) { + cUpdated = true + res := make(map[string]interface{}) + res[existingImage] = existingImage + if err := unstructured.SetNestedField(res, desiredImage, "image"); err != nil { + logger.Error("failed to set container image to existing object: ", err) + cUpdated = false + } + } + if !reflect.DeepEqual(existingName, desiredName) { + cUpdated = true + res := make(map[string]interface{}) + res[existingName] = existingName + if err := unstructured.SetNestedField(res, desiredName, "name"); err != nil { + logger.Error("failed to set container name to existing object: ", err) + cUpdated = false + } + } + if !reflect.DeepEqual(existingPorts, desiredPorts) { + cUpdated = true + for _, c := range existingPorts { + if err := unstructured.SetNestedSlice(c.(map[string]interface{}), desiredPorts, "ports"); err != nil { + logger.Error("failed to set container ports to existing object: ", err) + cUpdated = false + } + } + } + if !reflect.DeepEqual(existingVolumeMount, desiredVolumeMount) { + cUpdated = true + for _, c := range existingVolumeMount { + if err := unstructured.SetNestedSlice(c.(map[string]interface{}), desiredVolumeMount, "volumeMounts"); err != nil { + logger.Error("failed to set container volumeMount to existing object: ", err) + cUpdated = false + } + } + } + if !reflect.DeepEqual(existingResources, desiredResources) { + cUpdated = true + if err := unstructured.SetNestedField(existingResources.(map[string]interface{}), desiredResources, "resources"); err != nil { + logger.Error("failed to set container resources to existing object: ", err) + cUpdated = false + } + } + if cUpdated { + updated = true + err := unstructured.SetNestedField(existing.Object, desiredContainersData, "spec", "template", "spec", "containers") + if err != nil { + updated = false + } + } + desiredSA, change := getNestedFieldCopyData(existing, desired, "spec", "template", "spec", "serviceAccountName") + if !change { + updated = true + if err := unstructured.SetNestedField(existing.Object, desiredSA, "spec", "template", "spec", "serviceAccountName"); err != nil { + logger.Error("failed to set service account to existing object: ", err) + updated = false + } + } + desiredVol, change := getNestedFieldCopyData(existing, desired, "spec", "template", "spec", "volumes") + if !change { + updated = true + if err := unstructured.SetNestedField(existing.Object, desiredVol, "spec", "template", "spec", "volumes"); err != nil { + logger.Error("failed to set volumes to existing object: ", err) + updated = false + } + } + desiredTolerations, change := getNestedFieldCopyData(existing, desired, "spec", "template", "spec", "tolerations") + if !change { + updated = true + if err := unstructured.SetNestedField(existing.Object, desiredTolerations, "spec", "template", "spec", "tolerations"); err != nil { + logger.Error("failed to set tolerations to existing object: ", err) + updated = false + } + } + desiredNodeSelector, change := getNestedFieldCopyData(existing, desired, "spec", "template", "spec", "nodeSelector") + if !change { + updated = true + if err := unstructured.SetNestedField(existing.Object, desiredNodeSelector, "spec", "template", "spec", "nodeSelector"); err != nil { + logger.Error("failed to set nodeSelector to existing object: ", err) + updated = false + } + } + return +} + +func getNestedFieldCopyData(existing, desired *unstructured.Unstructured, fields ...string) (interface{}, bool) { + updated, _, _ := unstructured.NestedFieldCopy(existing.Object, fields...) + original, _, _ := unstructured.NestedFieldCopy(desired.Object, fields...) + return original, reflect.DeepEqual(original, updated) +} + +// MergeMaps merges the values in the passed maps into a new map. +// Values within m2 potentially clobber m1 values. +func MergeMaps(m1, m2 map[string]string) map[string]string { + merged := make(map[string]string, len(m1)+len(m2)) + for k, v := range m1 { + merged[k] = v + } + for k, v := range m2 { + merged[k] = v + } + return merged +} + +func GetConditions(existingData *unstructured.Unstructured) (v1beta1.Conditions, interface{}, error) { + statusData, ok, err := unstructured.NestedMap(existingData.Object, "status") + if !ok || err != nil { + // No status in the created object, it is weird but let's not fail + logger.Warn("empty status for the created custom object") + return nil, nil, err + } + conditionData, ok, err := unstructured.NestedFieldCopy(statusData, "conditions") + if !ok || err != nil { + // No conditions in the created object, it is weird but let's not fail + logger.Warn("empty status conditions for the created custom object") + return nil, nil, err + } + cMarshalledData, err := json.Marshal(conditionData) + if err != nil { + return nil, nil, err + } + var customConditions v1beta1.Conditions + if err = json.Unmarshal(cMarshalledData, &customConditions); err != nil { + return nil, nil, err + } + return customConditions, statusData["url"], nil +} diff --git a/pkg/dynamic/custom_reconcile_test.go b/pkg/dynamic/custom_reconcile_test.go new file mode 100644 index 000000000..e6d0a56dd --- /dev/null +++ b/pkg/dynamic/custom_reconcile_test.go @@ -0,0 +1,334 @@ +/* +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 dynamic + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + logger "github.com/sirupsen/logrus" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + duckv1 "knative.dev/pkg/apis/duck/v1" +) + +func TestGetNestedFieldCopyData(t *testing.T) { + original := &duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "event-listener", + Image: "test", + Ports: []corev1.ContainerPort{{ + ContainerPort: int32(8888), + Protocol: corev1.ProtocolTCP, + }}, + }}, + }, + }, + }, + } + marshaledData, err := json.Marshal(original) + if err != nil { + logger.Error("failed to marshal custom object", err) + } + existingData := new(unstructured.Unstructured) + originalData := new(unstructured.Unstructured) + if err := originalData.UnmarshalJSON(marshaledData); err != nil { + logger.Error("failed to unmarshal to unstructured object", err) + } + _, equal := getNestedFieldCopyData(originalData, existingData) + if diff := cmp.Diff(equal, false); diff != "" { + t.Errorf("GetNestedFieldCopyData equality mismatch. Diff request body: -want +got: %s", diff) + } +} + +func TestGetConditions(t *testing.T) { + tests := []struct { + name string + objData *appsv1.Deployment + }{{ + name: "No status", + objData: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + }, + }, { + name: "Status but no conditions", + objData: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "v1", + }, + Status: appsv1.DeploymentStatus{ + ObservedGeneration: 1, + }, + }, + }, { + name: "Status with conditions", + objData: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "v1", + }, + Status: appsv1.DeploymentStatus{ + ObservedGeneration: 1, + Conditions: []appsv1.DeploymentCondition{{ + Type: appsv1.DeploymentAvailable, + Status: corev1.ConditionTrue, + Message: "deployment created", + }}, + }, + }, + }} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + marshaledData, err := json.Marshal(tt.objData) + if err != nil { + logger.Error("failed to marshal custom object", err) + } + originalData := new(unstructured.Unstructured) + if err := originalData.UnmarshalJSON(marshaledData); err != nil { + logger.Error("failed to unmarshal to unstructured object", err) + } + cond, url, err := GetConditions(originalData) + if cond != nil && url != nil && err != nil { + t.Error("GetConditions is not working as expected") + } + }) + } +} + +func TestGetConditionsInvalidObj(t *testing.T) { + objWithNoStatus := map[string]interface{}{ + "kind": "test", + "apiVersion": "v1", + "bacon": "delicious", + } + marshaledData, err := json.Marshal(objWithNoStatus) + if err != nil { + logger.Error("failed to marshal custom object", err) + } + originalData := new(unstructured.Unstructured) + if err := originalData.UnmarshalJSON(marshaledData); err != nil { + logger.Error("failed to unmarshal to unstructured object", err) + } + cond, url, err := GetConditions(originalData) + if cond != nil && url != nil && err != nil { + t.Error("GetConditions is not working as expected") + } +} + +func TestReconcileCustomObject(t *testing.T) { + existing := &duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + Labels: map[string]string{"key": "value"}, + Annotations: map[string]string{"key": "value"}, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "v1", + Kind: "EventListener", + }}, + }, + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Labels: map[string]string{"key": "value"}, + Annotations: map[string]string{"key": "value"}, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "sa", + NodeSelector: map[string]string{"node": "value"}, + Tolerations: []corev1.Toleration{{ + Key: "key", + Value: "value", + }}, + Volumes: []corev1.Volume{{ + Name: "key", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }}, + Containers: []corev1.Container{{ + Name: "event-listener", + Image: "test", + Ports: []corev1.ContainerPort{{ + ContainerPort: int32(8888), + Protocol: corev1.ProtocolTCP, + }}, + Args: []string{"key"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "testvolume", + }}, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.Quantity{Format: resource.DecimalSI}, + }, + }, + Env: []corev1.EnvVar{{ + Name: "key", + Value: "value", + }}, + }}, + }, + }, + }, + } + desired := &duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + Namespace: "test1", + Labels: map[string]string{"key1": "value"}, + Annotations: map[string]string{"key1": "value"}, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "v1", + Kind: "Pod", + }}, + }, + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + Labels: map[string]string{"key1": "value"}, + Annotations: map[string]string{"key1": "value"}, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "sa1", + NodeSelector: map[string]string{"node1": "value"}, + Tolerations: []corev1.Toleration{{ + Key: "key", + Value: "value1", + }}, + Volumes: []corev1.Volume{{ + Name: "key1", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }}, + Containers: []corev1.Container{{ + Name: "event-listener1", + Image: "test1", + Ports: []corev1.ContainerPort{{ + ContainerPort: int32(8888), + Protocol: corev1.ProtocolUDP, + }}, + Args: []string{"key1"}, + VolumeMounts: []corev1.VolumeMount{{ + Name: "testvolume1", + }}, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.Quantity{Format: resource.DecimalSI}, + }, + }, + Env: []corev1.EnvVar{{ + Name: "key1", + Value: "value1", + }}, + }}, + }, + }, + }, + } + existingMarshaledData, err := json.Marshal(existing) + if err != nil { + logger.Error("failed to marshal custom object", err) + } + desiredMarshaledData, err := json.Marshal(desired) + if err != nil { + logger.Error("failed to marshal custom object", err) + } + existingData := new(unstructured.Unstructured) + desiredData := new(unstructured.Unstructured) + if err := existingData.UnmarshalJSON(existingMarshaledData); err != nil { + logger.Error("failed to unmarshal to unstructured object", err) + } + if err := desiredData.UnmarshalJSON(desiredMarshaledData); err != nil { + logger.Error("failed to unmarshal to unstructured object", err) + } + ReconcileCustomObject(existingData, desiredData) +} + +func Test_mergeMaps(t *testing.T) { + tests := []struct { + name string + l1, l2 map[string]string + expectedLabels map[string]string + }{{ + name: "Both maps empty", + l1: nil, + l2: nil, + expectedLabels: map[string]string{}, + }, { + name: "Map one empty", + l1: nil, + l2: map[string]string{"k": "v"}, + expectedLabels: map[string]string{"k": "v"}, + }, { + name: "Map two empty", + l1: map[string]string{"k": "v"}, + l2: nil, + expectedLabels: map[string]string{"k": "v"}, + }, { + name: "Both maps", + l1: map[string]string{"k1": "v1"}, + l2: map[string]string{"k2": "v2"}, + expectedLabels: map[string]string{"k1": "v1", "k2": "v2"}, + }, { + name: "Both maps with clobber", + l1: map[string]string{"k1": "v1"}, + l2: map[string]string{"k1": "v2"}, + expectedLabels: map[string]string{"k1": "v2"}, + }} + for i := range tests { + t.Run(tests[i].name, func(t *testing.T) { + actualLabels := MergeMaps(tests[i].l1, tests[i].l2) + if diff := cmp.Diff(tests[i].expectedLabels, actualLabels); diff != "" { + t.Errorf("mergeLabels() did not return expected. -want, +got: %s", diff) + } + }) + } +} diff --git a/pkg/dynamic/dynamic.go b/pkg/dynamic/dynamic.go new file mode 100644 index 000000000..73db98742 --- /dev/null +++ b/pkg/dynamic/dynamic.go @@ -0,0 +1,60 @@ +/* +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 dynamic + +import ( + "context" + + "github.com/tektoncd/triggers/pkg/apis/triggers/v1alpha1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/cache" + "knative.dev/pkg/apis/duck" + "knative.dev/pkg/controller" +) + +type ListableTracker interface { + WatchOnDynamicObject(ctx context.Context, gvr schema.GroupVersionResource) error +} + +type listableTracker struct { + informerFactory duck.InformerFactory + impl *controller.Impl +} + +// NewListableTracker creates a new ListableTracker, backed by a TypedInformerFactory. +func NewListableTracker(ctx context.Context, getter func(ctx context.Context) duck.InformerFactory, impl *controller.Impl) ListableTracker { + return &listableTracker{ + informerFactory: getter(ctx), + impl: impl, + } +} + +func (t *listableTracker) WatchOnDynamicObject(ctx context.Context, gvr schema.GroupVersionResource) error { + return t.watchOnDynamicObject(ctx, gvr) +} + +func (t *listableTracker) watchOnDynamicObject(ctx context.Context, gvr schema.GroupVersionResource) error { + shInformer, _, err := t.informerFactory.Get(ctx, gvr) + if err != nil { + return err + } + shInformer.AddEventHandler(cache.FilteringResourceEventHandler{ + FilterFunc: controller.FilterControllerGVK(v1alpha1.SchemeGroupVersion.WithKind("EventListener")), + Handler: controller.HandleAll(t.impl.EnqueueControllerOf), + }) + return nil +} diff --git a/pkg/reconciler/v1alpha1/eventlistener/controller.go b/pkg/reconciler/v1alpha1/eventlistener/controller.go index 1e4d62771..32086cdea 100644 --- a/pkg/reconciler/v1alpha1/eventlistener/controller.go +++ b/pkg/reconciler/v1alpha1/eventlistener/controller.go @@ -22,15 +22,17 @@ import ( "github.com/tektoncd/triggers/pkg/apis/triggers/v1alpha1" triggersclient "github.com/tektoncd/triggers/pkg/client/injection/client" eventlistenerinformer "github.com/tektoncd/triggers/pkg/client/injection/informers/triggers/v1alpha1/eventlistener" - eventlistenerreconciler "github.com/tektoncd/triggers/pkg/client/injection/reconciler/triggers/v1alpha1/eventlistener" + dynamicduck "github.com/tektoncd/triggers/pkg/dynamic" "k8s.io/client-go/tools/cache" + duckinformer "knative.dev/pkg/client/injection/ducks/duck/v1/podspecable" kubeclient "knative.dev/pkg/client/injection/kube/client" deployinformer "knative.dev/pkg/client/injection/kube/informers/apps/v1/deployment" configmapinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/configmap" serviceinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/service" "knative.dev/pkg/configmap" "knative.dev/pkg/controller" + "knative.dev/pkg/injection/clients/dynamicclient" "knative.dev/pkg/logging" ) @@ -38,6 +40,7 @@ import ( func NewController(config Config) func(context.Context, configmap.Watcher) *controller.Impl { return func(ctx context.Context, cmw configmap.Watcher) *controller.Impl { logger := logging.FromContext(ctx) + dynamicclientset := dynamicclient.Get(ctx) kubeclientset := kubeclient.Get(ctx) triggersclientset := triggersclient.Get(ctx) eventListenerInformer := eventlistenerinformer.Get(ctx) @@ -45,6 +48,7 @@ func NewController(config Config) func(context.Context, configmap.Watcher) *cont serviceInformer := serviceinformer.Get(ctx) reconciler := &Reconciler{ + DynamicClientSet: dynamicclientset, KubeClientSet: kubeclientset, TriggersClientSet: triggersclientset, configmapLister: configmapinformer.Get(ctx).Lister(), @@ -62,6 +66,9 @@ func NewController(config Config) func(context.Context, configmap.Watcher) *cont }) logger.Info("Setting up event handlers") + + reconciler.podspecableTracker = dynamicduck.NewListableTracker(ctx, duckinformer.Get, impl) + eventListenerInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: impl.Enqueue, UpdateFunc: controller.PassNew(impl.Enqueue), diff --git a/pkg/reconciler/v1alpha1/eventlistener/eventlistener.go b/pkg/reconciler/v1alpha1/eventlistener/eventlistener.go index daa25a4e7..ff3385c50 100644 --- a/pkg/reconciler/v1alpha1/eventlistener/eventlistener.go +++ b/pkg/reconciler/v1alpha1/eventlistener/eventlistener.go @@ -17,33 +17,41 @@ limitations under the License. package eventlistener import ( + "bytes" "context" + "encoding/json" "fmt" "reflect" "strconv" + "strings" + "sync" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/tektoncd/triggers/pkg/apis/triggers/v1alpha1" + triggersclientset "github.com/tektoncd/triggers/pkg/client/clientset/versioned" + eventlistenerreconciler "github.com/tektoncd/triggers/pkg/client/injection/reconciler/triggers/v1alpha1/eventlistener" listers "github.com/tektoncd/triggers/pkg/client/listers/triggers/v1alpha1" + dynamicduck "github.com/tektoncd/triggers/pkg/dynamic" "github.com/tektoncd/triggers/pkg/system" "go.uber.org/zap" "golang.org/x/xerrors" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" appsv1lister "k8s.io/client-go/listers/apps/v1" corev1lister "k8s.io/client-go/listers/core/v1" + duckv1 "knative.dev/pkg/apis/duck/v1" "knative.dev/pkg/logging" - pkgreconciler "knative.dev/pkg/reconciler" - - triggersclientset "github.com/tektoncd/triggers/pkg/client/clientset/versioned" - eventlistenerreconciler "github.com/tektoncd/triggers/pkg/client/injection/reconciler/triggers/v1alpha1/eventlistener" "knative.dev/pkg/ptr" + pkgreconciler "knative.dev/pkg/reconciler" ) const ( @@ -66,6 +74,7 @@ const ( // Reconciler implements controller.Reconciler for Configuration resources. type Reconciler struct { + DynamicClientSet dynamic.Interface // KubeClientSet allows us to talk to the k8s for core APIs KubeClientSet kubernetes.Interface @@ -80,7 +89,9 @@ type Reconciler struct { serviceLister corev1lister.ServiceLister // config is the configuration options that the Reconciler accepts. - config Config + config Config + podspecableTracker dynamicduck.ListableTracker + onlyOnce sync.Once } var ( @@ -103,6 +114,10 @@ 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)) + if el.Spec.Resources.CustomResource != nil { + kError := r.reconcileCustomObject(ctx, logger, el) + return wrapError(kError, nil) + } deploymentReconcileError := r.reconcileDeployment(ctx, logger, el) serviceReconcileError := r.reconcileService(ctx, logger, el) @@ -143,7 +158,7 @@ func reconcileObjectMeta(existing *metav1.ObjectMeta, desired metav1.ObjectMeta) } // TODO(dibyom): We should exclude propagation of some annotations such as `kubernetes.io/last-applied-revision` - if !reflect.DeepEqual(existing.Annotations, mergeMaps(existing.Annotations, desired.Annotations)) { + if !reflect.DeepEqual(existing.Annotations, dynamicduck.MergeMaps(existing.Annotations, desired.Annotations)) { updated = true existing.Annotations = desired.Annotations } @@ -239,7 +254,16 @@ func (r *Reconciler) reconcileDeployment(ctx context.Context, logger *zap.Sugare return err } - container := getContainer(el, r.config) + container := getContainer(el, r.config, nil) + container.Env = append(container.Env, corev1.EnvVar{ + Name: "SYSTEM_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }}, + }) + container = addCertsForSecureConnection(container, r.config) + deployment := getDeployment(el, r.config) existingDeployment, err := r.deploymentLister.Deployments(el.Namespace).Get(el.Status.Configuration.GeneratedResourceName) @@ -359,6 +383,129 @@ func (r *Reconciler) reconcileDeployment(ctx context.Context, logger *zap.Sugare return nil } +func (r *Reconciler) reconcileCustomObject(ctx context.Context, logger *zap.SugaredLogger, el *v1alpha1.EventListener) error { + // check logging config, create if it doesn't exist + if err := r.reconcileLoggingConfig(ctx, logger, el); err != nil { + logger.Error(err) + return err + } + + original := &duckv1.WithPod{} + decoder := json.NewDecoder(bytes.NewBuffer(el.Spec.Resources.CustomResource.Raw)) + if err := decoder.Decode(&original); err != nil { + logger.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 := getContainer(el, r.config, original) + + container.Env = append(container.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, + }) + + podlabels := dynamicduck.MergeMaps(el.Labels, GenerateResourceLabels(el.Name, r.config.StaticResourceLabels)) + + podlabels = dynamicduck.MergeMaps(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 { + logger.Errorf("failed to marshal custom object", err) + return err + } + data := new(unstructured.Unstructured) + if err := data.UnmarshalJSON(marshaledData); err != nil { + logger.Errorf("failed to unmarshal to unstructured object", err) + return err + } + + if data.GetName() == "" { + data.SetName(el.Status.Configuration.GeneratedResourceName) + } + gvr, _ := meta.UnsafeGuessKindToResource(data.GetObjectKind().GroupVersionKind()) + + data.SetOwnerReferences([]metav1.OwnerReference{*el.GetOwnerReference()}) + + var watchError error + r.onlyOnce.Do(func() { + watchError = r.podspecableTracker.WatchOnDynamicObject(ctx, gvr) + }) + if watchError != nil { + logger.Errorf("failed to watch on created custom object", watchError) + return err + } + + existingCustomObject, err := r.DynamicClientSet.Resource(gvr).Namespace(namespace).Get(ctx, data.GetName(), metav1.GetOptions{}) + switch { + case err == nil: + if dynamicduck.ReconcileCustomObject(existingCustomObject, data) { + if _, err := r.DynamicClientSet.Resource(gvr).Namespace(namespace).Update(ctx, existingCustomObject, metav1.UpdateOptions{}); err != nil { + logger.Errorf("error updating to eventListener custom object: %v", err) + return err + } + logger.Infof("Updated EventListener Custom Object %s in Namespace %s", data.GetName(), el.Namespace) + } + + customConditions, url, err := dynamicduck.GetConditions(existingCustomObject) + if customConditions == nil { + // No status in the created object, it is weird but let's not fail + logger.Warn("empty status for the created custom object") + return nil + } + if err != nil { + return err + } + el.Status.SetConditionsForDynamicObjects(customConditions) + if url != nil { + el.Status.SetAddress(strings.Split(fmt.Sprintf("%v", url), "//")[1]) + } + case errors.IsNotFound(err): + createDynamicObject, err := r.DynamicClientSet.Resource(gvr).Namespace(namespace).Create(ctx, data, metav1.CreateOptions{}) + if err != nil { + logger.Errorf("Error creating EventListener Dynamic object: ", err) + return err + } + logger.Infof("Created EventListener Deployment %s in Namespace %s", createDynamicObject.GetName(), el.Namespace) + default: + logger.Error(err) + return err + } + return nil +} + func getDeployment(el *v1alpha1.EventListener, c Config) *appsv1.Deployment { var replicas = ptr.Int32(1) if el.Spec.Replicas != nil { @@ -370,7 +517,7 @@ func getDeployment(el *v1alpha1.EventListener, c Config) *appsv1.Deployment { serviceAccountName string securityContext corev1.PodSecurityContext ) - podlabels = mergeMaps(el.Labels, GenerateResourceLabels(el.Name, c.StaticResourceLabels)) + podlabels = dynamicduck.MergeMaps(el.Labels, GenerateResourceLabels(el.Name, c.StaticResourceLabels)) serviceAccountName = el.Spec.ServiceAccountName @@ -385,7 +532,15 @@ func getDeployment(el *v1alpha1.EventListener, c Config) *appsv1.Deployment { }, }} - container := getContainer(el, c) + container := getContainer(el, c, nil) + container.Env = append(container.Env, corev1.EnvVar{ + Name: "SYSTEM_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }}, + }) + container = addCertsForSecureConnection(container, c) 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" { @@ -410,7 +565,7 @@ func getDeployment(el *v1alpha1.EventListener, c Config) *appsv1.Deployment { serviceAccountName = el.Spec.Resources.KubernetesResource.Template.Spec.ServiceAccountName } annotations = el.Spec.Resources.KubernetesResource.Template.Annotations - podlabels = mergeMaps(podlabels, el.Spec.Resources.KubernetesResource.Template.Labels) + podlabels = dynamicduck.MergeMaps(podlabels, el.Spec.Resources.KubernetesResource.Template.Labels) } if *c.SetSecurityContext { @@ -445,41 +600,12 @@ func getDeployment(el *v1alpha1.EventListener, c Config) *appsv1.Deployment { } } -func getContainer(el *v1alpha1.EventListener, c Config) 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", - }, - }, - }, { - Name: "TEKTON_INSTALL_NAMESPACE", - Value: system.GetNamespace(), - }} - +func addCertsForSecureConnection(container corev1.Container, c Config) corev1.Container { + var elCert, elKey string 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 - } - } + for i := range container.Env { + certEnv[container.Env[i].Name] = container.Env[i].ValueFrom } - var scheme corev1.URIScheme if v, ok := certEnv["TLS_CERT"]; ok { elCert = "/etc/triggers/tls/" + v.SecretKeyRef.Key @@ -494,7 +620,7 @@ func getContainer(el *v1alpha1.EventListener, c Config) corev1.Container { if elCert != "" && elKey != "" { scheme = corev1.URISchemeHTTPS - vMount = append(vMount, corev1.VolumeMount{ + container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ Name: "https-connection", ReadOnly: true, MountPath: "/etc/triggers/tls", @@ -502,6 +628,59 @@ func getContainer(el *v1alpha1.EventListener, c Config) corev1.Container { } else { scheme = corev1.URISchemeHTTP } + container.LivenessProbe = &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/live", + Scheme: scheme, + Port: intstr.FromInt(eventListenerContainerPort), + }, + }, + PeriodSeconds: int32(*c.PeriodSeconds), + FailureThreshold: int32(*c.FailureThreshold), + } + container.ReadinessProbe = &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/live", + Scheme: scheme, + Port: intstr.FromInt(eventListenerContainerPort), + }, + }, + PeriodSeconds: int32(*c.PeriodSeconds), + FailureThreshold: int32(*c.FailureThreshold), + } + container.Args = append(container.Args, "--tls-cert="+elCert, "--tls-key="+elKey) + return container +} + +func getContainer(el *v1alpha1.EventListener, c Config, pod *duckv1.WithPod) corev1.Container { + var resources corev1.ResourceRequirements + vMount := []corev1.VolumeMount{{ + Name: "config-logging", + MountPath: "/etc/config-logging", + }} + + env := []corev1.EnvVar{{ + Name: "TEKTON_INSTALL_NAMESPACE", + Value: system.GetNamespace(), + }} + + 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 + env = append(env, el.Spec.Resources.KubernetesResource.Template.Spec.Containers[0].Env...) + } + } + // handle env and resources for custom object + if pod != nil { + if len(pod.Spec.Template.Spec.Containers) == 1 { + for i := range pod.Spec.Template.Spec.Containers[0].Env { + env = append(env, pod.Spec.Template.Spec.Containers[0].Env[i]) + } + resources = pod.Spec.Template.Spec.Containers[0].Resources + } + } isMultiNS := false if len(el.Spec.NamespaceSelector.MatchNames) != 0 { @@ -515,28 +694,6 @@ func getContainer(el *v1alpha1.EventListener, c Config) corev1.Container { ContainerPort: int32(eventListenerContainerPort), Protocol: corev1.ProtocolTCP, }}, - LivenessProbe: &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/live", - Scheme: scheme, - Port: intstr.FromInt(eventListenerContainerPort), - }, - }, - PeriodSeconds: int32(*c.PeriodSeconds), - FailureThreshold: int32(*c.FailureThreshold), - }, - ReadinessProbe: &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/live", - Scheme: scheme, - Port: intstr.FromInt(eventListenerContainerPort), - }, - }, - PeriodSeconds: int32(*c.PeriodSeconds), - FailureThreshold: int32(*c.FailureThreshold), - }, Resources: resources, Args: []string{ "--el-name=" + el.Name, @@ -547,8 +704,6 @@ func getContainer(el *v1alpha1.EventListener, c Config) corev1.Container { "--idletimeout=" + strconv.FormatInt(*c.IdleTimeOut, 10), "--timeouthandler=" + strconv.FormatInt(*c.TimeOutHandler, 10), "--is-multi-ns=" + strconv.FormatBool(isMultiNS), - "--tls-cert=" + elCert, - "--tls-key=" + elKey, }, VolumeMounts: vMount, Env: env, @@ -556,9 +711,7 @@ func getContainer(el *v1alpha1.EventListener, c Config) corev1.Container { } func getServicePort(el *v1alpha1.EventListener, c Config) corev1.ServicePort { - var ( - elCert, elKey string - ) + var elCert, elKey string servicePortName := eventListenerServicePortName servicePortPort := *c.Port @@ -620,24 +773,11 @@ func generateObjectMeta(el *v1alpha1.EventListener, staticResourceLabels map[str Namespace: el.Namespace, Name: el.Status.Configuration.GeneratedResourceName, OwnerReferences: []metav1.OwnerReference{*el.GetOwnerReference()}, - Labels: mergeMaps(el.Labels, GenerateResourceLabels(el.Name, staticResourceLabels)), + Labels: dynamicduck.MergeMaps(el.Labels, GenerateResourceLabels(el.Name, staticResourceLabels)), Annotations: el.Annotations, } } -// mergeMaps merges the values in the passed maps into a new map. -// Values within m2 potentially clobber m1 values. -func mergeMaps(m1, m2 map[string]string) map[string]string { - merged := make(map[string]string, len(m1)+len(m2)) - for k, v := range m1 { - merged[k] = v - } - for k, v := range m2 { - merged[k] = v - } - return merged -} - // wrapError wraps errors together. If one of the errors is nil, the other is // returned. func wrapError(err1, err2 error) error { diff --git a/pkg/reconciler/v1alpha1/eventlistener/eventlistener_test.go b/pkg/reconciler/v1alpha1/eventlistener/eventlistener_test.go index 9ebbd1311..3c259bb46 100644 --- a/pkg/reconciler/v1alpha1/eventlistener/eventlistener_test.go +++ b/pkg/reconciler/v1alpha1/eventlistener/eventlistener_test.go @@ -27,6 +27,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/tektoncd/triggers/pkg/apis/triggers/v1alpha1" + dynamicduck "github.com/tektoncd/triggers/pkg/dynamic" "github.com/tektoncd/triggers/pkg/system" "github.com/tektoncd/triggers/test" bldr "github.com/tektoncd/triggers/test/builder" @@ -242,15 +243,15 @@ func makeDeployment(ops ...func(d *appsv1.Deployment)) *appsv1.Deployment { MountPath: "/etc/config-logging", }}, Env: []corev1.EnvVar{{ + Name: "TEKTON_INSTALL_NAMESPACE", + Value: "tekton-pipelines", + }, { Name: "SYSTEM_NAMESPACE", ValueFrom: &corev1.EnvVarSource{ FieldRef: &corev1.ObjectFieldSelector{ FieldPath: "metadata.namespace", }, }, - }, { - Name: "TEKTON_INSTALL_NAMESPACE", - Value: "tekton-pipelines", }}, }}, Volumes: []corev1.Volume{{ @@ -330,13 +331,6 @@ var withTLSConfig = func(d *appsv1.Deployment) { ReadOnly: true, }}, Env: []corev1.EnvVar{{ - Name: "SYSTEM_NAMESPACE", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.namespace", - }, - }, - }, { Name: "TEKTON_INSTALL_NAMESPACE", Value: "tekton-pipelines", }, { @@ -359,6 +353,13 @@ var withTLSConfig = func(d *appsv1.Deployment) { Key: "tls.key", }, }, + }, { + Name: "SYSTEM_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, }}, }} d.Spec.Template.Spec.Volumes = []corev1.Volume{{ @@ -380,6 +381,76 @@ var withTLSConfig = func(d *appsv1.Deployment) { }} } +// makeWithPod is a helper to build a Knative Service that is created by an EventListener. +// It generates a basic Knative Service for the simplest EventListener and accepts functions for modification +func makeWithPod(ops ...func(d *duckv1.WithPod)) *duckv1.WithPod { + ownerRefs := makeEL().GetOwnerReference() + + d := duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: generatedResourceName, + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + *ownerRefs, + }, + Labels: generatedLabels, + }, + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "event-listener", + Image: DefaultImage, + Ports: []corev1.ContainerPort{{ + ContainerPort: int32(eventListenerContainerPort), + Protocol: corev1.ProtocolTCP, + }}, + Args: []string{ + "--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), + }, + VolumeMounts: []corev1.VolumeMount{{ + Name: "config-logging", + MountPath: "/etc/config-logging", + }}, + Env: []corev1.EnvVar{{ + Name: "TEKTON_INSTALL_NAMESPACE", + Value: "tekton-pipelines", + }, { + Name: "SYSTEM_NAMESPACE", + Value: "test-pipelines", + }}, + }}, + Volumes: []corev1.Volume{{ + Name: "config-logging", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: eventListenerConfigMapName, + }, + }, + }, + }}, + }, + }, + }, + } + for _, op := range ops { + op(&d) + } + return &d +} + // 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 { @@ -423,6 +494,19 @@ var withTLSPort = bldr.EventListenerStatus( bldr.EventListenerAddress(listenerHostname(generatedResourceName, namespace, 8443)), ) +var withKnativeStatus = bldr.EventListenerStatus( + bldr.EventListenerCondition( + v1alpha1.ServiceExists, + corev1.ConditionFalse, + "", "", + ), + bldr.EventListenerCondition( + v1alpha1.DeploymentExists, + corev1.ConditionFalse, + "", "", + ), +) + var withStatus = bldr.EventListenerStatus( bldr.EventListenerConfig(generatedResourceName), bldr.EventListenerAddress(listenerHostname(generatedResourceName, namespace, DefaultPort)), @@ -562,6 +646,101 @@ func TestReconcile(t *testing.T) { } }) + elWithCustomResourceForEnv := makeEL(withStatus, withKnativeStatus, func(el *v1alpha1.EventListener) { + el.Spec.Resources.CustomResource = &v1alpha1.CustomResource{ + RawExtension: test.RawExtension(t, duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: generatedResourceName, + }, + Spec: duckv1.WithPodSpec{Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Env: []corev1.EnvVar{{ + Name: "key", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "test"}, + Key: "a.crt", + }, + }, + }}, + }}, + }, + }}, + }), + } + }) + elWithCustomResourceForNodeSelector := makeEL(withStatus, withKnativeStatus, func(el *v1alpha1.EventListener) { + el.Spec.Resources.CustomResource = &v1alpha1.CustomResource{ + RawExtension: test.RawExtension(t, duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: generatedResourceName, + }, + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + NodeSelector: map[string]string{ + "hi": "hello", + }, + }, + }}, + }), + } + }) + + elWithCustomResourceForArgs := makeEL(withStatus, withKnativeStatus, func(el *v1alpha1.EventListener) { + el.Spec.Resources.CustomResource = &v1alpha1.CustomResource{ + RawExtension: test.RawExtension(t, duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: generatedResourceName, + }, + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Args: []string{ + "--test" + "10", + }, + }}, + }, + }}, + }), + } + }) + elWithCustomResourceForImage := makeEL(withStatus, withKnativeStatus, func(el *v1alpha1.EventListener) { + el.Spec.Resources.CustomResource = &v1alpha1.CustomResource{ + RawExtension: test.RawExtension(t, duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: generatedResourceName, + }, + Spec: duckv1.WithPodSpec{ + Template: duckv1.PodSpecable{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Image: "test", + }}, + }, + }}, + }), + } + }) + elWithTLSConnection := makeEL(withStatus, withTLSPort, func(el *v1alpha1.EventListener) { el.Spec.Resources.KubernetesResource = &v1alpha1.KubernetesResource{ WithPodSpec: duckv1.WithPodSpec{ @@ -615,9 +794,9 @@ func TestReconcile(t *testing.T) { elDeployment := makeDeployment() elDeploymentWithLabels := makeDeployment(func(d *appsv1.Deployment) { - d.Labels = mergeMaps(updateLabel, generatedLabels) + d.Labels = dynamicduck.MergeMaps(updateLabel, generatedLabels) d.Spec.Selector.MatchLabels = generatedLabels - d.Spec.Template.Labels = mergeMaps(updateLabel, generatedLabels) + d.Spec.Template.Labels = dynamicduck.MergeMaps(updateLabel, generatedLabels) }) elDeploymentWithAnnotations := makeDeployment(func(d *appsv1.Deployment) { @@ -679,10 +858,43 @@ func TestReconcile(t *testing.T) { d.Spec.Template.ObjectMeta.Annotations = map[string]string{"annotationkey": "annotationvalue"} }) + nodeSelectorForCustomResource := makeWithPod(func(data *duckv1.WithPod) { + data.Spec.Template.Spec.NodeSelector = map[string]string{ + "hi": "hello", + } + }) + + argsForCustomResource := makeWithPod(func(data *duckv1.WithPod) { + data.Spec.Template.Spec.Containers[0].Args = []string{ + "--is-multi-ns=" + strconv.FormatBool(true), + } + }) + + imageForCustomResource := makeWithPod(func(data *duckv1.WithPod) { + data.Spec.Template.Spec.Containers[0].Image = DefaultImage + }) + envForCustomResource := makeWithPod(func(data *duckv1.WithPod) { + data.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{{ + Name: "TEKTON_INSTALL_NAMESPACE", + Value: "tekton-pipelines", + }, { + Name: "key", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "test"}, + Key: "a.crt", + }, + }, + }, { + Name: "SYSTEM_NAMESPACE", + Value: "test-pipelines", + }} + }) + elService := makeService() elServiceWithLabels := makeService(func(s *corev1.Service) { - s.Labels = mergeMaps(updateLabel, generatedLabels) + s.Labels = dynamicduck.MergeMaps(updateLabel, generatedLabels) s.Spec.Selector = generatedLabels }) @@ -1062,23 +1274,81 @@ func TestReconcile(t *testing.T) { Services: []*corev1.Service{elService}, ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, }, + }, { + name: "eventlistener with port set in config", + key: reconcileKey, + config: configWithPortSet, + startResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithStatus}, + Deployments: []*appsv1.Deployment{elDeployment}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + }, + endResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithPortSet}, + Deployments: []*appsv1.Deployment{elDeployment}, + Services: []*corev1.Service{elServiceWithPortSet}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + }, }, { - name: "eventlistener with port set in config", - key: reconcileKey, - config: configWithPortSet, + name: "eventlistener with added env for custome resource", + key: reconcileKey, + startResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResourceForEnv}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + }, + endResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResourceForEnv}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + WithPod: []*duckv1.WithPod{envForCustomResource}, + }, + }, + { + name: "eventlistener with added NodeSelector for custom resource", + key: reconcileKey, + startResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResourceForNodeSelector}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + WithPod: []*duckv1.WithPod{nodeSelectorForCustomResource}, + }, + endResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResourceForNodeSelector}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + WithPod: []*duckv1.WithPod{nodeSelectorForCustomResource}, + }, + }, { + name: "eventlistener with added Args for custom resource", + key: reconcileKey, + startResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResourceForArgs}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + }, + endResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResourceForArgs}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + WithPod: []*duckv1.WithPod{argsForCustomResource}, + }, + }, { + name: "eventlistener with added Image for custom resource", + key: reconcileKey, startResources: test.Resources{ Namespaces: []*corev1.Namespace{namespaceResource}, - EventListeners: []*v1alpha1.EventListener{elWithStatus}, - Deployments: []*appsv1.Deployment{elDeployment}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResourceForImage}, ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, }, endResources: test.Resources{ Namespaces: []*corev1.Namespace{namespaceResource}, - EventListeners: []*v1alpha1.EventListener{elWithPortSet}, - Deployments: []*appsv1.Deployment{elDeployment}, - Services: []*corev1.Service{elServiceWithPortSet}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResourceForImage}, ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + WithPod: []*duckv1.WithPod{imageForCustomResource}, }, }, } @@ -1108,6 +1378,175 @@ func TestReconcile(t *testing.T) { } } +func TestReconcile_InvalidForCustomResource(t *testing.T) { + err := os.Setenv("SYSTEM_NAMESPACE", "tekton-pipelines") + if err != nil { + t.Fatal(err) + } + + elWithCustomResource := makeEL(withStatus, withKnativeStatus, func(el *v1alpha1.EventListener) { + el.Spec.Resources.CustomResource = &v1alpha1.CustomResource{ + RawExtension: test.RawExtension(t, duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: generatedResourceName, + Labels: map[string]string{"serving.knative.dev/visibility": "cluster-local"}, + Annotations: map[string]string{"key": "value"}, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "v1", + }}, + }, + Spec: duckv1.WithPodSpec{Template: duckv1.PodSpecable{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rev1", + Labels: map[string]string{"key": "value"}, + Annotations: map[string]string{"key": "value"}, + }, + Spec: corev1.PodSpec{ + Tolerations: updateTolerations, + NodeSelector: map[string]string{ + "hi1": "hello1", + }, + ServiceAccountName: "sa", + Containers: []corev1.Container{{ + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.Quantity{Format: resource.DecimalSI}, + }, + }, + Env: []corev1.EnvVar{{ + Name: "key", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "test"}, + Key: "a.crt", + }, + }, + }}, + }}, + }, + }}, + }), + } + }) + customResource := makeWithPod(func(data *duckv1.WithPod) { + data.ObjectMeta.Labels = map[string]string{"serving.knative.dev/visibility": "cluster-local1"} + data.ObjectMeta.Annotations = map[string]string{"key1": "value1"} + data.ObjectMeta.OwnerReferences = []metav1.OwnerReference{{ + APIVersion: "v2", + }} + data.Spec.Template.ObjectMeta.Name = "rev" + data.Spec.Template.ObjectMeta.Labels = map[string]string{"key1": "value1"} + data.Spec.Template.ObjectMeta.Annotations = map[string]string{"key1": "value1"} + data.Spec.Template.Spec.NodeSelector = map[string]string{"hi": "hello"} + data.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{{ + Name: "SYSTEM_NAMESPACE", + Value: "test-pipelines", + }, { + Name: "TEKTON_INSTALL_NAMESPACE", + Value: "tekton-pipelines", + }, { + Name: "key1", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "test"}, + Key: "a.crt", + }, + }, + }} + data.Spec.Template.Spec.Containers[0].Args = []string{ + "--is-multi-ns1=" + strconv.FormatBool(true), + "--el-namespace=" + "test", + } + data.Spec.Template.Spec.Containers[0].Image = "test" + data.Spec.Template.Spec.Containers[0].Name = "test" + data.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{{ + Protocol: corev1.ProtocolUDP, + }} + data.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{{ + Name: "config-logging-dummy", + MountPath: "/etc/config-logging-dummy", + ReadOnly: true, + }} + data.Spec.Template.Spec.Containers[0].Resources = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.Quantity{Format: resource.BinarySI}, + }, + } + data.Spec.Template.Spec.Volumes = []corev1.Volume{{ + Name: "config-logging1", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: eventListenerConfigMapName, + }, + }, + }, + }} + data.Spec.Template.Spec.ServiceAccountName = "test" + data.Spec.Template.Spec.Tolerations = []corev1.Toleration{{ + Key: "key1", + }} + }) + + loggingConfigMap := defaultLoggingConfigMap() + loggingConfigMap.ObjectMeta.Namespace = namespace + reconcilerLoggingConfigMap := defaultLoggingConfigMap() + reconcilerLoggingConfigMap.ObjectMeta.Namespace = reconcilerNamespace + + tests := []struct { + name string + key string + config *Config // Config of the reconciler + startResources test.Resources // State of the world before we call Reconcile + endResources test.Resources // Expected State of the world after calling Reconcile + }{ + { + name: "eventlistener with custome resource", + key: reconcileKey, + startResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResource}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + WithPod: []*duckv1.WithPod{customResource}, + }, + endResources: test.Resources{ + Namespaces: []*corev1.Namespace{namespaceResource}, + EventListeners: []*v1alpha1.EventListener{elWithCustomResource}, + ConfigMaps: []*corev1.ConfigMap{loggingConfigMap}, + WithPod: []*duckv1.WithPod{customResource}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup with startResources + testAssets, cancel := getEventListenerTestAssets(t, tt.startResources, tt.config) + defer cancel() + // Run Reconcile + err := testAssets.Controller.Reconciler.Reconcile(context.Background(), tt.key) + if err != nil { + t.Errorf("eventlistener.Reconcile() returned error: %s", err) + return + } + // Grab test resource results + actualEndResources, err := test.GetResourcesFromClients(testAssets.Clients) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(tt.endResources, *actualEndResources, cmpopts.IgnoreTypes( + apis.Condition{}.LastTransitionTime.Inner.Time, + metav1.ObjectMeta{}.Finalizers, + )); diff == "" { + t.Errorf("eventlistener.Reconcile() equality mismatch. Diff request body: -want +got: %s", diff) + } + }) + } +} + func TestReconcile_Delete(t *testing.T) { tests := []struct { name string @@ -1314,54 +1753,13 @@ func Test_wrapError(t *testing.T) { } } -func Test_mergeMaps(t *testing.T) { - tests := []struct { - name string - l1, l2 map[string]string - expectedLabels map[string]string - }{{ - name: "Both maps empty", - l1: nil, - l2: nil, - expectedLabels: map[string]string{}, - }, { - name: "Map one empty", - l1: nil, - l2: map[string]string{"k": "v"}, - expectedLabels: map[string]string{"k": "v"}, - }, { - name: "Map two empty", - l1: map[string]string{"k": "v"}, - l2: nil, - expectedLabels: map[string]string{"k": "v"}, - }, { - name: "Both maps", - l1: map[string]string{"k1": "v1"}, - l2: map[string]string{"k2": "v2"}, - expectedLabels: map[string]string{"k1": "v1", "k2": "v2"}, - }, { - name: "Both maps with clobber", - l1: map[string]string{"k1": "v1"}, - l2: map[string]string{"k1": "v2"}, - expectedLabels: map[string]string{"k1": "v2"}, - }} - for i := range tests { - t.Run(tests[i].name, func(t *testing.T) { - actualLabels := mergeMaps(tests[i].l1, tests[i].l2) - if diff := cmp.Diff(tests[i].expectedLabels, actualLabels); diff != "" { - t.Errorf("mergeLabels() did not return expected. -want, +got: %s", diff) - } - }) - } -} - func TestGenerateResourceLabels(t *testing.T) { staticResourceLabels := map[string]string{ "app.kubernetes.io/managed-by": "EventListener", "app.kubernetes.io/part-of": "Triggers", } - expectedLabels := mergeMaps(staticResourceLabels, map[string]string{"eventlistener": eventListenerName}) + expectedLabels := dynamicduck.MergeMaps(staticResourceLabels, map[string]string{"eventlistener": eventListenerName}) actualLabels := GenerateResourceLabels(eventListenerName, staticResourceLabels) if diff := cmp.Diff(expectedLabels, actualLabels); diff != "" { t.Errorf("mergeLabels() did not return expected. -want, +got: %s", diff) @@ -1429,7 +1827,7 @@ func Test_generateObjectMeta(t *testing.T) { Controller: &isController, BlockOwnerDeletion: &blockOwnerDeletion, }}, - Labels: mergeMaps(map[string]string{"k": "v"}, generatedLabels), + Labels: dynamicduck.MergeMaps(map[string]string{"k": "v"}, generatedLabels), }, }, { name: "EventListener with Annotation", diff --git a/test/controller.go b/test/controller.go index af93ccd7e..c172a2972 100644 --- a/test/controller.go +++ b/test/controller.go @@ -18,9 +18,11 @@ package test import ( "context" + "encoding/json" "testing" // Link in the fakes so they get injected into injection.Fake + logger "github.com/sirupsen/logrus" fakepipelineclientset "github.com/tektoncd/pipeline/pkg/client/clientset/versioned/fake" fakepipelineclient "github.com/tektoncd/pipeline/pkg/client/injection/client/fake" fakeresourceclientset "github.com/tektoncd/pipeline/pkg/client/resource/clientset/versioned/fake" @@ -37,10 +39,16 @@ import ( faketriggertemplateinformer "github.com/tektoncd/triggers/pkg/client/injection/informers/triggers/v1alpha1/triggertemplate/fake" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" fakedynamic "k8s.io/client-go/dynamic/fake" fakekubeclientset "k8s.io/client-go/kubernetes/fake" + "knative.dev/pkg/apis/duck" + duckv1 "knative.dev/pkg/apis/duck/v1" + duckinformerfake "knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/fake" fakekubeclient "knative.dev/pkg/client/injection/kube/client/fake" fakedeployinformer "knative.dev/pkg/client/injection/kube/informers/apps/v1/deployment/fake" fakeconfigmapinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/configmap/fake" @@ -49,6 +57,7 @@ import ( fakeserviceinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/service/fake" fakeserviceaccountinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/serviceaccount/fake" "knative.dev/pkg/controller" + fakedynamicclientset "knative.dev/pkg/injection/clients/dynamicclient/fake" ) // Resources represents the desired state of the system (i.e. existing resources) @@ -66,15 +75,18 @@ type Resources struct { Secrets []*corev1.Secret ServiceAccounts []*corev1.ServiceAccount Pods []*corev1.Pod + WithPod []*duckv1.WithPod } // Clients holds references to clients which are useful for reconciler tests. type Clients struct { - Kube *fakekubeclientset.Clientset - Triggers *faketriggersclientset.Clientset - Pipeline *fakepipelineclientset.Clientset - Resource *fakeresourceclientset.Clientset - Dynamic *dynamicclientset.Clientset + Kube *fakekubeclientset.Clientset + Triggers *faketriggersclientset.Clientset + Pipeline *fakepipelineclientset.Clientset + Resource *fakeresourceclientset.Clientset + Dynamic *dynamicclientset.Clientset + DynamicClient *fakedynamic.FakeDynamicClient + DuckInformerFactory duck.InformerFactory } // Assets holds references to the controller and clients. @@ -89,11 +101,12 @@ func SeedResources(t *testing.T, ctx context.Context, r Resources) Clients { t.Helper() dynamicClient := fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()) c := Clients{ - Kube: fakekubeclient.Get(ctx), - Triggers: faketriggersclient.Get(ctx), - Pipeline: fakepipelineclient.Get(ctx), - Resource: fakeresourceclient.Get(ctx), - Dynamic: dynamicclientset.New(tekton.WithClient(dynamicClient)), + Kube: fakekubeclient.Get(ctx), + Triggers: faketriggersclient.Get(ctx), + Pipeline: fakepipelineclient.Get(ctx), + Resource: fakeresourceclient.Get(ctx), + Dynamic: dynamicclientset.New(tekton.WithClient(dynamicClient)), + DynamicClient: fakedynamicclientset.Get(ctx), } // Teach Kube clients about the Tekton resources (needed by discovery client when creating resources) @@ -111,6 +124,7 @@ func SeedResources(t *testing.T, ctx context.Context, r Resources) Clients { secretInformer := fakesecretinformer.Get(ctx) saInformer := fakeserviceaccountinformer.Get(ctx) podInformer := fakepodinformer.Get(ctx) + duckInformerFactory := duckinformerfake.Get(ctx) // Create Namespaces for _, ns := range r.Namespaces { @@ -209,10 +223,35 @@ func SeedResources(t *testing.T, ctx context.Context, r Resources) Clients { t.Fatal(err) } } + for _, d := range r.WithPod { + marshaledData, err := json.Marshal(d) + if err != nil { + logger.Errorf("failed to marshal custom object %v ", err) + t.Fatal(err) + } + data := new(unstructured.Unstructured) + if err := data.UnmarshalJSON(marshaledData); err != nil { + logger.Errorf("failed to unmarshal to unstructured object %v ", err) + t.Fatal(err) + } + gvr, _ := meta.UnsafeGuessKindToResource(data.GetObjectKind().GroupVersionKind()) + shInformer, _, err := duckInformerFactory.Get(ctx, gvr) + if err != nil { + t.Fatal(err) + } + if err := shInformer.GetIndexer().Add(data); err != nil { + t.Fatal(err) + } + dynamicInterface := c.DynamicClient.Resource(gvr) + if _, err := dynamicInterface.Namespace(data.GetNamespace()).Create(context.Background(), data, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + } c.Kube.ClearActions() c.Triggers.ClearActions() c.Pipeline.ClearActions() + c.DynamicClient.ClearActions() return c } @@ -308,6 +347,28 @@ func GetResourcesFromClients(c Clients) (*Resources, error) { for _, pod := range podList.Items { testResources.Pods = append(testResources.Pods, pod.DeepCopy()) } + // Hardcode GVR for custom resource test + gvr := schema.GroupVersionResource{ + Group: "serving.knative.dev", + Version: "v1", + Resource: "services", + } + dynamicInterface := c.DynamicClient.Resource(gvr) + var withPod = duckv1.WithPod{} + customData, err := dynamicInterface.Namespace(ns.Name).List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + for _, cData := range customData.Items { + backToWithPod, err := cData.MarshalJSON() + if err != nil { + return nil, err + } + if err = json.Unmarshal(backToWithPod, &withPod); err != nil { + return nil, err + } + testResources.WithPod = append(testResources.WithPod, withPod.DeepCopy()) + } } return testResources, nil diff --git a/test/controller_test.go b/test/controller_test.go index 727f3e44e..01a533aff 100644 --- a/test/controller_test.go +++ b/test/controller_test.go @@ -24,6 +24,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + duckv1 "knative.dev/pkg/apis/duck/v1" rtesting "knative.dev/pkg/reconciler/testing" ) @@ -145,6 +146,27 @@ func TestGetResourcesFromClients(t *testing.T) { }, } + cData := &duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "knativeservice", + Namespace: "foo", + }, + } + cData1 := &duckv1.WithPod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "serving.knative.dev/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "knativeservice1", + Namespace: "foo", + }, + } + tests := []struct { name string Resources Resources @@ -165,6 +187,7 @@ func TestGetResourcesFromClients(t *testing.T) { Deployments: []*appsv1.Deployment{deployment1}, Services: []*corev1.Service{service1}, Pods: []*corev1.Pod{pod1}, + WithPod: []*duckv1.WithPod{cData}, }, }, { @@ -179,6 +202,7 @@ func TestGetResourcesFromClients(t *testing.T) { Deployments: []*appsv1.Deployment{deployment1, deployment2}, Services: []*corev1.Service{service1, service2}, Pods: []*corev1.Pod{pod1, pod2}, + WithPod: []*duckv1.WithPod{cData, cData1}, }, }, { diff --git a/test/wait.go b/test/wait.go index 10887e203..8f93387e8 100644 --- a/test/wait.go +++ b/test/wait.go @@ -34,9 +34,8 @@ package test import ( "context" - "time" - "testing" + "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" diff --git a/vendor/knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/fake/fake.go b/vendor/knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/fake/fake.go new file mode 100644 index 000000000..b40daf97d --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/fake/fake.go @@ -0,0 +1,30 @@ +/* +Copyright 2020 The Knative 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. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package fake + +import ( + podspecable "knative.dev/pkg/client/injection/ducks/duck/v1/podspecable" + injection "knative.dev/pkg/injection" +) + +var Get = podspecable.Get + +func init() { + injection.Fake.RegisterDuck(podspecable.WithDuck) +} diff --git a/vendor/knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/podspecable.go b/vendor/knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/podspecable.go new file mode 100644 index 000000000..9ae0971da --- /dev/null +++ b/vendor/knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/podspecable.go @@ -0,0 +1,60 @@ +/* +Copyright 2020 The Knative 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. +*/ + +// Code generated by injection-gen. DO NOT EDIT. + +package podspecable + +import ( + context "context" + + duck "knative.dev/pkg/apis/duck" + v1 "knative.dev/pkg/apis/duck/v1" + controller "knative.dev/pkg/controller" + injection "knative.dev/pkg/injection" + dynamicclient "knative.dev/pkg/injection/clients/dynamicclient" + logging "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterDuck(WithDuck) +} + +// Key is used for associating the Informer inside the context.Context. +type Key struct{} + +func WithDuck(ctx context.Context) context.Context { + dc := dynamicclient.Get(ctx) + dif := &duck.CachedInformerFactory{ + Delegate: &duck.TypedInformerFactory{ + Client: dc, + Type: (&v1.PodSpecable{}).GetFullType(), + ResyncPeriod: controller.GetResyncPeriod(ctx), + StopChannel: ctx.Done(), + }, + } + return context.WithValue(ctx, Key{}, dif) +} + +// Get extracts the typed informer from the context. +func Get(ctx context.Context) duck.InformerFactory { + untyped := ctx.Value(Key{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch knative.dev/pkg/apis/duck.InformerFactory from context.") + } + return untyped.(duck.InformerFactory) +} diff --git a/vendor/knative.dev/pkg/injection/clients/dynamicclient/dynamicclient.go b/vendor/knative.dev/pkg/injection/clients/dynamicclient/dynamicclient.go new file mode 100644 index 000000000..2eece5c55 --- /dev/null +++ b/vendor/knative.dev/pkg/injection/clients/dynamicclient/dynamicclient.go @@ -0,0 +1,49 @@ +/* +Copyright 2019 The Knative 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 dynamicclient + +import ( + "context" + + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + + "knative.dev/pkg/injection" + "knative.dev/pkg/logging" +) + +func init() { + injection.Default.RegisterClient(withClient) +} + +// Key is used as the key for associating information +// with a context.Context. +type Key struct{} + +func withClient(ctx context.Context, cfg *rest.Config) context.Context { + return context.WithValue(ctx, Key{}, dynamic.NewForConfigOrDie(cfg)) +} + +// Get extracts the Dynamic client from the context. +func Get(ctx context.Context) dynamic.Interface { + untyped := ctx.Value(Key{}) + if untyped == nil { + logging.FromContext(ctx).Panic( + "Unable to fetch k8s.io/client-go/dynamic.Interface from context.") + } + return untyped.(dynamic.Interface) +} diff --git a/vendor/knative.dev/pkg/injection/clients/dynamicclient/fake/fake.go b/vendor/knative.dev/pkg/injection/clients/dynamicclient/fake/fake.go new file mode 100644 index 000000000..12670edd2 --- /dev/null +++ b/vendor/knative.dev/pkg/injection/clients/dynamicclient/fake/fake.go @@ -0,0 +1,53 @@ +/* +Copyright 2019 The Knative 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 fake + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/rest" + + "knative.dev/pkg/injection" + "knative.dev/pkg/injection/clients/dynamicclient" + "knative.dev/pkg/logging" +) + +func init() { + injection.Fake.RegisterClient(withClient) +} + +func withClient(ctx context.Context, cfg *rest.Config) context.Context { + ctx, _ = With(ctx, runtime.NewScheme()) + return ctx +} + +func With(ctx context.Context, scheme *runtime.Scheme, objects ...runtime.Object) (context.Context, *fake.FakeDynamicClient) { + cs := fake.NewSimpleDynamicClient(scheme, objects...) + return context.WithValue(ctx, dynamicclient.Key{}, cs), cs +} + +// Get extracts the Kubernetes client from the context. +func Get(ctx context.Context) *fake.FakeDynamicClient { + untyped := ctx.Value(dynamicclient.Key{}) + if untyped == nil { + logging.FromContext(ctx).Panicf( + "Unable to fetch %T from context.", (*fake.FakeDynamicClient)(nil)) + } + return untyped.(*fake.FakeDynamicClient) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8720d1560..3d9d28f98 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -266,6 +266,7 @@ github.com/rogpeppe/go-internal/modfile github.com/rogpeppe/go-internal/module github.com/rogpeppe/go-internal/semver # github.com/sirupsen/logrus v1.7.0 +## explicit github.com/sirupsen/logrus # github.com/spf13/cobra v1.0.0 ## explicit @@ -970,6 +971,8 @@ knative.dev/pkg/apis/duck/v1 knative.dev/pkg/apis/duck/v1alpha1 knative.dev/pkg/apis/duck/v1beta1 knative.dev/pkg/changeset +knative.dev/pkg/client/injection/ducks/duck/v1/podspecable +knative.dev/pkg/client/injection/ducks/duck/v1/podspecable/fake knative.dev/pkg/client/injection/kube/client knative.dev/pkg/client/injection/kube/client/fake knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/mutatingwebhookconfiguration @@ -996,6 +999,8 @@ knative.dev/pkg/configmap/informer knative.dev/pkg/controller knative.dev/pkg/hash knative.dev/pkg/injection +knative.dev/pkg/injection/clients/dynamicclient +knative.dev/pkg/injection/clients/dynamicclient/fake knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret knative.dev/pkg/injection/clients/namespacedkube/informers/factory knative.dev/pkg/injection/sharedmain