Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.jar binary
* text eol=lf
*.patch text eol=lf
59 changes: 59 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Agent Instructions and Reminders

This file contains important reminders and guidelines for AI agents working on this codebase.

## Build Script

### Avoid `-DisableCache` Flag

**Do NOT use `-DisableCache`** when running `build.ps1` from agentic contexts. The `start.spring.io` service may block or rate-limit automated traffic, causing connection failures.

Instead, to get a fresh build:

1. Delete the expanded project folder (e.g., `workspace/springbootadmin/`)
2. Run `.\build.ps1 <image-name>` without the flag

### Testing Changes

Before submitting patch changes:

1. Run a dry-run of each patch: `git apply --check <patch-file>`
2. If dry-run succeeds, run the full build and verify Java compilation
3. Test the resulting Docker image with a real client app

## Patch Files

The build script uses `git apply --unidiff-zero --recount --ignore-whitespace` to apply patches, which is more forgiving than the traditional `patch` command.

### Patch Format Rules

1. **Hunk headers should be accurate**: The format is `@@ -old_start,old_count +new_start,new_count @@`
- `old_count` is the number of lines in the hunk from the old file (context lines plus lines with `-` prefix)
- `new_count` is the number of lines in the hunk in the new file (context lines plus lines with `+` prefix)
- For new file patches (`--- /dev/null`), `old_count` is 0 and `new_count` is the total number of lines in the new-file hunk
- Note: `--recount` will automatically correct line counts, but keeping them accurate is still good practice
2. **Trailing newlines are required**: Patch files must end with a newline character.
3. **Preserve exact whitespace**: Context lines must match the target file exactly, including trailing spaces and tabs. The `--ignore-whitespace` flag provides some tolerance but exact matches are preferred.
4. **New file patches**: Use `/dev/null` as the old file:

```diff
--- /dev/null
+++ ./path/to/NewFile.java 2026-01-27 00:00:00.000000000 +0000
@@ -0,0 +1,N @@
+line 1
+line 2
...
```

### Example

If a patch adds 1 line, the hunk header should reflect this:

```diff
-@@ -37,3 +37,10 @@
+@@ -37,3 +37,11 @@
```

### Why This Matters

While `git apply --recount` can fix minor line count issues, keeping patches accurate ensures reliable application and easier debugging.
25 changes: 10 additions & 15 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ try {
$DockerOrg = $Registry
}
else {
$DockerOrg = "steeltoeoss"
$DockerOrg = "steeltoe.azurecr.io"
}

if ($Help) {
Expand Down Expand Up @@ -112,11 +112,11 @@ try {
if (!$Tag) {
if ($env:GITHUB_ACTIONS -eq "true") {
$ImageNameWithTag = "$DockerOrg/${Name}:$Version"
$Revision = Get-Content (Join-Path $ImageDirectory "metadata" "IMAGE_REVISION")
if ($Revision) {
$Revision = (Get-Content (Join-Path $ImageDirectory "metadata" "IMAGE_REVISION") -ErrorAction SilentlyContinue | ForEach-Object { $_.Trim() }) -join ""
if ($Revision -and $Revision -ne "") {
$ImageNameWithTag += "-$Revision"
}
$AdditionalTags = "$(Get-Content (Join-Path $ImageDirectory "metadata" "ADDITIONAL_TAGS") | ForEach-Object { $_.replace("$Name","$DockerOrg/$Name") })"
$AdditionalTags = "$(Get-Content (Join-Path $ImageDirectory "metadata" "ADDITIONAL_TAGS") -ErrorAction SilentlyContinue | ForEach-Object { $_.replace("$Name","$DockerOrg/$Name") })"
}
else {
$ImageNameWithTag = "$DockerOrg/${Name}:dev"
Expand Down Expand Up @@ -149,14 +149,8 @@ try {
Invoke-Expression $docker_command
}
else {
if (!(Get-Command "patch" -ErrorAction SilentlyContinue)) {
if (Test-Path "$Env:ProgramFiles\Git\usr\bin\patch.exe") {
Write-Host "'patch' command not found, but Git is installed; adding Git usr\bin to PATH"
$env:Path += ";$Env:ProgramFiles\Git\usr\bin"
}
else {
throw "'patch' command not found"
}
if (!(Get-Command "git" -ErrorAction SilentlyContinue)) {
throw "'git' command not found"
}

switch ($Name) {
Expand Down Expand Up @@ -207,7 +201,7 @@ try {

# Scaffold project on start.spring.io
if (!(Test-Path "$artifactName")) {
Write-Host "Using start.spring.io to create project"
Write-Host "Using start.spring.io to create project with dependencies: $dependencies"
Invoke-WebRequest `
-Uri "https://start.spring.io/starter.zip" `
-Method Post `
Expand Down Expand Up @@ -238,10 +232,11 @@ try {
# Apply patches
foreach ($patch in Get-ChildItem -Path (Join-Path $ImageDirectory patches) -Filter "*.patch") {
Write-Host "Applying patch $($patch.Name)"
Get-Content $patch | & patch -p1
git apply --unidiff-zero --recount --ignore-whitespace $patch.FullName
if ($LASTEXITCODE -ne 0) {
throw "Patch failed with exit code $LASTEXITCODE"
throw "Patch $($patch.Name) failed with exit code $LASTEXITCODE"
}
Write-Host "Patch $($patch.Name) applied successfully"
}

# Build the image
Expand Down
2 changes: 1 addition & 1 deletion config-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ docker run --publish 8888:8888 steeltoe.azurecr.io/config-server \
| ---- | ----------- |
| /_{app}_/_{profile}_ | Configuration data for app in Spring profile |
| /_{app}_/_{profile}_/_{label}_ | Add a git label |
| /_{app}_/_{profiles}/{label}_/_{path}_ | Environment-specific plain text config file at _{path}_|
| /_{app}_/_{profiles}/{label}_/_{path}_ | Environment-specific plain text config file at _{path}_ |

_Example:_ <http://localhost:8888/foo/bar>
2 changes: 1 addition & 1 deletion config-server/metadata/IMAGE_REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1

2 changes: 1 addition & 1 deletion config-server/metadata/IMAGE_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.3.0
4.3.1
2 changes: 1 addition & 1 deletion config-server/metadata/SPRING_BOOT_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.5.6
3.5.10
5 changes: 3 additions & 2 deletions config-server/patches/build.gradle.patch
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
--- ./build.gradle 2025-09-30 14:48:20.000000000 -0500
+++ ./build.gradle 2025-09-30 14:49:16.584226000 -0500
@@ -41,3 +41,10 @@
@@ -41,3 +41,11 @@
tasks.named('test') {
useJUnitPlatform()
}
+
+bootBuildImage {
+ createdDate = "now"
+ environment = [
+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true"
+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true",
+ "BP_JVM_AOTCACHE_ENABLED": "true"
+ ]
+}
1 change: 1 addition & 0 deletions eureka-server/metadata/IMAGE_REVISION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
2 changes: 1 addition & 1 deletion eureka-server/metadata/SPRING_BOOT_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.5.6
3.5.10
9 changes: 7 additions & 2 deletions eureka-server/patches/application.properties.patch
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
--- eurekaserver/src/main/resources/application.properties 2024-02-21 15:43:09.000000000 -0600
+++ eurekaserver/src/main/resources/application.properties 2024-04-02 13:15:18.461432100 -0500
@@ -0,0 +1,9 @@
+++ eurekaserver/src/main/resources/application.properties 2026-01-27 00:00:00.000000000 -0500
@@ -0,0 +1,14 @@
+server.port = 8761
+eureka.client.fetch-registry = false
+eureka.client.register-with-eureka = false
+eureka.client.serviceUrl.defaultZone = ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka/}
+eureka.instance.hostname = localhost
+# Set myUrl to match defaultZone so Eureka recognizes itself and skips self-replication
+eureka.server.myUrl = ${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka/}
+eureka.server.enable-self-preservation = false
+eureka.server.numberOfReplicationRetries = 0
+eureka.server.evictionIntervalTimerInMs = 1000
+eureka.server.responseCacheUpdateIntervalMs = 1000
+eureka.server.wait-time-in-ms-when-sync-empty = 0
Expand Down
5 changes: 3 additions & 2 deletions eureka-server/patches/build.gradle.patch
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
--- ./build.gradle 2025-09-30 14:48:20.000000000 -0500
+++ ./build.gradle 2025-09-30 14:49:16.584226000 -0500
@@ -41,3 +41,10 @@
@@ -41,3 +41,11 @@
tasks.named('test') {
useJUnitPlatform()
}
+
+bootBuildImage {
+ createdDate = "now"
+ environment = [
+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true"
+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true",
+ "BP_JVM_AOTCACHE_ENABLED": "true"
+ ]
+}
2 changes: 1 addition & 1 deletion spring-boot-admin/metadata/IMAGE_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.5.5
3.5.7
2 changes: 1 addition & 1 deletion spring-boot-admin/metadata/SPRING_BOOT_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.5.6
3.5.10
5 changes: 3 additions & 2 deletions spring-boot-admin/patches/application.properties.patch
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
--- ./src/main/resources/application.properties 2025-10-01 14:13:49.968047867 -0500
+++ ./src/main/resources/application.properties 2025-10-01 14:13:24.727639700 -0500
@@ -0,0 +1,2 @@
+++ ./src/main/resources/application.properties 2026-01-27 00:00:00.000000000 -0500
@@ -0,0 +1,3 @@
+server.port=9099
+spring.thymeleaf.check-template-location=false
+logging.level.io.steeltoe.docker=INFO
5 changes: 3 additions & 2 deletions spring-boot-admin/patches/build.gradle.patch
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
--- ./build.gradle 2025-09-22 14:48:20.000000000 -0500
+++ ./build.gradle 2026-01-27 00:00:00.000000000 -0500
@@ -38,3 +38,10 @@
@@ -38,3 +38,11 @@
tasks.named('test') {
useJUnitPlatform()
}
+
+bootBuildImage {
+ createdDate = "now"
+ environment = [
+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true"
+ "BP_SPRING_CLOUD_BINDINGS_DISABLED": "true",
+ "BP_NATIVE_IMAGE_BUILD_ARGUMENTS": "-H:+UnlockExperimentalVMOptions"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to https://answers.ycrash.io/question/what-is-unlock-experimental-vm-options--xxunlockexperimentalvmoptions?q=702:

The -XX:+UnlockExperimentalVMOptions flag unlocks experimental JVM features. These features may be unstable and are therefore not available by default, so this flag is required to make them available.
Using experimental features enabled by this flag is not encouraged in a production system. They may crash or cause unexpected behaviour. Also the JVM vendor will not provide support for experimental features.

Do we really need this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added UnlockExperimentalVMOptions to avoid this build log entry:

[creator]      1 experimental option(s) unlocked:
[creator]      - '-H:Name' (alternative API option(s): -o io.steeltoe.docker.springbootadmin.SpringBootAdmin; origin(s): command line)  

We could remove it and just ignore the warning. Here's an explanation of what that does:

The -H:Name is being passed by the Paketo native-image buildpack itself, not by us. When it builds the native image, it automatically sets -H:Name=/layers/.../io.steeltoe.docker.springbootadmin.SpringBootAdmin based on the Spring Boot main class. We can't easily override this - it's internal to the buildpack. The UnlockExperimentalVMOptions is the correct workaround until Paketo updates their buildpack to use the non-experimental -o syntax.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be safer not to enable experimental features unless they negatively affect our usage beyond the warning.

+ ]
+}
8 changes: 4 additions & 4 deletions spring-boot-admin/patches/enable-springbootadmin.patch
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
--- ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdmin.java 2024-09-20 12:49:35.099908129 -0500
+++ ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdmin.java 2024-09-20 12:49:59.410273961 -0500
@@ -2,8 +2,10 @@
+++ ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdmin.java 2026-01-27 00:00:00.000000000 -0500
@@ -2,7 +2,11 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Import;
+import de.codecentric.boot.admin.server.config.EnableAdminServer;

@SpringBootApplication
+@EnableAdminServer
+@Import(SteeltoeAdminConfiguration.class)
public class SpringBootAdmin {

public static void main(String[] args) {
82 changes: 82 additions & 0 deletions spring-boot-admin/patches/spring-boot-admin-ssl-config.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
--- /dev/null
+++ ./src/main/java/io/steeltoe/docker/springbootadmin/SpringBootAdminSslConfiguration.java 2026-01-27 00:00:00.000000000 +0000
@@ -0,0 +1,79 @@
+package io.steeltoe.docker.springbootadmin;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.reactive.ClientHttpConnector;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import io.netty.handler.ssl.SslContext;
+import reactor.netty.http.client.HttpClient;
+import reactor.netty.tcp.SslProvider;
+import reactor.netty.tcp.TcpSslContextSpec;
+
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Spring Boot Admin SSL Configuration
+ *
+ * Configures Spring Boot Admin's WebClient to use the shared SSL trust manager
+ * for trusting development certificates (e.g., ASP.NET Core development certificates).
+ *
+ * Uses ObjectProvider for AOT compatibility - the trust manager is resolved at runtime
+ * when the bean method is called, not at configuration class construction time.
+ */
+@Configuration
+public class SpringBootAdminSslConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(SpringBootAdminSslConfiguration.class);
+ private final ObjectProvider<X509TrustManager> trustManagerProvider;
+
+ public SpringBootAdminSslConfiguration(ObjectProvider<X509TrustManager> trustManagerProvider) {
+ this.trustManagerProvider = trustManagerProvider;
+ }
+
+ /**
+ * Provides a ClientHttpConnector with custom SSL trust for Spring Boot Admin's WebClient.
+ *
+ * Uses ObjectProvider to defer trust manager resolution until runtime, making this
+ * AOT-compatible. The trust manager is resolved when this bean method is called,
+ * not during AOT processing at build time.
+ */
+ @Bean
+ @ConditionalOnMissingBean(ClientHttpConnector.class)
+ public ClientHttpConnector clientHttpConnector() {
+ logger.info("Configuring Spring Boot Admin WebClient with SSL trust support");
+ X509TrustManager trustManager = trustManagerProvider.getIfAvailable();
+
+ if (trustManager == null) {
+ logger.debug("No custom X509TrustManager available, using default SSL configuration");
+ return new ReactorClientHttpConnector(HttpClient.create());
+ }
+
+ try {
+ logger.info("Using custom X509TrustManager for Spring Boot Admin WebClient");
+ // Build SslContext first to avoid deprecated sslContext(ProtocolSslContextSpec) method
+ SslContext sslContext = TcpSslContextSpec.forClient()
+ .configure(sslContextBuilder -> {
+ sslContextBuilder.trustManager(trustManager);
+ })
+ .sslContext();
+
+ SslProvider sslProvider = SslProvider.builder()
+ .sslContext(sslContext)
+ .build();
+
+ HttpClient httpClient = HttpClient.create()
+ .secure(sslProvider);
+
+ logger.debug("Configured Spring Boot Admin WebClient with custom SSL trust");
+ return new ReactorClientHttpConnector(httpClient);
+ } catch (Exception e) {
+ logger.error("Failed to configure SSL trust for Spring Boot Admin WebClient, using default", e);
+ // Fall back to default connector if SSL configuration fails
+ return new ReactorClientHttpConnector(HttpClient.create());
+ }
+ }
+}
Loading