Skip to content

Commit 228f13e

Browse files
authored
Merge pull request #678 from grego952/add-movies-rest
Add movies-rest
2 parents 4c1b91e + 17f0e94 commit 228f13e

12 files changed

Lines changed: 326 additions & 5 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ Running various samples requires access to the Kyma environment. There are also
9191

9292
| Name | Description | References |
9393
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
94-
| [Deploy a Go PostgreSQL API Endpoint in SAP BTP, Kyma Runtime](./api-postgresql-go/README.md) | This sample provides a Golang API endpoint for communication with a PostgreSQL database | |
95-
| [Use and Seed SAP BTP PostgreSQL in SAP BTP, Kyma Runtime](./database-postgres/README.md) | This sample demonstrates how to containerize and deploy a PostgreSQL database | |
94+
| [Deploy a Go PostgreSQL API Endpoint in SAP BTP, Kyma Runtime](./api-postgresql-go/README.md) | This sample provides a Golang API endpoint for communication with an PostgreSQL database | [Tutorial](https://developers.sap.com/tutorials/cp-kyma-api-postgres-golang.html) |
95+
| [Use and Seed SAP BTP PostgreSQL in SAP BTP, Kyma Runtime](./database-postgres/README.md) | This sample demonstrates how to seed the PostgreSQL database with sample schema and data using a Kubernetes Job | [Tutorial](https://developers.sap.com/tutorials/cp-kyma-postgres-seed.html) |
9696

9797
## Advanced Scenarios
9898

api-postgresql-go/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Overview
44

55
> [!NOTE]
6-
> This sample is used in the Deploy a Go PostgreSQL API Endpoint in SAP BTP, Kyma Runtime tutorial.
6+
> This sample is used in the [Deploy a Go PostgreSQL API Endpoint in SAP BTP, Kyma Runtime](https://developers.sap.com/tutorials/cp-kyma-api-postgres-golang.html) tutorial.
77
88
This sample provides a Golang API endpoint for communication with PostgreSQL databases. The API connects to a BTP-managed PostgreSQL instance using Service Binding credentials available in the Kyma cluster.
99

database-postgres/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Overview
44

55
> [!NOTE]
6-
> This sample is used in the Use and Seed SAP BTP PostgreSQL in SAP BTP, Kyma Runtime tutorial.
6+
> This sample is used in the [Use and Seed SAP BTP PostgreSQL in SAP BTP, Kyma Runtime](https://developers.sap.com/tutorials/cp-kyma-postgres-seed.html) tutorial.
77
88
This sample seeds a managed PostgreSQL instance on SAP BTP with a small `orders` table. You use the provided `postgres-instance-binding.yaml` to create a PostgreSQL Service Instance and Service Binding for your Kyma cluster. The Service Binding produces a Kubernetes Secret containing the connection details (`hostname`, `port`, `dbname`, `username`, `password`, and optionally `sslmode`).
99

frontend-ui5-postgresql/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Overview
44

55
> [!NOTE]
6-
> This sample is used in the Deploy the SAPUI5 Frontend in SAP BTP, Kyma Runtime tutorial.
6+
> This sample is used in the [Deploy the SAPUI5 Frontend in SAP BTP, Kyma Runtime](https://developers.sap.com/tutorials/cp-kyma-frontend-ui5-postgresql.html) tutorial.
77
88
This sample provides a frontend SAPUI5 application that you can configure with any of the sample `Order` APIs.
99

movies-rest/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
BPL_JVM_THREAD_COUNT=20
2+
JAVA_TOOL_OPTIONS=-XX:ReservedCodeCacheSize=40M -XX:MaxMetaspaceSize=80M -Xss512k

movies-rest/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Deploy a Spring Boot Movies REST API in SAP BTP, Kyma Runtime
2+
3+
## Overview
4+
5+
> [!NOTE]
6+
> This sample is used in the Fast Prototyping in SAP BTP, Kyma Runtime Using App Push tutorial.
7+
8+
This sample provides a Spring Boot REST API that manages movie records stored as JSON objects in an S3-compatible **SAP Object Store** service.
9+
10+
This sample demonstrates how to perform the following operations:
11+
12+
- Go from source code to a running, externally accessible application on Kyma runtime in a single command
13+
- Iterate quickly on a prototype without writing Kubernetes manifests, Dockerfiles, or configuring a container registry
14+
- Evolve a local prototype into an automated GitHub Actions CD pipeline
15+
16+
## Architecture
17+
18+
```
19+
GitHub Actions (CI/CD)
20+
21+
22+
Kyma Runtime (Kubernetes)
23+
├── movies-rest Pod (Spring Boot, port 8080)
24+
│ └── Istio sidecar (mTLS + ingress)
25+
└── SAP Service Operator
26+
└── ObjectStore ServiceInstance → S3 bucket
27+
```
28+
29+
Each movie is stored as a JSON file at `movies/<id>.json` inside the bound S3 bucket.
30+
31+
> [!NOTE]
32+
> Object Store is used here for the sake of simplicity. For applications with structured, relational use a proper database such as Hana Cloud or PostgreSQL.
33+
34+
## Tech Stack
35+
36+
| Layer | Technology |
37+
|---|---|
38+
| Language | Java 21 |
39+
| Framework | Spring Boot 3.3 |
40+
| API docs | springdoc-openapi / Swagger UI |
41+
| Storage | AWS SDK v2 → SAP Object Store (S3-compatible) |
42+
| Service binding | `java-sap-service-operator` (SAP Cloud Service Binding) |
43+
| Runtime | SAP BTP Kyma (Kubernetes + Istio) |
44+
| CI/CD | GitHub Actions + `kyma-project/setup-kyma-cli` |
45+
46+
## API Endpoints
47+
48+
| Method | Path | Description |
49+
|---|---|---|
50+
| `GET` | `/movies` | List all movies |
51+
| `GET` | `/movies/{id}` | Get a movie by ID |
52+
| `POST` | `/movies` | Create a new movie (ID auto-generated) |
53+
| `PUT` | `/movies/{id}` | Update an existing movie |
54+
| `DELETE` | `/movies/{id}` | Delete a movie |
55+
56+
Interactive documentation is available at `/swagger-ui.html` after deployment.
57+
58+
### Movie Resource
59+
60+
```json
61+
{
62+
"id": "4e92e9c6-ebe3-4840-ae3c-2ede35ee4b74",
63+
"title": "Blade Runner",
64+
"year": 1982,
65+
"director": "Ridley Scott",
66+
"rating": 8.1
67+
}
68+
```

movies-rest/pom.xml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.springframework.boot</groupId>
9+
<artifactId>spring-boot-starter-parent</artifactId>
10+
<version>3.3.0</version>
11+
</parent>
12+
13+
<groupId>com.example</groupId>
14+
<artifactId>movies</artifactId>
15+
<version>1.0.0</version>
16+
17+
<properties>
18+
<java.version>21</java.version>
19+
</properties>
20+
21+
<dependencyManagement>
22+
<dependencies>
23+
<dependency>
24+
<groupId>com.sap.cloud.environment.servicebinding</groupId>
25+
<artifactId>java-bom</artifactId>
26+
<version>0.10.5</version>
27+
<type>pom</type>
28+
<scope>import</scope>
29+
</dependency>
30+
</dependencies>
31+
</dependencyManagement>
32+
33+
<dependencies>
34+
<dependency>
35+
<groupId>org.springframework.boot</groupId>
36+
<artifactId>spring-boot-starter-web</artifactId>
37+
</dependency>
38+
<dependency>
39+
<groupId>org.springdoc</groupId>
40+
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
41+
<version>2.5.0</version>
42+
</dependency>
43+
<dependency>
44+
<groupId>com.sap.cloud.environment.servicebinding</groupId>
45+
<artifactId>java-sap-service-operator</artifactId>
46+
</dependency>
47+
<dependency>
48+
<groupId>software.amazon.awssdk</groupId>
49+
<artifactId>s3</artifactId>
50+
<version>2.25.0</version>
51+
</dependency>
52+
</dependencies>
53+
54+
<build>
55+
<plugins>
56+
<plugin>
57+
<groupId>org.springframework.boot</groupId>
58+
<artifactId>spring-boot-maven-plugin</artifactId>
59+
</plugin>
60+
</plugins>
61+
</build>
62+
</project>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.example.movies;
2+
3+
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
4+
import io.swagger.v3.oas.annotations.info.Info;
5+
import org.springframework.boot.SpringApplication;
6+
import org.springframework.boot.autoconfigure.SpringBootApplication;
7+
8+
@SpringBootApplication
9+
@OpenAPIDefinition(info = @Info(
10+
title = "Movies API",
11+
version = "1.0.0",
12+
description = "CRUD REST service for movies, backed by SAP BTP Object Store"))
13+
public class Application {
14+
public static void main(String[] args) {
15+
SpringApplication.run(Application.class, args);
16+
}
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.example.movies;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
@Schema(description = "Movie resource")
6+
public record Movie(
7+
@Schema(description = "Auto-generated ID", example = "1714900000000", accessMode = Schema.AccessMode.READ_ONLY)
8+
String id,
9+
@Schema(description = "Movie title", example = "Blade Runner")
10+
String title,
11+
@Schema(description = "Release year", example = "1982")
12+
int year,
13+
@Schema(description = "Director name", example = "Ridley Scott")
14+
String director,
15+
@Schema(description = "Rating out of 10", example = "8.1")
16+
Double rating) {
17+
public Movie withId(String newId) {
18+
return new Movie(newId, title, year, director, rating);
19+
}
20+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.example.movies;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import io.swagger.v3.oas.annotations.Operation;
5+
import io.swagger.v3.oas.annotations.tags.Tag;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.web.bind.annotation.*;
8+
import org.springframework.web.server.ResponseStatusException;
9+
import software.amazon.awssdk.core.sync.RequestBody;
10+
import software.amazon.awssdk.services.s3.S3Client;
11+
import software.amazon.awssdk.services.s3.model.*;
12+
13+
import java.io.IOException;
14+
import java.util.List;
15+
import java.util.UUID;
16+
17+
@RestController
18+
@RequestMapping("/movies")
19+
@Tag(name = "Movies", description = "CRUD operations for movie resources")
20+
public class MovieController {
21+
22+
private final S3Client s3;
23+
private final String bucket;
24+
private final ObjectMapper mapper = new ObjectMapper();
25+
26+
public MovieController(S3Client s3, String bucketName) {
27+
this.s3 = s3;
28+
this.bucket = bucketName;
29+
}
30+
31+
@GetMapping
32+
@Operation(summary = "List all movies")
33+
public List<Movie> list() throws IOException {
34+
ListObjectsV2Request request = ListObjectsV2Request.builder()
35+
.bucket(bucket)
36+
.prefix("movies/")
37+
.build();
38+
ListObjectsV2Response response = s3.listObjectsV2(request);
39+
return response.contents().stream()
40+
.map(obj -> getMovie(obj.key()))
41+
.toList();
42+
}
43+
44+
@GetMapping("/{id}")
45+
@Operation(summary = "Get a movie by ID")
46+
public Movie get(@PathVariable String id) {
47+
return getMovie("movies/" + id + ".json");
48+
}
49+
50+
@PostMapping
51+
@ResponseStatus(HttpStatus.CREATED)
52+
@Operation(summary = "Create a new movie")
53+
public Movie create(@org.springframework.web.bind.annotation.RequestBody Movie movie) throws Exception {
54+
Movie saved = movie.withId(UUID.randomUUID().toString());
55+
putMovie(saved);
56+
return saved;
57+
}
58+
59+
@PutMapping("/{id}")
60+
@Operation(summary = "Update an existing movie")
61+
public Movie update(@PathVariable String id, @org.springframework.web.bind.annotation.RequestBody Movie movie) throws Exception {
62+
Movie saved = movie.withId(id);
63+
putMovie(saved);
64+
return saved;
65+
}
66+
67+
@DeleteMapping("/{id}")
68+
@ResponseStatus(HttpStatus.NO_CONTENT)
69+
@Operation(summary = "Delete a movie")
70+
public void delete(@PathVariable String id) {
71+
DeleteObjectRequest request = DeleteObjectRequest.builder()
72+
.bucket(bucket)
73+
.key("movies/" + id + ".json")
74+
.build();
75+
s3.deleteObject(request);
76+
}
77+
78+
private void putMovie(Movie movie) throws Exception {
79+
byte[] json = mapper.writeValueAsBytes(movie);
80+
PutObjectRequest request = PutObjectRequest.builder()
81+
.bucket(bucket)
82+
.key("movies/" + movie.id() + ".json")
83+
.contentType("application/json")
84+
.build();
85+
s3.putObject(request, software.amazon.awssdk.core.sync.RequestBody.fromBytes(json));
86+
}
87+
88+
private Movie getMovie(String key) {
89+
try {
90+
GetObjectRequest request = GetObjectRequest.builder()
91+
.bucket(bucket)
92+
.key(key)
93+
.build();
94+
byte[] data = s3.getObject(request).readAllBytes();
95+
return mapper.readValue(data, Movie.class);
96+
} catch (NoSuchKeyException e) {
97+
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Movie not found");
98+
} catch (IOException e) {
99+
throw new RuntimeException(e);
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)