Compare commits

10 Commits
main ... dev

Author SHA1 Message Date
ae73403644 Fix the compile failure on dev by adding the missing semicolon after store.put(id, item) in ItemsController.create().
All checks were successful
Build and Push to ACR / Build and Push (push) Successful in 57s
2026-05-07 21:14:08 +00:00
75a18723c8 test(#310): break compile to test copilot fix flow
Some checks failed
Build and Push to ACR / Build and Push (push) Failing after 47s
2026-05-07 21:10:21 +00:00
a9f574ec6e feat(k6): add bespoke load test files
All checks were successful
Build and Push to ACR / Build and Push (push) Successful in 1m55s
2026-05-07 21:06:42 +00:00
8af257493e feat(scaffold): add src/test/java/com/kyndryl/platform/service/ApplicationTests.java [skip ci] 2026-05-07 21:06:30 +00:00
8ae7baba87 feat(scaffold): add src/main/java/com/kyndryl/platform/service/ItemsController.java [skip ci] 2026-05-07 21:06:29 +00:00
acfb8bf865 feat(scaffold): add src/main/java/com/kyndryl/platform/service/Application.java [skip ci] 2026-05-07 21:06:28 +00:00
a053548dc0 feat(scaffold): add src/main/resources/application.yml [skip ci] 2026-05-07 21:06:27 +00:00
f1be70ba80 feat(scaffold): add pom.xml [skip ci] 2026-05-07 21:06:27 +00:00
9e6a11b4bc feat(scaffold): add Dockerfile [skip ci] 2026-05-07 21:06:26 +00:00
f6de26505d feat(scaffold): add .gitignore [skip ci] 2026-05-07 21:06:25 +00:00
10 changed files with 393 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
target/
*.class
*.jar
*.war
*.ear
*.log
.DS_Store
.idea/
*.iml
*.ipr
*.iws
.vscode/
.mvn/
mvnw
mvnw.cmd

30
Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
# ---- Build stage ----
FROM maven:3.9-eclipse-temurin-17-alpine AS build
WORKDIR /workspace
COPY pom.xml .
RUN mvn dependency:go-offline -q
COPY src ./src
RUN mvn package -DskipTests -q
# ---- Runtime stage ----
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Download OTel Java agent for zero-code auto-instrumentation
RUN wget -q -O /app/otel-agent.jar \
"https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.5.0/opentelemetry-javaagent.jar"
# Non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --from=build /workspace/target/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health | grep -q '"status":"UP"' || exit 1
ENTRYPOINT ["java", "-javaagent:/app/otel-agent.jar", "-jar", "app.jar"]

59
k6/configmap.yaml Normal file
View File

@@ -0,0 +1,59 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: k6-test-test-for-310--006
namespace: dev
labels:
app: test-for-310--006
app.kubernetes.io/managed-by: backstage
app.kubernetes.io/component: load-testing
data:
K6_OUT: opentelemetry
K6_OTEL_GRPC_EXPORTER_INSECURE: 'true'
K6_OTEL_GRPC_EXPORTER_ENDPOINT: otel-collector.monitoring.svc.cluster.local:4317
K6_OTEL_METRIC_PREFIX: k6_
K6_OTEL_FLUSH_INTERVAL: '1000'
K6_OTEL_EXPORT_INTERVAL: '5000'
K6_OTEL_SERVICE_NAME: k6-test-for-310--006
load-test.js: "import http from 'k6/http';\nimport { check, sleep, group } from\
\ 'k6';\n\nconst vus = parseInt(__ENV.TEST_VUS || '10');\nconst duration = __ENV.TEST_DURATION\
\ || '30s';\nconst targetUrl = __ENV.TARGET_URL || 'http://test-for-310--006.dev.svc.cluster.local:8080';\n\
\nexport const options = {\n scenarios: {\n load_test: {\n executor:\
\ 'ramping-vus',\n startVUs: 0,\n stages: [\n { duration: '10s',\
\ target: vus },\n { duration: duration, target: vus },\n { duration:\
\ '5s', target: 0 },\n ],\n },\n },\n thresholds: {\n http_req_duration:\
\ ['p(95)<500'],\n http_req_failed: ['rate<0.01'],\n },\n};\n\nhttp.setResponseCallback(http.expectedStatuses({\
\ min: 200, max: 399 }));\n\nexport default function () {\n group('Items API',\
\ () => {\n const listRes = http.get(`${targetUrl}/api/items`);\n check(listRes,\
\ {\n 'GET /api/items status is 200': (r) => r.status === 200,\n 'GET\
\ /api/items response time < 500ms': (r) => r.timings.duration < 500,\n 'GET\
\ /api/items returns array': (r) => Array.isArray(r.json()),\n });\n\n const\
\ createRes = http.post(\n `${targetUrl}/api/items`,\n JSON.stringify({\
\ name: 'Test Item', description: 'A test item description' }),\n { headers:\
\ { 'Content-Type': 'application/json' } }\n );\n check(createRes, {\n \
\ 'POST /api/items status is 201': (r) => r.status === 201,\n 'POST /api/items\
\ response time < 500ms': (r) => r.timings.duration < 500,\n 'POST /api/items\
\ returns created item': (r) => r.json().id && r.json().name === 'Test Item',\n\
\ });\n\n const itemId = createRes.json().id;\n\n const getRes = http.get(`${targetUrl}/api/items/${itemId}`);\n\
\ check(getRes, {\n 'GET /api/items/{id} status is 200': (r) => r.status\
\ === 200,\n 'GET /api/items/{id} response time < 500ms': (r) => r.timings.duration\
\ < 500,\n 'GET /api/items/{id} returns correct item': (r) => r.json().id\
\ === itemId,\n });\n\n const updateRes = http.put(\n `${targetUrl}/api/items/${itemId}`,\n\
\ JSON.stringify({ name: 'Updated Item', description: 'Updated description'\
\ }),\n { headers: { 'Content-Type': 'application/json' } }\n );\n \
\ check(updateRes, {\n 'PUT /api/items/{id} status is 200': (r) => r.status\
\ === 200,\n 'PUT /api/items/{id} response time < 500ms': (r) => r.timings.duration\
\ < 500,\n 'PUT /api/items/{id} returns updated item': (r) => r.json().name\
\ === 'Updated Item',\n });\n\n const deleteRes = http.del(`${targetUrl}/api/items/${itemId}`);\n\
\ check(deleteRes, {\n 'DELETE /api/items/{id} status is 200': (r) =>\
\ r.status === 200,\n 'DELETE /api/items/{id} response time < 500ms': (r)\
\ => r.timings.duration < 500,\n 'DELETE /api/items/{id} confirms deletion':\
\ (r) => r.json().deleted === itemId,\n });\n });\n\n sleep(0.5);\n\n group('Actuator\
\ API', () => {\n const healthRes = http.get(`${targetUrl}/actuator/health`);\n\
\ check(healthRes, {\n 'GET /actuator/health status is 200': (r) => r.status\
\ === 200,\n 'GET /actuator/health response time < 500ms': (r) => r.timings.duration\
\ < 500,\n 'GET /actuator/health returns UP status': (r) => r.json().status\
\ === 'UP',\n });\n\n const prometheusRes = http.get(`${targetUrl}/actuator/prometheus`);\n\
\ check(prometheusRes, {\n 'GET /actuator/prometheus status is 200': (r)\
\ => r.status === 200,\n 'GET /actuator/prometheus response time < 500ms':\
\ (r) => r.timings.duration < 500,\n });\n });\n\n sleep(0.5);\n}"

94
k6/load-test.js Normal file
View File

@@ -0,0 +1,94 @@
import http from 'k6/http';
import { check, sleep, group } from 'k6';
const vus = parseInt(__ENV.TEST_VUS || '10');
const duration = __ENV.TEST_DURATION || '30s';
const targetUrl = __ENV.TARGET_URL || 'http://test-for-310--006.dev.svc.cluster.local:8080';
export const options = {
scenarios: {
load_test: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '10s', target: vus },
{ duration: duration, target: vus },
{ duration: '5s', target: 0 },
],
},
},
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
http.setResponseCallback(http.expectedStatuses({ min: 200, max: 399 }));
export default function () {
group('Items API', () => {
const listRes = http.get(`${targetUrl}/api/items`);
check(listRes, {
'GET /api/items status is 200': (r) => r.status === 200,
'GET /api/items response time < 500ms': (r) => r.timings.duration < 500,
'GET /api/items returns array': (r) => Array.isArray(r.json()),
});
const createRes = http.post(
`${targetUrl}/api/items`,
JSON.stringify({ name: 'Test Item', description: 'A test item description' }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(createRes, {
'POST /api/items status is 201': (r) => r.status === 201,
'POST /api/items response time < 500ms': (r) => r.timings.duration < 500,
'POST /api/items returns created item': (r) => r.json().id && r.json().name === 'Test Item',
});
const itemId = createRes.json().id;
const getRes = http.get(`${targetUrl}/api/items/${itemId}`);
check(getRes, {
'GET /api/items/{id} status is 200': (r) => r.status === 200,
'GET /api/items/{id} response time < 500ms': (r) => r.timings.duration < 500,
'GET /api/items/{id} returns correct item': (r) => r.json().id === itemId,
});
const updateRes = http.put(
`${targetUrl}/api/items/${itemId}`,
JSON.stringify({ name: 'Updated Item', description: 'Updated description' }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(updateRes, {
'PUT /api/items/{id} status is 200': (r) => r.status === 200,
'PUT /api/items/{id} response time < 500ms': (r) => r.timings.duration < 500,
'PUT /api/items/{id} returns updated item': (r) => r.json().name === 'Updated Item',
});
const deleteRes = http.del(`${targetUrl}/api/items/${itemId}`);
check(deleteRes, {
'DELETE /api/items/{id} status is 200': (r) => r.status === 200,
'DELETE /api/items/{id} response time < 500ms': (r) => r.timings.duration < 500,
'DELETE /api/items/{id} confirms deletion': (r) => r.json().deleted === itemId,
});
});
sleep(0.5);
group('Actuator API', () => {
const healthRes = http.get(`${targetUrl}/actuator/health`);
check(healthRes, {
'GET /actuator/health status is 200': (r) => r.status === 200,
'GET /actuator/health response time < 500ms': (r) => r.timings.duration < 500,
'GET /actuator/health returns UP status': (r) => r.json().status === 'UP',
});
const prometheusRes = http.get(`${targetUrl}/actuator/prometheus`);
check(prometheusRes, {
'GET /actuator/prometheus status is 200': (r) => r.status === 200,
'GET /actuator/prometheus response time < 500ms': (r) => r.timings.duration < 500,
});
});
sleep(0.5);
}

24
k6/testrun.yaml Normal file
View File

@@ -0,0 +1,24 @@
apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
name: k6-test-for-310--006
namespace: dev
labels:
app: test-for-310--006
backstage.io/component: test-for-310--006
app.kubernetes.io/managed-by: backstage
app.kubernetes.io/component: load-testing
spec:
parallelism: 1
script:
configMap:
name: k6-test-test-for-310--006
file: load-test.js
runner:
image: grafana/k6:latest
envFrom:
- configMapRef:
name: k6-test-test-for-310--006
env:
- name: K6_OTEL_SERVICE_NAME
value: k6-test-for-310--006

57
pom.xml Normal file
View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/>
</parent>
<groupId>com.kyndryl.platform</groupId>
<artifactId>test-for-310--006</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test-for-310--006</name>
<description>Test for work on issue 310</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Actuator for health + prometheus metrics -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package com.kyndryl.platform.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,70 @@
package com.kyndryl.platform.service;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* A minimal but fully functional CRUD REST API for "items".
*
* Endpoints:
* GET /api/items — list all items
* POST /api/items — create an item { "name": "...", "description": "..." }
* GET /api/items/{id} — get single item
* PUT /api/items/{id} — update item
* DELETE /api/items/{id} — delete item
*
* Health + metrics are exposed via Spring Actuator at /actuator/health and
* /actuator/prometheus (see application.yml).
*/
@RestController
@RequestMapping("/api/items")
public class ItemsController {
record Item(long id, String name, String description) {}
private final Map<Long, Item> store = new ConcurrentHashMap<>();
private final AtomicLong counter = new AtomicLong(1);
@GetMapping
public Collection<Item> list() {
return store.values();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Item create(@RequestBody Map<String, String> body) {
long id = counter.getAndIncrement();
Item item = new Item(id,
body.getOrDefault("name", "unnamed"),
body.getOrDefault("description", ""));
store.put(id, item);
return item;
}
@GetMapping("/{id}")
public Item get(@PathVariable long id) {
Item item = store.get(id);
if (item == null) throw new NoSuchElementException("Item not found: " + id);
return item;
}
@PutMapping("/{id}")
public Item update(@PathVariable long id, @RequestBody Map<String, String> body) {
if (!store.containsKey(id)) throw new NoSuchElementException("Item not found: " + id);
Item updated = new Item(id,
body.getOrDefault("name", store.get(id).name()),
body.getOrDefault("description", store.get(id).description()));
store.put(id, updated);
return updated;
}
@DeleteMapping("/{id}")
public Map<String, Object> delete(@PathVariable long id) {
store.remove(id);
return Map.of("deleted", id);
}
}

View File

@@ -0,0 +1,21 @@
server:
port: 8080
spring:
application:
name: test-for-310--006
profiles:
active: ${SPRING_PROFILES_ACTIVE:default}
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
base-path: /actuator
endpoint:
health:
show-details: always
metrics:
tags:
application: test-for-310--006

View File

@@ -0,0 +1,12 @@
package com.kyndryl.platform.service;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ApplicationTests {
@Test
void contextLoads() {
}
}