Skip to content

Commit

Permalink
Merge branch 'release/0.6.2'
Browse files Browse the repository at this point in the history
  • Loading branch information
usommerl committed Apr 18, 2021
2 parents 73c7a38 + 3319c7c commit 8e8d0d8
Show file tree
Hide file tree
Showing 15 changed files with 193 additions and 137 deletions.
2 changes: 1 addition & 1 deletion .github/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name-template: '$RESOLVED_VERSION'
name-template: 'v$RESOLVED_VERSION'
categories:
- title: 'Enhancements'
label: 'enhancement'
Expand Down
23 changes: 13 additions & 10 deletions .github/workflows/ci_cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
uses: actions/setup-java@v2
with:
java-version: 1.11
distribution: 'adopt'
java-version: 11
- name: Run tests
run: sbt testCovered
cd:
Expand All @@ -22,14 +23,16 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
uses: actions/setup-java@v2
with:
java-version: 1.11
- name: Docker login
env:
TOKEN: ${{ secrets.PAT_GHCR }}
USERNAME: ${{ github.repository_owner }}
run: echo $TOKEN | docker login ghcr.io -u $USERNAME --password-stdin
distribution: 'adopt'
java-version: 11
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Create and publish a docker image with a UPX compressed executable
run: export UPX_COMPRESSION='--best'; sbt ';docker;dockerPush'
- name: Create and publish a docker image without compression
Expand All @@ -43,7 +46,7 @@ jobs:
docker tag $IMAGE_GHCR $IMAGE_GCR
echo "image_gcr=$IMAGE_GCR" >> $GITHUB_ENV
- name: Deploy to Cloud Run
uses: stefda/action-cloud-run@v1.2
uses: stefda/action-cloud-run@v1.4
with:
image: ${{ env.image_gcr }}
service: graalnative4s
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cloud_run_env_vars
Original file line number Diff line number Diff line change
@@ -1 +1 @@
APIDOCS_SERVER_URL=https://graalnative4s.usommerl.dev,APIDOCS_DESCRIPTION="[github.com/usommerl/graalnative4s](https://github.com/usommerl/graalnative4s)"
APIDOCS_SERVER_URL=https://graalnative4s.usommerl.dev,APIDOCS_DESCRIPTION="[github.com/usommerl/graalnative4s](https://github.com/usommerl/graalnative4s)",LOG_FORMATTER="default"
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM ghcr.io/graalvm/graalvm-ce:java11-21.0.0.2 as builder

ARG upx_compression
RUN gu install native-image
RUN curl https://bintray.com/sbt/rpm/rpm | tee /etc/yum.repos.d/bintray-sbt-rpm.repo && microdnf install sbt git xz
Expand Down Expand Up @@ -38,8 +39,12 @@ RUN if [ -n "${upx_compression}" ]; then \
upx-3.96-amd64_linux/upx "$upx_compression" /build/target/graalvm-native-image/graalnative4s; \
fi

RUN echo 'unprivileged:x:65534:65534:unprivileged:/:' > /etc/passwd.minimal

FROM scratch
COPY --from=builder /build/target/graalvm-native-image/graalnative4s /server
COPY --from=builder /etc/passwd.minimal /etc/passwd
USER unprivileged
ENV PATH "/"
EXPOSE 8080
ENTRYPOINT [ "/server" ]
16 changes: 8 additions & 8 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
ThisBuild / scalaVersion := "2.13.4"
ThisBuild / scalaVersion := "2.13.5"
ThisBuild / organization := "dev.usommerl"
ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.5.0"

val v = new {
val http4s = "0.21.19"
val http4s = "0.21.22"
val circe = "0.13.0"
val ciris = "1.2.1"
val tapir = "0.17.13"
val tapir = "0.17.19"
val odin = "0.11.0"
val munit = "0.7.22"
val munitCE = "0.13.1"
val munit = "0.7.23"
val munitCE = "1.0.1"
}

val upx = "UPX_COMPRESSION"
Expand All @@ -25,8 +25,10 @@ lazy val graalnative4s = project
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % v.tapir,
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % v.tapir,
"com.softwaremill.sttp.tapir" %% "tapir-openapi-circe-yaml" % v.tapir,
"com.softwaremill.sttp.tapir" %% "tapir-refined" % v.tapir,
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-http4s" % v.tapir,
"com.github.valskalla" %% "odin-core" % v.odin,
"com.github.valskalla" %% "odin-json" % v.odin,
"com.github.valskalla" %% "odin-slf4j" % v.odin,
"io.circe" %% "circe-core" % v.circe,
"io.circe" %% "circe-generic" % v.circe,
Expand All @@ -53,9 +55,7 @@ lazy val graalnative4s = project
assembly / assemblyMergeStrategy := {
case "META-INF/maven/org.webjars/swagger-ui/pom.properties" => MergeStrategy.singleOrError
case x if x.endsWith("module-info.class") => MergeStrategy.discard
case x =>
val oldStrategy = (assemblyMergeStrategy in assembly).value
oldStrategy(x)
case x => (assembly / assemblyMergeStrategy).value(x)
}
)

Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.4.7
sbt.version=1.5.0
8 changes: 4 additions & 4 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.26")
addSbtPlugin("com.alejandrohdezma" % "sbt-codecov" % "0.2.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.27")
addSbtPlugin("com.alejandrohdezma" % "sbt-codecov" % "0.2.1")
addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")
addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.0")
addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.16")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.1")
addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.17")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1")
addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.8.2")
18 changes: 11 additions & 7 deletions src/main/scala/server/Api.scala → src/main/scala/app/Api.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package server
package app

import cats.Applicative
import cats.data.Kleisli
import cats.effect.{Concurrent, ContextShift, Sync, Timer}
import cats.effect.{Concurrent, ContextShift, Timer}
import cats.implicits._
import dev.usommerl.BuildInfo
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.collection._
import io.circe.generic.auto._
import org.http4s.{HttpRoutes, Request, Response}
import org.http4s.dsl.Http4sDsl
Expand All @@ -15,6 +17,7 @@ import org.http4s.server.middleware.CORS
import sttp.model.StatusCode
import sttp.tapir._
import sttp.tapir.apispec.Tag
import sttp.tapir.codec.refined._
import sttp.tapir.docs.openapi._
import sttp.tapir.generic.auto._
import sttp.tapir.json.circe.jsonBody
Expand All @@ -25,7 +28,7 @@ import sttp.tapir.server.http4s._
import sttp.tapir.swagger.http4s.SwaggerHttp4s

object Api {
def apply[F[_]: Sync: Concurrent: ContextShift: Timer](config: ApiDocsConfig): Kleisli[F, Request[F], Response[F]] = {
def apply[F[_]: Concurrent: ContextShift: Timer](config: ApiDocsConfig): Kleisli[F, Request[F], Response[F]] = {

val dsl = Http4sDsl[F]
import dsl._
Expand All @@ -46,9 +49,10 @@ object Api {
}

object Examples {
def apply[F[_]: Sync: Concurrent: ContextShift: Timer]()(implicit F: Applicative[F]) = new TapirApi[F] {
def apply[F[_]: Concurrent: ContextShift: Timer]()(implicit F: Applicative[F]) = new TapirApi[F] {
override val tag = Tag("Getting started", None)
override lazy val serverEndpoints = List(info, hello)
type NonEmptyString = String Refined NonEmpty

private val info: ServerEndpoint[Unit, StatusCode, Info, Any, F] =
endpoint.get
Expand All @@ -71,12 +75,12 @@ object Examples {
)
)

private val hello: ServerEndpoint[Option[String], StatusCode, String, Any, F] =
private val hello: ServerEndpoint[Option[NonEmptyString], StatusCode, String, Any, F] =
endpoint.get
.summary("The infamous hello world endpoint")
.tag(tag.name)
.in("hello")
.in(query[Option[String]]("name").description("Optional name to greet"))
.in(query[Option[NonEmptyString]]("name").description("Optional name to greet"))
.out(stringBody)
.errorOut(statusCode)
.serverLogic(name => F.pure(s"Hello ${name.getOrElse("World")}!".asRight))
Expand All @@ -93,7 +97,7 @@ object Examples {
}
}

abstract class TapirApi[F[_]: Sync: Concurrent: ContextShift: Timer] {
abstract class TapirApi[F[_]: Concurrent: ContextShift: Timer] {
def tag: Tag
def serverEndpoints: List[ServerEndpoint[_, _, _, Any, F]]
def endpoints: List[Endpoint[_, _, _, _]] = serverEndpoints.map(_.endpoint)
Expand Down
62 changes: 62 additions & 0 deletions src/main/scala/app/Config.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package app

import cats.implicits._
import ciris._
import ciris.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.string.Url
import eu.timepit.refined.types.net.PortNumber
import io.odin.Level
import io.odin.formatter.Formatter
import io.odin.json.{Formatter => JFormatter}

import app.ServerUrl

case class Config(server: ServerConfig, logger: LoggerConfig)
case class ServerConfig(port: PortNumber, apiDocs: ApiDocsConfig)
case class ApiDocsConfig(serverUrl: ServerUrl, description: Option[String])
case class LoggerConfig(level: Level, formatter: Formatter)

package object app {

type ServerUrl = String Refined Url

implicit val logLevelDecoder: ConfigDecoder[String, Level] =
ConfigDecoder[String, String].mapOption("Level")(_.toLowerCase match {
case "trace" => Level.Trace.some
case "debug" => Level.Debug.some
case "info" => Level.Info.some
case "warn" => Level.Warn.some
case "error" => Level.Error.some
case _ => None
})

implicit val logFormatterDecoder: ConfigDecoder[String, Formatter] =
ConfigDecoder[String, String].mapOption("Formatter")(_.toLowerCase match {
case "default" => Formatter.default.some
case "colorful" => Formatter.colorful.some
case "json" => JFormatter.json.some
case _ => None
})

private val loggerConfig: ConfigValue[LoggerConfig] = (
env("LOG_LEVEL").as[Level].default(Level.Info),
env("LOG_FORMATTER").as[Formatter].default(Formatter.colorful)
).parMapN(LoggerConfig)

private val apiDocsConfig: ConfigValue[ApiDocsConfig] = (
env("APIDOCS_SERVER_URL").as[ServerUrl].default("http://localhost:8080"),
env("APIDOCS_DESCRIPTION").option
).parMapN(ApiDocsConfig)

private val serverConfig: ConfigValue[ServerConfig] = (
env("PORT").as[PortNumber].default(8080),
apiDocsConfig
).parMapN(ServerConfig)

val config: ConfigValue[Config] = (
serverConfig,
loggerConfig
).parMapN(Config)
}
46 changes: 46 additions & 0 deletions src/main/scala/app/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package app

import scala.concurrent.ExecutionContext.global

import cats.effect.{Resource, _}
import cats.implicits._
import dev.usommerl.BuildInfo
import eu.timepit.refined.auto._
import io.odin._
import org.http4s.server.{middleware, Server}
import org.http4s.server.blaze.BlazeServerBuilder

object Main extends IOApp {

def run(args: List[String]): IO[ExitCode] =
runF[IO].use(_ => IO.never)

private def runF[F[_]: ContextShift: ConcurrentEffect: Timer]: Resource[F, Unit] =
for {
config <- app.config.resource[F]
logger <- createLogger[F](config.logger)
_ <- Resource.eval(logger.info(startMessage))
_ <- serve[F](config.server)
} yield ()

private def createLogger[F[_]: ConcurrentEffect: Timer](config: LoggerConfig): Resource[F, Logger[F]] =
Resource
.pure[F, Logger[F]](consoleLogger[F](config.formatter, config.level))
.evalTap(logger => Sync[F].delay(OdinInterop.globalLogger.set(logger.mapK(Effect.toIOK).some)))

private def serve[F[_]: ContextShift: ConcurrentEffect: Timer](config: ServerConfig): Resource[F, Server[F]] =
BlazeServerBuilder[F](global)
.bindHttp(config.port, "0.0.0.0")
.withHttpApp(middleware.Logger.httpApp(logHeaders = true, logBody = false)(Api[F](config.apiDocs)))
.resource

private lazy val startMessage: String =
"STARTED [ name: %s, version: %s, vmVersion: %s, scalaVersion: %s, sbtVersion: %s, builtAt: %s ]".format(
BuildInfo.name,
BuildInfo.version,
System.getProperty("java.vm.version"),
BuildInfo.scalaVersion,
BuildInfo.sbtVersion,
BuildInfo.builtAtString
)
}
26 changes: 26 additions & 0 deletions src/main/scala/app/OdinInterop.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package app

import java.util.concurrent.atomic.AtomicReference

import cats.effect.Clock
import cats.effect.Effect
import cats.effect.IO
import io.odin.Logger
import io.odin.slf4j.OdinLoggerBinder

/** This implementation was stolen from here:
* https://github.com/pitgull/pitgull/blob/v0.0.7/src/main/scala/io/pg/OdinInterop.scala
*/
class OdinInterop extends OdinLoggerBinder[IO] {
implicit val F: Effect[IO] = IO.ioEffect
implicit val clock: Clock[IO] = Clock.create[IO]

val loggers: PartialFunction[String, Logger[IO]] = {
val theLogger: String => Option[Logger[IO]] = _ => OdinInterop.globalLogger.get()
theLogger.unlift
}
}

object OdinInterop {
val globalLogger = new AtomicReference[Option[Logger[IO]]](None)
}
27 changes: 1 addition & 26 deletions src/main/scala/org/slf4j/impl/StaticLoggerBinder.scala
Original file line number Diff line number Diff line change
@@ -1,33 +1,8 @@
package org.slf4j.impl

import scala.concurrent.ExecutionContext

import cats.effect.{Clock, Effect, IO, Timer}
import io.odin.{consoleLogger, Level, Logger}
import io.odin.formatter.Formatter
import io.odin.slf4j.OdinLoggerBinder

/** This is bridge is needed for project dependencies that require an SLF4J API
* See: https://github.com/valskalla/odin/tree/v0.9.1#slf4j-bridge
*
* This particular implementation was stolen from here:
* https://github.com/pitgull/pitgull/blob/v0.0.2/src/main/scala/org/slf4j/impl/StaticLoggerBinder.scala
*/
class StaticLoggerBinder extends OdinLoggerBinder[IO] {
implicit val F: Effect[IO] = IO.ioEffect
implicit val clock: Clock[IO] = Clock.create[IO]

import StaticLoggerBinder.baseLogger

val loggers: PartialFunction[String, Logger[IO]] = { case _ =>
baseLogger.withMinimalLevel(Level.Info)
}
}
class StaticLoggerBinder extends app.OdinInterop

object StaticLoggerBinder extends StaticLoggerBinder {
//EC isn't used - only Clock is required
implicit val timer: Timer[IO] = IO.timer(ExecutionContext.parasitic)
val baseLogger = consoleLogger[IO](formatter = Formatter.colorful)
val REQUESTED_API_VERSION: String = "1.7"
def getSingleton: StaticLoggerBinder = this
}
Loading

0 comments on commit 8e8d0d8

Please sign in to comment.