Browse Source

feat(solutions): add solutionId to install context, separate zip extraction error handling

pull/15508/head
Igor Kulikov 6 months ago
parent
commit
34bfd3b4a8
  1. 49
      application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java
  2. 4
      application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java
  3. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java
  4. 21
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DockerComposeParams.java
  5. 8
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  6. 73
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

49
application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java

@ -75,6 +75,8 @@ import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceConnectivityService;
import org.thingsboard.server.dao.device.DockerComposeParams;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
@ -126,8 +128,12 @@ import org.thingsboard.server.service.solutions.data.solution.SolutionInstallRes
import org.thingsboard.server.service.solutions.data.solution.TenantSolutionTemplateInstructions;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@ -191,6 +197,7 @@ public class DefaultSolutionService implements SolutionService {
private final TbServiceInfoProvider serviceInfoProvider;
private final TelemetrySubscriptionService tsSubService;
private final BCryptPasswordEncoder passwordEncoder;
private final DeviceConnectivityService deviceConnectivityService;
private final ExecutorService emulatorExecutor = ThingsBoardExecutors.newWorkStealingPool(10, "solution-emulators-executor");
@ -202,10 +209,17 @@ public class DefaultSolutionService implements SolutionService {
@Override
public SolutionInstallResponse installSolution(SecurityUser user, TenantId tenantId, byte[] zipData, HttpServletRequest request) throws Exception {
Path tempDir = Files.createTempDirectory("iot-hub-solution-");
SolutionInstallContext ctx = new SolutionInstallContext(tenantId, tempDir, user);
try {
extractZip(zipData, tempDir);
} catch (Throwable e) {
log.error("[{}] Failed to extract solution template zip", tenantId, e);
deleteDirectory(tempDir);
TenantSolutionTemplateInstructions instructions = new TenantSolutionTemplateInstructions();
instructions.setDetails(e.getMessage());
return new SolutionInstallResponse(instructions, false, List.of());
}
SolutionInstallContext ctx = new SolutionInstallContext(tenantId, loadSolutionId(tempDir), tempDir, user);
try {
registerEmulatorsAndComputeOldestTelemetryTs(ctx);
provisionTenantDetails(ctx);
@ -370,6 +384,18 @@ public class DefaultSolutionService implements SolutionService {
}
}
private String loadSolutionId(Path tempDir) {
Path solutionJson = tempDir.resolve("solution.json");
if (Files.exists(solutionJson)) {
JsonNode node = JacksonUtil.toJsonNode(solutionJson);
if (node != null && node.has("title")) {
String title = node.get("title").asText("");
return title.trim().toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("^-|-$", "");
}
}
return null;
}
private long loadInstallTimeoutMs(Path tempDir) {
Path solutionJson = tempDir.resolve("solution.json");
if (Files.exists(solutionJson)) {
@ -1048,6 +1074,11 @@ public class DefaultSolutionService implements SolutionService {
devList.append(System.lineSeparator());
template = template.replace("${" + credentialsInfo.getName() + "ACCESS_TOKEN}", credentialsInfo.getCredentials().getCredentialsId());
if (credentialsInfo.isGateway()) {
template = template.replace("${DOCKER_CONFIG}",
prepareDockerComposeFile(ctx.getTenantId(), ctx.getSolutionId(), baseUrl, credentialsInfo.getCredentials().getDeviceId()));
}
}
template = template.replace("${device_list_and_credentials}", devList.toString());
@ -1298,6 +1329,20 @@ public class DefaultSolutionService implements SolutionService {
return "/dashboards/" + dashboardId.getId();
}
private String prepareDockerComposeFile(TenantId tenantId, String solutionId, String baseUrl, DeviceId deviceId) {
Device device = new Device(deviceId);
device.setTenantId(tenantId);
String containerName = "tb-gateway-" + solutionId.replace('_', '-');
DockerComposeParams params = new DockerComposeParams(false, containerName, false, true, false, false);
try (InputStream inputStream = deviceConnectivityService.createGatewayDockerComposeFile(baseUrl, device, params).getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
) {
return reader.lines().collect(Collectors.joining("\n"));
} catch (Exception e) {
throw new RuntimeException("Failed to read or process the docker-compose.yml file.", e);
}
}
private void rollback(TenantId tenantId, SolutionInstallContext ctx, Throwable e) {
List<EntityId> createdEntities = ctx.getCreatedEntitiesList();
Collections.reverse(createdEntities);

4
application/src/main/java/org/thingsboard/server/service/solutions/data/SolutionInstallContext.java

@ -72,13 +72,15 @@ public class SolutionInstallContext {
private final Map<UUID, CreatedEntityInfo> createdEntities = new LinkedHashMap<>();
private final Map<UUID, CreatedAlarmRuleInfo> createdAlarmRules = new LinkedHashMap<>();
private final Map<UUID, CreatedCalculatedFieldInfo> createdCalculatedFields = new LinkedHashMap<>();
private final String solutionId;
private final long installTs;
private long oldestTelemetryTs;
private Map<String, EmulatorDefinition> deviceEmulators;
private Map<String, EmulatorDefinition> assetEmulators;
public SolutionInstallContext(TenantId tenantId, Path tempDir, SecurityUser user) {
public SolutionInstallContext(TenantId tenantId, String solutionId, Path tempDir, SecurityUser user) {
this.tenantId = tenantId;
this.solutionId = solutionId;
this.tempDir = tempDir;
this.user = user;
this.solutionInstructions = new TenantSolutionTemplateInstructions();

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java

@ -30,4 +30,6 @@ public interface DeviceConnectivityService {
Resource getPemCertFile(String protocol);
Resource createGatewayDockerComposeFile(String baseUrl, Device device) throws URISyntaxException;
Resource createGatewayDockerComposeFile(String baseUrl, Device device, DockerComposeParams params) throws URISyntaxException;
}

21
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DockerComposeParams.java

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* 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 org.thingsboard.server.dao.device;
public record DockerComposeParams(boolean includeVersion, String containerName, boolean includePortBindings,
boolean includeExtraHosts, boolean includeVolumesBind,
boolean includeVolumesDeclaration) {
}

8
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -156,11 +156,17 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
@Override
public Resource createGatewayDockerComposeFile(String baseUrl, Device device) throws URISyntaxException {
DockerComposeParams params = new DockerComposeParams(true, "tb-gateway", true, true, true, true);
return createGatewayDockerComposeFile(baseUrl, device, params);
}
@Override
public Resource createGatewayDockerComposeFile(String baseUrl, Device device, DockerComposeParams params) throws URISyntaxException {
String mqttType = isEnabled(MQTTS) ? MQTTS : MQTT;
DeviceConnectivityInfo properties = getConnectivity(mqttType);
DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId());
String host = getHost(baseUrl, properties, mqttType);
return DeviceConnectivityUtil.getGatewayDockerComposeFile(host, gatewayImageVersion, creds);
return DeviceConnectivityUtil.getGatewayDockerComposeFile(host, gatewayImageVersion, creds, params);
}
private DeviceConnectivityInfo getConnectivity(String protocol) {

73
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.device.DeviceConnectivityInfo;
import org.thingsboard.server.dao.device.DockerComposeParams;
import java.net.Inet6Address;
import java.net.InetAddress;
@ -92,28 +93,34 @@ public class DeviceConnectivityUtil {
return command.toString();
}
public static Resource getGatewayDockerComposeFile(String host, String gatewayImageVersion, DeviceCredentials deviceCredentials) {
public static Resource getGatewayDockerComposeFile(String host, String gatewayImageVersion, DeviceCredentials deviceCredentials, DockerComposeParams params) {
StringBuilder dockerComposeBuilder = new StringBuilder();
dockerComposeBuilder.append("version: '3.4'\n");
if (params.includeVersion()) {
dockerComposeBuilder.append("version: '3.4'\n");
}
dockerComposeBuilder.append("services:\n");
dockerComposeBuilder.append(" # ThingsBoard IoT Gateway Service Configuration\n");
dockerComposeBuilder.append(" tb-gateway:\n");
dockerComposeBuilder.append(" image: thingsboard/tb-gateway:").append(gatewayImageVersion).append("\n");
dockerComposeBuilder.append(" container_name: tb-gateway\n");
dockerComposeBuilder.append(" container_name: ").append(params.containerName()).append("\n");
dockerComposeBuilder.append(" restart: always\n");
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append(" # Ports bindings - required by some connectors\n");
dockerComposeBuilder.append(" ports:\n");
dockerComposeBuilder.append(" - \"5000:5000\" # Comment if you don't use REST connector and change if you use another port\n");
dockerComposeBuilder.append(" # Uncomment and modify the following ports based on connector usage:\n");
dockerComposeBuilder.append("# - \"1052:1052\" # BACnet connector\n");
dockerComposeBuilder.append("# - \"5026:5026\" # Modbus TCP connector (Modbus Slave)\n");
dockerComposeBuilder.append("# - \"50000:50000/tcp\" # Socket connector with type TCP\n");
dockerComposeBuilder.append("# - \"50000:50000/udp\" # Socket connector with type UDP\n");
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append(" # Necessary mapping for Linux\n");
dockerComposeBuilder.append(" extra_hosts:\n");
dockerComposeBuilder.append(" - \"host.docker.internal:host-gateway\"\n");
if (params.includePortBindings()) {
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append(" # Ports bindings - required by some connectors\n");
dockerComposeBuilder.append(" ports:\n");
dockerComposeBuilder.append(" - \"5000:5000\" # Comment if you don't use REST connector and change if you use another port\n");
dockerComposeBuilder.append(" # Uncomment and modify the following ports based on connector usage:\n");
dockerComposeBuilder.append("# - \"1052:1052\" # BACnet connector\n");
dockerComposeBuilder.append("# - \"5026:5026\" # Modbus TCP connector (Modbus Slave)\n");
dockerComposeBuilder.append("# - \"50000:50000/tcp\" # Socket connector with type TCP\n");
dockerComposeBuilder.append("# - \"50000:50000/udp\" # Socket connector with type UDP\n");
}
if (params.includeExtraHosts()) {
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append(" # Necessary mapping for Linux\n");
dockerComposeBuilder.append(" extra_hosts:\n");
dockerComposeBuilder.append(" - \"host.docker.internal:host-gateway\"\n");
}
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append(" # Environment variables\n");
dockerComposeBuilder.append(" environment:\n");
@ -139,21 +146,25 @@ public class DeviceConnectivityUtil {
}
break;
}
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append(" # Volumes bind\n");
dockerComposeBuilder.append(" volumes:\n");
dockerComposeBuilder.append(" - tb-gw-config:/thingsboard_gateway/config\n");
dockerComposeBuilder.append(" - tb-gw-logs:/thingsboard_gateway/logs\n");
dockerComposeBuilder.append(" - tb-gw-extensions:/thingsboard_gateway/extensions\n");
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append("# Volumes declaration for configurations, extensions and configuration\n");
dockerComposeBuilder.append("volumes:\n");
dockerComposeBuilder.append(" tb-gw-config:\n");
dockerComposeBuilder.append(" name: tb-gw-config\n");
dockerComposeBuilder.append(" tb-gw-logs:\n");
dockerComposeBuilder.append(" name: tb-gw-logs\n");
dockerComposeBuilder.append(" tb-gw-extensions:\n");
dockerComposeBuilder.append(" name: tb-gw-extensions\n");
if (params.includeVolumesBind()) {
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append(" # Volumes bind\n");
dockerComposeBuilder.append(" volumes:\n");
dockerComposeBuilder.append(" - tb-gw-config:/thingsboard_gateway/config\n");
dockerComposeBuilder.append(" - tb-gw-logs:/thingsboard_gateway/logs\n");
dockerComposeBuilder.append(" - tb-gw-extensions:/thingsboard_gateway/extensions\n");
}
if (params.includeVolumesDeclaration()) {
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append("# Volumes declaration for configurations, extensions and configuration\n");
dockerComposeBuilder.append("volumes:\n");
dockerComposeBuilder.append(" tb-gw-config:\n");
dockerComposeBuilder.append(" name: tb-gw-config\n");
dockerComposeBuilder.append(" tb-gw-logs:\n");
dockerComposeBuilder.append(" name: tb-gw-logs\n");
dockerComposeBuilder.append(" tb-gw-extensions:\n");
dockerComposeBuilder.append(" name: tb-gw-extensions\n");
}
return new ByteArrayResource(dockerComposeBuilder.toString().getBytes(StandardCharsets.UTF_8));
}

Loading…
Cancel
Save