├── archetype
├── src
│ ├── test
│ │ └── resources
│ │ │ └── projects
│ │ │ └── basic
│ │ │ ├── goal.txt
│ │ │ └── archetype.properties
│ └── main
│ │ └── resources
│ │ ├── archetype-resources
│ │ ├── local-server
│ │ │ ├── src
│ │ │ │ └── main
│ │ │ │ │ ├── resources
│ │ │ │ │ └── log4j2.xml
│ │ │ │ │ └── kotlin
│ │ │ │ │ └── localserver
│ │ │ │ │ └── Main.kt
│ │ │ └── pom.xml
│ │ ├── core
│ │ │ ├── src
│ │ │ │ ├── main
│ │ │ │ │ ├── resources
│ │ │ │ │ │ └── log4j2.xml
│ │ │ │ │ ├── kotlin
│ │ │ │ │ │ └── core
│ │ │ │ │ │ │ ├── Config.kt
│ │ │ │ │ │ │ ├── generated
│ │ │ │ │ │ │ └── Generated.kt
│ │ │ │ │ │ │ └── ApiDefinition.kt
│ │ │ │ │ └── cloudformation
│ │ │ │ │ │ └── root.template
│ │ │ │ └── assembly
│ │ │ │ │ └── dist.xml
│ │ │ └── pom.xml
│ │ └── pom.xml
│ │ └── META-INF
│ │ └── maven
│ │ └── archetype-metadata.xml
└── pom.xml
├── .travis.yml
├── gradle-plugin
├── settings.gradle
├── src
│ └── main
│ │ ├── resources
│ │ ├── META-INF
│ │ │ └── gradle-plugins
│ │ │ │ ├── ws.osiris.deploy.properties
│ │ │ │ └── ws.osiris.project.properties
│ │ └── archetype-resources
│ │ │ ├── settings.gradle
│ │ │ ├── local-server
│ │ │ └── build.gradle
│ │ │ ├── build.gradle
│ │ │ └── core
│ │ │ └── build.gradle
│ │ ├── gradle
│ │ └── generateProject.gradle
│ │ └── kotlin
│ │ └── ws
│ │ └── osiris
│ │ └── gradle
│ │ ├── ProjectPlugin.kt
│ │ └── DeployPlugin.kt
└── build.gradle
├── core
├── src
│ ├── test
│ │ ├── static
│ │ │ ├── foo
│ │ │ │ └── bar.html
│ │ │ └── index.html
│ │ └── kotlin
│ │ │ └── ws
│ │ │ └── osiris
│ │ │ └── core
│ │ │ ├── ContentTypeTest.kt
│ │ │ ├── StandardFilterTest.kt
│ │ │ ├── InMemoryTestClientTest.kt
│ │ │ ├── HttpTest.kt
│ │ │ ├── MatchTest.kt
│ │ │ ├── TestClient.kt
│ │ │ └── ModelTest.kt
│ └── main
│ │ └── kotlin
│ │ └── ws
│ │ └── osiris
│ │ └── core
│ │ ├── Route.kt
│ │ └── Filters.kt
└── pom.xml
├── integration
├── src
│ ├── test
│ │ ├── e2e-test
│ │ │ ├── static
│ │ │ │ ├── baz
│ │ │ │ │ └── bar.html
│ │ │ │ └── index.html
│ │ │ └── code
│ │ │ │ └── ApiDefinition.kt
│ │ ├── static
│ │ │ ├── baz
│ │ │ │ └── bar.html
│ │ │ └── index.html
│ │ ├── kotlin
│ │ │ └── ws
│ │ │ │ └── osiris
│ │ │ │ └── integration
│ │ │ │ ├── Main.kt
│ │ │ │ ├── TestHelpersTest.kt
│ │ │ │ └── IntegrationTests.kt
│ │ └── resources
│ │ │ └── log4j2.xml
│ └── main
│ │ └── kotlin
│ │ └── ws
│ │ └── osiris
│ │ └── integration
│ │ └── IntegrationTestApi.kt
└── pom.xml
├── local-server
├── src
│ └── test
│ │ ├── static
│ │ ├── foo
│ │ │ └── bar.html
│ │ └── index.html
│ │ └── kotlin
│ │ └── ws
│ │ └── osiris
│ │ └── localserver
│ │ ├── LocalHttpTestClient.kt
│ │ └── LocalServerTest.kt
└── pom.xml
├── .gitignore
├── RELEASE.md
├── README.md
├── aws
├── src
│ ├── main
│ │ └── kotlin
│ │ │ └── ws
│ │ │ └── osiris
│ │ │ └── aws
│ │ │ ├── Request.kt
│ │ │ ├── KeepAlive.kt
│ │ │ └── Lambda.kt
│ └── test
│ │ └── kotlin
│ │ └── ws
│ │ └── osiris
│ │ └── aws
│ │ ├── ConfigTest.kt
│ │ └── RequestTest.kt
└── pom.xml
├── .github
├── workflows
│ └── maven.yml
└── dependabot.yml
├── aws-deploy
├── src
│ ├── test
│ │ └── kotlin
│ │ │ └── ws
│ │ │ └── osiris
│ │ │ ├── awsdeploy
│ │ │ ├── DeployTest.kt
│ │ │ └── cloudformation
│ │ │ │ └── ResourceNodeTest.kt
│ │ │ └── aws
│ │ │ └── cloudformation
│ │ │ └── TemplateTest.kt
│ └── main
│ │ └── kotlin
│ │ └── ws
│ │ └── osiris
│ │ └── awsdeploy
│ │ ├── cloudformation
│ │ └── ResourceTree.kt
│ │ ├── Profile.kt
│ │ └── Deploy.kt
└── pom.xml
├── maven-plugin
├── pom.xml
└── src
│ └── main
│ └── kotlin
│ └── ws
│ └── osiris
│ └── maven
│ └── Maven.kt
├── server
├── src
│ └── test
│ │ └── kotlin
│ │ └── ws
│ │ └── osiris
│ │ └── server
│ │ └── HttpTestClient.kt
└── pom.xml
├── bom
└── pom.xml
└── .idea
└── codeStyleSettings.xml
/archetype/src/test/resources/projects/basic/goal.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 | jdk:
3 | - openjdk8
4 |
--------------------------------------------------------------------------------
/gradle-plugin/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = "osiris-gradle-plugin"
2 |
--------------------------------------------------------------------------------
/core/src/test/static/foo/bar.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello, bar!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/core/src/test/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello, world!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/integration/src/test/e2e-test/static/baz/bar.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello, bar!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/integration/src/test/e2e-test/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello, world!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/integration/src/test/static/baz/bar.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello, bar!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/integration/src/test/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello, world!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/local-server/src/test/static/foo/bar.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello, bar!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/local-server/src/test/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello, world!
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/resources/META-INF/gradle-plugins/ws.osiris.deploy.properties:
--------------------------------------------------------------------------------
1 | implementation-class=ws.osiris.gradle.OsirisDeployPlugin
2 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/resources/META-INF/gradle-plugins/ws.osiris.project.properties:
--------------------------------------------------------------------------------
1 | implementation-class=ws.osiris.gradle.OsirisProjectPlugin
2 |
--------------------------------------------------------------------------------
/archetype/src/test/resources/projects/basic/archetype.properties:
--------------------------------------------------------------------------------
1 | #Sun May 28 11:08:44 BST 2017
2 | package=it.pkg
3 | version=0.1-SNAPSHOT
4 | groupId=archetype.it
5 | artifactId=basic
6 | osirisVersion=whatever
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
2 | *.iml
3 | *.DS_Store
4 | */target/*
5 | !.idea/codeStyleSettings.xml
6 | gradle-plugin/gradle/wrapper
7 | gradle-plugin/gradlew
8 | gradle-plugin/gradlew.bat
9 | gradle-plugin/.gradle
10 | gradle-plugin/build
11 | gradle-plugin/out
12 | # Project exclude paths
13 | /target/
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Main Project
2 | `gpg2` needs to be on the path.
3 |
4 | From the root directory:
5 |
6 | export GPG_TTY=$(tty)
7 | mvn clean install
8 | mvn deploy -Prelease
9 |
10 | # Gradle Plugin
11 | From the `gradle-plugin` directory
12 |
13 | ./gradlew clean publishPlugins
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/resources/archetype-resources/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = '${rootArtifactId}'
2 | include ':${rootArtifactId}-core'
3 | include ':${rootArtifactId}-local-server'
4 |
5 | project(':${rootArtifactId}-core').projectDir = "$rootDir/core" as File
6 | project(':${rootArtifactId}-local-server').projectDir = "$rootDir/local-server" as File
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Osiris - Simple Serverless Web Apps
2 |
3 | Osiris is a Kotlin library that makes it easy to write and deploy serverless web applications on AWS. For more details please see the [website](http://www.osiris.ws/), the [wiki](https://github.com/cjkent/osiris/wiki/) or the [example projects](https://github.com/cjkent/osiris-examples).
4 |
5 | Osiris is used in production at [CodeScreen](https://www.codescreen.com/).
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/resources/archetype-resources/local-server/build.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation project(':${rootArtifactId}-core')
3 | implementation platform("ws.osiris:osiris-bom:$osirisVersion")
4 | implementation 'org.jetbrains.kotlin:kotlin-stdlib'
5 | implementation 'ws.osiris:osiris-local-server'
6 | implementation 'org.apache.logging.log4j:log4j-core'
7 | implementation 'org.apache.logging.log4j:log4j-slf4j2-impl'
8 | }
9 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/gradle/generateProject.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'ws.osiris.project' version '2.0.0'
3 | }
4 | // For local testing the plugins block needs to be replaced with
5 | //
6 | // buildscript {
7 | // repositories {
8 | // jcenter()
9 | // mavenLocal()
10 | // }
11 | // dependencies {
12 | // classpath 'ws.osiris:osiris-gradle-plugin:2.0.0'
13 | // }
14 | // }
15 | // apply plugin: 'ws.osiris.project'
16 |
17 | defaultTasks 'generateProject'
18 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/resources/archetype-resources/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'org.jetbrains.kotlin.jvm' version '1.3.31' apply false
3 | }
4 |
5 | subprojects {
6 | apply plugin: 'org.jetbrains.kotlin.jvm'
7 |
8 | repositories {
9 | jcenter()
10 | }
11 | compileKotlin {
12 | kotlinOptions {
13 | jvmTarget = '11'
14 | }
15 | }
16 | compileTestKotlin {
17 | kotlinOptions {
18 | jvmTarget = '11'
19 | }
20 | }
21 | }
22 |
23 | ext {
24 | osirisVersion = '2.0.0'
25 | }
26 |
--------------------------------------------------------------------------------
/integration/src/test/kotlin/ws/osiris/integration/Main.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.integration
2 |
3 | import ws.osiris.server.HttpTestClient
4 | import ws.osiris.server.Protocol
5 |
6 | fun main(args: Array) {
7 | if (args.size < 3) throw IllegalArgumentException("Required arguments: API ID, region, stage")
8 | val apiId = args[0]
9 | val region = args[1]
10 | val stage = args[2]
11 | val client = HttpTestClient(Protocol.HTTPS, "$apiId.execute-api.$region.amazonaws.com", 443, "/$stage")
12 | assertApi(client)
13 | println("Everything passed")
14 | }
15 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/resources/archetype-resources/core/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'ws.osiris.deploy' version '2.0.0'
3 | }
4 |
5 | dependencies {
6 | implementation platform("ws.osiris:osiris-bom:$osirisVersion")
7 | implementation 'org.jetbrains.kotlin:kotlin-stdlib'
8 | implementation 'ws.osiris:osiris-core'
9 | implementation 'ws.osiris:osiris-aws'
10 | implementation 'com.google.code.gson:gson'
11 | implementation 'org.apache.logging.log4j:log4j-slf4j2-impl'
12 | implementation 'com.amazonaws:aws-lambda-java-log4j2'
13 | }
14 |
15 | osiris {
16 | rootPackage = '${package}'
17 | }
18 |
--------------------------------------------------------------------------------
/integration/src/test/kotlin/ws/osiris/integration/TestHelpersTest.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.integration
2 |
3 | import org.testng.annotations.Test
4 | import java.nio.file.Files
5 | import java.nio.file.Paths
6 | import kotlin.test.assertTrue
7 |
8 | @Test
9 | class TestHelpersTest {
10 |
11 | fun copyDirectoryContents() {
12 | TmpDirResource().use {
13 | copyDirectoryContents(Paths.get("src/test/static"), it.path)
14 | assertTrue(Files.isRegularFile(it.path.resolve("baz/bar.html")))
15 | assertTrue(Files.isRegularFile(it.path.resolve("index.html")))
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/integration/src/test/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5p %c - %m%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/local-server/src/main/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5p %c - %m%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/core/src/main/resources/log4j2.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %d{yyyy-MM-dd HH:mm:ss.SSS} %X{AWSRequestId} %-5p %c - %m%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/core/src/assembly/dist.xml:
--------------------------------------------------------------------------------
1 |
4 | dist
5 |
6 | zip
7 |
8 | false
9 |
10 |
11 | /lib
12 | true
13 | false
14 | runtime
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/local-server/src/main/kotlin/localserver/Main.kt:
--------------------------------------------------------------------------------
1 | package ${package}.localserver
2 |
3 | import com.beust.jcommander.JCommander
4 | import ws.osiris.localserver.ServerArgs
5 | import ws.osiris.localserver.runLocalServer
6 |
7 | import ${package}.core.api
8 | import ${package}.core.createComponents
9 |
10 | /**
11 | * Main function for running the application in an HTTP server (Jetty).
12 | *
13 | * The arguments are:
14 | * * `-p`, `--port` - the port the server listens on. Defaults to 8080.
15 | * * `-r`, `--root` - the context root. This is the base path from which everything is served.
16 | */
17 | fun main(args: Array) {
18 | val serverArgs = ServerArgs()
19 | JCommander.newBuilder().addObject(serverArgs).build().parse(*args)
20 | val components = createComponents()
21 | runLocalServer(api, components, serverArgs.port, serverArgs.root, "core/src/main/static")
22 | }
23 |
--------------------------------------------------------------------------------
/aws/src/main/kotlin/ws/osiris/aws/Request.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.aws
2 |
3 | import com.amazonaws.services.lambda.runtime.Context
4 | import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
5 | import ws.osiris.core.Request
6 |
7 | /** The AWS stage variables; this is an extension property to avoid putting AWS concepts into the core module. */
8 | @Suppress("UNCHECKED_CAST")
9 | val Request.stageVariables: Map
10 | get() = this.attributes[STAGE_VARS_ATTR] as? Map? ?: mapOf()
11 |
12 | /** The event passed to the lambda by AWS. */
13 | val Request.lambdaEvent: APIGatewayProxyRequestEvent?
14 | // If this isn't available in the attributes it could be created from the context
15 | get() = this.attributes[LAMBDA_EVENT_ATTR] as? APIGatewayProxyRequestEvent
16 |
17 | /** The context passed to the lambda by AWS. */
18 | val Request.lambdaContext: Context?
19 | get() = this.attributes[LAMBDA_CONTEXT_ATTR] as? Context
20 |
21 | /** The AWS stage name; this is an extension property to avoid putting AWS concepts into the core module. */
22 | val Request.stageName: String get() = this.context.optional("stage") ?: "UNKNOWN"
23 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/core/src/main/kotlin/core/Config.kt:
--------------------------------------------------------------------------------
1 | package ${package}.core
2 |
3 | import ws.osiris.aws.ApplicationConfig
4 | import ws.osiris.aws.Stage
5 | import java.time.Duration
6 |
7 | /**
8 | * Configuration that controls how the application is deployed to AWS.
9 | */
10 | val config = ApplicationConfig(
11 | applicationName = "${rootArtifactId}",
12 | lambdaMemorySizeMb = 512,
13 | lambdaTimeout = Duration.ofSeconds(10),
14 | environmentVariables = mapOf(
15 | "EXAMPLE_ENVIRONMENT_VARIABLE" to "Bob"
16 | ),
17 | stages = listOf(
18 | Stage(
19 | name = "dev",
20 | description = "Development stage",
21 | deployOnUpdate = true,
22 | variables = mapOf(
23 | "VAR1" to "devValue1",
24 | "VAR2" to "devValue2"
25 | )
26 | ),
27 | Stage(
28 | name = "prod",
29 | description = "Production stage",
30 | deployOnUpdate = false,
31 | variables = mapOf(
32 | "VAR1" to "prodValue1",
33 | "VAR2" to "prodValue2"
34 | )
35 | )
36 | )
37 | )
38 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/core/src/main/kotlin/core/generated/Generated.kt:
--------------------------------------------------------------------------------
1 | package ${package}.core.generated
2 |
3 | import ws.osiris.aws.ApiFactory
4 | import ws.osiris.aws.ApplicationConfig
5 | import ws.osiris.aws.ProxyLambda
6 | import ws.osiris.core.Api
7 | import ws.osiris.core.ComponentsProvider
8 |
9 | import ${package}.core.api
10 | import ${package}.core.createComponents
11 |
12 | /**
13 | * The lambda function that is deployed to AWS and provides the entry point to the application.
14 | *
15 | * This is generated code and should not be modified or deleted.
16 | */
17 | @Suppress("UNCHECKED_CAST", "unused")
18 | class GeneratedLambda : ProxyLambda(api as Api, createComponents())
19 |
20 | /**
21 | * Creates the API and application configuration; used by the build plugins during deployment.
22 | *
23 | * This is generated code and should not be modified or deleted.
24 | */
25 | @Suppress("UNCHECKED_CAST", "unused")
26 | class GeneratedApiFactory : ApiFactory {
27 | override val api: Api = ${package}.core.api as Api
28 | override val config: ApplicationConfig = ${package}.core.config
29 | }
30 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Java CI with Maven
10 |
11 | on:
12 | push:
13 | branches: [ "main" ]
14 | pull_request:
15 | branches: [ "main" ]
16 |
17 | jobs:
18 | build:
19 |
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - uses: actions/checkout@v3
24 | - name: Set up JDK 17
25 | uses: actions/setup-java@v3
26 | with:
27 | java-version: '17'
28 | distribution: 'temurin'
29 | cache: maven
30 | - name: Build with Maven
31 | run: mvn -B package --file pom.xml
32 |
33 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
34 | # - name: Update dependency graph
35 | # uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
36 |
--------------------------------------------------------------------------------
/aws-deploy/src/test/kotlin/ws/osiris/awsdeploy/DeployTest.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.awsdeploy
2 |
3 | import org.intellij.lang.annotations.Language
4 | import org.testng.annotations.Test
5 | import kotlin.test.assertEquals
6 |
7 | @Test
8 | class DeployTest {
9 |
10 | fun generatedTemplateParameters() {
11 | val templateUrl = "https://test-app-code.s3.eu-west-1.amazonaws.com/test-app.template"
12 | @Language("yaml")
13 | val template = """
14 | Resources:
15 | Foo:
16 | Type: AWS::Whatever
17 | Properties:
18 | Bar: baz
19 | Bar:
20 | Type: AWS::CloudFormation::Stack
21 | Properties:
22 | TemplateURL: "https://some_bucket.s3.eu-west-1.amazonaws.com/whatever.template"
23 | Parameters:
24 | FooParam: fooValue
25 | BarParam: barValue
26 | ApiStack:
27 | Type: AWS::CloudFormation::Stack
28 | Properties:
29 | TemplateURL: "$templateUrl"
30 | Parameters:
31 | Param1: value1
32 | Param2: value2
33 | """.trimIndent()
34 | val parameters = generatedTemplateParameters(template, "test-app")
35 | assertEquals(setOf("Param1", "Param2"), parameters)
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/local-server/src/test/kotlin/ws/osiris/localserver/LocalHttpTestClient.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.localserver
2 |
3 | import org.eclipse.jetty.server.Server
4 | import ws.osiris.core.Api
5 | import ws.osiris.core.ComponentsProvider
6 | import ws.osiris.core.RequestContextFactory
7 | import ws.osiris.core.TestClient
8 | import ws.osiris.server.HttpTestClient
9 | import ws.osiris.server.Protocol
10 |
11 | /**
12 | * Test client that executes HTTP requests against an API hosted by an in-process Jetty server.
13 | */
14 | class LocalHttpTestClient private constructor(
15 | private val httpClient: TestClient,
16 | private val server: Server
17 | ) : TestClient by httpClient, AutoCloseable {
18 |
19 | override fun close() {
20 | server.stop()
21 | }
22 |
23 | companion object {
24 |
25 | // TODO randomise the port, retry a few times if the server doesn't start
26 |
27 | /** Returns a client for an API that uses components in its handlers. */
28 | fun create(
29 | api: Api,
30 | components: T,
31 | staticFilesDir: String? = null,
32 | requestContextFactory: RequestContextFactory = RequestContextFactory.empty()
33 | ): LocalHttpTestClient {
34 |
35 | val port = 8080
36 | val server = createLocalServer(
37 | api,
38 | components,
39 | staticFilesDir = staticFilesDir,
40 | requestContextFactory = requestContextFactory
41 | )
42 | val client = HttpTestClient(Protocol.HTTP, "localhost", port)
43 | server.start()
44 | return LocalHttpTestClient(client, server)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/aws-deploy/src/main/kotlin/ws/osiris/awsdeploy/cloudformation/ResourceTree.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.awsdeploy.cloudformation
2 |
3 | internal fun ResourceTemplate.partitionChildren(
4 | firstPartitionMaxSize: Int,
5 | partitionMaxSize: Int
6 | ): List> {
7 |
8 | val firstPartition = partition(firstPartitionMaxSize, children)
9 | val resources = mutableListOf>()
10 | var partitionedCount = firstPartition.size
11 | var remaining = children.subList(partitionedCount, children.size)
12 | resources.add(firstPartition)
13 | while (partitionedCount < children.size) {
14 | val partition = partition(partitionMaxSize, remaining)
15 | partitionedCount += partition.size
16 | resources.add(partition)
17 | remaining = children.subList(partitionedCount, children.size)
18 | }
19 | return resources
20 | }
21 |
22 | private fun partition(maxSize: Int, nodes: List): List {
23 | // This happens if there is nothing but the root node
24 | if (nodes.isEmpty()) return listOf()
25 | // TODO is there a sane way to do this without all the mutation? or maybe with just mutation of the list?
26 | var curCount = 0
27 | var idx = 0
28 | val resources = mutableListOf()
29 | while (idx < nodes.size) {
30 | val node = nodes[idx]
31 | curCount += node.resourceCount
32 | if (curCount <= maxSize) {
33 | resources.add(node)
34 | idx++
35 | } else {
36 | break
37 | }
38 | }
39 | if (resources.isEmpty()) {
40 | throw IllegalStateException("Failed to partition resources")
41 | }
42 | return resources
43 | }
44 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: maven
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | ignore:
9 | - dependency-name: com.amazonaws:aws-java-sdk-bom
10 | versions:
11 | - 1.11.1000
12 | - 1.11.1001
13 | - 1.11.1002
14 | - 1.11.1003
15 | - 1.11.1004
16 | - 1.11.1005
17 | - 1.11.1006
18 | - 1.11.945
19 | - 1.11.946
20 | - 1.11.947
21 | - 1.11.948
22 | - 1.11.949
23 | - 1.11.950
24 | - 1.11.951
25 | - 1.11.952
26 | - 1.11.953
27 | - 1.11.954
28 | - 1.11.955
29 | - 1.11.956
30 | - 1.11.957
31 | - 1.11.958
32 | - 1.11.959
33 | - 1.11.960
34 | - 1.11.961
35 | - 1.11.962
36 | - 1.11.963
37 | - 1.11.964
38 | - 1.11.965
39 | - 1.11.966
40 | - 1.11.967
41 | - 1.11.968
42 | - 1.11.969
43 | - 1.11.970
44 | - 1.11.971
45 | - 1.11.972
46 | - 1.11.973
47 | - 1.11.974
48 | - 1.11.975
49 | - 1.11.976
50 | - 1.11.977
51 | - 1.11.978
52 | - 1.11.979
53 | - 1.11.980
54 | - 1.11.981
55 | - 1.11.982
56 | - 1.11.983
57 | - 1.11.986
58 | - 1.11.987
59 | - 1.11.988
60 | - 1.11.990
61 | - 1.11.991
62 | - 1.11.992
63 | - 1.11.993
64 | - 1.11.994
65 | - 1.11.995
66 | - 1.11.996
67 | - 1.11.997
68 | - 1.11.998
69 | - 1.11.999
70 | - dependency-name: org.jetbrains.dokka:dokka-maven-plugin
71 | versions:
72 | - 1.4.20-dev-65
73 | - 1.4.30
74 | - dependency-name: org.jetbrains.kotlin:kotlin-maven-plugin
75 | versions:
76 | - 1.4.21-2
77 | - dependency-name: org.jetbrains.kotlin:kotlin-test
78 | versions:
79 | - 1.4.21-2
80 | - dependency-name: org.jetbrains.kotlin:kotlin-reflect
81 | versions:
82 | - 1.4.21-2
83 | - dependency-name: org.jetbrains.kotlin:kotlin-stdlib
84 | versions:
85 | - 1.4.21-2
86 | - dependency-name: com.beust:jcommander
87 | versions:
88 | - "1.80"
89 | - dependency-name: org.apache.logging.log4j:log4j-slf4j2-impl
90 | versions:
91 | - 2.14.0
92 | - dependency-name: com.amazonaws:aws-lambda-java-events
93 | versions:
94 | - 3.7.0
95 |
--------------------------------------------------------------------------------
/maven-plugin/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | ws.osiris
8 | osiris
9 | 2.0.1-SNAPSHOT
10 | ..
11 |
12 |
13 | osiris-maven-plugin
14 | maven-plugin
15 |
16 | Osiris Maven Plugin
17 | Maven plugin for deploying to AWS
18 |
19 |
20 |
21 | ws.osiris
22 | osiris-aws-deploy
23 |
24 |
25 | org.jetbrains.kotlin
26 | kotlin-stdlib
27 |
28 |
29 |
30 | com.googlecode.slf4j-maven-plugin-log
31 | slf4j-maven-plugin-log
32 | 1.0.0
33 |
34 |
35 |
36 |
37 | org.apache.maven
38 | maven-plugin-api
39 | 3.9.6
40 | provided
41 |
42 |
43 | org.apache.maven.plugin-tools
44 | maven-plugin-annotations
45 | 3.11.0
46 | provided
47 |
48 |
49 | org.apache.maven
50 | maven-project
51 | 2.2.1
52 | provided
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/aws/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | ws.osiris
8 | osiris
9 | 2.0.1-SNAPSHOT
10 | ..
11 |
12 | osiris-aws
13 | jar
14 | Osiris AWS
15 | Utilities for deploying to AWS
16 |
17 |
18 |
19 | ws.osiris
20 | osiris-core
21 |
22 |
23 | ws.osiris
24 | osiris-server
25 |
26 |
27 | org.jetbrains.kotlin
28 | kotlin-stdlib
29 |
30 |
31 | org.slf4j
32 | slf4j-api
33 |
34 |
35 | com.amazonaws
36 | aws-java-sdk-lambda
37 |
38 |
39 | com.amazonaws
40 | aws-lambda-java-core
41 |
42 |
43 | com.amazonaws
44 | aws-lambda-java-events
45 |
46 |
47 |
48 |
49 | org.testng
50 | testng
51 | test
52 |
53 |
54 | org.jetbrains.kotlin
55 | kotlin-test
56 | test
57 |
58 |
59 | com.fasterxml.jackson.core
60 | jackson-databind
61 | test
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/core/src/main/kotlin/core/ApiDefinition.kt:
--------------------------------------------------------------------------------
1 | package ${package}.core
2 |
3 | import ws.osiris.core.MimeTypes
4 | import ws.osiris.core.ComponentsProvider
5 | import ws.osiris.core.HttpHeaders
6 | import ws.osiris.core.api
7 |
8 | /**
9 | * The name of an environment variable used to pass in configuration.
10 | *
11 | * The values of environment variables can be specified by the `environmentVariables` property in the configuration:
12 | *
13 | * val config = ApplicationConfig(
14 | * environmentVariables = mapOf(
15 | * "EXAMPLE_ENVIRONMENT_VARIABLE" to "Bob"
16 | * )
17 | * ...
18 | * )
19 | */
20 | const val EXAMPLE_ENVIRONMENT_VARIABLE = "EXAMPLE_ENVIRONMENT_VARIABLE"
21 |
22 | /** The API. */
23 | val api = api {
24 |
25 | get("/helloworld") {
26 | // return a map that is automatically converted to JSON
27 | mapOf("message" to "hello, world!")
28 | }
29 |
30 | get("/helloplain") { req ->
31 | // return a response with customised headers
32 | req.responseBuilder()
33 | .header(HttpHeaders.CONTENT_TYPE, MimeTypes.TEXT_PLAIN)
34 | .build("hello, world!")
35 | }
36 |
37 | get("/helloqueryparam") { req ->
38 | // get a query parameter
39 | val name = req.queryParams["name"]
40 | mapOf("message" to "hello, $name!")
41 | }
42 |
43 | get("/helloenv") {
44 | // use the name property from ExampleComponents for the name
45 | mapOf("message" to "hello, $name!")
46 | }
47 |
48 | get("/hello/{name}") { req ->
49 | // get a path parameter
50 | val name = req.pathParams["name"]
51 | mapOf("message" to "hello, $name!")
52 | }
53 | }
54 |
55 | /**
56 | * Creates the components used by the example API.
57 | */
58 | fun createComponents(): ExampleComponents = ExampleComponentsImpl()
59 |
60 | /**
61 | * A trivial set of components that exposes a simple property to the request handling code in the API definition.
62 | */
63 | interface ExampleComponents : ComponentsProvider {
64 | val name: String
65 | }
66 |
67 | /**
68 | * An implementation of `ExampleComponents` that uses an environment variable to populate its data.
69 | */
70 | class ExampleComponentsImpl : ExampleComponents {
71 | override val name: String = System.getenv(EXAMPLE_ENVIRONMENT_VARIABLE) ?:
72 | "[Environment variable EXAMPLE_ENVIRONMENT_VARIABLE not set]"
73 | }
74 |
--------------------------------------------------------------------------------
/core/src/test/kotlin/ws/osiris/core/ContentTypeTest.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.core
2 |
3 | import org.testng.annotations.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertNull
6 |
7 | @Test
8 | class ContentTypeTest {
9 |
10 | fun parseSimple() {
11 | val (mimeType, charset) = ContentType.parse("text/plain; charset=UTF-8")
12 | assertEquals("text/plain", mimeType)
13 | assertEquals(Charsets.UTF_8, charset)
14 | }
15 |
16 | fun parseWithWhitespace() {
17 | val (mimeType, charset) = ContentType.parse(" text/plain ; charset=UTF-8 ")
18 | assertEquals("text/plain", mimeType)
19 | assertEquals(Charsets.UTF_8, charset)
20 | }
21 |
22 | fun parseNoWhitespace() {
23 | val (mimeType, charset) = ContentType.parse("text/plain;charset=UTF-8")
24 | assertEquals("text/plain", mimeType)
25 | assertEquals(Charsets.UTF_8, charset)
26 | }
27 |
28 | fun parseUppercaseCharset() {
29 | val (mimeType, charset) = ContentType.parse("text/plain; CHARSET=UTF-8")
30 | assertEquals("text/plain", mimeType)
31 | assertEquals(Charsets.UTF_8, charset)
32 | }
33 |
34 | fun parseNoCharset() {
35 | val (mimeType, charset) = ContentType.parse("text/plain")
36 | assertEquals("text/plain", mimeType)
37 | assertNull(charset)
38 | }
39 |
40 | fun header() {
41 | assertEquals("text/plain; charset=UTF-8", ContentType("text/plain", Charsets.UTF_8).header)
42 | }
43 |
44 | fun headerNoCharset() {
45 | assertEquals("text/plain", ContentType("text/plain").header)
46 | assertEquals("text/plain", ContentType(" text/plain ").header)
47 | }
48 |
49 | fun multipartForm() {
50 | val contentType = ContentType.parse("multipart/form-data;boundary=abc123")
51 | assertEquals(contentType.mimeType, "multipart/form-data")
52 | assertEquals(contentType.header, "multipart/form-data; boundary=abc123")
53 | assertEquals(contentType.boundary, "abc123")
54 | assertNull(contentType.charset)
55 | }
56 |
57 | fun multipartFormWithWhitespace() {
58 | val contentType = ContentType.parse(" multipart/form-data ; boundary=abc123 ")
59 | assertEquals(contentType.mimeType, "multipart/form-data")
60 | assertEquals(contentType.header, "multipart/form-data; boundary=abc123")
61 | assertEquals(contentType.boundary, "abc123")
62 | assertNull(contentType.charset)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/gradle-plugin/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.gradle.plugin-publish' version '0.11.0'
3 | id 'org.jetbrains.kotlin.jvm' version '1.3.31'
4 | // For publishing to local maven repo during development
5 | id 'maven-publish'
6 | }
7 |
8 | apply plugin: 'maven'
9 |
10 | group = 'ws.osiris'
11 | version = '2.0.0'
12 |
13 | ext {
14 | kotlinVersion = '1.9.22'
15 | osirisVersion = '2.0.0'
16 | }
17 |
18 | repositories {
19 | mavenLocal()
20 | jcenter()
21 | }
22 |
23 | dependencies {
24 | implementation gradleApi()
25 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
26 | implementation "ws.osiris:osiris-archetype:$osirisVersion"
27 | implementation "ws.osiris:osiris-aws:$osirisVersion"
28 | implementation "ws.osiris:osiris-aws-deploy:$osirisVersion"
29 | }
30 |
31 | compileKotlin {
32 | kotlinOptions {
33 | jvmTarget = '11'
34 | }
35 | }
36 |
37 | compileTestKotlin {
38 | kotlinOptions {
39 | jvmTarget = '11'
40 | }
41 | }
42 |
43 | pluginBundle {
44 | website = 'http://www.osiris.ws/'
45 | vcsUrl = 'https://github.com/cjkent/osiris'
46 | description = 'Plugins for the Osiris Project'
47 | tags = ['osiris', 'aws', 'serverless', 'apigateway', 'lambda', 'web']
48 |
49 | plugins {
50 | projectPlugin {
51 | id = 'ws.osiris.project'
52 | displayName = 'Osiris Project Plugin'
53 | description = 'Generates a project for building an application with Osiris'
54 | }
55 | deployPlugin {
56 | id = 'ws.osiris.deploy'
57 | displayName = 'Osiris Deploy Plugin'
58 | description = 'Builds the artifacts for an Osiris application, generates the configuration and deploys to AWS'
59 | }
60 | }
61 | }
62 |
63 | // Publishes the plugin to the local Maven repo during development
64 | // Use: gradlew publishToMavenLocal
65 | // Consuming projects will need to apply plugins using the old style buildscript syntax (core module):
66 | //
67 | // buildscript {
68 | // repositories {
69 | // jcenter()
70 | // mavenLocal()
71 | // }
72 | // dependencies {
73 | // classpath 'ws.osiris:osiris-gradle-plugin:2.0.0'
74 | // }
75 | // }
76 | //
77 | // apply plugin: 'ws.osiris.deploy'
78 | //
79 | // The root build.gradle in the consuming project will need mavenLocal() adding to the repositories block
80 |
81 | publishing {
82 | publications {
83 | maven(MavenPublication) {
84 | groupId = 'ws.osiris'
85 | artifactId = 'osiris-gradle-plugin'
86 | version = '2.0.0'
87 | from components.java
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/core/src/main/kotlin/ws/osiris/core/Route.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.core
2 |
3 | /**
4 | * A route describes one endpoint in a REST API.
5 | *
6 | * A route contains
7 | *
8 | * * The HTTP method it accepts, for example GET or POST
9 | * * The path to the endpoint, for example `/foo/bar`
10 | * * The authorisation needed to invoke the endpoint
11 | */
12 | sealed class Route {
13 |
14 | abstract val path: String
15 | abstract val auth: Auth
16 |
17 | companion object {
18 |
19 | // TODO read the RFC in case there are any I've missed
20 | internal fun validatePath(path: String) {
21 | if (!pathPattern.matcher(path).matches()) throw IllegalArgumentException("Illegal path $path")
22 | }
23 | }
24 | }
25 |
26 | /**
27 | * Describes an endpoint in a REST API whose requests are handled by a lambda.
28 | *
29 | * It contains
30 | *
31 | * * The HTTP method it accepts, for example GET or POST
32 | * * The path to the endpoint, for example `/foo/bar`
33 | * * The code that is run when the endpoint receives a request (the "handler")
34 | * * The authorisation needed to invoke the endpoint
35 | */
36 | data class LambdaRoute(
37 | val method: HttpMethod,
38 | override val path: String,
39 | val handler: RequestHandler,
40 | override val auth: Auth = NoAuth,
41 | val cors: Boolean = false
42 | ): Route() {
43 |
44 | init {
45 | validatePath(path)
46 | }
47 |
48 | internal fun wrap(filters: List>): LambdaRoute {
49 | val chain = filters.reversed().fold(handler, { requestHandler, filter -> wrapFilter(requestHandler, filter) })
50 | return copy(handler = chain)
51 | }
52 |
53 | private fun wrapFilter(handler: RequestHandler, filter: Filter): RequestHandler {
54 | return { req ->
55 | val returnVal = when {
56 | filter.matches(req) -> filter.handler(this, req, handler)
57 | else -> handler(this, req)
58 | }
59 | returnVal as? Response ?: req.responseBuilder().build(returnVal)
60 | }
61 | }
62 | }
63 |
64 | /**
65 | * Describes an endpoint in a REST API that serves static files.
66 | *
67 | * It contains
68 | *
69 | * * The path to the endpoint, for example `/foo/bar`
70 | * * The authorisation needed to invoke the endpoint
71 | * * The name of the index file that will be served if no file is included in the URL
72 | */
73 | data class StaticRoute(
74 | override val path: String,
75 | val indexFile: String?,
76 | override val auth: Auth = NoAuth
77 | ) : Route() {
78 |
79 | init {
80 | validatePath(path)
81 | // TODO validate the method and index file
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/server/src/test/kotlin/ws/osiris/server/HttpTestClient.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.server
2 |
3 | import okhttp3.MediaType.Companion.toMediaType
4 | import okhttp3.OkHttpClient
5 | import okhttp3.Request
6 | import okhttp3.RequestBody.Companion.toRequestBody
7 | import org.slf4j.LoggerFactory
8 | import ws.osiris.core.Headers
9 | import ws.osiris.core.HttpHeaders
10 | import ws.osiris.core.TestClient
11 | import ws.osiris.core.TestResponse
12 |
13 | typealias OkHttpResponse = okhttp3.Response
14 |
15 | /**
16 | * The protocol used when making a request.
17 | */
18 | enum class Protocol(val protocolName: String, val defaultPort: Int) {
19 | HTTP("http", 80),
20 | HTTPS("https", 443)
21 | }
22 |
23 | /**
24 | * A very simple implementation of [TestClient] that makes HTTP or HTTPS requests.
25 | */
26 | class HttpTestClient(
27 | private val protocol: Protocol,
28 | private val server: String,
29 | private val port: Int = protocol.defaultPort,
30 | private val basePath: String = ""
31 | ) : TestClient {
32 |
33 | private val client = OkHttpClient()
34 |
35 | override fun get(path: String, headers: Map): TestResponse {
36 | val request = Request.Builder().url(buildPath(path)).build()
37 | log.debug("Making request {}", request)
38 | val response = client.newCall(request).execute()
39 | val testResponse = TestResponse(response.code, response.headerMap(), response.body?.string())
40 | log.debug("Received response {}", testResponse)
41 | return testResponse
42 | }
43 |
44 | override fun post(path: String, body: String, headers: Map): TestResponse {
45 | val mediaType = headers[HttpHeaders.CONTENT_TYPE]?.toMediaType()
46 | val requestBody = body.toRequestBody(mediaType)
47 | val request = Request.Builder().url(buildPath(path)).post(requestBody).build()
48 | log.debug("Making request {}", request)
49 | val response = client.newCall(request).execute()
50 | val testResponse = TestResponse(response.code, response.headerMap(), response.body?.string())
51 | log.debug("Received response {}", testResponse)
52 | return testResponse
53 | }
54 |
55 | override fun options(path: String, headers: Map): TestResponse {
56 | val request = Request.Builder().url(buildPath(path)).method("OPTIONS", null).build()
57 | log.debug("Making request {}", request)
58 | val response = client.newCall(request).execute()
59 | val testResponse = TestResponse(response.code, response.headerMap(), null)
60 | log.debug("Received response {}", testResponse)
61 | return testResponse
62 | }
63 |
64 | private fun buildPath(path: String): String = "${protocol.protocolName}://$server:$port$basePath$path"
65 |
66 | private fun OkHttpResponse.headerMap(): Headers =
67 | Headers(this.headers.toMultimap().mapValues { (_, list) -> list[0] })
68 |
69 | companion object {
70 | private val log = LoggerFactory.getLogger(HttpTestClient::class.java)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/META-INF/maven/archetype-metadata.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 | 1.0.0-SNAPSHOT
12 |
13 |
14 | ${project.version}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | src/main/kotlin
23 |
24 | **/*.kt
25 |
26 |
27 |
28 | src/main/cloudformation
29 |
30 | **/*.*
31 |
32 |
33 |
34 | src/main/static
35 |
36 |
37 | src/assembly
38 |
39 | **/*.xml
40 |
41 |
42 |
43 | src/main/resources
44 |
45 | **/*.xml
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | src/main/kotlin
54 |
55 | **/*.kt
56 |
57 |
58 |
59 | src/main/resources
60 |
61 | **/*.xml
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/local-server/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | ${groupId}
8 | ${rootArtifactId}
9 | ${version}
10 | ..
11 |
12 | ${artifactId}
13 | jar
14 |
15 |
16 |
17 | org.jetbrains.kotlin
18 | kotlin-stdlib
19 |
20 |
21 | ${groupId}
22 | ${rootArtifactId}-core
23 |
24 |
25 | com.amazonaws
26 | aws-lambda-java-log4j2
27 |
28 |
29 |
30 |
31 | ws.osiris
32 | osiris-local-server
33 |
34 |
35 | org.apache.logging.log4j
36 | log4j-core
37 |
38 |
39 | org.apache.logging.log4j
40 | log4j-slf4j2-impl
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | org.codehaus.mojo
49 | exec-maven-plugin
50 | 1.5.0
51 |
52 |
53 |
54 | exec
55 |
56 |
57 |
58 |
59 | java
60 |
61 | -classpath
62 |
63 | ${package}.localserver.MainKt
64 | -p
65 | 8080
66 |
67 |
68 | Bob
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/aws-deploy/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | ws.osiris
6 | osiris
7 | 2.0.1-SNAPSHOT
8 |
9 |
10 | 4.0.0
11 | osiris-aws-deploy
12 | jar
13 | Osiris AWS Deploy
14 |
15 |
16 |
17 | ws.osiris
18 | osiris-core
19 |
20 |
21 | ws.osiris
22 | osiris-aws
23 |
24 |
25 | org.jetbrains.kotlin
26 | kotlin-stdlib
27 |
28 |
29 | org.slf4j
30 | slf4j-api
31 |
32 |
33 |
34 |
35 | com.amazonaws
36 | aws-java-sdk-lambda
37 |
38 |
39 | com.amazonaws
40 | aws-java-sdk-api-gateway
41 |
42 |
43 | com.amazonaws
44 | aws-java-sdk-sts
45 |
46 |
47 | com.amazonaws
48 | aws-java-sdk-iam
49 |
50 |
51 | com.amazonaws
52 | aws-java-sdk-s3
53 |
54 |
55 | com.amazonaws
56 | aws-java-sdk-cloudformation
57 |
58 |
59 |
60 | com.google.guava
61 | guava
62 |
63 |
64 | com.fasterxml.jackson.core
65 | jackson-databind
66 |
67 |
68 | com.fasterxml.jackson.dataformat
69 | jackson-dataformat-yaml
70 |
71 |
72 |
73 |
74 | org.jetbrains.kotlin
75 | kotlin-test
76 |
77 |
78 | org.testng
79 | testng
80 | test
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/aws/src/test/kotlin/ws/osiris/aws/ConfigTest.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.aws
2 |
3 | import org.testng.Assert.assertThrows
4 | import org.testng.annotations.Test
5 |
6 | @Test
7 | class ConfigTest {
8 |
9 | fun validateBucketNameValidName() {
10 | validateBucketName("foo-bar-baz")
11 | }
12 |
13 | fun validateBucketNameTooLong() {
14 | assertThrows(IllegalArgumentException::class.java) {
15 | validateBucketName("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")
16 | }
17 | }
18 |
19 | fun validateBucketNameTooShort() {
20 | assertThrows(IllegalArgumentException::class.java) {
21 | validateBucketName("ab")
22 | }
23 | }
24 |
25 | fun validateBucketNameUpperCase() {
26 | assertThrows(IllegalArgumentException::class.java) {
27 | validateBucketName("Foo-bar-baz")
28 | }
29 | }
30 |
31 | fun validateBucketNameIllegalChars() {
32 | assertThrows(IllegalArgumentException::class.java) {
33 | validateBucketName("foo-bar-!baz")
34 | }
35 | }
36 |
37 | fun validateBucketNameStartsOrEndsWithDash() {
38 | assertThrows(IllegalArgumentException::class.java) {
39 | validateBucketName("-foo-bar-baz")
40 | }
41 | assertThrows(IllegalArgumentException::class.java) {
42 | validateBucketName("foo-bar-baz-")
43 | }
44 | }
45 |
46 | fun configInvalidStaticFilesBucket() {
47 | assertThrows(IllegalArgumentException::class.java) {
48 | ApplicationConfig(
49 | staticFilesBucket = "UpperCaseName",
50 | applicationName = "notUsed",
51 | stages = listOf(
52 | Stage(
53 | name = "test",
54 | deployOnUpdate = false
55 | )
56 | )
57 | )
58 | }
59 | }
60 |
61 | fun configInvalidAppName() {
62 | assertThrows(IllegalArgumentException::class.java) {
63 | ApplicationConfig(
64 | applicationName = "not valid",
65 | stages = listOf(
66 | Stage(
67 | name = "test",
68 | deployOnUpdate = false
69 | )
70 | )
71 | )
72 | }
73 | }
74 |
75 | fun configInvalidStaticCodeBucket() {
76 | assertThrows(IllegalArgumentException::class.java) {
77 | ApplicationConfig(
78 | codeBucket = "UpperCaseName",
79 | applicationName = "notUsed",
80 | stages = listOf(
81 | Stage(
82 | name = "test",
83 | deployOnUpdate = false
84 | )
85 | )
86 | )
87 | }
88 | }
89 |
90 | fun validateNameValidName() {
91 | validateName("Foo-Bar")
92 | }
93 |
94 | fun validateNameIllegalChars() {
95 | assertThrows(IllegalArgumentException::class.java) {
96 | validateName("Foo_Bar")
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/aws/src/main/kotlin/ws/osiris/aws/KeepAlive.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.aws
2 |
3 | import com.amazonaws.services.lambda.AWSLambda
4 | import com.amazonaws.services.lambda.AWSLambdaClientBuilder
5 | import com.amazonaws.services.lambda.model.InvocationType
6 | import com.amazonaws.services.lambda.model.InvokeRequest
7 | import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent
8 | import com.google.gson.Gson
9 | import org.slf4j.LoggerFactory
10 | import java.nio.ByteBuffer
11 | import kotlin.math.pow
12 |
13 | /**
14 | * The number of retries if the keep-alive call fails with access denied.
15 | *
16 | * Retrying is necessary because policy updates aren't visible immediately after the stack is updated.
17 | */
18 | private const val RETRIES = 7
19 |
20 | /**
21 | * Lambda that invokes other lambdas with keep-alive messages.
22 | *
23 | * It is triggered by a CloudWatch event containing the ARN of the lambda to keep alive and
24 | * the number of instances that should be kept alive.
25 | */
26 | class KeepAliveLambda {
27 |
28 | private val lambdaClient: AWSLambda = AWSLambdaClientBuilder.defaultClient()
29 | private val gson: Gson = Gson()
30 |
31 | fun handle(trigger: KeepAliveTrigger) {
32 | log.debug("Triggering keep-alive, count: {}, function: {}", trigger.instanceCount, trigger.functionArn)
33 | // The lambda expects a request from API Gateway of this type - this fakes it
34 | val requestEvent = APIGatewayProxyRequestEvent().apply {
35 | resource = KEEP_ALIVE_RESOURCE
36 | headers = mapOf(KEEP_ALIVE_SLEEP to trigger.sleepTimeMs.toString())
37 | }
38 | val json = gson.toJson(requestEvent)
39 | val payloadBuffer = ByteBuffer.wrap(json.toByteArray())
40 | val invokeRequest = InvokeRequest().apply {
41 | functionName = trigger.functionArn
42 | invocationType = InvocationType.Event.name
43 | payload = payloadBuffer
44 | }
45 |
46 | /**
47 | * Invokes multiple copies of the function, retrying if access is denied.
48 | *
49 | * The retry is necessary because policy updates aren't visible immediately after the stack is updated.
50 | */
51 | tailrec fun invokeFunctions(attemptCount: Int = 1) {
52 | try {
53 | repeat(trigger.instanceCount) {
54 | lambdaClient.invoke(invokeRequest)
55 | }
56 | log.debug("Keep-alive complete")
57 | return
58 | } catch (e: Exception) {
59 | if (attemptCount == RETRIES) throw e
60 | // Back off retrying - sleep for 2, 4, 8, 16, ...
61 | val sleep = 1000L * (2.0.pow(attemptCount)).toLong()
62 | log.debug("Exception triggering keep-alive: {} {}, sleeping for {}ms", e.javaClass.name, e.message, sleep)
63 | Thread.sleep(sleep)
64 | }
65 | invokeFunctions(attemptCount + 1)
66 | }
67 | invokeFunctions()
68 | }
69 |
70 | companion object {
71 | private val log = LoggerFactory.getLogger(KeepAliveLambda::class.java)
72 | }
73 | }
74 |
75 | /**
76 | * Message sent from the CloudWatch event to trigger keep-alive calls.
77 | */
78 | class KeepAliveTrigger(var functionArn: String = "", var instanceCount: Int = 0, var sleepTimeMs: Int = 200)
79 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | ${groupId}
7 | ${artifactId}
8 | 1.0.0-SNAPSHOT
9 | pom
10 |
11 |
12 | ${osirisVersion}
13 | 1.9.22
14 |
15 |
16 |
17 | core
18 | local-server
19 |
20 |
21 |
22 |
23 |
24 | ${groupId}
25 | ${artifactId}-core
26 | ${project.version}
27 |
28 |
29 | ws.osiris
30 | osiris-bom
31 | ${osiris.version}
32 | pom
33 | import
34 |
35 |
36 |
37 |
38 |
39 | src/main/kotlin
40 | src/test/kotlin
41 |
42 |
43 | org.jetbrains.kotlin
44 | kotlin-maven-plugin
45 | ${kotlin.version}
46 |
47 |
48 | compile
49 | compile
50 |
51 | compile
52 |
53 |
54 |
55 | test-compile
56 | test-compile
57 |
58 | test-compile
59 |
60 |
61 |
62 |
63 | 11
64 |
65 | src/main/kotlin
66 | ${project.build.directory}/generated-sources/osiris
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | maven-deploy-plugin
76 |
77 | true
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/server/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | ws.osiris
8 | osiris
9 | 2.0.1-SNAPSHOT
10 | ..
11 |
12 | osiris-server
13 | jar
14 | Osiris Server
15 | Shared utilities for the different servers
16 |
17 |
18 |
19 | ws.osiris
20 | osiris-core
21 |
22 |
23 | org.jetbrains.kotlin
24 | kotlin-stdlib
25 |
26 |
27 | org.slf4j
28 | slf4j-api
29 |
30 |
31 |
32 |
33 | org.testng
34 | testng
35 | test
36 |
37 |
38 | org.jetbrains.kotlin
39 | kotlin-test
40 | test
41 |
42 |
43 | com.squareup.okhttp3
44 | okhttp
45 | test
46 |
47 |
48 | ws.osiris
49 | osiris-core
50 | test-jar
51 | test
52 |
53 |
54 |
55 |
56 |
57 |
58 | org.apache.maven.plugins
59 | maven-jar-plugin
60 |
61 |
62 |
63 | test-jar
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | release
74 |
75 |
76 |
77 | org.apache.maven.plugins
78 | maven-jar-plugin
79 |
80 |
81 |
82 | test-jar
83 |
84 |
85 |
86 |
87 | true
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/bom/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | ws.osiris
8 | osiris
9 | 2.0.1-SNAPSHOT
10 | ..
11 |
12 |
13 | osiris-bom
14 | pom
15 | Osiris BOM
16 | Maven BOM containing dependencies shared by Osiris and projects built using Osiris
17 |
18 |
19 |
20 |
21 | ws.osiris
22 | osiris-core
23 | ${project.version}
24 |
25 |
26 | ws.osiris
27 | osiris-local-server
28 | ${project.version}
29 |
30 |
31 | ws.osiris
32 | osiris-aws
33 | ${project.version}
34 |
35 |
36 | org.jetbrains.kotlin
37 | kotlin-stdlib
38 | ${kotlin.version}
39 |
40 |
41 | com.google.code.gson
42 | gson
43 | ${gson.version}
44 |
45 |
46 | org.slf4j
47 | slf4j-api
48 | ${slf4j.version}
49 |
50 |
51 | org.apache.logging.log4j
52 | log4j-slf4j2-impl
53 | ${log4j.version}
54 |
55 |
56 | com.amazonaws
57 | aws-lambda-java-log4j2
58 | ${aws-lambda-java-log4j2.version}
59 |
60 |
61 | org.apache.logging.log4j
62 | log4j-core
63 | ${log4j.version}
64 |
65 |
66 | com.amazonaws
67 | aws-java-sdk-bom
68 | ${aws-java-sdk.version}
69 | pom
70 | import
71 |
72 |
73 | com.amazonaws
74 | aws-lambda-java-events
75 | ${aws-lambda-java-events.version}
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/core/src/main/cloudformation/root.template:
--------------------------------------------------------------------------------
1 | #set($region = '${AWS::Region}')
2 | Resources:
3 |
4 | # This policy defines the permissions given to the application code.
5 | # By default it is allowed to write to CloudWatch logs and nothing else.
6 | # If the application needs access to any other resources, for example a database or message queue, then
7 | # the permissions should be added to this policy.
8 | #
9 | # The DynamoDB example project shows how to add permissions for accessing DynamoDB database tables:
10 | # https://github.com/cjkent/osiris-examples/blob/master/dynamodb/core/src/main/cloudformation/root.template#L17
11 | #
12 | # See the AWS documentation for more details:
13 | # https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
14 | #
15 | LambdaPolicy:
16 | Type: AWS::IAM::Policy
17 | Properties:
18 | Roles:
19 | - !Ref LambdaRole
20 | PolicyName: LambdaPolicy
21 | PolicyDocument:
22 | Version: 2012-10-17
23 | Statement:
24 | - Effect: Allow
25 | Action:
26 | - "logs:*"
27 | Resource: "arn:aws:logs:*:*:*"
28 |
29 | # This role is used by the lambda function containing the application logic.
30 | # It should not be necessary to modify it except to delete ManagedPolicyArns if it is not required.
31 | LambdaRole:
32 | Type: AWS::IAM::Role
33 | Properties:
34 | AssumeRolePolicyDocument:
35 | Version: 2012-10-17
36 | Statement:
37 | - Effect: Allow
38 | Principal:
39 | Service:
40 | - lambda.amazonaws.com
41 | Action: sts:AssumeRole
42 | # This is required for running the application in a VPC. It can be deleted if VPC access is not required.
43 | ManagedPolicyArns:
44 | - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
45 |
46 | # This references the CloudFormation file generated by Osiris. This defines the REST API in API Gateway and the
47 | # lambda that contains the application logic.
48 | #
49 | # It should not be necessary to edit it apart from adding parameters. The LambdaRole parameter must not be removed
50 | # or changed.
51 | #
52 | # There are three reasons to add parameters:
53 | #
54 | # 1) When running the application in a VPC the subnet IDs and security group IDs must be provided.
55 | # See here for details: https://github.com/cjkent/osiris/wiki/Deploying-to-a-VPC
56 | #
57 | # 2) When you are using Cognito and the user pool is defined in this file, you must pass the
58 | # ARN of the user pool to ApiStack in a parameter called CognitoUserPoolArn.
59 | # If your user pool is defined in a resource called UserPool then it will look like this:
60 | #
61 | # Parameters:
62 | # LambdaRole: !GetAtt LambdaRole.Arn
63 | # CognitoUserPoolArn: !GetAtt UserPool.Arn
64 | #
65 | # See here for details: https://github.com/cjkent/osiris/wiki/Authorization#cognito-user-pools
66 | #
67 | # 3) If you define resources in this file and need to pass values into the application code.
68 | # Any parameters defined here are automatically passed to the application code as environment variables.
69 | #
70 | ApiStack:
71 | Type: AWS::CloudFormation::Stack
72 | Properties:
73 | TemplateURL: !Sub "https://${codeS3Bucket}.s3.${region}.amazonaws.com/${rootArtifactId}.template"
74 | Parameters:
75 | LambdaRole: !GetAtt LambdaRole.Arn
76 |
--------------------------------------------------------------------------------
/core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | ws.osiris
8 | osiris
9 | 2.0.1-SNAPSHOT
10 | ..
11 |
12 |
13 | osiris-core
14 | jar
15 | Osiris Core
16 | Public API
17 |
18 |
19 |
20 | org.jetbrains.kotlin
21 | kotlin-stdlib
22 |
23 |
24 | org.slf4j
25 | slf4j-api
26 |
27 |
28 | com.google.code.gson
29 | gson
30 |
31 |
32 |
33 |
34 | org.testng
35 | testng
36 | test
37 |
38 |
39 | org.jetbrains.kotlin
40 | kotlin-test
41 | test
42 |
43 |
44 | org.apache.logging.log4j
45 | log4j-core
46 | test
47 |
48 |
49 | org.jetbrains.kotlin
50 | kotlin-reflect
51 | test
52 |
53 |
54 | com.fasterxml.jackson.module
55 | jackson-module-kotlin
56 | test
57 |
58 |
59 |
60 |
61 |
62 |
63 | org.apache.maven.plugins
64 | maven-jar-plugin
65 |
66 |
67 |
68 | test-jar
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | release
79 |
80 |
81 |
82 | org.apache.maven.plugins
83 | maven-jar-plugin
84 |
85 |
86 |
87 | test-jar
88 |
89 |
90 |
91 |
92 | true
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/aws-deploy/src/test/kotlin/ws/osiris/aws/cloudformation/TemplateTest.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.aws.cloudformation
2 |
3 | import org.intellij.lang.annotations.Language
4 | import org.testng.annotations.Test
5 | import ws.osiris.aws.ApplicationConfig
6 | import ws.osiris.aws.Stage
7 | import ws.osiris.awsdeploy.cloudformation.ApiTemplate
8 | import ws.osiris.awsdeploy.cloudformation.ResourceTemplate
9 | import ws.osiris.awsdeploy.cloudformation.Templates
10 | import ws.osiris.core.ComponentsProvider
11 | import ws.osiris.core.api
12 | import java.io.StringWriter
13 | import java.time.Duration
14 | import kotlin.test.assertEquals
15 |
16 | @Test
17 | class TemplateTest {
18 |
19 | fun apiTemplateOnly() {
20 | val apiTemplate = ApiTemplate(
21 | ResourceTemplate(listOf(), listOf(), "", "", true),
22 | "foo",
23 | "desc",
24 | null,
25 | setOf()
26 | )
27 | val writer = StringWriter()
28 | apiTemplate.write(writer)
29 | @Language("yaml")
30 | val expected = """
31 | |
32 | | Api:
33 | | Type: AWS::ApiGateway::RestApi
34 | | Properties:
35 | | Name: "foo"
36 | | Description: "desc"
37 | | FailOnWarnings: true
38 | | BinaryMediaTypes: []
39 | |
40 | | CloudWatchRole:
41 | | Type: AWS::IAM::Role
42 | | Properties:
43 | | AssumeRolePolicyDocument:
44 | | Version: 2012-10-17
45 | | Statement:
46 | | - Effect: Allow
47 | | Principal:
48 | | Service:
49 | | - apigateway.amazonaws.com
50 | | Action: sts:AssumeRole
51 | | ManagedPolicyArns:
52 | | - arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs
53 | |
54 | | ApiGatewayAccount:
55 | | Type: AWS::ApiGateway::Account
56 | | Properties:
57 | | CloudWatchRoleArn: !GetAtt CloudWatchRole.Arn
58 | """.trimMargin()
59 | assertEquals(expected, writer.toString())
60 | }
61 |
62 | fun createFromApi() {
63 | val api = api {
64 | get("/") { }
65 | get("/foo") { }
66 | get("/foo/bar") { }
67 | get("/baz") { }
68 | staticFiles {
69 | path = "/public"
70 | indexFile = "index.html"
71 | }
72 | }
73 | val config = ApplicationConfig(
74 | applicationName = "my-application",
75 | lambdaMemorySizeMb = 512,
76 | lambdaTimeout = Duration.ofSeconds(10),
77 | environmentVariables = mapOf(
78 | "FOO" to "foo value",
79 | "BAR" to "bar value"
80 | ),
81 | stages = listOf(
82 | Stage(
83 | name = "dev",
84 | deployOnUpdate = true,
85 | variables = mapOf("STAGE_VAR" to "dev value")
86 | ),
87 | Stage(
88 | name = "prod",
89 | deployOnUpdate = false,
90 | variables = mapOf("STAGE_VAR" to "prod value")
91 | )
92 | )
93 | )
94 | Templates.create(
95 | api,
96 | config,
97 | setOf("UserParam1", "UserParam2"),
98 | "com.example.GeneratedLambda",
99 | "testHash",
100 | "staticHash",
101 | "testApi.code",
102 | "testApi.jar",
103 | "dev",
104 | "12345678"
105 | )
106 | // TODO assertions
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/archetype/src/main/resources/archetype-resources/core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | ${groupId}
8 | ${rootArtifactId}
9 | ${version}
10 | ..
11 |
12 | ${artifactId}
13 | jar
14 |
15 |
16 |
17 | ws.osiris
18 | osiris-core
19 |
20 |
21 | ws.osiris
22 | osiris-aws
23 |
24 |
25 | org.jetbrains.kotlin
26 | kotlin-stdlib
27 |
28 |
29 | com.google.code.gson
30 | gson
31 |
32 |
33 | org.apache.logging.log4j
34 | log4j-slf4j2-impl
35 |
36 |
37 | com.amazonaws
38 | aws-lambda-java-log4j2
39 |
40 |
41 |
42 |
43 | src/main/kotlin
44 | src/test/kotlin
45 |
46 |
50 |
51 | maven-assembly-plugin
52 | 3.0.0
53 |
54 |
55 | ${project.basedir}/src/assembly/dist.xml
56 |
57 |
58 |
59 |
60 | make-assembly
61 | package
62 |
63 | single
64 |
65 |
66 |
67 |
68 |
69 |
70 | ws.osiris
71 | osiris-maven-plugin
72 | ${osiris.version}
73 |
74 | ${package}
75 |
76 |
77 |
78 | generate-cloudformation
79 |
80 | generate-cloudformation
81 |
82 |
83 |
84 |
85 | deploy
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/aws-deploy/src/main/kotlin/ws/osiris/awsdeploy/Profile.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.awsdeploy
2 |
3 | import com.amazonaws.ClientConfiguration
4 | import com.amazonaws.auth.AWSCredentialsProvider
5 | import com.amazonaws.auth.DefaultAWSCredentialsProviderChain
6 | import com.amazonaws.auth.profile.ProfileCredentialsProvider
7 | import com.amazonaws.regions.AwsProfileRegionProvider
8 | import com.amazonaws.regions.DefaultAwsRegionProviderChain
9 | import com.amazonaws.services.apigateway.AmazonApiGateway
10 | import com.amazonaws.services.apigateway.AmazonApiGatewayClientBuilder
11 | import com.amazonaws.services.cloudformation.AmazonCloudFormation
12 | import com.amazonaws.services.cloudformation.AmazonCloudFormationClientBuilder
13 | import com.amazonaws.services.lambda.AWSLambda
14 | import com.amazonaws.services.lambda.AWSLambdaClientBuilder
15 | import com.amazonaws.services.s3.AmazonS3
16 | import com.amazonaws.services.s3.AmazonS3ClientBuilder
17 | import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder
18 | import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest
19 |
20 | /**
21 | * Provides AWS clients and related values (region, account ID).
22 | */
23 | class AwsProfile private constructor(private val credentialsProvider: AWSCredentialsProvider, val region: String) {
24 |
25 | private val clientConfiguration = ClientConfiguration()
26 | .withSocketTimeout(120_000)
27 | .withConnectionTimeout(20_000)
28 | .withMaxErrorRetry(20)
29 |
30 | val s3Client: AmazonS3 by lazy {
31 | AmazonS3ClientBuilder.standard()
32 | .withCredentials(credentialsProvider)
33 | .withRegion(region)
34 | .withClientConfiguration(clientConfiguration)
35 | .build()
36 | }
37 |
38 | val apiGatewayClient: AmazonApiGateway by lazy {
39 | AmazonApiGatewayClientBuilder.standard()
40 | .withCredentials(credentialsProvider)
41 | .withRegion(region)
42 | .withClientConfiguration(clientConfiguration)
43 | .build()
44 | }
45 |
46 | val cloudFormationClient: AmazonCloudFormation by lazy {
47 | AmazonCloudFormationClientBuilder.standard()
48 | .withCredentials(credentialsProvider)
49 | .withRegion(region)
50 | .withClientConfiguration(clientConfiguration)
51 | .build()
52 | }
53 |
54 | val lambdaClient: AWSLambda by lazy {
55 | AWSLambdaClientBuilder.standard()
56 | .withCredentials(credentialsProvider)
57 | .withRegion(region)
58 | .withClientConfiguration(clientConfiguration)
59 | .build()
60 | }
61 |
62 | val accountId: String by lazy {
63 | val stsClient = AWSSecurityTokenServiceClientBuilder.standard()
64 | .withCredentials(credentialsProvider)
65 | .withRegion(region)
66 | .build()
67 | stsClient.getCallerIdentity(GetCallerIdentityRequest()).account
68 | }
69 |
70 | companion object {
71 |
72 | /**
73 | * Returns a profile that gets the credentials and region using the default AWS mechanism.
74 | *
75 | * See [here](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default)
76 | * for details.
77 | */
78 | fun default(): AwsProfile = AwsProfile(
79 | DefaultAWSCredentialsProviderChain(),
80 | DefaultAwsRegionProviderChain().region
81 | )
82 |
83 | /**
84 | * Returns a profile that takes the credentials and region from a named profile in the AWS credentials and
85 | * config files.
86 | */
87 | fun named(profileName: String): AwsProfile = AwsProfile(
88 | ProfileCredentialsProvider(profileName),
89 | AwsProfileRegionProvider(profileName).region
90 | )
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/integration/src/main/kotlin/ws/osiris/integration/IntegrationTestApi.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.integration
2 |
3 | import com.google.gson.Gson
4 | import ws.osiris.core.ComponentsProvider
5 | import ws.osiris.core.DataNotFoundException
6 | import ws.osiris.core.ForbiddenException
7 | import ws.osiris.core.HttpHeaders
8 | import ws.osiris.core.MimeTypes
9 | import ws.osiris.core.api
10 |
11 | interface TestComponents : ComponentsProvider {
12 | val gson: Gson
13 | val name: String
14 | val size: Int
15 | }
16 |
17 | class TestComponentsImpl(override val name: String, override val size: Int) : TestComponents {
18 |
19 | constructor() : this("Bob", 42)
20 |
21 | override val gson: Gson = Gson()
22 | }
23 |
24 | /**
25 | * Simple class demonstrating automatic conversion to JSON.
26 | *
27 | * Produces JSON like:
28 | *
29 | * {"message":"hello, world!"}
30 | */
31 | internal data class JsonMessage(val message: String)
32 |
33 | /**
34 | * Simple class demonstrating creating an object from JSON in the request body.
35 | *
36 | * Expects JSON like:
37 | *
38 | * {"name":"Bob"}
39 | */
40 | internal data class JsonPayload(val name: String)
41 |
42 | /**
43 | * An API definition that can be deployed to AWS and have integration tests run against it.
44 | */
45 | val api = api {
46 |
47 | staticFiles {
48 | path = "/public"
49 | indexFile = "index.html"
50 | }
51 | get("/") {
52 | mapOf("message" to "hello, root!")
53 | }
54 | get("/helloworld") {
55 | // return a map that is automatically converted to JSON
56 | mapOf("message" to "hello, world!")
57 | }
58 | get("/helloplain") { req ->
59 | // return a response with customised headers
60 | req.responseBuilder()
61 | .header(HttpHeaders.CONTENT_TYPE, MimeTypes.TEXT_PLAIN)
62 | .build("hello, world!")
63 | }
64 | get("/hello/queryparam1") { req ->
65 | // get an optional query parameter
66 | val name = req.queryParams.optional("name") ?: "world"
67 | mapOf("message" to "hello, $name!")
68 | }
69 | get("/hello/queryparam2") { req ->
70 | // get a required query parameter
71 | val name = req.queryParams["name"]
72 | mapOf("message" to "hello, $name!")
73 | }
74 | // use path() to group multiple endpoints under the same sub-path
75 | path("/hello") {
76 | get("/{name}") { req ->
77 | // get a path parameter
78 | val name = req.pathParams["name"]
79 | // this will be automatically converted to a JSON object like {"message":"hello, Bob!"}
80 | JsonMessage("hello, $name!")
81 | }
82 | get("/env") {
83 | // use the name property from TestComponents for the name
84 | JsonMessage("hello, $name!")
85 | }
86 | }
87 | post("/foo") { req ->
88 | // expecting a JSON payload like {"name":"Bob"}
89 | val payload = gson.fromJson(req.body(), JsonPayload::class.java)
90 | // this will be automatically converted to a JSON object like {"message":"hello, Bob!"}
91 | JsonMessage("hello, ${payload.name}!")
92 | }
93 | // Endpoints demonstrating the mapping of exceptions to responses
94 | // Demonstrates mapping DataNotFoundException to a 404
95 | get("/foo/{fooId}") { req ->
96 | val fooId = req.pathParams["fooId"]
97 | when (fooId) {
98 | "123" -> JsonMessage("foo 123 found")
99 | else -> throw DataNotFoundException("No foo found with ID $fooId")
100 | }
101 | }
102 | // Status 403 (forbidden)
103 | get("/forbidden") {
104 | throw ForbiddenException("top secret")
105 | }
106 | // Status 500 (server error). Returned when there is not specific handler for the exception type
107 | get("/servererror") {
108 | throw RuntimeException("oh no!")
109 | }
110 | }
111 |
112 | /**
113 | * Creates the components used by the test API.
114 | */
115 | fun createComponents(): TestComponents = TestComponentsImpl()
116 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/ws/osiris/gradle/ProjectPlugin.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.gradle
2 |
3 | import org.gradle.api.DefaultTask
4 | import org.gradle.api.Plugin
5 | import org.gradle.api.Project
6 | import org.gradle.api.tasks.TaskAction
7 | import java.nio.file.Files
8 | import java.nio.file.Path
9 | import java.nio.file.Paths
10 |
11 | /**
12 | * Plugin to generate an empty Osiris project with build files and example code.
13 | */
14 | class OsirisProjectPlugin : Plugin {
15 |
16 | override fun apply(project: Project) {
17 | project.tasks.create("generateProject", OsirisProjectTask::class.java)
18 | }
19 | }
20 | /**
21 | * Task to generate an empty project.
22 | */
23 | open class OsirisProjectTask : DefaultTask() {
24 |
25 | @TaskAction
26 | fun generateProject() {
27 | checkPaths("core", "local-server")
28 | if (!project.hasProperty("package")) {
29 | throw IllegalStateException("Please specify the application package using '-Ppackage=...', " +
30 | "for example: '-Ppackage=com.example.application'")
31 | }
32 | val rootPackage = project.property("package") as String
33 | for (resource in resources) {
34 | val bytes = javaClass.getResourceAsStream(resource).use { it.readBytes() }
35 | val fileStr = String(bytes, Charsets.UTF_8)
36 | val replacedFile = fileStr
37 | .replace("\${package}", rootPackage)
38 | .replace("\${rootArtifactId}", project.name)
39 | .replace(Regex("""#set.*?\n"""), "")
40 | .replace("\${region}", "\${AWS::Region}")
41 | .replace("\${osirisVersion}", project.properties["osirisVersion"].toString())
42 | .replace("\${kotlinVersion}", project.properties["kotlinVersion"].toString())
43 | val projectDir = project.projectDir.toPath()
44 | val path = projectDir.resolve(insertPackageDirs(resource, rootPackage))
45 | Files.createDirectories(path.parent)
46 | logger.info("Writing file {} to {}", path.fileName, path.toAbsolutePath())
47 | Files.write(path, replacedFile.toByteArray(Charsets.UTF_8))
48 | }
49 | Files.createDirectories(project.projectDir.toPath().resolve("core/src/main/static"))
50 | }
51 |
52 | private fun insertPackageDirs(resource: String, rootPackage: String): Path {
53 | val match = packageRegex.find(resource) ?: return Paths.get(resource.substring("/archetype-resources/".length))
54 | val packagePath = rootPackage.replace('.', '/')
55 | val groupValues = match.groupValues
56 | val path = "${groupValues[1]}/src/main/kotlin/$packagePath/${groupValues[2]}/${groupValues[3]}"
57 | return Paths.get(path)
58 | }
59 |
60 | private fun checkPaths(vararg paths: String) {
61 | for (path in paths) {
62 | if (Files.exists(Paths.get(path))) {
63 | throw IllegalStateException("'$path' already exists, cannot create project")
64 | }
65 | }
66 | }
67 |
68 | companion object {
69 | private val resources: List = listOf(
70 | // these come from this project
71 | "/archetype-resources/settings.gradle",
72 | "/archetype-resources/build.gradle",
73 | "/archetype-resources/core/build.gradle",
74 | "/archetype-resources/local-server/build.gradle",
75 | // these come from the Maven archetype
76 | "/archetype-resources/core/src/main/resources/log4j2.xml",
77 | "/archetype-resources/core/src/main/kotlin/core/generated/Generated.kt",
78 | "/archetype-resources/core/src/main/kotlin/core/ApiDefinition.kt",
79 | "/archetype-resources/core/src/main/kotlin/core/Config.kt",
80 | "/archetype-resources/core/src/main/cloudformation/root.template",
81 | "/archetype-resources/local-server/src/main/resources/log4j2.xml",
82 | "/archetype-resources/local-server/src/main/kotlin/localserver/Main.kt"
83 | )
84 |
85 | private val packageRegex: Regex = Regex("/archetype-resources/(.+?)/src/main/kotlin/(.+?)/(.*)")
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/integration/src/test/kotlin/ws/osiris/integration/IntegrationTests.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.integration
2 |
3 | import com.google.gson.Gson
4 | import org.testng.annotations.Test
5 | import ws.osiris.core.ContentType
6 | import ws.osiris.core.HttpHeaders
7 | import ws.osiris.core.InMemoryTestClient
8 | import ws.osiris.core.JSON_CONTENT_TYPE
9 | import ws.osiris.core.MimeTypes
10 | import ws.osiris.core.TestClient
11 | import ws.osiris.localserver.LocalHttpTestClient
12 | import java.nio.file.Paths
13 | import kotlin.test.assertEquals
14 | import kotlin.test.assertTrue
15 |
16 | private val components: TestComponents = TestComponentsImpl("Bob", 42)
17 |
18 | private const val STATIC_DIR = "src/test/static"
19 |
20 | @Test
21 | class InMemoryIntegrationTest {
22 |
23 | fun testApiInMemory() {
24 | val client = InMemoryTestClient.create(components, api, Paths.get(STATIC_DIR))
25 | assertApi(client)
26 | }
27 | }
28 |
29 | @Test
30 | class LocalHttpIntegrationTest {
31 |
32 | fun testApiLocalHttpServer() {
33 | LocalHttpTestClient.create(api, components, STATIC_DIR).use { assertApi(it) }
34 | }
35 | }
36 |
37 | internal fun assertApi(client: TestClient) {
38 | val gson = Gson()
39 | fun Any?.parseJson(): Map<*, *> {
40 | val json = this as? String ?: throw IllegalArgumentException("Value is not a string: $this")
41 | return gson.fromJson(json, Map::class.java)
42 | }
43 | val rootResponse = client.get("/")
44 | assertEquals(mapOf("message" to "hello, root!"), rootResponse.body.parseJson())
45 | assertEquals(JSON_CONTENT_TYPE, ContentType.parse(rootResponse.headers[HttpHeaders.CONTENT_TYPE]!!))
46 | assertEquals(200, rootResponse.status)
47 |
48 | val response1 = client.get("/helloworld")
49 | assertEquals(mapOf("message" to "hello, world!"), response1.body.parseJson())
50 | assertEquals(JSON_CONTENT_TYPE, ContentType.parse(rootResponse.headers[HttpHeaders.CONTENT_TYPE]!!))
51 | assertEquals(200, response1.status)
52 |
53 | val response2 = client.get("/helloplain")
54 | assertEquals("hello, world!", response2.body)
55 | assertEquals(MimeTypes.TEXT_PLAIN, response2.headers[HttpHeaders.CONTENT_TYPE])
56 |
57 | assertEquals(mapOf("message" to "hello, world!"), client.get("/hello/queryparam1").body.parseJson())
58 | assertEquals(mapOf("message" to "hello, Alice!"), client.get("/hello/queryparam1?name=Alice").body.parseJson())
59 | assertEquals(mapOf("message" to "hello, Tom!"), client.get("/hello/queryparam2?name=Tom").body.parseJson())
60 | assertEquals(mapOf("message" to "hello, Peter!"), client.get("/hello/Peter").body.parseJson())
61 | assertEquals(mapOf("message" to "hello, Bob!"), client.get("/hello/env").body.parseJson())
62 | assertEquals(mapOf("message" to "hello, Max!"), client.post("/foo", "{\"name\":\"Max\"}").body.parseJson())
63 |
64 | val response3 = client.get("/hello/queryparam2")
65 | assertEquals(400, response3.status)
66 | assertEquals("No value named 'name'", response3.body)
67 |
68 | assertEquals(mapOf("message" to "foo 123 found"), client.get("/foo/123").body.parseJson())
69 | val response5 = client.get("/foo/234")
70 | assertEquals(404, response5.status)
71 | assertEquals("No foo found with ID 234", response5.body)
72 |
73 | val response6 = client.get("/servererror")
74 | assertEquals(500, response6.status)
75 | assertEquals("Server Error", response6.body)
76 |
77 | val response7 = client.get("/public")
78 | assertEquals(200, response7.status)
79 | val body7 = response7.body
80 | assertTrue(body7 is String && body7.contains("hello, world!"))
81 |
82 | val response8 = client.get("/public/")
83 | assertEquals(200, response8.status)
84 | val body8 = response8.body
85 | assertTrue(body8 is String && body8.contains("hello, world!"))
86 |
87 | val response9 = client.get("/public/index.html")
88 | assertEquals(200, response9.status)
89 | val body9 = response9.body
90 | assertTrue(body9 is String && body9.contains("hello, world!"))
91 |
92 | val response10 = client.get("/public/baz/bar.html")
93 | assertEquals(200, response10.status)
94 | val body10 = response10.body
95 | assertTrue(body10 is String && body10.contains("hello, bar!"))
96 | }
97 |
--------------------------------------------------------------------------------
/integration/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | osiris
7 | ws.osiris
8 | 2.0.1-SNAPSHOT
9 | ..
10 |
11 | 4.0.0
12 | osiris-integration
13 | Osiris Integration
14 | Integration tests
15 |
16 |
17 |
18 | ws.osiris
19 | osiris-core
20 |
21 |
22 | ws.osiris
23 | osiris-local-server
24 |
25 |
26 | ws.osiris
27 | osiris-aws
28 |
29 |
30 | com.google.code.gson
31 | gson
32 |
33 |
34 |
35 |
36 | org.testng
37 | testng
38 | test
39 |
40 |
41 | ws.osiris
42 | osiris-aws-deploy
43 | test
44 |
45 |
46 | ws.osiris
47 | osiris-core
48 | test-jar
49 | test
50 |
51 |
52 | ws.osiris
53 | osiris-server
54 | test-jar
55 | test
56 |
57 |
58 | ws.osiris
59 | osiris-local-server
60 | test-jar
61 | test
62 |
63 |
64 | com.squareup.okhttp3
65 | okhttp
66 | test
67 |
68 |
69 | org.jetbrains.kotlin
70 | kotlin-test
71 | test
72 |
73 |
74 | com.amazonaws
75 | aws-java-sdk-cloudformation
76 | test
77 |
78 |
79 | com.amazonaws
80 | aws-java-sdk-s3
81 | test
82 |
83 |
84 | org.slf4j
85 | slf4j-api
86 | test
87 |
88 |
89 | org.apache.logging.log4j
90 | log4j-slf4j2-impl
91 | test
92 |
93 |
94 | org.apache.logging.log4j
95 | log4j-core
96 | test
97 |
98 |
99 | org.apache.logging.log4j
100 | log4j-api
101 | test
102 |
103 |
104 | com.fasterxml.jackson.core
105 | jackson-databind
106 | test
107 |
108 |
109 | com.fasterxml.jackson.module
110 | jackson-module-kotlin
111 | test
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/core/src/test/kotlin/ws/osiris/core/StandardFilterTest.kt:
--------------------------------------------------------------------------------
1 | package ws.osiris.core
2 |
3 | import com.google.gson.Gson
4 | import org.testng.annotations.Test
5 | import kotlin.test.assertEquals
6 |
7 | /**
8 | * Tests for the functionality provided by the standard set of filters that are included by default.
9 | */
10 | @Test
11 | class StandardFilterTest {
12 |
13 | fun defaultContentType() {
14 | val api = api {
15 | get("/foo") { _ ->
16 | "Foo"
17 | }
18 | }
19 | val client = InMemoryTestClient.create(api)
20 | val (_, headers, _) = client.get("/foo")
21 | assertEquals(JSON_CONTENT_TYPE.header, headers[HttpHeaders.CONTENT_TYPE])
22 | }
23 |
24 | /**
25 | * Tests the automatic encoding of the response body into a JSON string.
26 | *
27 | * Handling of body types by content type:
28 | * * content type = JSON
29 | * * null - no body
30 | * * string - assumed to be JSON, used as-is
31 | * * ByteArray - base64 encoded - does API Gateway return this as binary? or a base64 encoded string?
32 | * * object - converted to a JSON string using Jackson
33 | * * content type != JSON
34 | * * null - no body
35 | * * string - used as-is, no base64 - Jackson should handle escaping when AWS does the conversion
36 | * * ByteArray - base64 encoded - does API Gateway return this as binary? or a base64 encoded string?
37 | * * any other type throws an exception. or should it just use toString()? seems friendlier
38 | */
39 | fun serialiseObjectsToJson() {
40 | val api = api {
41 | get("/nullbody") { req ->
42 | req.responseBuilder().build(null)
43 | }
44 | get("/stringbody") { _ ->
45 | """{"foo":"abc"}"""
46 | }
47 | get("/mapbody") { _ ->
48 | mapOf("foo" to 42, "bar" to "Bar")
49 | }
50 | get("/objectbody") { _ ->
51 | BodyObject(42, "Bar")
52 | }
53 | }
54 | val client = InMemoryTestClient.create(api)
55 | assertEquals(null, client.get("/nullbody").body)
56 | assertEquals("""{"foo":"abc"}""", client.get("/stringbody").body)
57 | assertEquals("""{"foo":42,"bar":"Bar"}""", client.get("/mapbody").body)
58 | assertEquals("""{"foo":42,"bar":"Bar"}""", client.get("/objectbody").body)
59 | }
60 |
61 | // TODO serialisation when the content type isn't JSON. not a high priority for now
62 |
63 | @Test
64 | fun exceptionMapping() {
65 | val api = api {
66 | get("/badrequest") { _ ->
67 | throw IllegalArgumentException("illegal arg")
68 | }
69 | get("/notfound") { _ ->
70 | throw DataNotFoundException("not found")
71 | }
72 | get("/badjson") { _ ->
73 | // This throws a JsonSyntaxException which is mapped to a bad request
74 | Gson().fromJson("this is invalid JSON", Map::class.java)
75 | }
76 | get("/forbidden") { _ ->
77 | throw ForbiddenException("top secret")
78 | }
79 | get("/servererror") { _ ->
80 | throw RuntimeException("oh no!")
81 | }
82 | }
83 | val client = InMemoryTestClient.create(api)
84 | val (status1, _, body1) = client.get("/badrequest")
85 | assertEquals(400, status1)
86 | assertEquals("illegal arg", body1)
87 |
88 | val (status2, _, body2) = client.get("/notfound")
89 | assertEquals(404, status2)
90 | assertEquals("not found", body2)
91 |
92 | val (status3, _, body3) = client.get("/forbidden")
93 | assertEquals(403, status3)
94 | assertEquals("top secret", body3)
95 |
96 | val (status4, _, body4) = client.get("/servererror")
97 | assertEquals(500, status4)
98 | assertEquals("Server Error", body4)
99 | }
100 |
101 | fun testNoFilters() {
102 | val api = api {
103 |
104 | globalFilters = listOf()
105 |
106 | get("/foo") { _ ->
107 | "Foo"
108 | }
109 | }
110 | val client = InMemoryTestClient.create(api)
111 | val (_, headers, _) = client.get("/foo")
112 | assertEquals(null, headers[HttpHeaders.CONTENT_TYPE])
113 | }
114 |
115 | //--------------------------------------------------------------------------------------------------
116 |
117 | private class BodyObject(val foo: Int, val bar: String)
118 | }
119 |
--------------------------------------------------------------------------------
/local-server/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | ws.osiris
8 | osiris
9 | 2.0.1-SNAPSHOT
10 | ..
11 |
12 |
13 | osiris-local-server
14 | jar
15 | Osiris Local Server
16 | Utilities for running in a local HTTP server
17 |
18 |
19 |
20 | ws.osiris
21 | osiris-core
22 |
23 |
24 | ws.osiris
25 | osiris-server
26 |
27 |
28 | ws.osiris
29 | osiris-aws
30 |
31 |
32 | org.jetbrains.kotlin
33 | kotlin-stdlib
34 |
35 |
36 | org.slf4j
37 | slf4j-api
38 |
39 |
40 | org.eclipse.jetty
41 | jetty-servlet
42 |
43 |
44 | jakarta.servlet
45 | jakarta.servlet-api
46 |
47 |
48 | com.beust
49 | jcommander
50 |
51 |
52 |
53 | org.apache.logging.log4j
54 | log4j-core
55 | runtime
56 |
57 |
58 |
59 |
60 | org.testng
61 | testng
62 | test
63 |
64 |
65 | ws.osiris
66 | osiris-core
67 | test-jar
68 | test
69 |
70 |
71 | com.squareup.okhttp3
72 | okhttp
73 | test
74 |
75 |
76 | ws.osiris
77 | osiris-server
78 | test-jar
79 | test
80 |
81 |
82 | com.fasterxml.jackson.module
83 | jackson-module-kotlin
84 | test
85 |
86 |
87 |
88 |
89 |
90 |
91 | org.apache.maven.plugins
92 | maven-jar-plugin
93 |
94 |
95 |
96 | test-jar
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | release
107 |
108 |
109 |
110 | org.apache.maven.plugins
111 | maven-jar-plugin
112 |
113 |
114 |
115 | test-jar
116 |
117 |
118 |
119 |
120 | true
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/integration/src/test/e2e-test/code/ApiDefinition.kt:
--------------------------------------------------------------------------------
1 | // TODO allow this to be replaced so the end-to-end test can handle packages other than the default
2 | package com.example.osiris.core
3 |
4 | import com.google.gson.Gson
5 | import ws.osiris.aws.IamAuth
6 | import ws.osiris.core.MimeTypes
7 | import ws.osiris.core.DataNotFoundException
8 | import ws.osiris.core.ForbiddenException
9 | import ws.osiris.core.ComponentsProvider
10 | import ws.osiris.core.HttpHeaders
11 | import ws.osiris.core.api
12 |
13 | /**
14 | * The name of an environment variable used to pass configuration to the code that handles the HTTP requests.
15 | *
16 | * The values of environment variables can be specified in the `environmentVariables` configuration element in the
17 | * Maven plugin. For example:
18 | *
19 | *
20 | * Example value
21 | *
22 | */
23 | const val EXAMPLE_ENVIRONMENT_VARIABLE = "EXAMPLE_ENVIRONMENT_VARIABLE"
24 |
25 | /** The API. */
26 | val api = api {
27 |
28 | staticFiles {
29 | path = "/public"
30 | indexFile = "index.html"
31 | }
32 | get("/") { _ ->
33 | mapOf("message" to "hello, root!")
34 | }
35 | get("/helloworld") { _ ->
36 | // return a map that is automatically converted to JSON
37 | mapOf("message" to "hello, world!")
38 | }
39 | get("/helloplain") { req ->
40 | // return a response with customised headers
41 | req.responseBuilder()
42 | .header(HttpHeaders.CONTENT_TYPE, MimeTypes.TEXT_PLAIN)
43 | .build("hello, world!")
44 | }
45 | get("/hello/queryparam1") { req ->
46 | // get an optional query parameter
47 | val name = req.queryParams.optional("name") ?: "world"
48 | mapOf("message" to "hello, $name!")
49 | }
50 | get("/hello/queryparam2") { req ->
51 | // get a required query parameter
52 | val name = req.queryParams["name"]
53 | mapOf("message" to "hello, $name!")
54 | }
55 | // use path() to group multiple endpoints under the same sub-path
56 | path("/hello") {
57 | get("/{name}") { req ->
58 | // get a path parameter
59 | val name = req.pathParams["name"]
60 | // this will be automatically converted to a JSON object like {"message":"hello, Bob!"}
61 | JsonMessage("hello, $name!")
62 | }
63 | get("/env") { _ ->
64 | // use the name property from TestComponents for the name
65 | JsonMessage("hello, $name!")
66 | }
67 | }
68 | // require authorisation for all endpoints inside the auth block
69 | auth(IamAuth) {
70 | // this will be inaccessible unless a policy is created and attached to the calling user, role or group
71 | get("/topsecret") { req ->
72 | JsonMessage("For your eyes only")
73 | }
74 | }
75 | post("/foo") { req ->
76 | val gson = Gson()
77 | val payload = gson.fromJson