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());
+    }
 }

Reply via email to