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

Generate OpenAPI spec during compile #1279

Open
wants to merge 55 commits into
base: master
Choose a base branch
from
Open
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
6f96681
Separate endpoints description from implemenation
adamw Aug 6, 2024
cec53c5
Task to generate openapi spec
adamw Aug 7, 2024
738fb84
generate spec
Aug 15, 2024
6c6ba37
moved scripts and deps to ui, added typescript
Aug 23, 2024
006e3d2
uses 'concurrently' instead of '&&' in a 'start' script
Aug 25, 2024
4593ad0
uses chokidar-cli
Aug 25, 2024
6b1d759
sets up scripts for types generation and downgrades axios due to a co…
Aug 25, 2024
8df0966
base working version of user registration using openapi-client-axios
Aug 25, 2024
beed970
build script: generate openapi-types before building an app
Aug 25, 2024
62530a8
removes the "packageManager" field from package.json
Aug 25, 2024
025a666
fixes tests for Register component
Aug 25, 2024
f488c11
removes unused .gitignore path
Aug 25, 2024
aea0d00
removes 'registerUser' method from UserService
Aug 25, 2024
5ca5c77
migrates "login" method & related use
Aug 25, 2024
92fd984
adds "Generate OpenAPI Spec" step to CI workflow (verify)
Aug 25, 2024
16ac4df
moves redefined login and register methods to UserService and removes…
Aug 25, 2024
5ec529f
migrates logout method
Aug 25, 2024
b235c5e
migrates getCurrentUser
Aug 25, 2024
25f5278
migrates changeProfileDetails
Aug 26, 2024
8bbcfc7
migrates changePassword
Aug 26, 2024
8597773
avoids using real impl of getCurrentUser in test
Aug 26, 2024
ef8084e
migrates getVersion
Aug 26, 2024
547a4cc
migrates claimPasswordReset
Aug 26, 2024
55c7639
migrates resetPassword
Aug 26, 2024
063a12f
removes unused file
Aug 26, 2024
6048f90
removes needless ui/public/openapi.yaml file
Aug 26, 2024
5ac7806
fixes "watch:openapi" command
Aug 26, 2024
226457a
better start script
Aug 26, 2024
bd0fed8
uses proper apiSpecPath, pointing to the backend endpoint
Aug 26, 2024
6391886
removes unused js-yaml dependency
Aug 26, 2024
51e0a5c
Merge pull request #1289 from softwaremill/ui-generate-spec-for-api
katekozlowska Aug 26, 2024
cd1742b
augments README for the ui
Aug 26, 2024
1951387
Avoid password4j warnings
adamw Sep 5, 2024
88c8153
frontend tooling description
Sep 6, 2024
120dc8f
Update dependencies, use macwire
adamw Sep 13, 2024
bc43505
Cleanup
adamw Sep 13, 2024
5a59070
Dependencies
adamw Sep 13, 2024
0cdb967
Cleanup
adamw Sep 16, 2024
c9b7e87
Update macwire
adamw Sep 16, 2024
a4c10b3
Remove unused
adamw Sep 16, 2024
f5c481c
Merge branch 'master' into generate-openapi-spec
adamw Sep 18, 2024
4a345d5
Cleanup
adamw Sep 19, 2024
25ad6dc
Update CI
adamw Sep 19, 2024
6e5d6a7
Bridge JUL to SLF4J, fix docker build
adamw Sep 20, 2024
48706d6
Grafana LGTM in docker compose
adamw Sep 20, 2024
cea46e7
JMX metrics
adamw Sep 20, 2024
67946fb
added dynamic params
Sep 22, 2024
b3b337d
Merge branch 'generate-openapi-spec' of github.com:softwaremill/bootz…
Sep 22, 2024
2b96ead
Update ox
adamw Sep 23, 2024
8984df3
Capture logs in otel
adamw Sep 23, 2024
5a81c5c
Merge branch 'master' into generate-openapi-spec
adamw Sep 23, 2024
151d6ae
docs
adamw Sep 23, 2024
ddbae80
Use better way for JVM monitoring
adamw Sep 24, 2024
4883c14
user service refactor
Sep 30, 2024
5f5f00b
Update tapir & ox
adamw Oct 8, 2024
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
49 changes: 18 additions & 31 deletions .github/workflows/bootzooka-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ on:
release:
types:
- released
paths-ignore:
- "helm/**"

jobs:
verify:
Expand All @@ -21,23 +19,18 @@ jobs:
steps:
- name: Check-out repository
id: repo-checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Set up JDK 21
id: jdk-setup
uses: actions/setup-java@v1
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 21
distribution: 'zulu'
java-version: '21'
cache: 'sbt'

- name: Cache SBT
id: cache-sbt
uses: actions/cache@v2
with:
path: |
~/.sbt
~/.ivy2/cache
~/.coursier
key: ${{ runner.os }}-sbt-${{ hashFiles('**/build.sbt') }}
- name: Generate OpenAPI Spec
id: generate-openapi-spec
run: sbt "backend/generateOpenAPIDescription"

- name: Run tests
id: run-tests
Expand All @@ -56,33 +49,26 @@ jobs:
steps:
- name: Check-out repository
id: repo-checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Set up JDK 21
id: jdk-setup
uses: actions/setup-java@v1
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: 21

- name: Cache SBT
id: cache-sbt
uses: actions/cache@v2
with:
path: |
~/.sbt
~/.ivy2/cache
~/.coursier
key: ${{ runner.os }}-sbt-${{ hashFiles('**/build.sbt') }}
distribution: 'zulu'
java-version: '21'
cache: 'sbt'

- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Extract version
run: |
version=${GITHUB_REF/refs\/tags\/v/}
echo "VERSION=$version" >> $GITHUB_ENV

- name: Publish release notes
uses: release-drafter/release-drafter@v5
with:
Expand All @@ -93,5 +79,6 @@ jobs:
version: "v${{ env.VERSION }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Publish docker image
run: sbt backend/docker:publish
40 changes: 35 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
node_modules
.pnp
.pnp.js

# testing
/coverage

# production
build
target

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

*.log
work.txt

# generated code
/ui/src/api-client/openapi.d.ts

# IDE
*.iml
*.ipr
*.iws
.idea/
target/
*.log
.DS_Store
data/
*.bloop
*.metals
.bsp
.vscode
metals.sbt
.bsp
5 changes: 0 additions & 5 deletions .scala-steward.conf
Original file line number Diff line number Diff line change
@@ -1,5 +0,0 @@
updates.ignore = [
{groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.12."},
{groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.13."},
{groupId = "org.scala-lang", artifactId = "scala-compiler", version = "3."}
]
2 changes: 2 additions & 0 deletions .scalafix.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OrganizeImports.groupedImports = AggressiveMerge
OrganizeImports.targetDialect = Scala3
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@

## Quick start

### Using Docker compose

The fastest way to experiment with Bootzooka is using the provided Docker compose setup. It starts three images:
Bootzooka itself (either locally built or downloaded), PostgreSQL server and Graphana LGTM for observability.

### Backend: PostgreSQL & API

In order to run Bootzooka, you'll need a running instance of PostgreSQL with a `bootzooka` database. You can spin
up one easily using docker:
To run Bootzooka's backend locally, you'll still need a running instance of PostgreSQL with a `bootzooka` database.
You can spin up one easily using docker:

```sh
# use "bootzooka" as a password
Expand All @@ -19,10 +24,12 @@ docker run --name bootzooka-postgres -p 5432:5432 -e POSTGRES_PASSWORD=bootzooka
Then, you can start the backend:

```sh
export SQL_PASSWORD=bootzooka
./backend-start.sh
OTEL_SDK_DISABLED=true SQL_PASSWORD=bootzooka ./backend-start.sh
```

Unless you've got an OpenTelemetry collector running, OpenTelemetry should be disabled to avoid telemetry export
exceptions.

### Frontend: Yarn & webapp

You will need the [yarn package manager](https://yarnpkg.com) to run the UI. Install it using your package manager or:
Expand Down
2 changes: 0 additions & 2 deletions backend/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ db {
migrate-on-start = ${?MIGRATE_ON_START}

driver = "org.postgresql.Driver"

connect-thread-pool-size = 32
}

email {
Expand Down
36 changes: 23 additions & 13 deletions backend/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -1,35 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>

<configuration scan="true">
<configuration>
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>

<if condition='p("LOGBACK_JSON_ENCODE").equals("true")'>
<then>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
<encoder class="net.logstash.logback.encoder.LogstashEncoder" />
</appender>
</then>
<else>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS}%boldYellow(%replace( [%X{cid}] ){' \[\] ', ' '})[%thread] %-5level %logger{5} - %msg%n%rEx</pattern>
</encoder>
</appender>
<encoder>
<pattern><![CDATA[%d{HH:mm:ss.SSS}%boldYellow(%replace( [%X{cid}] ){' \[\] ', ' '})[%thread] %-5level %logger{5} - %msg%n%rEx]]></pattern>
</encoder>
</appender>
</else>
</if>

<appender name="OpenTelemetry"
class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender"></appender>

<logger name="com.softwaremill.bootzooka" level="${LOG_LEVEL:-DEBUG}" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="STDOUT" />
<appender-ref ref="OpenTelemetry" />
</logger>

<logger name="sttp.client3" level="${LOG_LEVEL:-DEBUG}" additivity="false">
<appender-ref ref="STDOUT"/>
<logger name="sttp.client3" level="${LOG_LEVEL:-INFO}" additivity="false">
<appender-ref ref="STDOUT" />
<appender-ref ref="OpenTelemetry" />
</logger>

<logger name="sttp.tapir" level="${LOG_LEVEL:-DEBUG}" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="STDOUT" />
<appender-ref ref="OpenTelemetry" />
</logger>

<root level="${LOG_LEVEL:-INFO}">
<appender-ref ref="STDOUT"/>
<appender-ref ref="STDOUT" />
<appender-ref ref="OpenTelemetry" />
</root>

</configuration>
</configuration>
1 change: 1 addition & 0 deletions backend/src/main/resources/psw4j.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global.salt.length=64
118 changes: 58 additions & 60 deletions backend/src/main/scala/com/softwaremill/bootzooka/Dependencies.scala
Original file line number Diff line number Diff line change
@@ -1,75 +1,73 @@
package com.softwaremill.bootzooka

import com.softwaremill.bootzooka.admin.VersionApi
import com.softwaremill.bootzooka.config.Config
import com.softwaremill.bootzooka.email.EmailService
import com.softwaremill.bootzooka.email.sender.EmailSender
import com.softwaremill.bootzooka.email.{EmailModel, EmailService, EmailTemplates}
import com.softwaremill.bootzooka.http.{Http, HttpApi}
import com.softwaremill.bootzooka.http.{HttpApi, HttpConfig}
import com.softwaremill.bootzooka.infrastructure.{DB, SetCorrelationIdBackend}
import com.softwaremill.bootzooka.metrics.{Metrics, VersionApi}
import com.softwaremill.bootzooka.passwordreset.{PasswordResetApi, PasswordResetAuthToken, PasswordResetCodeModel, PasswordResetService}
import com.softwaremill.bootzooka.security.{ApiKeyAuthToken, ApiKeyModel, ApiKeyService, Auth}
import com.softwaremill.bootzooka.user.{UserApi, UserModel, UserService}
import com.softwaremill.bootzooka.metrics.Metrics
import com.softwaremill.bootzooka.passwordreset.{PasswordResetApi, PasswordResetAuthToken}
import com.softwaremill.bootzooka.security.{ApiKeyAuthToken, ApiKeyService, Auth}
import com.softwaremill.bootzooka.user.UserApi
import com.softwaremill.bootzooka.util.{Clock, DefaultClock, DefaultIdGenerator, IdGenerator}
import com.softwaremill.macwire.{autowire, autowireMembersOf}
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.metrics.SdkMeterProvider
import io.opentelemetry.sdk.metrics.`export`.PeriodicMetricReader
import ox.{IO, Ox, tap, useCloseableInScope, useInScope}
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender
import io.opentelemetry.instrumentation.runtimemetrics.java8.{Classes, Cpu, GarbageCollector, MemoryPools, Threads}
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk
import ox.{Ox, discard, tap, useCloseableInScope, useInScope}
import sttp.client3.logging.slf4j.Slf4jLoggingBackend
import sttp.client3.opentelemetry.OpenTelemetryMetricsBackend
import sttp.client3.{HttpClientSyncBackend, SttpBackend}
import sttp.shared.Identity
import sttp.tapir.AnyEndpoint

trait Dependencies(using Ox, IO):
// TODO use macwire/autowire once available for Scala3
lazy val config: Config = Config.read.tap(Config.log)
lazy val otel: OpenTelemetry = createOtel()
lazy val metrics = new Metrics(otel)
lazy val sttpBackend: SttpBackend[Identity, Any] =
useInScope(
case class Dependencies(httpApi: HttpApi, emailService: EmailService)

object Dependencies:
val endpointsForDocs: List[AnyEndpoint] = List(UserApi, PasswordResetApi, VersionApi).flatMap(_.endpointsForDocs)

private case class Apis(userApi: UserApi, passwordResetApi: PasswordResetApi, versionApi: VersionApi):
def endpoints = List(userApi, passwordResetApi, versionApi).flatMap(_.endpoints)

def create(using Ox): Dependencies =
val config = Config.read.tap(Config.log)
val otel = initializeOtel()
val sttpBackend = useInScope(
Slf4jLoggingBackend(OpenTelemetryMetricsBackend(new SetCorrelationIdBackend(HttpClientSyncBackend()), otel), includeTiming = true)
)(_.close())
lazy val db: DB = useCloseableInScope(DB.createTestMigrate(config.db))
lazy val idGenerator: IdGenerator = DefaultIdGenerator
lazy val clock: Clock = DefaultClock
lazy val http = new Http
lazy val emailTemplates = new EmailTemplates
lazy val emailModel = new EmailModel
lazy val emailSender: EmailSender = EmailSender.create(sttpBackend, config.email)
lazy val emailService = new EmailService(emailModel, idGenerator, emailSender, config.email, db, metrics)
lazy val apiKeyModel = new ApiKeyModel
lazy val apiKeyAuthToken = new ApiKeyAuthToken(apiKeyModel)
lazy val apiKeyService = new ApiKeyService(apiKeyModel, idGenerator, clock)
lazy val apiKeyAuth = new Auth(apiKeyAuthToken, db, clock)
lazy val passwordResetCodeModel = new PasswordResetCodeModel
lazy val passwordResetAuthToken = new PasswordResetAuthToken(passwordResetCodeModel)
lazy val passwordResetAuth = new Auth(passwordResetAuthToken, db, clock)
lazy val userModel = new UserModel
lazy val userService = new UserService(userModel, emailService, emailTemplates, apiKeyService, idGenerator, clock, config.user)
lazy val userApi = new UserApi(http, apiKeyAuth, userService, db, metrics)
lazy val passwordResetService = new PasswordResetService(
userModel,
passwordResetCodeModel,
emailService,
emailTemplates,
passwordResetAuth,
idGenerator,
config.passwordReset,
clock,
db
)
lazy val passwordResetApi = new PasswordResetApi(http, passwordResetService, db)
lazy val versionApi = new VersionApi(http)
lazy val httpApi =
new HttpApi(http, userApi.endpoints ++ passwordResetApi.endpoints, List(versionApi.versionEndpoint), otel, config.api)
val db: DB = useCloseableInScope(DB.createTestMigrate(config.db))

create(config, otel, sttpBackend, db, DefaultClock)

/** Create the service graph using the given infrastructure services & configuration. */
def create(config: Config, otel: OpenTelemetry, sttpBackend: SttpBackend[Identity, Any], db: DB, clock: Clock): Dependencies =
autowire[Dependencies](
autowireMembersOf(config),
otel,
sttpBackend,
db,
DefaultIdGenerator,
clock,
EmailSender.create,
(apis: Apis, otel: OpenTelemetry, httpConfig: HttpConfig) =>
new HttpApi(apis.endpoints, Dependencies.endpointsForDocs, otel, httpConfig),
classOf[EmailService],
new Auth(_: ApiKeyAuthToken, _: DB, _: Clock),
new Auth(_: PasswordResetAuthToken, _: DB, _: Clock)
)

private def createOtel(): OpenTelemetry =
// An exporter that sends metrics to a collector over gRPC
val grpcExporter = OtlpGrpcMetricExporter.builder().build()
// A metric reader that exports using the gRPC exporter
val metricReader: PeriodicMetricReader = PeriodicMetricReader.builder(grpcExporter).build()
// A meter registry whose meters are read by the above reader
val meterProvider: SdkMeterProvider = SdkMeterProvider.builder().registerMetricReader(metricReader).build()
// An instance of OpenTelemetry using the above meter registry
OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build()
private def initializeOtel(): OpenTelemetry =
AutoConfiguredOpenTelemetrySdk
.initialize()
.getOpenTelemetrySdk()
.tap { otel =>
// see https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/runtime-telemetry/runtime-telemetry-java8/library
Classes.registerObservers(otel)
Cpu.registerObservers(otel)
MemoryPools.registerObservers(otel)
Threads.registerObservers(otel)
GarbageCollector.registerObservers(otel).discard
}
.tap(OpenTelemetryAppender.install)
Loading