-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3bbcdfc
commit 54c3548
Showing
21 changed files
with
751 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
name: CI-Build | ||
|
||
on: | ||
push: | ||
branches: [ master ] | ||
pull_request: | ||
branches: [ master ] | ||
|
||
concurrency: | ||
group: ci_${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
env: | ||
javaVersion: "17" | ||
javaDistribution: "liberica" | ||
|
||
jobs: | ||
ci-platform-aws: | ||
runs-on: ubuntu-latest | ||
needs: | ||
- s3 | ||
steps: | ||
- run: echo "CI-Build completed!" | ||
|
||
s3: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/setup-java@v3 | ||
with: | ||
java-version: ${{ env.javaVersion }} | ||
distribution: ${{ env.javaDistribution }} | ||
- run: | | ||
chmod +x gradlew | ||
./gradlew :examples:s3:build | ||
- uses: actions/upload-artifact@v3 | ||
if: always() | ||
with: | ||
name: test-results_s3 | ||
path: "**/build/reports/tests" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension | ||
import org.gradle.api.file.DuplicatesStrategy.INCLUDE | ||
import org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED | ||
import org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED | ||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile | ||
|
||
plugins { | ||
id("org.springframework.boot") version "3.1.0" apply false | ||
id("io.spring.dependency-management") version "1.1.0" apply false | ||
id("org.asciidoctor.jvm.convert") version "3.3.2" apply false | ||
|
||
kotlin("jvm") version "1.8.21" apply false | ||
kotlin("plugin.spring") version "1.8.21" apply false | ||
kotlin("plugin.jpa") version "1.8.21" apply false | ||
kotlin("plugin.noarg") version "1.8.21" apply false | ||
} | ||
|
||
allprojects { | ||
repositories { mavenCentral(); mavenLocal() } | ||
|
||
if (project.childProjects.isNotEmpty()) return@allprojects | ||
|
||
apply { | ||
plugin("io.spring.dependency-management") | ||
} | ||
|
||
the<DependencyManagementExtension>().apply { | ||
imports { | ||
mavenBom("org.jetbrains.kotlin:kotlin-bom:1.8.21") | ||
mavenBom("org.testcontainers:testcontainers-bom:1.18.3") | ||
mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) | ||
} | ||
|
||
dependencies { | ||
dependency("com.ninja-squad:springmockk:4.0.2") | ||
dependency("io.mockk:mockk-jvm:1.13.5") | ||
dependency("org.testcontainers:junit-jupiter:1.18.1") | ||
dependency("org.testcontainers:localstack:1.18.1") | ||
dependency("com.amazonaws:aws-java-sdk-s3:1.12.272") | ||
dependency("com.amazonaws:aws-java-sdk-sts:1.12.272") | ||
dependency("io.kotest:kotest-assertions-core:5.6.2") | ||
} | ||
} | ||
|
||
tasks { | ||
withType<Copy> { duplicatesStrategy = INCLUDE } | ||
withType<Jar> { duplicatesStrategy = INCLUDE } | ||
withType<JavaCompile> { | ||
sourceCompatibility = "17" | ||
targetCompatibility = "17" | ||
} | ||
withType<KotlinCompile> { | ||
kotlinOptions { | ||
freeCompilerArgs = listOf("-Xjsr305=strict") | ||
jvmTarget = "17" | ||
incremental = false | ||
} | ||
} | ||
withType<Test> { | ||
group = "verification" | ||
useJUnitPlatform() | ||
testLogging { events(FAILED, SKIPPED) } | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
plugins { | ||
id("org.springframework.boot") | ||
id("io.spring.dependency-management") | ||
|
||
kotlin("jvm") | ||
kotlin("plugin.spring") | ||
} | ||
|
||
dependencies { | ||
implementation("org.springframework.boot:spring-boot-starter") | ||
implementation("org.springframework.boot:spring-boot-starter-web") | ||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") | ||
implementation("org.jetbrains.kotlin:kotlin-reflect") | ||
implementation("org.testcontainers:localstack") | ||
implementation("com.amazonaws:aws-java-sdk-s3") | ||
implementation("com.amazonaws:aws-java-sdk-sts") | ||
|
||
testImplementation("org.springframework.boot:spring-boot-starter-test") | ||
testImplementation("io.mockk:mockk-jvm") | ||
testImplementation("com.ninja-squad:springmockk") | ||
testImplementation("org.testcontainers:junit-jupiter") | ||
testImplementation("io.kotest:kotest-assertions-core") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
### Get Clean Code Cover | ||
GET localhost:8080/books/{{clean-code}}/cover | ||
|
||
### Get Lord of the Rings Cover | ||
GET localhost:8080/books/{{lord-of-the-rings}}/cover | ||
|
||
### Get Mythical Man Month Cover | ||
### Will fail at first because the cover has to be added by the PUT request below. | ||
GET localhost:8080/books/{{mythical-man-moth}}/cover | ||
|
||
### Put Mythical Man Month Cover | ||
PUT localhost:8080/books/{{mythical-man-moth}}/cover | ||
Content-Type: multipart/form-data; boundary=boundary | ||
|
||
--boundary | ||
Content-Disposition: form-data; name="cover"; filename="mythical-man-month.jpg" | ||
Content-Type: image/jpeg | ||
|
||
< ../src/main/resources/bookcovers/mythical-man-month.jpg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"ldev": { | ||
"clean-code": "73a3a94e-9ec7-4c35-82ff-9235fa4a2a86", | ||
"lord-of-the-rings": "81abeacb-9850-4691-94b8-2fff9bb7b1a7", | ||
"mythical-man-moth": "021e0ea7-6fda-4682-9747-eb7161154885" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package example.aws.s3 | ||
|
||
import org.springframework.boot.autoconfigure.SpringBootApplication | ||
import org.springframework.boot.builder.SpringApplicationBuilder | ||
import org.springframework.boot.runApplication | ||
|
||
@SpringBootApplication | ||
class Application | ||
|
||
/** | ||
* Start the application from a builder (instead of the usual [runApplication]) so we can supply our [S3Initializer]. | ||
*/ | ||
fun main(args: Array<String>) { | ||
SpringApplicationBuilder(Application::class.java) | ||
.initializers(S3Initializer()) | ||
.run(*args) | ||
} |
90 changes: 90 additions & 0 deletions
90
examples/s3/src/main/kotlin/example/aws/s3/ApplicationConfiguration.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package example.aws.s3 | ||
|
||
import com.amazonaws.auth.AWSStaticCredentialsProvider | ||
import com.amazonaws.auth.BasicAWSCredentials | ||
import com.amazonaws.client.builder.AwsClientBuilder | ||
import com.amazonaws.services.s3.AmazonS3 | ||
import com.amazonaws.services.s3.AmazonS3ClientBuilder | ||
import example.aws.s3.domain.S3Properties | ||
import org.springframework.boot.context.event.ApplicationStartedEvent | ||
import org.springframework.context.ApplicationContextInitializer | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.context.event.EventListener | ||
import org.springframework.context.support.GenericApplicationContext | ||
import org.testcontainers.containers.localstack.LocalStackContainer | ||
import org.testcontainers.utility.DockerImageName | ||
import java.io.File | ||
import java.util.function.Supplier | ||
|
||
@Configuration | ||
class ApplicationConfiguration( | ||
|
||
/** | ||
* There is no real [AmazonS3] bean available to us, we artificially create and inject our own using the | ||
* [S3Initializer] below. IntelliJ does not know that, so we have to suppress its inspections about missing beans. | ||
*/ | ||
@Suppress("SpringJavaInjectionPointsAutowiringInspection") | ||
private val s3: AmazonS3, | ||
private val s3properties: S3Properties, | ||
) { | ||
|
||
/** | ||
* Our S3 is not permanent, but is instead re-created every time the app is running. Therefore, we also have to | ||
* create a bucket every time the container starts. | ||
* | ||
* We also use this opportunity to add 2 book covers into the bucket to serve as pre-made examples. | ||
*/ | ||
@EventListener | ||
fun createBucketAddExamples(event: ApplicationStartedEvent) { | ||
s3.createBucket(s3properties.bucketName) | ||
|
||
val cleanCodeCover = File(this::class.java.getResource("/bookcovers/clean_code.jpg")!!.path) | ||
val lotrCover = File(this::class.java.getResource("/bookcovers/fellowship_of_the_ring.jpg")!!.path) | ||
|
||
s3.putObject(s3properties.bucketName, s3properties.books.cleanCode, cleanCodeCover) | ||
s3.putObject(s3properties.bucketName, s3properties.books.lordOfTheRings, lotrCover) | ||
} | ||
} | ||
|
||
/** | ||
* This [ApplicationContextInitializer] is the means by which we create an S3 instance, both for our tests, and also as | ||
* a substitute for not having a real AWS environment in this simple example. | ||
*/ | ||
class S3Initializer : ApplicationContextInitializer<GenericApplicationContext> { | ||
|
||
override fun initialize(applicationContext: GenericApplicationContext) { | ||
val container = S3Container().withServices(LocalStackContainer.Service.S3).apply { start() } | ||
createS3Bean(container, applicationContext) | ||
} | ||
|
||
/** | ||
* Normally you would declare a [Bean] method where you create and configure your [AmazonS3] client. Since we use | ||
* a Test Container we instead inject our client directly in Spring's application context. | ||
*/ | ||
private fun createS3Bean( | ||
container: LocalStackContainer, | ||
applicationContext: GenericApplicationContext | ||
) { | ||
applicationContext.registerBean( | ||
AmazonS3::class.java.simpleName, | ||
AmazonS3::class.java, | ||
Supplier { | ||
AmazonS3ClientBuilder | ||
.standard() | ||
.withEndpointConfiguration( | ||
AwsClientBuilder.EndpointConfiguration( | ||
container.getEndpointOverride(LocalStackContainer.Service.S3).toString(), | ||
container.region | ||
) | ||
) | ||
.withCredentials( | ||
AWSStaticCredentialsProvider(BasicAWSCredentials(container.accessKey, container.secretKey)) | ||
) | ||
.build() | ||
} | ||
) | ||
} | ||
|
||
private class S3Container : LocalStackContainer(DockerImageName.parse("localstack/localstack:0.11.3")) | ||
} |
49 changes: 49 additions & 0 deletions
49
examples/s3/src/main/kotlin/example/aws/s3/api/BookCoverController.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package example.aws.s3.api | ||
|
||
import example.aws.s3.domain.BookCoverData | ||
import example.aws.s3.domain.BookCoverService | ||
import org.springframework.core.io.InputStreamResource | ||
import org.springframework.core.io.Resource | ||
import org.springframework.http.HttpStatus.CREATED | ||
import org.springframework.http.MediaType | ||
import org.springframework.http.ResponseEntity | ||
import org.springframework.http.ResponseEntity.notFound | ||
import org.springframework.http.ResponseEntity.ok | ||
import org.springframework.web.bind.annotation.GetMapping | ||
import org.springframework.web.bind.annotation.PathVariable | ||
import org.springframework.web.bind.annotation.PutMapping | ||
import org.springframework.web.bind.annotation.RequestParam | ||
import org.springframework.web.bind.annotation.ResponseStatus | ||
import org.springframework.web.bind.annotation.RestController | ||
import org.springframework.web.multipart.MultipartFile | ||
import java.util.* | ||
|
||
/** | ||
* Simple REST controller that allows to either GET or PUT a cover for a specific book. | ||
*/ | ||
@RestController | ||
class BookCoverController( | ||
private val bookCoverService: BookCoverService | ||
) { | ||
|
||
@GetMapping("/books/{id}/cover") | ||
fun getCover(@PathVariable("id") id: UUID): ResponseEntity<Resource> { | ||
val coverData: BookCoverData = bookCoverService.findCover(id) | ||
?: return notFound().build() | ||
|
||
return ok() | ||
.contentLength(coverData.contentLength) | ||
.contentType(MediaType.parseMediaType(coverData.contentType)) | ||
.body(InputStreamResource(coverData.byteStream)) | ||
} | ||
|
||
@ResponseStatus(CREATED) | ||
@PutMapping("/books/{id}/cover") | ||
fun putCover( | ||
@PathVariable("id") id: UUID, | ||
@RequestParam("cover") cover: MultipartFile, | ||
) { | ||
bookCoverService.saveCover(id, cover) | ||
} | ||
|
||
} |
63 changes: 63 additions & 0 deletions
63
examples/s3/src/main/kotlin/example/aws/s3/domain/BookCoverService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
package example.aws.s3.domain | ||
|
||
import com.amazonaws.services.s3.AmazonS3 | ||
import com.amazonaws.services.s3.model.AmazonS3Exception | ||
import com.amazonaws.services.s3.model.ObjectMetadata | ||
import org.springframework.boot.context.properties.EnableConfigurationProperties | ||
import org.springframework.http.HttpStatus.NOT_FOUND | ||
import org.springframework.stereotype.Service | ||
import org.springframework.web.multipart.MultipartFile | ||
import java.io.ByteArrayInputStream | ||
import java.io.InputStream | ||
import java.util.* | ||
|
||
/** | ||
* Business class that implements the details of loading and saving book covers. Offers a clean api that hides the | ||
* lower level details of the S3 sdk. | ||
*/ | ||
@Service | ||
@Suppress("SpringJavaInjectionPointsAutowiringInspection") | ||
@EnableConfigurationProperties(S3Properties::class) | ||
class BookCoverService( | ||
private val s3: AmazonS3, | ||
private val s3Properties: S3Properties, | ||
) { | ||
|
||
fun findCover(id: UUID): BookCoverData? { | ||
val s3Object = try { | ||
s3.getObject(s3Properties.bucketName, id.toString()) | ||
} catch (e: AmazonS3Exception) { | ||
when (e.statusCode) { | ||
NOT_FOUND.value() -> return null | ||
else -> throw e | ||
} | ||
} | ||
return BookCoverData( | ||
byteStream = s3Object.objectContent, | ||
contentLength = s3Object.objectMetadata.contentLength, | ||
contentType = s3Object.objectMetadata.contentType, | ||
) | ||
} | ||
|
||
fun saveCover( | ||
id: UUID, | ||
cover: MultipartFile, | ||
) { | ||
s3.putObject( | ||
s3Properties.bucketName, | ||
id.toString(), | ||
cover.inputStream, | ||
ObjectMetadata().also { | ||
it.contentLength = cover.size | ||
it.contentType = cover.contentType | ||
} | ||
) | ||
} | ||
|
||
} | ||
|
||
data class BookCoverData( | ||
val byteStream: InputStream, | ||
val contentLength: Long, | ||
val contentType: String, | ||
) |
Oops, something went wrong.