From e46374ee3c507a4974403b6c5f12efa6ccbbbc46 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 27 Apr 2026 19:24:25 +0300 Subject: [PATCH] feat(solutions): track tenant telemetry/attribute keys for uninstall cleanup SolutionTemplateInstalledItemDescriptor and SolutionInstallResponse now carry tenantTelemetryKeys and tenantAttributeKeys lists. SolutionService .deleteSolution takes the full descriptor (instead of just the created-entity list) so uninstall can also clean up tenant-scoped telemetry/attributes. Update IotHub install descriptor population and delete call sites; force-update path logs and continues if delete throws. --- .../service/iot_hub/DefaultIotHubService.java | 11 ++- .../solutions/DefaultSolutionService.java | 73 +++++++++++++++---- .../service/solutions/SolutionService.java | 4 +- .../solution/SolutionInstallResponse.java | 12 +++ ...lutionTemplateInstalledItemDescriptor.java | 2 + 5 files changed, 86 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java index 77cd7588cb..40c7a824a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java +++ b/application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java @@ -24,6 +24,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -243,6 +244,8 @@ public class DefaultIotHubService implements IotHubService { } SolutionTemplateInstalledItemDescriptor descriptor = new SolutionTemplateInstalledItemDescriptor(); descriptor.setCreatedEntityIds(response.getCreatedEntityIds()); + descriptor.setTenantTelemetryKeys(response.getTenantTelemetryKeys()); + descriptor.setTenantAttributeKeys(response.getTenantAttributeKeys()); descriptor.setDashboardId(response.getDashboardId()); descriptor.setPublicId(response.getPublicId()); descriptor.setMainDashboardPublic(response.isMainDashboardPublic()); @@ -301,7 +304,7 @@ public class DefaultIotHubService implements IotHubService { } case "SOLUTION_TEMPLATE" -> { SolutionTemplateInstalledItemDescriptor stDescriptor = (SolutionTemplateInstalledItemDescriptor) descriptor; - solutionService.deleteSolution(tenantId, stDescriptor.getCreatedEntityIds(), user); + solutionService.deleteSolution(tenantId, stDescriptor, user); SolutionInstallResponse response = solutionService.installSolution(user, tenantId, fileData, request); if (!response.isSuccess()) { throw new RuntimeException(response.getDetails()); @@ -591,7 +594,11 @@ public class DefaultIotHubService implements IotHubService { } else if (descriptor instanceof DeviceInstalledItemDescriptor dd) { deleteDevicePackageEntities(tenantId, dd.getCreatedEntityIds(), user); } else if (descriptor instanceof SolutionTemplateInstalledItemDescriptor st) { - solutionService.deleteSolution(tenantId, st.getCreatedEntityIds(), user); + try { + solutionService.deleteSolution(tenantId, st, user); + } catch (ThingsboardException e) { + log.error("[{}] Failed to delete solution for installed item {}", tenantId, installedItemId, e); + } } iotHubInstalledItemService.deleteById(tenantId, installedItemId); diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java index 882400b258..85489e82fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java @@ -37,6 +37,11 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.iot_hub.SolutionTemplateInstalledItemDescriptor; +import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; +import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.EntityType; @@ -85,6 +90,7 @@ import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.queue.discovery.PartitionService; @@ -192,6 +198,7 @@ public class DefaultSolutionService implements SolutionService { private final CalculatedFieldService calculatedFieldService; private final TbCalculatedFieldService tbCalculatedFieldService; private final AttributesService attributesService; + private final TimeseriesService tsService; private final EntityActionService entityActionService; private final SystemSecurityService systemSecurityService; private final TbClusterService tbClusterService; @@ -239,19 +246,35 @@ public class DefaultSolutionService implements SolutionService { } @Override - public void deleteSolution(TenantId tenantId, List createdEntityIds, SecurityUser user) { - if (createdEntityIds == null || createdEntityIds.isEmpty()) { - return; - } - List entityIds = new ArrayList<>(createdEntityIds); - // Delete in the descending order of creation to avoid dependency issues. - Collections.reverse(entityIds); - for (EntityId entityId : entityIds) { - try { - deleteEntity(tenantId, entityId, user); - } catch (RuntimeException e) { - log.error("[{}] Failed to delete the entity: {}", tenantId, entityId, e); + public void deleteSolution(TenantId tenantId, SolutionTemplateInstalledItemDescriptor descriptor, SecurityUser user) throws ThingsboardException { + try { + if (descriptor.getCreatedEntityIds() != null && descriptor.getCreatedEntityIds().isEmpty()) { + List entityIds = new ArrayList<>(descriptor.getCreatedEntityIds()); + // Delete in the descending order of creation to avoid dependency issues. + Collections.reverse(entityIds); + for (EntityId entityId : entityIds) { + try { + deleteEntity(tenantId, entityId, user); + } catch (RuntimeException e) { + log.error("[{}] Failed to delete the entity: {}", tenantId, entityId, e); + } + } } + List tsKeys = descriptor.getTenantTelemetryKeys(); + if (tsKeys != null && !tsKeys.isEmpty()) { + List queries = new ArrayList<>(tsKeys.size()); + for (String tsKey : tsKeys) { + queries.add(new BaseDeleteTsKvQuery(tsKey, 0, System.currentTimeMillis(), false)); + } + tsService.remove(tenantId, tenantId, queries).get(); + } + List attrKeys = descriptor.getTenantAttributeKeys(); + if (tsKeys != null && !tsKeys.isEmpty()) { + attributesService.removeAll(tenantId, tenantId, AttributeScope.SERVER_SCOPE, attrKeys).get(); + } + } catch (Exception e) { + log.error("[{}] Failed to delete the solution", tenantId, e); + throw new ThingsboardException(e, ThingsboardErrorCode.GENERAL); } } @@ -379,7 +402,9 @@ public class DefaultSolutionService implements SolutionService { return new SolutionInstallResponse( new TenantSolutionTemplateInstructions(ctx.getSolutionInstructions()), true, - ctx.getCreatedEntitiesList() + ctx.getCreatedEntitiesList(), + loadTenantTelemetryKeys(ctx.getTempDir()), + loadTenantAttributeKeys(ctx.getTempDir()) ); } catch (Throwable e) { log.error("[{}][{}] Failed to install solution template", tenantId, solutionId, e); @@ -1503,6 +1528,28 @@ public class DefaultSolutionService implements SolutionService { return 0L; } + private List loadTenantTelemetryKeys(Path tempDir) { + Path solutionJson = tempDir.resolve("solution.json"); + if (Files.exists(solutionJson)) { + JsonNode node = JacksonUtil.toJsonNode(solutionJson); + if (node != null && node.has("tenantTelemetryKeys")) { + return JacksonUtil.convertValue(node.get("tenantTelemetryKeys"), new TypeReference<>() {}); + } + } + return Collections.emptyList(); + } + + private List loadTenantAttributeKeys(Path tempDir) { + Path solutionJson = tempDir.resolve("solution.json"); + if (Files.exists(solutionJson)) { + JsonNode node = JacksonUtil.toJsonNode(solutionJson); + if (node != null && node.has("tenantAttributeKeys")) { + return JacksonUtil.convertValue(node.get("tenantAttributeKeys"), new TypeReference<>() {}); + } + } + return Collections.emptyList(); + } + private static void extractZip(byte[] zipData, Path destDir) throws IOException { try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipData))) { ZipEntry entry; diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java index 312376cc7c..58c47d1dc2 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java @@ -16,8 +16,10 @@ package org.thingsboard.server.service.solutions; import jakarta.servlet.http.HttpServletRequest; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.iot_hub.SolutionTemplateInstalledItemDescriptor; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.solutions.data.solution.SolutionInstallResponse; @@ -27,6 +29,6 @@ public interface SolutionService { SolutionInstallResponse installSolution(SecurityUser user, TenantId tenantId, byte[] zipData, HttpServletRequest request) throws Exception; - void deleteSolution(TenantId tenantId, List createdEntityIds, SecurityUser user); + void deleteSolution(TenantId tenantId, SolutionTemplateInstalledItemDescriptor descriptor, SecurityUser user) throws ThingsboardException; } diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java b/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java index e625a6bfce..a122a562a9 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; +import java.util.Collections; import java.util.List; @Schema @@ -29,11 +30,22 @@ public class SolutionInstallResponse extends TenantSolutionTemplateInstructions private boolean success; @Schema(description = "List of entity IDs created during solution installation") private List createdEntityIds; + @Schema(description = "What keys to delete during template uninstall") + private List tenantTelemetryKeys; + @Schema(description = "What attributes to delete during template uninstall") + private List tenantAttributeKeys; public SolutionInstallResponse(TenantSolutionTemplateInstructions instructions, boolean success, List createdEntityIds) { + this(instructions, success, createdEntityIds, Collections.emptyList(), Collections.emptyList()); + } + + public SolutionInstallResponse(TenantSolutionTemplateInstructions instructions, boolean success, List createdEntityIds, + List tenantTelemetryKeys, List tenantAttributeKeys) { super(instructions); this.success = success; this.createdEntityIds = createdEntityIds; + this.tenantTelemetryKeys = tenantTelemetryKeys; + this.tenantAttributeKeys = tenantAttributeKeys; } public SolutionInstallResponse() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java index 51a34576aa..488fe3c410 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java @@ -26,6 +26,8 @@ import java.util.List; public class SolutionTemplateInstalledItemDescriptor implements IotHubInstalledItemDescriptor { private List createdEntityIds; + private List tenantTelemetryKeys; + private List tenantAttributeKeys; private DashboardId dashboardId; private CustomerId publicId; private boolean mainDashboardPublic;