This is an automated email from the ASF dual-hosted git repository. DaanHoogland pushed a commit to branch fix-npe-external-template-no-store in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit ed9571df61e0b31bd418338f334cd0dcfa82dec1 Author: Daan Hoogland <[email protected]> AuthorDate: Wed Oct 7 14:33:39 2026 +0200 Fix listTemplates NPE for External templates --- .../storage/datastore/db/TemplateDataStoreVO.java | 8 +++++++ .../cloud/api/query/dao/TemplateJoinDaoImpl.java | 14 ++++++++++-- .../api/query/dao/TemplateJoinDaoImplTest.java | 26 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/TemplateDataStoreVO.java b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/TemplateDataStoreVO.java index a6e7a5a4fea..3097378cb81 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/TemplateDataStoreVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/storage/datastore/db/TemplateDataStoreVO.java @@ -184,6 +184,14 @@ public class TemplateDataStoreVO implements StateObject<ObjectInDataStoreStateMa return dataStoreId; } + /** + * Baremetal and External templates are registered with a store-less entry (store_id NULL), + * for which {@link #getDataStoreId()} cannot be called. + */ + public boolean hasDataStore() { + return dataStoreId != null; + } + public void setDataStoreId(long storeId) { dataStoreId = storeId; } diff --git a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java index bd73fee5c59..27ad87e27cb 100644 --- a/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/TemplateJoinDaoImpl.java @@ -209,11 +209,21 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation<Templa return templateStatus.getStatus(); } + /** + * Lists the not bypassed store entries of a template, leaving out store-less entries (store_id NULL) + * as created for Baremetal and External templates. These are returned when storeIds is empty. + */ + protected List<TemplateDataStoreVO> listTemplatesInImageStores(long templateId, Long... storeIds) { + return _templateStoreDao.listByTemplateNotBypassed(templateId, storeIds).stream() + .filter(TemplateDataStoreVO::hasDataStore) + .collect(Collectors.toList()); + } + @Override public TemplateResponse newTemplateResponse(EnumSet<ApiConstants.DomainDetails> detailsView, ResponseView view, TemplateJoinVO template) { List<ImageStoreVO> storesInZone = dataStoreDao.listStoresByZoneId(template.getDataCenterId()); Long[] storeIds = storesInZone.stream().map(ImageStoreVO::getId).toArray(Long[]::new); - List<TemplateDataStoreVO> templatesInStore = _templateStoreDao.listByTemplateNotBypassed(template.getId(), storeIds); + List<TemplateDataStoreVO> templatesInStore = listTemplatesInImageStores(template.getId(), storeIds); List<Long> dataStoreIdList = templatesInStore.stream().map(TemplateDataStoreVO::getDataStoreId).collect(Collectors.toList()); Map<Long, ImageStoreVO> imageStoreMap = dataStoreDao.listByIds(dataStoreIdList).stream().collect(Collectors.toMap(ImageStoreVO::getId, imageStore -> imageStore)); @@ -532,7 +542,7 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation<Templa isoResponse.setStatus(templateStatus); } isoResponse.setUrl(iso.getUrl()); - List<TemplateDataStoreVO> isosInStore = _templateStoreDao.listByTemplateNotBypassed(iso.getId()); + List<TemplateDataStoreVO> isosInStore = listTemplatesInImageStores(iso.getId()); List<Map<String, String>> downloadProgressDetails = new ArrayList<>(); HashMap<String, String> downloadDetailInImageStores = null; for (TemplateDataStoreVO isoInStore : isosInStore) { diff --git a/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java b/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java index 94d6722a7c5..6b5e30e734f 100755 --- a/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java +++ b/server/src/test/java/com/cloud/api/query/dao/TemplateJoinDaoImplTest.java @@ -27,6 +27,8 @@ import com.cloud.template.TemplateManager; import com.cloud.user.Account; import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.VnfTemplateResponse; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -39,6 +41,7 @@ import org.springframework.test.util.ReflectionTestUtils; import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Map; @RunWith(MockitoJUnitRunner.class) @@ -53,6 +56,9 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas @Mock private VnfTemplateDetailsDao vnfTemplateDetailsDao; + @Mock + private TemplateDataStoreDao templateDataStoreDao; + private TemplateJoinVO template = new TemplateJoinVO(); private TemplateResponse templateResponse = new TemplateResponse(); @@ -150,4 +156,24 @@ public class TemplateJoinDaoImplTest extends GenericDaoBaseWithTagInformationBas Assert.assertEquals(3, ((VnfTemplateResponse)response).getVnfDetails().size()); } + + @Test + public void testListTemplatesInImageStoresSkipsStorelessEntry() { + TemplateDataStoreVO storeless = new TemplateDataStoreVO(null, templateId); + TemplateDataStoreVO inStore = new TemplateDataStoreVO(5L, templateId); + Mockito.doReturn(Arrays.asList(storeless, inStore)).when(templateDataStoreDao).listByTemplateNotBypassed(templateId); + + List<TemplateDataStoreVO> result = _templateJoinDaoImpl.listTemplatesInImageStores(templateId); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals(5L, result.get(0).getDataStoreId()); + } + + @Test + public void testListTemplatesInImageStoresOnlyStorelessEntry() { + TemplateDataStoreVO storeless = new TemplateDataStoreVO(null, templateId); + Mockito.doReturn(List.of(storeless)).when(templateDataStoreDao).listByTemplateNotBypassed(templateId); + + Assert.assertTrue(_templateJoinDaoImpl.listTemplatesInImageStores(templateId).isEmpty()); + } }
