diff --git a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/CoreAutoConfiguration.java b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/CoreAutoConfiguration.java index f5bd919f3..9ae1ae1df 100644 --- a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/CoreAutoConfiguration.java +++ b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/CoreAutoConfiguration.java @@ -20,6 +20,7 @@ import com.nvidia.boot.core.cors.ServletCoreCorsConfiguration; import com.nvidia.boot.core.cors.ReactiveCoreCorsConfiguration; import com.nvidia.boot.core.health.HealthConfiguration; +import com.nvidia.boot.core.info.InfoConfiguration; import com.nvidia.boot.core.openapi.OpenApiConfiguration; import com.nvidia.boot.core.openapi.ServletOpenApiCorsConfiguration; import com.nvidia.boot.core.openapi.ReactiveOpenApiCorsConfiguration; @@ -29,6 +30,7 @@ @AutoConfiguration @Import({ HealthConfiguration.class, + InfoConfiguration.class, OpenApiConfiguration.class, ReactiveCoreCorsConfiguration.class, ReactiveOpenApiCorsConfiguration.class, diff --git a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/env/BootCoreEnvironmentPostProcessor.java b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/env/BootCoreEnvironmentPostProcessor.java index 17d45a010..9bd957106 100644 --- a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/env/BootCoreEnvironmentPostProcessor.java +++ b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/env/BootCoreEnvironmentPostProcessor.java @@ -107,6 +107,11 @@ private void loadAppVersionFromGitProperties(ConfigurableEnvironment environment versionProps.put("app.git.commit", commitId); } + var commitIdFull = gitProperties.getProperty("git.commit.id.full"); + if (StringUtils.isNotBlank(commitIdFull)) { + versionProps.put("app.git.commit.full", commitIdFull); + } + var branch = gitProperties.getProperty("git.branch"); if (StringUtils.isNotBlank(branch)) { versionProps.put("app.git.branch", branch); diff --git a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/info/InfoConfiguration.java b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/info/InfoConfiguration.java new file mode 100644 index 000000000..6b1592e11 --- /dev/null +++ b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/info/InfoConfiguration.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nvidia.boot.core.info; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +/** Auto-configuration for the shared {@code GET /info} endpoint and controller. */ +@Configuration +@ConditionalOnWebApplication +public class InfoConfiguration { + + @Bean + public InfoResponseService infoResponseService(Environment environment) { + return new InfoResponseService(environment); + } + + @Bean + public InfoController infoController(InfoResponseService infoResponseService) { + return new InfoController(infoResponseService); + } +} diff --git a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/info/InfoController.java b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/info/InfoController.java new file mode 100644 index 000000000..b746040db --- /dev/null +++ b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/info/InfoController.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nvidia.boot.core.info; + +import com.nvidia.boot.core.info.InfoResponseService.InfoResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Shared build-info controller for {@code GET /info}. Returns a flat {service, version, commit} + * body, matching the equivalent Go services' contract, so build identification is consistent + * across NVCF control plane services. + */ +@RestController +@RequiredArgsConstructor +public class InfoController { + + private final InfoResponseService infoResponseService; + + @GetMapping("/info") + public ResponseEntity getInfo() { + return ResponseEntity.ok(infoResponseService.getInfo()); + } +} diff --git a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/info/InfoResponseService.java b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/info/InfoResponseService.java new file mode 100644 index 000000000..dce7b729c --- /dev/null +++ b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/main/java/com/nvidia/boot/core/info/InfoResponseService.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nvidia.boot.core.info; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.env.Environment; + +@RequiredArgsConstructor +public class InfoResponseService { + + private static final String UNKNOWN = "unknown"; + + private final Environment environment; + + public InfoResponse getInfo() { + return new InfoResponse( + resolve("spring.application.name"), + resolve("spring.application.version"), + resolve("app.git.commit.full")); + } + + private String resolve(String key) { + String value = environment.getProperty(key); + return StringUtils.isBlank(value) ? UNKNOWN : value; + } + + public record InfoResponse(String service, String version, String commit) { + } +} diff --git a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/env/BootCoreEnvironmentPostProcessorTest.java b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/env/BootCoreEnvironmentPostProcessorTest.java index f3ae70d46..6e168e575 100644 --- a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/env/BootCoreEnvironmentPostProcessorTest.java +++ b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/env/BootCoreEnvironmentPostProcessorTest.java @@ -54,11 +54,13 @@ void usesClosestTagNameAsVersionWhenPresent() { git.closest.tag.name=v2.5.0 git.build.version=2.5.0-SNAPSHOT git.commit.id.abbrev=abc1234 + git.commit.id.full=abc1234def5678901234567890abcdef12345678 """); assertThat(env.getProperty("spring.application.version")).isEqualTo("v2.5.0"); assertThat(env.getProperty("app.git.tag")).isEqualTo("v2.5.0"); assertThat(env.getProperty("app.git.commit")).isEqualTo("abc1234"); + assertThat(env.getProperty("app.git.commit.full")).isEqualTo("abc1234def5678901234567890abcdef12345678"); assertThat(env.getProperty("app.git.branch")).isNull(); } @@ -96,6 +98,7 @@ void fallsBackToUnknownWhenAllVersionFieldsAbsent() { assertThat(env.getProperty("spring.application.version")).isEqualTo("unknown"); assertThat(env.getProperty("app.git.tag")).isNull(); assertThat(env.getProperty("app.git.commit")).isNull(); + assertThat(env.getProperty("app.git.commit.full")).isNull(); assertThat(env.getProperty("app.git.branch")).isEqualTo("main"); } @@ -105,10 +108,12 @@ void populatesAllGitMetadataProperties() { git.closest.tag.name=v2.5.0 git.build.version=2.5.0-SNAPSHOT git.commit.id.abbrev=abc1234 + git.commit.id.full=abc1234def5678901234567890abcdef12345678 git.branch=main """); assertThat(env.getProperty("app.git.commit")).isEqualTo("abc1234"); + assertThat(env.getProperty("app.git.commit.full")).isEqualTo("abc1234def5678901234567890abcdef12345678"); assertThat(env.getProperty("app.git.branch")).isEqualTo("main"); assertThat(env.getProperty("app.git.tag")).isEqualTo("v2.5.0"); } diff --git a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/info/InfoControllerTest.java b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/info/InfoControllerTest.java new file mode 100644 index 000000000..aff8690f6 --- /dev/null +++ b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/info/InfoControllerTest.java @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nvidia.boot.core.info; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.nvidia.boot.core.info.InfoResponseService.InfoResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class InfoControllerTest { + + private InfoResponseService infoResponseService; + private InfoController controller; + + @BeforeEach + void setUp() { + infoResponseService = mock(InfoResponseService.class); + controller = new InfoController(infoResponseService); + } + + @Test + void getInfoReturnsOkWithResponseBody() { + var expected = new InfoResponse("nvcf-ess", "v1.2.3", "77c5d932abcdef1234567890abcdef1234567890"); + when(infoResponseService.getInfo()).thenReturn(expected); + + var response = controller.getInfo(); + + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + assertThat(response.getBody()).isEqualTo(expected); + } +} diff --git a/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/info/InfoResponseServiceTest.java b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/info/InfoResponseServiceTest.java new file mode 100644 index 000000000..177eccd4f --- /dev/null +++ b/src/libraries/java/nv-boot-parent/nv-boot-starter-core/src/test/java/com/nvidia/boot/core/info/InfoResponseServiceTest.java @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nvidia.boot.core.info; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.nvidia.boot.core.info.InfoResponseService.InfoResponse; +import org.junit.jupiter.api.Test; +import org.springframework.mock.env.MockEnvironment; + +class InfoResponseServiceTest { + + @Test + void buildsResponseFromEnvironmentProperties() { + var environment = new MockEnvironment() + .withProperty("spring.application.name", "nvcf-ess") + .withProperty("spring.application.version", "v1.2.3") + .withProperty("app.git.commit.full", "77c5d932abcdef1234567890abcdef1234567890"); + + var service = new InfoResponseService(environment); + + assertThat(service.getInfo()) + .isEqualTo(new InfoResponse("nvcf-ess", "v1.2.3", "77c5d932abcdef1234567890abcdef1234567890")); + } + + @Test + void fallsBackToUnknownWhenPropertiesAbsent() { + var service = new InfoResponseService(new MockEnvironment()); + + assertThat(service.getInfo()).isEqualTo(new InfoResponse("unknown", "unknown", "unknown")); + } + + @Test + void fallsBackToUnknownWhenPropertiesBlank() { + var environment = new MockEnvironment() + .withProperty("spring.application.name", " ") + .withProperty("spring.application.version", "") + .withProperty("app.git.commit.full", ""); + + var service = new InfoResponseService(environment); + + assertThat(service.getInfo()).isEqualTo(new InfoResponse("unknown", "unknown", "unknown")); + } +}