diff --git a/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/account/dto/AccountDetailsDto.java b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/account/dto/AccountDetailsDto.java index e6c2aa483..f749f160a 100644 --- a/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/account/dto/AccountDetailsDto.java +++ b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/account/dto/AccountDetailsDto.java @@ -16,7 +16,7 @@ */ package com.nvidia.nvcf.rest.account.dto; -import com.nvidia.nvcf.rest.registry.dto.RegistryCredentialDto; +import com.nvidia.nvcf.rest.registry.dto.TempRegistryCredentialDetailsDto; import com.nvidia.nvcf.rest.telemetry.dto.TelemetryDto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; @@ -43,7 +43,7 @@ public record AccountDetailsDto( @Nullable List telemetries, @Schema(description = "Registry credentials associated with the account") - @Nullable List registryCredentials, + @Nullable List registryCredentials, @Schema(description = "Maximum number of functions allowed for Account") @NotNull Integer maxFunctionsAllowed, diff --git a/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/registry/dto/TempRegistryCredentialDetailsDto.java b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/registry/dto/TempRegistryCredentialDetailsDto.java new file mode 100644 index 000000000..66673d1de --- /dev/null +++ b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/rest/registry/dto/TempRegistryCredentialDetailsDto.java @@ -0,0 +1,73 @@ +/* + * 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.nvcf.rest.registry.dto; + +import com.nvidia.boot.registries.service.registry.dto.ArtifactTypeEnum; +import com.nvidia.nvcf.rest.function.management.dto.SecretDto; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.time.Instant; +import java.util.Set; +import java.util.UUID; +import lombok.Builder; + +@Builder(toBuilder = true) +@Schema(description = "Temporary registry credential details, including the resolved secret") +public record TempRegistryCredentialDetailsDto( + @Schema(description = "Registry Credential Id") + @NotNull UUID registryCredentialId, + + @Schema(description = "NVIDIA Cloud Account Id owning the Registry Credential") + @NotBlank String ncaId, + + @Schema(description = "Registry Credential name") + @NotBlank String registryCredentialName, + + @Schema(description = "Recognized registry name") + @NotBlank String registryName, + + @Schema(description = "Registry hostname") + @NotBlank String registryHostname, + + @Schema(description = "Registry type") + @NotNull @NotEmpty Set artifactTypes, + + @Schema(description = "Optional set of tags") + @Nullable Set tags, + + @Schema(description = "Registry credential description") + @Nullable String description, + + @Schema(description = "Registry credential provisioned by system or user") + @NotNull ProvisionedByEnum provisionedBy, + + @Schema(description = "Optional registry credential key type") + @Nullable String keyType, + + @Schema(description = "Timestamp for last registry credential update") + @NotNull Instant lastUpdatedAt, + + @Schema(description = "Timestamp for registry credential creation") + @NotNull Instant createdAt, + + @Schema(description = "Registry credential - secret value must be base64 encoded " + + "string in username:password format") + @NotNull SecretDto secret) { +} diff --git a/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/service/account/AccountMapperService.java b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/service/account/AccountMapperService.java index 9c9c3f2d5..c2f1841e5 100644 --- a/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/service/account/AccountMapperService.java +++ b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/service/account/AccountMapperService.java @@ -27,7 +27,7 @@ import com.nvidia.nvcf.rest.account.dto.AccountDto; import com.nvidia.nvcf.rest.account.dto.CreateAccountRequest; import com.nvidia.nvcf.rest.registry.dto.RegistryCredentialDetailsDto; -import com.nvidia.nvcf.rest.registry.dto.RegistryCredentialDto; +import com.nvidia.nvcf.rest.registry.dto.TempRegistryCredentialDetailsDto; import com.nvidia.nvcf.service.registry.RegistryFunctionMapperService; import com.nvidia.nvcf.service.telemetry.TelemetryMapperService; import com.nvidia.nvcf.configuration.account.AccountLimitsProperties; @@ -76,7 +76,7 @@ public AccountDetailsDto toAccountDetailsDto( AccountEntity accountEntity, Stream telemetryByAccountEntities, List registryCredentialDetailsDtos) { - var registryCredentials = toRegistryCredentialDtos(registryCredentialDetailsDtos); + var registryCredentials = toRegistryCredentialDetailsDtos(registryCredentialDetailsDtos); var ncaId = accountEntity.getNcaId(); var telemetryDtos = telemetryMapperService.toTelemetryDtos(telemetryByAccountEntities); var currentNumberOfFunctions = (int) functionsRepository.countByNcaId(ncaId); @@ -141,10 +141,10 @@ public static Optional toClientEntity( } @Nullable - private List toRegistryCredentialDtos( + private List toRegistryCredentialDetailsDtos( List registryCredentialDetailsDtos) { var registryCredentials = registryCredentialDetailsDtos.stream() - .map(registryFunctionMapperService::toRegistryCredentialDto) + .map(registryFunctionMapperService::toTempRegistryCredentialDetailsDto) .filter(Objects::nonNull) .toList(); diff --git a/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/service/registry/RegistryFunctionMapperService.java b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/service/registry/RegistryFunctionMapperService.java index 10215848b..f98811fbf 100644 --- a/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/service/registry/RegistryFunctionMapperService.java +++ b/src/control-plane-services/cloud-functions/nvcf-core/src/main/java/com/nvidia/nvcf/service/registry/RegistryFunctionMapperService.java @@ -28,6 +28,7 @@ import com.nvidia.nvcf.rest.registry.dto.ProvisionedByEnum; import com.nvidia.nvcf.rest.registry.dto.RegistryCredentialDetailsDto; import com.nvidia.nvcf.rest.registry.dto.RegistryCredentialDto; +import com.nvidia.nvcf.rest.registry.dto.TempRegistryCredentialDetailsDto; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Base64; @@ -93,6 +94,48 @@ public RegistryCredentialDto toRegistryCredentialDto( }); } + public TempRegistryCredentialDetailsDto toTempRegistryCredentialDetailsDto( + RegistryCredentialDetailsDto registryCredentialDetailsDto) { + var registryCredentialId = registryCredentialDetailsDto.registryCredentialId(); + var ncaId = registryCredentialDetailsDto.ncaId(); + + return registryCredentialEssService + .getRegistryCredentialSecret(ncaId, registryCredentialId) + .map(secret -> TempRegistryCredentialDetailsDto.builder() + .registryCredentialId(registryCredentialId) + .ncaId(ncaId) + .registryCredentialName(registryCredentialDetailsDto.registryCredentialName()) + .registryName(registryCredentialDetailsDto.registryName()) + .registryHostname(registryCredentialDetailsDto.registryHostname()) + .artifactTypes(registryCredentialDetailsDto.artifactTypes()) + .tags(registryCredentialDetailsDto.tags()) + .description(registryCredentialDetailsDto.description()) + .provisionedBy(registryCredentialDetailsDto.provisionedBy()) + .keyType(registryCredentialDetailsDto.keyType()) + .lastUpdatedAt(registryCredentialDetailsDto.lastUpdatedAt()) + .createdAt(registryCredentialDetailsDto.createdAt()) + .secret(secret) + .build()) + .orElseGet(() -> { + // Secret not found in ESS. Check if the registry credential still exists in DB. + // If registry credential still exists in db, it's probably because of the + // db replication delay after deletion. Log instead of throwing an error. + var existsInDb = registryCredentialsByAccountRepository + .findByKeyNcaIdAndKeyRegistryCredentialId(ncaId, registryCredentialId) + .isPresent(); + if (existsInDb) { + var mesg = MESG_MISSING_REGISTRY_SECRET + .formatted(ncaId, registryCredentialId); + log.error(mesg); + } else { + // If the registry credential doesn't exist in the DB + // it was perhaps deleted and cache has a stale entry + log.debug(MESG_STALE_CACHED_ENTRY, ncaId, registryCredentialId); + } + return null; + }); + } + public RegistryCredentialDetailsDto toRegistryCredentialDetailsDto( RegistryCredentialByAccountEntity entity) { return RegistryCredentialDetailsDto.builder() diff --git a/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/rest/account/AccountControllerTest.java b/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/rest/account/AccountControllerTest.java index daba6d0da..d8abcb764 100644 --- a/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/rest/account/AccountControllerTest.java +++ b/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/rest/account/AccountControllerTest.java @@ -419,6 +419,11 @@ void shouldGetAccountDetails( .isEqualTo(NvcfConstants.DEFAULT_MAX_REGISTRY_CREDENTIALS_ALLOWED); assertThat(responseBody.account().lastUpdatedAt()).isNotNull(); assertThat(responseBody.account().registryCredentials()).isNotEmpty().hasSize(3); + assertThat(responseBody.account().registryCredentials()) + .allSatisfy(registryCredential -> { + assertThat(registryCredential.registryCredentialId()).isNotNull(); + assertThat(registryCredential.secret()).isNotNull(); + }); } Stream getAccountWithTelemetryArgs() { diff --git a/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/service/registry/RegistryFunctionMapperServiceTest.java b/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/service/registry/RegistryFunctionMapperServiceTest.java index ca6e302f3..59207a753 100644 --- a/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/service/registry/RegistryFunctionMapperServiceTest.java +++ b/src/control-plane-services/cloud-functions/nvcf-core/src/test/java/com/nvidia/nvcf/service/registry/RegistryFunctionMapperServiceTest.java @@ -20,12 +20,14 @@ import static org.mockito.Mockito.when; import com.nvidia.boot.registries.service.registry.RegistryMapperService; +import com.nvidia.boot.registries.service.registry.dto.ArtifactTypeEnum; import com.nvidia.nvcf.persistence.registry.RegistryCredentialsByAccountRepository; import com.nvidia.nvcf.persistence.registry.entity.ArtifactType; import com.nvidia.nvcf.persistence.registry.entity.ProvisionedBy; import com.nvidia.nvcf.persistence.registry.entity.RegistryCredentialByAccountEntity; import com.nvidia.nvcf.persistence.registry.entity.RegistryCredentialByAccountKey; import com.nvidia.nvcf.rest.function.management.dto.SecretDto; +import com.nvidia.nvcf.rest.registry.dto.RegistryCredentialDetailsDto; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Optional; @@ -70,6 +72,49 @@ void shouldSetKeyTypeOnlyForLegacyNgcCredentials( assertThat(result.keyType()).isEqualTo(expectedKeyType); } + @Test + void shouldPopulateRegistryCredentialIdWhenMappingToTempRegistryCredentialDetailsDto() { + var registryCredentialId = UUID.randomUUID(); + var ncaId = "account-id"; + var detailsDto = RegistryCredentialDetailsDto.builder() + .registryCredentialId(registryCredentialId) + .ncaId(ncaId) + .registryName("ngc") + .registryHostname("nvcr.io") + .registryCredentialName("registry-credential") + .artifactTypes(Set.of(ArtifactTypeEnum.CONTAINER)) + .build(); + when(registryCredentialEssService.getRegistryCredentialSecret(ncaId, registryCredentialId)) + .thenReturn(Optional.of(secret("$oauthtoken:nvapi-key"))); + + var result = mapper().toTempRegistryCredentialDetailsDto(detailsDto); + + assertThat(result).isNotNull(); + assertThat(result.registryCredentialId()).isEqualTo(registryCredentialId); + assertThat(result.registryHostname()).isEqualTo("nvcr.io"); + assertThat(result.secret()).isNotNull(); + } + + @Test + void shouldReturnNullTempRegistryCredentialDetailsDtoWhenEssSecretIsUnavailable() { + var registryCredentialId = UUID.randomUUID(); + var ncaId = "account-id"; + var detailsDto = RegistryCredentialDetailsDto.builder() + .registryCredentialId(registryCredentialId) + .ncaId(ncaId) + .registryName("ngc") + .registryHostname("nvcr.io") + .registryCredentialName("registry-credential") + .artifactTypes(Set.of(ArtifactTypeEnum.CONTAINER)) + .build(); + when(registryCredentialEssService.getRegistryCredentialSecret(ncaId, registryCredentialId)) + .thenReturn(Optional.empty()); + + var result = mapper().toTempRegistryCredentialDetailsDto(detailsDto); + + assertThat(result).isNull(); + } + @Test void shouldLeaveKeyTypeNullWhenEssSecretIsUnavailable() { var entity = registryCredential("helm.ngc.nvidia.com");