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

feat: renku-core client #1652

Merged
merged 55 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
424cec7
feat: Project Update API on TG
jachro Aug 4, 2023
9c126e8
feat: Project Update API on KG to update TS with a call to TG
jachro Aug 4, 2023
34f1edb
refactor: changes to the shape of the payload on the TS Project Updat…
jachro Aug 7, 2023
053a410
refactor: TG Client renamed to TriplesGeneratorClient for consistency
jachro Aug 7, 2023
5139056
feat: renku-core-client with the version APIs
jachro Aug 9, 2023
7fd70e0
Merge branch 'development'
jachro Aug 9, 2023
f0e83e4
refactor: VersionClient to return Result from findCoreUri
jachro Aug 10, 2023
cda8ba7
refactor: Result encoder moved to ModelEncoders
jachro Aug 10, 2023
d08a83c
feat: Renku Core Client
jachro Aug 11, 2023
22b7255
chore: Update logback-classic from 1.4.9 to 1.4.11 (#1653)
RenkuBot Aug 10, 2023
93932dd
fix: Cross-Entity Search to fetch creator name from the Persons graph…
jachro Aug 11, 2023
4e9d418
feat: RenkuCoreClient.getMigrationCheck; HttpUrl moved to projects file
jachro Aug 11, 2023
4172697
fix: imports and reformat
jachro Aug 11, 2023
40a012d
Merge branch 'development'
jachro Aug 14, 2023
e59f10e
reformat: LowLevelApis and RenkuCoreClient
jachro Aug 14, 2023
b1b9a9c
feat: RenkuCoreClient.findCoreUri(projects.GitHttpUrl)
jachro Aug 14, 2023
4c56c5b
Merge branch 'development'
jachro Aug 15, 2023
4c5a122
feat: LowLevelApis.postProjectUpdate
jachro Aug 15, 2023
314e7ee
feat: BranchProtectionCheck
jachro Aug 16, 2023
eb5ae6f
refactor: TG's Project Update API changed from PUT to PATCH
jachro Aug 16, 2023
f4c742d
refactor: KG's Project Update API to work for PUT and PATCH
jachro Aug 16, 2023
787bbc1
refactor: KG's NewValues dto renamed to ProjectUpdates
jachro Aug 16, 2023
35e4c0d
refactor: ProjectUpdater with business logic extracted from the endpoint
jachro Aug 16, 2023
24de95c
feat: KG's Project Update API to update project image
jachro Aug 17, 2023
0590f48
feat: KG's Project Update API to update project desc and keywords
jachro Aug 17, 2023
70b643d
feat: KG's Project Update API to return 409 if user cannot push
jachro Aug 17, 2023
52bb287
feat: RenkuCoreClient to expose updateProject API
jachro Aug 17, 2023
6942305
feat: ProjectGitUrlFinder
jachro Aug 17, 2023
defed1a
feat: UserInfoFinder
jachro Aug 17, 2023
6bca77e
feat: ProjectUpdater to check pushing and find git url
jachro Aug 17, 2023
f25bc69
feat: ProjectUpdater to collect relevant data and update in Core
jachro Aug 17, 2023
12aa32e
feat: ProjectUpdater and Endpoint implementation
jachro Aug 18, 2023
7fc1835
feat: ProjectUpdater to run update on core in a fiber
jachro Aug 18, 2023
057403a
chore: fixing core urls in the config for testing
jachro Aug 18, 2023
d4b0f42
chore: fixing core urls in the config for testing
jachro Aug 18, 2023
737cf70
refactor: RenkuCoreUri.Current -> RenkuCoreUri.Latest
jachro Aug 21, 2023
5a6e96c
refactor: helm config & conf reading redone
jachro Aug 21, 2023
2a22279
refactor: core protocol to be defined in helm template
jachro Aug 21, 2023
bdf3c0f
chore: fix in the knowledge-graph-deployment.yaml
jachro Aug 21, 2023
5caf25e
fix: unit tests failures after httpUrls gen changes
jachro Aug 21, 2023
3b299bd
fix: versioned core api uri
jachro Aug 21, 2023
c0f141f
feat: TS to be updated with avatar_url from GL
jachro Aug 22, 2023
04ecef2
Merge branch 'development'
jachro Aug 22, 2023
7d1e74c
feat: GL project update with multipart request
jachro Aug 23, 2023
56ba328
feat: GL project update to work with multipart request
jachro Aug 25, 2023
a247c83
Merge branch 'development'
jachro Aug 25, 2023
4ace52f
doc: README & open api docs updated
jachro Aug 25, 2023
0c4bb22
feat: log user failures at INFO level
jachro Aug 25, 2023
6b8727d
feat: core client reporting improved
jachro Aug 25, 2023
eeaf23d
refactor: using http4s renderer for media type string representation
jachro Aug 29, 2023
1efb1ac
chore: API docs updated
jachro Aug 29, 2023
383c2bb
chore: default for renku-core-service-urls in core-client's applicati…
jachro Aug 29, 2023
e16f880
feat: improved tailRecM for NestedF and Result
jachro Aug 30, 2023
69e61fa
feat: Project Update API to fail if project in non-recoverable failure
jachro Aug 30, 2023
b60af0d
Merge branch 'development'
jachro Aug 30, 2023
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
9 changes: 9 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ lazy val root = project
entitiesViewingsCollector,
projectAuth,
triplesGenerator,
renkuCoreClient,
knowledgeGraph
)

Expand Down Expand Up @@ -215,6 +216,13 @@ lazy val tokenRepository = project
AutomateHeaderPlugin
)

lazy val renkuCoreClient = project
.in(file("renku-core-client"))
.withId("renku-core-client")
.settings(commonSettings)
.dependsOn(graphCommons % "compile->compile; test->test")
.enablePlugins(AutomateHeaderPlugin)

lazy val knowledgeGraph = project
.in(file("knowledge-graph"))
.withId("knowledge-graph")
Expand All @@ -231,6 +239,7 @@ lazy val knowledgeGraph = project
graphCommons % "compile->compile; test->test",
entitiesSearch % "compile->compile; test->test",
triplesGeneratorApi % "compile->compile; test->test",
renkuCoreClient % "compile->compile; test->test",
entitiesViewingsCollector
)
.enablePlugins(
Expand Down
17 changes: 10 additions & 7 deletions generators/src/test/scala/io/renku/generators/Generators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,18 @@ object Generators {

val httpPorts: Gen[Port] = choose(2000, 10000).map(Port.fromInt).map(_.getOrElse(sys.error("Invalid generated port")))

def httpUrls(hostGenerator: Gen[String] = nonEmptyStrings(),
pathGenerator: Gen[String] = relativePaths(minSegments = 0, maxSegments = 2)
def httpUrls(protocolGenerator: Gen[String] = Gen.oneOf("http", "https"),
hostGenerator: Gen[String] = nonEmptyStrings(),
portGenerator: Gen[Option[Port]] = Gen.some(httpPorts),
pathGenerator: Gen[String] = relativePaths(minSegments = 0, maxSegments = 2)
): Gen[String] = for {
protocol <- Gen.oneOf("http", "https")
port <- httpPorts
host <- hostGenerator
path <- pathGenerator
protocol <- protocolGenerator
host <- hostGenerator
maybePort <- portGenerator
path <- pathGenerator
portValidated = maybePort.map(p => s":$p").getOrElse("")
pathValidated = if (path.isEmpty) "" else s"/$path"
} yield s"$protocol://$host:$port$pathValidated"
} yield s"$protocol://$host$portValidated$pathValidated"

val localHttpUrls: Gen[String] = for {
protocol <- Gen.oneOf("http", "https")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import io.renku.http.rest.paging.model.{Page, Total}
import io.renku.metrics.{GitLabApiCallRecorder, MetricsRegistry}
import org.http4s.Method.{DELETE, GET, HEAD, POST, PUT}
import org.http4s.circe.{jsonEncoder, jsonEncoderOf}
import org.http4s.{EntityEncoder, Method, Response, Uri, UrlForm}
import org.http4s.multipart.Multipart
import org.http4s.{EntityEncoder, Method, Response, Uri}
import org.typelevel.ci._
import org.typelevel.log4cats.Logger

Expand All @@ -55,7 +56,7 @@ trait GitLabClient[F[_]] {
mapResponse: ResponseMappingF[F, ResultType]
)(implicit maybeAccessToken: Option[AccessToken]): F[ResultType]

def put[ResultType](path: Uri, endpointName: String Refined NonEmpty, payload: UrlForm)(
def put[ResultType](path: Uri, endpointName: String Refined NonEmpty, payload: Multipart[F])(
mapResponse: ResponseMappingF[F, ResultType]
)(implicit maybeAccessToken: Option[AccessToken]): F[ResultType]

Expand Down Expand Up @@ -104,13 +105,17 @@ final class GitLabClientImpl[F[_]: Async: Logger](
result <- super.send(request)(mapResponse)
} yield result

override def put[ResultType](path: Uri, endpointName: String Refined NonEmpty, payload: UrlForm)(
override def put[ResultType](path: Uri, endpointName: String Refined NonEmpty, payload: Multipart[F])(
mapResponse: ResponseMappingF[F, ResultType]
)(implicit maybeAccessToken: Option[AccessToken]): F[ResultType] = for {
uri <- validateUri(show"$gitLabApiUrl/$path")
request <- secureNamedRequest(PUT, uri, endpointName, payload)
result <- super.send(request)(mapResponse)
} yield result
)(implicit maybeAccessToken: Option[AccessToken]): F[ResultType] =
validateUri(show"$gitLabApiUrl/$path")
.flatMap(
secureNamedRequest(PUT, _, endpointName)
.map(req => req.copy(request = req.request.withEntity(payload).putHeaders(payload.headers)))
)
.flatMap(
send(_: NamedRequest[F])(mapResponse)
)

override def delete[ResultType](path: Uri, endpointName: Refined[String, NonEmpty])(
mapResponse: ResponseMappingF[F, ResultType]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ abstract class RestClient[F[_]: Async: Logger, ThrottlingTarget](
new MultipartBuilder(request, parts).build()

class MultipartBuilder private[RequestOps] (request: Request[F], parts: Vector[Part[F]] = Vector.empty[Part[F]]) {

def addPart[PartType](name: String, value: PartType)(implicit
encoder: PartEncoder[PartType]
): MultipartBuilder =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import io.renku.generators.CommonGraphGenerators.microserviceBaseUrls
import io.renku.generators.Generators.Implicits._
import io.renku.generators.Generators.{httpUrls, nonBlankStrings, relativePaths}
import io.renku.microservices.MicroserviceBaseUrl
import org.scalacheck.Gen
import org.scalatest.matchers.should
import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
Expand All @@ -44,7 +43,7 @@ class SubscriberUrlSpec extends AnyWordSpec with should.Matchers with ScalaCheck
"to MicroserviceBaseUrl conversion" should {

"successfully convert MicroserviceBaseUrl if well defined" in {
forAll(httpUrls(pathGenerator = Gen.const("")), relativePaths(minSegments = 0, maxSegments = 2)) { (url, path) =>
forAll(httpUrls(pathGenerator = ""), relativePaths(minSegments = 0, maxSegments = 2)) { (url, path) =>
val pathValidated = if (path.isEmpty) "" else s"/$path"
SubscriberUrl(s"$url$pathValidated").toUnsafe[MicroserviceBaseUrl] shouldBe MicroserviceBaseUrl(url)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ import io.renku.logging.TestExecutionTimeRecorder
import io.renku.metrics.GitLabApiCallRecorder
import io.renku.stubbing.ExternalServiceStubbing
import io.renku.testtools.IOSpec
import org.http4s.Method.{GET, _}
import org.http4s.MediaType.multipart
import org.http4s.Method._
import org.http4s.Status.{Accepted, NotFound, Ok, Unauthorized}
import org.http4s.circe.CirceEntityCodec.circeEntityDecoder
import org.http4s.{Header, Method, Request, Response, Status, Uri, UrlForm}
import org.http4s.multipart.{Multiparts, Part}
import org.http4s.util.Renderer
import org.http4s.{Header, Method, Request, Response, Status, Uri}
import org.scalacheck.Gen
import org.scalamock.scalatest.MockFactory
import org.scalatest.TryValues
Expand Down Expand Up @@ -226,46 +229,40 @@ class GitLabClientSpec

"put" should {

val mapPutResponse: PartialFunction[(Status, Request[IO], Response[IO]), IO[Unit]] = {
case (Accepted, _, _) => ().pure[IO]
case (Unauthorized, _, _) => UnauthorizedException.raiseError[IO, Unit]
}

forAll(tokenScenarios) { (tokenType, accessToken: AccessToken) =>
s"send form data with the $tokenType to the endpoint" in new TestCase {
s"send the given multipart request with the $tokenType to the endpoint" in new TestCase {

implicit val mat: Option[AccessToken] = accessToken.some
val partName = nonEmptyStrings().generateOne
val partValue = nonEmptyStrings().generateOne

val propName = nonEmptyStrings().generateOne
val propValue = nonEmptyStrings().generateOne
val multipartPayload = Multiparts
.forSync[IO]
.flatMap(_.multipart(Vector(Part.formData[IO](partName, partValue))))
.unsafeRunSync()

stubFor {
put(s"/api/v4/$path")
.withAccessToken(accessToken.some)
.withRequestBody(equalTo(s"$propName=$propValue"))
.withMultipartRequestBody(
aMultipart(partName).withBody(equalTo(partValue))
)
.withHeader(
"Content-Type",
containing(Renderer.renderString(multipart.`form-data`))
)
.withHeader(
"Content-Type",
containing(s"""boundary="${multipartPayload.boundary.value}"""")
)
.willReturn(aResponse().withStatus(Accepted.code))
}

client
.put(path, endpointName, UrlForm(propName -> propValue))(mapPutResponse)(accessToken.some)
.put(path, endpointName, multipartPayload) { case (Accepted, _, _) => ().pure[IO] }
.unsafeRunSync() shouldBe ()
}
}

"return an UnauthorizedException if remote client responds with UNAUTHORIZED" in new TestCase {

val propName = nonEmptyStrings().generateOne
val propValue = nonEmptyStrings().generateOne

stubFor {
put(s"/api/v4/$path")
.withAccessToken(maybeAccessToken)
.withRequestBody(equalTo(s"$propName=$propValue"))
.willReturn(unauthorized())
}

intercept[Exception] {
client.put(path, endpointName, UrlForm(propName -> propValue))(mapPutResponse).unsafeRunSync()
} shouldBe UnauthorizedException
}
}

"delete" should {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,25 @@
package io.renku.interpreters

import cats.Show
import cats.effect.kernel.Sync
import cats.effect.Async
import cats.syntax.all._
import fs2.Stream
import io.renku.interpreters.TestLogger.LogMessage._
import org.scalatest.matchers.should
import org.scalatest.{Assertion, Succeeded}
import org.scalatest.{Assertion, Failed, Outcome, Succeeded}
import org.typelevel.log4cats.Logger

import java.util.concurrent.ConcurrentLinkedQueue
import scala.concurrent.duration._
import scala.jdk.CollectionConverters._

class TestLogger[F[_]: Sync] extends Logger[F] with should.Matchers {
class TestLogger[F[_]: Async] extends Logger[F] with should.Matchers {

import TestLogger.Level._
import TestLogger._
import LogMessage._

private[this] val F = Sync[F]
private[this] val F = Async[F]
private[this] val invocations = new ConcurrentLinkedQueue[LogEntry]()

def getMessages(severity: Level): List[LogMessage] =
Expand Down Expand Up @@ -70,6 +72,27 @@ class TestLogger[F[_]: Sync] extends Logger[F] with should.Matchers {

def reset(): Unit = invocations.clear()

def waitFor(expected: LogEntry*): F[Assertion] = {

val interval = 100 millis
val attempts = 20

val logCheck =
F.delay(loggedOnly(expected.toList))
.as(Succeeded)
.widen[Outcome]
.handleError(Failed(_))

val stream = Stream.eval(logCheck) ++
Stream
.awakeDelay[F](interval)
.void
.evalMap(_ => logCheck)
.take(attempts)
.takeThrough(_ != Succeeded)
stream.covary[F].compile.lastOrError.map(_ shouldBe Succeeded)
}

private def add(entry: LogEntry): F[Unit] = F.delay {
invocations.add(entry)
()
Expand Down Expand Up @@ -119,7 +142,7 @@ class TestLogger[F[_]: Sync] extends Logger[F] with should.Matchers {

object TestLogger {

def apply[F[_]: Sync](): TestLogger[F] = new TestLogger[F]
def apply[F[_]: Async](): TestLogger[F] = new TestLogger[F]

private[TestLogger] case class LogEntry(level: Level, message: LogMessage) {
override lazy val toString = show"\n\t$level: $message"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

package io.renku.stubbing

import cats.effect.{IO, Resource}
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock.equalTo
import com.github.tomakehurst.wiremock.client.{MappingBuilder, WireMock}
Expand All @@ -26,6 +27,7 @@ import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Positive
import io.renku.http.client.AccessToken
import io.renku.http.client.AccessToken._
import org.http4s.Uri
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite}

trait ExternalServiceStubbing extends BeforeAndAfterEach with BeforeAndAfterAll {
Expand All @@ -46,6 +48,7 @@ trait ExternalServiceStubbing extends BeforeAndAfterEach with BeforeAndAfterAll
}

lazy val externalServiceBaseUrl: String = s"http://localhost:${server.port()}"
lazy val externalServiceBaseUri: Uri = Uri.unsafeFromString(externalServiceBaseUrl)

override def beforeEach(): Unit =
server.resetAll()
Expand All @@ -64,4 +67,30 @@ trait ExternalServiceStubbing extends BeforeAndAfterEach with BeforeAndAfterAll
case None => mappingBuilder
}
}

protected def otherWireMockResource =
Resource.make[IO, WireMockServer] {
IO {
val config = WireMockConfiguration.wireMockConfig().dynamicPort()
val server = new WireMockServer(config)

server.start()

server
}
}(server =>
IO {
server.shutdownServer()
}
)

protected implicit class WireMockOps(server: WireMockServer) {

lazy val baseUri: Uri = Uri.unsafeFromString(server.baseUrl())

def stubFor(mappingBuilder: MappingBuilder): Unit =
new WireMock(server.port()).register {
WireMock.stubFor(mappingBuilder)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import io.renku.generators.Generators.Implicits._
import io.renku.http.client.RestClient.ResponseMappingF
import io.renku.http.client.{AccessToken, GitLabClient}
import org.http4s.Method.{DELETE, GET, HEAD, POST, PUT}
import org.http4s.{Method, Uri, UrlForm}
import org.http4s.multipart.Multipart
import org.http4s.{Method, Uri}
import org.scalacheck.Gen
import org.scalamock.clazz.Mock
import org.scalamock.function.MockFunctions
Expand Down Expand Up @@ -73,7 +74,7 @@ trait GitLabClientTools[F[_]] {
.repeat(expectedNumberOfCalls)
case PUT =>
(gitLabClient
.put(_: Uri, _: String Refined NonEmpty, _: UrlForm)(_: ResponseMappingF[F, ResultType])(
.put(_: Uri, _: String Refined NonEmpty, _: Multipart[F])(_: ResponseMappingF[F, ResultType])(
_: Option[AccessToken]
))
.expects(*, maybeEndpointName.map(new MockParameter(_)).getOrElse(*), *, capture(responseMapping), *)
Expand Down
19 changes: 19 additions & 0 deletions helm-chart/renku-graph/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,25 @@ If release name contains chart name it will be used as a full name.
{{- printf "%s-commit-event-service" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}

{{- define "renkuCoreLatest.fullname" -}}
{{- $coreBaseName := printf "%s-core" .Release.Name -}}
{{- printf "%s-%s" $coreBaseName (get $.Values.global.core.versions "latest").name -}}
{{- end -}}

{{/*
Comma separated list of renku-core service names
*/}}
{{- define "renkuCore.serviceUrls" -}}
{{- $serviceUrls := list -}}
{{- $coreBaseName := printf "%s-core" .Release.Name -}}
{{- range $i, $k := (keys .Values.global.core.versions | sortAlpha) -}}
{{- $serviceUrl := printf "http://%s-%s" $coreBaseName (get $.Values.global.core.versions $k).name -}}
{{- $serviceUrls = mustAppend $serviceUrls $serviceUrl -}}
{{- end -}}
{{- join "," $serviceUrls | quote -}}
{{- end -}}

{{/*
Create chart name and version as used by the chart label.
*/}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ spec:
value: "http://{{ template "tokenRepository.fullname" . }}:{{ .Values.tokenRepository.service.port }}"
- name: EVENT_LOG_BASE_URL
value: "http://{{ template "eventLog.fullname" . }}:{{ .Values.eventLog.service.port }}"
- name: RENKU_CORE_LATEST_URL
value: "http://{{ template "renkuCoreLatest.fullname" . }}"
- name: RENKU_CORE_SERVICE_URLS
value: {{ template "renkuCore.serviceUrls" . }}
- name: GITLAB_BASE_URL
value: {{ .Values.gitlab.url }}
- name: GITLAB_RATE_LIMIT
Expand Down
Loading