Browse Source

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.
pull/15539/head
Igor Kulikov 2 months ago
parent
commit
e46374ee3c
  1. 11
      application/src/main/java/org/thingsboard/server/service/iot_hub/DefaultIotHubService.java
  2. 73
      application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java
  3. 4
      application/src/main/java/org/thingsboard/server/service/solutions/SolutionService.java
  4. 12
      application/src/main/java/org/thingsboard/server/service/solutions/data/solution/SolutionInstallResponse.java
  5. 2
      common/data/src/main/java/org/thingsboard/server/common/data/iot_hub/SolutionTemplateInstalledItemDescriptor.java

11
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);

73
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<EntityId> createdEntityIds, SecurityUser user) {
if (createdEntityIds == null || createdEntityIds.isEmpty()) {
return;
}
List<EntityId> 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<EntityId> 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<String> tsKeys = descriptor.getTenantTelemetryKeys();
if (tsKeys != null && !tsKeys.isEmpty()) {
List<DeleteTsKvQuery> 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<String> 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<String> 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<String> 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;

4
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<EntityId> createdEntityIds, SecurityUser user);
void deleteSolution(TenantId tenantId, SolutionTemplateInstalledItemDescriptor descriptor, SecurityUser user) throws ThingsboardException;
}

12
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<EntityId> createdEntityIds;
@Schema(description = "What keys to delete during template uninstall")
private List<String> tenantTelemetryKeys;
@Schema(description = "What attributes to delete during template uninstall")
private List<String> tenantAttributeKeys;
public SolutionInstallResponse(TenantSolutionTemplateInstructions instructions, boolean success, List<EntityId> createdEntityIds) {
this(instructions, success, createdEntityIds, Collections.emptyList(), Collections.emptyList());
}
public SolutionInstallResponse(TenantSolutionTemplateInstructions instructions, boolean success, List<EntityId> createdEntityIds,
List<String> tenantTelemetryKeys, List<String> tenantAttributeKeys) {
super(instructions);
this.success = success;
this.createdEntityIds = createdEntityIds;
this.tenantTelemetryKeys = tenantTelemetryKeys;
this.tenantAttributeKeys = tenantAttributeKeys;
}
public SolutionInstallResponse() {

2
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<EntityId> createdEntityIds;
private List<String> tenantTelemetryKeys;
private List<String> tenantAttributeKeys;
private DashboardId dashboardId;
private CustomerId publicId;
private boolean mainDashboardPublic;

Loading…
Cancel
Save