From 1a858922af84287a49ddec5386b9a73ee51cd0fb Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri Date: Wed, 29 Mar 2023 11:57:06 +0100 Subject: [PATCH 1/9] feat(cdevents-webhooks) : CDEvents webhook API implementation --- .../spinnaker/echo/model/trigger/CDEvent.java | 39 +++++ .../eventhandlers/CDEventsWebhookHandler.java | 136 ++++++++++++++++++ echo-webhooks/echo-webhooks.gradle | 3 + .../CloudEventHandlerConfiguration.java | 16 +++ .../controllers/WebhooksController.groovy | 32 +++++ 5 files changed, 226 insertions(+) create mode 100644 echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java create mode 100644 echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java create mode 100644 echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/config/CloudEventHandlerConfiguration.java diff --git a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java new file mode 100644 index 000000000..bca270048 --- /dev/null +++ b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java @@ -0,0 +1,39 @@ +/* + * Copyright 2016 Google, Inc. + * + * 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 com.netflix.spinnaker.echo.model.trigger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.netflix.spinnaker.kork.artifacts.model.Artifact; +import java.util.List; +import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties(ignoreUnknown = true) +public class CDEvent extends TriggerEvent { + public static final String TYPE = "CDEVENTS"; + private Content content; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Content { + private List artifacts; + private Map parameters; + } +} diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java new file mode 100644 index 000000000..3c2c7183a --- /dev/null +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java @@ -0,0 +1,136 @@ +/* + * 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 com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers; + +import static com.netflix.spinnaker.echo.pipelinetriggers.artifacts.ArtifactMatcher.isJsonPathConstraintInPayload; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spectator.api.Registry; +import com.netflix.spinnaker.echo.model.Pipeline; +import com.netflix.spinnaker.echo.model.Trigger; +import com.netflix.spinnaker.echo.model.trigger.CDEvent; +import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator; +import com.netflix.spinnaker.kork.artifacts.model.Artifact; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation of TriggerEventHandler for events of type {@link CDEvent}, which occur when a + * CDEvents webhook is received. + */ +@Slf4j +@Component +public class CDEventsWebhookHandler extends BaseTriggerEventHandler { + private static final String TRIGGER_TYPE = "cdevents"; + private static final List supportedTriggerTypes = Collections.singletonList(TRIGGER_TYPE); + + @Autowired + public CDEventsWebhookHandler( + Registry registry, + ObjectMapper objectMapper, + FiatPermissionEvaluator fiatPermissionEvaluator) { + super(registry, objectMapper, fiatPermissionEvaluator); + } + + @Override + public List supportedTriggerTypes() { + return supportedTriggerTypes; + } + + @Override + public boolean handleEventType(String eventType) { + return eventType != null && !eventType.equals("manual"); + } + + @Override + public Class getEventType() { + return CDEvent.class; + } + + @Override + public boolean isSuccessfulTriggerEvent(CDEvent cdEvent) { + return true; + } + + @Override + protected Function buildTrigger(CDEvent cdEvent) { + CDEvent.Content content = cdEvent.getContent(); + Map payload = cdEvent.getPayload(); + + return trigger -> + trigger + .atParameters(content.getParameters()) + .atPayload(payload) + .atEventId(cdEvent.getEventId()); + } + + @Override + protected boolean isValidTrigger(Trigger trigger) { + return trigger.isEnabled() && TRIGGER_TYPE.equals(trigger.getType()); + } + + @Override + protected Predicate matchTriggerFor(CDEvent cdEvent) { + final String type = cdEvent.getDetails().getType(); + final String source = cdEvent.getDetails().getSource(); + + return trigger -> + trigger.getType() != null + && trigger.getType().equalsIgnoreCase(type) + && trigger.getSource() != null + && trigger.getSource().equals(source) + && (trigger.getAttributeConstraints() != null + && isAttributeConstraintInReqHeader( + trigger.getAttributeConstraints(), cdEvent.getDetails().getRequestHeaders())) + && (trigger.getPayloadConstraints() == null + || (trigger.getPayloadConstraints() != null + && isJsonPathConstraintInPayload( + trigger.getPayloadConstraints(), cdEvent.getPayload()))); + } + + private boolean isAttributeConstraintInReqHeader( + final Map attConstraints, final Map> reqHeaders) { + for (Object key : attConstraints.keySet()) { + if (!reqHeaders.containsKey(key) || reqHeaders.get(key).isEmpty()) { + return false; + } + + if (attConstraints.get(key) != null + && !(reqHeaders.get(key).contains(attConstraints.get(key).toString()))) { + return false; + } + } + return true; + } + + @Override + public Map getAdditionalTags(Pipeline pipeline) { + Map tags = new HashMap<>(); + tags.put("type", pipeline.getTrigger().getType()); + return tags; + } + + @Override + protected List getArtifactsFromEvent(CDEvent cdEvent, Trigger trigger) { + return cdEvent.getContent().getArtifacts(); + } +} diff --git a/echo-webhooks/echo-webhooks.gradle b/echo-webhooks/echo-webhooks.gradle index f7cdbef6f..00737abb5 100644 --- a/echo-webhooks/echo-webhooks.gradle +++ b/echo-webhooks/echo-webhooks.gradle @@ -20,6 +20,9 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-web" implementation "io.spinnaker.kork:kork-artifacts" implementation "javax.validation:validation-api" + implementation "io.cloudevents:cloudevents-spring:2.3.0" + implementation "io.cloudevents:cloudevents-json-jackson:2.3.0" + implementation "io.cloudevents:cloudevents-http-basic:2.3.0" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.assertj:assertj-core" diff --git a/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/config/CloudEventHandlerConfiguration.java b/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/config/CloudEventHandlerConfiguration.java new file mode 100644 index 000000000..8550bb79a --- /dev/null +++ b/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/config/CloudEventHandlerConfiguration.java @@ -0,0 +1,16 @@ +package com.netflix.spinnaker.echo.config; + +import io.cloudevents.spring.mvc.CloudEventHttpMessageConverter; +import java.util.List; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CloudEventHandlerConfiguration implements WebMvcConfigurer { + + @Override + public void configureMessageConverters(List> converters) { + converters.add(0, new CloudEventHttpMessageConverter()); + } +} diff --git a/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/controllers/WebhooksController.groovy b/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/controllers/WebhooksController.groovy index b47b33794..6c3996e3a 100644 --- a/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/controllers/WebhooksController.groovy +++ b/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/controllers/WebhooksController.groovy @@ -29,6 +29,8 @@ import org.springframework.http.HttpHeaders import org.springframework.util.CollectionUtils import org.springframework.util.MultiValueMap import org.springframework.web.bind.annotation.* +import io.cloudevents.CloudEvent +import java.nio.charset.StandardCharsets @RestController @Slf4j @@ -132,6 +134,36 @@ class WebhooksController { WebhookResponse.newInstance(eventProcessed: true, eventId: event.eventId) } + @RequestMapping(value = '/webhooks/cdevents/{source}', method = RequestMethod.POST) + WebhooksController.WebhookResponse forwardEvent(@PathVariable String source, + @RequestBody CloudEvent cdevent, + @RequestHeader HttpHeaders headers) { + log.info("CDEvents Webhook received with source ${source} and with event type ${cdevent.getType()}") + + String ceDataJsonString = new String(cdevent.getData().toBytes(), StandardCharsets.UTF_8); + + Event event = new Event() + boolean sendEvent = true + event.details = new Metadata() + event.details.source = source + event.details.type = "cdevents" + event.details.requestHeaders = headers + event.rawContent = ceDataJsonString + Map postedEvent + try { + postedEvent = mapper.readValue(ceDataJsonString, Map) ?: [:] + } catch (Exception e) { + log.error("Failed to parse payload ceDataJsonString: {}", ceDataJsonString, e); + throw e + } + event.content = postedEvent + event.payload = new HashMap(postedEvent) + + propagator.processEvent(event) + + WebhookResponse.newInstance(eventProcessed: true, eventId: event.eventId) + } + private static class WebhookResponse { boolean eventProcessed; String eventId; From 857d49132335b276907a1083b19091eaadab0349 Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri Date: Wed, 12 Apr 2023 13:33:40 +0100 Subject: [PATCH 2/9] feat(cdevents-webhooks) : tests for CDEvents webhook API --- .../spinnaker/echo/model/trigger/CDEvent.java | 1 - .../eventhandlers/CDEventsWebhookHandler.java | 8 +- .../CDEventsWebhookHandlerSpec.groovy | 151 ++++++++++++++++++ .../spinnaker/echo/test/RetrofitStubs.groovy | 24 ++- .../CloudEventHandlerConfiguration.java | 15 ++ .../controllers/WebhooksControllerSpec.groovy | 41 +++++ 6 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandlerSpec.groovy diff --git a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java index bca270048..02d585627 100644 --- a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java +++ b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java @@ -1,5 +1,4 @@ /* - * Copyright 2016 Google, Inc. * * Licensed under the Apache License, Version 2.0 (the "License") * you may not use this file except in compliance with the License. diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java index 3c2c7183a..97c6d0279 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java @@ -98,9 +98,11 @@ protected Predicate matchTriggerFor(CDEvent cdEvent) { && trigger.getType().equalsIgnoreCase(type) && trigger.getSource() != null && trigger.getSource().equals(source) - && (trigger.getAttributeConstraints() != null - && isAttributeConstraintInReqHeader( - trigger.getAttributeConstraints(), cdEvent.getDetails().getRequestHeaders())) + && (trigger.getAttributeConstraints() == null + || trigger.getAttributeConstraints() != null + && isAttributeConstraintInReqHeader( + trigger.getAttributeConstraints(), + cdEvent.getDetails().getRequestHeaders())) && (trigger.getPayloadConstraints() == null || (trigger.getPayloadConstraints() != null && isJsonPathConstraintInPayload( diff --git a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandlerSpec.groovy b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandlerSpec.groovy new file mode 100644 index 000000000..0bbb20044 --- /dev/null +++ b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandlerSpec.groovy @@ -0,0 +1,151 @@ +/* + * 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 com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers + + +import com.netflix.spectator.api.NoopRegistry +import com.netflix.spinnaker.echo.api.events.Metadata +import com.netflix.spinnaker.echo.jackson.EchoObjectMapper +import com.netflix.spinnaker.echo.model.pubsub.MessageDescription +import com.netflix.spinnaker.echo.model.pubsub.PubsubSystem +import com.netflix.spinnaker.echo.model.trigger.PubsubEvent +import com.netflix.spinnaker.echo.test.RetrofitStubs +import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator +import com.netflix.spinnaker.kork.artifacts.model.Artifact +import com.netflix.spinnaker.kork.artifacts.model.ExpectedArtifact +import groovy.json.JsonOutput +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +class CDEventsWebhookHandlerSpec extends Specification implements RetrofitStubs { + def registry = new NoopRegistry() + def objectMapper = EchoObjectMapper.getInstance() + def handlerSupport = new EventHandlerSupport() + def fiatPermissionEvaluator = Mock(FiatPermissionEvaluator) + + @Shared + def goodExpectedArtifacts = [ + ExpectedArtifact.builder() + .matchArtifact( + Artifact.builder() + .name('myArtifact') + .type('artifactType') + .build()) + .id('goodId') + .build() + ] + + @Subject + def eventHandler = new CDEventsWebhookHandler(registry, objectMapper, fiatPermissionEvaluator) + + void setup() { + fiatPermissionEvaluator.hasPermission(_ as String, _ as String, "APPLICATION", "EXECUTE") >> true + } + + def 'triggers pipelines for successful builds for CDEvent'() { + given: + def pipeline = createPipelineWith(goodExpectedArtifacts, trigger) + def pipelines = handlerSupport.pipelineCache(pipeline) + + when: + def matchingPipelines = eventHandler.getMatchingPipelines(event, pipelines) + print("matchingPipelines=======>>>> " +matchingPipelines) + + then: + matchingPipelines.size() == 1 + matchingPipelines[0].application == pipeline.application + matchingPipelines[0].name == pipeline.name + + where: + event = createCDEvent('pipelineRunFinished', + [foo: 'bar', artifacts: [[name: 'myArtifact', type: 'artifactType']]]) + trigger = enabledCDEventsTrigger + .withSource('pipelineRunFinished') + .withPayloadConstraints([foo: 'bar']) + .withExpectedArtifactIds(['goodId']) + } + + def 'attaches cdevents trigger to the pipeline'() { + given: + def pipelines = handlerSupport.pipelineCache(pipeline) + + when: + def matchingPipelines = eventHandler.getMatchingPipelines(event, pipelines) + + then: + matchingPipelines.size() == 1 + matchingPipelines[0].trigger.type == enabledCDEventsTrigger.type + + where: + event = createCDEvent('pipelineRunStarted') + pipeline = createPipelineWith([], + enabledCDEventsTrigger.withSource('pipelineRunStarted'), + disabledCDEventsTrigger) + } + + def "triggers pipeline on matching attribute constraints"() { + given: + def pipeline = createPipelineWith(goodExpectedArtifacts, trigger) + def pipelines = handlerSupport.pipelineCache(pipeline) + def requestHeaders = new TreeMap<>() + def listHeaders = new ArrayList<>() + listHeaders.add("dev.cdevents.artifactPublished") + requestHeaders.put("ce-type", listHeaders) + def event = createCDEventRequestHeaders('artifactPublished', + [foo: 'bar', artifacts: [[name: 'myArtifact', type: 'artifactType']]], requestHeaders ) + + when: + def matchingPipelines = eventHandler.getMatchingPipelines(event, pipelines) + + then: + matchingPipelines.size() == 1 + matchingPipelines[0].application == pipeline.application + matchingPipelines[0].name == pipeline.name + + where: + trigger = enabledCDEventsTrigger + .withSource('artifactPublished') + .withAttributeConstraints(['ce-type':'dev.cdevents.artifactPublished']) + .withPayloadConstraints([foo: 'bar']) + .withExpectedArtifactIds(['goodId']) + + } + + @Unroll + def "does not trigger #description pipelines for CDEvent"() { + given: + def pipelines = handlerSupport.pipelineCache(pipeline) + + when: + def matchingPipelines = eventHandler.getMatchingPipelines(event, pipelines) + + then: + matchingPipelines.size() == 0 + + where: + trigger | description + disabledCDEventsTrigger | 'disabled cdevents trigger' + enabledCDEventsTrigger.withSource('wrongName') | 'different source name' + enabledCDEventsTrigger.withSource('artifactPackaged').withPayloadConstraints([foo: 'bar']) | + 'unsatisfied payload constraints' + enabledWebhookTrigger.withSource('artifactPackaged') .withExpectedArtifactIds(['goodId']) | + 'unmatched expected artifact' + + pipeline = createPipelineWith(goodExpectedArtifacts, trigger) + event = createCDEvent('artifactPackaged') + } +} diff --git a/echo-test/src/main/groovy/com/netflix/spinnaker/echo/test/RetrofitStubs.groovy b/echo-test/src/main/groovy/com/netflix/spinnaker/echo/test/RetrofitStubs.groovy index 4fadd634f..9250a8412 100644 --- a/echo-test/src/main/groovy/com/netflix/spinnaker/echo/test/RetrofitStubs.groovy +++ b/echo-test/src/main/groovy/com/netflix/spinnaker/echo/test/RetrofitStubs.groovy @@ -56,7 +56,8 @@ trait RetrofitStubs { .enabled(false).type('pubsub').pubsubSystem('google').subscriptionName('projects/project/subscriptions/subscription').expectedArtifactIds([]).build() final Trigger enabledHelmTrigger = Trigger.builder().enabled(true).type('helm').account('account').version('1.0.0').digest('digest').build() final Trigger disabledHelmTrigger = Trigger.builder().enabled(false).type('helm').account('account').version('1.0.0').digest('digest').build() - + final Trigger enabledCDEventsTrigger = Trigger.builder().enabled(true).type('cdevents').build() + final Trigger disabledCDEventsTrigger = Trigger.builder().enabled(false).type('cdevents').build() private nextId = new AtomicInteger(1) RetrofitError unavailable() { @@ -109,6 +110,27 @@ trait RetrofitStubs { return res } + CDEvent createCDEvent(final String source) { + return createCDEvent(source, [:]) + } + + CDEvent createCDEvent(final String source, final Map payload) { + def res = new CDEvent() + res.details = new Metadata([type: CDEvent.TYPE, source: source]) + res.payload = payload + res.content = EchoObjectMapper.getInstance().convertValue(payload, CDEvent.Content) + return res + } + + CDEvent createCDEventRequestHeaders(final String source, final Map payload, final TreeMap requestHeaders) { + def res = new CDEvent() + res.details = new Metadata([type: CDEvent.TYPE, source: source]) + res.payload = payload + res.content = EchoObjectMapper.getInstance().convertValue(payload, CDEvent.Content) + res.details.requestHeaders = requestHeaders + return res + } + PubsubEvent createPubsubEvent(PubsubSystem pubsubSystem, String subscriptionName, List artifacts, Map payload) { def res = new PubsubEvent() diff --git a/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/config/CloudEventHandlerConfiguration.java b/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/config/CloudEventHandlerConfiguration.java index 8550bb79a..fe55b9553 100644 --- a/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/config/CloudEventHandlerConfiguration.java +++ b/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/config/CloudEventHandlerConfiguration.java @@ -1,3 +1,18 @@ +/* + * + * 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 com.netflix.spinnaker.echo.config; import io.cloudevents.spring.mvc.CloudEventHttpMessageConverter; diff --git a/echo-webhooks/src/test/groovy/com/netflix/spinnaker/echo/controllers/WebhooksControllerSpec.groovy b/echo-webhooks/src/test/groovy/com/netflix/spinnaker/echo/controllers/WebhooksControllerSpec.groovy index f71b05ca0..c045dd17f 100644 --- a/echo-webhooks/src/test/groovy/com/netflix/spinnaker/echo/controllers/WebhooksControllerSpec.groovy +++ b/echo-webhooks/src/test/groovy/com/netflix/spinnaker/echo/controllers/WebhooksControllerSpec.groovy @@ -28,6 +28,11 @@ import com.netflix.spinnaker.echo.scm.StashWebhookEventHandler import com.netflix.spinnaker.echo.scm.bitbucket.server.BitbucketServerEventHandler import org.springframework.http.HttpHeaders import spock.lang.Specification +import io.cloudevents.CloudEvent +import io.cloudevents.core.builder.CloudEventBuilder; + + +import java.nio.charset.StandardCharsets class WebhooksControllerSpec extends Specification { @@ -815,4 +820,40 @@ class WebhooksControllerSpec extends Specification { event.content.branch == "master" event.content.action == "repo:push" } + + void "handles CDEvents Webhook Event"() { + def event + + given: + WebhooksController controller = new WebhooksController(mapper: EchoObjectMapper.getInstance(), scmWebhookHandler: scmWebhookHandler) + controller.propagator = Mock(EventPropagator) + + HttpHeaders headers = new HttpHeaders(); + headers.add("Ce-Id", "1234") + headers.add("Ce-Specversion", "1.0") + headers.add("Ce-Type", "dev.cdevents.artifact.packaged") + + String eventData = "{\"id\": \"1234\", \"subject\": \"event\"}"; + CloudEvent cdevent = CloudEventBuilder.v1() // + .withId("12345") // + .withType("dev.cdevents.artifact.packaged") // + .withSource(URI.create("https://cdevents.dev")) // + .withData(eventData.getBytes(StandardCharsets.UTF_8)) // + .build(); + + when: + def response = controller.forwardEvent("artifactPackaged",cdevent, headers) + + then: + 1 * controller.propagator.processEvent(_) >> { + event = it[0] + } + + event.details.type == "cdevents" + event.details.source == "artifactPackaged" + event.rawContent == eventData + event.details.requestHeaders.get("Ce-Type")[0] == "dev.cdevents.artifact.packaged" + event.details.requestHeaders.get("Ce-Id")[0] == "1234" + + } } From da4d45cefffdd755c8d56a10790bc5011952e257 Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri Date: Mon, 22 May 2023 16:19:20 +0100 Subject: [PATCH 3/9] fix(cdevents-webhooks) : Response type to match with events-broker expectations --- .../spinnaker/echo/controllers/WebhooksController.groovy | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/controllers/WebhooksController.groovy b/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/controllers/WebhooksController.groovy index 6c3996e3a..b7daa4f9c 100644 --- a/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/controllers/WebhooksController.groovy +++ b/echo-webhooks/src/main/groovy/com/netflix/spinnaker/echo/controllers/WebhooksController.groovy @@ -26,6 +26,7 @@ import com.netflix.spinnaker.echo.api.events.Metadata import groovy.util.logging.Slf4j import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpHeaders +import org.springframework.http.ResponseEntity import org.springframework.util.CollectionUtils import org.springframework.util.MultiValueMap import org.springframework.web.bind.annotation.* @@ -135,9 +136,9 @@ class WebhooksController { } @RequestMapping(value = '/webhooks/cdevents/{source}', method = RequestMethod.POST) - WebhooksController.WebhookResponse forwardEvent(@PathVariable String source, - @RequestBody CloudEvent cdevent, - @RequestHeader HttpHeaders headers) { + ResponseEntity forwardEvent(@PathVariable String source, + @RequestBody CloudEvent cdevent, + @RequestHeader HttpHeaders headers) { log.info("CDEvents Webhook received with source ${source} and with event type ${cdevent.getType()}") String ceDataJsonString = new String(cdevent.getData().toBytes(), StandardCharsets.UTF_8); @@ -162,6 +163,8 @@ class WebhooksController { propagator.processEvent(event) WebhookResponse.newInstance(eventProcessed: true, eventId: event.eventId) + ResponseEntity.ok().build(); + } private static class WebhookResponse { From b0568b32d8c366f5df7582435576d89ca86bf9e8 Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri Date: Thu, 22 Jun 2023 11:34:59 +0100 Subject: [PATCH 4/9] addressing review comments --- .../spinnaker/echo/model/WebhookContent.java | 40 +++++++++++++++++++ .../spinnaker/echo/model/trigger/CDEvent.java | 12 ++---- .../echo/model/trigger/WebhookEvent.java | 11 +---- 3 files changed, 45 insertions(+), 18 deletions(-) create mode 100644 echo-model/src/main/java/com/netflix/spinnaker/echo/model/WebhookContent.java diff --git a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/WebhookContent.java b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/WebhookContent.java new file mode 100644 index 000000000..a41b03c1a --- /dev/null +++ b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/WebhookContent.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 Nordix Foundation. + * + * 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 com.netflix.spinnaker.echo.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.netflix.spinnaker.echo.model.trigger.TriggerEvent; +import com.netflix.spinnaker.kork.artifacts.model.Artifact; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; +import java.util.Map; + +@Data +@EqualsAndHashCode(callSuper = true) +@JsonIgnoreProperties(ignoreUnknown = true) +public class WebhookContent extends TriggerEvent { + private Content content; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Content { + private List artifacts; + private Map parameters; + } +} diff --git a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java index 02d585627..84c887eb8 100644 --- a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java +++ b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Nordix Foundation. * * Licensed under the Apache License, Version 2.0 (the "License") * you may not use this file except in compliance with the License. @@ -16,6 +17,7 @@ package com.netflix.spinnaker.echo.model.trigger; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.netflix.spinnaker.echo.model.WebhookContent; import com.netflix.spinnaker.kork.artifacts.model.Artifact; import java.util.List; import java.util.Map; @@ -25,14 +27,6 @@ @Data @EqualsAndHashCode(callSuper = true) @JsonIgnoreProperties(ignoreUnknown = true) -public class CDEvent extends TriggerEvent { +public class CDEvent extends WebhookContent { public static final String TYPE = "CDEVENTS"; - private Content content; - - @Data - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Content { - private List artifacts; - private Map parameters; - } } diff --git a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/WebhookEvent.java b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/WebhookEvent.java index 78eaf4a95..7b24eb318 100644 --- a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/WebhookEvent.java +++ b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/WebhookEvent.java @@ -17,6 +17,7 @@ package com.netflix.spinnaker.echo.model.trigger; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.netflix.spinnaker.echo.model.WebhookContent; import com.netflix.spinnaker.kork.artifacts.model.Artifact; import java.util.List; import java.util.Map; @@ -26,14 +27,6 @@ @Data @EqualsAndHashCode(callSuper = true) @JsonIgnoreProperties(ignoreUnknown = true) -public class WebhookEvent extends TriggerEvent { +public class WebhookEvent extends WebhookContent { public static final String TYPE = "WEBHOOK"; - private Content content; - - @Data - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Content { - private List artifacts; - private Map parameters; - } } From 55bd9798fa9f06c4952fb0ca311c5cc5cb5a6cb4 Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri <97948659+rjalander@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:37:46 +0100 Subject: [PATCH 5/9] Update echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java Co-authored-by: Matt <6519811+mattgogerly@users.noreply.github.com> --- .../eventhandlers/CDEventsWebhookHandler.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java index 97c6d0279..fa287d1d6 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java @@ -126,9 +126,7 @@ private boolean isAttributeConstraintInReqHeader( @Override public Map getAdditionalTags(Pipeline pipeline) { - Map tags = new HashMap<>(); - tags.put("type", pipeline.getTrigger().getType()); - return tags; + return Map.of("type", pipeline.getTrigger().getType()); } @Override From 055899239062ca2363b635ed17e0f5e302e63fba Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri <97948659+rjalander@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:37:58 +0100 Subject: [PATCH 6/9] Update echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java Co-authored-by: Matt <6519811+mattgogerly@users.noreply.github.com> --- .../pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java index fa287d1d6..9ab0e95cd 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java @@ -41,7 +41,7 @@ @Component public class CDEventsWebhookHandler extends BaseTriggerEventHandler { private static final String TRIGGER_TYPE = "cdevents"; - private static final List supportedTriggerTypes = Collections.singletonList(TRIGGER_TYPE); + private static final List supportedTriggerTypes = List.of(TRIGGER_TYPE); @Autowired public CDEventsWebhookHandler( From 82684d25448956eee48d37f61f077fdbb9e22939 Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri Date: Thu, 22 Jun 2023 11:39:18 +0100 Subject: [PATCH 7/9] Updating cloudevents version to 2.5.0 --- echo-webhooks/echo-webhooks.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/echo-webhooks/echo-webhooks.gradle b/echo-webhooks/echo-webhooks.gradle index 3877ee4ad..1e5585ef0 100644 --- a/echo-webhooks/echo-webhooks.gradle +++ b/echo-webhooks/echo-webhooks.gradle @@ -20,9 +20,9 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-web" implementation "io.spinnaker.kork:kork-artifacts" implementation "javax.validation:validation-api" - implementation "io.cloudevents:cloudevents-spring:2.3.0" - implementation "io.cloudevents:cloudevents-json-jackson:2.3.0" - implementation "io.cloudevents:cloudevents-http-basic:2.3.0" + implementation "io.cloudevents:cloudevents-spring:2.5.0" + implementation "io.cloudevents:cloudevents-json-jackson:2.5.0" + implementation "io.cloudevents:cloudevents-http-basic:2.5.0" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.assertj:assertj-core" From c9159af52965097826327e285aa08a4d0e6fb7c3 Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri Date: Fri, 23 Jun 2023 15:21:26 +0100 Subject: [PATCH 8/9] fixing format violations --- .../com/netflix/spinnaker/echo/model/WebhookContent.java | 5 ++--- .../com/netflix/spinnaker/echo/model/trigger/CDEvent.java | 3 --- .../netflix/spinnaker/echo/model/trigger/WebhookEvent.java | 3 --- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/WebhookContent.java b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/WebhookContent.java index a41b03c1a..5612bd420 100644 --- a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/WebhookContent.java +++ b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/WebhookContent.java @@ -19,11 +19,10 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.netflix.spinnaker.echo.model.trigger.TriggerEvent; import com.netflix.spinnaker.kork.artifacts.model.Artifact; -import lombok.Data; -import lombok.EqualsAndHashCode; - import java.util.List; import java.util.Map; +import lombok.Data; +import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = true) diff --git a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java index 84c887eb8..358e01fc3 100644 --- a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java +++ b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/CDEvent.java @@ -18,9 +18,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.netflix.spinnaker.echo.model.WebhookContent; -import com.netflix.spinnaker.kork.artifacts.model.Artifact; -import java.util.List; -import java.util.Map; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/WebhookEvent.java b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/WebhookEvent.java index 7b24eb318..b0be357fe 100644 --- a/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/WebhookEvent.java +++ b/echo-model/src/main/java/com/netflix/spinnaker/echo/model/trigger/WebhookEvent.java @@ -18,9 +18,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.netflix.spinnaker.echo.model.WebhookContent; -import com.netflix.spinnaker.kork.artifacts.model.Artifact; -import java.util.List; -import java.util.Map; import lombok.Data; import lombok.EqualsAndHashCode; From 1a4304a3cbd3e68199c5a89c371b3741a2446ee1 Mon Sep 17 00:00:00 2001 From: Jalander Ramagiri Date: Mon, 26 Jun 2023 09:16:29 +0100 Subject: [PATCH 9/9] fix format violations --- .../pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java index 9ab0e95cd..22923286c 100644 --- a/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java +++ b/echo-pipelinetriggers/src/main/java/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/CDEventsWebhookHandler.java @@ -23,8 +23,6 @@ import com.netflix.spinnaker.echo.model.trigger.CDEvent; import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator; import com.netflix.spinnaker.kork.artifacts.model.Artifact; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function;