Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

S3 #1

Merged
merged 2 commits into from
Oct 20, 2023
Merged

S3 #1

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
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"
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.gradle
/build/
build/
!gradle/wrapper/gradle-wrapper.jar

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### MacOS ###
.DS_Store

### ActiveMQ ###
**/activemq-data
65 changes: 65 additions & 0 deletions build.gradle.kts
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) }
}
}
}
23 changes: 23 additions & 0 deletions examples/s3/build.gradle.kts
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")
}
19 changes: 19 additions & 0 deletions examples/s3/http/S3.http
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
7 changes: 7 additions & 0 deletions examples/s3/http/http-client.env.json
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"
}
}
17 changes: 17 additions & 0 deletions examples/s3/src/main/kotlin/example/aws/s3/Application.kt
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)
}
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"))
}
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)
}

}
Loading