Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CID-1812 Implements heartbeat after getting assignment #4

Merged
merged 4 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# vsm-gitlab-broker
VSM GitLab Broker is used to establish the communication between VSM SaaS Application and GitLab Enterprise
on premise deployments that are not publicly accessible from the internet.


## Usage

The VSM GitLab Broker is published as a Docker image. The configuration is performed with environment variables as
described below.

To use the Broker client with a GitLab Enterprise deployment, run `docker pull leanixacrpublic.azurecr.io/vsm-gitlab-broker` tag. The following environment variables are mandatory to configure the Broker client:

- `LEANIX_DOMAIN` - the LeanIX domain, obtained from your LeanIX url (example if your workspace is located at `https://my-company.leanix.net` then the domain is `my-company`).
- `LEANIX_API_TOKEN` - the LeanIX token, obtained from your admin panel. :warning: Make sure the api token has `ADMIN`rights.


#### Command-line arguments

You can run the docker container by providing the relevant configuration:

```console
docker run --pull=always --restart=always \
-p 8080:8080 \
-e LEANIX_DOMAIN=<region>.leanix.net \
-e LEANIX_TECHNICAL_USER_TOKEN=<technical_user-token>\
leanixacrpublic.azurecr.io/vsm-gitlab-broker
```
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ dependencies {
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")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
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")
}

dependencyManagement {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package net.leanix.vsm.gitlab.broker

import net.leanix.vsm.gitlab.broker.shared.properties.GitLabOnPremProperties
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.cloud.openfeign.EnableFeignClients
import org.springframework.scheduling.annotation.EnableScheduling

@EnableScheduling
@EnableFeignClients
@SpringBootApplication
@EnableConfigurationProperties(GitLabOnPremProperties::class)
class GitlabBrokerApplication

fun main() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package net.leanix.vsm.gitlab.broker.connector.adapter.feign

import net.leanix.vsm.gitlab.broker.connector.domain.AssignmentProvider
import net.leanix.vsm.gitlab.broker.connector.domain.GitLabAssignment
import org.springframework.stereotype.Component

@Component
class FeignAssignmentProvider(private val vsmClient: VsmClient) : AssignmentProvider {
override fun getAssignments(): Result<List<GitLabAssignment>> {
return kotlin.runCatching {
vsmClient.getAssignments()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.leanix.vsm.gitlab.broker.connector.adapter.feign

import net.leanix.vsm.gitlab.broker.connector.domain.GitLabAssignment
import net.leanix.vsm.gitlab.broker.shared.auth.adapter.feign.config.MtmFeignClientConfiguration
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestParam

@FeignClient(
name = "vsmClient",
url = "\${leanix.vsm.events-broker.base-url}",
configuration = [MtmFeignClientConfiguration::class]
)
interface VsmClient {

@GetMapping("/gitlab-on-prem/assignments")
fun getAssignments(): List<GitLabAssignment>

@PutMapping("/gitlab-on-prem/health/heartbeat")
fun heartbeat(@RequestParam("runId") runId: String): String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.leanix.vsm.gitlab.broker.connector.applicaiton

import net.leanix.vsm.gitlab.broker.connector.domain.AssignmentProvider
import net.leanix.vsm.gitlab.broker.connector.domain.GitLabAssignment
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service

@Service
class AssignmentService(
private val assignmentProvider: AssignmentProvider
) {

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

fun getAssignments(): List<GitLabAssignment> {
return assignmentProvider.getAssignments().onFailure {
logger.error("Failed to retrieve assignment list: ", it)
}.onSuccess {
logger.info("Assignment list retrieved with success with ${it.size} assignments")
}.getOrThrow()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.leanix.vsm.gitlab.broker.connector.domain

interface AssignmentProvider {
fun getAssignments(): Result<List<GitLabAssignment>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package net.leanix.vsm.gitlab.broker.connector.domain

import java.util.UUID

data class GitLabAssignment(
val runId: UUID,
val workspaceId: UUID,
val configurationId: UUID,
val connectorConfiguration: GitLabConfiguration
)

data class GitLabConfiguration(
val orgName: String,
)
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
package net.leanix.vsm.gitlab.broker.connector.runner

import net.leanix.vsm.gitlab.broker.connector.applicaiton.AssignmentService
import net.leanix.vsm.gitlab.broker.connector.domain.GitLabAssignment
import net.leanix.vsm.gitlab.broker.shared.cache.AssignmentsCache
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.stereotype.Component

@Component
class InitialStateRunner : ApplicationRunner {
class InitialStateRunner(
private val assignmentService: AssignmentService,
) : ApplicationRunner {

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

override fun run(args: ApplicationArguments?) {
logger.info("Started get initial state")
logger.info("Started to get initial state")
runCatching {
getAssignments()?.forEach { assignment ->
logger.info(
"Received assignment for ${assignment.connectorConfiguration.orgName} " +
"with configuration id: ${assignment.configurationId} and with run id: ${assignment.runId}"
)
}
}.onSuccess {
logger.info("Cached ${AssignmentsCache.getAll().size} assignments")
}.onFailure { e ->
logger.error("Failed to get initial state", e)
}
}

private fun getAssignments(): List<GitLabAssignment>? {
kotlin.runCatching {
val assignments = assignmentService.getAssignments()
AssignmentsCache.addAll(assignments)
return assignments
}.onFailure {
logger.error("Failed to get initial state. No assignment found for this workspace id")
}
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ class ShutdownService {

@PreDestroy
fun onDestroy() {
logger.info("Shutting down github broker")
logger.info("Shutting down gitlab on-prem")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package net.leanix.vsm.gitlab.broker.connector.scheduler

import net.leanix.vsm.gitlab.broker.connector.adapter.feign.VsmClient
import net.leanix.vsm.gitlab.broker.shared.cache.AssignmentsCache
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Component
class HeartbeatScheduler(
private val vsmClient: VsmClient
) {

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

@Scheduled(fixedRate = 300000) // 5 minute
fun heartbeat() {
AssignmentsCache.getAll().values.forEach { assigment ->
logger.info("Sending heartbeat for runId: ${assigment.runId}")
vsmClient.heartbeat(assigment.runId.toString())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package net.leanix.vsm.gitlab.broker.shared

object Constants {

const val GITLAB_ENTERPRISE_CONNECTOR = "gitlab-enterprise-connector"
const val API_USER = "apitoken"
const val GITLAB_ON_PREM_VERSION_HEADER = "X-LX-VsmGitLABBroker-Version"
const val EVENT_TYPE_HEADER = "X-LX-CanopyItem-EventType"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.leanix.vsm.gitlab.broker.shared.auth.adapter.feign

import net.leanix.vsm.gitlab.broker.shared.auth.adapter.feign.data.JwtTokenResponse
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.http.HttpHeaders.AUTHORIZATION
import org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader

@FeignClient(
name = "authentication",
url = "\${leanix.vsm.auth.access-token-uri}"
)
fun interface AuthClient {

@PostMapping(value = ["/oauth2/token"], consumes = [APPLICATION_FORM_URLENCODED_VALUE])
fun getToken(
@RequestHeader(name = AUTHORIZATION) authorization: String,
@RequestBody body: String
): JwtTokenResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.leanix.vsm.gitlab.broker.shared.auth.adapter.feign.config

import feign.RequestInterceptor
import net.leanix.vsm.gitlab.broker.shared.Constants.GITLAB_ON_PREM_VERSION_HEADER
import net.leanix.vsm.gitlab.broker.shared.auth.application.GetBearerToken
import net.leanix.vsm.gitlab.broker.shared.properties.GradleProperties.Companion.GITLAB_ENTERPRISE_VERSION
import org.springframework.context.annotation.Bean
import org.springframework.http.HttpHeaders.AUTHORIZATION

class MtmFeignClientConfiguration(private val getBearerToken: GetBearerToken) {

@Bean
fun requestInterceptor(): RequestInterceptor {
return RequestInterceptor {
it.header(GITLAB_ON_PREM_VERSION_HEADER, GITLAB_ENTERPRISE_VERSION)
it.header(AUTHORIZATION, "Bearer ${getBearerToken()}")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package net.leanix.vsm.gitlab.broker.shared.auth.adapter.feign.data

import com.fasterxml.jackson.annotation.JsonProperty

data class JwtTokenResponse(
val scope: String,
val expired: Boolean,
@JsonProperty("access_token")
val accessToken: String,
@JsonProperty("token_type")
val tokenType: String,
@JsonProperty("expired_in")
val expiredIn: Int
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package net.leanix.vsm.gitlab.broker.shared.auth.application

import net.leanix.vsm.gitlab.broker.shared.Constants.API_USER
import net.leanix.vsm.gitlab.broker.shared.auth.adapter.feign.AuthClient
import net.leanix.vsm.gitlab.broker.shared.properties.GitLabOnPremProperties
import org.springframework.stereotype.Service
import java.util.*

@Service
class GetBearerToken(
private val authClient: AuthClient,
private val vsmProperties: GitLabOnPremProperties
) {

operator fun invoke(): String {
return authClient.getToken(
authorization = getBasicAuthHeader(),
body = "grant_type=client_credentials"
).accessToken
}

private fun getBasicAuthHeader(): String =
"Basic " + Base64.getEncoder().encodeToString(
"$API_USER:${vsmProperties.apiUserToken}".toByteArray()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.leanix.vsm.gitlab.broker.shared.cache

import net.leanix.vsm.gitlab.broker.connector.domain.GitLabAssignment

object AssignmentsCache {

private val assigmentCache: MutableMap<String, GitLabAssignment> = mutableMapOf()

fun addAll(newAssignments: List<GitLabAssignment>) {
newAssignments.forEach { assignment -> assigmentCache[assignment.connectorConfiguration.orgName] = assignment }
}

fun get(key: String): GitLabAssignment? {
return assigmentCache[key]
}

fun getAll(): Map<String, GitLabAssignment> {
return assigmentCache
}

fun deleteAll() {
assigmentCache.clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.leanix.vsm.gitlab.broker.shared.properties

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "leanix.vsm.connector")
data class GitLabOnPremProperties(
val apiUserToken: String
)
11 changes: 11 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
leanix:
base-url: https://${LEANIX_DOMAIN}/services
vsm:
connector:
api-user-token: ${LEANIX_TECHNICAL_USER_TOKEN}
events-broker:
base-url: ${leanix.vsm.base-url}/vsm-events-broker
auth:
access-token-uri: ${leanix.base-url}/mtm/v1

server:
port: 8082
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.leanix.vsm.gitlab.broker.connector.runner

import com.github.tomakehurst.wiremock.client.WireMock
import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor
import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
import org.awaitility.kotlin.await
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock

@SpringBootTest(properties = ["application.runner.enabled=true"])
@AutoConfigureWireMock(port = 6666)
class InitialStateRunnerTest {

@Test
fun `it should get the assignments`() {
await.untilAsserted {
WireMock.verify(
1,
getRequestedFor(
urlEqualTo(
"/gitlab-on-prem/assignments"
)
)
)
}
}
}
Loading
Loading