diff --git a/README.md b/README.md index 0d06faf..d3401f0 100644 --- a/README.md +++ b/README.md @@ -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=.leanix.net \ + -e LEANIX_TECHNICAL_USER_TOKEN=\ + leanixacrpublic.azurecr.io/vsm-gitlab-broker +``` \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 6cd0ea3..bd15bb2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/GitlabBrokerApplication.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/GitlabBrokerApplication.kt index ba99f85..90e517e 100644 --- a/src/main/kotlin/net/leanix/vsm/gitlab/broker/GitlabBrokerApplication.kt +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/GitlabBrokerApplication.kt @@ -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() { diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/FeignAssignmentProvider.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/FeignAssignmentProvider.kt new file mode 100644 index 0000000..3cf12fb --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/FeignAssignmentProvider.kt @@ -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> { + return kotlin.runCatching { + vsmClient.getAssignments() + } + } +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/VsmClient.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/VsmClient.kt new file mode 100644 index 0000000..96d6136 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/adapter/feign/VsmClient.kt @@ -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 + + @PutMapping("/gitlab-on-prem/health/heartbeat") + fun heartbeat(@RequestParam("runId") runId: String): String +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/applicaiton/AssignmentService.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/applicaiton/AssignmentService.kt new file mode 100644 index 0000000..2c32872 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/applicaiton/AssignmentService.kt @@ -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 { + 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() + } +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/domain/AssignmentProvider.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/domain/AssignmentProvider.kt new file mode 100644 index 0000000..aa8cd13 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/domain/AssignmentProvider.kt @@ -0,0 +1,5 @@ +package net.leanix.vsm.gitlab.broker.connector.domain + +interface AssignmentProvider { + fun getAssignments(): Result> +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/domain/GitLabAssignment.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/domain/GitLabAssignment.kt new file mode 100644 index 0000000..7bd884a --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/domain/GitLabAssignment.kt @@ -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, +) diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunner.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunner.kt index ca5912f..2dd8bf8 100644 --- a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunner.kt +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunner.kt @@ -1,5 +1,8 @@ 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 @@ -7,11 +10,36 @@ 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? { + 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 } } diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/ShutdownService.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/ShutdownService.kt index 1dfd2e1..828fa8f 100644 --- a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/ShutdownService.kt +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/ShutdownService.kt @@ -11,6 +11,6 @@ class ShutdownService { @PreDestroy fun onDestroy() { - logger.info("Shutting down github broker") + logger.info("Shutting down gitlab on-prem") } } diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/scheduler/HeartbeatScheduler.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/scheduler/HeartbeatScheduler.kt new file mode 100644 index 0000000..fa69632 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/connector/scheduler/HeartbeatScheduler.kt @@ -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()) + } + } +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/Constants.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/Constants.kt new file mode 100644 index 0000000..04caeee --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/Constants.kt @@ -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" +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/AuthClient.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/AuthClient.kt new file mode 100644 index 0000000..4c523c2 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/AuthClient.kt @@ -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 +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/config/MtmFeignClientConfiguration.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/config/MtmFeignClientConfiguration.kt new file mode 100644 index 0000000..d48785a --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/config/MtmFeignClientConfiguration.kt @@ -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()}") + } + } +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/data/JwtTokenResponse.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/data/JwtTokenResponse.kt new file mode 100644 index 0000000..05c3efb --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/adapter/feign/data/JwtTokenResponse.kt @@ -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 +) diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/application/GetBearerToken.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/application/GetBearerToken.kt new file mode 100644 index 0000000..3a711f5 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/auth/application/GetBearerToken.kt @@ -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() + ) +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/cache/AssignmentsCache.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/cache/AssignmentsCache.kt new file mode 100644 index 0000000..ac343a0 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/cache/AssignmentsCache.kt @@ -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 = mutableMapOf() + + fun addAll(newAssignments: List) { + newAssignments.forEach { assignment -> assigmentCache[assignment.connectorConfiguration.orgName] = assignment } + } + + fun get(key: String): GitLabAssignment? { + return assigmentCache[key] + } + + fun getAll(): Map { + return assigmentCache + } + + fun deleteAll() { + assigmentCache.clear() + } +} diff --git a/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/properties/GitLabOnPremProperties.kt b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/properties/GitLabOnPremProperties.kt new file mode 100644 index 0000000..3ec4b86 --- /dev/null +++ b/src/main/kotlin/net/leanix/vsm/gitlab/broker/shared/properties/GitLabOnPremProperties.kt @@ -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 +) diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8b13789..07a1dfc 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -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 \ No newline at end of file diff --git a/src/test/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunnerTest.kt b/src/test/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunnerTest.kt new file mode 100644 index 0000000..039d952 --- /dev/null +++ b/src/test/kotlin/net/leanix/vsm/gitlab/broker/connector/runner/InitialStateRunnerTest.kt @@ -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" + ) + ) + ) + } + } +} diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml new file mode 100644 index 0000000..dd5bf67 --- /dev/null +++ b/src/test/resources/application.yaml @@ -0,0 +1,9 @@ +leanix: + base-url: http://localhost:${wiremock.server.port:6666}/services + vsm: + connector: + api-user-token: ${LEANIX_TECHNICAL_USER_TOKEN} + events-broker: + base-url: http://localhost:${wiremock.server.port:6666} + auth: + access-token-uri: ${leanix.base-url}/mtm/v1 \ No newline at end of file diff --git a/src/test/resources/mappings/authenticate.json b/src/test/resources/mappings/authenticate.json new file mode 100644 index 0000000..986b3d7 --- /dev/null +++ b/src/test/resources/mappings/authenticate.json @@ -0,0 +1,13 @@ +{ + "request": { + "url": "/services/mtm/v1/oauth2/token", + "method": "POST" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{\"scope\": \"\",\"expired\": false,\"access_token\": \"dummyToken\",\"token_type\": \"bearer\",\"expires_in\": 3599}" + } +} diff --git a/src/test/resources/mappings/get-assignments.json b/src/test/resources/mappings/get-assignments.json new file mode 100644 index 0000000..f17d7c7 --- /dev/null +++ b/src/test/resources/mappings/get-assignments.json @@ -0,0 +1,14 @@ +{ + "request": { + "url": "/gitlab-on-prem/assignments", + "method": "GET" + }, + "response": { + "status": 200, + "headers": { + "X-LX-VsmGitLavBroker-Version": "v0.0.2", + "Content-Type": "application/json" + }, + "body": "{ \"runId\": \"f1abfae5-f144-47e1-9cda-3eaa94e5286d\", \"configurationId\": \"a7a74e83-dde9-48a0-8b0d-c74f954671fb\", \"workspaceId\": \"38718fc9-d106-47a5-a25c-e4e595c8c2d4\", \"organizationNameList\": [ \"super-repo\", \"super-duper-repo\" ] }" + } +}