Skip to content

Commit

Permalink
listen webhook. work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmed-ali-55 committed Aug 28, 2023
1 parent 1d855db commit 7662caa
Show file tree
Hide file tree
Showing 16 changed files with 546 additions and 17 deletions.
3 changes: 0 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ val springCloudVersion: String by extra("2022.0.3")

dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation("org.springframework.boot:spring-boot-starter-web")
Expand All @@ -37,7 +35,6 @@ dependencies {
implementation("io.github.oshai:kotlin-logging-jvm:5.1.0")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.awaitility:awaitility-kotlin:4.2.0")
testImplementation("com.ninja-squad:springmockk:4.0.2")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import net.leanix.vsm.gitlab.broker.connector.adapter.graphql.parser.LanguagePar
import net.leanix.vsm.gitlab.broker.connector.domain.GitLabAssignment
import net.leanix.vsm.gitlab.broker.connector.domain.GitlabProvider
import net.leanix.vsm.gitlab.broker.connector.domain.Repository
import net.leanix.vsm.gitlab.broker.shared.exception.VsmException
import net.leanix.vsm.gitlab.broker.shared.exception.VsmException.GraphqlException
import net.leanix.vsm.gitlab.broker.shared.exception.GraphqlException
import net.leanix.vsm.gitlab.broker.shared.exception.NoRepositoriesFound
import net.leanix.vsm.gitlab.broker.shared.properties.GitLabOnPremProperties
import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Component
Expand Down Expand Up @@ -83,7 +83,7 @@ class GitlabGraphqlProvider(private val gitLabOnPremProperties: GitLabOnPremProp
}
} else {
logger.info { "Zero repositories found" }
throw VsmException.NoRepositoriesFound()
throw NoRepositoriesFound()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import net.leanix.vsm.gitlab.broker.connector.domain.GitlabProvider
import net.leanix.vsm.gitlab.broker.connector.domain.Repository
import net.leanix.vsm.gitlab.broker.connector.domain.RepositoryProvider
import net.leanix.vsm.gitlab.broker.logs.domain.LogStatus
import net.leanix.vsm.gitlab.broker.shared.exception.VsmException
import net.leanix.vsm.gitlab.broker.shared.exception.NoRepositoriesFound
import org.springframework.stereotype.Service

@Service
Expand Down Expand Up @@ -45,7 +45,7 @@ class RepositoryService(

private fun handleExceptions(exception: Throwable, assignment: GitLabAssignment) {
when (exception) {
is VsmException.NoRepositoriesFound -> {
is NoRepositoriesFound -> {
logFailedMessages("vsm.repos.not_found", arrayOf(assignment.connectorConfiguration.orgName), assignment)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package net.leanix.vsm.gitlab.broker.connector.application

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import net.leanix.vsm.gitlab.broker.connector.domain.MergeRequest
import net.leanix.vsm.gitlab.broker.connector.domain.ProjectCreated
import net.leanix.vsm.gitlab.broker.connector.domain.WebhookConsumerService
import net.leanix.vsm.gitlab.broker.connector.domain.WebhookEventType
import net.leanix.vsm.gitlab.broker.connector.json.computeWebhookEventType
import net.leanix.vsm.gitlab.broker.shared.exception.GitlabTokenException
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service

@Service
class WebhookConsumerServiceImpl(
@Value("\${leanix.gitlab.leanix-id}") private val leanixId: String
) : WebhookConsumerService {

val mapper: ObjectMapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
override fun consumeWebhookEvent(payloadToken: String?, payload: String) {

// validate token
if (payloadToken?.equals(leanixId) == true) {
throw GitlabTokenException(payloadToken)
}

// figure out event type
when (computeWebhookEventType(payload)) {
WebhookEventType.REPOSITORY -> processProjectCreated(payload)
WebhookEventType.MERGE_REQUEST -> println()
}
}

private fun processProjectCreated(payload: String) {
val project = mapper.readValue<ProjectCreated>(payload)
println(project)
}

private fun processMergeRequest(payload: String) {
val mergeRequest = mapper.readValue<MergeRequest>(payload)
println(mergeRequest)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.leanix.vsm.gitlab.broker.connector.domain

interface WebhookConsumerService {
fun consumeWebhookEvent(payloadToken: String?, payload: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package net.leanix.vsm.gitlab.broker.connector.domain

import com.fasterxml.jackson.annotation.JsonProperty
import java.util.Date

data class ProjectCreated(
@JsonProperty("project_id")
val id: Int,
val eventName: String,
val name: String,
val path: String,
@JsonProperty("path_with_namespace")
val pathWithNamespace: String,
@JsonProperty("project_visibility")
val projectVisibility: String,
)

data class MergeRequest(
val project: Project,
@JsonProperty("object_attributes")
val objectAttributes: ObjectAttributes,
)

data class Project(
val id: Int,
val name: String,
@JsonProperty("web_url")
val webURL: String,
val namespace: String,
@JsonProperty("path_with_namespace")
val pathWithNamespace: String,
@JsonProperty("default_branch")
val defaultBranch: String,
)

data class ObjectAttributes(
val description: String,
val created_at: Date,
val source_branch: String,
val target_branch: String,
val title: String,
val updated_at: String,
val last_commit: GitlabCommit,
val state: String,
val action: String,
)

data class GitlabCommit(
val id: String,
val message: String,
val title: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.leanix.vsm.gitlab.broker.connector.domain

enum class WebhookEventType {
REPOSITORY,
MERGE_REQUEST
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.leanix.vsm.gitlab.broker.connector.json

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import net.leanix.vsm.gitlab.broker.connector.domain.WebhookEventType
import net.leanix.vsm.gitlab.broker.shared.exception.GitlabPayloadNotSupportedException

val mapper: ObjectMapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

fun computeWebhookEventType(payload: String): WebhookEventType {
val node = mapper.readTree(payload)

return if (node.at("/event_name").asText() == "project_create") WebhookEventType.REPOSITORY
else if (
node.at("/event_type").asText() == "merge_request"
&& node.at("/object_attributes/action").asText() == "merge"
&& node.path("/project/default_branch").asText()
== node.path("/object_attributes/target_branch").asText()
) WebhookEventType.MERGE_REQUEST
else throw GitlabPayloadNotSupportedException()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package net.leanix.vsm.gitlab.broker.connector.rest

import net.leanix.vsm.gitlab.broker.connector.domain.WebhookConsumerService
import net.leanix.vsm.gitlab.broker.webhook.adapter.feign.LEANIX_WEBHOOK_PATH
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(LEANIX_WEBHOOK_PATH)
class GitlabWebhookController(
private val webhookService: WebhookConsumerService
) {

private val logger = LoggerFactory.getLogger(this::class.java)

@PostMapping
@ResponseStatus(HttpStatus.ACCEPTED)
fun webhook(
@RequestHeader("X-Gitlab-Instance", required = false) instance: String?,
@RequestHeader("X-Gitlab-Webhook-UUID", required = false) webhookUUID: String?,
@RequestHeader("X-Gitlab-Event", required = false) event: String?,
@RequestHeader("X-Gitlab-Event-UUID", required = false) eventUUID: String?,
@RequestHeader("X-Gitlab-Token", required = false) payloadToken: String?,
@RequestBody payload: String
) {
logger.info("instance: $instance")
logger.info("webhookUUID: $webhookUUID")
logger.info("event: $event")
logger.info("eventUUID: $eventUUID")
logger.info("payloadToken: $payloadToken")
logger.info("payload: $payload")

runCatching {
webhookService.consumeWebhookEvent(payloadToken, payload)
}.onFailure {
logger.error("Error consuming github event", it)
}
}

@GetMapping
fun health() = "OK"
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class InitialStateRunner(

override fun run(args: ApplicationArguments?) {
logger.info { "Started to get initial state" }
fetchAssignments()
// fetchAssignments()
setupWebhook()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.leanix.vsm.gitlab.broker.shared.exception

sealed class VsmException(message: String? = null) : RuntimeException(message)

class NoRepositoriesFound : VsmException()

class GraphqlException(message: String?) : VsmException(message)
class GitlabTokenException(token: String?) : VsmException(message = "Invalid gitlab payload token: $token")
class GitlabPayloadNotSupportedException() :
VsmException(message = "Payload is neither for project creation nor for merge request being merged")

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.leanix.vsm.gitlab.broker.connector.json

import net.leanix.vsm.gitlab.broker.connector.domain.WebhookEventType
import net.leanix.vsm.gitlab.broker.shared.exception.GitlabPayloadNotSupportedException
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class GitlabParseUtilTest {

@Test
fun `should return webhook event type REPOSITORY when event_name=project_create in payload`() {
val payload = this::class.java.getResource("/webhook_calls/project_created.json")!!.readText()

val result = computeWebhookEventType(payload)

assertEquals(WebhookEventType.REPOSITORY, result)
}

@Test
fun `should return webhook event type MERGE_REQUEST when object_kind=merge_request and action=merged in payload`() {
val payload = this::class.java.getResource("/webhook_calls/merge_request_merged.json")!!.readText()

val result = computeWebhookEventType(payload)

assertEquals(WebhookEventType.MERGE_REQUEST, result)
}

@Test
fun `should throw GitlabPayloadNotSupportedException when object_kind=merge_request and action!=merged in payload`() {
val payload = this::class.java.getResource("/webhook_calls/merge_request_opened.json")!!.readText()

assertThrows<GitlabPayloadNotSupportedException> { computeWebhookEventType(payload) }
}

@Test
fun `should throw GitlabPayloadNotSupportedException when payload has no supported fields`() {

assertThrows<GitlabPayloadNotSupportedException> {
computeWebhookEventType("{ \"dummy_key\": \"dummy value\" }")
}
}
}
Loading

0 comments on commit 7662caa

Please sign in to comment.